10、位操作与CRC校验:用位运算实现简单的CRC-8算法

说到CRC校验,很多刚入行的朋友会觉得这东西很玄乎。其实说白了,CRC就是一种用二进制除法算出来的校验码。我当年第一次接触CRC是在一个串口通信项目里,数据传着传着就乱码了,加个CRC尾巴,世界就清净了。

今天咱们就用位运算,手撸一个CRC-8算法。你想想看,位操作和CRC简直就是天生一对——移位、异或,全是位运算的看家本领。

CRC-8的原理,一句话讲清楚

CRC校验的核心思想是:把数据看作一个很长的二进制数,然后除以一个固定的生成多项式,余数就是CRC校验码。

对于CRC-8,生成多项式通常写成 0x07(对应多项式 x⁸ + x² + x + 1)。接收方用同样的多项式去除收到的数据,如果余数为0,说明数据没被篡改。

嗯,这里要注意:除法不是普通的算术除法,而是模2除法——说白了就是不带进位的异或运算。

位运算实现CRC-8的步骤

我个人习惯把CRC计算分成三步走:

  1. 初始化:CRC寄存器清零或置初值
  2. 逐位处理:对每个数据位,左移CRC寄存器,判断最高位
  3. 异或多项式:如果最高位是1,就异或生成多项式

听起来是不是有点像移位寄存器?没错,硬件上CRC就是这么干的,我们用软件模拟而已。

直接上代码:CRC-8的位运算实现

#include <stdint.h>

#define CRC8_POLY 0x07  // 生成多项式 x^8 + x^2 + x + 1

uint8_t crc8_bitwise(uint8_t *data, uint16_t len) {
    uint8_t crc = 0x00;  // 初始值,有些协议用0xFF
    
    for (uint16_t i = 0; i < len; i++) {
        crc ^= data[i];  // 新数据与当前CRC异或
        
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x80) {          // 检查最高位
                crc = (crc << 1) ^ CRC8_POLY;  // 左移后异或多项式
            } else {
                crc <<= 1;            // 只左移
            }
        }
    }
    
    return crc;
}

这段代码我用了很多年,简单可靠。核心就是内层那个8次循环——每个字节的8个bit,一个一个地喂给CRC寄存器。

关键点crc ^= data[i] 这一步是把新数据与当前CRC异或,相当于把数据字节放到CRC寄存器的低位。很多初学者会漏掉这一步,结果算出来的CRC永远不对。

查表法:用空间换时间

逐位法虽然直观,但效率不高。每个字节要循环8次,处理大量数据时CPU扛不住。我在一个数据采集项目里,每秒要处理几兆字节的流数据,逐位法直接让MCU跑满了。

解决方案就是查表法。提前算好0~255每个字节对应的CRC值,存成一张表。计算时直接查表,一个字节一次异或就搞定。

// 生成CRC-8查表
void crc8_init_table(uint8_t *table) {
    for (uint16_t i = 0; i < 256; i++) {
        uint8_t crc = (uint8_t)i;
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x80) {
                crc = (crc << 1) ^ CRC8_POLY;
            } else {
                crc <<= 1;
            }
        }
        table[i] = crc;
    }
}

// 查表法计算CRC-8
uint8_t crc8_table(uint8_t *data, uint16_t len, uint8_t *table) {
    uint8_t crc = 0x00;
    for (uint16_t i = 0; i < len; i++) {
        crc = table[crc ^ data[i]];
    }
    return crc;
}

你看,查表法的核心代码就一行:crc = table[crc ^ data[i]]。效率比逐位法高了整整8倍。我曾经在一个项目里把CRC计算从逐位法改成查表法,CPU占用率从30%降到了4%。

两种方法的对比

对比项 逐位法 查表法
代码量 小,约10行 需要256字节表 + 少量代码
速度 慢,每字节8次循环 快,每字节1次查表
RAM占用 几乎为0 256字节(可放ROM)
适用场景 偶尔校验、代码空间紧张 高频通信、大数据量

我的建议:如果MCU的Flash够用,优先用查表法。256字节换8倍性能,这笔买卖太划算了。我在STM32、GD32、ESP32上都是这么干的。

避坑指南:我曾经踩过的坑

我曾经在一个Modbus协议的项目里,CRC怎么算都对不上主机。折腾了两天,最后发现是多项式方向搞反了——有的协议用正向移位(左移),有的用反向移位(右移)。

这里给大家几个经验:

  • 确认多项式方向:左移还是右移?查协议文档,别猜。
  • 初始值别搞错:有些协议初始值是0x00,有些是0xFF,错了全盘皆输。
  • 输出异或值:部分CRC-8最后还要异或一个固定值,比如0xFF。
  • 字节序:多字节数据是大端还是小端?这会影响CRC结果。

警告:不同协议用的CRC-8参数可能完全不同。比如CRC-8/ATM用的是0x07,CRC-8/MAXIM用的是0x31。千万别拿A协议的CRC去校验B协议的数据,结果肯定不对。

CRC-8的完整知识结构

下面这张图总结了CRC-8的核心逻辑,从数据输入到校验码输出,一目了然:

CRC-8 计算流程 输入数据字节 CRC ^= data[i] 8次循环 检查最高位 crc & 0x80 ? 左移后异或多项式 只左移

从这张图可以清楚看到:每个字节进来,先异或当前CRC值,然后逐位处理8次。每次判断最高位,决定是只左移还是左移后异或多项式。8位处理完,得到这个字节的CRC结果,再喂给下一个字节。

实际项目中的选择

我个人在实际项目中的选择标准很简单:

  • Bootloader、OTA升级:用查表法,因为要校验几十上百KB的数据,速度很重要。
  • 传感器单次读取:用逐位法,代码省空间,反正就几个字节。
  • 通信协议栈:查表法,而且要确认协议规定的多项式、初始值、输出异或值。

嗯,最后说一句:CRC-8虽然简单,但用在关键场合一定要仔细验证。我习惯用已知数据算一遍,跟在线CRC计算器对一下结果,确认无误再集成到项目里。

总结:位操作实现CRC-8,核心就是移位和异或。逐位法适合小数据量,查表法适合高性能场景。不管用哪种,参数配置一定要跟协议一致,否则校验永远过不了。


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