CRC校验码生成过程详解:多项式除法、移位寄存器实现

讲CRC,说到底就是讲怎么把一堆数据比特,跟一个约定的“多项式”做除法,然后拿余数当校验码。

我刚开始接触这个的时候,也觉得很绕。多项式除法?二进制除法?移位寄存器又是干嘛的?

后来在项目中调一个串口通信,数据老出错,逼着我彻底搞懂了它。今天我就把这个过程掰开了跟你讲,保证听完你也能手算出来。

1. 多项式表达:CRC的数学底座

我们平时说的CRC-16、CRC-32,其实背后都有一个多项式。这个多项式用二进制表示,就是除法的除数。

举个例子:CRC-8的多项式是 x⁸ + x² + x + 1。

怎么转成二进制?

  • 看指数:最高是8次方,代表二进制长度9位(指数+1)。
  • 有哪几项,哪几位就置1。
  • x⁸ → bit8 = 1;x² → bit2 = 1;x¹ → bit1 = 1;x⁰ → bit0 = 1。
  • 所以多项式二进制是:1 0000 0111(十六进制0x107)。

这里有个坑,我提醒你一下:最高位的1在计算时会被隐去。为什么?因为移位寄存器里,我们只看低n位(n是多项式最高次数)。这个后面会讲。

个人习惯:我喜欢把多项式写成十六进制,方便查表。比如CRC-16/CCITT是0x1021,CRC-32是0x04C11DB7。每次用之前,先确认一下多项式是MSB-first还是LSB-first,不然算出来的校验码是反的。

2. 多项式除法:手算CRC

说白了,就是二进制下的模2除法。跟普通除法区别就一点:减法用异或(XOR),不进位也不借位。

我拿一个简单例子演示:

  • 数据:0b1101(4比特)
  • 多项式:0b1011(4位,对应x³+x+1)
  • CRC-3,所以寄存器初始全0,数据后面补3个0 → 1101 000

手算步骤:

  1. 被除数 = 1101000,除数 = 1011。
  2. 看最高位,被除数第1位是1,商1。做XOR:1101 XOR 1011 = 0110。
  3. 结果高位为0,左移一位,补下一个比特0,变成1100。
  4. 最高位又是1,商1,XOR:1100 XOR 1011 = 0111。
  5. 左移一位,补下一位0,变成1110。
  6. 最高位1,XOR:1110 XOR 1011 = 0101。
  7. 左移,补最后一位0,变成1010。
  8. 最高位1,XOR:1010 XOR 1011 = 0001。
  9. 已经移完所有数据位+补位,最终余数001就是CRC校验码。

你想想看,这个过程中我们一直在做异或和移位,完全可以用硬件实现。

我曾经犯过的错:手算时忘记把数据后面补n个0(n是多项式次数)。结果怎么也算不对。记住:CRC余数位数 = 多项式最高次数,所以数据尾补n位,才能刚好把余数挤进寄存器里。

3. 移位寄存器实现:从数学到硬件

多项式除法的手算,说白了就是一遍遍“移位 → 看最高位 → 决定要不要异或”。这个逻辑用移位寄存器加几个异或门就能实现。

结构长这样:

  • 一个n位的移位寄存器(n = 多项式次数),初始全0。
  • 每个寄存器位后面,根据多项式对应位置,拉出一根线,经过异或门再反馈。
  • 数据比特从高位或低位串行移入,每移入一位,寄存器做一次移位+条件异或。

我画了张图,帮你理解这个结构:

CRC-3 移位寄存器实现结构(多项式 1011) D0 D1 D2 XOR 数据输入 反馈(多项式 bit0=1) 多项式 bit1=1 反馈 余数输出 数据从左侧移入,每移1位,D2输出与多项式对应位异或后反馈到D0。 多项式 1011 → D0前反馈(bit0=1),D1反馈(bit1=1),D2无直接反馈(bit2=0),最高位隐去。

看到没?每个寄存器D0、D1、D2之间串接,D2的输出(也就是最高位)决定是否启动XOR反馈。反馈的抽头位置,就是多项式为1的位置。

4. 软件实现:用代码模拟移位寄存器

搞嵌入式的时候,不一定都用硬件CRC单元。有时候为了省成本,或者处理器太老,我们就用软件模拟。

核心逻辑就三条:

  1. 把寄存器初始化为0(或全1,取决于CRC算法变种)。
  2. 每个数据比特,从高位到低位(或反之)移入寄存器。
  3. 每次移位后,如果移出的比特是1,就跟多项式做异或。

我贴一段C语言代码,模拟CRC-8(多项式0x07,不含高位):

uint8_t crc8_software(uint8_t *data, uint8_t len) {
    uint8_t crc = 0x00;               // 寄存器初始值
    uint8_t poly = 0x07;              // 多项式 x⁸ + x² + x + 1 的低8位

    for (uint8_t i = 0; i < len; i++) {
        crc ^= data[i];               // 逐字节异或
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x80) {         // 检查最高位
                crc = (crc << 1) ^ poly;
            } else {
                crc = crc << 1;
            }
        }
    }
    return crc;
}

代码很简单,就两个循环。外层按字节处理,内层按比特处理。如果你处理的数据量不大,效率也能接受。

我实际项目的做法:如果数据量超过几百KB,软件CRC慢得受不了。我就会查查手册,看片上有没有CRC外设。一般STM32、GD32都有硬件CRC模块,配置好多项式,直接扔DMA,CPU几乎零开销。但硬件CRC的初始值、输出异或、字节序这些细节,一定要对着参考手册配,否则算出来不一样。

5. 常见的CRC算法变种

同样是CRC-16,不同的厂商标准,结果可能不一样。原因就在于那几张“表”的差异。

参数 CRC-16/CCITT CRC-16/MODBUS CRC-32
多项式(含高位) 0x11021 0x18005 0x104C11DB7
初始值 0xFFFF 0xFFFF 0xFFFFFFFF
结果异或值 0x0000 0x0000 0xFFFFFFFF
输入反转 否 是(LSB First) 是
输出反转 否 是 是

这些参数看起来很繁琐,但理解了移位寄存器原理,就知道它们其实都是在调整“初始状态”和“最终处理”。

举个例子:初始值全1,相当于在数据前面先补了一串1,避免前导0让CRC全0。结果再异或一次,是让全0数据也能产生非零的CRC,用来检测全0帧。

6. 避坑指南

避坑1:多项式写成十六进制时,要不要包含最高位?不同资料写法不一样。我统一用“含高位”的写法,跟RFC文档对齐。比如CRC-16/CCITT,我写0x11021,而不是0x1021。算的时候心里清楚就行了。
避坑2:移位方向。有些标准是LSB-first,数据从低位移入。这时你的移位操作要反过来,检查最低位而不是最高位。
我曾经遇到的一个bug:在一个CAN通信项目中,两边用同一个CRC-16库,但一边配置了输入反转,另一边没配置。结果同样数据算出来的CRC就是不一样。排查了一下午才发现。从那以后,我每次对接协议,都会先让对方发一个已知测试向量,验证CRC计算一致,再往下做。

好了,今天把CRC的生成原理、手算过程、硬件结构、软件实现都过了一遍。核心就一句话:CRC就是用数据除以一个预设的多项式,把余数当校验码。无论是用移位寄存器还是用代码,本质都一样。

能把这个过程想清楚,后面理解CRC检错能力和误码排查,就轻松多了。