简介:CRC-8校验方法PDF是一份系统讲解循环冗余校验码在数据通信中应用的资料,面向嵌入式开发工程师、单片机学习者与通信专业学生。内容从CRC基本概念入手,说明信息字段与校验字段的配合方式,重点拆解生成多项式g(x)=x8+x5+x4+1对应的二进制100110001,并通过0x0102生成CRC8校验码的完整计算示例,逐步演示模二除法与按位异或对齐过程。针对DS18B20温度传感器,专门介绍了其采用的逆序CRC信息单元编码算法,与常规算法做了区分,并给出按位运算和查表法两种C语言实现,便于读者直接移植到项目中;内容预览还展示了查表数组与应用场景,方便实践对照。资源共1个PDF文件,压缩包整体仅242KB,内容精炼,适合在数据通信课程、单片机项目或嵌入式笔试复习中快速查阅。目前已有107人学习,原理讲解结合代码示例,是一份实用的CRC-8入门与进阶参考。
1. 一字节的 CRC-8 校验,为什么值得单独拆开讲
当一根单总线连着的 DS18B20 温度传感器偶尔回传 0xFF、0xFE 这类乱码时,很多开发者的第一反应是“时序又被中断打断了吧”,然后死磕 GPIO 延时。其实更常见的根因是读回来的字节根本没有经过 CRC-8 校验,或校验参数没对齐。CRC-8 是数据通信里最常用的短帧差错校验方式之一,只占 1 个校验字节,但能快速识别出帧内是否有单比特甚至部分多比特翻转。对 8 位单片机而言,它的计算量比 CRC-16 更可控,也因此被大量传感器、存储器和普通串口协议采用。这篇文章我会直接用多项式 g(x)=x^8+x^5+x^4+1 走一遍完整计算,并给出按位运算与查表法实现,以及 DS18B20 里那套“逆序编码”的 C 程序。无论你是刚接触协议栈的嵌入式新人,还是想搞清楚参数差异的工程师,都可以按图索骥。
2. 模二除法与生成多项式:先看懂 0x0102 为什么算出 0x96
2.1 多项式 g(x)=x^8+x^5+x^4+1 的二进制形态
CRC-8 以 8 位余数作为校验码,但生成多项式本身必须包含 x^8 项,所以写出来是 9 位。g(x)=x^8+x^5+x^4+1 展开成系数序列,从 x^8 到 x^0,依次是 1、0、0、1、1、0、0、0、1,对应二进制数 100110001,十六进制记作 0x131。这里的 0x131 是“含 x^8 的完整多项式”,而平时程序里常见的 poly=0x31 是去掉最高位后的低 8 位。两者并不是两个不同的多项式,只是在按位实现时一个用于判断是否需要“消最高位”,另一个作为左移后的异或常量。下面这个表把对应关系列清楚:
| 项 | x^8 | x^7 | x^6 | x^5 | x^4 | x^3 | x^2 | x^1 | x^0 |
|---|---|---|---|---|---|---|---|---|---|
| 系数 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 1 |
| 完整多项式 | 0x131 | ||||||||
| 去除 x^8(poly) | 0x31 |
先理解这个形态,再看模二除法就不会晕。模二除法本质上就是不断做异或,没有进位、没有借位。每次把除数的最高位对齐到被除数的当前位,异或后把后面的位继续拉下来,直到余数位数小于除数位数。整个过程里“减”和“加”都用异或代替,这也是为什么 CRC 算起来对 CPU 特别友好,一个异或门加几个移位寄存器就完成了。
2.2 手工推演 0x0102 的 CRC-8 校验码
发送端约定:给 2 字节数据 0x0102 计算一字节 CRC-8,然后把校验码附加在后面,得到 0x01 0x02 0x96。为什么是 0x96?计算过程可以分两步看。第一步,把信息字段整体左移 8 位,为校验码留出空间,0x0102 变成 0x010200。左移意味着低位补 0,相当于在数学上先乘上 x^8。第二步,用 0x010200 对多项式 0x131 做模二除法,最后的余数就是 0x96。
为了不在这里手抄一大段竖式,我直接用一个 Python 函数模拟标准 CRC-8/MAXIM 的按位过程,结果与手工推演一致:
def crc8_maxim(data: bytes) -> int: crc = 0 for b in data: crc ^= b for _ in range(8): if crc & 0x80: crc = ((crc << 1) ^ 0x31) & 0xFF else: crc = (crc << 1) & 0xFF return crc print(hex(crc8_maxim(b"\x01\x02"))) # 0x96这段代码先让 crc 与当前字节异或,再逐位处理 8 个 bit。if crc & 0x80判断最高位是否为 1,如果是,就在左移后与 0x31 异或;否则只左移。0x31 正是完整多项式 0x131 去掉最高位后的低 8 位,因为左移一位后 x^8 位会被移出 8 位寄存器,我们需要用 0x31 把它“带回来”。每次结果都& 0xFF保持 8 位宽度。这个实现对应参数是:初始值 0x00,输入不反转,输出不反转,输出不异或。它和手工模二除法得到的是同一份余数。
这里有一个容易混淆的点:手工竖式里用的是 0x131,代码里用的是 0x31。因为竖式里除数始终是 9 位,而代码用一个 8 位寄存器,当最高位溢出时自动用 0x31 补位。两种写法只是在描述同一件事,前者方便理解数学,后者方便落到工程。拿到 0x96 后,接收端把 0x01 0x02 0x96 三个字节整体做同样计算,得到的结果是 0x00,说明传输正确。
3. 常规 CRC-8 的 C 语言实现:从按位异或到查表
3.1 按位异或实现与参数说明
在单片机里我用短小精悍的 C 实现,尽量不依赖库。下面是最常见的 MSB-first 按位版:
#include <stdint.h> uint8_t crc8_msb(const uint8_t *data, size_t len) { uint8_t crc = 0x00; /* 初始值,CRC-8/MAXIM 通常为 0 */ while (len--) { crc ^= *data++; /* 输入字节参与计算 */ for (uint8_t i = 0; i < 8; i++) { if (crc & 0x80) { /* 最高位是 1 时 */ crc = (uint8_t)((crc << 1) ^ 0x31); } else { crc = (uint8_t)(crc << 1); } } } return crc; /* 返回 8 位 CRC 码 */ }crc ^= *data++实现输入字节与当前寄存器异或;每一步循环检查最高位,若为 1 则左移后与多项式 0x31 异或,否则直接左移。这个过程完全对应上一章的 Python 版,只是把分支写成了 if 形式。size_t len表示参与计算的字节数,比如计算 0x0102 时传 len=2,返回值是 0x96。常见错误是把 len 多传一位或者把多项式 0x31 错写成 0x131,前者会把校验码本身也算进去,后者会让结果差一个移位。
3.2 查表法:用 256 字节换时间
如果每个字节都走 8 次循环,数据量一大,8 位 MCU 的算力就不够看了。查表法是典型的时间换空间思路:预先对 0x00 到 0xFF 这 256 个输入值各算一次 CRC,运行时把crc ^ 当前字节作为索引直接取结果。表生成逻辑和按位版本完全相同,用 Python 生成后搬进工程:
def gen_crc8_table(poly=0x31): table = [] for i in range(256): crc = i for _ in range(8): if crc & 0x80: crc = ((crc << 1) ^ poly) & 0xFF else: crc = (crc << 1) & 0xFF table.append(crc) return table print([hex(x) for x in gen_crc8_table()[:8]]) # 输出示例:['0x0', '0x31', '0x62', '0x53', ...]上面生成的前几个表项可以用于自检:table[1]=0x31,table[2]=0x62,table[3]=0x53。生成后把数组原样搬到 C 里,查表函数就非常简单:
extern const uint8_t crc8_tab[256]; uint8_t crc8_table_calc(const uint8_t *data, size_t len) { uint8_t crc = 0x00; while (len--) { crc = crc8_tab[crc ^ *data++]; } return crc; }这里crc ^ *data++先让旧 CRC 与输入字节异或,再查表。表长度为 256 字节,在带几千字节 Flash 的 MCU 上完全可接受。如果 ROM 太紧张,也可以只保留按位版,或者用 4 bit 半字节查表法把表压缩到 16 项,但逻辑会稍绕。实际项目中我通常先用按位版调试,确认数据无误后,再把最终 CRC 表固化进工程,这样每一步都可在测试时对照。
3.3 常见参数对照
CRC 算法不是只有一个固定公式,换一下初始值、反转位序或输出异或,同一个多项式得到的结果完全不同。下面列出与本文相关的两组参数:
| 配置 | 完整多项式 | poly | init | refin | refout | xorout |
|---|---|---|---|---|---|---|
| CRC-8/MAXIM(常规) | 0x131 | 0x31 | 0x00 | false | false | 0x00 |
| DS18B20 总线 CRC | 0x131 | 0x8C | 0x00 | true | true | 0x00 |
DS18B20 这一行的 poly 是 0x8C,因为它的移位寄存器是低位先行,对 0x131 做位序反转后,x^8 位被移出,留下的低 8 位就是 0x8C。这直接带出下一章的内容。
4. DS18B20 的逆序 CRC-8:移位寄存器方向对多项式的影响
4.1 为什么 DS18B20 采用逆序编码
DS18B20 手册和 Maxim 应用笔记 Note27 里都强调,它内部的多项式寄存器是“逆序 CRC 信息单元编码”。这句话的工程含义是:数据从最低位开始进入移位寄存器,相当于常见 CRC-8/MAXIM 的位序被反转了。如果你直接套用上一章的 MSB-first 代码去校验 DS18B20 的序列号或温度存储器的第 9 字节,结果永远是错的。逆序之后,完整的 9 位多项式 0x131 被表示为 LSB-first 的 8 位常量 0x8C,也就是10001100。注意这里不能再用 0x31,否则每一个字节的处理次序都不对。
那么为什么 DS18B20 要这么做?因为它内部硬件就是移位寄存器加异或门,从低位逐位移入最省门电路。协议规定宿主机在软件里也必须配合这种位序,才能和硬件算出的 CRC 吻合。实际编程时,按位算法会不断检查当前寄存器的最低位,而异或常量从 0x31 变成 0x8C,本质上就是把“先左移再处理”翻转为“先右移再处理”。
4.2 按位运算实现:calcrc_1byte 与 calcrc_bytes
这一版是直接从 DS18B20 常用参考代码里整理的,逻辑清晰,一个字节一个字节地处理:
unsigned char calcrc_1byte(unsigned char abyte) { unsigned char i, crc_1byte; crc_1byte = 0; /* 初始化 CRC 寄存器 */ for (i = 0; i < 8; i++) { if ((crc_1byte ^ abyte) & 0x01) { /* 判断最低位 */ crc_1byte ^= 0x18; /* 反馈异或 0x18 */ crc_1byte >>= 1; /* 右移 */ crc_1byte |= 0x80; /* 补上最高位 */ } else { crc_1byte >>= 1; } abyte >>= 1; /* 准备下一位 */ } return crc_1byte; } unsigned char calcrc_bytes(unsigned char *p, unsigned char len) { unsigned char crc = 0; while (len--) { crc = calcrc_1byte(crc ^ *p++); } return crc; }先看calcrc_1byte。(crc_1byte ^ abyte) & 0x01取两者异或后的最低位,这一位就是当前输入位与寄存器反馈位的异或结果。如果它是 1,按位运算的规范做法是crc = (crc >> 1) ^ 0x8C,但代码里拆成了三步:先crc ^= 0x18,再右移,再|= 0x80。为什么是 0x18?因为0x18 >> 1 = 0x0C,右移后高位置 1 相当于补 0x80,三步行事的组合结果就是(crc >> 1) ^ 0x8C。写成这种拆解形式是为了贴近硬件寄存器动作,每一条语句都有明确含义。calcrc_bytes是外层循环,注意它先把上一个字节算出的 crc 与当前字节异或,再交给calcrc_1byte处理 8 位,这与查表法里crc ^ *p++的次序保持一致。如果最终返回值是 0,说明参与计算的字节和附带的 CRC 字节是匹配的。
4.3 查表法实现与表数据
DS18B20 的查表法同样是预生成 256 个可能结果,实际运行时一次索引得到 8 位结果。下面是 C 函数骨架和表头数据:
unsigned char crc8_table_ds18b20(unsigned char *p, char counter) { static const unsigned char crc8_table[256] = { 0x00, 0x5e, 0xbc, 0xe2, 0x61, 0x3f, 0xdd, 0x83, 0xc2, 0x9c, 0x7e, 0x20, 0xa3, 0xfd, 0x1f, 0x41, 0x9d, 0xc3, 0x21, 0x7f, 0xfc, 0xa2, 0x40, 0x1e, 0x5f, 0x01, 0xe3, 0xbd, 0x3e, 0x60, 0x82, 0xdc, /* 后续表项由生成脚本批量展开 */ }; unsigned char crc8 = 0; while (counter-- > 0) { crc8 = crc8_table[crc8 ^ *p++]; } return crc8; }表的前几项足够用来自检:查表索引 1 时应得到 0x5e,索引 2 时应得到 0xbc,索引 3 时应得到 0xe2。这些值与常规 CRC-8/MAXIM 的表完全不同,因为它们是按 LSB-first 预生成的。查表法省去了每字节 8 次循环,单字节计算只做一次异或加一次数组取值,在传感器数据连续读取、中断频繁的场景下收益明显。缺点也很直白:256 字节表在 1KB RAM 的老芯片上有点奢侈,建议放到 Flash 区,C 里用const修饰,编译后直接进只读段。
4.4 对 9 字节高速暂存器做整体校验
DS18B20 温度读取的典型数据帧是 9 字节,第 9 字节是前 8 个字节的 CRC。建议不要手动逐字节单独调用,而是把 9 个字节连续交给calcrc_bytes或查表函数,然后看返回值。返回 0 表示接收端重新计算的 CRC 与来的第 9 字节一致;返回非 0 则温度数据放弃使用,重新发一次转换命令。序列号 ROM 校验同理,前 7 字节参与计算,第 8 字节是 CRC,校验函数不应把第 8 字节排除在 len 之外——正确做法是把 8 个字节一起算,最后判断返回是否为 0。很多时序上抓不到的偶发错误,都能在这一步被尽早拦下。
5. 用 Python 反推 CRC-8 参数,验证结果不再靠猜
如果对上位机或调试日志里某个 CRC 值不放心,最快的方法是写 20 行 Python 把两端逻辑同时跑一遍。下面的函数同时给出 MSB 和 LSB 两种实现,直接就能对照:
def crc8_msb(data: bytes) -> int: crc = 0 for b in data: crc ^= b for _ in range(8): if crc & 0x80: crc = ((crc << 1) ^ 0x31) & 0xFF else: crc = (crc << 1) & 0xFF return crc def crc8_lsb(data: bytes) -> int: crc = 0 for b in data: crc ^= b for _ in range(8): if crc & 0x01: crc = (crc >> 1) ^ 0x8C else: crc >>= 1 return crc print(hex(crc8_msb(b"\x01\x02"))) # 0x96 print(hex(crc8_lsb(b"\x01\x02"))) # 0x69,位序反转产物crc8_msb对应常规 CRC-8/MAXIM,crc8_lsb对应 DS18B20。0x0102 的 LSB 结果恰好是 0x96 的位序反转值 0x69,这能帮你快速判断自己是不是用错了方向。用它验证固件时,我一般会把捕获的数据帧原样贴进 Python,先算再比,确认无误后才开始怀疑硬件链路。
实际调试中的一个小技巧:把 Python 按位函数里每一字节处理后的 crc 值打印出来,和 C 代码里加断点观测到的 crc 逐字节对比。一旦某个字节后不一致,就说明分歧出现在那一段。若按位版与查表版结果不一致,优先检查表是否生成错位,例如把 MSB 表误用到了 LSB 函数。若结果差一个固定值,检查输出有没有额外异或 0xFF;若只是首字节不同,检查初始值是否从 0 被改成 0xFF。最后一条经验:接收端校验时一定把“数据 + 收到的 CRC”一起送进函数,返回值固定为 0 才算通过,这样能顺带避免把 CRC 字节漏算。
本文还有配套的精品资源,点击获取