CRC校验从原理到实现:余式、按位算法与查表优化详解
2026/9/4 15:13:55 网站建设 项目流程

如果最近你在调串口协议、报文完整性校验或者嵌入式设备的通信帧,大概率会对“校验失败”这四个字印象深刻。尤其是自己照着网上代码写了一个 CRC 函数,明明把数据和多项式都填进去了,结果算出来的值怎么都对不上。折腾一晚后盯着终端里的十六进制输出,真的会有一种“余式哀嚎”的感觉。

这里的“余式”不是某个陌生名词,而是二进制多项式做除法之后留下的余项,术语里经常叫 remainder polynomial。很多 CRC 教程一上来就写多项式、异或、移位,看起来很高深,实际动手时反而会被初值、反射、字节序这些细节不断绊倒。本文会从一个常见的通信帧校验场景切入,把 CRC 的余式原理、按位实现、查表优化和排错思路完整串起来。没有额外工具依赖,纯 Python 就能运行。

适合对 CRC 只知道概念、还不清楚代码里为什么要这样写的读者,也适合想在项目里快速落地一个 CRC-16/MODBUS 或 CRC-32 校验函数的朋友。

1. 背景:CRC 校验里的“余式”到底指什么

1.1 为什么需要校验码

信道传输、串口通信、网络包传输都不能保证数据百分之百正确。电磁干扰、线缆接触不良、缓冲区溢出,都会造成某些 bit 位发生翻转。对大多数场景来说,我们不需要纠错,只要在接收端发现错误并丢弃或重发就行。

这种场景下,校验码是非常经济的方案。常见的有:

  • 奇偶校验
  • 校验和
  • CRC 循环冗余校验
  • 更复杂的消息认证码 MAC

奇偶校验实现简单,但只能发现单 bit 错误,检测能力有限。普通校验和(checksum)虽然实现容易,但对某些按块变化的错误不敏感。CRC 则用一个生成多项式对数据整体求余数,能够以较低的代价发现常见的突发错误,因此在串口、Modbus、CAN 总线、压缩文件、网络协议中被大量使用。

1.2 “余式”是什么

从数学角度看,CRC 可以把一段数据看作一个二进制多项式,每一位 bit 对应多项式的一项。比如二进制数据1101可以看成:

1 * x^3 + 1 * x^2 + 0 * x^1 + 1 * x^0

发送方在数据后面补 k 个 0,然后用一个双方约定好的生成多项式G(x)去做“模 2 除法”,得到的余数就是 CRC 校验码。由于计算机里的异或运算天然等价于模 2 加法,因此 CRC 非常容易用移位和异或指令实现。

这句话看起来很复杂,实际操作中其实不需要掌握多项式理论。只要记住关键一点:数据末尾追加的若干 bit,本质上就是除法余式。接收方拿到完整数据后,再对整串数据做一次同样的除法,如果余数为 0,就认为数据传输没有出错。

1.3 为什么名字里都带 CRC

CRC 是 Cyclic Redundancy Check 的缩写,中文通常翻译为循环冗余校验。

  • Cyclic指它的移位和反馈结构是循环的
  • Redundancy指数据中加入了额外冗余信息
  • Check指它的用途是检测问题,而不是自动纠错

很多初学者会误以为 CRC 和加密、哈希是一回事。实际上,CRC 不具备密码学上的防碰撞能力,它并不刻意抵抗人为恶意修改。只要攻击者知道协议,完全可以重新计算出一个合法的 CRC。它解决的是“意外噪声导致的数据错误检测”,不是“防止别人篡改的安全机制”。

2. 环境准备与算法参数

2.1 运行环境

本文示例代码使用 Python,不需要安装第三方库。

开发环境:Windows / macOS / Linux 均可 Python 版本:3.8+ 依赖包:无

如果你后续要接真实串口设备,可以安装pyserial

pip install pyserial

不过本文的重点是 CRC 算法本身,所以不依赖任何串口库,直接用字节数组模拟发送和接收即可。

建议的项目目录结构如下:

crc_demo/ ├── crc_lib.py # CRC 核心算法 ├── sender.py # 模拟发送端,构造带 CRC 的报文 ├── receiver.py # 模拟接收端,解析并校验 CRC └── test_crc.py # 用标准测试向量验证算法

2.2 一个很容易让人抓狂的事实:CRC 不是只有一种

很多初学者拿到代码时会发现,网上搜 CRC-16,能搜出好几种写法,CRC 结果还不一样。原因不是代码错了,而是 CRC 有非常多的参数变体。同一个“CRC-16”可能代表下面完全不同的参数组合:

参数含义
width校验码的位宽,例如 8、16、32
poly生成多项式,写成十六进制通常是去掉最高位的形式
init寄存器初值
refin输入数据是否按位反射,也叫低位优先
refout输出结果是否再次反射
xorout最终结果异或值
check对标准测试数据"123456789"计算出的固定校验值

不同协议会选用不同参数组合。哪怕 poly 相同,只要 init 或者 xorout 不同,最终结果也会不同。

举个例子,下面两种都是常见参数:

  • CRC-16/MODBUS:poly =0x8005,反射多项式为0xA001,init =0xFFFF,refin = true,refout = true,xorout =0x0000
  • CRC-32(ZIP/GZIP 常见):poly =0x04C11DB7,反射多项式为0xEDB88320,init =0xFFFFFFFF,refin = true,refout = true,xorout =0xFFFFFFFF

可以看到,CRC-32 标准不仅初始化寄存器为全 1,最终还要做一次异或。这些细节如果没有对齐,你自己实现的算法和协议栈要求的算法就永远对不上。

3. CRC 核心代码实现

先看最简单、最容易理解的按位实现。这个版本虽然速度慢一点,但可以直接和原理对应起来,非常适合学习与排错。

3.1 CRC-16/MODBUS 按位实现

实现思路:

  1. 初始寄存器为0xFFFF
  2. 遍历每一个字节,先将字节异或到寄存器的低位。
  3. 对 8 个 bit 做循环。
  4. 每次判断当前寄存器最低位是否为 1。
  5. 如果为 1,则右移一位后与反射多项式0xA001异或。
  6. 如果为 0,则只右移一位。
  7. 返回结果时再次与0xFFFF做与运算,确保结果是 16 位整数。
def crc16_modbus_bitwise(data: bytes) -> int: crc = 0xFFFF for byte in data: crc ^= byte for _ in range(8): if crc & 0x0001: crc = (crc >> 1) ^ 0xA001 else: crc >>= 1 return crc & 0xFFFF

为什么这里用的是0xA001,而不是生成多项式习惯上写的0x8005?因为 CRC-16/MODBUS 是反射输入、反射输出,计算时按照最低位优先处理。0x8005的位反转结果正好是0xA001

0x8005 = 1000 0000 0000 0101 位反转后 = 1010 0000 0000 0001 = 0xA001

所以这里直接用反射多项式0xA001作为循环中异或的常量。

3.2 CRC-32 标准实现

CRC-32 的代码结构和 CRC-16 非常像,只是位宽从 16 位变成了 32 位,所以掩码、反射多项式都不同。下面这个实现对应 ZIP、GZIP 等场景常用的标准 CRC-32:

def crc32_standard(data: bytes) -> int: crc = 0xFFFFFFFF for byte in data: crc ^= byte for _ in range(8): if crc & 1: crc = (crc >> 1) ^ 0xEDB88320 else: crc >>= 1 return crc ^ 0xFFFFFFFF

这里有两个容易看懵的地方:

第一个是0xEDB88320。标准 CRC-32 的生成多项式通常写作0x04C11DB7,它在反射处理方式下的等价形式是0xEDB88320。因为计算时按最低位优先,所以代码里直接用反射形式。

第二个是最后的crc ^ 0xFFFFFFFF。CRC-32 标准要求xorout0xFFFFFFFF,也就是说算完寄存器里的值后,还要把所有位取反。

标准组织给出了一个很常用的验证值:当输入字符串为"123456789"时,CRC-32 的结果应为:

0xCBF43926

如果你的代码对这个固定输入能算出0xCBF43926

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询