Long is the way, and hard, that out of Hell leads up to light.
— Paradise lost, John Milton
Embedded system programming is an art. Ever since the commencement of development boards like Arduino and ESP, embedded systems development has become accessible to a wider set of people. And, getting an LED to blink became the hello world of embedded programming. One such code is given below.
const int ledPin = 13;
void setup() {
pinMode(ledPin, OUTPUT);
}
void loop() {
digitalWrite(ledPin, HIGH);
delay(1000);
digitalWrite(ledPin, LOW);
delay(1000);
}
The issue with above snippet is everything it is. The delay(1000) blocks the microcontroller from doing anything at all during that 1000 ms (of course ISRs get executed, but you get my point, right?). The excitement of getting an LED to blink is quite alluring and admittedly one way to get someone interested in embedded programming. But, Let us not do that. Please!
Let us do it the right way. Every microprocessor out there, even the tiniest, cheapest one, has features such as timers and interrupts. That is the way to go. That is the proper way to do it. That is how you realize the true potential of a microcontroller. You give it a task and it is taken care by it in the background. Let us get an LED to blink, the right way.
Let us do some research before writing to code. I have a Nucleo-H723ZG development board with STM32H7 controller. The datasheet of STM32H7[1] provides us with all the information we need. The controller has 24 timers, seventeen 16-bit, four 32-bit timers, two watchdog timers and a systick timer.
ISR is a method/function that is called based on an ISR request. When an ISR request is raised, based on the priority, the controller pauses whatever it is currently doing, executes the corresponding ISR and then resumes it normal process.
A timer is an internal device with a clock as source and it usually counts the positive edge of the clock source. Everytime, a positive edge is detected, the timer is incremented and hence this can aptly be used as a counter. When a 16-bit timer is used as a counter, it overflows when its value is 2¹⁶, unless a predefined value is set as period value. Generally, when a counter overflows, the overflow bit is set, an update event (UEV) is set and an interrupt service routine request is raised which consequently executes an interrupt service routine(ISR). Prescaler of a timer is used to control the counting speed in relation to the clock. For example, if the prescaler is 4, the counter is incremented once, for every 4 clock cycles. Figure 1 provides a visual representation of the same.
Figure 1. Counter timing diagram with prescaler 4 [1]
Almost all the elements of the controller, such as timers, drivers, GPIO, etc.. has control register and data register. Control register is used to define the behavior of the element and the data register holds the data pertinent to it.
Control register of a timer can be used to configure the counter. Let us consider the direction of the counter, for example. Counter can be configured to either count up or down. This can be defined by manipulating the DIR bit of the control register.
Figure 2: Control Register of Timer
It is mentioned in the documentation[1] that the counter counts up from 0 to the period value and counts down from period value to 0 based on the DIR bit being 0 and 1 respectively. Setting the CEN bit to 1 would enable the counter, i.e start the counter.
Prescaler value can be defined by setting the desired value to the TIM prescaler register. STM32H7 takes any value between 0 and 2¹⁶ as prescaler value. This is a control register too.
Figure 3. Prescaler register[1]
The counter value at any instance can be read from the TIMER counter register. Since this holds the data, i.e the present count of the counter, it is a data register. Figure 4 shows the counter register. As you can see, the value of the timer is held in two 16-bit registers as it is a 32 bit timer.
Figure 4. Counter Register of the Timer. [1]
Documentation provides us with the technical details we need. All good! But how do we translate it into usable content? This is the part I love the most while building things. Translating the requirements into engineering terms.
Let us consider that the requirement is to blink the LED at a frequency of 4hz. How can this be achieved? Which feature of the microcontroller can be used? An ISR! A function can be defined to toggle the state of a GPIO pin to which the LED is connected. And that ISR could be attached to the overflow event of the counter. Hence, every time the counter overflows, the state of the LED is toggled.
To achieve, 4hz LED blink, 8 UEV per second is required. Why 8 times a second for 4hz, you ask? If the LED is turned on and off alternatively, everytime an UEV is triggered, then the LED will be turned ON during the 1st UEV, turned OFF during the 2nd UEV, turned ON during the 3rd UEV and so on. Since, it takes two UEV to complete a blink event, 8 UEV a second is required. Figure 5 provides the pictorial representation of this process for a better understanding.
Figure 5: Update event to GPIO state
How to achieve the 8hz UEV using the counter is the next question. We know that an UEV is triggered when a counter overflows or when the counter reaches a predefined value. From the documentation[1], we know that the counter frequency is calculated using equation 1.
Equation 1: counter frequency
Equation 1 can be re-arranged in order to calculate the unknown PSC based on the known source clock frequency (internal clock frequency) and counter frequency.
Now, an UEV is triggered by the counter, when it reaches the count value of 2³⁶, which is quite a big number. counter period register of the counter can be used to set the overflow threshold of the counter. Factoring the counter period into Equation 1, we get, Equation 2.
Equation 2
Equation 2 can be rearranged to calculate the PSC based on the counter frequency, source clock frequency and counter period.
Instead of manipulating the control registers of the timers using pointers, an abstraction with structs and functions for each element of the controller is provided by the development kit of STM.
TIM_HandleTypeDef timer2;
/*
* Initialize timer 2 - TIM2
* Set the counter controls and enable it
*
*/
uint32_t timer_period = 10000;
uint32_t target_frequency = 8;
uint32_t prescaler = (HAL_RCC_GetHCLKFreq()/(timer_period * target_frequency)) - 1;
__HAL_RCC_TIM2_CLK_ENABLE();
timer2.Instance = TIM2;
// set the calculated prescaler value in the prescaler register
timer2.Init.Prescaler = prescaler;
// set the timer_period value in the counter period register
timer2.Init.Period = timer_period;
//set the DIR bit in the control register
timer2.Init.CounterMode = TIM_COUNTERMODE_UP;
// The code until now just created a timer object. No values were set in the register.
// The following function sets the defined values in the corresponding registers
HAL_TIM_Base_Init(&timer2);
Note that the timer_period is assigned the value, 10000, which is an arbitrary value. It can be set to anything between 1 to 2³² which would consequently affect the value of the prescaler.
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
// Set priority for TIM2 interrupt
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
// Enable TIM2 interrupt in NVIC
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
}
void TIM2_IRQHandler(void) {
// Check whether TIM2 update interrupt is pending
if (__HAL_TIM_GET_FLAG(&timer2, TIM_FLAG_UPDATE) != 0) {
// Clear the update interrupt flag
__HAL_TIM_CLEAR_IT(&timer2, TIM_IT_UPDATE);
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // Toggle the LED
}
}
The HAL_TIM_Base_MspInit function enables the TIM2 interrupt in the nested vector interrupt control (NVIC), which is necessary to allow the TIM2 timer to generate interrupt requests.
The function TIM2_IRQHandler is the ISR that gets triggered by the UEV of our timer.
// This function sets the CEN bit of timer2 - TIM2
HAL_TIM_Base_Start_IT(&timer2);
/* Author : Roche Christopher */
#include "main.h"
#include "string.h"
TIM_HandleTypeDef timer2;
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
// enable the GPIOB clock
__HAL_RCC_GPIOB_CLK_ENABLE();
/* Initialize the Green LED control registers - PB1
* Set the control for GPIOB port as output
*/
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = LED_GREEN_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/*
* Initialize timer 2 - TIM2
* Set the counter controls and enable it
*
*/
uint32_t timer_period = 10000;
uint32_t target_frequency = 8;
uint32_t prescaler = (HAL_RCC_GetHCLKFreq()/(timer_period * target_frequency)) - 1;
__HAL_RCC_TIM2_CLK_ENABLE();
timer2.Instance = TIM2;
timer2.Init.Prescaler = prescaler;
timer2.Init.Period = timer_period;
timer2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_Base_Init(&timer2);
HAL_TIM_Base_Start_IT(&timer2);
while (1)
{
// Let the device alone to meditate. Do not disturb it. Please!!!!
}
}
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
// Set priority for TIM2 interrupt
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
// Enable TIM2 interrupt in NVIC
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
}
void TIM2_IRQHandler(void) {
// Check whether TIM2 update interrupt is pending
if (__HAL_TIM_GET_FLAG(&timer2, TIM_FLAG_UPDATE) != 0) {
// Clear the update interrupt flag
__HAL_TIM_CLEAR_IT(&timer2, TIM_IT_UPDATE);
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // Toggle the LED
}
}
Hacking off things is fine, but doing things the way they are supposed to be done gives a satisfaction that hacking will not. Ultimately, it is efficient. It is harder to do it this way than to just call the sleep() function a couple of times inside the infinite while loop. Still, we choose the harder option because it is the right way to do it!
Checkout the code at https://github.com/rocheparadox/simple-integrated-system