Union of bitfields and larger type on ARM

Feb 23, 2016 34 Replies

A vote on this: At least GCC optimizes these to single bit field extract instructions on a Cortex.

Alternative format of the same:

static inline uint8_t integer_high(uint32_t x) { return (uint8_t)(x >> 24); }

This should work correctly even without the cast.

-TV

Are you sure? Have you checked the assembly output for this code? For non-volatile variables at least, I expect that these inline functions are optimized to the same instructions. I don'T have an ARM compiler handy, but maybe you could check if it also works with "volatile unit32_t x"

If C++ is used, an inline function with a reference parameter is also easily optimized:

static inline void set_integer_low (uint32_t x&, uint8_t y) { x= (x & 0xff00ffff) | ((uint32_t) y

Yes, I have checked - though not for a wide variety of versions, optimisation flags, etc. The inline functions will result in the same ARM code in the non-volatile case (assuming at least -O1 optimisation). But in the volatile case, the compiler correctly generates different code for the structure access and the manual bitfield extraction.

It is still a mess compared to the bitfield struct access, and it is still different in the volatile case. Yes, the compiler can optimise the ugly, unmaintainable and error-prone source code into good object code - but why not just write clear, simple source code that gets optimised into good object code?

I would expect the compiler to manage this optimisation.

I think the OP should look first and foremost at the /source/ code, and decide from that. The code generation details are up to the compiler - enable optimisation, give it clear and correct source code, and let it do its job. It is only in very rare situations that the generated code should be the guide for a decision like this.

In both C and C++, you have to embed the item in a struct - "typedef" itself does not introduce a new type. Yes, this technique can make the code safer.

But you are going round in circles, writing lots of extra source code, writing access functions with potentially error-prone masks (imagine a case where the bitfields were not as simple, or where they were changed later), with duplicate information in multiple places, and finally resulting in some unnecessarily verbose and ugly usage.

In the end, all you have done is duplicate a feature that C and C++ support as part of the language. It is a pointless exercise.

I should have mentioned this before, but in the case where your bitfield sizes match the type, you don't need the bitfield specifiers:

typedef union { uint32_t phase_accumulator_full; struct { uint8_t integer_high; uint8_t integer_low; uint8_t frac_high; uint8_t frac_low; }; } phase_accumulator;

(Of course, the byte ordering is still wrong - I just wrote it that way to keep it consistent.)

Under K&R C the ordering of fields was not guaranteed, but I think this was cleaned up by C99 or possibly somewhat later.

OP should see which language standard their compiler uses and refer to that standard and comment appropriately. (In addition to testing the code...)

Steve

I haven't looked at K&R for an older definition, but C11 certainly says that the order of allocation of bit-fields is implementation-dependent. There are some rules about packing, but there is plenty of scope for weird implementations if the compiler writers so fancy.

As long as his compiler follows the ARM EABI (and all serious embedded ARM compilers will do so), there should be no problem - the EABI gives more details for this sort of thing than any of the C or C++ standards.

(He could make it a little easier by dropping the bitfields - they are not necessary when the struct fields are uint8_t and the bitfields are 8 bits.)

If it doesn't, fix it. Endianness is more or less undefined behavior in 'C' so you have to test it.

Walk a 1 through phase_accumulator_full and print out ( or just store the result and dump it with a JTAG if you are without a console ).

Les Cargill

I don't mean "this is an inappropriate question" but I'd think he could have the answer in 15 minutes or less by trying it.

To wit:

phase_accumulator p;

p.phase_accumulator_full = 0x012345678;

printf("p.bob.integer_high = %x\n",p.bob.integer_high);

etc. etc.

If'n ya ain't got no printfs, then you hopefully know how to deal with that.

Les Cargill

I thought I had no printfs, but I discovered the other day that I can serial println to the console via USB on the development board I'm using...

No, endianness is not remotely "undefined behaviour" - it is /implementation/ defined behaviour. There is a huge difference.

This means that the implementation has to document exactly what endianness is used - and in the case of ARM compilers for embedded systems, this is specified in the ARM EABI to be little endian.

Being implementation-defined behaviour, this also means that you /can/ test it - sometimes that is easier than trying to find the relevant documentation! Once you have tested it, you know exactly how it works for that implementation. And while in theory it could change in later versions of the compiler, making such a change between compiler versions would be astoundingly stupid and unhelpful - especially in an embedded compiler - so it is not going to happen. (I know of only one case where this happened - and that was on MS's x86 compiler, a long time ago, where they changed the bitfield endianness.)

If endianness (or bitfield endianness) really were undefined behaviour, you could not rely on it being the same between two runs of the same compiler on the same source code - testing would be useless.

We have software that is built for both big-endian and little-endian hosts (and more interestingly, it must internally simulate both big-endian and little-endian processors).

We have this in one of the project header files.

#include #ifndef __BYTE_ORDER #if defined(__BIG_ENDIAN) && !defined(__LITTLE_ENDIAN) #define __BYTE_ORDER __BIG_ENDIAN #elif !defined(__BIG_ENDIAN) && defined(__LITTLE_ENDIAN) #define __BYTE_ORDER __LITTLE_ENDIAN #define __BIG_ENDIAN 4321 #elif !defined(__BIG_ENDIAN) && !defined(__LITTLE_ENDIAN) #define __BIG_ENDIAN 4321 #define __BYTE_ORDER __BIG_ENDIAN #else #error Unable to determine Endian mode #endif #endif

#if !defined(htobe16) # if __BYTE_ORDER == __LITTLE_ENDIAN # define htobe16(x) __bswap_16 (x) # define htole16(x) (x) # define be16toh(x) __bswap_16 (x) # define le16toh(x) (x)

# define htobe32(x) __bswap_32 (x) # define htole32(x) (x) # define be32toh(x) __bswap_32 (x) # define le32toh(x) (x)

# define htobe64(x) __bswap_64 (x) # define htole64(x) (x) # define be64toh(x) __bswap_64 (x) # define le64toh(x) (x) # else # define htobe16(x) (x) # define htole16(x) __bswap_16 (x) # define be16toh(x) (x) # define le16toh(x) __bswap_16 (x)

# define htobe32(x) (x) # define htole32(x) __bswap_32 (x) # define be32toh(x) (x) # define le32toh(x) __bswap_32 (x)

# define htobe64(x) (x) # define htole64(x) __bswap_64 (x) # define be64toh(x) (x) # define le64toh(x) __bswap_64 (x) # endif #endif

Then use hto{bl}eXX and {bl}e64tohXX in the code without the need for local ifdefs.

Yes, that looks like a reasonable solution if you need to handle different endiannesses. Another possibility will come with gcc 6, which supports attributes for structs with specific endiannesses (and I know at least one compiler that has had that feature for perhaps a couple of decades).

But if your code does not have to be portable - such as if you are modelling a hardware register on a particular device - you can just use the one fixed layout.

Ah, there you go. Right! I'd forgotten the term of art.

Les Cargill

It happened to me on an 8051 C compiler in the '90s. The endianness of bitfields changed between one compiler version and the next. I learned a lesson, and have not used bitfields in C since then.

I believe the compiler vendor is still in business today (well, they were bought by a microcontroller manufacturer).

Regards, Allan

Thanks - now I know of /two/ cases where compilers have changed bit ordering!

You are throwing out the baby with the bathwater. If you avoid using a feature just because a small-time compiler writer for a brain-dead and C-unfriendly processor made an inconvenient decision two decades ago, you would have to give up programming altogether. Bitfields have portability issues, but can be extremely useful - they are perfectly safe to use once you understand them.

Join the Discussion

Have something to add? Share your thoughts — no account required.

Didn't find your answer?

Ask the community — no account required