1. 数值与编码的底层逻辑:为什么计算机需要这么多“码”
1.1 从一段真实的调试经历说起
前阵子帮一个朋友排查他做的智能电表采集程序,现象很诡异:电表通过串口往主控板发数据,偶尔会解析出完全离谱的电压值,比如220V变成380V,重启一下又正常了。他一开始怀疑是电表固件的问题,换了三块表都一样。后来我让他把原始报文打出来看,发现是CRC校验没做,程序直接把噪声当数据用了。加上CRC校验之后,问题立刻消失。
这件事特别典型。很多人学计算机组成原理的时候,觉得数值与编码这一章就是一堆公式和表格,背完考试就完了。但真正到了嵌入式、通信、存储这些领域,BCD码、奇偶校验、海明码、CRC这些东西是每天都要打交道的。你写的每一行串口通信代码、每一次从Flash读数据、每一个网络包的处理,背后都有这些编码方案在兜底。
这篇文章我打算把这几类编码从“为什么需要”到“怎么算”再到“怎么用”完整讲一遍。不是教科书那种定义堆砌,而是按照一个从业者实际会遇到的场景来组织。如果你正在学计算机组成原理,或者在做嵌入式通信、数据存储相关的工作,这篇内容应该能帮你把这块知识真正落地。
1.2 核心概念速览:四类编码各自解决什么问题
在展开细节之前,先用一张表把这四类编码的定位说清楚。很多人学的时候容易混淆,是因为没搞明白它们各自要解决的核心问题不同。
| 编码类型 | 核心解决的问题 | 典型应用场景 | 是否可纠错 |
|---|---|---|---|
| BCD码 | 二进制与十进制之间的高效转换 | 数码管显示、电表读数、金融计算 | 否 |
| 奇偶校验 | 检测单比特错误 | 串口通信、内存校验 | 仅检错,不能纠错 |
| 海明码 | 检测并纠正单比特错误 | ECC内存、卫星通信 | 可纠错 |
| CRC校验 | 检测突发错误 | 网络通信、存储校验、Modbus | 仅检错,不能纠错 |
这张表建议先记住。BCD解决的是“人机交互”问题,奇偶校验和海明码解决的是“传输可靠性”问题,CRC解决的是“数据完整性”问题。它们不是互相替代的关系,而是在不同层面各司其职。
我见过不少初学者把CRC和海明码搞混,觉得都是校验码应该差不多。实际上它们的数学基础完全不同:海明码基于线性分组码理论,CRC基于多项式除法。这个后面会详细展开。
1.3 学习路径建议:先理解“为什么”,再记“怎么算”
这一章的内容有个特点:公式多、表格多、计算步骤繁琐。如果上来就死记硬背,很容易学了后面忘了前面。我的建议是按照这个顺序来:
先理解每种编码要解决的实际问题,然后理解它的设计思路,最后才是具体的计算步骤。比如海明码,你先要知道它是为了在内存里自动纠正单比特错误而设计的,然后理解“校验位放在2的幂次位置”这个设计的原因,最后再去算具体的校验值。这样学下来,即使公式忘了,你也能自己推导出来。
下面进入正题,从BCD码开始。
2. BCD码:让二进制和十进制和平共处
2.1 BCD码的本质:用二进制“模仿”十进制
BCD的全称是Binary-Coded Decimal,中文叫二-十进制编码。它的核心思想特别简单:用4位二进制来表示1位十进制数字。比如十进制数25,用BCD表示就是0010 0101,高4位表示2,低4位表示5。
为什么要这么做?因为二进制和十进制之间的转换是有成本的。一个8位二进制数能表示0到255,但如果你要把它显示在数码管上,或者打印出来给人看,就需要做除法取余。除法在硬件里是很耗资源的操作。而BCD码直接把每一位十进制数字用4位二进制存起来,显示的时候只需要查表译码,不需要做除法。
这就是BCD码存在的根本原因:在需要频繁进行十进制显示的场合,用存储空间的浪费换取转换效率的提升。
一个4位二进制能表示16个值,但BCD只用了其中10个(0000到1001),剩下6个(1010到1111)是无效的。这就是BCD码的“浪费”。但在这个场景下,这种浪费是值得的。
2.2 压缩BCD与非压缩BCD:两种存储格式的选择
BCD码在实际使用中有两种格式,这个在写底层驱动的时候经常遇到,需要分清楚。
压缩BCD:一个字节存两位十进制数,高4位存十位,低4位存个位。比如十进制数42,压缩BCD就是0100 0010。这种格式节省空间,一个字节顶两个十进制位。
非压缩BCD:一个字节存一位十进制数,低4位是数值,高4位通常是0000或者1111(取决于具体规范)。比如十进制数42,非压缩BCD需要两个字节:0000 0100和0000 0010。
在x86汇编里,非压缩BCD的高4位在加法运算后可能是任意值,需要用AAA指令调整。压缩BCD用DAA指令调整。这些指令现在用得少了,但理解它们有助于理解BCD的本质。
实际做嵌入式开发的时候,选择哪种格式取决于你的需求。如果存储空间紧张,用压缩BCD;如果处理方便优先,用非压缩BCD。我个人的经验是,在STM32这类MCU上做电表数据存储,压缩BCD用得更多,因为EEPROM空间有限。
2.3 BCD码的运算调整:为什么需要“加6修正”
BCD码的加法有个坑:直接用二进制加法器算,结果可能不是合法的BCD码。
举个例子:十进制8 + 7 = 15。用BCD表示,8是1000,7是0111,二进制相加得到1111。但1111不是合法的BCD码(合法范围是0000到1001)。这时候就需要修正:因为结果大于9,需要加6(0110)进行调整。1111 + 0110 = 1 0101,进位1,低4位是0101,正好是15的BCD表示。
这个“加6修正”的原理是:4位二进制有16个状态,BCD只用了10个,中间跳过了6个状态。当结果落在1010到1111这个无效区间时,加6就能把它“推”回有效区间并产生正确的进位。
减法类似,如果低位向高位借位,需要减6修正。
注意:做BCD运算的时候,一定要在每一步运算后都做调整,不能等所有运算做完再统一调整。因为中间结果可能已经溢出了合法范围,后续运算会基于错误的值继续算。
2.4 BCD码在实际项目中的应用要点
我在电表项目里用BCD码比较多,分享几个实操经验。
第一,BCD码和ASCII码的转换。很多电表协议里,数据是用ASCII字符表示的,比如字符'2'的ASCII是0x32,字符'5'是0x35。要转成压缩BCD,需要把每个字节的高4位去掉,然后合并。这个转换在串口接收中断里做,要注意字节序。
第二,BCD码的合法性检查。从外部设备收到的BCD数据,一定要检查每一位是否在0到9之间。我遇到过电表在异常情况下发出0xFA这种非法BCD值,如果不检查直接当数据用,算出来的电压值会完全错误。
第三,BCD码的显示。驱动数码管的时候,BCD到七段码的转换通常用查表法。表的大小是10个字节(0到9),查表比实时计算快得多。如果用的是共阴极数码管,段码表大概是这样的:
const uint8_t bcd_to_7seg[] = { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F // 9 };这个表在实际项目里直接抄就行,省得每次重新算。
3. 奇偶校验:最简单也最容易被忽视的检错手段
3.1 奇偶校验的基本原理:一个比特的代价
奇偶校验的思路极其简单:在数据后面加一个校验位,使得整个数据(包括校验位)中1的个数为奇数(奇校验)或偶数(偶校验)。
比如数据1011001,里面有4个1。如果用偶校验,校验位填0,总共4个1(偶数);如果用奇校验,校验位填1,总共5个1(奇数)。
接收方收到数据后,重新计算1的个数,如果不符合约定的奇偶性,就知道传输过程中出错了。
这个方案的优点是极其简单,只需要一个异或门就能实现。缺点是只能检测奇数个比特错误。如果有2个比特同时翻转,1的个数变化是偶数,奇偶性不变,就检测不出来。
3.2 奇偶校验在串口通信中的实际配置
串口通信是奇偶校验最常见的应用场景。配置串口的时候,你会看到这样的选项:数据位8位、停止位1位、校验位None/Odd/Even。这里的Odd就是奇校验,Even就是偶校验。
在STM32的HAL库里,配置奇偶校验的代码大概是这样:
huart1.Init.Parity = UART_PARITY_EVEN; // 偶校验 huart1.Init.WordLength = UART_WORDLENGTH_9B; // 注意:带校验位时数据长度要设为9位这里有个坑:启用奇偶校验后,数据位要设为9位。因为校验位占用了第9位。很多人配置的时候忘了改这个,结果数据解析全乱。
还有一个经验:在电磁干扰比较严重的工业现场,奇偶校验的检错能力其实不太够。我做过一个RS485的温湿度采集项目,现场有变频器,干扰大的时候奇偶校验经常报错,但偶尔也会有漏检的情况。后来换成了CRC校验才稳定下来。所以如果你的通信环境比较恶劣,建议直接上CRC,别在奇偶校验上省事。
3.3 奇偶校验的硬件实现:一个异或门就够了
奇偶校验在硬件上实现特别简单。发送方把所有数据位做异或运算,结果就是偶校验位(如果1的个数是奇数,异或结果是1,补上这个1就变成偶数个1)。接收方把所有数据位和校验位一起做异或,如果结果是0(偶校验)或1(奇校验),说明没有错误。
这个特性使得奇偶校验在早期硬件中被广泛使用。比如老式的DRAM内存,每个字节配一个奇偶校验位,成本增加12.5%,但能检测出大部分单比特错误。
现在服务器上用的ECC内存,其实是海明码的简化版本,能纠错而不只是检错。这个后面讲海明码的时候会详细说。
3.4 奇偶校验的局限性与适用边界
奇偶校验最大的问题是检错能力有限。它能检测所有奇数个比特错误,但检测不出偶数个比特错误。在突发错误(连续多个比特出错)的场景下,奇偶校验基本没用。
那它为什么还在用?因为简单、便宜、快。在短距离、低干扰的通信场景下,比如芯片之间的I2C通信,奇偶校验够用了。而且有些场景下,错误检测不是目的,只是提供一个“大概靠谱”的信号,真正的可靠性由上层协议保证。
我的建议是:如果是板级通信(同一块PCB上的芯片之间),奇偶校验可以用;如果是板间通信或者现场总线,直接上CRC。这个选择在项目初期就要定好,后期改协议成本很高。
4. 海明码:能自动纠错的校验码
4.1 海明码的核心思想:用多个校验位定位错误
奇偶校验只能告诉你“出错了”,但不能告诉你“哪里错了”。海明码的突破在于:通过多个校验位的组合,不仅能检测错误,还能定位到具体是哪一位出错,从而自动纠正。
海明码的设计非常巧妙。它把校验位插入到数据位的特定位置(2的幂次位置:第1、2、4、8、16...位),每个校验位负责校验一组特定的数据位。当错误发生时,所有校验失败的校验位的位置编号相加,就是出错数据位的位置。
举个例子:如果第1、2、4位的校验都失败了,1+2+4=7,说明第7位出错了。把第7位取反,就完成了纠错。
这个设计的数学基础是:每个数据位的位置编号都可以表示为若干个校验位位置编号的和。比如第7位 = 第1位 + 第2位 + 第4位,所以第7位的数据会参与第1、2、4位校验位的计算。
4.2 海明码的编码步骤:从数据位到校验位
下面用一个具体例子来演示海明码的编码过程。假设要传输的数据是1011(4位),采用偶校验。
第一步:确定校验位的数量。校验位数量r需要满足:2^r >= 数据位数量 + r + 1。对于4位数据,2^3=8 >= 4+3+1=8,所以需要3位校验位。总长度是4+3=7位。
第二步:确定校验位的位置。校验位放在第1、2、4位(2的幂次位置),数据位放在其余位置。
| 位置 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| 类型 | P1 | P2 | D1 | P4 | D2 | D3 | D4 |
| 值 | ? | ? | 1 | ? | 0 | 1 | 1 |
第三步:确定每个校验位负责的数据位。规则是:位置编号的二进制表示中,从右往左第k位为1的数据位,由第2^(k-1)个校验位负责。
- P1(位置1)负责所有位置编号二进制最低位为1的位:3, 5, 7
- P2(位置2)负责所有位置编号二进制次低位为1的位:3, 6, 7
- P4(位置4)负责所有位置编号二进制第三位为1的位:5, 6, 7
第四步:计算校验位的值(偶校验)。
- P1 = D1 ⊕ D2 ⊕ D4 = 1 ⊕ 0 ⊕ 1 = 0
- P2 = D1 ⊕ D3 ⊕ D4 = 1 ⊕ 1 ⊕ 1 = 1
- P4 = D2 ⊕ D3 ⊕ D4 = 0 ⊕ 1 ⊕ 1 = 0
最终传输的码字是:0 1 1 0 0 1 1(位置1到7)。
4.3 海明码的纠错过程:定位并翻转错误位
假设接收方收到的码字是0 1 1 0 1 1 1,第5位从0变成了1。
接收方重新计算三个校验组:
- G1 = P1 ⊕ D1 ⊕ D2 ⊕ D4 = 0 ⊕ 1 ⊕ 1 ⊕ 1 = 1(失败)
- G2 = P2 ⊕ D1 ⊕ D3 ⊕ D4 = 1 ⊕ 1 ⊕ 1 ⊕ 1 = 0(通过)
- G4 = P4 ⊕ D2 ⊕ D3 ⊕ D4 = 0 ⊕ 1 ⊕ 1 ⊕ 1 = 1(失败)
失败的是G1和G4,位置编号相加:1+4=5。第5位出错,取反即可纠正。
这个过程的精妙之处在于:校验失败的位置编号之和直接指向错误位。不需要查表,不需要复杂的计算,硬件实现就是一个异或网络加一个加法器。
4.4 海明码的扩展:SEC-DED与ECC内存
基本的海明码只能纠正单比特错误(SEC,Single Error Correction)。如果同时发生两个比特错误,海明码可能会误判为另一个单比特错误,导致纠错后结果更错。
为了解决这个问题,实际应用中通常会增加一个总的奇偶校验位,形成SEC-DED(Single Error Correction, Double Error Detection)码。这个额外的校验位能检测出双比特错误,但只能纠正单比特错误。
服务器上的ECC内存用的就是SEC-DED方案。每64位数据配8位校验位,能纠正单比特错误,检测双比特错误。这就是为什么ECC内存比普通内存贵——多出来的8位存储芯片和更复杂的控制器。
在做高可靠性嵌入式系统的时候,如果MCU支持ECC内存,建议开启。我做过一个工业网关的项目,现场电磁环境复杂,开启ECC后系统稳定性明显提升,之前偶尔出现的莫名死机问题也消失了。
5. CRC校验:通信领域最常用的检错方案
5.1 CRC的数学基础:多项式除法
CRC的全称是Cyclic Redundancy Check,循环冗余校验。它的核心思想是:把数据看作一个多项式,用一个约定的生成多项式去除,余数就是CRC校验值。
比如数据1101,可以看作多项式x^3 + x^2 + 1。生成多项式比如x^3 + x + 1(对应1011)。做多项式除法,得到的余数就是CRC值。
实际计算的时候,用的是模2除法(异或运算,不借位)。这个在硬件上特别容易实现,一个移位寄存器加几个异或门就够了。
CRC的检错能力很强,特别是对突发错误。一个r位的CRC能检测出所有长度不超过r的突发错误,以及大部分更长的突发错误。这就是为什么网络通信、存储系统都大量使用CRC。
5.2 常见CRC参数:CRC-8、CRC-16、CRC-32怎么选
CRC有很多变种,区别在于生成多项式的位数和具体参数。常见的有:
| 类型 | 生成多项式 | 校验位长度 | 典型应用 |
|---|---|---|---|
| CRC-8 | x^8+x^2+x+1 | 8位 | 单总线、I2C |
| CRC-16/Modbus | x^16+x^15+x^2+1 | 16位 | Modbus、串口通信 |
| CRC-16/CCITT | x^16+x^12+x^5+1 | 16位 | X.25、蓝牙 |
| CRC-32 | 标准32位多项式 | 32位 | 以太网、ZIP、PNG |
选择哪种CRC,主要看两个因素:数据长度和错误检测要求。数据越长,需要的CRC位数越多。一般来说,CRC位数应该大于等于数据长度的对数。对于几十字节的串口数据,CRC-16够用了;对于几KB的网络包,CRC-32更合适。
Modbus协议用的是CRC-16,生成多项式是0x8005(x^16+x^15+x^2+1),初始值0xFFFF,结果异或0x0000,输入输出都不反转。这个参数组合在工业现场用了很多年,可靠性经过验证。
5.3 CRC-16/Modbus的完整计算过程
下面用具体例子演示CRC-16/Modbus的计算。假设要发送的数据是0x01 0x03 0x00 0x00 0x00 0x01(Modbus读保持寄存器的请求帧)。
方法一:按位计算(适合理解原理)
uint16_t crc16_modbus(uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x0001) { crc >>= 1; crc ^= 0xA001; // 0x8005的反转 } else { crc >>= 1; } } } return crc; }计算结果是0x840A。在Modbus帧中,CRC低字节在前,高字节在后,所以发送时附加0x0A 0x84。
方法二:查表法(适合实际项目)
按位计算每次要循环8次,对于高速通信来说太慢了。实际项目里用查表法,预先算好256个值,每次处理一个字节只需要一次查表和一次异或。
const uint16_t crc16_table[256] = { /* 预计算的值 */ }; uint16_t crc16_modbus_fast(uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc = (crc >> 8) ^ crc16_table[(crc ^ data[i]) & 0xFF]; } return crc; }这个表可以用Python脚本生成,也可以在网上找现成的。我一般用这个脚本生成:
def generate_crc16_table(): table = [] for i in range(256): crc = i for _ in range(8): if crc & 1: crc = (crc >> 1) ^ 0xA001 else: crc >>= 1 table.append(crc) return table5.4 CRC在实际项目中的避坑指南
坑一:字节序搞反。Modbus的CRC是低字节在前,但有些协议是高字节在前。我见过一个项目,发送方和接收方对CRC字节序的理解不一致,导致所有帧都校验失败。调试的时候把CRC值打印出来对比,一眼就能看出来。
坑二:初始值和结果异或搞错。不同的CRC变种,初始值可能是0x0000、0xFFFF或其他值,结果可能异或0x0000或0xFFFF。用在线计算器验证的时候,一定要把参数设对。我习惯用“CRC计算器”这类工具先验证算法,确认无误后再写代码。
坑三:数据范围搞错。CRC计算应该包含哪些字节?Modbus是包含从站地址到数据末尾的所有字节,不包括CRC本身。有些协议只对数据部分计算CRC,不包括帧头。这个一定要看协议文档确认。
坑四:查表法的表生成错误。如果表生成脚本的多项式写错了,整个表都是错的。建议生成表之后,用几个已知输入验证一下。比如CRC-16/Modbus对空数据(长度为0)的结果应该是0xFFFF。
提示:调试CRC问题的时候,最有效的方法是把发送方和接收方的原始数据、计算的中间过程都打印出来,逐字节对比。不要只看最终结果,中间过程往往能暴露问题。
6. 四类编码的联合应用与选型建议
6.1 一个完整的通信帧里可能同时用到多种编码
在实际的通信协议中,这几种编码经常是配合使用的。以一个智能电表的通信帧为例:
- 电表读数用BCD码存储,方便显示和抄读
- 帧头用固定字节,用于同步
- 数据区可能用奇偶校验做字节级保护
- 整帧用CRC-16做完整性校验
这种分层保护的设计思路是:每一层解决不同粒度的问题。BCD解决数据表示问题,奇偶校验解决字节传输问题,CRC解决整帧完整性问题。它们不是互相替代,而是互相补充。
6.2 选型决策表:什么场景用什么编码
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 数码管显示 | BCD码 | 译码简单,无需除法 |
| 短距离板级通信 | 奇偶校验 | 简单快速,够用 |
| 工业现场总线 | CRC-16 | 抗干扰强,检错率高 |
| 网络通信 | CRC-32 | 数据量大,需要强检错 |
| 高可靠存储 | 海明码/ECC | 需要纠错能力 |
| 金融计算 | BCD码 | 避免浮点误差 |
这个表不是绝对的,实际选型还要考虑成本、功耗、开发周期等因素。比如有些低成本的MCU没有硬件CRC模块,用软件算CRC会占用CPU时间,这时候可能就用奇偶校验凑合了。
6.3 硬件加速:什么时候该用硬件CRC
很多现代MCU都带硬件CRC模块,比如STM32的CRC外设。硬件CRC的优势是速度快、不占CPU。但硬件CRC的灵活性差,通常只支持固定的多项式。
我的经验是:如果通信速率高(比如1Mbps以上)或者数据量大(比如每帧几百字节),用硬件CRC。如果通信速率低、数据量小,软件CRC完全够用,而且更灵活。
STM32的硬件CRC配置很简单:
hcrc.Instance = CRC; hcrc.Init.DefaultPolynomialUse = DEFAULT_POLYNOMIAL_ENABLE; hcrc.Init.DefaultInitValueUse = DEFAULT_INIT_VALUE_ENABLE; hcrc.Init.CRCLength = CRC_POLYLENGTH_32B; HAL_CRC_Init(&hcrc);但要注意,STM32的硬件CRC默认多项式是CRC-32,和Modbus的CRC-16不一样。如果需要CRC-16,要么用软件实现,要么用支持可配置多项式的型号。
7. 常见问题速查与调试技巧
7.1 校验码计算常见错误速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| CRC校验一直失败 | 字节序反了 | 交换高低字节再试 |
| CRC校验偶尔失败 | 初始值或异或值不对 | 用在线工具对比 |
| 海明码纠错后更错 | 发生了双比特错误 | 增加总校验位做SEC-DED |
| BCD码显示乱码 | 收到非法BCD值 | 检查每一位是否≤9 |
| 奇偶校验漏检 | 偶数个比特翻转 | 换用CRC校验 |
| 查表法结果不对 | 表生成多项式错误 | 用已知输入验证表 |
7.2 调试CRC问题的三个实用技巧
技巧一:用已知向量验证。找一个标准的测试向量,比如CRC-16/Modbus对字符串"123456789"的结果是0x4B37。如果你的代码算出来不是这个值,说明参数有问题。
技巧二:分步打印中间结果。在循环里把每次异或和移位的结果打印出来,和手工计算的结果对比。虽然麻烦,但能精确定位到哪一步出错。
技巧三:用Python快速验证。写个几行的Python脚本算CRC,和C代码的结果对比。Python的binascii库自带CRC-32,但CRC-16需要自己实现。网上有很多现成的脚本可以抄。
7.3 海明码的硬件实现思路
如果要在FPGA或者ASIC里实现海明码,思路是这样的:
编码器:把数据位和校验位按位置排好,用异或网络计算每个校验位的值。异或网络可以用LFSR(线性反馈移位寄存器)实现,面积很小。
译码器:重新计算校验组,把失败的位置编号相加得到错误位置,然后用一个译码器把该位翻转。整个过程是组合逻辑,延迟很低。
对于SEC-DED,还需要一个总的奇偶校验位。如果总校验失败但海明校验通过,说明是双比特错误,只能报错不能纠错。
7.4 一个真实的Modbus CRC调试案例
最后分享一个我实际踩过的坑。有一次调试Modbus RTU通信,发送方用STM32,接收方用PC上的串口助手。STM32发的帧,串口助手显示CRC错误。但用示波器看波形,数据完全正确。
排查了半天,发现是串口助手的CRC设置不对。它默认用的是CRC-16/CCITT,而Modbus用的是CRC-16/Modbus。两个多项式不同,算出来的CRC当然不一样。把串口助手的CRC类型改成Modbus,问题解决。
这个案例的教训是:调试通信问题的时候,先确认双方的协议参数完全一致。不光是CRC类型,还包括波特率、数据位、停止位、校验位、字节序。这些参数任何一个不一致,都会导致通信失败。我现在的习惯是,在代码注释里把这些参数都写清楚,换人维护的时候不容易搞错。
BCD码、奇偶校验、海明码、CRC这四类编码,本质上都是为了解决“数据在存储和传输过程中可能出错”这个问题。不同的编码方案是在检错能力、纠错能力、实现复杂度、额外开销之间做权衡。理解了这个权衡逻辑,具体用哪种方案就是查表的事了。