Our Christmas tree is almost ready.
In our previous tutorial we saw how to blink all LEDs at the same time.
The lights are blinking but Santa thinks it's too static.
Let's make them rotate.
The previous tutorial for this series:
The Github for the current tutorial with full files and code:
IDE:
Board:
In the previous tutorial we used a software delay delay_ms() to blink all 8 LEDs together.
It worked well but it was wasteful: The CPU was stuck in a loop doing nothing useful.
In this tutorial we replace it with a hardware timer (TIM2) configured to generate an interrupt every 500ms.
The CPU is free to do whatever it wants between interrupts or in our case, sleep with the wfi instruction (Wait For Interrupt).
The result: One LED at a time, rotating around the ring, every 500ms.
TIM2 is one of the general purpose timers on the STM32F303.
It has a counter that increments at a configurable frequency.
When the counter reaches the ARR (Auto-Reload Register) value, it resets to 0 and fires an update event (which can trigger an interrupt).
Two registers control the timing:
PSC (Prescaler) divides the input clock:
TIM tick frequency = TIM clock / (PSC + 1)
We need it because it tells TIM2 to not count every clock tick but to divide the input clock by 8000 in order to increment the counter once every 8000 clock cycles.
ARR (Auto-Reload Register) defines when the counter resets:
Period = (PSC + 1) * (ARR + 1) / TIM clock
The -1 in both formulas is because the registers count from 0.
Writing 7999 to PSC means the timer actually counts 8000 ticks before incrementing, not only 7999 times.
In our case, with an 8 MHz CPU clock:
500ms = (7999 + 1) * (499 + 1) / 8 000 000
= 8000 * 500 / 8 000 000
= 0.5s ✓
The NVIC is the ARM Cortex-M hardware block that manages interrupts.
To receive a TIM2 interrupt, two things must be enabled:
IRQ stands for Interrupt ReQuest: A peripheral (the timer TIM2) triggers a request with number 28.
Each interrupt has a fixed IRQ number defined by ST.
TIM2 is IRQ number 28 on the STM32F303.
The handler name must match exactly what's in the startup file's vector table.
Indeed, the linker connects them by name:
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_UIF) {
TIM2->SR &= ~TIM_SR_UIF; // Clear the flag — mandatory
led_set(led_pos);
led_pos = (led_pos + 1) % 8;
}
}
Clearing the flag is mandatory.
If you don't clear TIM_SR_UIF, the interrupt fires again immediately after returning.
The result will be an infinite loop at interrupt level: The CPU never returns to main().
int main(void)
{
leds_init();
tim2_init();
while (1) {
__asm__("wfi"); // Wait For Interrupt — saves power
}
}
The wfi instruction puts the CPU into a low-power sleep state until an interrupt wakes it up.
Since everything is handled in the interrupt handler, there is nothing to do in the main loop.
Go to the firmware/stm32f3discovery folder then type:
$ cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake -DCMAKE_BUILD_TYPE=Debug -G Ninja
$ ninja -C build
$ openocd -f interface/stlink.cfg \
-f target/stm32f3x.cfg \
-c "program build/stm32f3discovery.bin verify reset exit 0x08000000"
You should now see a single LED rotating around the ring every 500ms.
The key difference from the previous tutorial: the CPU is no longer blocked in a delay loop.
It sleeps between interrupts and wakes up only when TIM2 fires.
This is the foundation of efficient embedded programming: Let the hardware do the timing, let the CPU do the work.
Santa approves 
Add new comment