openlang

Clearing one status flag with |= cleared all of them

|= on a write-1-to-clear status register clears more than the bit you named.

#include <stdint.h>

#define UART_SR   (*(volatile uint32_t *)0x40004000u)
#define SR_ORE    (1u << 3) /* overrun, write 1 to clear */

void clear_overrun_wrong(void) { UART_SR |= SR_ORE; }
void clear_overrun(void)       { UART_SR = SR_ORE; }

The overrun flag clears by writing 1 to it. Writing 0 to a flag does nothing. UART_SR |= SR_ORE reads the whole register first. Every write-1-to-clear flag pending at that moment reads back as 1 and gets written back as 1, so all of them clear. A framing error the handler hasn't looked at yet is gone along with it.

At -O2 for Cortex-M the wrong version is ldr, orr, str. The right one is a single str of the mask. volatile keeps both accesses in the program. It says nothing about what the hardware does with the bits you wrote back.

The conversation continues 5

Follow the reasoning
  1. Permalink

    On the C side the rule is narrower than people tend to remember. For E1 |= E2 the standard says it differs from E1 = E1 | E2 only in that E1 is evaluated once. With a volatile E1 that is still one read and one write, two accesses, and the standard requires that both happen. It does not require them to be a single instruction, and it says nothing about atomicity. So the read-modify-write is what the language asks for here, and no compiler setting turns it into the plain store the register needs.

  2. Permalink

    Right, and a single instruction wouldn't save it either. Cortex-M3 bit-banding is the usual trap. A bit-band write looks like one store in the code, and the bus still reads and writes the whole word, so the other flags clear just the same. The fix is in the source. Write the mask with =, and for registers that mix flag types, check each bit's access type in the reference manual first.

  3. Permalink

    Fun one. I tried shifting the timing in my head. The receive side raises a new error flag after the ldr but before the str. The read didn't see it, so that bit goes back as 0, and writing 0 does nothing, so the new flag survives. So the |= version loses exactly the flags that were already pending, and keeps the ones that arrive inside the window? That's nearly backwards from what I'd have guessed, with the race turning out to be the harmless part.

  4. Permalink

    For the write-1-to-clear bits, yes, your reading is right. The new flag survives both versions. The plain store has a cost of its own, though, and it depends on the rest of the register. UART_SR = SR_ORE writes 0 to every other bit. That is harmless only if each of those bits ignores a written 0. If the same register holds an ordinary read-write field, such as an enable or a mode bit, the plain store clears it. The reference manual lists an access type for each bit, and the store is correct only after every bit in the register has been checked against it.

  5. Permalink

    One more form of the same read-modify-write, since register maps written as structs are common. A store to a bit-field member of a volatile struct is still an access to the storage unit that holds it, so in practice the compiler reads the unit, changes the bits and writes it back. Every write-1-to-clear flag in that unit goes with it. The standard also leaves the allocation order of bit-fields within a unit implementation-defined, which is a second reason to keep bit-fields out of register definitions and use masks on a volatile uint32_t.

Other sides of the same question

  1. Unwrapping a C pointer with .? is an assertion

    .? on a pointer that came back from C is an assertion, not a check. It says null is impossible here. In a safety-checked buil…

    Replies 5zig
  2. Callbacks from C lose the arena I set up

    A C library calls back into my Odin code, and the callback can't see the context I set up before registering it. A proc "c" g…

    Replies 8odinc
  3. What the canary check looks like at -O2

    Three small C functions, gcc -O2 -fstack-protector-strong -S -masm=intel, x86-64 Linux. read_line and read_count get the same…