Skip to content
入门#数制#补码#格雷码#编码#二进制

数制、编码、补码与格雷码 ​

数字电路里只有 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 内
有符号比较大小反了最高位是符号位还是数值位比较前统一为有符号或无符号

不算会摔在哪,三个带数字的代价:

  1. 符号扩展补零。 8 位补码 11110110 代表 −10,扩成 16 位时写成 00000000_11110110,值变成 +246。一个 8 位温度传感器接到 16 位 ADC 通道,零下温度会显示成 20 多度,误差 256 个 LSB。
  2. 溢出不判。 两个 8 位正数 100+100=200,超出 +127,硬件得到 11001000,按补码读是 −56。若这个值被当成温度,控制器会从“加热”切到“制冷”。
  3. 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-Magnitude01011101两个(0000、1000)加减要分开
反码 1's Complement01011010两个减法要循环进位
补码 2's Complement01011011唯一 0000加减共用一套加法器

补码求负的规则是按位取反再加 1:

-B=\overline{B}+1

以 4 位 B=0101 为例,\overline{0101}=1010,加 1 得 1011,即 −5。n 位补码的范围是

-2^{n-1}\le x\le 2^{n-1}-1

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
0000000
1001001
2010011
3011010
4100110
5101111
6110101
7111100

注意一个容易说错的点:“相邻只差一位”说的是汉明距离,不是数值差 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),不会出现第三个码字。多比特跳变的错误不是“某一位慢”,而是“多位不同步”;只允许一位变化,就把“多位不同步”从根上消掉了。

4 位二进制转格雷码:最高位直通,其余每位由相邻两位异或得到
4 位二进制转格雷码:最高位直通,其余每位由相邻两位异或得到

需要补充一个工程边界:格雷码消除的是多比特跳变歧义,不消除触点抖动。机械编码器在一位翻转时仍可能抖动多次,读到的仍是相邻码字,但可能来回跳;需要外加 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};缺点是很多十进制小数不能精确表示,而且“大数吃小数”。

4 位补码加减法器:SUB=1 时 B 逐位取反且 Cin=1,实现 A−B;溢出由最高两位进位异或得到
4 位补码加减法器:SUB=1 时 B 逐位取反且 Cin=1,实现 A−B;溢出由最高两位进位异或得到

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(单精度)3218231272^{-23}=1.1921\times10^{-7}
binary64(双精度)641115210232^{-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 校正:

\text{0x3C}+\text{0x06}=\text{0x42}

即十进制 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 单端 ADC12 位无符号0\sim4095,1 LSB =0.8057\ \text{mV}
双极性电流采样16 位补码-32768\sim32767,1 LSB =3.3/65536=50.35\ \mu\text{V}(按 3.3 V 满量程)
音频/振动 DSP16/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 定点数据类型的定标与饱和语义