CRC 循环冗余校验详解:原理、手工推导与 Matlab/C 代码实现
数据在传输和存储的过程中,难免因为噪声、干扰等原因发生比特错误。如何低成本、高效地发现这些错误?CRC(Cyclic Redundancy Check,循环冗余校验)给出了答案。本文将从零开始,带你彻底搞懂 CRC 的原理、推导过程,并给出可直接运行的 Matlab 和 C 代码。
1. 什么是 CRC
CRC(Cyclic Redundancy Check,循环冗余校验) 是一种根据数据内容计算出一段固定长度校验码(称为 CRC 码、校验值或帧检验序列 FCS)的检错算法。
它的基本思想是:把待发送的数据看成一个二进制多项式,用这个多项式去除以一个约定的"生成多项式",把除得的余数作为校验码附在数据后面一起发送。接收方用同样的方式计算,如果计算出的校验码与收到的校验码不一致(或者除不尽),就说明数据在传输过程中发生了错误。
在讲 CRC 之前,先看看它的"前辈们":
| 检错手段 | 原理 | 缺点 |
|---|---|---|
| 奇偶校验 | 统计 1 的个数奇偶性,补 1 位 | 只能检测奇数个比特错误,检测能力弱 |
| 校验和(Checksum) | 数据按字求和取反 | 实现简单,但漏检率高,抗突发错误能力差 |
| CRC | 多项式模 2 除法取余数 | 检错能力强、硬件实现简单,工业界事实标准 |
CRC 之所以被广泛采用,主要有三个特点:
- 检错能力强:能检测出所有奇数个错误、所有长度不超过 CRC 位数的突发错误,以及绝大多数的其他错误模式;
- 实现简单:核心运算只有"移位"和"异或",非常适合硬件实现(芯片里几行逻辑门就能搞定),软件实现也很快;
- 不增加太多开销:CRC-32 只给每帧增加 4 个字节,对以太网 1500 字节的帧来说开销极小。
2. CRC 实现的原理
2.1 二进制数据可以看作多项式
CRC 的理论基础是多项式算术。任意一段二进制数据,都可以表示成一个以 0/1 为系数的多项式:
1011001 ⟺ 1⋅x6+0⋅x5+1⋅x4+1⋅x3+0⋅x2+0⋅x1+1=x6+x4+x3+1 1011001 \iff 1 \cdot x^6 + 0 \cdot x^5 + 1 \cdot x^4 + 1 \cdot x^3 + 0 \cdot x^2 + 0 \cdot x^1 + 1 = x^6 + x^4 + x^3 + 1 1011001⟺1⋅x6+0⋅x5+1⋅x4+1⋅x3+0⋅x2+0⋅x1+1=x6+x4+x3+1
规律很简单:第 i 位为 1,就对应一项 xix^ixi(从右往左从 0 开始编号)。
2.2 模 2 运算 = 按位异或
CRC 中所有的加减乘除都是在 模 2(GF(2) 有限域) 下进行的,这里只有两条规则:
- 加法 = 减法 = 异或 (XOR):
1+1=0,1+0=1,没有进位也没有借位; - 除法就是重复"异或消位"的过程。
例如:
1011⊕1001=0010 1011 \oplus 1001 = 0010 1011⊕1001=0010
这正是为什么 CRC 的硬件实现只需要移位寄存器 + 异或门,而不需要任何加法器。
2.3 生成多项式 G(x)
发送端和接收端必须约定同一个生成多项式 G(x)G(x)G(x),这是 CRC 的灵魂。G(x)G(x)G(x) 的最高次数 n 决定了 CRC 校验码的位数(n 位 CRC 对应 n+1 位的多项式)。
G(x)G(x)G(x) 的选择一般遵循以下原则:
- 最高位和最低位必须为 1(否则可以整体约掉一个 x,退化);
- 多项式不能含有因子 x,且应尽量选择"不可约多项式",使检错能力最大化;
- 实际工程中不必自己设计,直接用现成的国际标准即可(见 2.5 节)。
2.4 发送端计算流程
#mermaid-svg-Tqho0ndU4K1J1h0l{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Tqho0ndU4K1J1h0l .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Tqho0ndU4K1J1h0l .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Tqho0ndU4K1J1h0l .error-icon{fill:#552222;}#mermaid-svg-Tqho0ndU4K1J1h0l .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Tqho0ndU4K1J1h0l .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Tqho0ndU4K1J1h0l .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Tqho0ndU4K1J1h0l .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Tqho0ndU4K1J1h0l .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Tqho0ndU4K1J1h0l .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Tqho0ndU4K1J1h0l .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Tqho0ndU4K1J1h0l .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Tqho0ndU4K1J1h0l .marker.cross{stroke:#333333;}#mermaid-svg-Tqho0ndU4K1J1h0l svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Tqho0ndU4K1J1h0l p{margin:0;}#mermaid-svg-Tqho0ndU4K1J1h0l .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Tqho0ndU4K1J1h0l .cluster-label text{fill:#333;}#mermaid-svg-Tqho0ndU4K1J1h0l .cluster-label span{color:#333;}#mermaid-svg-Tqho0ndU4K1J1h0l .cluster-label span p{background-color:transparent;}#mermaid-svg-Tqho0ndU4K1J1h0l .label text,#mermaid-svg-Tqho0ndU4K1J1h0l span{fill:#333;color:#333;}#mermaid-svg-Tqho0ndU4K1J1h0l .node rect,#mermaid-svg-Tqho0ndU4K1J1h0l .node circle,#mermaid-svg-Tqho0ndU4K1J1h0l .node ellipse,#mermaid-svg-Tqho0ndU4K1J1h0l .node polygon,#mermaid-svg-Tqho0ndU4K1J1h0l .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Tqho0ndU4K1J1h0l .rough-node .label text,#mermaid-svg-Tqho0ndU4K1J1h0l .node .label text,#mermaid-svg-Tqho0ndU4K1J1h0l .image-shape .label,#mermaid-svg-Tqho0ndU4K1J1h0l .icon-shape .label{text-anchor:middle;}#mermaid-svg-Tqho0ndU4K1J1h0l .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Tqho0ndU4K1J1h0l .rough-node .label,#mermaid-svg-Tqho0ndU4K1J1h0l .node .label,#mermaid-svg-Tqho0ndU4K1J1h0l .image-shape .label,#mermaid-svg-Tqho0ndU4K1J1h0l .icon-shape .label{text-align:center;}#mermaid-svg-Tqho0ndU4K1J1h0l .node.clickable{cursor:pointer;}#mermaid-svg-Tqho0ndU4K1J1h0l .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Tqho0ndU4K1J1h0l .arrowheadPath{fill:#333333;}#mermaid-svg-Tqho0ndU4K1J1h0l .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Tqho0ndU4K1J1h0l .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Tqho0ndU4K1J1h0l .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tqho0ndU4K1J1h0l .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Tqho0ndU4K1J1h0l .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tqho0ndU4K1J1h0l .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Tqho0ndU4K1J1h0l .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Tqho0ndU4K1J1h0l .cluster text{fill:#333;}#mermaid-svg-Tqho0ndU4K1J1h0l .cluster span{color:#333;}#mermaid-svg-Tqho0ndU4K1J1h0l div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Tqho0ndU4K1J1h0l .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Tqho0ndU4K1J1h0l rect.text{fill:none;stroke-width:0;}#mermaid-svg-Tqho0ndU4K1J1h0l .icon-shape,#mermaid-svg-Tqho0ndU4K1J1h0l .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tqho0ndU4K1J1h0l .icon-shape p,#mermaid-svg-Tqho0ndU4K1J1h0l .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Tqho0ndU4K1J1h0l .icon-shape .label rect,#mermaid-svg-Tqho0ndU4K1J1h0l .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tqho0ndU4K1J1h0l .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Tqho0ndU4K1J1h0l .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Tqho0ndU4K1J1h0l :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 原始数据 D(如 1011001)
左移 n 位:末尾补 n 个 0
得到 D' = D × x^n
模 2 除法:D' ÷ G(x)
得到余数 R,即 CRC 校验码(n 位)
发送帧 = D + R(数据后拼接校验码)
具体步骤:
- 设生成多项式 G(x)G(x)G(x) 的次数为 nnn,把数据 DDD 左移 nnn 位,即末尾补 nnn 个 0,得到 D′=D×xnD' = D \times x^nD′=D×xn;
- 用 D′D'D′ 对 G(x)G(x)G(x) 做模 2 除法 ,得到余数 RRR(不足 nnn 位时高位补 0);
- 把 RRR 拼接到 DDD 的末尾,构成发送帧 D+RD + RD+R 发出去。
2.5 接收端校验流程
#mermaid-svg-lOQp1NnE4vC5dXdE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lOQp1NnE4vC5dXdE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lOQp1NnE4vC5dXdE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lOQp1NnE4vC5dXdE .error-icon{fill:#552222;}#mermaid-svg-lOQp1NnE4vC5dXdE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lOQp1NnE4vC5dXdE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lOQp1NnE4vC5dXdE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lOQp1NnE4vC5dXdE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lOQp1NnE4vC5dXdE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lOQp1NnE4vC5dXdE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lOQp1NnE4vC5dXdE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lOQp1NnE4vC5dXdE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lOQp1NnE4vC5dXdE .marker.cross{stroke:#333333;}#mermaid-svg-lOQp1NnE4vC5dXdE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lOQp1NnE4vC5dXdE p{margin:0;}#mermaid-svg-lOQp1NnE4vC5dXdE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lOQp1NnE4vC5dXdE .cluster-label text{fill:#333;}#mermaid-svg-lOQp1NnE4vC5dXdE .cluster-label span{color:#333;}#mermaid-svg-lOQp1NnE4vC5dXdE .cluster-label span p{background-color:transparent;}#mermaid-svg-lOQp1NnE4vC5dXdE .label text,#mermaid-svg-lOQp1NnE4vC5dXdE span{fill:#333;color:#333;}#mermaid-svg-lOQp1NnE4vC5dXdE .node rect,#mermaid-svg-lOQp1NnE4vC5dXdE .node circle,#mermaid-svg-lOQp1NnE4vC5dXdE .node ellipse,#mermaid-svg-lOQp1NnE4vC5dXdE .node polygon,#mermaid-svg-lOQp1NnE4vC5dXdE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lOQp1NnE4vC5dXdE .rough-node .label text,#mermaid-svg-lOQp1NnE4vC5dXdE .node .label text,#mermaid-svg-lOQp1NnE4vC5dXdE .image-shape .label,#mermaid-svg-lOQp1NnE4vC5dXdE .icon-shape .label{text-anchor:middle;}#mermaid-svg-lOQp1NnE4vC5dXdE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lOQp1NnE4vC5dXdE .rough-node .label,#mermaid-svg-lOQp1NnE4vC5dXdE .node .label,#mermaid-svg-lOQp1NnE4vC5dXdE .image-shape .label,#mermaid-svg-lOQp1NnE4vC5dXdE .icon-shape .label{text-align:center;}#mermaid-svg-lOQp1NnE4vC5dXdE .node.clickable{cursor:pointer;}#mermaid-svg-lOQp1NnE4vC5dXdE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lOQp1NnE4vC5dXdE .arrowheadPath{fill:#333333;}#mermaid-svg-lOQp1NnE4vC5dXdE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lOQp1NnE4vC5dXdE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lOQp1NnE4vC5dXdE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lOQp1NnE4vC5dXdE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lOQp1NnE4vC5dXdE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lOQp1NnE4vC5dXdE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lOQp1NnE4vC5dXdE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lOQp1NnE4vC5dXdE .cluster text{fill:#333;}#mermaid-svg-lOQp1NnE4vC5dXdE .cluster span{color:#333;}#mermaid-svg-lOQp1NnE4vC5dXdE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lOQp1NnE4vC5dXdE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lOQp1NnE4vC5dXdE rect.text{fill:none;stroke-width:0;}#mermaid-svg-lOQp1NnE4vC5dXdE .icon-shape,#mermaid-svg-lOQp1NnE4vC5dXdE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lOQp1NnE4vC5dXdE .icon-shape p,#mermaid-svg-lOQp1NnE4vC5dXdE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lOQp1NnE4vC5dXdE .icon-shape .label rect,#mermaid-svg-lOQp1NnE4vC5dXdE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lOQp1NnE4vC5dXdE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lOQp1NnE4vC5dXdE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lOQp1NnE4vC5dXdE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
接收到的帧 D + R
模 2 除法:÷ G(x)(同一个多项式)
余数是否为 0?
✅ 传输无错,通过校验
❌ 检测到错误,丢弃或请求重传
因为 (D×xn+R)(D \times x^n + R)(D×xn+R) 恰好是 G(x)G(x)G(x) 的倍数,所以接收方把整个帧除以 G(x)G(x)G(x):
- 余数为 0:说明数据没有出错(注意:CRC 只能"检错"不能"纠错",且极小概率下错误也可能恰好被整除而漏检);
- 余数不为 0:说明数据一定出错了。
2.6 CRC 参数与常见标准
除了生成多项式,一个完整的 CRC 算法还包含 4 个参数,它们共同决定最终结果,缺一个都对不上:
| 参数 | 含义 |
|---|---|
| 初始值 Init | 移位寄存器计算的初始状态(常见 0x00 或 0xFFFF) |
| 输入反转 RefIn | 输入数据是否按位反转后参与计算 |
| 输出反转 RefOut | 计算结果是否按位反转后输出 |
| 结果异或 XorOut | 输出前是否与一个固定值异或 |
工业上常用的标准如下(测试向量:对 ASCII 字符串 "123456789" 计算):
| 算法 | 生成多项式(含隐式最高位) | 初始值 | 输入反转 | 输出反转 | 结果异或 | 校验值(123456789) |
|---|---|---|---|---|---|---|
| CRC-8 | 0x07,x8+x2+x+1x^8+x^2+x+1x8+x2+x+1 | 0x00 | 否 | 否 | 0x00 | 0xF4 |
| CRC-16/MODBUS | 0x8005,x16+x15+x2+1x^{16}+x^{15}+x^2+1x16+x15+x2+1 | 0xFFFF | 是 | 是 | 0x0000 | 0x4B37 |
| CRC-32(以太网) | 0x04C11DB7,x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1x^{32}+x^{26}+x^{23}+x^{22}+x^{16}+x^{12}+x^{11}+x^{10}+x^8+x^7+x^5+x^4+x^2+x+1x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1 | 0xFFFFFFFF | 是 | 是 | 0xFFFFFFFF | 0xCBF43926 |
后面实现代码时,我们会用这些标准值来验证程序是否正确。
3. CRC 的用途
CRC 是计算机和通信领域应用最广泛的检错手段之一,几乎无处不在:
- 有线/无线通信协议:以太网(IEEE 802.3)每个帧尾部都有 4 字节的帧检验序列 FCS(CRC-32);USB、蓝牙、CAN 总线、RS-485 等也都内置了 CRC 校验;
- 工业现场总线:Modbus-RTU 报文自带 CRC-16;Profibus、S7 通信等同样使用 CRC 族算法保障可靠性;
- 文件格式与压缩包:PNG 图像、ZIP/GZIP 压缩包用 CRC-32 检测文件块是否损坏,下载工具显示"校验失败"就是 CRC 在起作用;
- 存储设备:磁盘扇区、SSD、光盘刻录都会记录 CRC,用于在读取时发现介质损坏;
- 嵌入式与固件:固件升级时对 .bin 文件算 CRC 并在烧录后校验,防止烧录进"坏固件";通信协议帧普遍在末尾加 CRC 字段;
- 数据完整性校验:日志、配置文件、数据库页等在写入/读取时用 CRC 快速发现意外篡改或掉电损坏。
一句话总结:凡是"传数据怕出错"的场景,几乎都有 CRC 的身影。
4. CRC 推理实例
空谈原理不如动手算一遍。我们用一个小例子把整个计算过程完整走一遍。
参数设定:
- 数据:D=1011001D = 1011001D=1011001(7 位)
- 生成多项式:G(x)=x4+x+1G(x) = x^4 + x + 1G(x)=x4+x+1,二进制为
10011(5 位),因此 CRC 位数 n = 4
步骤 1:数据左移 4 位(补 4 个 0)
D′=1011001 0000=10110010000 D' = 1011001\,0000 = 10110010000 D′=10110010000=10110010000
步骤 2:模 2 除法(只做异或,没有借位)
10110⊕10011=00101⇒余串 10101000010101⊕10011=00110⇒余串 110000011000⊕10011=01011⇒余串 10110010110⊕10011=00101⇒余串 1010(不足 5 位,停止) \begin{aligned} & 10110 \oplus 10011 = 00101 \quad \Rightarrow \text{余串 } 101010000\\ & 10101 \oplus 10011 = 00110 \quad \Rightarrow \text{余串 } 1100000\\ & 11000 \oplus 10011 = 01011 \quad \Rightarrow \text{余串 } 101100\\ & 10110 \oplus 10011 = 00101 \quad \Rightarrow \text{余串 } 1010 \quad \text{(不足 5 位,停止)} \end{aligned} 10110⊕10011=00101⇒余串 10101000010101⊕10011=00110⇒余串 110000011000⊕10011=01011⇒余串 10110010110⊕10011=00101⇒余串 1010(不足 5 位,停止)
每一步都取出当前余串的最高 5 位做异或,异或结果接上剩余位,再去掉前导 0,直到余串位数不足 5 位为止。
步骤 3:得到 CRC 校验码
CRC=1010 \boxed{CRC = 1010} CRC=1010
步骤 4:构造发送帧
发送帧=D+R=1011001+1010=10110011010 \text{发送帧} = D + R = 1011001 + 1010 = 10110011010 发送帧=D+R=1011001+1010=10110011010
步骤 5:接收端校验
接收方用同一个多项式 10011 去除整个帧 10110011010:
10110011010÷10011=整除,余数为 0⇒校验通过,数据无误✓ 10110011010 \div 10011 = \text{整除,余数为 } 0 \quad \Rightarrow \text{校验通过,数据无误} \checkmark 10110011010÷10011=整除,余数为 0⇒校验通过,数据无误✓
如果传输中任意一位翻转为 0 或 1,余数几乎必然不为 0,错误就会被发现。
4.1 换个角度看:移位寄存器(等价推导)
上面的长除法,等价于 4 位移位寄存器逐位计算。寄存器初始为 0000,多项式取 10011 去掉最高位后的低 4 位 0011,每移入一个比特,若移出位为 1 则与 0011 异或:
| 序号 | 输入位 | 原寄存器 | 移出位 | 操作 | 新寄存器 |
|---|---|---|---|---|---|
| 1 | 1 | 0000 | 0 | 不移除 | 0001 |
| 2 | 0 | 0001 | 0 | 不移除 | 0010 |
| 3 | 1 | 0010 | 0 | 不移除 | 0101 |
| 4 | 1 | 0101 | 0 | 不移除 | 1011 |
| 5 | 0 | 1011 | 1 | 异或 0011 | 0101 |
| 6 | 0 | 0101 | 0 | 不移除 | 1010 |
| 7 | 1 | 1010 | 1 | 异或 0011 | 0110 |
| 8 | 0 | 0110 | 0 | 不移除 | 1100 |
| 9 | 0 | 1100 | 1 | 异或 0011 | 1011 |
| 10 | 0 | 1011 | 1 | 异或 0011 | 0101 |
| 11 | 0 | 0101 | 0 | 不移除 | 1010 |
处理完全部 7 个数据位 + 4 个补零位后,寄存器值 1010 正是 CRC。长除法和移位寄存器是同一件事的两种描述,而后者正是软件(和硬件)真正实现时用的方法。
5. CRC 代码实现
下面给出两种主流实现思路:按位计算 (直观、易理解,适合教学和低速场景)和查表法(一次处理一个字节,速度提升 8 倍,工业界最常用)。
(1)Matlab 代码实现
1.1 按位计算(与第 4 节实例完全一致)
matlab
function crc_bits = crc_bitwise(data_bits, poly_bits)
% CRC 按位计算(模2除法 / 异或),与手工推导完全一致
% 输入:
% data_bits : 数据位行向量, 如 [1 0 1 1 0 0 1]
% poly_bits : 生成多项式位行向量, 如 [1 0 0 1 1] (x^4 + x + 1)
% 输出:
% crc_bits : CRC 校验码位向量, 如 [1 0 1 0]
n = length(poly_bits) - 1; % CRC 位数 = 多项式次数
bits = [data_bits, zeros(1, n)]; % 数据后补 n 个 0
% 逐位模2除法: 当前位为 1 就与多项式异或
for i = 1 : length(bits) - n
if bits(i) == 1
bits(i : i + n) = xor(bits(i : i + n), poly_bits);
end
end
crc_bits = bits(end - n + 1 : end); % 最后 n 位即余数
end
验证第 4 节的实例:
matlab
data = [1 0 1 1 0 0 1]; % 数据 1011001
poly = [1 0 0 1 1]; % G(x) = x^4 + x + 1
crc = crc_bitwise(data, poly);
disp(crc); % 输出: 1 0 1 0 ✓
1.2 查表法计算 CRC-32(以太网标准)
matlab
function crc_out = crc32_lookup(data_bytes)
% 查表法计算 CRC-32(以太网标准, 多项式 0x04C11DB7 的反向形式 0xEDB88320)
% 输入: data_bytes - uint8 类型数组
% 输出: crc_out - uint32 校验值
crc = uint32(hex2dec('FFFFFFFF')); % 初始值
% 1. 生成 256 项查找表(只需生成一次)
table = zeros(256, 1, 'uint32');
for i = 0 : 255
c = uint32(i);
for j = 1 : 8
if bitand(c, uint32(1))
c = bitxor(bitshift(c, -1), uint32(hex2dec('EDB88320')));
else
c = bitshift(c, -1);
end
end
table(i + 1) = c;
end
% 2. 逐字节查表计算
for k = 1 : numel(data_bytes)
idx = bitxor(bitand(crc, uint32(255)), uint32(data_bytes(k))) + 1;
crc = bitxor(bitshift(crc, -8), table(idx));
end
% 3. 结果异或输出
crc_out = bitxor(crc, uint32(hex2dec('FFFFFFFF')));
end
调用示例(用标准测试向量验证):
matlab
data = uint8('123456789');
crc = crc32_lookup(data);
fprintf('CRC-32 = 0x%08X\n', crc); % 输出: CRC-32 = 0xCBF43926 ✓(标准值)
(2)C 代码实现
2.1 按位计算(通用函数)
c
#include <stdint.h>
#include <stdio.h>
/**
* 按位计算 CRC(MSB 优先,逐位移位 + 异或)
* @param data 输入数据(如 0x59 = 0b1011001)
* @param data_len 数据有效位数(如 7)
* @param poly CRC 多项式去掉最高位后的值
* 如 x^4+x+1(10011) → 低4位 0011 = 0x3
* @param crc_len CRC 位数(如 4)
* @return CRC 校验值(低 crc_len 位有效)
*/
uint32_t crc_bitwise(uint32_t data, int data_len, uint32_t poly, int crc_len)
{
uint32_t reg = 0;
int i;
for (i = 0; i < data_len + crc_len; i++) {
/* 依次取出数据位,取完数据位后自动补 0(等效于末尾补 crc_len 个 0)*/
uint32_t bit = (i < data_len) ? ((data >> (data_len - 1 - i)) & 1) : 0;
uint32_t msb = (reg >> (crc_len - 1)) & 1; /* 移出位 */
reg = ((reg << 1) | bit) & ((1u << crc_len) - 1);
if (msb) {
reg ^= poly;
}
}
return reg;
}
int main(void)
{
/* 数据 1011001 = 0x59, 7 位有效位, G = x^4+x+1 → poly=0x3, CRC 4 位 */
uint32_t crc = crc_bitwise(0x59, 7, 0x3, 4);
printf("CRC = %04X\n", crc); /* 输出: CRC = 000A (即 1010) ✓ */
return 0;
}
2.2 查表法计算 CRC-16/MODBUS(工业总线最常用)
c
#include <stdint.h>
#include <stddef.h>
#define CRC16_POLY 0xA001 /* 0x8005 按位反转得到的反向多项式 */
static uint16_t crc16_table[256];
/* 生成查找表(程序初始化时调用一次即可) */
void crc16_init(void)
{
int i, j;
for (i = 0; i < 256; i++) {
uint16_t crc = (uint16_t)i;
for (j = 0; j < 8; j++) {
if (crc & 0x0001)
crc = (crc >> 1) ^ CRC16_POLY;
else
crc >>= 1;
}
crc16_table[i] = crc;
}
}
/* 计算 CRC-16/MODBUS: 初始值 0xFFFF, 输入输出反转, 结果不异或 */
uint16_t crc16_modbus(const uint8_t *buf, size_t len)
{
uint16_t crc = 0xFFFF;
size_t i;
for (i = 0; i < len; i++) {
crc = (uint16_t)((crc >> 8) ^ crc16_table[(crc ^ buf[i]) & 0xFF]);
}
return crc;
}
调用示例:
c
#include <stdio.h>
int main(void)
{
uint8_t data[] = "123456789";
crc16_init();
printf("CRC-16/MODBUS = 0x%04X\n", crc16_modbus(data, 9));
/* 输出: CRC-16/MODBUS = 0x4B37 ✓(标准测试向量)*/
return 0;
}
2.3 查表法计算 CRC-32(以太网/PNG/ZIP 标准)
c
#include <stdint.h>
#include <stddef.h>
static uint32_t crc32_table[256];
/* 生成查找表 */
void crc32_init(void)
{
int i, j;
for (i = 0; i < 256; i++) {
uint32_t crc = (uint32_t)i;
for (j = 0; j < 8; j++) {
crc = (crc & 1) ? (crc >> 1) ^ 0xEDB88320 : crc >> 1;
}
crc32_table[i] = crc;
}
}
/* 计算 CRC-32: 初始值 0xFFFFFFFF, 输入输出反转, 结果异或 0xFFFFFFFF */
uint32_t crc32(const uint8_t *buf, size_t len)
{
uint32_t crc = 0xFFFFFFFF;
size_t i;
for (i = 0; i < len; i++) {
crc = crc32_table[(crc ^ buf[i]) & 0xFF] ^ (crc >> 8);
}
return crc ^ 0xFFFFFFFF;
}
调用示例:
c
#include <stdio.h>
int main(void)
{
uint8_t data[] = "123456789";
crc32_init();
printf("CRC-32 = 0x%08X\n", crc32(data, 9));
/* 输出: CRC-32 = 0xCBF43926 ✓(标准测试向量)*/
return 0;
}
2.4 按位法与查表法对比
| 对比项 | 按位法 | 查表法 |
|---|---|---|
| 计算速度 | 慢(每比特都要判断/异或) | 快(一次处理 1 字节,快约 8 倍) |
| 内存占用 | 几乎为 0 | 需 256 × CRC 位数的查找表(如 CRC-32 需 1KB) |
| 代码复杂度 | 简单直观 | 稍复杂,但逻辑固定可复用 |
| 适用场景 | 教学、低速、表空间紧张(如 51 单片机小 RAM) | 高速通信、大量数据、桌面/服务器程序 |
工程建议:追求速度用查表法;追求代码简洁或内存极小用按位法;如果 MCU 自带硬件 CRC 外设(如 STM32 的 CRC 单元),直接调用硬件即可,速度最快且不占 CPU。
6. 总结
CRC虽然是很简单的校验算法,但是在很多行业都有使用,理解其核心算法思想,后续在工作中可以随意结合其它算法使用。如果有什么不合理的地方,欢迎大家留言讨论!