ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CRC校验从原理到实现:余式、按位算法与查表优化详解

CRC校验从原理到实现:余式、按位算法与查表优化详解 如果最近你在调串口协议、报文完整性校验或者嵌入式设备的通信帧大概率会对“校验失败”这四个字印象深刻。尤其是自己照着网上代码写了一个 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 为什么名字里都带 CRCCRC 是 Cyclic Redundancy Check 的缩写中文通常翻译为循环冗余校验。Cyclic指它的移位和反馈结构是循环的Redundancy指数据中加入了额外冗余信息Check指它的用途是检测问题而不是自动纠错很多初学者会误以为 CRC 和加密、哈希是一回事。实际上CRC 不具备密码学上的防碰撞能力它并不刻意抵抗人为恶意修改。只要攻击者知道协议完全可以重新计算出一个合法的 CRC。它解决的是“意外噪声导致的数据错误检测”不是“防止别人篡改的安全机制”。2. 环境准备与算法参数2.1 运行环境本文示例代码使用 Python不需要安装第三方库。开发环境Windows / macOS / Linux 均可 Python 版本3.8 依赖包无如果你后续要接真实串口设备可以安装pyserialpip 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、32poly生成多项式写成十六进制通常是去掉最高位的形式init寄存器初值refin输入数据是否按位反射也叫低位优先refout输出结果是否再次反射xorout最终结果异或值check对标准测试数据123456789计算出的固定校验值不同协议会选用不同参数组合。哪怕 poly 相同只要 init 或者 xorout 不同最终结果也会不同。举个例子下面两种都是常见参数CRC-16/MODBUSpoly 0x8005反射多项式为0xA001init 0xFFFFrefin truerefout truexorout 0x0000CRC-32ZIP/GZIP 常见poly 0x04C11DB7反射多项式为0xEDB88320init 0xFFFFFFFFrefin truerefout truexorout 0xFFFFFFFF可以看到CRC-32 标准不仅初始化寄存器为全 1最终还要做一次异或。这些细节如果没有对齐你自己实现的算法和协议栈要求的算法就永远对不上。3. CRC 核心代码实现先看最简单、最容易理解的按位实现。这个版本虽然速度慢一点但可以直接和原理对应起来非常适合学习与排错。3.1 CRC-16/MODBUS 按位实现实现思路初始寄存器为0xFFFF。遍历每一个字节先将字节异或到寄存器的低位。对 8 个 bit 做循环。每次判断当前寄存器最低位是否为 1。如果为 1则右移一位后与反射多项式0xA001异或。如果为 0则只右移一位。返回结果时再次与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的位反转结果正好是0xA0010x8005 1000 0000 0000 0101 位反转后 1010 0000 0000 0001 0xA001所以这里直接用反射多项式0xA001作为循环中异或的常量。3.2 CRC-32 标准实现CRC-32 的代码结构和 CRC-16 非常像只是位宽从 16 位变成了 32 位所以掩码、反射多项式都不同。下面这个实现对应 ZIP、GZIP 等场景常用的标准 CRC-32def 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 标准要求xorout为0xFFFFFFFF也就是说算完寄存器里的值后还要把所有位取反。标准组织给出了一个很常用的验证值当输入字符串为123456789时CRC-32 的结果应为0xCBF43926如果你的代码对这个固定输入能算出0xCBF43926
返回列表