2 分钟阅读 次阅读

PMBus 协议中的 Linear-11 是一种数据编码格式,用 16 位(2 字节)数据表示浮点数(电压、电流、功率、温度等物理量)。

单片机、CPLD/FPGA 处理标准 IEEE 754 浮点数比较耗资源,PMBus 因此设计了这种线性数据格式。

1 Linear-11 的数据结构

Linear-11 把 16 位数据(Read Word 返回的 2 个字节)拆成两部分:

  • 高 5 位:阶码(Exponent,简称 $N$)。
  • 低 11 位:尾数(Mantissa,简称 $Y$)。

两部分都是二进制补码(Two’s Complement),可正可负。

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
阶码 $N$ (5-bit 补码)         尾数 $Y$ (11-bit 补码)                    

计算公式:

\[X = Y \times 2^N\]

2 为什么 READ_VOUT 是 Linear-16

列表里 8Bh READ_VOUT 比较特殊,用的是 Linear-16

输出电压(VOUT)对读取精度要求高,11 位尾数分辨率不够,所以 PMBus 规定:

  1. Linear-16 把返回的整个 16 位都作为尾数 $Y$(通常是无符号数)。
  2. 阶码 $N$ 不在数据里,由单独寄存器 VOUT_MODE (20h) 统一配置。

所以计算 READ_VOUT 时,得先读一次 VOUT_MODE 拿到 $N$,再用这个固定的 $N$ 去解析读回的 16 位 $Y$。

下面用两个例子演示 Linear-11 的计算过程。

3 如何读取 Linear-11 数据

示例 1:读取输入电压(常规正数)

88h READ_VIN 读回 E860h

第一步:转二进制 E860h -> 1110 1000 0110 0000

第二步:拆分阶码 ($N$) 和尾数 ($Y$) 高 5 位是 $N$,低 11 位是 $Y$:

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
1 1 1 0 1 0 0 0 0 1 1 0 0 0 0 0

第三步:阶码 $N$ (5-bit 补码)

  • $N$ 二进制 11101,最高位为 1,负数。
  • 还原原码:减 1 得 11100,取反得 00011,即十进制 $3$。
  • 所以 $N = -3$。

第四步:尾数 $Y$ (11-bit 补码)

  • $Y$ 二进制 00001100000,最高位为 0,正数。
  • 直接转十进制:$2^6 + 2^5 = 64 + 32 = 96$。
  • 所以 $Y = 96$。

第五步:代入公式

\[X = 96 \times 2^{-3}\] \[X = 96 \times 0.125 = 12.0\]

输入电压为 12.0 V


示例 2:读取电流(负数尾数)

放电或反向电流时,89h READ_IIN 读回 F7FAh

第一步:转二进制并拆分 F7FAh -> 1111 0111 1111 1010

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 0

第二步:阶码 $N$

  • 二进制 11110,最高位为 1,负数。
  • 还原为十进制 $-2$。

第三步:尾数 $Y$

  • 二进制 11111111010,最高位为 1,负数。
  • 还原原码:减 1 得 11111111001,后 10 位取反得 00000000110,即十进制 $6$。
  • 所以 $Y = -6$。

第四步:代入公式

\[X = -6 \times 2^{-2}\] \[X = -6 \times 0.25 = -1.5\]

输入电流为 -1.5 A


4 c语言解析示例

READ_IIN (89h) 读回的 16 位数据(高低字节已组合成 uint16_t data)按以下方式还原:

uint16_t data = 0x...; // PMBus 总线读回的 16-bit 数据

// 1. 提取 11 位尾数 Y,符号扩展成 16 位带符号整数
int16_t Y = (data & 0x07FF);
if (Y > 0x03FF) {
    Y |= 0xF800; // 第 10 位为 1 表示负数,高 5 位补 1
}

// 2. 提取 5 位阶码 N,符号扩展成 16 位带符号整数
int16_t N = (data >> 11) & 0x001F;
if (N > 0x000F) {
    N |= 0xFFE0; // 第 4 位为 1 表示负数,高 11 位补 1
}

// 3. 计算实际值:N 为负数时相当于除以 2^|N|
double actual_value = (double)Y * pow(2, N);

c语言中,并没有求补码的这一步,为什么呢?只看到对提前出来的尾数和阶码分别做了符号扩展,然后直接计算了。

这是因为手动计算是我们人类将补码还原成十进制原码的过程,而 C 语言代码利用了计算机底层本就是使用补码运行的特性

在代码中,补码转换的步骤被“符号扩展(Sign Extension)”巧妙地替代了

在 C 语言中,int16_t 是一个 16 位的有符号整数。计算机在内存中存储 int16_t 类型的负数时,天然就使用补码形式。其实,计算机在内存中存储的任何数据,都是以补码形式存储的。对于正数,它的原码、反码、补码是完全相同的。而对于负数,它的补码是计算反码后加一得到的。

因此,我们要做的不是把 11 位的补码像人类手算那样“减 1 取反”变成原码,而是把一个 11 位的补码,变成一个 16 位的补码,然后直接扔给 CPU,CPU 自动就会把它识别成正确的十进制负数。

怎么把 11 位的负数补码变成 16 位的负数补码? 答案是:将高位全部用符号位(1)填满。这就是代码里按位或(|=)在做的事情。

4.1 解析示例(以刚才的 -1.5A 示例数据 F7FAh 为例)

假设 data = 0xF7FA (1111 0111 1111 1010)。

int16_t Y = (data & 0x07FF);
  • 0x07FF 的二进制是 0000 0111 1111 1111
  • 按位与之后,高 5 位被清零,只保留低 11 位。
  • 此时 Y 变成了 07FAh,即 0000 0111 1111 1010
  • 注意此时的危险: 对于 16 位的 int16_t 来说,最高位(第15位)是 0,所以 CPU 会把它当成一个正数(十进制的 2042),但这显然不对,它在 Linear-11 中明明是个负数!
if (Y > 0x03FF) {
    Y |= 0xF800;
}
  • 如何判断它是负数? 0x03FF0000 0011 1111 1111。如果 Y 大于它,意味着 Y 的第 10 位(Linear-11 尾数的符号位)必然是 1
  • 如何修正为 16 位负数? 0xF800 的二进制是 1111 1000 0000 0000
  • 执行 Y |= 0xF800 后,Y 变成了 1111 1111 1111 1010(十六进制 FFFAh)。

奇迹发生了: 在 C 语言的 int16_t 世界里,1111 1111 1111 1010 这个 16 位补码,正是十进制的 -6。我们不需要手动求原码,CPU 直接就懂了。

留下评论