UNIT 09 · LESSON 4 OF 6

Acknowledging and Clearing Interrupt Flags

How hard can writing one bit be?

INTERACTIVEClearing one flag without clearing the others
Interrupt flags before and after a clearing write, with lost flags markedflags pending0111value written0111flags after0000reg |= 1u << 0 on a write-1-to-clear registerFlags lost: bit 1, bit 2. Their events will never be handled.
Interrupt flags before and after a clearing write, with lost flags markedflags pending0111value written0111flags after0000reg |= 1u << 0 on a write-1-to-clear registerFlags lost: bit 1, bit 2. Their events will never behandled.

Try this

Register
Code
Flag to clear
reg |= 1u << 0: after = 0000, lost 0110.

Interrupt status registers use special write rules. On a write-1-to-clear register (the RP2040 timer’s INTR, marked “WC” in the SDK headers) a 1 clears that flag and a 0 leaves it; on an rc_w0 register (STM32 status registers, which the HAL clears with SR = ~flag) a 0 clears and a 1 leaves it. A read-modify-write can clear flags you never meant to touch, including one that arrived between the read and the write.

What you will be able to do
  • Explain why an uncleared flag makes a handler run again immediately.
  • Clear one flag correctly on a write-1-to-clear register, a write-0-to-clear register and a clear-on-read register.
  • Identify a read-modify-write on a status register that loses other flags, including ones raised mid-operation.
  • Explain why clearing a flag as the last instruction of a handler can cause a spurious second entry, and avoid it.
  • Order the steps of a handler: identify, clear, handle.
Before you start
  • Interrupt states and shared lines (lesson 2).
  • Registers with side effects and read-modify-write races (unit 2, lessons 5 and 6).
Steps in this lesson
  1. Why the flag must be cleared
  2. Three kinds of clearing
  3. The read-modify-write trap
  4. The late clear
  5. Worked example: the handler order
  6. Common misconceptions

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:

kindclear a flag byexample
write 1 to clear (W1C)writing 1 to its bit; 0 bits change nothingRP2040 timer INTR (“WC”)
write 0 to clear (rc_w0)writing 0 to its bit; 1 bits change nothingSTM32 timer and UART status registers
clear on readreading the status (and sometimes a data) registerthe 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 &= ~bit works 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.

↑ This step uses the figure at the top of the page.

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

tclear+twrite delay>treturnt_{\text{clear}} + t_{\text{write delay}} > t_{\text{return}}

where t_return is the moment the exception return begins, when the core re-evaluates pending interrupts: whenever the write lands after that point.

INTERACTIVEA flag cleared too late fires twice
A handler timeline showing when the flag clear takes effect relative to the exception returnhandlerexitspurious re-entryflagreturn begins: pending re-checkedThe clear lands at cycle 58, after the core re-checked at 40: the handler runs asecond time with nothing to do.
A handler timeline showing when the flag clear takes effect relative to the exception returnhandlerexitre-entryflagreturn begins: pending re-checkedThe clear lands at cycle 58, after the corere-checked at 40: the handler runs a second timewith nothing to do.
Flag cleared
The clear lands at cycle 58, after the core re-checked at 40: the handler runs a second time with nothing to do.

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)
  1. 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”)
  2. 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
  3. Arm, CMSIS 6, CMSIS/Core/Include/cmsis_gcc.h — __DSB() (data synchronization barrier), which waits until preceding memory accesses have completed