Convert Raw 14 bit Two's Complement to Signed 16 bit Integer

为君一笑 提交于 2019-12-08 15:38:39

问题


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 :(.


回答1:


Converting the 14 bit 2's complement value to 16 bit signed, while maintaining the value is simply a metter of:

int16_t accel = (int16_t)(raw << 2) / 4 ;

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 an int expression, and unless int 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.

int16_t accel = raw << 2 ;

In both cases, if you want the unsigned magnitude then that is simply:

int32_t mag = (int32_t)labs( (int)accel ) ;



回答2:


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.

int16_t fxls8471qr1_convert_raw_accel_to_mag(uint16_t raw, enum fxls8471qr1_fs_range range) 
{
  int16_t raw_signed = raw;
  if(raw_signed >= 1 << 13) {
    raw_signed -= 1 << 14;
  }

  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;
}

Please check my arithmetic. (Do I have 13 and 14 correct?)




回答3:


Supposing that int in your particular C implementation is 16 bits wide, the expression (1 << 15), which you use in mangling raw, 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 type unsigned int == uint16_t. This is a result of "the usual arithmetic conversions", given that (16-bit) int cannot represent all values of type uint16_t. The result will have the high bit set and therefore be outside the range representable by type int, so assigning it to an lvalue of type int 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 signed int, so an explicit conversion to int16_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:

raw_signed = -(int16_t)((~raw + 1) & 0x3fff);

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 default int.




回答4:


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

int16_t raw_signed = raw << 2; 
raw_signed >>= 2;

instead

int16_t fxls8471qr1_convert_raw_accel_to_mag(uint16_t raw,enum fxls8471qr1_fs_range range){
  int16_t raw_signed = raw << 2;
  uint16_t divisor;
  ...
  // return ((int32_t)raw_signed * RAW_SCALE_FACTOR) / divisor;
  return ((int32_t)raw_signed * (RAW_SCALE_FACTOR/4)) / divisor;
}



回答5:


To convert the 14-bit two's-complement into a signed value, you can flip the sign bit and subtract the offset:

int16_t raw_signed = (raw ^ 1 << 13) - (1 << 13);


来源:https://stackoverflow.com/questions/34075922/convert-raw-14-bit-twos-complement-to-signed-16-bit-integer

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!