Bit fields: Show-stopper

There are comments in open-source code about this issue “biting” the programmer.
Their solution? “Be careful when using this field!”

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 or rc_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:

  1. Detects the first of the two fields (whichever one it is);
  2. Executes the code to handle that cause;
  3. 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 before Handler() 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.

Leave a comment