I am doing some work in embedded C with an accelerometer that returns data as a 14 bit 2's complement number. I am storing this result directly into a uint16_t
. Later in my code I am trying to convert this "raw" form of the data into a signed integer to represent / work with in the rest of my code.
I am having trouble getting the compiler to understand what I am trying to do. In the following code I'm checking if the 14th bit is set (meaning the number is negative) and then I want to invert the bits and add 1 to get the magnitude of the number.
int16_t fxls8471qr1_convert_raw_accel_to_mag(uint16_t raw, enum fxls8471qr1_fs_range range) {
int16_t raw_signed;
if(raw & _14BIT_SIGN_MASK) {
// Convert 14 bit 2's complement to 16 bit 2's complement
raw |= (1 << 15) | (1 << 14); // 2's complement extension
raw_signed = -(~raw + 1);
}
else {
raw_signed = raw;
}
uint16_t divisor;
if(range == FXLS8471QR1_FS_RANGE_2G) {
divisor = FS_DIV_2G;
}
else if(range == FXLS8471QR1_FS_RANGE_4G) {
divisor = FS_DIV_4G;
}
else {
divisor = FS_DIV_8G;
}
return ((int32_t)raw_signed * RAW_SCALE_FACTOR) / divisor;
}
This code unfortunately doesn't work. The disassembly shows me that for some reason the compiler is optimizing out my statement raw_signed = -(~raw + 1);
How do I acheive the result I desire?
The math works out on paper, but I feel like for some reason the compiler is fighting with me :(.
To convert the 14-bit two's-complement into a signed value, you can flip the sign bit and subtract the offset:
Supposing that
int
in your particular C implementation is 16 bits wide, the expression(1 << 15)
, which you use in manglingraw
, produces undefined behavior. In that case, the compiler is free to generate code to do pretty much anything -- or nothing -- if the branch of the conditional is taken wherein that expression is evaluated.Also if
int
is 16 bits wide, then the expression-(~raw + 1)
and all intermediate values will have typeunsigned int
==uint16_t
. This is a result of "the usual arithmetic conversions", given that (16-bit)int
cannot represent all values of typeuint16_t
. The result will have the high bit set and therefore be outside the range representable by typeint
, so assigning it to an lvalue of typeint
produces implementation-defined behavior. You'd have to consult your documentation to determine whether the behavior it defines is what you expected and wanted.If you instead perform a 14-bit sign conversion, forcing the higher-order bits off (
(~raw + 1) & 0x3fff
) then the result -- the inverse of the desired negative value -- is representable by a 16-bit signedint
, so an explicit conversion toint16_t
is well-defined and preserves the (positive) value. The result you want is the inverse of that, which you can obtain simply by negating it. Overall:Of course, if
int
were wider than 16 bits in your environment then I see no reason why your original code would not work as expected. That would not invalidate the expression above, however, which produces consistently-defined behavior regardless of the size of defaultint
.I would do simple arithmetic instead. The result is 14-bit signed, which is represented as a number from 0 to 2^14 - 1. Test if the number is 2^13 or above (signifying a negative) and then subtract 2^14.
Please check my arithmetic. (Do I have 13 and 14 correct?)
Converting the 14 bit 2's complement value to 16 bit signed, while maintaining the value is simply a metter of:
The left-shift pushes the sign bit into the 16 bit sign bit position, the divide by four restores the magnitude but maintains its sign. The divide avoids the implementation defined behaviour of an right-shift, but will normally result in a single arithmetic-shift-right on instruction sets that allow. The cast is necessary because
raw << 2
is anint
expression, and unlessint
is 16 bit, the divide will simply restore the original value.It would be simpler however to just shift the accelerometer data left by two bits and treat it as if the sensor was 16 bit in the first place. Normalising everything to 16 bit has the benefit that the code needs no change if you use a sensor with any number of bits up-to 16. The magnitude will simply be four times greater, and the least significant two bits will be zero - no information is gained or lost, and the scaling is arbitrary in any case.
In both cases, if you want the unsigned magnitude then that is simply:
Assuming when code reaches
return ((int32_t)raw_signed ...
, it has a value in the[-8192 ... +8191]
range:If
RAW_SCALE_FACTOR
is a multiple of 4 then a little savings can be had.So rather than
instead