Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C

ARM Tutorial Part 2 Timers

5.00/5 (9 votes)
12 Mar 2023CPOL14 min read 9.7K   79  
A look at the STM32 Timer peripheral
This is the second in the ARM tutorial series in which we look at the STM32 Timer peripheral.
“Half our time is spent trying to find something to do with the time we have rushed through life trying to save.”
Will Rogers

In this, the second in the ARM tutorial series, we take a look at the STM32 Timer peripheral. The STM32 Timer peripheral is very versatile and "provides multiple operating modes to off-load the CPU from repetitive tasks while minimizing interfacing circuitry needs".

Because the Timer peripheral is such a complex subject, I am taking a somewhat different approach in this article as well as giving a detailed description of the Timer peripheral, I have provided a variety of code examples that cover a wide range of functionality. Direct Memory Access (DMA) in association with the Timer peripheral will be saved for another article where I'll tackle DMA in greater detail.

I have tried to use the on-board User Button and LED for most examples, but in some instances this was not possible. The STM32CubeIDE debugger is necessary to view the STM32C031C6 registers; but to view the Timer output other than the LED, an oscilloscope or logic analyzer is needed.

The code was written, programmed and debugged using the STM32CubeIDE which is a IDE from STMicroelectonics that may be downloaded here. You will need to jump through a few hoops to get it but it is a great tool, free with no restrictions on use. There are plenty of tutorials on setting up the STM32CubeIDe environment, so I will not beat a dead horse.

Getting to Know the Microcontroller

I intend on using a different Microcontroller in each of these articles so that one can get a taste for the various devices, their capabilities, functionality and what peripherals they offer. For this article, I chose a NUCLEO-C031C6 with an STM32C031C6 Microcontroller as the main processor. It is a fairly new, inexpensive, entry level processor that is touted as "Your next generation 8-bit MCU is a 32-bit". It may be purchased direct from STMicroelectronics for $12.14.

A short list of features for the NUCLEO-C031C6 include:

  • 32-bit Arm® Cortex®-M0+ core running at 48MHz, on reset configured to run at 12MHz
  • 32 Kbytes of flash memory, and 12 Kbytes of RAM
  • ARDUINO® Uno V3 connectivity support
  • No requirement for any separate probe as it integrates the ST-LINK debugger/programmer
  • STM32C0 MCUs are available in 8 to 48-pin packages
  • 8 16-bit timers; 1 advanced, 4 general purpose, 2 watchdogs and 1 systick

Image 1

Figure 1. NUCLEO-C031C6 Pinout

Because the MCU is so new, there is not much information available other than the manufacturer's literature. If you plan on working with this device, the literature that's available is critical.

STM32031C6 Reference Manual st.com (rm0490)
STM32031C6 Datasheet st.com (ds13867)
NUCLEO-C031C6 User Manual st.com (um2953)

Timer Peripheral Overview

STM32 Microcontrollers are all based on the same architecture with a resolution of 16- or 32-bits. They may be independently configured as an input or output and can each have from 1 to 9 channels. The Timer peripherals are linked internally for timer synchronization or chaining and may also interconnect with other peripherals for monitoring or triggering purposes.

Figure 2 shows the general purpose Timer peripherals that the STM32C031C6 has on board. Although all STM32 Microcontrollers implement a different Timer configuration, they all incorporate the advanced control TIM1 Timer peripheral.

Image 2

Figure 2. Timer comparison chart

Timer 1 Peripheral, a Close Look (Datasheet Pg. 21)

Figure 3 shows the block diagram for the STM32 timer peripheral TIM1 which is made by the assembly of four units:

  • Master/Slave controller unit (blue)
  • The time-base unit (green)
  • The timer channels unit (purple)
  • The break feature unit (yellow)

Image 3

Figure 3. NUCLEO-C031C6 Timer Block (Reference Manual Pg. 299)

In the following sections, delve into each section of the Master/Slave unit in more detail.

Master/Slave Controller Unit

The Master/Slave unit mainly provides control signals for the time-base unit. This includes providing the time-base unit with the counting clock signal, as well as the counting direction control signal. It decides the right counting configuration for the time-base unit based on master/slave configuration.

This unit handles inter-timer synchronization. It can be configured to output a synchronization signal to another peripheral by way of TRG0 as well as to control time-base counter events of external signals.

All STM32 devices incorporate timers, but not all devices feature the same master/slave capabilities. I would refer you to the reference manual of the device you are using to see what timers are implemented and their capabilities.

Time-base Unit

The time-base unit encompasses a counter, prescaler and the repetition counter. The incoming signal is first passed through the prescaler where the frequency may be adjusted before reaching the time-base unit. The incoming signal must be 3 times less than the clock frequency.

The timer counter is controlled by two registers:

TIMx_CNT which is the timer register where the current count is stored.

  • TIMx_ARR contains the reload value for the counter. When counting up, count is set to 0.
  • when TIMx_CNT = ARR and set to ARR when TIMx_CNT = 0 when counting down.

When the counters cycle is restarted, it fires an "update event" which can be found in the TIM_SR register as the Update Interrupt Flag (UIF). This value needs to be reset to continue receiving notifications.

Timer Channels Unit

The Timer Channels are the working elements in which the timer interacts with the external environment, with few exceptions mapped through the MCUs pins that can be configured as either input or output.

Timer Channel Configured as Output

When the timer channels is configured as output, it is used to generate a signal that is the result of comparing the content of the TIMx_CCRy with the timer counter.

Referring to Figure 4 below, it can be seen that the result of the logical comparison is OCyREF which is then fed into the Channel Output Stage which applies conditioning to the OCyREF signal based on some configuration parameters and then output to the devices pins as alternate function. Notice that some channels may output a complementary signal as well.

Image 4

Figure 4. Timer channel configured as input

Timer Channel Configured as Input

Figure 5 below shows the Input stage and we can immediately see that TIMx_CHy can be configured as either input or output as alternate function. When configured as input, it can be used to time stamp the rising, falling or both edges of external signals. The incoming signal passes through a conditioning stage which includes a filter and an edge detection unit. The polarity of the edge detector may be configured using the TIMx-CCER register. The conditioning circuitry outputs two signals; (source: General Purpose Timer Cookbook)

  • FIvFPy: is the TIy timer input signal which was filtered and on which an active edge is detected depending on the polarity of the timer channel "y".
  • TIyFPz: is always the TIy timer input signal which was filtered but on which an active edge is detected depending on the polarity of the timer channel "z”.

Each timer channel may be configured on one of three modes that the prescaler mux connected to the timer channel prescaler. The passing through the prescaler, which scales the incoming signal and emits as ICyPS which triggers the transfer of count into the capture/compare y register.

Image 5

Figure 5. Block diagram when timer configured as input

Break Unit

The Break unit is available only to channels that implement at least one channel with two complementary outputs. The Break unit works on output channels and pretty much does as its name implies, on the first edge after the break is executed, the output channel is either turned off or put in a safe state.

Timer Clock Source

The Timer peripherals may be clocked either by an internal clock or an external clock.

Internal Clock

The internal Timer clock (TIMCLK) is derived from SYSCLK and may be scaled using the AHB Prescaler and the APB Prescaler (Refer to Figure 6). As shown in Figure 6, the APB Prescaler has an anomaly that if the APB Prescaler value is 1 TIMCLK is equal to PCLK times 1 and any other value TIMCLK equals PCLK times 2.

Image 6

Figure 6. NUCLEO-C031C6 Clock Tree (Reference Manual Pg. 108)

External Clock

There are two ways to synchronize (or externally clock) an STM Timer:

  • External Clock Mode 1: External signal is input from TIx inputs
  • External Clock Mode 2: External signal is input from ETR

All incoming external signals must be 3 times less than the internal clock frequency. The timer synchronizes the signal first in all cases except on ETR thus allowing the input signal to be greater than the internal clock frequency. In all cases, the input signal must be three times less than the input frequency.

TIMCLK <= 3 <= Input Signal

Even though the Time is clocked by an external source, the APB clock is still required to provide synchronization. The synchronization is provided by a D flip-flop, as shown in Figure 7 below. The external signal is fed into the first stage D flip-flop input where the synchronized signal is retrieved from the output of the second D flip-flop.

Image 7

Figure 7. Synchronization Block

Timer inputs like TIx and ETR feature a filter stage that may be activated to filter out unwanted signals of a duration that is less than a configured threshold. Both the Input Capture prescaler and the signal filter may be programmed using the Capture/Compare Control Register (CCMR); for the Prescaler ISxPSC and for the filter ICxF. The filtering is also dependent on the CKD field in the CR1 register which controls the ratio between the sampling clock source and the minimal timer duration of a valid pulse. A very good reference for STM32 Timers is the STM32 General Purpose Cookbook.

In Figure 3, the TIMx_ETR signal passes through the synchronization prescaler called ETRP, then it is routed through the resynchronization circuit (which is called the ETRF signal which has the same constraint as above), the frequency output from the prescaler should be three times less than the internal frequency.

Example Code

Code TOC

If you are only interested in the code or a particular implementation, click on the link you are interested in.

It is really hard to understand a new technology without getting your hands dirty, if your keyboard is anything like mine anyway. In this section, I will explain by example how the various timers work by the examples in the following sections. We will start simple with the Basic Count example and with each example get more complex and learn what the timer is capable of.

The Basic Count example is a very simple routine that sets a target frequency of 10Hz, the counter counts up to the configured value and when it reaches that value and the Update Interrupt Flag (UIF) is set. In the while loop, we wait for the UIF flag to be set and when it is we reset it and toggle the User LED. This is not very efficient for two reasons: one it is a blocking routine and two because of the latency between the polling and the setting of the flag.

C++
void BasicCount()
{
    // Configure PA5 as output
    ConfigureUserLED();

    // TIM3 clock enable
    RCC->APBENR1 |= (1 << 1);

    // Set Prescaler and Auto Reload Register to slow the
    //	processor down so we can see the LED blink.
    // Freq = SYSCLK / ((PSC - 1) * (ARR - 1))
    // Freq = 12MHz / ((12000 - 1) * (100 - 1))
    // Freq = ~10Hz
    TIM3->PSC = 12000 - 1;
    TIM3->ARR = 100 - 1;

    // Start the timer
    TIM3->CR1 |= 1;

    while(1)
    {
        // Wait for the overflow event to be fired
        while(!(TIM3->SR & TIM_SR_UIF)){}

        // Reset the Update Interrupt Flag
        TIM3->SR &= ~TIM_SR_UIF;

        // Toggle the LED
        GPIOA->ODR ^= (1 << 5);
    }
}
Listing 1. Basic Counter

Compare With Output

There's a little more work involved configuring this routine but the polling has been taken out of the picture and the output directly controls the User LED. In order to do this, we need to configure the User LED, on PA5 to its alternate function, the code for ConfigureAFUserLED is provided in the download. Additionally, we need to configure the time to output the 1Hz signal to that pin.

In Listing 2 line 17, the timer is configured to toggle the output instead of manually toggling it in the while loop as was done in the BasicCounter example. Then the CCER register is set to enable the timer to output on pin TIM1_CH1 which we have configured to be PA5 and finally the Master Output Enable is set to begin the process.

C++
void BasicCountWithOutput()
{
    // Configure PA5 as alternate function TIM1 CH1 output
    ConfigureAFUserLED();

    // TIM1 clock enable
    RCC->APBENR2 |= (1 << 11);

    // Set Prescaler and Auto Reload Register to slow the
    // processor down so we can see the LED blink.
    // Freq = 12MHz / ((1200 - 1) * (10000 - 1)) = 1Hz
    TIM1->PSC = 1200 - 1;
    TIM1->ARR = 10000 - 1;

    // OC1M Toggle output
    TIM1->CCMR1 |= (3 << 4);

    // CC1E Enable output
    TIM1->CCER |= 1;
    // Master Output Enable
    TIM1->BDTR |= (1 << 15);

    // Start the timer
    TIM1->CR1 |= 1;

    while(1)
    {

    }
}
Listing 2. Compare output

Basic Mode 1 Pulse Width Modulation (PWM)

PWM is a signal that is generated by the channel where the frequency is determined by the value in the TIMx_ARR register and the duty cycle from the TIMx_CCRx register.

There are two PWN modes that may be configured using the TIMx_CCMR1 register OCxM field.

  • PWM Mode 1: When upcounting the channel stays active as log as TIMx_CNT is less than TIMx_CCRx else inactive and when down counting active long as TIMx_CNT is greater than TIMx_CCRx else active.
  • PWN Mode 2: When upcounting the channel stays inactive as log as TIMx_CNT is less than TIMx_CCRx else active and when down counting inactive long as TIMx_CNT is greater than TIMx_CCRx else active.
C++
void BasicPWMMode1()
{
    // Configure PA5 as alternate function TIM1 CH1 output
    ConfigureAFUserLED();

    // Enable TIM1 clock
    RCC->APBENR2 |= (1 << 11);

    // Set Prescaler and Auto Reload Register to slow the
    //	processor down so we can see the LED blink.
    // Freq = 12MHz / ((12000 - 1) * (1000 - 1)) = 1Hz
    TIM1->PSC = 12000 - 1;
    TIM1->ARR = 1000 - 1;

    // Capture/Compare Register set the Duty Cycle
    // Duty Cycle = ARR / CCR1 = 999 / 500 = .50 = 50%
    TIM1->CCR1 = 500;
    // PWM Mode 1
    TIM1->CCMR1 |= (6 << 4) | (1 << 3);
    // CC1E Enable output
    TIM1->CCER |= 1;
    // Master Output Enable
    TIM1->BDTR |= (1 << 15);

    // Enable/Start Timer
    TIM1->CR1 |= TIM_CR1_CEN;
}
Listing 3. Basic Mode 1 PWM

The demo code links timers 1 and 3 together to make a 32-bit times that toggles the User LED every 4 seconds. Time 1 is configured as the Master with a frequency of 1Hz and Timer 3 as the slave that counts to 4.

C++
void LinkTimer1And3()
{
    // GPIOB clock enable
    RCC->IOPENR = 2;

    // Set PB8 alternate function
    GPIOB->MODER &= ~(3 << 16);
    GPIOB->MODER |= (2 << 16);
    // Alternate function to TIM3 CH1
    GPIOB->AFR[1] &= ~0x0f;
    GPIOB->AFR[1] |= 3;

    // Enable TIM1 and TIM3 clocks
    RCC->APBENR2 |= (1 << 11);
    RCC->APBENR1 |= (1 << 1);

    // Master Mode Selection - Select Update Event as Trigger output (TRG0)
    TIM1->CR2 |= (2 << 4);

    // Set Prescaler and Auto Reload Register
    // Freq = 12MHz / ((12000 - 1) * (1000 - 1)) = 1Hz
    TIM1->PSC = 12000 - 1;
    TIM1->ARR = 1000 - 1;

    // Slave Mode Control Register - Configure in slave mode using ITR1 as
    //	internal trigger. External Clock Mode 1 - Rising edges of the selected
    //	trigger (TRGI) clock the counter.
    TIM3->SMCR |= 7;

    TIM3->PSC = 0;
    TIM3->ARR = 4 - 1;

    // OC1M Toggle output
    TIM3->CCMR1 |= (3 << 4);
    // CC1E Enable output
    TIM3->CCER |= 1;
    // Master Output Enable
    TIM3->BDTR |= (1 << 15);

    TIM1->CR1 |= 1;
    TIM3->CR1 |= 1;

    while(1);
}
Listing 4. Linking Timer1 to Timer 3

One Shot Mode

One shot pulse or one shot, as I like to call it, allows the counter to be started in response to some stimulus and output a pulse of programmable length and delay.

Delay = value in CCRx register

  • Pulse With = TIMx_ARR - TIMx_CCRx

The code is set up that the pulse is triggered by pressing the User Button and the output may be viewed on PA8. Using the demo code requires an Oscilloscope or logic analyzer to view the results.

In the demo code, the Delay and Pulse Width calculations are:

        MCU frequency = 12MHz with prescale of 100 = 1Mhz 
Delay = 1MHz / 250 = 480 =? 1/480 = 2.08mS
Pulse Width = 1Mhz / (500 - 250) = 2.08mS

Since the Repetition Control Register (RCR) is set to 0, the pulse will only occur one time.

C++
void OneShotMode()
{
    // Enable GPIOA and GPIOC clocks
    RCC->IOPENR = 5;
    // Enable Time1 and Timer3 clocks
    RCC->APBENR2 |= (1 << 11);

    // Using Timer14 as a basic counter to provide
    //	debounce for the User Button.
    ConfigDelay();

    // Configure PA8 as alt func TIM1 CH1
    GPIOA->MODER &= ~(3 << 16);
    GPIOA->MODER |= (2 << 16);
    GPIOA->AFR[1] |= 2;

    // Configure PC13 as input
    GPIOC->MODER &= ~(3 << 26);

    /******* Waveform setup ********
        * Delay defined by CCR1
        * Pulse defined by difference of ARR - CCR2
        * Repeat 1 time
        * Delay = 2.12mS
        * Pulse Width = 2.12mS
        *******************************/
    TIM1->PSC = 100;
    TIM1->ARR = 500;
    TIM1->CCR1 = 250;
    TIM1->RCR = 0;

    // PWM Mode 2
    TIM1->CCMR1 |= (7 << 4);
    // Select One-pulse mode
    TIM1->CR1   |= TIM_CR1_OPM;
    // Capture/Compare 1 Enable
    TIM1->CCER |= 1;
    // Master Output enable
    TIM1->BDTR |= TIM_BDTR_MOE;

    while(1)
    {
        if (!(GPIOC->IDR & (1 << 13)))
        {
            // Trigger the Timer by enabling, once complete the
            //	UIF flag is set and the Timer is automatically 
            //	reset/disabled.
            TIM1->CR1 |= TIM_CR1_CEN;

            // Wait for the Update flag to be set
			while(!(TIM1->SR & TIM_SR_UIF)){}

            // The UIF flag needs to be reset so we can repeat the process.
            TIM1->SR &= ~1;
            
            // Button debounce using Timer14
            Delay(100);
        }
    }
}
Listing 5. One Shot Mode triggered by User Button

Input Capture External Source

The Input Capture demo code configures PA8 as an alternate function and then in SMCR register maps it to the ITR2 input. To view the results of the capture, install a jumper from User Button (PC13 CN7 Pin 23) to the User LED (PA5 CN10 Pin 24). When the User Button is pressed, the User LED will toggle. To view the time result from the CCR1 register, you will need to run the program in debug mode and put a break point at line 35.

When the User Button is pressed, a capture is detected on TI1 and the CC1IF flag is set and the contents of CCR1 contain the time stamp value. If the CC1IF flag is already set when the capture is detected, the over-capture flag (CCxOF) will be set. The CC1IF flag requires that the application reset the flag.

C++
void InputCaptureExternalSource()
{
    uint16_t result = 0;

    // TIM1 clock enable
    RCC->APBENR2 |= (1 << 11);

    // Configure PA8 as alt func TIM1 CH1
    RCC->IOPENR = 1;
    GPIOA->MODER &= ~(3 << 16);
    GPIOA->MODER |= (2 << 16);
    GPIOA->AFR[1] &= ~(0x0f);
    GPIOA->AFR[1] |= 2;

    // Configure PA5 as output
    GPIOA->MODER &= ~(3 << 10);
    GPIOA->MODER |= (1 << 10);

    // Channel 1 (PA8) configured as input, mapped to ITR2
    TIM1->CCMR1 |= 1;
    // Trigger selection TI1FP1
    TIM1->SMCR |= (5 << 4) + (4 << 0);
    // CC1E Capture Enable
    TIM1->CCER |= 1;

    TIM1->CR1 |= 1;

    while (1)
    {
        while(!(TIM1->SR & TIM_SR_CC1IF)){}

        result = TIM1->CCR1;
        GPIOA->ODR ^= (1 << 5);
        // Reset the Update Interrupt Flag
        TIM1->SR &= ~TIM_SR_CC1IF;
    }
}
Listing 6. Input Capture demo code

Input Capture Internal Source

Time 1 is configured to output a signal every 250mS through TRG0 and since Timer 1 and Timer are linked though ITR2 Timer 3 is set up to receive the event on ITR2. When this occurs, I toggle the User LED and reset the CC1IF flag.

C++
void InputCaptureInternalSource()
{
    // TIM1 clock enable
    RCC->APBENR2 |= (1 << 11);
    // Enable TIM3 clock
    RCC->APBENR1 |= RCC_APBENR1_TIM3EN;

    // Configure PA5 as output
    RCC->IOPENR = 1;
    GPIOA->MODER &= ~(3 << 10);
    GPIOA->MODER |= (1 << 10);

    // Every 250mS
    TIM3->PSC = 3000;
    TIM3->ARR = 1000;

    // The update event is selected as trigger output (TRGO).
    TIM3->CR2 |= (2 << 4);

    // Configured as input, IC1 is mapped on TI1
    TIM1->CCMR1 |= 3;
    // Trigger selection ITR2 Trigger mode
    // And Slave Mode selection Reset Mode
    TIM1->SMCR |= (2 << 4) | (4 << 0);
    // CC1E Capture Enable
    TIM1->CCER |= 1;

    TIM1->CR1 |= TIM_CR1_CEN;
    TIM3->CR1 |= TIM_CR1_CEN;

    while (1)
    {
        while(!(TIM1->SR & TIM_SR_CC1IF)){}

        GPIOA->ODR ^= (1 << 5);
        // Reset the Update Interrupt Flag
        TIM1->SR &= ~TIM_SR_CC1IF;
    }
}
Listing 7. Input capture via Internal Source

Blink via Interrupt

This is a very simple interrupt routine that blinks the User LED on each interrupt which occurs every 1S.

The routine basically sets up the PA5 pin as output, sets the interrupt enable flag and then sets the NVIC registers.

I have not gone much into interrupts of DMA in this article because I intend to address both subjects in subsequent articles.

C++
void BlinkyWithInterrupt()
{
    ConfigureUserLED();

    // TIM1 clock enable
    RCC->APBENR2 |= (1 << 11);

    // Set Prescaller and Auto Reload for 1 sec
    TIM1->PSC = 12000;
    TIM1->ARR = 1000;

    // Reinitialize the counter and generates an update of the registers
    TIM1->EGR |= TIM_EGR_UG;

    // Update interrupt enable
    TIM1->DIER |= (1 << 0);

    // Set Interrupt Priority
    NVIC_SetPriority(TIM1_BRK_UP_TRG_COM_IRQn, 0);
    
    // Enable Interrupt
    NVIC_EnableIRQ(TIM1_BRK_UP_TRG_COM_IRQn);
    
    // Start Timer
    TIM1->CR1 |= (1 << 0);

    while(1);
}
Listing 8. Blink via Interrupt

Below is the interrupt handler for the demo code above:

C++
void TIM1_BRK_UP_TRG_COM_IRQHandler()
{
    // Toggle USer LED
    GPIOA->ODR ^= (1 << 5);
    // Reset UIF flag
    TIM1->SR &= ~(1<<0);
}
Listing 9. Interrupt handler

Conclusion

There's so much more I wanted to add to this article but it got so long that I left it to the code to fill in some of the blanks. I really enjoyed writing this article but the research took a long time as I started pretty much from scratch. I had worked with Arduino timers but they don't have near the capabilities of the STM32 Timers. I hope that you will get as much out of this as I have.

References

  • Understanding STM32 Naming Conventions, 5/20/2020, Maker.io Staff digikey.com (AN4013)
  • Application Node, STM32 cross-series timer overview (AN4776) Application Node, General-purpose timer cookbook for STM32 microcontrollers

History

  • 12th March, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)