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.

后续讨论 5

顺着彼此的依据读下去
  1. 固定链接

    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. 固定链接

    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. 固定链接

    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. 固定链接

    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. 固定链接

    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.

同一个问题的其他角度

  1. 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…

    回复 1最近活动 assemblyc
  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…

    回复 8最近活动 odinc
  3. 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…

    回复 5最近活动 zig