Guaranteeing bit endianness of structs for serial data transmissions (SPI)

254 Views Asked by At

I'm trying to send data over serial peripheral interface (SPI) which takes a buffer and sends it MSbit first through to data out. For this to succeed, I need my structs to be translated to a bit buffer in exactly the right way and I'm not really sure how I can guarantee cross platform that it always works.

Here's the struct that I want to send:

using data_settings = uint8_t;

enum class channel_settings : uint8_t
{
    power_down_setting = 0,
    DA1,
    DA2,
    DA3,
    DA4,
    DA5,
    DA6,
    DA7,
    DA8,
    power_down_release,
    NA1,
    NA2,
    io_da_select,
    io_serial_parallel,
    io_parallel_serial,
    io_status_setting,
};

struct message // send this over SPI
{
    data_settings data;
    channel_settings channel;
};

As you can see the struct message is 2 bytes in size and I suppose it will be layed out differently on big endian / little endian systems. But what about the bit ordering? In theory, the bits could also be layed out either way regardless of the byte endianness, or can't they? But the SPI driver only accepts exactly one solution where most significant bytes are sent first and most significant bits are also sent first. Here's how I think about it:

Byte and bit ordering for a struct layout

Now my questions:

  • Do htonl and htons also flip the bit ordering?
  • Does something like big endian with LSbit first even exist in real life? Or is the byte ordering always indicative of the bit ordering?
  • How can I guarantee cross platform that the datastruct in question is always layed out exactly right in the bit buffer?
1

There are 1 best solutions below

6
David van rijn On

Bit ordering not really a thing in most (all?) processors, since memory is addressed in bytes. Look at it as if all the bits are next to eachother, and bytes are stacked on top of eachother.

For example:

uint8_t c = 13;
bool lsBit = c & 0x01;

lsBit is always the least significant bit. It doesn't matter where it is physically stored in register. Because we cannot address it like c[n] to get the n'th bit.
Same for bitshifts:

c << 1 

is always the same as c*2.

Where endianness comes into play is when you do this:

uint32_t someValue = 123;
uint8_t* smallptr = (uint8_t*) &someValue;
*smallptr == ???

Now we take an address of a 32 bit value, and interpret it as an 8bit value. So now we are dealing with addresses, since there are 4 addresses that point to the 4 bytes of someValue, and which one does smallptr point to, depends on your endianness.


That being said, on wires there is an order. So then it is the responsibility of the peripheral (or the driver if you are bit-banging) to put the bits in the right order. Most protocols actually define which bit goes first. But wikipedia says this about spi:

Data is usually shifted out with the most significant bit first.

Which seems like it not entirely enforced.


Structs don't enforce byte ordering/packing. So it is not guaranteed that sizeof(message)==2. Or that the order stays the same (though that is very likely).

The hacky way to do it is to use the packed attribute. But then you might have other issues: Is gcc's __attribute__((packed)) / #pragma pack unsafe?

I think the most proper way to do this is to write a function like:

message::serialize(uint8_t* dest){
  dest[0] = data;
  dest[1] = channel;
}

Although you will most likely get away with struct packing.