Attitude Estimation· Live
Home / Chapter 1 / 1.3

STM32: a fixed-rate loop with a timer interrupt

Lesson 1.3 · STM32 · free

Video coming soon

HAL_Delay is never exact, so we let a hardware timer keep the time: TIM4 counts at 1 MHz (84 MHz, prescaler 83) up to 19999, so it fires every 20 ms, fifty times per second. Its interrupt sets a flag, tick, and the main loop does its work once per tick.

In this lesson · 8 steps
  1. Clock: 84 MHz
  2. TIM4: 1 MHz, period 20 ms, interrupt on
  3. A flag set by the timer
  4. Timer callback
  5. Start the timer
  6. Run once per tick
  7. Build and run
  8. Check the rate in the serial plotter

1Clock: 84 MHz

  1. Open CH1_UART.ioc → Clock Configuration: type 84 in HCLK (MHz), press Enter
  2. Check: APB1 timer clocks = 84 MHz (APB1 is the bus that feeds TIM4)
Why?
The timer clock is what the timer counts. On the NUCLEO-F401RE it is 84 MHz. On another board it could be different, so check it here before you set the prescaler.

2TIM4: 1 MHz, period 20 ms, interrupt on

  1. Pinout & Configuration → Timers → TIM4: tick Internal Clock (this turns the timer on)
  2. Parameter Settings: Prescaler = 83 → 84 MHz / (83 + 1) = 1 MHz, one count every 1 µs
  3. Counter Period = 19999 (typed directly, as in the video) → (19999 + 1) µs = 20 ms → 50 Hz
  4. Optional: a named constant instead. User Constants tab → Add: Name TIMER_PERIOD, Value 19999, then type TIMER_PERIOD in Counter Period. Both work
  5. NVIC Settings (the interrupt controller): tick TIM4 global interrupt → save → "Generate code?" → Yes
  6. Later, for 200 Hz: set the period to 4999 (5 ms) and generate again
Why?
Prescaler 83 divides 84 MHz by 84, so the timer counts once every microsecond. It counts from 0 to 19999, that's 20000 counts: 20 ms, so fifty times per second.

3A flag set by the timer

main.c · USER CODE BEGIN PV

tick is our flag: the interrupt sets it, the loop reads it. volatile tells the compiler to always read tick from memory, never an old copy.

/* USER CODE BEGIN PV */
volatile int tick = 0;
/* USER CODE END PV */
Why?
Without it, the loop might never see the change.

4Timer callback

main.c · USER CODE BEGIN 0 (below _write)

HAL runs this function by itself every time TIM4 reaches its period, every 20 ms.

  return len;
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM4)
  {
    tick = 1;
  }
}
/* USER CODE END 0 */
Why?
We never call it ourselves. Inside, we check it's timer four, and set tick to 1. That's all it does. Add it after the _write function from 1.2.

5Start the timer

main.c · USER CODE BEGIN 2 (after the hello line)

This line starts TIM4 with its interrupt.

  (after the hello line and int counter = 0 from 1.2)
  HAL_TIM_Base_Start_IT(&htim4);
  /* USER CODE END 2 */
Why?
Don't forget it, otherwise the callback never runs. Add only this line: the hello line and the counter are already there.

6Run once per tick

main.c · inside while (1), USER CODE BEGIN 3 (replace the 1.2 loop code)

No HAL_Delay any more: the loop waits for the tick.

    /* USER CODE BEGIN 3 */
    if (tick == 1)
    {
      tick = 0;
      counter++;
      printf("counter:%d,time:%lu\r\n", counter, HAL_GetTick());
    }
  }
  /* USER CODE END 3 */
Why?
When tick is set, we reset it right away, so the block runs once per tick. HAL_GetTick returns the time in milliseconds, so we can check the rate.

7Build and run

  1. Right-click the project → Build Project (0 errors)
  2. Debug, then press Resume (F8) and it runs

8Check the rate in the serial plotter

  1. serial.steppeschool.com → baud 115200 → Connect → pick the STLink port
  2. Two lines, counter and time; in the console, time goes up by 20 on every line → 50 lines per second
Why?
Two values in one line, with a comma, give two curves. time grows twenty times faster than counter, so the counter looks almost flat. Look at the console: time goes up by exactly 20 on each line.
Check & troubleshoot

What you should see

  • Lines like counter:226,time:4520 in the console
  • time goes up by 20 on every line: fifty lines per second
  • The plot shows two lines: counter and time. The counter looks almost flat next to time, because time grows twenty times faster

If it doesn't work

  • Counter does not change: HAL_TIM_Base_Start_IT(&htim4) is missing in USER CODE BEGIN 2
  • Callback never runs: tick "TIM4 global interrupt" in NVIC Settings and generate the code again
  • Wrong rate: check the APB1 timer clock is 84 MHz, the prescaler is 83 and the period is 19999
  • Code gone after Generate Code: keep all your lines inside USER CODE sections

Good to know

  • The timer divides its clock by the prescaler plus one: 84 MHz / (83 + 1) = 1 MHz, one count every microsecond. Your board may run at 72 or 160 MHz: then pick the prescaler so the timer still counts at 1 MHz
  • The counter starts at zero, so 19999 gives 20000 counts: 20 ms, or 50 Hz. For 200 Hz, use 4999 (5 ms)

Next: Lesson 1.4: the same serial output on the ESP32, with the Arduino IDE.