1

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?
glades
  • 3,778
  • 1
  • 12
  • 34
  • If you are using an OS and not baremetal, it (the OS) quite likely has helper functions to convert between endianess (in Linux, for example, they are `cpu_to_leXX()`/`leXX()_to_cpu()` and all the same for BE. – 0andriy Mar 07 '23 at 12:55
  • Thanks but I think the conversion between byte endianness is not so much a problem as knowing the bit endianness which worries me much more. How do I know for sure that bits are ordered in MSbit first in big endian or LSbit first in little endian to make the serial communication work? – glades Mar 07 '23 at 13:07
  • This is up to hardware. You read the manuals and datasheets and deduct it from there. – 0andriy Mar 09 '23 at 14:34

1 Answers1

1

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.

David van rijn
  • 2,108
  • 9
  • 21
  • I miss mentioning of the at least two ways in which different compilers arrange struct bits (and struct bytes); at least less recent C compilers, on which my experience focuses. Did C++ define this? Or more recent C standards? Stating that memory adresses are in bytes does not cover this point. – Yunnosch Mar 07 '23 at 08:04
  • yes, you are right, I hope OP does not send the data like `writeSPI((void*) &myStruct, sizeof mystruct)` Because that is not guaranteed to work. I will add that as well :) – David van rijn Mar 07 '23 at 08:08
  • @Davidvanrijn yes this was my intention. What do I need to do instead? – glades Mar 07 '23 at 08:10
  • I put an example in an edit – David van rijn Mar 07 '23 at 08:19
  • @Davidvanrijn Thank you. Could you explain a bit more about question 2). Are there such things as reverse-bit big endian or little endian? This question would also be relevant in case if I had bitfields within my struct. They could be ordered to either end of the byte, is there a way to find out how the compiler does this at compile time? – glades Mar 07 '23 at 13:05
  • It is kinda the same as the struct packing. It is an extension and non-standard. See also here: https://stackoverflow.com/questions/1490092/c-c-force-bit-field-order-and-alignment But most people on embedded use gcc and struct packing. – David van rijn Mar 07 '23 at 13:15