在“青云仓”快递分拣中心,三位老师傅各有一套独门编码手艺。仓管老王负责给货箱贴标签,他把一个两位数拆成两个单独的数字,每个数字用 4 格小标签编码,分拣员一眼就能读回十进制——这就是 BCD 码。电梯维修工小李发现,楼层从 3 到 4 切换时多位指示灯同时闪烁会造成瞬间乱码,于是他重新设计按钮编码,让相邻楼层只变化一个灯——这就是格雷码。质检员小张在每个包裹上额外贴一张“1 的个数奇偶”的核对贴纸,收货方数一数 1 的个数,就能知道运输途中数据是否被篡改——这就是奇偶校验码。三种编码分别回答了数字世界的三个根本问题:十进制数字如何在二进制世界中被直观表达?状态切换时如何把误读风险降到最低?数据被噪声破坏时如何用最小代价发现异常?本文用这个故事带你理解这三种编码的原理与应用。
对 AI 从业者而言,这些底层编码是读懂芯片数据手册、调试通信协议、设计数据采集管线的基本功——你的模型再好,硬件层读错一个 bit,整个推理链都会崩塌。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
BCD码基础
青云仓里最繁忙的就是仓管老王。他的工作台前摆着一排排纸箱格子,每个格子固定有 4 个小窗口,窗口上方印着 8、4、2、1 四个数字。老王要标记“47 号货箱”时,他不会费劲把 47 整个换算成二进制,而是把 47 拆成“4”和“7”两个字,分别放进高位格和低位格。在“4”的格子里,他点亮权重 4 的那扇窗;在“7”的格子里,他点亮权重 4、2、1 的三扇窗。分拣员路过一看,立刻读出“四十七”,完全不需要心算二进制。
这套“每位十进制数字单独用 4 位二进制编码”的方法,就是 BCD 码(Binary-Coded Decimal,二进制编码的十进制数)。因为最常用的 4 位权重是 8、4、2、1,所以它又叫 8421 码。4 位二进制本来能表示 0 到 15 共 16 个状态,BCD 码只使用 0 到 9 这 10 个状态,剩下的 1010 到 1111 是“废码”。一旦运算结果落进废码区,就说明出现了错误或需要修正。
老王的标签系统虽然比纯二进制多用了一点纸箱,却让整个分拣链路的可读性大幅提升。BCD 码的工程哲学正是如此:用约 20% 的存储空间冗余,换来十进制显示、电路译码和人类调试的极大便利。
- 拆分十进制位把 47 拆成十位 4 和个位 7,每位单独处理。
- 编码十位 4按权重 8、4、2、1 展开,4 只需用到权重 4 这一位。
- 编码个位 77 等于权重 4 加 2 加 1,三窗齐亮。
- 拼接两位按“先高位后低位”的顺序把两组 4 位码连起来。
BCD码运算与转换
货箱不仅要贴标签,还要合并。老王把 7 号箱和 5 号箱的货物合并成一个新箱,按十进制心算是“7 + 5 = 12”。可标签纸箱每个格子最多认到 9,11、12 这些结果在格子里根本放不下。老王挠头:标签系统自己不会“满十进一”吗?答案是:会,但必须按 BCD 的规矩来。
两个 BCD 数直接按二进制相加时,可能得到大于 9 的结果,或者直接产生进位。因为 BCD 只承认 0 到 9,一旦结果越过 9,就进入废码区,必须进行“加 6 修正”。为什么是加 6?因为 4 位二进制满 16 才进位,而 BCD 满 10 就要进位,中间差了 6。补上这个差,低 4 位就能回到合法区间,同时向更高位正确进位。
老王后来把这条规则写进了仓库手册:任何一位 BCD 相加,超过 9 或产生进位,就当场补 6。这个“人工进位”看起来笨拙,却在所有十进制显示设备里默默运行——电子表、计算器、收银机,都是靠这一补,才让人类看到的数字永远不错位。
- 二进制硬加把 7 的 BCD 码 0111 与 5 的 BCD 码 0101 直接相加。
- 检查越界结果 1100 大于 BCD 最大合法码 1001(即 9),进入废码区。
- 补 6 修正给结果加上 0110(即 6),让低 4 位回到 0 到 9 的范围,同时产生进位。
- 解释结果进位 1 作为高一位的“1 个十”,低 4 位 0010 就是数字 2,合起来是 12。
格雷码原理
小李是青云仓的电梯维修工。他修过最诡异的一个故障是:电梯明明从 3 楼到 4 楼,楼层显示屏却会瞬间闪出一个“0 楼”甚至“8 楼”。他蹲在控制柜前一查,发现问题出在编码方式上:二进制从 3(011)到 4(100),三个指示灯同时变化,继电器动作总有先后,中途就会拼出 000(0 楼)、111(8 楼)这样的乱码状态。
小李的解决思路不是让继电器变快,而是换一种编码:让相邻楼层之间永远只变化一位。这种“相邻码距恒为 1”的编码,就是格雷码(Gray Code),由贝尔实验室的 Frank Gray 在 1953 年提出。格雷码构造时像照镜子一样反射:先写 0、1,然后以中间为镜,镜中头部补 0,镜像部分补 1,不断递归生成。3 位格雷码序列是 000、001、011、010、110、111、101、100——每一步确实只翻一位。
小李把电梯按钮全部换成格雷码后,楼层切换瞬间只有一个灯在动,显示屏再也没有闪过“幽灵楼层”。格雷码的启示是:当变化不可能同时发生时,就让变化的内容尽量少——这比任何昂贵的电路优化都更彻底。
- 右移一位把二进制数 B 整体向右移一位,最高位补 0,得到 B>>1。
- 逐位异或相同位取 0,不同位取 1,即 G_i = B_i XOR B_{i+1}。
- 保留最高位最高位 G_{n-1} 直接等于 B_{n-1},不参与异或。
- 验证相邻性把 0 到 15 全部转换后,任意相邻数字的格雷码只差 1 位。
格雷码典型应用
小李的维修手册里还有一章:电梯井里的位置传感器。轿厢要在几十米高的井道里精确停靠,靠的是井道顶部的一个旋转码盘。码盘上有 n 圈黑白条纹,光敏元件同时读数。如果码盘用的是普通二进制,轿厢一震动,多个读数同时跳变,控制芯片瞬间读到完全错误的位置,轻则急停,重则冲顶。
格雷码正是为这类场景而生的。把码盘按格雷码顺序刻上条纹,每转过一个扇区,码盘输出只变化一位。即使读数刚好发生在翻转瞬间,最多也只差一格,绝不会跳到毫不相关的角度。现代工业旋转编码器、机器人关节角度传感器、电梯楼层计数器都采用这种方案。在芯片设计里,格雷码还有一个著名用途:异步 FIFO 的读写指针跨时钟域传递时,用格雷码指针可以避免多位同时变化带来的亚稳态采样错误。
小李把 17 位格雷码码盘装进轿厢位置传感器后,电梯运行曲线变得丝滑。格雷码在应用中的真谛是:用“每次只变一点”的代价,换回“任何时刻读数都可信”的确定性。这对高速运动的控制系统而言,比什么都重要。
- 格雷码数量n 位格雷码共有 2^n 个互不相同的码字。
- 划分圆周把 360 度的圆盘按码字数量等分。
- 计算分辨率相除得到每个扇区对应约 1.4 度。
- 工程含义位数每加 1,分辨率翻倍,码盘条纹密度也翻倍。
奇偶校验原理
质检员小张站在传送带末端,手里拿着一沓“奇偶贴纸”。他和发货端约定:每个包裹的标签上,“1”的个数加起来必须是偶数。发货前,如果数据位里“1”的个数是奇数,就在末尾的贴纸上写 1;如果是偶数,就写 0。包裹到站,小张只要把所有位(含贴纸位)的“1”数一遍:是偶数,放行;是奇数,立刻拦截。
这就是奇偶校验码(Parity Check Code)的工作方式。它把一个校验位附加在数据之后,使整个码字中“1”的个数满足约定的奇偶性。接收方重新统计 1 的个数,就能发现传输过程中是否发生了奇数个比特翻转——尤其是最常见的单比特翻转。校验位的计算可以用异或极简实现:偶校验时,校验位等于所有数据位的异或结果。
小张的贴纸法虽然只花 1 比特的代价,却是世界上最广泛使用的检错手段。UART 串口通信、PCI 总线、动态存储器的行校验,到处都有它的身影。它回答的问题只有一个——“1 的个数对不对”,但正是这一个问题,挡住了无数次数据被噪声篡改的灾难。
- 列出数据位取数据帧的所有比特,例如 1011。
- 逐位异或从高位到低位依次异或,相同为 0,不同为 1。
- 得到校验位最终异或结果是 1,说明数据位有奇数个 1。
- 拼接发送把校验位附加在数据末尾,构成新码字 1011|1。
校验局限与改进
有一天,小张收到一个包裹,奇偶贴纸显示“偶数”,校验通过。可打开一看,有两件货物被调包了。他立刻明白:两件货物同时被换,一个“1”变成“0”、另一个“0”变成“1”,总数奇偶性没变,贴纸自然发现不了。小张感叹:贴纸只能发现“一个或奇数个”问题,遇上“偶数个”差错就彻底失灵了。
这正是奇偶校验的固有局限。它只需 1 位冗余,也只能输出 1 比特的结论“对或错”,因此既无法检测偶数位翻转,也无法定位出错位置,更谈不上纠错。为了对抗更复杂的噪声环境,工程界发展了两类更强的校验技术。海明码(Hamming Code)布置多个校验位,让每个校验位覆盖一组不同位置的比特,不仅能用校验结果组合定位出错的位置,还能实现 1 比特自动纠错。CRC(循环冗余校验)则把一个数据帧看作多项式,用生成多项式做模 2 除法,得到的余数作为校验码,检错能力远强于奇偶校验,以太网和硬盘存储都在使用。
小张后来把防护体系升级成“组合拳”:普通包裹贴奇偶贴纸,贵重包裹走 CRC 装甲,医疗包加海明码医疗兵。三种校验各有定位——轻量、重型、可修复。选择哪种,取决于你要防的是什么样的敌人,以及数据坏了之后,你能不能承担。
- 统计状态数k 个校验位可以产生 2^k 种组合。
- 位置总数n 个数据位加 k 个校验位,共有 n + k 个比特可能出错。
- 再加无错状态除了“某一位出错”的 n+k 种情况,还要表示“一切正常”这一种。
- 代入验证8 位数据选 4 个校验位,16 >= 13 成立;3 个校验位则 7 < 12 不够用。
AI 应用场景
嵌入式设备的BCD时间与金额显示
阶段:数据采集与预处理(硬件数据表示与校准)
解决的问题:传统收银、计时设备需要把二进制数转成人类可读的十进制显示,转换电路复杂;使用二进制小数存储金额还会带来 0.1 这类浮点误差,在金融场景中不可接受。
具体案例:智能电子秤的金额计算与显示。单片机直接以 BCD 格式保存单价与重量寄存器,数码管驱动芯片逐位译码点量,金额计算全程无浮点误差;调试时打印寄存器值,工程师一眼就能读出当前金额,无需二次换算。
机器人关节的格雷码角度反馈
阶段:感知与定位(关节角度实时反馈)
解决的问题:机械臂关节在高速运动时,普通二进制编码器的多位输出同时跳变,控制器可能瞬间读到错误角度,导致轨迹规划出现抖动甚至飞车。
具体案例:某六轴协作机械臂采用 17 位光电格雷码编码器,关节角度分辨率达到约 0.0027 度。由于格雷码任意时刻采样最多偏差一个最小步长,视觉伺服模块收到的角度永远“可解释”,机械臂才能完成平滑的力控装配动作。
IoT数据链路的奇偶校验把关
阶段:数据传输与数据清洗(喂给模型前的质量把关)
解决的问题:物联网传感器通过 LoRa/ZigBee 无线链路将数据送到云端,射频噪声会造成零星比特翻转;若不检错,坏数据会直接污染时序数据库,进而误导 AI 模型的训练与预测。
具体案例:某智慧农业大棚项目在每帧温湿度数据尾部附加奇偶校验位,网关先丢弃校验失败的帧,再写入数据湖。实施后坏帧率从 5% 降到 0.1%,后续 LSTM 温度预测模型的预测误差下降了 18%。
Python 演示
# -*- coding: utf-8 -*-"""演示 BCD 码, 格雷码, 奇偶校验码的编解码与检错.""" def to_bcd(n): """十进制整数转 BCD 码, 例如 47 -> '0100 0111'""" return ' '.join(f"{int(ch):04b}" for ch in str(n)) def bcd_to_int(bcd): """BCD 码字符串转十进制整数, 例如 '0100 0111' -> 47""" value = 0 for part in bcd.split(): value = value * 10 + int(part, 2) return value def to_gray(n): """二进制整数转格雷码: G = B ^ (B >> 1)""" return n ^ (n >> 1) def from_gray(g): """格雷码转二进制整数: 逐位异或还原""" b = 0 while g: b ^= g g >>= 1 return b def parity_bit(bits, even=True): """按偶校验或奇校验生成 1 位校验位""" ones = bits.count('1') return ones % 2 if even else 1 - (ones % 2) def check_parity(bits_with_parity, even=True): """验证带校验位的码流是否通过校验""" total = bits_with_parity.count('1') return total % 2 == (0 if even else 1) if __name__ == '__main__': print('=== 0~15 的三种编码 ===') for n in range(16): bin5 = f"{n:05b}" p = parity_bit(bin5, even=True) print(f"{n:2d} | BCD:{to_bcd(n)} | 二进制:{bin5} | 格雷码:{to_gray(n):05b} | 偶校验位:{p}") print() print('=== 奇偶校验检错演示 ===') data = '10110' p = parity_bit(data, even=True) frame = data + str(p) print(f'原始帧: {frame} 校验通过? {check_parity(frame, even=True)}') flipped = frame[:2] + ('1' if frame[2] == '0' else '0') + frame[3:] print(f'第3位翻转: {flipped} 校验通过? {check_parity(flipped, even=True)}') print() print('=== 格雷码往返转换 ===') for g in [0, 1, 3, 7, 15, 10]: print(f'格雷码 {g:04b} -> 二进制 {from_gray(g):04b}')
return ' '.join(f"{int(ch):04b}" for ch in str(n))把十进制数的每一位字符单独格式化成 4 位二进制,再空格拼接,实现 BCD 编码。value = value * 10 + int(part, 2)逐个还原 4 位组为十进制位,并按十进制权重累加,实现 BCD 解码。return n ^ (n >> 1)二进制转格雷码的核心异或公式,与正文公式完全一致。b ^= g; g >>= 1格雷码转二进制时,反复让当前值与右移后的 g 异或,逐位还原原码。return ones % 2 if even else 1 - (ones % 2)偶校验时 1 的个数为奇数就补 1;奇校验时正好相反。flipped = frame[:2] + ('1' if frame[2] == '0' else '0') + frame[3:]人为翻转第 3 位,模拟传输过程中的单比特错误,验证校验函数能否捕获。
常见误区
误区BCD 码和二进制数都是 0/1 串,所以 BCD 码就是把十进制数整个转成二进制。47 的 BCD 码应该就是它的二进制形式 101111。
正解BCD 码是“每位十进制数字”单独转成 4 位二进制,47 要拆成 4 和 7,分别编码为 0100 和 0111,拼接得到 0100 0111。而整个 47 转成二进制才是 101111。两者看似都是 0 和 1,本质完全不同:前者是“十进制位独立编码”,后者是“整个数值按二进制展开”。
为何常见:因为 0 到 9 的 BCD 编码与二进制形式完全一样,初学者很容易以偏概全,以为所有情况都相同。
误区格雷码也是 0 和 1 组成的,肯定可以直接比大小吧?比如 1011 的数值肯定大于 1000。
正解格雷码的码序是为“相邻只变一位”而设计的,它的码值大小与十进制大小没有单调对应关系。4 位格雷码 1000 代表十进制的 15,而 0000 代表 0,前者的二进制数值虽然大,但它们在码序中分别是末端和起点。需要比较大小时必须先把格雷码转回二进制。
为何常见:人看到 0/1 串的第一反应是“这就是二进制数”,而格雷码恰好长着二进制数的样子。
误区加了校验位就万无一失了吧?校验通过就代表数据一定正确,校验失败就代表数据一定坏了。
正解奇偶校验只能检测奇数个比特位翻转。最常见的单比特翻转当然能查到,但如果有两位同时翻转,1 的个数奇偶性不变,校验会漏报。它只适合错误率较低、以单比特错为主的信道。CRC 检错能力更强,海明码还能纠错,它们才是应对恶劣环境的利器。
为何常见:把“能查出常见错误”误解成“能查出所有错误”,忽略了奇偶校验只统计奇偶性这一本质。
误区格雷码和二进制之间的映射是设计好的查表关系,没有公式,只能死记硬背。
正解格雷码有严格的位运算公式。二进制转格雷码是 G = B ^ (B >> 1);格雷码转二进制可以按 B_{n-1} = G_{n-1}、B_i = B_{i+1} ^ G_i 从高位逐位还原。查表只是工程中的查表加速手段,不是唯一方法。
为何常见:教材通常先展示格雷码对照表,再讲反射构造,初学者误以为编码本质就是查表,忽略了其背后的代数规律。
误区4 位二进制能表示 16 个值,BCD 码只用到 10 个,浪费了 6 种状态,这种设计应该被淘汰。
正解BCD 码确实牺牲了约 20% 的编码空间,但换来了三个实际好处:与十进制显示天然对接(数码管译码器可直接驱动)、避免二进制小数在金融计算中的误差、软件调试时可读性极强。电子表、收银机、金融计算器至今仍大量使用。
为何常见:只从信息论压缩率看问题,忽略了工程上“直观、简单、可靠”往往比空间效率更重要。
面试题
Q1. 十进制数字 9 的 8421 BCD 编码是多少?
- A.1001← 正确答案
- B.1010
- C.1000
- D.0110
答案:A。8421 码按权重 8、4、2、1 展开,9 = 8 + 1,对应 1001。1010 是 BCD 中的废码(10 不合法);1000 是 8;0110 是 6。
Q2. 二进制数 1011 对应的 4 位格雷码是?
- A.1101
- B.1110← 正确答案
- C.1000
- D.1111
答案:B。格雷码公式 G = B ^ (B>>1)。B=1011,B>>1=0101,逐位异或得 1110。这与“高位保留、低位与相邻高位异或”的规则一致。
Q3. 数据位 101101 按偶校验生成的校验位是?
- A.校验位为 0(4 个 1)← 正确答案
- B.校验位为 1(4 个 1)
- C.校验位为 0(3 个 1)
- D.校验位为 1(3 个 1)
答案:A。101101 中 1 的个数为 4,是偶数,偶校验要求总 1 数为偶数,所以校验位补 0。若 1 的个数是奇数,则校验位补 1。
Q4. BCD 码加法中,当 4 位结果大于 9 或产生进位时,正确的修正是?
- A.结果加 6 修正← 正确答案
- B.结果减 6 修正
- C.结果加 3 修正
- D.结果加 12 修正
答案:A。BCD 每满 10 进一位,而 4 位二进制满 16 才进位,中间差 6,所以越界时加 0110 即可。例如 7+5=1100,加 6 得 1 0010,代表十进制 12。
Q5. 4 位格雷码 1011 对应的二进制数是?
- A.1011
- B.1110
- C.1101← 正确答案
- D.1001
答案:C。格雷码转二进制从高位开始:B3=1,B2=B3^G2=1^0=1,B1=B2^G1=1^1=0,B0=B1^G0=0^1=1,得到 1101。也可以用循环异或法验证。
Q6. 格雷码最独特的性质是?
- A.相邻码仅一位不同← 正确答案
- B.所有码距都是二
- C.只能用于加法运算
- D.码值大小与二进制一致
答案:A。格雷码的核心定义就是相邻码距为 1,例如 3 位格雷码 010 到 110 只变最高位。它不能直接比较大小,也不是只用于加法,数值顺序与二进制并不一致。
Q7. 关于 BCD 码与纯二进制的比较,正确的是?
- A.相同位数可取更大范围
- B.纯二进制却更节省空间← 正确答案
- C.BCD 码可直接相加运算
- D.二进制可作十进制显示
答案:B。4 位纯二进制可表示 0 到 15,而 BCD 只表示 0 到 9,所以纯二进制更省空间。BCD 码加法需要加 6 修正,不能直接相加;二进制数也不方便直接作十进制显示。
Q8. 下列哪些属于格雷码的典型工程应用?
- A.旋转编码器← 正确答案
- B.电梯楼层显示← 正确答案
- C.FIFO 读指针← 正确答案
- D.浮点数运算
答案:ABC。旋转编码器、电梯楼层显示和异步 FIFO 指针都利用“相邻码只变一位”来避免多位同时变化带来的误读。浮点数运算有自己的 IEEE 754 标准,与格雷码无关。
Q9. 关于奇偶校验码,下列说法正确的是?
- A.能检测单比特翻转错误← 正确答案
- B.能检测偶数个比特翻转
- C.不能指出出错的具体位置← 正确答案
- D.能纠正一个比特的错误
答案:AC。奇偶校验只统计 1 的个数奇偶性,因此只能检测奇数位翻转(如单比特错)。偶数位翻转后奇偶性不变会漏检;校验结果只有 0/1 一个值,无法定位位置,更不能纠错。
Q10. 在异步 FIFO 设计中,读写指针用格雷码表示的主要原因是?
- A.降低多位同步亚稳态风险← 正确答案
- B.比二进制指针占用更少位
- C.相邻指针只差一位更安全← 正确答案
- D.可以直接比较读数的大小
答案:AC。异步 FIFO 的指针要跨时钟域传输。二进制指针跳变时多位同时翻转,采样瞬间可能读到中间乱码;格雷码每次只变一位,即使采样在翻转瞬间,也只会差一位而不会跳到错误位置。格雷码不省位数,也不能直接比大小。