Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C - Signedness when adding raw bytes

I have sensor data in a uint8_t array which are signed (when concatenated to a 16 Bit value). Now, I want to average the values over N cycles, such that I have to add them into a larger than int16_t value, i.e. a int32_t value.

int32_t val_x = 0 

//...

//do this n times when data is received, dataReceived is a uint8_t array
val_x += ((int16_t)dataReceived[3] << 8) | dataReceived[4];

But I have the feeling that this might cause errors. Assume I have a value of -1 (0xFFFF) and add it to a 32Bit signed value, it will interpret it as a positive value, right? Or does the compiler know, that when I cast to int16_t that this is a negative value and will represent it in the int32_t variable val_x as 0xFFFFFFFF? Do I have to do on more cast on the total value like

val_x += (int16_t)(((int16_t)dataReceived[3] << 8) | dataReceived[4]); 

I program on a 16Bit (or possibly 24Bit with some always-zero-bits) (PIC24F from Microchip). I hope you get the problem I am having. I'm a little lost by such a seemingly trivial problem...

like image 238
jjstcool Avatar asked Aug 11 '26 14:08

jjstcool


1 Answers

In (int16_t)dataReceived[3] << 8, the uint8_t dataReceived[3] is converted to int16_t. This conversion operates by value; the bits representing it are irrelevant. Since all uint8_t values are representable in int16_t, the result is a value from 0 to 255, inclusive. In the case you ask about, the value is 255.

Then the integer promotions are performed on the left operand of <<. If int is 16 bits in this C implementation, then either int16_t is an alias for int or it is converted to int. (Exception: If the C implementation does not use two’s complement for int, the behavior is more complicated. However, this answer will not go into that.) Then the shift operation is performed. Shifting 255 left 8 bits mathematically yields 65,280. However, this is not representable in a 16-bit int, so the behavior is undefined, per C 2018 6.5.7 4.

If int is wider than 16 bits, then the integer promotions promote int16_t to int, the shift does not overflow, and the result is some value in 0 to 65,280, inclusive, that is a multiple of 256.

Then ORing in dataReceived[4] merges its bits into this value. The result is a value from 0 to 65,535, inclusive.

Then the cast to (int16_t) is applied. If the value is 0 to 32,767, it is the result. Otherwise, the value cannot be represented in int16_t. Then the result is implementation-defined or an implementation-defined signal is raised, per C 2018 6.3.13. GCC and Clang define the result to wrap modulo 216, so this will produce the result you desire. Other compilers might not produce the result you desire.

If int is 16 bits, you could avoid the overflow in shifting by casting dataReceived[3] to int32_t instead of to int16_t. Then the integer promotions will not change it, and the int32_t value will be shifted.

However, since we still need to deal with the overflow in the final conversion to int16_t, there is another method:

uint16_t tu = (uint16_t) dataReceived[3] << 8 | dataReceived[4];
int16_t  ti;
memcpy(&ti, &tu, sizeof ti);
val_x += ti;

This:

  • Converts dataReceived[3] to uint16_t so the shift will not overflow.
  • Completes the shift and merge and stores the result in a temporary uint16_t object.
  • Copies the bits of the uint16_t object into an int16_t object to reinterpret them as a 16-bit two’s complement integer.
  • Adds the integer to val_x.

The following code is effectively the same and avoids the named temporaries but may be less familiar and harder to read for novice C programmers:

val_x += (union { uint16_t u; int16_t i; }) { (uint16_t) dataReceived[3] | dataReceived[4] } .i;
like image 71
Eric Postpischil Avatar answered Aug 14 '26 08:08

Eric Postpischil