The puzzle
A timer interrupt is enabled and the handler runs, and runs, and runs, and the main loop never gets another instruction. A second product loses a UART error flag every few hours. A third counts two events for every one on the scope. All three handlers clear their flags; all three clear them the wrong way. How hard can writing one bit be?
STEP 1
Why the flag must be cleared
A peripheral usually holds its interrupt line asserted for as long as its (enabled) event flag is set. Returning from the handler without clearing that flag leaves the request active, so the core takes it again at once: the handler runs in a loop and nothing else of equal or lower priority ever runs. Clearing the source is the handler’s first duty, and on a shared line it must clear every source it handled (lesson 2).
STEP 2
Three kinds of clearing
Status registers use special write rules so that one flag can be cleared without touching the others:
| kind | clear a flag by | example |
|---|---|---|
| write 1 to clear (W1C) | writing 1 to its bit; 0 bits change nothing | RP2040 timer INTR (“WC”) |
| write 0 to clear (rc_w0) | writing 0 to its bit; 1 bits change nothing | STM32 timer and UART status registers |
| clear on read | reading the status (and sometimes a data) register | the STM32 UART parity-error flag: read SR, then DR |
The right code is therefore a plain write of a mask with only the target bit active: timer_hw->intr = 1u << alarm; on the RP2040, TIMx->SR = ~TIM_SR_UIF; on the STM32, which is exactly what ST’s HAL macros do.
STEP 3
The read-modify-write trap
Habits from ordinary registers break here. reg |= bit reads the register, ORs in the bit and writes the result back:
- on a W1C register the read value contains every pending flag as a 1, so writing it back clears them all;
- on a write-0-to-clear register,
reg &= ~bitworks in the quiet case, but a flag raised between the read and the write is 0 in the written value, and is cleared without ever being seen.
STEP 4
The late clear
A store to a peripheral register is not instantaneous. It may wait in a write buffer and cross a slower peripheral bus. If the handler clears its flag as its very last instruction, the exception return can begin while the flag is still set; the core sees the interrupt still pending and enters the handler again, which finds nothing to do. It happens whenever
where t_return is the moment the exception return begins, when the core re-evaluates pending interrupts: whenever the write lands after that point.
A store to a peripheral register is not instantaneous: it may sit in a write buffer and cross a slower peripheral bus. If the handler clears its flag as its last instruction, the exception return can begin while the flag is still set; the core sees the interrupt still pending and enters the handler again for nothing. Clear the flag early, or read the register back before returning (a DSB barrier alone may not be enough behind a buffered peripheral bus bridge). Cycle counts are illustrative.
Two habits avoid it: clear the flag near the start of the handler, right after identifying the source; and if it must be cleared late, read the register back before returning, so the write has reached the peripheral. A __DSB() barrier completes the core’s own accesses, but on chips with a buffered bridge to a slower peripheral bus it may not be enough on its own.
STEP 5
Worked example: the handler order
A timer handler on a W1C register, written in the order that avoids all three problems:
void alarm0_handler(void) { /* irq_set_exclusive_handler(TIMER_IRQ_0, alarm0_handler) */
timer_hw->intr = 1u << 0; /* 1. clear only our flag, with a plain write, first */
uint32_t now = timer_hw->timerawl;
schedule_next(now); /* 2. do the (short) work */
}
Clearing first also means that if the event happens again while the handler is still running, its flag is set anew and the handler will run once more for it, instead of the late clear erasing it.
MYTHS AND FACTS
Common misconceptions
The NVIC clears the interrupt when the handler runs
It clears its own pending bit; the peripheral’s flag stays set until software clears it.
reg |= bit is the safe way to set or clear one bit
Not on status registers with special write rules.
Clearing at the end of the handler is tidy
It risks a spurious re-entry and can erase an event that arrived during the handler.
Reading a status register is side-effect free
Some flags clear on read, which a debugger’s register view can trigger too (unit 6, lesson 3).
Check yourself
Answer in your head, then open the card.
A W1C register reads 0b0101 and the handler wants to clear bit 0. What should it write, and what does INTR |= 1 do?
Write 0b0001 (a plain write). INTR |= 1 reads 0b0101 and writes 0b0101 back, clearing both bit 0 and bit 2; bit 2's event is lost.
On an STM32 timer, why does ST's HAL write SR = ~TIM_FLAG_UPDATE instead of SR &= ~TIM_FLAG_UPDATE?
SR is write-0-to-clear. The plain write has 0 only at the update bit and 1 elsewhere (no effect), so no other flag can be cleared by accident, even one set during the operation; the read-modify-write could clear a flag raised between its read and its write.
A handler never returns to the main loop. Its interrupt is a UART receive interrupt. What is the likely cause?
The receive flag is not being cleared: for example the data register is never read, so the flag (cleared by reading the data) stays set and the request stays asserted.
Where should a flag be cleared in a handler, and why?
Early, right after identifying the source: it cannot then cause a spurious re-entry through a late write, and a new event during the handler sets it again and is not lost.
Sources (3)
- Raspberry Pi Ltd, pico-sdk 1.5.1, hardware_regs/timer.h and hardware_timer/timer.c — TIMER_INTR “Raw Interrupts”, fields ALARM_0–3 with ACCESS "WC" (write 1 to clear); INTE enable, INTF force, INTS masked status; timer.c clears with timer_hw->intr = 1u << alarm_num (“clear any IRQ”)
- STMicroelectronics, stm32f4xx-hal-driver, Inc/stm32f4xx_hal_tim.h and stm32f4xx_hal_uart.h — __HAL_TIM_CLEAR_IT and __HAL_TIM_CLEAR_FLAG: (__HANDLE__)->Instance->SR = ~(__FLAG__); __HAL_UART_CLEAR_FLAG likewise; __HAL_UART_CLEAR_PEFLAG clears by reading SR and then DR
- Arm, CMSIS 6, CMSIS/Core/Include/cmsis_gcc.h — __DSB() (data synchronization barrier), which waits until preceding memory accesses have completed