Lab 4 Extra - Low Level Timers Configuration

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:

  1. Setup a periodic timer to toggle a pin connected to an LED

  2. Setup a timer peripheral to generate a PWM signal on the LED pin using the PWM Module

  3. Use the STM32Cube HAL to generate a PWM signal

Example 1: LED Toggle via a Periodic Timer

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.

GPIO Configuration

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

  1. Enable Timer 2 peripheral by setting TIM2EN bit in the RCC APB1ENR register. Connecting the peripheral to a beat.

  2. 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.

  3. We disable the timer count by clearing the CEN bit in CR1 register.

  4. We reinitialize the counter and update timer registers by setting the UG bit in the EGR register.

  5. 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 TcountT_{count} 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 TperiodT_{period} which is the duration the timer needs to count from 0 to the autoreload value (in count-up mode).

Tperiod=#counts×TcountT_{period}=\#counts \times T_{count}

If we choose an autoreload value of 1000-1 (minus one since zero is a count), that means the timer period would be Tperiod=1000×1/(20×103)T_{period}=1000 \times 1/(20\times10^3) or finterrupt=20×103/1000=20Hzf_{interrupt} = 20\times10^3 / 1000 = 20Hz

  1. We tell the timer to generate an interrupt when it reaches the autoreload value, by setting the UIE bit in the DIER register.

  2. 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.

Example 1 Complete

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 */
    }
}