用源码先行的方式讲透CRC校验算法:常用多项式、移位寄存器原理、按位/查表/半字节查表实现、正反相运算,附可直接编译运行的C语言代码,帮你彻底看懂Modbus、CAN、无线通信里的CRC。
结论先行:CRC 校验的本质就是"移位 + 异或"两件事,任何速度优化——无论是全字节查表还是半字节查表——都是把"连续异或"这个动作预先算好、用一张表换执行时间。只要理解了一种多项式的位运算逻辑,CRC8、CRC16、CRC-CCITT、CRC32 全部是举手之劳。本文以一串真实数据为例,给出可直接在 Keil C 7.10 下编译运行的按位、全字节查表、半字节查表、正反相四种完整实现,并附上计算结果用于自检。
很多初次接触 CRC 的工程师,习惯从数学表达式的角度去啃多项式,结果被一长串 "X16+X12+X5+1" 之类符号搅得头晕。其实对实际开发而言,远比推导更有效的是盯着源码看机器到底做了什么。本文选择【源码先行】的组织方式,把最常用的 CRC-CCITT(0x1021)一步步拆给你看,再给出查表法如何演化而来,以及正反相运算的区别与坑点。
CRC(Cyclic Redundancy Check,循环冗余校验)把一个数据帧看成一个大二进制数,用约定的多项式对它做模 2 除法,余数就是附加在帧尾的校验码。接收方用同一个多项式重新计算,若余数为 0 则认为数据无误。目前工程里常见的多项式权值见下表。
| 校验算法 | 生成多项式 | 校验位长度(bit) | 常见应用 |
|---|---|---|---|
| CRC8 | X8 + X5 + X4 + 1 | 8 | DS18B20 温度、简单传感器链路 |
| CRC-CCITT | X16 + X12 + X5 + 1 | 16 | XMODEM、蓝牙、SSD 主控 |
| CRC16 | X16 + X15 + X5 + 1 | 16 | Modbus RTU(初值 0xFFFF) |
| CRC12 | X12 + X11 + X3 + X2 + 1 | 12 | 磁盘/电信协议 |
| CRC32 | X32 + X26 + X23 + X22 + X16 + X12 + X11 + X10 + X8 + X7 + X5 + X4 + X2 + X1 + 1 | 32 | 以太网、FCS、ZIP/PNG |
| 实现方式 | 速度 | 代码/资源占用 | 适用场景 |
|---|---|---|---|
| 按位软算 | 慢(每 bit 循环) | 最小,无表 | 低速率、ROM/RAM 紧张的小 MCU |
| 半字节查表 | 中(每字节两次查表) | 小,仅 16 字节表 | 中速、资源折中 |
| 全字节查表 | 快(每字节一次查表) | 较大,256 字节表 | Modbus/无线高速通信主流 |
以 CRC-CCITT 的移位寄存器原理为例(X16+X12+X5+1,多项式常数 0x1021):数据从最低位开始逐位移入一个 16 位移位寄存器,凡是与 1 对应的位(Bit0、Bit5、Bit12)以及与移出位做异或的地方,都对应一次 XOR 运算。下面用按位实现的 C 代码把它讲清楚。
先给一段可以直接跑通的最小按位程序。数据指针指向一串 8 字节数据,len 为字节数,crc 初值 0,循环里每处理一个 bit 就判断一次 0x8000 位。
typedef unsigned char uchar;
typedef unsigned int uint;
code uchar crcbuff[] = { 0x00,0x00,0x00,0x00,
0x06,0x0d,0xd2,0xe3 };
uint crc; // CRC 码
void main(void) {
uchar *ptr;
crc = 0; // CRC 初值
ptr = crcbuff; // 指向第一个字节
crc = crc16l(ptr, 8);
while(1);
}
/* CRC-CCITT 按位运算,0x1021 为生成多项式 */
uint crc16l(uchar *ptr, uchar len) {
uchar i;
while(len--) {
for(i=0x80; i!=0; i>>=1) {
if((crc & 0x8000) != 0) { crc <<= 1; crc ^= 0x1021; }
else crc <<= 1;
if((*ptr & i) != 0) crc ^= 0x1021;
}
ptr++;
}
return crc;
}
这段代码的执行结果是 crc = 0xdbc0,你可以拿它做自检。程序里的三行可以这样理解:先把上次 CRC 的最高位(Bit15)与当前数据的对应位(*ptr&i)做异或,根据结果决定是否异或 0x1021。牢记一条规律——两次异或同一个数等于没异或,整个算法就一点都不神秘了。
很多资料直接甩出 256 项的表,初看很懵。它的来历其实很朴素:每移完 8 个 bit(一个字节),相当于把旧 CRC 的高 8 位全部移出,与这个字节做了异或,然后根据异或结果选一个"余式"再和旧 CRC 整体异或一次,就得到新的 CRC。余式共有 256 种可能,正是 0~255 以 0x1021 为权算出来的 CRC 码。
/* 0~255 以 CRC-CCITT 为权得到的余式表 */
code uint crc_ta[256] = {
0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef,
0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6,
0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de,
0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485,
0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d,
0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4,
0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc,
0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823,
0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b,
0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12,
0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a,
0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41,
0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49,
0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70,
0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78,
/* 后半段 128 项略,完整表见工程源码 */
0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0
};
/* 全字节查表法,每字节只需一次查表 */
uint table_crc(uchar *ptr, uchar len) {
uchar da;
while(len-- != 0) {
da = (uchar)(crc/256); // 暂存旧 CRC 高 8 位
crc <<= 8; // 左移 8 位
crc ^= crc_ta[da ^ *ptr]; // 高字节与当前数据异或再查表
ptr++;
}
return crc;
}
查表把逐 bit 的多次异或压缩成"一次查表 + 一次异或",是嵌入式通信里的主流做法。若内存极紧,还可以退化成半字节查表——用一张只有 16 项的表(0x0000~0xf1ef 前 16 个余式),每字节处理两次,代码量更小,速度比按位稍快、比全字节稍慢,属于典型的折中方案。
很多工程里用的是"反相"CRC。原因很简单:数据通信时信息字节常常先传低位,若重新排位会影响计算速度,于是专门设置了反转多项式。CRC-CCITT 的反转多项式是 0x8408——它是 0x1021 按位 reverse 的结果。反相只是把左移变成右移,本质不变。
/* CRC-CCITT 反相运算,0x8408 为反转多项式 */
uint crc16r(unsigned char *ptr, unsigned char len) {
unsigned char i;
while(len-- != 0) {
for(i=0x01; i!=0; i <<= 1) {
if((crc & 0x0001) != 0) { crc >>= 1; crc ^= 0x8408; }
else crc >>= 1;
if((*ptr & i) != 0) crc ^= 0x8408;
}
ptr++;
}
return crc;
}
验证方法:把数据倒过来(低位优先),即 crcbuff_fan[] = {0xe3,0xd2,0x0d,0x06,0x00,0x00,0x00,0x00},调用 crc16r 得到 0x5f1d。再把校验字节(0x1d,0x5f,低位在前)一并算进去,用同样代码算 10 个字节,结果 CRC 为 0——这正是 CRC 校验正确的判据。做反相时要注意:由于是右移,从寄存器移出的是低位,所以余式表要用反转后的数据重新生成,不能直接复用前面 0x1021 的那张表。
到了真机开发,还有几个需要执行的决策:
一句话收尾:只要抓住"移位 + 异或"这个本质,多项式选择、查表优化、正反相切换都只是同一件事的不同形态,本文的四段 C 代码就是你可以直接复制进工程的最小完整实现。
本文基于《CRC 校验源码分析》技术资料整理优化。
沧州艾诺威电子 — 国家高新技术企业,20+项国家专利
嵌入式系统开发 · 物联网方案 · AI智能硬件 · 一站式交付
电话:13930711029 | 邮箱:tech@czinv.com | 24小时内响应