数制、编码、补码与格雷码
数字电路里只有 0 和 1,但工程师要表达的是电压、温度、时间、地址、指令和负数。用一串 0/1 表示什么意思,就是编码。同一串 1111 0011 0011 0011,按无符号数读是 62259,按 16 位补码读是 −3277,按 Q1.15 读是 −0.100006,按 BCD 读则根本不是合法码。编码规则一旦选错,硬件再贵也会稳定地算出错误结果。
本章解决四件事:怎么在二进制、十六进制和十进制之间换算;怎么用补码表示负数并让同一套加法器做减法;怎么用定点数 Q 格式和 IEEE 754 浮点表示小数;怎么用格雷码让相邻状态只变一位。 这些约定是后面所有算术电路、计数器、总线和 DSP 算法的地基。读本章时可以对照逻辑门、布尔代数与卡诺图看门级实现,对照组合逻辑:译码、编码、选择与加法看加法器结构,对照触发器 RS / D / JK看这些编码怎样被时钟采样,时序后果则见时序分析:建立/保持时间与亚稳态。
本章约定:n 是位宽;b_i 是第 i 位的二进制数字,取 0 或 1;B_i、G_i 分别是二进制码和格雷码的第 i 位,从最低位 i=0 起算;2^n 是模数;X 是定点数的整数码,x 是它代表的实际值;Qm.n 表示 1 位符号位、m 位整数位、n 位小数位。所有算例都给出“公式 → 代入 → 结果 → 单位”,读者可以逐项复算。
1. 设计动机
场景一:12 位 ADC 的码值被当成有符号数。 3.3 V 基准、12 位单端 ADC 输出 0\sim4095,1 LSB 是 3.3/4096=0.8057\ \text{mV}。如果固件把码值直接减去 2048 偏置后塞进 12 位变量,0-2048 会回绕成 2048,负温度瞬间变成满量程正温度。正确做法是先扩成 16 位有符号数再减偏置,或者一开始就按无符号码值处理、只在显示层做偏移。
场景二:旋转编码器在 511→512 边界读错一格。 10 位绝对式编码器用普通二进制时,0111111111 到 1000000000 有 10 位同时翻转。PCB 走线长短不同、接收端阈值不同,采样瞬间可能读到 1111111111 或 0000000000——位置从 511 直接跳到 1023 或 0,电机换相电流会瞬间冲高。改用格雷码,相邻位置只变 1 位,采样到的只能是 511 或 512,误差不超过 1 个 LSB。
场景三:PID 积分器用 16 位 Q1.15 累加。 Q1.15 的 1 代表 2^{-15}=3.0518\times10^{-5},可表示范围只有 -1\sim+0.99997。误差 0.01 累加 100 次就到 1.0,再往上加会回绕成负数,积分器瞬间“反向饱和”,电机或加热器会朝反方向猛冲。工程上积分器至少用 32 位 Q 格式,并在输出端做饱和,而不是让中间结果回绕。
场景四:用浮点数逐次累加 0.1。 0.1 在二进制里是无限循环小数,binary32 只能存成 0.100000001490116,逐次加 10 次得到 1.0000001192;binary64 也会得到 0.9999999999999999。金额、计数、坐标这类“看起来是小数”的量,若直接比较 sum == 1.0,条件可能永远不成立。正确做法是定点整数(以分为单位)或带容差的比较。
| 你遇到的问题 | 先算/先查 | 判据 |
|---|---|---|
| 温度、电流出现“正负跳变” | 位宽、偏置、是否做了符号扩展 | 量程覆盖最坏工况,减法前已扩位 |
| 编码器过边界时位置乱跳 | 相邻码字的汉明距离 | 相邻状态只变 1 位 |
| 乘法结果小数点位不对 | Q 格式的 m、n 与中间位宽 | 中间积位宽 \ge 2n,移位和饱和齐全 |
| 浮点累加越加越偏 | ULP、累加次数、是否用补偿求和 | 误差小于业务允许的 1/2 LSB |
| BCD 显示数字错乱 | 是否做了加 6 校正 | 每个十进制位只在 0\sim9 内 |
| 有符号比较大小反了 | 最高位是符号位还是数值位 | 比较前统一为有符号或无符号 |
不算会摔在哪,三个带数字的代价:
- 符号扩展补零。 8 位补码
11110110代表 −10,扩成 16 位时写成00000000_11110110,值变成 +246。一个 8 位温度传感器接到 16 位 ADC 通道,零下温度会显示成 20 多度,误差 256 个 LSB。 - 溢出不判。 两个 8 位正数 100+100=200,超出 +127,硬件得到
11001000,按补码读是 −56。若这个值被当成温度,控制器会从“加热”切到“制冷”。 - Q15 乘法不移位。 0.75(码 24576)乘 0.5(码 16384)的整数积是 24576\times16384=402653184,直接截低 16 位得 0,正确结果 0.375 被丢成 0。乘法后必须右移 15 位(并做舍入/饱和),这是定点 DSP 最常见的错误之一。
2. 定义与直观
2.1 位权与进制
任何位置计数制都是“位权展开”。二进制每一位的权值是 2^i,所以
(1011)_2=1\cdot2^3+0\cdot2^2+1\cdot2^1+1\cdot2^0=8+0+2+1=(11)_{10}十六进制(Hex,Base-16)把 4 位二进制压成 1 位,0\sim9 后用 A~F 表示 10~15。0x2F 展开为 0010 1111,即 2\times16+15=47。十六进制的价值不是“另一种数学”,而是让 4 位一组的分组边界和人眼对齐:32 位寄存器写成 8 个十六进制数字,比 32 个 0/1 好读得多。
BCD(Binary-Coded Decimal)是另一种思路:每一位十进制数字单独用 4 位二进制表示。135 写成 0001 0011 0101,而不是二进制的 10000111。BCD 的优点是和数码管、键盘、RTC 日历直接对接;缺点是 4 位二进制能表示 16 种状态,BCD 只用了 10 种,剩下 6 个是非法码,而且加法结果超过 9 时必须加 6 校正。
2.2 补码:把减法变成加法
先看 4 位有符号数的三种表示:
| 表示法 | +5 | −5 | 零的个数 | 加减硬件 |
|---|---|---|---|---|
| 原码 Sign-Magnitude | 0101 | 1101 | 两个(0000、1000) | 加减要分开 |
| 反码 1's Complement | 0101 | 1010 | 两个 | 减法要循环进位 |
| 补码 2's Complement | 0101 | 1011 | 唯一 0000 | 加减共用一套加法器 |
补码求负的规则是按位取反再加 1:
-B=\overline{B}+1以 4 位 B=0101 为例,\overline{0101}=1010,加 1 得 1011,即 −5。n 位补码的范围是
4 位是 -8\sim+7,8 位是 -128\sim+127,16 位是 -32768\sim+32767。
把 4 位补码想成一个 16 格的钟面:0000 在 0 点,0111 在 7 点,再往前一格 1000 不是 8 点,而是 −8 点;继续往前 1111 是 −1 点。所谓“减 5”,就是“加 11”,因为从 0 点逆时针走 5 格和顺时针走 11 格落在同一格。钟面只有 16 格,走到 16 就自动回到 0,这就是模 2^n 运算。
类比的边界:钟面的“负数”是我们人为贴的标签,补码的负半轴也是;但钟面没有溢出标志,硬件加法器必须额外用最高两位进位判断“这一圈是不是走过头了”。
补码的本质是“模 2^n 计数”:把 -x 定义为 2^n-x。于是
a-b\equiv a+(2^n-b)\pmod{2^n}加法器天然做模 2^n 运算(最高位进位被丢弃),所以加减统一。这也是补码只有一个零、范围不对称(多一个最小负数 -2^{n-1})的原因。
2.3 格雷码:相邻状态只差一位
格雷码(Gray Code)是一种相邻码字汉明距离为 1 的二进制编码。3 位二进制与格雷码的对应关系是:
| 十进制 | 二进制 B | 格雷码 G |
|---|---|---|
| 0 | 000 | 000 |
| 1 | 001 | 001 |
| 2 | 010 | 011 |
| 3 | 011 | 010 |
| 4 | 100 | 110 |
| 5 | 101 | 111 |
| 6 | 110 | 101 |
| 7 | 111 | 100 |
注意一个容易说错的点:“相邻只差一位”说的是汉明距离,不是数值差 1。 格雷码序列 0,1,3,2,6,7,5,4 中,2→3 对应码字 011→010,数值只差 1;但 3→4 对应 010→110,数值从 2 跳到 6,差了 4。格雷码保证的是“每次只有一根码道翻转”,不是“每次加 1”。
为什么单比特变化能消除多比特毛刺?以二进制 3→4 为例:011→100 有 3 位同时翻转。假设三位翻转延迟分别是 1 ns、2 ns、3 ns,那么在 1~3 ns 的窗口里可能出现 111、110、101、100 等中间码,外部采样到的可能是 7、6、5,而不是 3 或 4。格雷码 3→4 是 010→110,只有最高位翻转;在翻转完成前后的任何时刻,读到的要么是 3(010),要么是 4(110),不会出现第三个码字。多比特跳变的错误不是“某一位慢”,而是“多位不同步”;只允许一位变化,就把“多位不同步”从根上消掉了。
需要补充一个工程边界:格雷码消除的是多比特跳变歧义,不消除触点抖动。机械编码器在一位翻转时仍可能抖动多次,读到的仍是相邻码字,但可能来回跳;需要外加 RC 滤波、施密特整形或数字去抖。格雷码也不适合直接做算术——0110 在格雷码里代表 4,在二进制里代表 6,直接送 BCD 译码必然错。
2.4 定点与浮点:小数点放在哪里
定点数(Fixed-Point)约定小数点的位置固定。Qm.n 格式用 1 位符号位、m 位整数位、n 位小数位,共 1+m+n 位。它的实际值是
x=\frac{X}{2^n}其中 X 是补码整数码,n 是小数位数。Q1.15 共 16 位,X 是 16 位补码,x=X/2^{15},范围 -1\sim+0.9999695,分辨率 2^{-15}=3.0518\times10^{-5}。Q3.12 的范围是 -8\sim+7.99976,分辨率 2^{-12}=2.4414\times10^{-4}。定点数的优点是加减法和整数完全一样、乘法只需移位、没有隐藏的舍入;缺点是要自己管理位宽、移位和饱和。
浮点数(Floating-Point)则把小数点“浮动”起来:用符号位 s、指数 e、尾数 f 表示
x=(-1)^s\cdot(1.f)\cdot2^{e-\text{bias}}IEEE 754 binary32 用 1 位符号、8 位指数(偏置 127)、23 位尾数;binary64 用 1 位符号、11 位指数(偏置 1023)、52 位尾数。浮点的优点是动态范围极大,同一格式既能表示 10^{-38} 也能表示 10^{38};缺点是很多十进制小数不能精确表示,而且“大数吃小数”。
3. 定量关系
3.1 进制转换
二进制定点整数转十进制用位权展开:
N=\sum_{i=0}^{n-1}b_i2^i十进制整数转二进制用“除 2 取余、从下往上读”。以 3277 为例:
\begin{aligned} 3277\div2&=1638\ \text{余}\ 1\\ 1638\div2&=819\ \text{余}\ 0\\ 819\div2&=409\ \text{余}\ 1\\ 409\div2&=204\ \text{余}\ 1\\ 204\div2&=102\ \text{余}\ 0\\ 102\div2&=51\ \text{余}\ 0\\ 51\div2&=25\ \text{余}\ 1\\ 25\div2&=12\ \text{余}\ 1\\ 12\div2&=6\ \text{余}\ 0\\ 6\div2&=3\ \text{余}\ 0\\ 3\div2&=1\ \text{余}\ 1\\ 1\div2&=0\ \text{余}\ 1 \end{aligned}从下往上读余数,得到 1100 1100 1101,即 0x0CCD。验算:0\times16^3+12\times16^2+12\times16+13=0+3072+192+13=3277。十六进制与二进制每 4 位一组对齐,所以 0x0CCD 直接写成 0000 1100 1100 1101,不需要再算。
BCD 与二进制不是一回事。 十进制 135 的二进制是 1000 0111(128+7),BCD 是 0001 0011 0101。两者长度和位权都不同,不能互换使用。
3.2 补码减法与溢出
补码减法用“加负数”实现:
A-B=A+\overline{B}+1其中 \overline{B} 是 B 的按位取反,+1 可以接到加法器的进位输入 C_{in}。硬件上每一位用一个异或门把 B_i 与 SUB 异或:SUB=0 时 B_i 原样送入,SUB=1 时取反,同时 C_{in}=1。这就是组合逻辑里加减法器的核心。
溢出的硬件判据:把 n 位加法的最高位进位记为 C_n,次高位进位记为 C_{n-1},则
\text{Overflow}=C_n\oplus C_{n-1}直观理解:符号位本来应该由两个操作数的符号和次高位进位决定;如果最高位进位和次高位进位不一样,说明符号位被“顶翻”了。等价的符号判据是:两个同号数相加,结果符号与之相反,即溢出;一正一负相加永远不会溢出。
符号扩展:把 n 位补码扩到 m>n 位,高位全部复制符号位,而不是补零。设原码为 x(无符号位模式),原值为 x-2^n;扩展后的位模式是 x+(2^m-2^n),按 m 位补码读为
x+(2^m-2^n)-2^m=x-2^n值不变。以 −5 的 4 位 1011 为例,扩成 8 位是 11111011,扩成 16 位是 1111111111111011;写成 00001011 就变成 +11。
最小负数的取负:对 x=-2^{n-1} 做“取反加 1”,硬件会得到 1000...0,也就是它自己;但数学上 -(-2^{n-1})=+2^{n-1},这个正数在 n 位补码里不可表示。所以这不是“结果仍成立”,而是取负溢出、结果回绕。固件里对最小值取绝对值或取负,必须单独判饱和。
3.3 Q 格式的定标
Q 格式的加减法可以直接对整数码做,前提是小数点位置相同:
x_A\pm x_B=\frac{X_A\pm X_B}{2^n}但两个 Q1.15 码相加可能超出 16 位范围,所以中间结果要用更宽的位宽,或者在输出端饱和。
乘法要小心。两个 Q1.15 数相乘:
x_Ax_B=\frac{X_AX_B}{2^{15}\cdot2^{15}}=\frac{X_AX_B}{2^{30}}整数积 X_AX_B 是 Q2.30 格式(32 位),要变回 Q1.15,必须右移 15 位:
X_P=\operatorname{round}\left(\frac{X_AX_B}{2^{15}}\right)右移会丢低位,通常加 2^{14} 再右移做“四舍五入”,然后判断是否超出 [-32768,32767],超出就饱和到端点。不移位、不舍入、不饱和是定点乘法的三大坑。
3.4 IEEE 754 的字段与精度
| 格式 | 总位数 | 符号 | 指数 | 尾数 | 偏置 | 机器精度 \varepsilon |
|---|---|---|---|---|---|---|
| binary32(单精度) | 32 | 1 | 8 | 23 | 127 | 2^{-23}=1.1921\times10^{-7} |
| binary64(双精度) | 64 | 1 | 11 | 52 | 1023 | 2^{-52}=2.2204\times10^{-16} |
规格化数的值是 (-1)^s(1.f)2^{e-\text{bias}},其中 1.f 是“隐含前导 1”的尾数。指数全 0 时是次正规数(subnormal),值为 (-1)^s(0.f)2^{1-\text{bias}};指数全 1 时表示无穷大或 NaN。
0.1 为什么不能精确表示? 0.1 的二进制是 0.000110011001100\ldots,即 1.100110011\ldots\times2^{-4},循环节 1100 无限重复。binary32 只能保留 23 位尾数,舍入后的值是 0.100000001490116119384765625,误差 1.4901\times10^{-9};binary64 保留 52 位,误差约 5.55\times10^{-18}。误差本身很小,但累加会放大。
ULP(Unit in the Last Place) 是相邻两个可表示数之间的距离。在 [1,2) 区间,binary32 的 ULP 是 2^{-23}=1.1921\times10^{-7}。给 1.0 加 10^{-8},因为 10^{-8} 小于半个 ULP(5.96\times10^{-8}),舍入后仍是 1.0——小到一定程度的值会被大数“吃掉”。这就是为什么浮点求和要按绝对值从小到大排、用 Kahan 补偿求和,或者干脆用定点整数。
3.5 格雷码转换
二进制转格雷码:
G_i=B_{i+1}\oplus B_i,\qquad G_{n-1}=B_{n-1}即最高位直通,其余每一位等于“当前位与高一位的异或”。格雷码转二进制:
B_{n-1}=G_{n-1},\qquad B_i=B_{i+1}\oplus G_i从高位向低位逐位异或还原。以 4 位 B=1011 为例:G_3=1,G_2=1\oplus0=1,G_1=0\oplus1=1,G_0=1\oplus1=0,所以 G=1110。反变换:B_3=1,B_2=1\oplus1=0,B_1=0\oplus1=1,B_0=1\oplus0=1,回到 1011。
4. 典型计算
4.1 算例一:−3277 的 16 位补码
需求:把十进制 −3277 写成 16 位补码,并验证。
步骤 1:求绝对值的二进制。 3277 反复除以 2 取余,再从下往上读:
3277=12\times256+13\times16+13=\text{0x0CCD}即 0000 1100 1100 1101。
步骤 2:按位取反。 16 位全取反:
\overline{\text{0x0CCD}}=\text{0xF332}即 1111 0011 0011 0010。
步骤 3:加 1。
\text{0xF332}+1=\text{0xF333}所以 -3277 的 16 位补码是 0xF333,二进制 1111 0011 0011 0011。
步骤 4:验算。 按无符号数读:
15\times4096+3\times256+3\times16+3=61440+768+48+3=62259再按 16 位补码还原:
62259-2^{16}=62259-65536=-3277结果与需求一致。范围检查:-32768\le-3277\le32767,没有溢出。若扩成 32 位,符号扩展为 0xFFFFF333,按无符号读是 4294964019,减 2^{32}=4294967296 仍得 −3277。
4.2 算例二:用补码做 12345−15622
需求:用 16 位补码加法器计算 12345-15622,验证结果并判断溢出。
步骤 1:写两个数的补码。
12345=\text{0x3039},\qquad 15622=\text{0x3D06}步骤 2:求减数的补码。
\overline{\text{0x3D06}}=\text{0xC2F9},\qquad \text{0xC2F9}+1=\text{0xC2FA}步骤 3:相加。
\text{0x3039}+\text{0xC2FA}=\text{0xF333}最高位没有产生第 17 位进位,所以 C_{16}=0;符号位相加时也没有次高位进位,C_{15}=0。溢出标志 C_{16}\oplus C_{15}=0,不溢出。
步骤 4:验算。 0xF333 按补码读是 −3277,而 12345-15622=-3277,一致。也可以直接看十进制:12345+49914=62259,62259-65536=-3277。
结论:一次减法只花了“取反 + 进位输入置 1 + 一次加法”,不需要单独的减法器。这就是补码对硬件的最大红利。
8 位算例:100+100。100=\text{0x64},\text{0x64}+\text{0x64}=\text{0xC8};最高位进位 C_8=0,次高位进位 C_7=1,溢出标志 C_8\oplus C_7=1。0xC8 按 8 位补码读是 -56,而真实结果 +200 超出 8 位补码上限 +127。再做 50-80:50=\text{0x32},-80=\text{0xB0},\text{0x32}+\text{0xB0}=\text{0xE2};C_8=0、C_7=0,不溢出;0xE2 按补码读是 -30,正确。
4.3 算例三:Q1.15 的 1 与乘法溢出
问题一:Q1.15 的整数码 1 代表多少?
x=\frac{X}{2^{15}}=\frac{1}{32768}=3.0517578125\times10^{-5}即约 30.5\ \mu\text{V}(若满量程为 1 V),或 100.7\ \mu\text{V}(若满量程为 3.3 V,3.3/32768=1.007\times10^{-4} V)。Q1.15 的最大码 32767 代表 0.9999695,最小码 −32768 代表 −1.0,所以它能表示 [-1,+0.9999695],不能表示 +1。
问题二:0.75×0.5。
0.75\rightarrow24576\ (\text{0x6000}),\qquad0.5\rightarrow16384\ (\text{0x4000})24576\times16384=402653184=\text{0x18000000}这是 Q2.30 的 0.375。右移 15 位:
402653184\gg15=12288=\text{0x3000}12288/32768=0.375,结果正确。
问题三:−1.0×−1.0 的溢出。
-1.0\rightarrow-32768\ (\text{0x8000})(-32768)\times(-32768)=1073741824=\text{0x40000000}这是 Q2.30 的 +1.0。右移 15 位得 32768,即 0x8000;按 16 位补码读却是 −32768(−1.0),符号完全反了。原因是 +1.0 超出 Q1.15 的表示范围,移位后最高位被当成符号位。正确做法是饱和到 0x7FFF(+0.9999695),并置溢出标志。这个例子说明:定点乘法的溢出不一定来自“乘数很大”,两个绝对值不超过 1 的数相乘也可能得到 +1。
问题四:0.99997×0.99997。
32767\times32767=1073676289\lfloor1073676289/32768\rfloor=32766,\qquad 32766/32768=0.9999389648真实乘积约 0.9999389658,精确码是 32766.00003,截断到 32766,差约 3.05\times10^{-5} 个 LSB(9.31\times10^{-10}),不是 1 个 LSB。若加 2^{14} 再右移做舍入,仍得 32766(因为余数 1 小于半个 LSB),误差不可避免;要更高精度只能保留 Q2.30 中间结果或改用更宽位宽。
4.4 算例四:BCD 校正与格雷码转换
BCD 加法 27+15。
\text{0x27}+\text{0x15}=\text{0x3C}低 4 位是 1100(12),大于 9,必须加 6 校正:
即十进制 42,正确。再看 9+8:
\text{0x09}+\text{0x08}=\text{0x11}低 4 位结果是 1,但产生了半字节进位;BCD 规则是“半字节和大于 9 或产生进位就加 6”,所以
\text{0x11}+\text{0x06}=\text{0x17}即 17,正确。
格雷码转换。 B=1011:
G_3=1,\quad G_2=1\oplus0=1,\quad G_1=0\oplus1=1,\quad G_0=1\oplus1=0结果 G=1110。反变换:
B_3=1,\quad B_2=1\oplus1=0,\quad B_1=0\oplus1=1,\quad B_0=1\oplus0=1回到 B=1011。这两个方向都只用到异或,硬件延迟为 \log_2 n 级(树形)或 n 级(串行)。
4.5 算例五:0.1 的浮点误差
binary32 的 0.1。
\text{0x3DCCCCCD}\rightarrow0.100000001490116119384765625与真实 0.1 的误差:
1.4901161\times10^{-9}相对误差 1.49\times10^{-8}。
逐次累加 10 次(每次舍入到 binary32):
0.1+0.1+\cdots+0.1\approx1.0000001192误差 1.192\times10^{-7},正好约 1 个 ULP(在 1.0 处 ULP 为 1.1921\times10^{-7})。如果改用 binary64 逐次累加,结果是 0.9999999999999999,误差 1.11\times10^{-16}。
大数吃小数。 binary32 下:
1.0+10^{-8}=1.0因为 10^{-8} 小于半个 ULP(5.96\times10^{-8})。在 1.0 附近做 1 亿次“加 10^{-8}”的累加,结果仍然是 1.0,而不是 2.0。这不是精度“差一点”,而是增量被完全丢弃。
5. 工程含义
位宽与量程。 选位宽先定量程,再定编码:
| 场景 | 典型位宽 | 编码 | 范围/分辨率 |
|---|---|---|---|
| 3.3 V 单端 ADC | 12 位 | 无符号 | 0\sim4095,1 LSB =0.8057\ \text{mV} |
| 双极性电流采样 | 16 位 | 补码 | -32768\sim32767,1 LSB =3.3/65536=50.35\ \mu\text{V}(按 3.3 V 满量程) |
| 音频/振动 DSP | 16/32 位 | Q1.15/Q1.31 | [-1,1),分辨率 3.05\times10^{-5} / 4.66\times10^{-10} |
| RTC 日历 | 每字节 4 位 | BCD | 直接对应十进制数字 |
| 旋转编码器 | 8~12 位 | 格雷码 | 每圈 2^n 个位置,10 位为 1024 格,360/1024=0.3516^\circ |
| 科学计算 | 32/64 位 | IEEE 754 | 动态范围大,精度约 7/16 位十进制 |
BCD 用在哪。 BCD 只适合“人机接口”和“十进制语义”场景:RTC 的秒/分/时寄存器、数码管显示、拨码开关输入、电表读数。它不适合乘除、滤波、PID——BCD 没有简单的加权乘法规则,必须先转二进制。用 BCD 做 15×2:
\text{BCD }15\times2=\text{BCD }30而把 0001 0101 当二进制乘 2 得 0010 1010,即十进制 42,不是 30。这个错误在电表、计价器里会直接变成金额错误。
格雷码用在哪。 机械/光学旋转编码器、绝对位置反馈、跨时钟域异步 FIFO 的读写指针、状态机中“相邻状态只允许一位变化”的场合。普通二进制计数器不需要格雷码;它的计数序列本身就是二进制,格雷码只用于“被外部异步采样”的边界。异步 FIFO 用格雷码指针的原因是:写指针跨到读时钟域时,多比特同时变化会被采样成任意组合,格雷码把“任意组合”压缩成“旧值或新值”,最多差一格。
定点还是浮点。 定点(Q 格式)的优点是确定性:同样的输入永远得到同样的输出,没有隐藏的舍入模式,适合电机控制、音频、传感器融合等硬实时场景;缺点是要手动管理位宽、移位和饱和。浮点的优点是动态范围大、编程简单,适合科学计算、图形、机器学习训练;缺点是累加误差、非结合性((a+b)+c\ne a+(b+c))和比较陷阱。能用定点且量程可控时优先定点;需要 10^{-30}\sim10^{30} 的动态范围时才用浮点。
“典型量级,以数据手册为准”。 本章出现的 74HC 门延迟、ADC 的 LSB、FPU 的吞吐率都是数量级示例。实际选型必须查数据手册的测试条件:同样的 12 位 ADC,采样率、基准电压、输入范围不同,LSB 和有效位数都不同;同样的 Q15 乘法,DSP 有没有单周期 MAC、有没有饱和指令,结果差很多。
6. 常见坑
- 现象:8 位温度传感器 −10(
11110110)接到 16 位通道后显示 +246;负温度全部变成正温度。 - 原因:扩展时把高 8 位补了 0,而不是复制符号位 1。
- 解决:先判断原数最高位;为 1 时高 8 位全填 1,为 0 时全填 0。C 语言里先转
int8_t再赋给int16_t,不要用无符号中间变量。
- 现象:对 −32768 取绝对值得到 −32768,后续比较、开方、PID 全部反向。
- 原因:+32768 超出 16 位补码范围,取负运算回绕到自身;这是溢出,不是“公式仍成立”。
- 解决:取负/取绝对值前先判 x=-2^{n-1},命中就饱和到 2^{n-1}-1 并置溢出标志,或直接用更宽位宽。
- 现象:0.75×0.5 得到 0,或 −1×−1 得到 −1;乘法结果“符号对了但数值全错”。
- 原因:两个 Q1.15 相乘得到 Q2.30,直接截低 16 位等于把小数点当成了整数;不饱和则 +1.0 回绕成 −1.0。
- 解决:中间积用 32 位;右移 15 位并做舍入;结果超出 [-32768,32767] 时饱和,同时置溢出标志。
- 现象:27+15 显示 3C(或 42 的十六进制误显示),9+8 显示 11 而不是 17。
- 原因:4 位二进制加法器把
1010~1111当合法数,BCD 只允许0000~1001。 - 解决:每个半字节相加后,若和大于 9 或产生半字节进位,加
0110;多位 BCD 要按半字节串行进位。
- 现象:
0.1+0.2 != 0.3;累加 10 次 0.1 不等于 1.0;金额差 1 分。 - 原因:十进制小数在二进制中多为无限循环,舍入误差在累加中积累;浮点加法不满足结合律。
- 解决:金额用整数分/厘;物理量用定点;必须用浮点时用容差比较、Kahan 补偿求和或按绝对值排序后求和。
- 现象:编码器输出
0110,程序按二进制读成 6,实际位置是 4;位置曲线出现“折返”。 - 原因:格雷码不是加权二进制,
0110的位权不等于 4+2。 - 解决:先做“格雷→二进制”异或链还原,再参与计算;或者查表。不要把格雷码直接送 BCD/七段译码。
7. 自测题
把 −3277 写成 16 位补码,并说明每一步的依据。
3277 = 0x0CCD;按位取反得 0xF332;加 1 得 0xF333。验算:0xF333 无符号读为 62259,减 2^{16}=65536 得 −3277。范围 -32768\sim32767 内,不溢出。
4 位补码计算 7+1,结果是什么?C3、C4 各是多少?溢出吗?
0111+0001=1000。逐位算:bit0 1+1=0 进位 1;bit1 1+0+1=0 进位 1;bit2 1+0+1=0 进位 1;bit3 0+0+1=1 进位 0。所以 C_3=1(进入符号位的进位)、C_4=0(符号位向外的进位),C_4\oplus C_3=1,溢出。结果 1000 按补码是 −8,而 7+1=+8 超出 4 位补码上限 +7。
Q1.15 的整数码 1、32767、−32768 各代表多少?0.5×0.5 的码值怎么算?
1/32768=3.0518\times10^{-5};32767/32768=0.9999695;-32768/32768=-1.0。0.5 的码是 16384,16384\times16384=268435456,右移 15 位得 8192,8192/32768=0.25。
为什么 0.1 在 binary32 中不等于 0.1?逐次加 10 次会得到多少?
0.1 的二进制是无限循环的 1.100110011\ldots\times2^{-4},23 位尾数只能舍入,binary32 存成 0.100000001490116,误差 1.49\times10^{-9}。逐次加 10 次(每次舍入)得约 1.0000001192,误差约 1 个 ULP。用 binary64 逐次加则得 0.9999999999999999。
二进制 1011 的格雷码是什么?格雷码 1110 还原成二进制是什么?
G_3=1,G_2=1\oplus0=1,G_1=0\oplus1=1,G_0=1\oplus1=0,所以 G=1110。反变换:B_3=1,B_2=1\oplus1=0,B_1=0\oplus1=1,B_0=1\oplus0=1,得 B=1011。
BCD 计算 9+8,为什么结果是 0x17 而不是 0x11?
0x09+0x08=0x11,低半字节产生了进位(9+8=17,超过 15 中的 9),按 BCD 规则要加 6 校正:0x11+0x06=0x17。如果不校正,显示会读成 11,少算 6。
8. 参考资料
- M. Morris Mano, Michael D. Ciletti, Digital Design (6th ed.), Chapter 1 — §1.5 Complements of Numbers、§1.6 Signed Binary Numbers、§1.7 Binary Codes
- IEEE Std 754-2019, IEEE Standard for Floating-Point Arithmetic — Clause 3 Formats、Clause 4 Rounding、Clause 5 Operations
- IEEE Std 1364-2005, IEEE Standard for Verilog Hardware Description Language — Clause 4 Data Types
- David A. Patterson, John L. Hennessy, Computer Organization and Design (RISC-V ed.), Chapter 3 Arithmetic for Computers — §3.2 Addition and Subtraction、§3.5 Floating Point
- Frank Gray, US Patent 2,632,058, Pulse Code Communication (1953) — 格雷码的原始专利
- Texas Instruments, C28x IQmath Library User's Guide — Q 格式定标、乘法移位与饱和处理
- ARM, CMSIS-DSP Reference — Q15、Q31 定点数据类型的定标与饱和语义