For some reason, I donโt understand, the developers of the C standard decided that specifying a number type together with a bit field should allocate enough space to store this number type if the previous field was not a bit field allocated from the same type that had enough free space to process the next field.
In your specific example, on a machine with 16-bit unsigned shorts, you should change the declarations in your bitfield to unsigned shorts. As it happens, unsigned char will also work and give the same results, but this is not always the case. If optimally packed bit fields will cover char boundaries, but not short borders, then to declare bit fields as unsigned char will need to fill in to avoid such a redistribution.
Although some processors will not have problems creating code for bit fields that limit the boundaries of storage blocks, this C standard would prohibit them from packing in this way (again, for reasons that I donโt understand). For example, on a machine with typical 8/16/32/64-bit data types, the compiler could not allow the programmer to specify a 3-byte structure containing eight three-byte fields, since the fields would have to saddle the byte boundaries, I could understand the specification that does not require compilers to process fields that cross borders, or require that these bit fields be deferred in a certain way (I would consider them to be infinitely more useful if I could indicate that a particular bit field should, for example, use a bit 4- 7 in some place), but this standard seems to give the worst of both worlds.
In any case, the only way to effectively use bit fields is to find out where the storage boundaries are and select the types for the bit codes accordingly.
PS - It is interesting to note that although I recall the compilers used to prohibit volatile declarations for structures containing bit fields (since the sequence of operations when writing a bit field may not be defined), the semantics can be clearly defined in accordance with the new rules (I donโt know whether the specification really requires them). For example, given:
typedef struct { uint64_t b0:8,b1:8,b2:8,b3:8, b4:8,b5:8,b6:8,b7:8; uint64_t b8:8,b9:8,bA:8,bB:8, bC:8,bD:8,bE:8,bF:8; } FOO; extern volatile FOO bar;
bar.b3 = 123; operator bar.b3 = 123; will read the first 64 bits of bar , and then write the first 64 bits of bar with the updated value. If bar were not volatile, the compiler could replace this sequence with a simple 8-bit record, but bar can be something like a hardware register that can only be written in 32-bit or 64-bit fragments.
If I had my druthers, it would be possible to define bit fields using something like:
typedef struct { uint32_t { baudRate:13=0, dataFormat:3, enableRxStartInt: 1=28, enableRxDoneInt: 1, enableTxReadyInt: 1, enableTxEmptyInt: 1;}; }; } UART_CONTROL;
indicates that baudRate is 13 bits starting at bit 0 (LSB), dataFormat is 3 bits starting at baudRate, enableRxStartInt is bit 28, etc. Such syntax will allow recording many types of data packing and unpacking in a portable mode and would allow performing many manipulations with I / O registers in the compiler-agnostic method (although such code would obviously be hardware specific).