CRC(Cyclic Redundancy Check,循环冗余校验)是一种 基于多项式除法的差错检测技术,广泛应用于数据传输和存储场景(如网络通信、磁盘存储、串口传输等),用于判断数据在传输或存储过程中是否发生了比特翻转等错误。
Part 1. CRC概述
核心原理
CRC的本质是将数据看作二进制多项式,通过模2除法计算余数,将余数作为校验码附加到原始数据后。接收方对包含校验码的完整数据重复相同的计算,如果余数为0,则判定数据无错;否则判定数据出错。
关键概念
- 原始数据:待传输的二进制序列,对应多项式 D ( x ) D(x) D(x)。
- 生成多项式:预先约定的固定二进制序列,对应多项式 G ( x ) G(x) G(x),是CRC的核心参数(如CRC-32的生成多项式为 x 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 + 1 x^{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+1 x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1)。
- 校验码(CRC码):原始数据左移 r r r 位( r r r 是生成多项式的阶数,即最高次幂)后,除以生成多项式得到的余数,长度为 r r r 位。
计算步骤(发送方)
- 设生成多项式 G ( x ) G(x) G(x) 的阶数为 r r r,将原始数据 D ( x ) D(x) D(x) 左移 r r r 位,得到 D ′ ( x ) = D ( x ) × x r D'(x) = D(x) \times x^r D′(x)=D(x)×xr(相当于在原始数据末尾补 r r r 个0)。
- 对 D ′ ( x ) D'(x) D′(x) 与 G ( x ) G(x) G(x) 进行模2除法(模2运算规则:加法无进位,减法无借位,即 0 + 0 = 0 , 0 + 1 = 1 , 1 + 0 = 1 , 1 + 1 = 0 0+0=0, 0+1=1, 1+0=1, 1+1=0 0+0=0,0+1=1,1+0=1,1+1=0)。
- 得到余数 R ( x ) R(x) R(x),其长度为 r r r 位(不足补0),这就是CRC校验码。
- 将校验码附加到原始数据末尾,得到传输数据 T ( x ) = D ( x ) × x r + R ( x ) T(x) = D(x) \times x^r + R(x) T(x)=D(x)×xr+R(x)。
校验步骤(接收方)
- 接收传输数据 T ( x ) T(x) T(x)。
- 对 T ( x ) T(x) T(x) 与生成多项式 G ( x ) G(x) G(x) 进行模2除法。
- 若余数为0 → 数据无错;若余数不为0 → 数据出错。
常见CRC标准
不同的生成多项式对应不同的CRC标准,适用于不同场景:
| 标准 | 生成多项式(二进制) | 特点 | 应用场景 |
|---|---|---|---|
| CRC-8 | 100000111 100000111 100000111( x 8 + x 2 + x + 1 x^8+x^2+x+1 x8+x2+x+1) | 短校验码,计算快 | 小数据块校验 |
| CRC-16 | 10001000000100001 10001000000100001 10001000000100001( x 16 + x 15 + x 2 + 1 x^{16}+x^{15}+x^2+1 x16+x15+x2+1) | 经典标准,兼容性好 | 串口通信、Modbus |
| CRC-32 | 32位标准多项式(见上文) | 高检错率,抗干扰强 | 以太网、ZIP、磁盘 |
核心特点
- 检错能力强:
- 可以检测出所有奇数个比特错误。
- 可以检测出所有长度小于等于生成多项式阶数的突发错误(突发错误指连续多个比特翻转)。
- 对长度大于阶数的突发错误,检错概率约为 1 − 2 − r 1 - 2^{-r} 1−2−r( r r r 为校验码长度),CRC-32的检错概率接近100%。
- 计算高效:
- 可以通过硬件电路(移位寄存器)或软件算法快速实现,适合实时系统。
- 局限性:
- 仅能检错,不能纠错:发现错误后,只能要求重传,无法直接修正错误比特。
- 存在极小概率的“漏检”:不同的原始数据可能生成相同的CRC码,导致错误未被检测到(概率极低,可忽略)。
典型应用场景
- 网络通信:以太网帧、TCP/UDP数据包的校验。
- 存储系统:磁盘扇区、光盘数据、压缩文件(ZIP、RAR)的校验。
- 嵌入式通信:串口、CAN总线、SPI等协议的数据校验。
与其他校验方式的对比
| 校验方式 | 原理 | 检错能力 | 适用场景 |
|---|---|---|---|
| CRC | 多项式模2除法 | 强(可检测多类错误) | 实时传输、存储 |
| 奇偶校验 | 统计1的个数的奇偶性 | 弱(仅检测奇数个错误) | 简单低成本场景 |
| 校验和 | 字节累加求和取模 | 中等 | 网络协议(如IP头校验) |
Part 2. CRC(循环冗余校验)的有效性证明
要证明 CRC(循环冗余校验)的正确性,核心是证明:接收方对无差错的传输数据执行模2除法时,余数必然为0;若余数不为0,则数据一定存在差错。
证明过程基于多项式模2运算的代数性质,以下是严谨的推导步骤:
1. 定义核心多项式
首先明确发送方和接收方约定的关键参数:
- 设原始数据对应的二进制多项式为 D ( x ) D(x) D(x),长度为 k k k 位。
- 设生成多项式为 G ( x ) G(x) G(x),阶数为 r r r(即最高次项为 x r x^r xr),长度为 r + 1 r+1 r+1 位。
- 模2运算规则:加法/减法无进位/借位,满足 a + a = 0 a+a=0 a+a=0, a − a = 0 a-a=0 a−a=0,等价于异或运算。
2. 发送方的编码过程推导
发送方的编码步骤对应多项式运算:
-
原始数据左移 r r r 位,等价于多项式乘以 x r x^r xr,得到:
D ′ ( x ) = D ( x ) ⋅ x r D'(x) = D(x) \cdot x^r D′(x)=D(x)⋅xr
这一步的目的是为校验码预留 r r r 位空间。 -
对 D ′ ( x ) D'(x) D′(x) 执行模2除法 D ′ ( x ) ÷ G ( x ) D'(x) \div G(x) D′(x)÷G(x),根据多项式除法性质,可分解为:
D ′ ( x ) = Q ( x ) ⋅ G ( x ) + R ( x ) D'(x) = Q(x) \cdot G(x) + R(x) D′(x)=Q(x)⋅G(x)+R(x)
其中:- Q ( x ) Q(x) Q(x) 是商多项式;
- R ( x ) R(x) R(x) 是余数多项式,且 deg ( R ( x ) ) < r \text{deg}(R(x)) < r deg(R(x))<r(余数的阶数一定小于除数的阶数),因此 R ( x ) R(x) R(x) 对应一个 r r r 位二进制数,即 CRC校验码。
-
发送方将校验码附加到原始数据后,最终传输的多项式为:
T ( x ) = D ′ ( x ) + R ( x ) T(x) = D'(x) + R(x) T(x)=D′(x)+R(x)
代入上式可得:
T ( x ) = Q ( x ) ⋅ G ( x ) + R ( x ) + R ( x ) T(x) = Q(x) \cdot G(x) + R(x) + R(x) T(x)=Q(x)⋅G(x)+R(x)+R(x)
根据模2运算性质 R ( x ) + R ( x ) = 0 R(x)+R(x)=0 R(x)+R(x)=0,因此:
T ( x ) = Q ( x ) ⋅ G ( x ) \boldsymbol{T(x) = Q(x) \cdot G(x)} T(x)=Q(x)⋅G(x)这是CRC正确性的核心等式:传输多项式 T ( x ) T(x) T(x) 是生成多项式 G ( x ) G(x) G(x) 的整数倍。
3. 接收方的校验过程证明
接收方收到数据后,得到多项式 T ′ ( x ) T'(x) T′(x),分两种情况讨论:
情况1:数据无差错, T ′ ( x ) = T ( x ) T'(x)=T(x) T′(x)=T(x)
此时代入核心等式:
T
′
(
x
)
=
Q
(
x
)
⋅
G
(
x
)
T'(x) = Q(x) \cdot G(x)
T′(x)=Q(x)⋅G(x)
接收方执行模2除法
T
′
(
x
)
÷
G
(
x
)
T'(x) \div G(x)
T′(x)÷G(x),余数为:
Remainder
[
T
′
(
x
)
/
G
(
x
)
]
=
Remainder
[
Q
(
x
)
⋅
G
(
x
)
/
G
(
x
)
]
=
0
\text{Remainder}[T'(x)/G(x)] = \text{Remainder}[Q(x)\cdot G(x)/G(x)] = 0
Remainder[T′(x)/G(x)]=Remainder[Q(x)⋅G(x)/G(x)]=0
结论:无差错时,余数必然为0。
情况2:数据有差错, T ′ ( x ) = T ( x ) + E ( x ) T'(x)=T(x)+E(x) T′(x)=T(x)+E(x)
其中
E
(
x
)
≠
0
E(x) \neq 0
E(x)=0 是错误多项式,对应数据中比特翻转的位置。
此时接收方计算的余数为:
Remainder
[
T
′
(
x
)
/
G
(
x
)
]
=
Remainder
[
(
T
(
x
)
+
E
(
x
)
)
/
G
(
x
)
]
\text{Remainder}[T'(x)/G(x)] = \text{Remainder}[(T(x)+E(x))/G(x)]
Remainder[T′(x)/G(x)]=Remainder[(T(x)+E(x))/G(x)]
代入
T
(
x
)
=
Q
(
x
)
⋅
G
(
x
)
T(x)=Q(x)\cdot G(x)
T(x)=Q(x)⋅G(x),可得:
Remainder
[
(
Q
(
x
)
⋅
G
(
x
)
+
E
(
x
)
)
/
G
(
x
)
]
=
Remainder
[
E
(
x
)
/
G
(
x
)
]
\text{Remainder}[(Q(x)\cdot G(x)+E(x))/G(x)] = \text{Remainder}[E(x)/G(x)]
Remainder[(Q(x)⋅G(x)+E(x))/G(x)]=Remainder[E(x)/G(x)]
- 若 Remainder [ E ( x ) / G ( x ) ] ≠ 0 \text{Remainder}[E(x)/G(x)] \neq 0 Remainder[E(x)/G(x)]=0,则余数不为0,接收方判定数据出错。
- 若 Remainder [ E ( x ) / G ( x ) ] = 0 \text{Remainder}[E(x)/G(x)] = 0 Remainder[E(x)/G(x)]=0,则 E ( x ) E(x) E(x) 是 G ( x ) G(x) G(x) 的倍数,此时会发生漏检,但这种情况的概率极低( 1 / 2 r 1/2^r 1/2r),且可通过选择合适的 G ( x ) G(x) G(x) 进一步降低概率。
4. CRC检错能力的补充证明
CRC的强检错能力也可通过多项式性质证明,以两个典型结论为例:
-
检测所有奇数个比特错误
奇数个比特错误对应的错误多项式 E ( x ) E(x) E(x) 有奇数个非零项。
若生成多项式 G ( x ) G(x) G(x) 包含因子 ( x + 1 ) (x+1) (x+1)(即 G ( 1 ) = 0 G(1)=0 G(1)=0,模2运算下 1 + 1 = 0 1+1=0 1+1=0),则 E ( 1 ) = 1 E(1)=1 E(1)=1(奇数个1相加为1),因此 E ( x ) E(x) E(x) 不能被 G ( x ) G(x) G(x) 整除,余数不为0,错误可被检测。 -
检测所有长度 ≤ r \le r ≤r 的突发错误
突发错误是指连续 b b b 位比特翻转,对应的多项式为 E ( x ) = x i ( 1 + x + x 2 + ⋯ + x b − 1 ) E(x)=x^i(1+x+x^2+\dots+x^{b-1}) E(x)=xi(1+x+x2+⋯+xb−1),长度 b ≤ r b \le r b≤r。
由于 deg ( E ( x ) ) = i + b − 1 \text{deg}(E(x))=i+b-1 deg(E(x))=i+b−1,且 G ( x ) G(x) G(x) 阶数为 r r r, b ≤ r b \le r b≤r 时 E ( x ) E(x) E(x) 的最高次项小于 G ( x ) G(x) G(x) 的最高次项,无法被 G ( x ) G(x) G(x) 整除,余数不为0,错误可被检测。
总结
CRC的正确性本质是多项式模2运算的整除性:
- 无差错时,传输多项式是生成多项式的倍数 → 余数为0;
- 有差错时,错误多项式大概率不是生成多项式的倍数 → 余数不为0。
Part 3. CRC漏检概率的证明
CRC 漏检概率的核心,其完整表述是:当错误多项式 E ( x ) E(x) E(x) 是生成多项式 G ( x ) G(x) G(x) 的倍数时,CRC 会发生漏检,且在随机错误模型下,漏检的概率约为 1 / 2 r 1/2^r 1/2r( r r r 是生成多项式 G ( x ) G(x) G(x) 的阶数,即 CRC 校验码的长度)。
下面我们从错误模型、多项式整除性、概率推导三个层面,严谨证明这个结论:
1. 前提:随机错误模型假设
漏检概率的计算基于一个合理的工程假设:
- 数据在传输过程中,每个比特发生翻转的概率是独立的,且错误模式是随机分布的。
- 错误多项式 E ( x ) E(x) E(x) 的每一项系数(对应比特是否翻转)是 0 或 1 的随机变量,所有可能的 E ( x ) E(x) E(x) 出现的概率均等。
2. 漏检的充要条件
根据之前的 CRC 正确性证明,接收方计算的余数为:
Remainder
[
T
′
(
x
)
/
G
(
x
)
]
=
Remainder
[
E
(
x
)
/
G
(
x
)
]
\text{Remainder}[T'(x)/G(x)] = \text{Remainder}[E(x)/G(x)]
Remainder[T′(x)/G(x)]=Remainder[E(x)/G(x)]
漏检发生的充要条件是:
Remainder
[
E
(
x
)
/
G
(
x
)
]
=
0
\text{Remainder}[E(x)/G(x)] = 0
Remainder[E(x)/G(x)]=0
等价于:
E
(
x
)
=
K
(
x
)
⋅
G
(
x
)
E(x) = K(x) \cdot G(x)
E(x)=K(x)⋅G(x)
其中
K
(
x
)
K(x)
K(x) 是任意非零多项式。也就是说,只有当错误模式恰好对应
G
(
x
)
G(x)
G(x) 的某个倍数时,才会漏检。
3. 概率推导:为什么漏检概率是 1 / 2 r 1/2^r 1/2r
我们可以从余数空间的大小和错误多项式的分布两个角度分析:
-
余数空间的大小
对于阶数为 r r r 的生成多项式 G ( x ) G(x) G(x),任何多项式除以 G ( x ) G(x) G(x) 得到的余数 R ( x ) R(x) R(x) 满足:
deg ( R ( x ) ) < r \text{deg}(R(x)) < r deg(R(x))<r
因此余数 R ( x ) R(x) R(x) 对应的二进制序列长度为 r r r 位,总共有 2 r 2^r 2r 种可能的余数(从 000 ⋯ 0 000\cdots0 000⋯0 到 111 ⋯ 1 111\cdots1 111⋯1)。 -
随机错误下的余数分布
在随机错误模型中,错误多项式 E ( x ) E(x) E(x) 是完全随机的,因此它除以 G ( x ) G(x) G(x) 得到的余数也是均匀分布在 2 r 2^r 2r 种可能中的。- 余数为 0 0 0 的情况,恰好对应漏检场景,仅占 1 1 1 种。
- 其余 2 r − 1 2^r-1 2r−1 种余数均会被检测到。
-
最终概率计算
漏检概率 = (余数为 0 的情况数) / (总余数情况数) = 1 2 r \frac{1}{2^r} 2r1
4. 关键补充:为什么实际漏检概率极低
- 对于 CRC-32( r = 32 r=32 r=32),漏检概率为 1 / 2 32 ≈ 2.3 × 1 0 − 10 1/2^{32} \approx 2.3 \times 10^{-10} 1/232≈2.3×10−10,这是一个几乎可以忽略的极小值。
- 工程中选择的生成多项式(如 CRC-32 标准多项式)还会额外满足一些性质(比如包含因子 x + 1 x+1 x+1),可以进一步排除特定类型的错误(如奇数个比特错误),让实际漏检概率比理论值更低。
总结
CRC 漏检的本质是错误多项式恰好是生成多项式的倍数,在随机错误模型下,由于余数均匀分布在 2 r 2^r 2r 种可能中,因此漏检概率为 1 / 2 r 1/2^r 1/2r,且 r r r 越大(校验码越长),漏检概率越低。
Part 4. CRC-16 的C语言实现
以下是 CRC-16(标准多项式:0x8005,对应
x
16
+
x
15
+
x
2
+
1
x^{16}+x^{15}+x^2+1
x16+x15+x2+1)的C语言实现,包含「逐位计算法」和「查表优化法」两种核心方式(前者直观易懂,后者效率更高),并附带详细注释和测试用例,帮助理解CRC的计算过程。
一、CRC-16 核心参数说明
- 生成多项式:
G
(
x
)
=
x
16
+
x
15
+
x
2
+
1
G(x) = x^{16} + x^{15} + x^2 + 1
G(x)=x16+x15+x2+1 → 二进制
1000000000000101→ 十六进制0x8005 - 校验码长度: r = 16 r=16 r=16 位(2字节)
- 初始值:
0xFFFF(常见约定,也可根据协议调整为0x0000) - 异或输出:
0xFFFF(最终结果需与该值异或,部分协议可能为0x0000) - 运算规则:高位优先(MSB First)
二、实现代码(含两种方法+测试用例)
#include <stdint.h>
#include <stdio.h>
#include <string.h>
// -------------------------- 方法1:逐位计算法(直观易懂,适合理解原理)--------------------------
/**
* @brief CRC-16 逐位计算(高位优先)
* @param data: 待校验的原始数据
* @param len: 原始数据长度(字节数)
* @return 16位CRC校验码(uint16_t)
*/
uint16_t crc16_bitwise(const uint8_t *data, uint32_t len) {
const uint16_t poly = 0x8005; // 生成多项式 G(x) = x^16 + x^15 + x^2 + 1
uint16_t crc = 0xFFFF; // 初始值(约定值)
// 1. 遍历每一个字节(原始数据的所有字节)
for (uint32_t i = 0; i < len; i++) {
// 2. 将当前字节与CRC的高8位异或(CRC共16位,高8位是 crc >> 8)
crc ^= (uint16_t)(data[i] << 8); // 左移8位,对齐CRC高8位
// 3. 遍历当前字节的每一位(8位)
for (uint8_t j = 0; j < 8; j++) {
// 4. 判断CRC最高位是否为1:若为1,左移后异或多项式;若为0,仅左移
if (crc & 0x8000) { // 0x8000 是 16位的最高位(第15位)
crc = (crc << 1) ^ poly;
} else {
crc <<= 1; // 最高位为0,左移后无需异或
}
}
}
// 5. 最终结果与 0xFFFF 异或(约定输出处理)
return crc ^ 0xFFFF;
}
// -------------------------- 方法2:查表优化法(效率更高,工程常用)--------------------------
// 步骤1:预生成CRC-16 查找表(256个值,对应0~255每个字节的CRC结果)
static uint16_t crc16_table[256] = {0};
/**
* @brief 预生成CRC-16查找表(程序启动时调用1次即可)
*/
void crc16_init_table(void) {
const uint16_t poly = 0x8005;
// 遍历0~255所有可能的字节值
for (uint16_t i = 0; i < 256; i++) {
uint16_t crc = i << 8; // 对齐16位CRC的高8位
// 逐位计算当前字节的CRC值,存入表中
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ poly;
} else {
crc <<= 1;
}
}
crc16_table[i] = crc;
}
}
/**
* @brief CRC-16 查表计算(高位优先,效率是逐位法的8倍)
* @param data: 待校验的原始数据
* @param len: 原始数据长度(字节数)
* @return 16位CRC校验码(uint16_t)
*/
uint16_t crc16_table_lookup(const uint8_t *data, uint32_t len) {
uint16_t crc = 0xFFFF; // 初始值
for (uint32_t i = 0; i < len; i++) {
// 核心:用当前字节与CRC高8位异或,作为查表索引,再更新CRC
crc = (crc << 8) ^ crc16_table[(crc >> 8) ^ data[i]];
}
return crc ^ 0xFFFF; // 最终异或输出
}
// -------------------------- 测试用例 --------------------------
int main(void) {
// 测试数据1:"123456789"(CRC-16标准测试数据,预期结果:0xBB3D)
const uint8_t test_data1[] = "123456789";
uint32_t len1 = strlen((const char *)test_data1);
// 测试数据2:自定义数据(如0x01, 0x02, 0x03),可自行验证
const uint8_t test_data2[] = {0x01, 0x02, 0x03, 0x04, 0x05};
uint32_t len2 = sizeof(test_data2) / sizeof(test_data2[0]);
// 初始化查找表(仅需1次)
crc16_init_table();
// 方法1:逐位计算测试
uint16_t crc1 = crc16_bitwise(test_data1, len1);
uint16_t crc3 = crc16_bitwise(test_data2, len2);
// 方法2:查表计算测试
uint16_t crc2 = crc16_table_lookup(test_data1, len1);
uint16_t crc4 = crc16_table_lookup(test_data2, len2);
// 输出结果
printf("=== CRC-16 测试结果 ===\n");
printf("测试数据1:%s\n", test_data1);
printf("逐位计算结果:0x%04X(预期:0xBB3D)\n", crc1);
printf("查表计算结果:0x%04X(预期:0xBB3D)\n\n", crc2);
printf("测试数据2:0x01, 0x02, 0x03, 0x04, 0x05\n");
printf("逐位计算结果:0x%04X\n", crc3);
printf("查表计算结果:0x%04X\n", crc4);
return 0;
}
三、代码核心逻辑解释(对应CRC原理)
1. 逐位计算法(重点理解)
完全对应之前讲的「多项式模2除法」,每一步都能映射到代数运算:
- 初始值
0xFFFF:相当于模2除法的「初始余数」(约定值,可调整)。 crc ^= (data[i] << 8):将当前字节(8位)与CRC的高8位异或,对应「多项式相加」(模2运算)。- 判断最高位
crc & 0x8000:对应模2除法中「除数(生成多项式)与被除数最高位对齐」,若被除数最高位为1,则减去除数(异或多项式)。 - 左移操作
crc << 1:对应模2除法的「被除数右移一位」(等价于多项式乘以 x x x)。 - 最终异或
0xFFFF:是CRC的约定输出处理,目的是避免全0数据的CRC结果为0(可选,部分协议无此步骤)。
2. 查表优化法(工程实用)
- 核心思想:预先生成「0~255每个字节对应的CRC中间结果」(存在
crc16_table中),计算时直接查表,无需逐位循环,效率提升8倍(因为每个字节少了8次位运算)。 - 查表公式
crc = (crc << 8) ^ crc16_table[(crc >> 8) ^ data[i]]:crc << 8:CRC左移8位,腾出低8位空间。(crc >> 8) ^ data[i]:用CRC的高8位与当前字节异或,得到查表索引(0~255)。- 异或查表结果:快速得到当前字节的CRC贡献值,更新CRC。
四、测试结果验证
编译运行代码后,输出应满足:
- 标准测试数据
"123456789"的CRC-16结果固定为0xBB3D(行业通用验证值),说明代码正确。 - 逐位法和查表法结果完全一致,证明两种实现等价。
五、灵活调整(适配不同协议)
如果需要适配其他CRC-16变种(如Modbus-RTU、CCITT),只需修改3个参数:
| 协议 | 生成多项式 | 初始值 | 异或输出 |
|---|---|---|---|
| 标准CRC-16 | 0x8005 | 0xFFFF | 0xFFFF |
| Modbus-RTU | 0x8005 | 0xFFFF | 0x0000 |
| CRC-16-CCITT | 0x1021 | 0x0000 | 0x0000 |
例如,若要实现Modbus-RTU的CRC-16,只需将代码中 return crc ^ 0xFFFF 改为 return crc ^ 0x0000 即可。
通过这段代码,可以直观看到:CRC的计算本质是「基于约定规则的位运算循环」,完全遵循多项式模2除法的代数逻辑。
1087

被折叠的 条评论
为什么被折叠?



