This is the sixth post of my Bit fields series; describing how to not use bit fields, how to use them, the limitations imposed by architecture and the compiler’s implementation, the use of volatile, and finally this show-stopper as well as a proposal to fix it.
Manual bit manipulation
The posts in this series about bit fields haven’t much discussed the actual details that the compiler needs to perform to implement the code. They do describe how, when bit fields aren’t used, the bit mask, bit shift, bit insertion and bit extraction operations need to be written explicitly by the programmer every time. This suffers the potential problem of the programmer mis-coding what is required; but it has the advantage that a knowledgeable programmer can command only the exact operations necessary, potentially omitting unnecessary instructions.
Compiler bit manipulation with bit fields
When bit fields are used, the compiler writes the bit manipulation code and usually doesn’t have enough information to use efficient shortcuts. That means that for all but a few cases, accesses need to be performed by using a read-modify-write sequence, to change only the targeted field that needs to be changed as a result of the code. However, the compiler is smart enough to know tricks like an all-1s field value can be immediately ORed in, without having to zero the old field’s value first.
The real world isn’t that tidy…
Of course, this makes perfect sense—unless that’s not what is required.
- Some bit fields in hardware registers are read-only; writing to them has no effect.
- Some bit fields are write-only; reading them doesn’t necessarily give the value that was written to them, so won’t be re-written as part of an update to another field! Luckily there are few real-world implementations of this…
- But the worst kind of bit field, and often implemented in microcontrollers, is referred to as “read, write
1
to clear” [rw1c
orrc_w1
].
The latter bit field is often referred to as a “latch”. Normally the field reads as a 0
. Some event triggers the latch, making the field read as a 1
. When the program notices that the latch is set, it performs some code, then must reset the latch by writing a 1
to the field to acknowledge it—this is to allow writing a 0
to the field as a “safe” operation with no side effect.
A contrived example
A common example is the Interrupt Status Register (ISR) of numerous hardware peripherals. For this example, assume that the three interrupt causes are called a
, b
and c
:
// Interrupt Status Register struct ISR { bool a : 1; // Interrupt cause 'a' bool b : 1; // Interrupt cause 'b' unsigned : 3; // Reserved bool c : 1; // Interrupt cause 'c' unsigned : 0; // Pad out to full width }; // ISR extern volatile ISR isr; // Address set by the linker
Reading the ISR usually returns all fields as 0
. Inside the interrupt handler routine, the code has to read the ISR to determine what caused the interrupt: the relevant field will now be a 1
. To acknowledge that the interrupt reason has been handled, the code writes a 1
to the relevant field in the ISR: all other fields should be 0
.
Naïve implementation
Naïve code to implement such a handler might look like this:
void Handler() { if (isr.a) { HandleA(); isr.a = true; // Clear isr.a latch } // if if (isr.b) { HandleB(); isr.b = true; // Clear isr.b latch } // if if (isr.c) { HandleC(); isr.c = true; // Clear isr.c latch } // if } // Handler()
But there’s a fatal flaw here: can you see it?
Flaw!
That’s right: what happens if there are two interrupt reasons simultaneously? The above code would perform the following operations:
- Detects the first of the two fields (whichever one it is);
- Executes the code to handle that cause;
- Writes a
1
to the first field.
But remember what the compiler has to do: to write a 1
in just that bit position, it needs to read the rest of the register, set the requested bit to a 1
, and write the compound value back—and the second field hasn’t been handled yet, so is still a 1
signalling that it needs to be handled. That means that the code writes the 1
back to the second field too, acknowledging the second interrupt cause as well even though it hasn’t been handled yet.
Workaround?
Using manual bit manipulation code, all that is needed is a simple write to the ISR, with no pre-read step. Using bit fields, the closest to a workaround that I can come up with is:
void Handler() { if (isr.a) { ISR ack = { }; // Create a temporary, zero ISR HandleA(); ack.a = true; // Set the correct bit isr = ack; // Clear acked ISR } // if if (isr.b) { ISR ack = { }; // Create a temporary, zero ISR HandleB(); ack.b = true; // Set the correct bit isr = ack; // Clear acked ISR } // if if (isr.c) { ISR ack = { }; // Create a temporary, zero ISR HandleC(); ack.c = true; // Set the correct bit isr = ack; // Clear acked ISR } // if } // Handler()
I felt that this was better than having a single function-wide
ack
definition, written back as the last step just beforeHandler()
exited. This ensures that handled interrupts are acknowledged as quickly as possible.
Note though that implementing something like this workaround is required for every write to any field within the struct
, not just the rw1c
field(s). And if there are other, normal fields that need to be maintained, the read-modify-zerorw1c
-write operation becomes nightmarish.
Summary
So, when a register has a rw1c
field, it is too unwieldy (if not actually dangerous) to represent the whole register by a struct
bit field, lest a write to any other field accidentally trips the rw1c
field.
So I’m proposing a syntax extension to the definition for struct
bit fields.
Comments are welcome. I suggest that generic comments on the whole “Bit fields” series and concepts go on the main page, while comments specific to this sub-page are written here.