Series / Working with STM32 Peripherals / Using Timers on STM32

Using Timers on STM32

Use an STM32 hardware timer for periodic work by connecting clock assumptions, prescalers, auto-reload values, update events, and a simple LED timing example.

On this page

The previous article explained STM32 clocks at the level needed for real projects. This article uses those clock assumptions to configure the first timing peripheral: a hardware timer.

HAL_Delay() is useful for simple examples, but it blocks the firmware while waiting. Hardware timers are the foundation for periodic work, timeouts, refresh loops, button sampling, display multiplexing, and many other embedded tasks.

This article uses a timer update event to toggle the user LED periodically.

What This Article Covers

This article covers:

  • The practical timer concepts needed for periodic work.
  • How timer input clocks, prescalers, counters, and auto-reload values fit together.
  • How to calculate a simple update rate.
  • How to start a timer interrupt with STM32 HAL.
  • How to toggle the user LED from the timer callback.
  • How to debug common timer setup failures.

It does not cover PWM, input capture, output compare, encoder mode, DMA, or low-power timer behavior. STM32 timers are feature-rich enough that trying to cover all modes at once makes the first useful timer example harder to understand.

Timer Mental Model

For periodic work, a timer is a counter driven by a clock.

The important pieces are:

  • Timer input clock: the clock feeding the timer peripheral.
  • Prescaler: divides the input clock down to a slower counter tick.
  • Counter: increments on each timer tick.
  • Auto-reload value: the value where the timer period ends.
  • Update event: the event generated when the timer rolls over.
  • Interrupt: optional CPU notification when the update event occurs.

The timer does not know that you want to blink an LED. It only counts. Firmware decides what to do when the timer reaches the configured period.

Choose a Timer

STM32 parts include several timer peripherals. For an early project, use a general-purpose timer available on your target.

The examples below use TIM2 because it is a common general-purpose timer name in STM32 examples. Treat it as a concrete example, not a universal requirement. If your CubeMX project or board setup uses a different timer, adjust the instance, handle name, interrupt handler, and clock assumptions accordingly.

Before coding, verify:

  • The timer instance exists on your MCU.
  • The timer is enabled in CubeMX or initialized in your code.
  • The timer clock source is understood.
  • The matching HAL timer source file is compiled.
  • The interrupt handler is present if using interrupts.

Calculate the Update Rate

The basic equations are:

timer_tick_hz = timer_input_clock_hz / (prescaler + 1)
update_hz = timer_tick_hz / (period + 1)

The + 1 matters because STM32 timer prescaler and auto-reload registers are zero-based. A prescaler value of 0 divides by 1. A period value of 999 counts 1000 ticks.

For example, if the timer input clock is 80 MHz and you want a 1 kHz timer tick:

prescaler = 80000 - 1

Then, if you want one update event per second:

period = 1000 - 1

That produces:

80,000,000 / 80,000 = 1,000 Hz
1,000 / 1,000 = 1 Hz

If your timer input clock is not 80 MHz, those numbers are wrong. Use the clock values from the previous article and the STM32 reference manual's timer clock rules for the APB bus that owns your timer.

Minimal HAL Timer Example

This example assumes the user LED setup from the GPIO article still exists.

The timer handle is usually global or file-scope because HAL functions and interrupt paths need access to it:

#include "stm32l4xx_hal.h"

#define USER_LED_GPIO_PORT GPIOA
#define USER_LED_PIN GPIO_PIN_5

TIM_HandleTypeDef htim2;

The LED initialization is the same idea as before:

static void user_led_init(void)
{
    __HAL_RCC_GPIOA_CLK_ENABLE();

    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = USER_LED_PIN;
    gpio.Mode = GPIO_MODE_OUTPUT_PP;
    gpio.Pull = GPIO_NOPULL;
    gpio.Speed = GPIO_SPEED_FREQ_LOW;

    HAL_GPIO_Init(USER_LED_GPIO_PORT, &gpio);
}

Then initialize the timer:

static void timer2_init(void)
{
    __HAL_RCC_TIM2_CLK_ENABLE();

    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 80000 - 1;
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 1000 - 1;
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;

    if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
    {
        while (1)
        {
        }
    }

    HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(TIM2_IRQn);
}

The interrupt handler calls into HAL:

void TIM2_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim2);
}

HAL then calls the period-elapsed callback when the update event occurs:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        HAL_GPIO_TogglePin(USER_LED_GPIO_PORT, USER_LED_PIN);
    }
}

Finally, start the timer interrupt in main:

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    user_led_init();
    timer2_init();

    if (HAL_TIM_Base_Start_IT(&htim2) != HAL_OK)
    {
        while (1)
        {
        }
    }

    while (1)
    {
    }
}

The main loop is empty because the timer interrupt performs the periodic LED toggle.

Explain the Important Lines

The timer clock enable turns on the peripheral block:

__HAL_RCC_TIM2_CLK_ENABLE();

Without this, timer register access and initialization will not behave correctly.

The prescaler and period define the update rate:

htim2.Init.Prescaler = 80000 - 1;
htim2.Init.Period = 1000 - 1;

These values only make sense with the assumed timer input clock. If the clock differs, recalculate them.

The NVIC setup allows the timer interrupt to reach the CPU:

HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);

The IRQ handler connects the MCU interrupt vector to HAL's timer interrupt processing:

void TIM2_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim2);
}

The callback keeps application behavior separate from low-level interrupt flag handling:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)

Always check the timer instance inside shared callbacks. Other timers may use the same HAL callback function later.

CubeMX and Generated Timer Code

If CubeMX generated the timer setup, you may already have:

  • MX_TIM2_Init() or a similar function.
  • A global TIM_HandleTypeDef htim2.
  • TIM2_IRQHandler() in stm32l4xx_it.c.
  • Timer clock setup in stm32l4xx_hal_msp.c.

Do not duplicate those pieces blindly. Either use the generated setup or write the setup yourself for the article, but keep one owner for each function and interrupt handler.

If generated code owns MX_TIM2_Init(), the main flow may look like:

HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM2_Init();
HAL_TIM_Base_Start_IT(&htim2);

The concepts are the same. The location of the initialization code changes.

CMake Support

If timer HAL code was not already part of the project, make sure the HAL timer source is compiled.

For STM32L4 HAL, that usually means adding a source like:

generated/Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_tim.c

Depending on the exact HAL configuration, related extension files may also be required.

If using CubeMX-generated interrupt handlers, make sure the file containing TIM2_IRQHandler() is compiled too, commonly:

generated/Core/Src/stm32l4xx_it.c

Missing HAL timer sources usually produce linker errors. Missing interrupt handlers may build successfully but prevent the callback from firing.

Build and Test

Build the firmware:

cmake --build build

Flash it:

openocd -f interface/stlink.cfg -f target/stm32l4x.cfg -c "program build/firmware.elf verify reset exit"

If the timer is configured for a 1 Hz update, the LED should toggle once per second. That means the visible blink cycle is two seconds: one second on and one second off.

Debug the Timer

If the LED does not toggle, start with the debugger.

Useful checks:

  • Inspect the timer input clock assumption from the previous article.
  • Break in timer2_init() or MX_TIM2_Init().
  • Confirm HAL_TIM_Base_Init() returns HAL_OK.
  • Confirm HAL_TIM_Base_Start_IT() returns HAL_OK.
  • Set a breakpoint in TIM2_IRQHandler().
  • Set a breakpoint in HAL_TIM_PeriodElapsedCallback().
  • Confirm htim->Instance == TIM2 when the callback runs.

If the IRQ handler fires but the LED does not change, suspect LED pin configuration or LED polarity.

If the timer starts but the IRQ handler never fires, suspect NVIC setup, interrupt handler naming, timer update interrupt enablement, or vector table/startup integration.

If the callback fires at the wrong rate, suspect the timer clock assumption, APB prescaler behavior, prescaler value, or period value.

Common Mistakes

The first common mistake is calculating prescaler and period from the wrong clock.

The second is forgetting the zero-based register behavior and missing the - 1 in prescaler or period values.

The third is enabling the wrong timer clock or not enabling the timer clock at all.

The fourth is starting the timer without interrupts and expecting the callback to run. Use HAL_TIM_Base_Start_IT, not only HAL_TIM_Base_Start, when using interrupt callbacks.

The fifth is forgetting NVIC configuration. The timer can generate an update event without the CPU receiving the interrupt.

The sixth is missing stm32l4xx_hal_tim.c from the CMake source list.

The seventh is defining two versions of the same IRQ handler when mixing generated and hand-written code.

The eighth is doing too much work inside the timer callback. Keep interrupt callbacks short. Toggle a pin, set a flag, or update a small counter. Do not block with long delays.

What This Proves

This timer example proves that the firmware can use a hardware peripheral to schedule periodic work without blocking in HAL_Delay().

It also connects several project pieces:

  • Clock configuration.
  • Timer prescaler and period math.
  • HAL timer initialization.
  • NVIC interrupt delivery.
  • Startup/vector table integration.
  • GPIO output feedback.

That is a major step from simple GPIO polling toward real embedded firmware structure.

What This Does Not Prove

This does not prove every STM32 timer mode.

It does not cover PWM generation, input capture, encoder interfaces, one-pulse mode, DMA-triggered transfers, or low-power timers. It also does not prove precise timing against an external measurement.

For precise timing, use a scope, logic analyzer, or timer output pin measurement. The LED is only a human-visible sanity check.

Next Steps

The next article adds UART logging on STM32.

Timers give firmware a way to schedule work. UART logging gives firmware a way to report what it is doing without relying only on LEDs and debugger breakpoints.