CRC 基础概念与原理

各位好,开始之前,我先问个问题:通信的时候,数据在线上跑,难免会遇到干扰。你收到一串数据,怎么判断它是不是完整的?

最简单的方法,就是发之前算一个“检验码“,收到后再算一遍,比对一下。CRC 就是这个思路,但它比简单的校验和要严谨得多。

我个人习惯把 CRC 理解成一种特殊的数学函数。它把数据看成一个巨大的二进制数,然后除以一个预先约定好的数(生成多项式),得到的余数就是 CRC 校验码。

接收方拿到完整数据后,用同样的步骤再算一次。如果余数为 0,说明数据大概率是对的。不为 0?那肯定出错了。

核心要点: CRC 的核心思想就是模 2 除法。它不产生进位,也不产生借位,说白了就是按位异或(XOR)。你想想看,硬件实现起来就非常快。

校验原理:到底怎么工作?

我刚开始接触的时候,觉得这东西有点玄学。后来撸了一遍算法,其实逻辑很清晰。整个过程分为三步:

  1. 发送端:根据原始数据,用生成多项式计算出 CRC 码,附加在数据尾部发送。
  2. 接收端:用同样的多项式,对收到的完整数据(原始数据 + CRC码)进行计算。
  3. 结果判断:如果余数为 0,则数据无误;若余数不为 0,则数据出错。

嗯,这里要注意,余数为 0 不代表数据没坏。只能说,坏的概率极低。尤其在噪声环境复杂的项目里,我见过好几次 CRC 碰撞的情况。

个人经验: 我在调试一款工业现场总线时,遇到过明明 CRC 验算通过了,但设备就是无法启动。后来发现,是物理层的电平抖动导致接收到的数据虽然是”合法“的,但实际数据已经被篡改。记住,CRC 只管”数学上的完整性“,不管”逻辑上的正确性“。

生成多项式:这个数怎么来的?

生成多项式是 CRC 的数学基础。它决定了一个 CRC 算法的检错能力。比如常见的 CRC-16、CRC-32,说的就是生成多项式的位数。

它不是随便写一个数就行的。好的多项式需要经过数学家的验证,能保证在固定的数据长度内,检测出所有单比特、双比特、奇数个比特的错误,以及一定长度的突发错误。

我列几个常用的给大家看看:

算法名称 生成多项式(简写) 常见应用
CRC-8 0x07 传感器、小数据包
CRC-16/IBM 0x8005 MODBUS 协议
CRC-32 0x04C11DB7 以太网、ZIP 压缩
CRC-CCITT 0x1021 蓝牙、PPP

避坑指南: 我曾经接手过一个项目,前任工程师用了 CRC-8 去校验 512 字节的数据包。结果是频频误报。后来我改成 CRC-16/IBM,问题就解决了。为啥?CRC-8 的检错能力在那個数据长度下撑不住了。所以,选多项式时一定要看数据长度和容错需求。

那么,生成多项式到底长什么样?你把它的二进制表示写出来,除了最高位的 1 之外,剩下的位就是比特位置。例如 CRC-8 的多项式 0x07,二进制是 00000111。但实际上包含了隐含的最高位 1,所以完整的多项式是 9 位:1 0000 0111。

我怎么做 CRC 计算?

原理简单,但手算的话,遇到大一点的数据会累死人。实际项目里,要么用查表法,要么直接用硬件。这里我给出一个纯软件的查表法实现,大家感受一下:


// 示例:生成 CRC-8 查表法
uint8_t crc8_table[256];
void generate_crc8_table(uint8_t poly) {
    for (int i = 0; i < 256; i++) {
        uint8_t crc = i;
        for (int j = 0; j < 8; j++) {
            if (crc & 0x80) {
                crc = (crc << 1) ^ poly;
            } else {
                crc <<= 1;
            }
        }
        crc8_table[i] = crc;
    }
}

// 计算 CRC-8
uint8_t compute_crc8(uint8_t *data, size_t len) {
    uint8_t crc = 0;
    for (size_t i = 0; i < len; i++) {
        crc = crc8_table[data[i] ^ crc];
    }
    return crc;
}

小提示: 上面的代码只是基础教学版。实际工程中,很多芯片厂商会提供 CRC 硬件模块,比如 STM32、NXP 的芯片。直接调用外设,算一次就一个指令周期的事,还不用浪费 CPU 时间。

比特反转与初始值

很多人做 CRC 失败,是因为忽略了两个细节:初始值和比特反转。

  • 初始值:在计算之前,CRC 寄存器要赋一个初值,常见的是 0x00 或 0xFF。
  • 输入/输出反转:有的算法要求输入数据的比特顺序反转,输出时也要反转。
  • 结果异或值:算完 CRC 后,有时还需要异或一个固定值输出。

这些参数构成了 CRC 算法的“模型”。你只把生成多项式对了,其他参数不对,结果还是会出错。我见过很多团队就因为这个问题,上线后 CRC 一直对不上,排查好几天。

CRC 校验核心逻辑 CRC 校验核心逻辑 发送数据 计算 CRC 值 CRC 附加到数据尾部 传输 接收数据 重新计算 CRC 比较余数是否为 0 余数为 0:数据正确 余数非 0:数据错误 说明 发送端 用生成多项式 计算 CRC 接收端 用相同多项式 重复计算 余数为 0? 数据可用 余数非 0? 丢弃或重传

你看这张图,整个流程其实很清晰。从发送数据到接收端验证,整个链条里,只要任何一个环节的 CRC 计算方式不一样,结果就炸了。

总结一下: CRC 看起来就一个异或过程,但背后牵扯到多项式的选择、初值、反转、数据长度等一堆细节。我的建议是:新手别自己发明多项式,直接用 IEEE 推荐的标准模型。写代码之前,先确认好参数列表。不然的话,调三天都调不出来。