PMBUS Linear-11数据编码解析
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 规定:
- Linear-16 把返回的整个 16 位都作为尾数 $Y$(通常是无符号数)。
- 阶码 $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;
}
- 如何判断它是负数?
0x03FF是0000 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 直接就懂了。
留下评论