In this lesson we will review how timers are configured through direct register access and how the STM32Cube HAL can be used to configure timers. Specifically, you will learn how to:
Setup a periodic timer to toggle a pin connected to an LED
Setup a timer peripheral to generate a PWM signal on the LED pin using the PWM Module
Use the STM32Cube HAL to generate a PWM signal
In this example, a timer will be configured to generate an interrupt at a fixed frequency. The interrupt will have an interrupt handler (a callback function) which will toggle the pin connected to the LED. Let's see how this can be done without using any libraries.
The LED is connected to PA5, we will have to configure the pin as an output and enable the GPIO port.
/* Enable GPIOA Clock */
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
/* Set Port A Pin 5 as Output */
GPIOA->MODER |= GPIO_MODER_MODER5_0;
For this example we can use any timer peripheral, as we are not constrained by which channel of the timer connects to which pin. We only require that a timer generates an internal event at a specific rate. We will use Timer 2.
So next, we do the following steps
Enable Timer 2 peripheral by setting TIM2EN
bit in the RCC APB1ENR
register. Connecting the peripheral to a beat.
Based on the speed of the APB1
bus which Timer 2 is connected on, we choose a prescaler value to bring the timer clock frequency to 20kHz. This is the rate at which the timer will count. Note that the APB1 bus clock runs at 16MHz by default. This can be increased to 42MHz if PLL is enabled on the System Clock configuration. But we will leave it as is here. To reduce the timer clock from 16MHz to 20kHz we need to divide 16Mhz by (16E3 / 20E3) The prescaler value, which is set in the PSC
register would then be (16E3 / 20E3) -1. The minus one is required to offset the value to zero in case no division is done.
We disable the timer count by clearing the CEN
bit in CR1
register.
We reinitialize the counter and update timer registers by setting the UG
bit in the EGR
register.
Now we just assign the Autoreload value. This autoreaload value and the timer frequency, will determine the period of the timer and thus the periodic interrupt frequency. Remember the relationship between the count period which is the reciprocal of the timer count frequency (20kHz in our case), the number of counts which is the autoreload value and the timer period which is the duration the timer needs to count from 0 to the autoreload value (in count-up mode).
If we choose an autoreload value of 1000-1 (minus one since zero is a count), that means the timer period would be or
We tell the timer to generate an interrupt when it reaches the autoreload value, by setting the UIE
bit in the DIER
register.
Now we enable the timer counter by setting the CEN
bit in the CR1
register.
/* Enable Timer 2 peripheral */
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
/* Default APB1 speed is 16MHz. Scale Timer 2 to 20kHz */
TIM2->PSC = (uint32_t)(16E6 / 20E3) - 1;
/* Disable Timer*/
TIM2->CR1 = 0;
/* Reinitialize counter and update registers */
TIM2->EGR = TIM_EGR_UG;
/* 1000 Counts at 20kHz => 20Hz IRQ */
TIM2->ARR = 1000 - 1;
/* Enable hardware interrupts */
TIM2->DIER |= TIM_DIER_UIE;
/* Start Timer */
TIM2->CR1 |= TIM_CR1_CEN;
Next we have to tell the CPU to enable the response to interrupts generated by Timer 2, we can also set the priority of those interrupts.
NVIC_SetPriority(TIM2_IRQn, 2);
NVIC_EnableIRQ(TIM2_IRQn);
We have completed the configuration of the timer. The remaining step would be to define the function that would be called when Timer 2 issues an interrupt.
In the startup file startup_stm32f401xe.S
you can see in line 171 a name of a function that matches Timer 2.
.word TIM2_IRQHandler /* TIM2 */
TIM2_IRQHandler
is the default name given to the function associated with any interrupt generated by Timer 2 (from which there could be more than one, for more than one event type). This function name and other interrupt handler names are listed in what's called a vector table. This table preserves a sequenced list of addresses for all available interrupt handlers on the cpu. This table is what the CPU would use to locate the address to go to in response to an interrupt.
These interrupt handler functions are declared weak
by default, which allows you to define them, and when you define them you override the weak declaration. You are probably used to having your code call the function you declare, but remember this is an interrupt handler, you only need to define the handler, and the CPU will take care of calling it when an associated interrupt event occurs.
In the declaration of the function, we check the UIF
bit in the SR
register to verify the type of event that occurred, which indicates an update event (count to autoreload completed). In other words, if that bit is set then the timer has indeed raised a flag when it completed the count.
We might not need to do this verification since we don't expect other interrupt flags to be raised by the timer. Meaning any call to TIM2_IRQHandler() would be in response to a timer period complete event.
When inside the interrupt handler, we have to clear the update flag bit in the SR
register. This allows for the interrupt flag to be raised again, otherwise the interrupt handler will not be called on subsequent update events.
void TIM2_IRQHandler(){
/* Check what type of event occurred */
if (TIM2->SR & TIM_SR_UIF)
{
/* Clear the interrupt even flag. CPU will only respond to new flags thereafter */
TIM2->SR &= ~(TIM_SR_UIF);
GPIOA->ODR ^= GPIO_ODR_OD5;
}
}
Unlike the callback functions when using the Arduino framework, which the interrupt handlers are similar to. The interrupt handler must have the exam same function name as declared in the startup file vector table. If you need to change the name, you would have to change the name in the startup file as well. The Arduino API actually uses these interrupt handlers within its stack.
The complete example is shown below. It should work stand-alone on PlatformIO with the stm32cube
framework.
/* Example 1:
* A bare-metal example for configuring Timer to issue a periodic interrupt.
* The timer is configured to generate an interrupt at 20Hz.
* The LED is toggled at each IRQ call. Effectively producing a 10Hz square wave signal.
*/
#include "stm32f4xx.h"
/* The name of the IRQ function should match the one in the vector table in the startup file */
void TIM2_IRQHandler(){
/* Check what type of event occurred */
if (TIM2->SR & TIM_SR_UIF)
{
/* Clear the interrupt even flag. CPU will only respond to new flags thereafter */
TIM2->SR &= ~(TIM_SR_UIF);
GPIOA->ODR ^= GPIO_ODR_OD5;
}
}
int main(){
/* Enable GPIOA Clock */
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
/* Set Port A Pin 5 as Output */
GPIOA->MODER |= GPIO_MODER_MODER5_0;
/* Enable Timer 2 peripheral */
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
/* Default APB1 speed is 16MHz. Scale Timer 2 to 20kHz */
TIM2->PSC = (uint32_t)(16E6 / 20E3) - 1;
/* Disable Timer*/
TIM2->CR1 = 0;
/* Reinitialize all registers */
TIM2->EGR = TIM_EGR_UG;
/* 1000 Counts at 20kHz => 20Hz IRQ */
TIM2->ARR = 1000 - 1;
/* Enable hardware interrupts */
TIM2->DIER |= TIM_DIER_UIE;
/* Start Timer */
TIM2->CR1 |= TIM_CR1_CEN;
/* These are CMSIS calls to enable interrupts */
NVIC_SetPriority(TIM2_IRQn, 2);
NVIC_EnableIRQ(TIM2_IRQn);
while (1){
/* Do nothing here */
}
}