将原始 14 位二进制补码转换为带符号的 16 位整数

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

我正在使用一个加速度计在嵌入式 C 中做一些工作,该加速度计 returns 数据作为 14 位 2 的补码。我将此结果直接存储到 uint16_t 中。稍后在我的代码中,我试图将这种 "raw" 形式的数据转换为带符号的整数,以在我的其余代码中表示/使用。

我无法让编译器理解我正在尝试做什么。在下面的代码中,我检查是否设置了第 14 位(意味着数字为负数),然后我想反转这些位并加 1 以获得数字的大小。

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

很遗憾,此代码无效。反汇编告诉我,出于某种原因,编译器正在优化我的语句 raw_signed = -(~raw + 1); 如何获得我想要的结果?

数学在纸上是可行的,但我觉得编译器出于某种原因与我作对:(。

我会做简单的算术运算。结果是 14 位有符号的,表示为从 0 到 2^14 - 1 的数字。测试数字是否为 2^13 或以上(表示负数),然后减去 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;
}

请检查我的算法。 (我的 13 和 14 对吗?)

将 14 位 2 的补码值转换为 16 位有符号,同时保持该值仅需以下几步:

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

左移将符号位推入16位符号位位置,除以四恢复大小但保持其符号。除法避免了右移的实现定义行为,但通常会在允许的指令集上导致单个算术右移。转换是必要的,因为 raw << 2 是一个 int 表达式,除非 int 是 16 位,除法将简单地恢复原始值。

然而,将加速度计数据左移两位并将其视为传感器首先是 16 位会更简单。将所有内容标准化为 16 位的好处是,如果您使用最多 16 位的传感器,则无需更改代码。幅度只会增加四倍,最低有效两位将为零 - 无信息是增益还是损失,缩放比例任意。

int16_t accel = raw << 2 ;

在这两种情况下,如果您想要无符号大小,那么就是:

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

假设您的特定 C 实现中的 int 是 16 位宽,您在 mangling raw 中使用的表达式 (1 << 15) 会产生未定义的行为。在那种情况下,编译器可以自由生成代码来做几乎任何事情——或者什么都不做——如果条件分支被采用,其中表达式被评估。

此外,如果 int 是 16 位宽,则表达式 -(~raw + 1) 和所有中间值的类型将是 unsigned int == uint16_t。这是 "the usual arithmetic conversions" 的结果,因为(16 位)int 不能表示类型 uint16_t 的所有值。结果将设置高位,因此超出类型 int 可表示的范围,因此将其分配给类型 int 的左值会产生实现定义的行为。您必须查阅您的文档以确定它定义的行为是否符合您的预期和想要的。

如果改为执行 14 位符号转换,强制关闭高阶位 ((~raw + 1) & 0x3fff),则结果(所需负值的倒数)可由 16-位符号 int,因此明确转换为 int16_t 并保留(正)值。你想要的结果是它的倒数,你可以简单地通过否定它来获得。总体:

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

当然,如果 int 在您的环境中比 16 位宽,那么我看不出您的原始代码无法按预期工作的原因。但是,这不会使上面的表达式无效,无论默认 int.

的大小如何,它都会产生一致定义的行为

假设代码达到 return ((int32_t)raw_signed ... 时,它的值在 [-8192 ... +8191] 范围内:

如果RAW_SCALE_FACTOR是4的倍数那么可以节省一点钱。

所以而不是

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

改为

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

要将 14 位二进制补码转换为有符号值,您可以翻转符号位并减去偏移量:

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