嵌入式CRC校验算法详解:从移位寄存器到查表法的完整C代码实现

用源码先行的方式讲透CRC校验算法:常用多项式、移位寄存器原理、按位/查表/半字节查表实现、正反相运算,附可直接编译运行的C语言代码,帮你彻底看懂Modbus、CAN、无线通信里的CRC。

2026-09-24
CRC校验查表法数据传输ModbusC语言数据校验

结论先行:CRC 校验的本质就是"移位 + 异或"两件事,任何速度优化——无论是全字节查表还是半字节查表——都是把"连续异或"这个动作预先算好、用一张表换执行时间。只要理解了一种多项式的位运算逻辑,CRC8、CRC16、CRC-CCITT、CRC32 全部是举手之劳。本文以一串真实数据为例,给出可直接在 Keil C 7.10 下编译运行的按位、全字节查表、半字节查表、正反相四种完整实现,并附上计算结果用于自检。

很多初次接触 CRC 的工程师,习惯从数学表达式的角度去啃多项式,结果被一长串 "X16+X12+X5+1" 之类符号搅得头晕。其实对实际开发而言,远比推导更有效的是盯着源码看机器到底做了什么。本文选择【源码先行】的组织方式,把最常用的 CRC-CCITT(0x1021)一步步拆给你看,再给出查表法如何演化而来,以及正反相运算的区别与坑点。

一、先认清 CRC 到底在算什么

CRC(Cyclic Redundancy Check,循环冗余校验)把一个数据帧看成一个大二进制数,用约定的多项式对它做模 2 除法,余数就是附加在帧尾的校验码。接收方用同一个多项式重新计算,若余数为 0 则认为数据无误。目前工程里常见的多项式权值见下表。

校验算法生成多项式校验位长度(bit)常见应用
CRC8X8 + X5 + X4 + 18DS18B20 温度、简单传感器链路
CRC-CCITTX16 + X12 + X5 + 116XMODEM、蓝牙、SSD 主控
CRC16X16 + X15 + X5 + 116Modbus RTU(初值 0xFFFF)
CRC12X12 + X11 + X3 + X2 + 112磁盘/电信协议
CRC32X32 + X26 + X23 + X22 + X16 + X12 + X11 + X10 + X8 + X7 + X5 + X4 + X2 + X1 + 132以太网、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 个余式),每字节处理两次,代码量更小,速度比按位稍快、比全字节稍慢,属于典型的折中方案。

四、正相与反相:Modbus 里的那个坑

很多工程里用的是"反相"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小时内响应