第28问:CRC校验——从原理到C语言实现

CRC校验,说白了就是给数据算个「指纹」。

我在嵌入式项目里用过不下十次CRC。从I2C通信到OTA升级包校验,几乎每个需要可靠传输的地方,都能看到它的身影。你可能会问:为什么不用简单的累加和?嗯,这个问题我当年也问过,直到有一次产品在现场出现了数据错乱,累加和愣是没检测出来——从那以后,我对CRC就格外上心了。

CRC的核心思想

CRC的全称是循环冗余校验。它的本质是:把数据看作一个巨大的二进制数,然后用一个固定的「除数」去做模2除法,得到的余数就是CRC值。

这里的关键是「模2除法」。它和我们小学学的除法不一样——它不考虑进位,说白了就是按位异或。你想想看,硬件实现起来就特别简单,几个异或门就能搞定。

核心公式:

发送数据 = 原始数据 × 2^n + CRC值

其中n是CRC的位数(8、16、32)。接收方用同样的多项式去除,余数为0则数据正确。

多项式——CRC的灵魂

每个CRC算法都有一个多项式。比如:

类型 多项式(十六进制) 多项式(二进制)
CRC-8 0x07 0000 0111
CRC-16/IBM 0x8005 1000 0000 0000 0101
CRC-32 0x04C11DB7 0000 0100 1100 0001 0001 1101 1011 0111

我个人习惯把多项式记成「生成多项式」。它决定了CRC的检错能力。多项式选得好,能检测出所有单比特错、双比特错、奇数个比特错,甚至突发错误。

小经验:我在做Modbus协议时,用的就是CRC-16/IBM。这个多项式在工业现场表现很稳定,误判率极低。

CRC的计算过程

咱们一步步来,别急。

  1. 初始化:CRC寄存器置为全1或全0(不同标准不一样)
  2. 逐字节处理:每个字节与CRC寄存器高8位异或
  3. 逐位运算:对每个比特,判断最高位是否为1
  4. 移位与异或:左移一位,如果最高位是1,就异或多项式
  5. 输出:处理完所有字节后,CRC寄存器取反(有些标准需要)

听起来有点绕?我画个图你就明白了。

CRC计算流程图 开始 初始化CRC寄存器(全1) 取下一个数据字节 字节与CRC高8位异或 循环处理8个比特(移位+异或)

CRC-8的C语言实现

先来个最简单的。CRC-8常用于1-Wire总线、SMBus等场合。

// CRC-8 查表法实现
// 多项式:0x07(x^8 + x^2 + x + 1)

static uint8_t crc8_table[256];

void crc8_init_table(void)
{
    for (int i = 0; i < 256; i++) {
        uint8_t crc = i;
        for (int j = 0; j < 8; j++) {
            if (crc & 0x80)
                crc = (crc << 1) ^ 0x07;
            else
                crc <<= 1;
        }
        crc8_table[i] = crc;
    }
}

uint8_t crc8_calc(uint8_t *data, uint32_t len)
{
    uint8_t crc = 0xFF;  // 初始值全1
    for (uint32_t i = 0; i < len; i++) {
        crc = crc8_table[(crc ^ data[i]) & 0xFF];
    }
    return crc ^ 0xFF;  // 输出取反
}

注意:我曾经在某个项目里忘了初始化查表,结果CRC算出来全是错的。排查了整整半天才发现——嗯,这种低级错误犯一次就够了。

CRC-16的C语言实现

CRC-16在工业通信里用得最多。Modbus、Profibus都用它。

// CRC-16/IBM 查表法
// 多项式:0x8005

static uint16_t crc16_table[256];

void crc16_init_table(void)
{
    for (int i = 0; i < 256; i++) {
        uint16_t crc = i << 8;
        for (int j = 0; j < 8; j++) {
            if (crc & 0x8000)
                crc = (crc << 1) ^ 0x8005;
            else
                crc <<= 1;
        }
        crc16_table[i] = crc;
    }
}

uint16_t crc16_calc(uint8_t *data, uint32_t len)
{
    uint16_t crc = 0xFFFF;
    for (uint32_t i = 0; i < len; i++) {
        crc = (crc << 8) ^ crc16_table[((crc >> 8) ^ data[i]) & 0xFF];
    }
    return crc;
}

你看,CRC-16和CRC-8的框架几乎一样。只是寄存器和多项式换成了16位的。这就是查表法的好处——一旦理解了核心逻辑,换什么多项式都只是改几个参数的事。

CRC-32的C语言实现

CRC-32是重量级选手。ZIP、PNG、以太网都用它。检错能力极强。

// CRC-32 查表法
// 多项式:0x04C11DB7

static uint32_t crc32_table[256];

void crc32_init_table(void)
{
    for (int i = 0; i < 256; i++) {
        uint32_t crc = i;
        for (int j = 0; j < 8; j++) {
            if (crc & 0x80000000)
                crc = (crc << 1) ^ 0x04C11DB7;
            else
                crc <<= 1;
        }
        crc32_table[i] = crc;
    }
}

uint32_t crc32_calc(uint8_t *data, uint32_t len)
{
    uint32_t crc = 0xFFFFFFFF;
    for (uint32_t i = 0; i < len; i++) {
        crc = (crc << 8) ^ crc32_table[((crc >> 24) ^ data[i]) & 0xFF];
    }
    return crc ^ 0xFFFFFFFF;
}

性能提示:查表法比直接计算快8倍左右。在STM32这类MCU上,CRC-32查表法处理1KB数据只需要几十微秒。如果你的MCU有硬件CRC模块(比如STM32的CRC外设),那就直接用硬件——又快又省心。

三种CRC的对比

特性 CRC-8 CRC-16 CRC-32
校验值长度 1字节 2字节 4字节
检错能力 一般 良好 极强
典型应用 1-Wire、小数据包 Modbus、工业总线 以太网、文件校验
查表内存开销 256字节 512字节 1024字节
处理速度 最快 较快 较慢(但可接受)

避坑指南

我踩过的坑,你最好别踩。

  • 初始值别搞错:不同标准初始值不同。CRC-8/MAXIM用0x00,CRC-8/DALLAS用0xFF。搞错了算出来全不对。
  • 输出要不要取反:有些标准最后要异或0xFF或0xFFFF。查一下你用的协议规范。
  • 字节序问题:多字节CRC要注意大小端。我在一个项目里,PC端算的CRC和MCU端对不上,最后发现是字节序反了。
  • 查表法要预初始化:别在每次计算时都重新建表。初始化一次,后面直接用。

我的建议:如果你刚开始用CRC,先拿CRC-16练手。它比CRC-8强,又比CRC-32简单。等熟悉了再升级到CRC-32。我在一个OTA项目里,就是用CRC-16做包内校验,CRC-32做整体固件校验——两层保险,稳得很。

CRC这东西,说白了就是「用空间换时间,用计算换可靠」。在嵌入式系统里,它是最实用的错误检测手段之一。你想想看,一个查表法CRC-16,512字节的表格,就能检测出几乎所有常见错误——这笔买卖,划算。


公众号:蓝海资料掘金营,微信deep3321