Maker.io main logo

Getting Started with STM32 - Timers and Timer Interrupts

16,269

2020-08-17 | By ShawnHymel

License: Attribution

Timers are one of the most important features in modern microcontrollers. They allow us to measure how long something takes to execute, create non-blocking code, precisely control pin timing, and even run operating systems.

The STM32 line of microcontrollers from STMicroelectronics are no exception: each controller offers a full suite of timers for us to use. In this guide, I’ll show you how to configure a timer using STM32CubeIDE, use it to measure execution time, and set up non-blocking code. Additionally, we’ll cover the basics of interrupts and how to use them to flash an LED.

If you are not familiar with STM32CubeIDE, please see here to get familiar with it.

Please see here if you would like to see this information presented in video format:

 

 

Timer Basics

A timer (sometimes referred to as a counter) is a special piece of hardware inside many microcontrollers. Their function is simple: they count (up or down, depending on the configuration--we'll assume up for now). For example, an 8-bit timer will count from 0 to 255. Most timers will “roll over” once they reach their max value. So, our 8-bit timer would start over again from 0 once it reaches 255.

You can apply a variety of settings to most timers to change the way they function. These settings are usually applied via other special function registers inside the microcontroller. For example, instead of counting to a maximum of 255, you might tell the timer that you want it to roll over at 100 instead. Additionally, you can often connect other hardware or peripherals inside the microcontroller to the timer, like toggling a specific pin automatically when the timer rolls over.

Here are some of the common hardware functions you’ll see with timers:

  • Output compare (OC): toggle a pin when a timer reaches a certain value

  • Input capture (IC): measure the number of counts of a timer between events on a pin

  • Pulse width modulation (PWM): toggle a pin when a timer reaches a certain value and on rollover. By adjusting the on versus off time (duty cycle), you can effectively control the amount of electrical power going to another device.

These hardware-connected timer functions will be a topic for another time. For now, I’d like to focus on using a timer to measure time between events (i.e. measure execution time) as well as triggering basic interrupts.

Prescaler

How fast do timers run? Well, that depends on how fast you tell them to run. All timers require a clock of some sort. Most will be connected to the microcontroller’s main CPU clock (others, like real time clocks, have their own clock sources). A timer will tick (increment by one) each time it receives a clock pulse.

In this demo, we will be using an STM32 Nucleo-L476RG, which has a default main clock (HCLK) of 80 MHz. We could have a timer tick at 80 MHz, but that might be too fast for many of our applications. A 16-bit timer can count to 65,535 before rolling over, which means we can measure events no longer than about 819 microseconds!

If we wish to measure longer events, we need to use a prescaler, which is a piece of hardware that divides the clock source. For example, a prescaler of 80 would turn an 80 MHz clock into a 1 MHz clock.

Microcontroller timer prescaler

Now, our timer would tick once every 1 microsecond and, assuming a 16-bit timer, be able to time events up to a maximum of about 65.5 milliseconds.

Choosing a Timer

If you look at the datasheet for your microcontroller, you will often find a section talking about the various timers available. For example, here is a table from section 3.24 of our STM32L476 datasheet giving us information about the different general purpose timers at our disposal.

STM32 timer table

Timer 16 offers some basic functionality, so we’ll use that. Note that it is a 16-bit timer and can only count up.

If you look in section 3.11 of that same datasheet, you will find a “clock tree,” which is a diagram showing you how the clocks are connected to various prescalers and peripherals inside the STM32 chip. I’ve highlighted in yellow the clock path that we would need to be concerned with to configure the speed of Timer 16.

STM32 clock tree

As you can see, there is more than one prescaler that we need to worry about in STM32. Our 80 MHz HCLK signal is first divided by the APB2 prescaler and then multiplied by either 1x or 2x (this is automatically set in hardware depending on what you choose for the APB2 prescaler). This signal is used to clock timers 1, 8, 15, 16, and 17. Each timer also has its own separate prescaler that we can set.

So, if we choose an APB2 prescaler of 8, which sets the multiplier to 2x, and a timer prescaler of 4, our timer would tick at a rate of 5 MHz (80 MHz / 8 * 2 / 4 = 5 MHz).

Note that you can get the value of a timer at any given moment by reading its CNT register. You can read more about the registers needed to control the timers for the STM32476RG in this reference manual. For example, section 28.6.20 talks about Timer 16.

Required Hardware

For the following examples, you only need a Nucleo-L476RG and a USB mini cable. We will just be timing events and toggling the onboard LED.

Example 1: Measure Execution Time

Open STM32CubeIDE, start a new project, select your board (Nucleo-L476RG), and give your project a good name.

In the CubeMX perspective, open Timers and select TIM16. Set the Prescaler (PSC) to 79. I’ll write “80 - 1” to show that a prescaler value of 79 actually means use a clock divider or 80. The prescaler is off by 1 because it’s 0-based: a PSC value of “0” means to use a prescaler (clock divider) of 1.

Set the Counter Period to 65535. Once again, I’ll write “65536 - 1” to show that this counter will actually tick 65536 times in total (the rollover from 65535 to 0 counts as a tick).

STM32 configure timer

Save to generate code and open main.c. There, include <stdio.h> so we can use sprintf and change main() so that we measure the time it takes to execute the HAL_Delay(50) function. Here is what the code should look like (note that I left out everything after main(), as that should have been generated by CubeMX).

Copy Code
/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
TIM_HandleTypeDef htim16;

UART_HandleTypeDef huart2;

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART2_UART_Init(void);
static void MX_TIM16_Init(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  char uart_buf[50];
  int uart_buf_len;
  uint16_t timer_val;

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART2_UART_Init();
  MX_TIM16_Init();
  /* USER CODE BEGIN 2 */

  // Say something
  uart_buf_len = sprintf(uart_buf, "Timer Test\r\n");
  HAL_UART_Transmit(&huart2, (uint8_t *)uart_buf, uart_buf_len, 100);

  // Start timer
  HAL_TIM_Base_Start(&htim16);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    // Get current time (microseconds)
    timer_val = __HAL_TIM_GET_COUNTER(&htim16);

    // Wait for 50 ms
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
    HAL_Delay(50);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);

    // Get time elapsed
    timer_val = __HAL_TIM_GET_COUNTER(&htim16) - timer_val;

    // Show elapsed time
    uart_buf_len = sprintf(uart_buf, "%u us\r\n", timer_val);
    HAL_UART_Transmit(&huart2, (uint8_t *)uart_buf, uart_buf_len, 100);

    // Wait again so we don't flood the Serial terminal
    HAL_Delay(1000);

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/* REST OF MAIN.C */

In our code, note that we start the timer with HAL_TIM_Base_Start(&htim16). From there, we can use __HAL_TIM_GET_COUNTER(&htim16) to get the value of the counter (from the CNT register) at that moment. We simply subtract timestamps to get the amount of time elapsed. Note that the method presented allows us to measure time between events even if the timer rolls over. However, it will not work if the difference between the timestamps is longer than 65,536 microseconds (the maximum value our timer can count to).

Build the project and start debugging. Press the play button and connect to your Nucleo board with a serial terminal program, such as PuTTY. You should see that it takes about 51000 microseconds to execute HAL_Delay(50) (interestingly, it seems that HAL_Delay() adds a millisecond).

STM32 measure execution time

Example 2: Non-blocking LED Blink

In addition to measuring execution time, we can recreate the blinky example using timers. By doing so, we can create non-blocking code to toggle the LED. This allows you to run other code while waiting for the LED to toggle.

Back in the CubeMX perspective, change the Timer 16 prescaler to 7999 (“8000 - 1”). With a main CPU clock of 80 MHz, our timer will now tick at a rate of 10 kHz (80 MHz / 8000 = 10 kHz).

STM32 configure timer prescaler

Change main.c to the following:

Copy Code
/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
TIM_HandleTypeDef htim16;

UART_HandleTypeDef huart2;

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART2_UART_Init(void);
static void MX_TIM16_Init(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  uint16_t timer_val;

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART2_UART_Init();
  MX_TIM16_Init();
  /* USER CODE BEGIN 2 */

  // Start timer
  HAL_TIM_Base_Start(&htim16);

  // Get current time (microseconds)
  timer_val = __HAL_TIM_GET_COUNTER(&htim16);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    // If enough time has passed (1 second), toggle LED and get new timestamp
    if (__HAL_TIM_GET_COUNTER(&htim16) - timer_val >= 10000)
    {
      HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
      timer_val = __HAL_TIM_GET_COUNTER(&htim16);
    }

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Note that you can get rid of the #include <stdio.h>, as we are not printing to the serial terminal anymore.

After starting the timer, we get a timestamp with __HAL_TIM_GET_COUNTER(&htim16). We subtract subsequent timestamps and compare them to a value. In this case, our value is 10,000. With a timer running at 10 kHz, it will take 1 second for the timer to count from one timestamp to 10,000 + that timestamp.

Once again, this method is capable of handling timer rollovers, but it cannot handle timing events longer than about 6.55 seconds (65,536 ticks / 10,000 kHz = 6.5536 sec).

Run the code, and you should see the LED start to toggle once per second.

STM32 blinky

Example 3: Timer Interrupts

Timers can be used to trigger a variety of interrupts (see section 72.2.9 of the HAL/LL API reference document for a list of possible HAL-supported interrupt callbacks). We will use a very basic interrupt: when the timer reaches its maximum value, it will rollover back to 0 and trigger an interrupt.

To accomplish that, we will set the Timer 16 prescaler to be 8000 so that it ticks at a rate of 10 kHz. We will then set its maximum value to 10,000 (actually, we’ll set it to 9,999 as we want the interrupt to trigger on the rollover) so that it takes 1 second to reach the maximum value. 

STM32 timer interrupt

Each time the interrupt occurs, we’ll toggle the LED. This accomplishes the same effect as before (blinky), but using interrupts instead. It makes our code even more non-blocking!

In CubeMX, change the counter period to 9,999 (“10000 - 1”).

STM32 timer settings

Click on the NVIC Settings tab and enable the TIM1 update interrupt and TIM16 global interrupt setting. Note that this is required to get interrupts to fire!

STM32 enable timer interrupts

In main.c change the main() function to the following (the includes and variable declarations above it should be the same as the previous example):

Copy Code
/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART2_UART_Init();
  MX_TIM16_Init();
  /* USER CODE BEGIN 2 */

  // Start timer
  HAL_TIM_Base_Start_IT(&htim16);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Note that the only thing we have in there now is HAL_TIM_Base_Start_IT(&htim16), which starts the timer in interrupt mode. The while superloop is blank! Our blinky code will be entirely handled by interrupts.

Scroll down to find the /* USER CODE BEGIN 4 */ guards. In there, add the following:

Copy Code
/* USER CODE BEGIN 4 */

// Callback: timer has rolled over
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  // Check which version of the timer triggered this callback and toggle LED
  if (htim == &htim16 )
  {
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
  }
}

/* USER CODE END 4 */

This is an interrupt handler. The HAL libraries will manage the main interrupt service routine (ISR) when the timer interrupt occurs (feel free to examine it in stm32l4xx_it.c). At some point in that ISR, the code will call HAL_TIM_PeriodElapsedCallback(), which we need to provide a definition for. In that definition, we check to make sure that the timer handle (htim) is indeed our Timer 16 and then toggle the LED pin. Note that this is a generic timer interrupt callback. If you set up multiple timer interrupts, this one callback will be called for any of them, which is why we check the timer instance handle (htim) to differentiate among the possible timers.

Run this code in debug mode, and you should see the LED flash again, just like before!

Going Further

I hope this has helped you get started using timers and timer interrupts. Timers are very powerful features in microcontrollers, as they allow you to perform a variety of tasks, including running non-blocking code. Interrupts can be used in conjunction with timers to perform functions outside of your main code.

See the following documents if you would like to dig into STM32 timers and interrupts.

Recommended Reading

Mfr Part # NUCLEO-L476RG
NUCLEO-64 STM32L476RG EVAL BRD
STMicroelectronics
$122.25
View More Details
Mfr Part # AK672M/2-2
CBL USB2.0 A PLG-MIN B PLG 6.56'
Assmann WSW Components
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.