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计算分成三步走:
- 初始化:CRC寄存器清零或置初值
- 逐位处理:对每个数据位,左移CRC寄存器,判断最高位
- 异或多项式:如果最高位是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次。每次判断最高位,决定是只左移还是左移后异或多项式。8位处理完,得到这个字节的CRC结果,再喂给下一个字节。
实际项目中的选择
我个人在实际项目中的选择标准很简单:
- Bootloader、OTA升级:用查表法,因为要校验几十上百KB的数据,速度很重要。
- 传感器单次读取:用逐位法,代码省空间,反正就几个字节。
- 通信协议栈:查表法,而且要确认协议规定的多项式、初始值、输出异或值。
嗯,最后说一句:CRC-8虽然简单,但用在关键场合一定要仔细验证。我习惯用已知数据算一遍,跟在线CRC计算器对一下结果,确认无误再集成到项目里。
总结:位操作实现CRC-8,核心就是移位和异或。逐位法适合小数据量,查表法适合高性能场景。不管用哪种,参数配置一定要跟协议一致,否则校验永远过不了。
公众号:蓝海资料掘金营,微信deep3321