5

The problem is simple: Take an 32-bit or 64-bit integer and split it up to send over an (usually)1-byte interface like uart, spi or i2c.

To do this I can easily use bit masking and shifting to get what I want. However, I want this to be portable that will work on big and little endian, but also make it work for platforms that don't discard bits but rotate through carry(masking gets rid of excess bits right?).

Example code:

uint32_t value;
uint8_t buffer[4];
buffer[0] = (value >> 24) & 0xFF;
buffer[1] = (value >> 16) & 0xFF;
buffer[2] = (value >> 8) & 0xFF;
buffer[3] = value & 0xFF;

I want to guarantee this works on any platform that supports 32 bit integers or more. I don't know if this is correct.

Tryphon
  • 161
  • 1
  • 13
  • 7
    Shifting and masking *is* endian-proof. – Weather Vane Jan 18 '19 at 14:21
  • Code is good and portable to modern machines. "make it work for platforms that don't discard bits but rather roll over to the other side as if it's a circular buffer" is unclear. Please explain more on that concern. – chux - Reinstate Monica Jan 18 '19 at 14:23
  • The `>>` operator is always a shift, never a rotate (i.e. it always discards the rightmost bits). – interjay Jan 18 '19 at 14:26
  • It is not the machine code that determines how the C code behaves, but the other way around. Unsigned shifts in C are guaranteed to behave as logical shifts, regardless of what instructions the CPU support. The resulting machine code might be a ROR instruction but carry is then discarded. The only exception being signed right-shifts on negative numbers, where C doesn't specify what will happen - the compiler is then free to either use logical shift or arithmetic shift. – Lundin Jan 18 '19 at 14:35
  • You may also try with unions, using something like `union { uint32_t u32; uint8_t u8[4]; } t; t.u32 = value;` – Giovanni Cerretani Jan 19 '19 at 14:56
  • Isn't using unions for this purpose non-standard and completely compiler dependent? – Tryphon Jan 19 '19 at 18:47
  • Probably you're right, according to https://stackoverflow.com/questions/4540638/how-are-the-union-members-stored – Giovanni Cerretani Jan 20 '19 at 10:09

1 Answers1

7

The code you presented is the most portable way of doing it. You convert a single unsigned integer value with 32 bits width into an array of unsigned integer values of exactly 8 bits width. The resulting bytes in the buffer array are in big endian order.

The masking is not needed. From C11 6.5.7p5:

The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has
a signed type and a nonnegative value, the value of the result is the integral part of the quotient of
E1 / 2^E2.

and casting to an integer with 8 bits width is (to the value) equal to masking 8 bits. So (result >> 24) & 0xff is equal to (uint8_t)(result >> 24) (to the value). As you assign to uint8_t variable the masking is not needed. Anyway I would safely assume it will be optimized out by a sane compiler.

I can recommend to take a look at one implementation that I remembered, that I guess has implemented in a really safe manner all the possible variants of splitting and composing fixed-width integers up to 64 bits from bytes and back, that is at gpsd bits.h.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • 1
    The masking is often needed to silence compiler/static analyser warnings. For example a MISRA-C checker will even require that you use `(uint8_t)(value >> 24)` with an explicit conversion through cast. – Lundin Jan 18 '19 at 14:39
  • MISRA should be considered as a separate distinct language :). – 0___________ Jan 18 '19 at 15:22