STM32: a fixed-rate loop with a timer interrupt
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
1Clock: 84 MHz
- Open
CH1_UART.ioc→ Clock Configuration: type84in HCLK (MHz), press Enter - 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
- Pinout & Configuration → Timers → TIM4: tick
Internal Clock(this turns the timer on) - Parameter Settings: Prescaler =
83→ 84 MHz / (83 + 1) = 1 MHz, one count every 1 µs - Counter Period =
19999(typed directly, as in the video) → (19999 + 1) µs = 20 ms → 50 Hz - Optional: a named constant instead. User Constants tab → Add: Name
TIMER_PERIOD, Value19999, then typeTIMER_PERIODin Counter Period. Both work - NVIC Settings (the interrupt controller): tick TIM4 global interrupt → save → "Generate code?" → Yes
- 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
- Right-click the project → Build Project (0 errors)
- Debug, then press Resume (F8) and it runs
8Check the rate in the serial plotter
- serial.steppeschool.com → baud
115200→ Connect → pick the STLink port - Two lines,
counterandtime; in the console,timegoes 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:4520in the console timegoes 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)