如何将此数字表示形式转换为浮点数?

How can I convert this number representation to a float?

我从温度传感器(类型 MCP9808)读取这个 16 位值

忽略前三个 MSB,将其他位转换为浮点数的简单方法是什么? 我设法将值 2^7 到 2^0 转换为具有一些位移的整数:

uint16_t rawBits = readSensor();
int16_t value = (rawBits << 3) / 128;

但是我想不出一个简单的方法来包含指数小于 0 的位,除了手动检查它们是否已设置然后添加 1/2、1/4、1/8和 1/16 分别为结果。

float convert(unsigned char msb, unsigned char lsb)
{
    return ((lsb | ((msb & 0x0f) << 8)) * ((msb & 0x10) ? -1 : 1)) / 16.0f;
}

float convert(uint16_t val)
{
    return (((val & 0x1000) ? -1 : 1) * (val << 4)) / 256.0f;
}

这样的事情似乎很合理。取数字部分,除以16,修正符号。

float tempSensor(uint16_t value) {
  bool negative = (value & 0x1000);
  return (negative ? -1 : 1) * (value & 0x0FFF) / 16.0f;
}

如果性能不是特别重要,我会选择不那么聪明但更明确的东西,例如:

bool is_bit_set(uint16_t value, uint16_t bit) {
    uint16_t mask = 1 << bit;
    return (value & mask) == mask;
}

float parse_temperature(uint16_t raw_reading) {
    if (is_bit_set(raw_reading, 15)) { /* temp is above Tcrit. Do something about it. */ }
    if (is_bit_set(raw_reading, 14)) { /* temp is above Tupper. Do something about it. */ }
    if (is_bit_set(raw_reading, 13)) { /* temp is above Tlower. Do something about it. */ }


    uint16_t whole_degrees = (raw_reading & 0x0FF0) >> 4;

    float magnitude = (float) whole_degrees;
    if (is_bit_set(raw_reading, 0)) magnitude += 1.0f/16.0f;
    if (is_bit_set(raw_reading, 1)) magnitude += 1.0f/8.0f;
    if (is_bit_set(raw_reading, 2)) magnitude += 1.0f/4.0f;
    if (is_bit_set(raw_reading, 3)) magnitude += 1.0f/2.0f;
    
    bool is_negative = is_bit_set(raw_reading, 12);
    
    // TODO: What do the 3 most significant bits do?

    return magnitude * (is_negative ? -1.0 : 1.0);
}

老实说,这是很多简单的常量数学运算,如果编译器无法对其进行大量优化,我会感到很惊讶。当然,这需要确认。

如果您的 C 编译器有一个 clz buitlin 或等效的,它可能有助于避免 mul 操作。 在您的情况下,由于提供的 temp 值看起来像尾数,并且如果您的 C 编译器使用 IEEE-754 浮点表示,则将 temp 值转换为 IEEE-754 等价物可能是最有效的方法:

更新:压缩代码一点,对尾数的解释更清楚。

float convert(uint16_t val) {
  uint16_t mantissa = (uint16_t)(val <<4);
  if (mantissa==0) return 0.0;
  unsigned char e =  (unsigned char)(__builtin_clz(mantissa) - 16);
  uint32_t r = (uint32_t)((val & 0x1000) << 19 | (0x86 - e) << 23 | ((mantissa << (e+8)) & 0x07FFFFF));
  return *((float *)(&r));
}

float convert(unsigned char msb, unsigned char lsb) {
  uint16_t mantissa = (uint16_t)((msb<<8 | lsb) <<4);
  if (mantissa==0) return 0.0;
  unsigned char e =  (unsigned char)(__builtin_clz(mantissa) - 16);
  uint32_t r = (uint32_t)((msb & 0x10) << 27 | (0x86 - e) << 23 | ((mantissa << (e+8)) & 0x07FFFFF));
  return *((float *)(&r));
}

解释:

我们使用这样一个事实,即温度值不知何故是 -255 到 255 范围内的尾数。 然后我们可以认为它的 IEEE-754 指数最大为 128,最小为 -128。 我们使用 clz buitlin 来获取尾数中第一位设置的“顺序”, 这样我们就可以将指数定义为理论最大值 (2^7 =>128) 减去这个“阶数”。 我们还使用此命令左移温度值以获得 IEEE-754 尾数, 加上一个左移以减去 IEEE-754 的尾数的“1”隐含部分。 因此,我们从临时值构建一个 32 位二进制 IEEE-754 表示:

  1. 首先是符号位到我们的二进制 IEEE-754 表示的第 32 位。
  2. 计算的指数为理论最大值 7 (2^7 =>128) 加上 IEEE-754 偏差 (127) 减去温度值的实际“顺序”。 通过 clz 内置函数,临时值的“顺序”从变量 mantissa 中其 12 位表示的前导“0”的数量中减去。 请注意,这里我们认为 clz 内置函数需要一个 32 位值作为参数,这就是我们在这里减去 16 的原因。如果您的 clz 需要任何其他内容,则此代码可能需要修改。 前导“0”的数量可以从 0(温度值高于 128 或低于 -127)到 11,因为我们直接 return 0.0 表示零温度值。 由于“顺序”的后续位在温度值中为 1,因此它相当于从理论最大值 7 减少 2 的幂。 因此,对于 7 + 127 => 0x86,我们可以简单地减去“顺序”,因为前导“0”的数量允许我们推导出 IEEE-754 的 'first' 基指数。 如果“顺序”大于 7,我们仍然会得到小于 1 值所需的负指数。 然后我们将这个 8 位指数添加到我们的二进制 IEEE-754 表示中,从第 24 位到第 31 位。
  3. 临时值在某种程度上已经是尾数,我们通过将其向左移动 (e + 1) 来抑制前导“0”及其第一位设置,同时还向左移动 7 位以将尾数放在32 位(e+7+1 => e+8)。然后我们只屏蔽所需的 23 位 (AND &0x7FFFFF)。 它的第一个位集必须被删除,因为它是 IEEE-754 中的“1”隐含有效数(指数 2 的幂)。 然后我们有 IEEEE-754 尾数并将它放在我们的二进制 IEEE-754 表示的第 8 位到第 23 位之间。 我们的 16 位临时值中的 4 个初始尾随 0 和移位中添加的七个 'right' 0 不会更改有效的 IEEE-754 值。 当我们从 32 位值开始并在 32 位指数和尾数上使用或运算符 (|) 时,我们就得到了最终的 IEEE-754 表示。
  4. 然后我们可以return将此二进制表示形式作为 IEEE-754 C 浮点值。

由于需要 clz 和 IEEE-754 翻译,这种方式的可移植性较差。主要兴趣是避免在生成的机器代码中进行 MUL 操作,以在具有“差” FPU 的 arch 上实现性能。

P.S.: 强制转换解释。我添加了显式强制转换,让 C 编译器知道我们自愿丢弃了一些位:

  • uint16_t mantissa = (uint16_t)(val <<4); :这里的强制转换告诉编译器我们知道我们将“松开”四个左边的位,因为它是这里的目标。我们丢弃尾数的临时值的前四位。
  • (unsigned char)(__builtin_clz(mantissa) - 16) :我们告诉 C 编译器我们将只考虑内置 return 的 8 位范围,因为我们知道我们的尾数只有 12 个有效位,因此范围输出从 0 到 12。因此我们不需要完整的 int return.
  • uint32_t r = (uint32_t) ... :我们告诉 C 编译器在构建 IEEE-754 表示时不要理会这里的符号表示。