Embedded C: Struct and Union (Part 2)
atadiat.com
atadiat.com
Struct bitfields may be useful to save memory (e.g. 1 bit for boolean-like variable) but don't rely on it when accessing register data or implenting protocols.
The biggest issue with struct bitfields is that its implementation is ABI-specific, meaning it is not even compiler-specific. For example, his "Application #2: Implementing Protocols" example is not portable across archtectures because the order of bits are swapped when compiling for a Big-endian CPU. We should use bitwise operations for this purpose.
Look at his last example, "Application #3: Access to MCU Registers". The SDK source code he mentioned does not use bitfield but bitwise operations to access register bits.
Add a comment explaining this too.
That will catch nonportable use in the future.
So you can use bitfields, however they have to be defined to work with your compiler/architecture/hardware, and expect that they're going to continue to work for the duration. (Whether bitfields are a bad idea for other reasons is outside the scope of my comment.)
> Please note that peripheral locations should not be accessed using __packed structs (where unaligned members are allowed and there is no internal padding), or using C bitfields.
Using bit fields for protocols is even more ill advised because often you may wish to implement the same code across different devices using the same protocol but the compilers will arrange the bit fields differently.
The only valid example he gives is the last one which is the correct way to do it: a struct made up of 32 bit values. This makes sure that everything is 4 byte aligned and the processor can access those variables in a single instruction.
If you wish to use the dense packing of bit fields but without the compiler ambiguity then you should use explicit bit shifting and bit masking (again shown in his last example)
Reminds me of the C# jockey who complained endlessly about the bitfields (using manual masking) I put into an IoT data structure that was supposed to be kept as small as possible to minimize hosting costs. He had no clue about how to use bitwise operations to extract bits. They managed to bloat that into a horrendous monstrosity but I wasn't going to play their game. If they had their way, a half dozen bits packed into a 32-bit word would have consumed 20x more space with unnecessary schema boilerplate.
The horror...
From a compiler writer's perspective, bitfields are one of the worst things ever proposed for a standard. The nonportability of which way bits are laid out is the least of their problems. The logic of how you actually pack bits in the struct is surprisingly nontrivial. The underlying semantics of bitfields are so weird pretty much every time they could be used, they need to be special-cased--both in the standard sense and in the implementation sense. This means that using bitfields requires navigating a field of landmines of potential compiler (or, worse, spec) bugs.
See my earlier comment about unit testing too.
* Pack the bitfields in a struct all by themselves. Don't mix bitfields and non-bitfields in the same struct. (This means that you limit the scope of insanity to other bitfields, not potentially expanding it to other regular fields).
* Make sure that the underlying type of all the bitfields within the struct have the same type. And make it be unsigned.
* Pad the bitfields to the appropriate size of the type.
* Only access the bitfields by copying them to/from the appropriate local variable (you don't want to trip code up on the bitfields-aren't-quite-the-type-they-say-they-are logic).
You seem overly paranoid. Is there something from your experience that makes you nervous about bitfields?
The last rule is a recognition that bitfields are weird constructs, and their interactions with other weird constructs are likely to cause problems. One example: try using a bitfield in a ternary lvalue operation, especially with clang.
"""Packed bit-fields of type char were not properly bit-packed on many targets prior to GCC 4.4. On these targets, the fix in GCC 4.4 causes an ABI change. For example there is no longer a 4-bit padding between field a and b in this structure:
struct foo
{
char a:4;
char b:8;
} __attribute__ ((packed));
There is a new warning to help identify fields that are affected: foo.c:5: note: Offset of packed bit-field 'b' has changed in GCC 4.4
""" (https://gcc.gnu.org/gcc-4.4/changes.html)Also, the code in Application #3 isn't using bit-fields at all (the definition of GPIO_P_TypeDef is here: https://github.com/SiliconLabs/Gecko_SDK/blob/master/platfor...)
The assumed behaviour of bit fields is well defined between gcc and clang with the proper pragmas. It is therefore probably a non-issue. You even get better aliasing optimisations* using union/bit fields over just using bit shifts.
*) this means, if your union contains a way to read the whole field as ie uint_32t and also to read each field individually and also write struct as u32 via pointer, compiler can properly detect which actions cause read/write invalidation of each other instead of having to assume all volatile-like.
Use case 1: https://gist.github.com/donkeybonks/8749545
Use case 2 (with asm showing better codegen with union): https://gist.github.com/donkeybonks/11103152
nb these gists are ~4 years old because I don’t much care about micro-optimisation anymore.
nb2 it’s a really good idea to static assert the size of all your structs below their declaration for confidence and splash a few unit tests for ordering.
From the GCC documentation[1], it says:
> Determined by ABI.
This means that it may be different when compiling for different architectures. How you can say it well-defined? Here is a good example [2] on the compability issue when using bit fields
[1]: https://gcc.gnu.org/onlinedocs/gcc/Structures-unions-enumera...
In an immense majority of case you won't be serializing or exchanging your bitfields over the network ; this is entirely a non-problem.
My own experience with bit field optimizations was that they were worse in general for GCC and ARM compilers from 4+ years ago. LLVM was much better. For example if you tried to set a few fields that fit within a machine word to a constant value, it should be possible to set them in a single opcode but only LLVM takes the liberty to do this.
The authors use of looking at lines of assembly language code as a metric of goodness for the choices was also problematic. If they coded that stuff for a VAX they would be treated to variable length bit field addressing (see [1] below, page 8-6). Easy to implement very complex bit manipulation in just a few assembly language statements :-).
A better measure is CPU clocks + Memory Accesses (typically measured in kilocoreseconds [another VMS joke])
Not quite sure why ARM removed it. Maybe there were performance costs to all instructions due to the extra hw/decode, while bit-banding only helped a small number of instructions.
If anyone has more info, would love to hear!
In the reference manual, I added a section on what I believe are the bitfield allocation rules used by GCC, as I empirically reverse engineered them.
https://www.nongnu.org/txr/txr-manpage.html#N-027D075C
Hope someone finds it useful.
Endian considerations are not covered in that section because that topic is documented elsewhere in the document.
In a nutshell, on big endian, bitfields are packed from the most significant end of the storage word, and on little endian, from the least significant end of the storage word. Thus the lowest addressed byte of the storage unit is filled first, and so on.
I am not sure whether the underlying spinlock_t struct is something in memory or mapped to a hardware register. But it seems like this was being used to save space rather than to access hardware.
Also, I think the comments are off in lines 5-6 of Application #2: "0 .. 1" should be "1 .. 2", "2 .. 7" should be "3 .. 7"
This was distracted for sure!
I have been a C coder (I'm not so much any more) for about 12 years of my career, and never came across bitfields in all that time, even when doing embedded work.
One style question - why not use the stdint.h types all the way through?
These days it shows up in databases stuff, either because you have e.g. one of these flags per row in your database (or imagine keeping track of used/unused fields in an in-memory block of data), which might add up to millions of booleans, and packing them by a ratio of 64 is a big improvement in space and also time it takes to process that data because the denser the data is, the more of it you can read per second and the more of it can fit in CPU caches. Also you can then use SIMD instructions to process it quicker too, etc etc.
typedef union {
struct {
unsigned REC :8;
};
struct {
unsigned REC0 :1;
unsigned REC1 :1;
unsigned REC2 :1;
unsigned REC3 :1;
unsigned REC4 :1;
unsigned REC5 :1;
unsigned REC6 :1;
unsigned REC7 :1;
};
} RXERRCNTbits_t; struct ip {
#if BYTE_ORDER == LITTLE_ENDIAN
u_char ip_hl:4, /* header length */
ip_v:4; /* version */
#endif
#if BYTE_ORDER == BIG_ENDIAN
u_char ip_v:4, /* version */
ip_hl:4; /* header length */
#endif
As you can already see, they're kind of a pain to work with, so most people avoid them when possible.I just end up creating configuration structs and mapping those to/from register values in some get/set functions as needed
More like: if your compiler is documented to implement bitfields a certain way.