资讯动态

一套代码覆盖CRC-8到CRC-32:参数化CRC校验实现与踩坑排查

发布时间:2026/10/27 4:35:51 来源:尧图企业网站定制
简介这份循环冗余校验码CRC计算源代码合集面向嵌入式开发、通信协议调试与数据存储校验学习者聚焦CRC-8、CRC-16、CRC-32等常见算法的代码实现与工程验证。压缩包共1220个文件约6.65MB包含C与汇编源文件、头文件、工程配置、编译中间文件及hex、lst、m51等烧录与列表产物并带有crc8、crc16、crc32等示例数据便于对照不同实现方式、生成多项式与校验流程。已有4199人学习下载适合作为课程实验、项目移植和算法比对的参考素材。读者可从中了解逐位与查表思路、单片机软件优化及硬件CRC模块映射借助现成工程与测试数据验证结果、排查位序和初值差异进而提升数据传输与存储校验的实践能力。1. 调试台上的那两字节为什么我最后还是回去翻这套 CRC 源代码去年冬天陪一个做传感器的朋友调板子协议文档上只写了一句话——“末两字节为 CRC 校验”。他用自己攒的代码算了一晚上回显帧的最后两个字节总是差一位抓波形、换串口、怀疑电平都试过最后发现问题出在参数上设备用的是 CRC-16/Modbus初值 0xFFFF、输入输出都反射而他手里那份是 CCITT-FALSE 的写法。这类事在嵌入式和上位机对接里反复出现难的不是算法本身而是 CRC 的参数组合太多名字还经常被叫混。这套循环冗余校验码计算源代码合集把 CRC-8 到 CRC-32 的常见变种收敛到一套参数模型下配了位运算、256 项查表、半字节查表三种实现以及一份拿来做交叉验证的参考实现。想把校验写进通信协议、固件升级包或者存储块的开发者照着往下读就能跑通。2. 源码包结构一套代码怎么同时装下十几种 CRC 变种2.1 目录划分接口、实现、参数表三层拆开这类 CRC 合集最容易写崩的地方是每来一个新变种就复制一份 c 文件改两个常量改到第十份的时候没人记得哪份对应哪个设备。合格的组织方式是按职责切三层头文件只放接口和参数结构体实现层放算法本身参数层用一张常量表描述每个变种。常见做法是这样落地crc-collection/ ├── include/ │ └── crc.h # 统一接口 参数模型结构体 ├── src/ │ ├── crc_bitwise.c # 逐位实现任意宽度都能跑 │ ├── crc_table256.c # 256 项查表速度优先 │ ├── crc_nibble.c # 16 项半字节查表省 RAM │ └── crc_params.c # 常见变种的参数常量 ├── tools/ │ └── gen_table.py # 生成查找表并输出 C 数组 ├── tests/ │ └── test_vectors.c # 标准测试向量回归 └── Makefile这一层拆开之后新增一个变种就只是往参数表里加一行算法文件一行都不用动。工具目录下那个生成脚本也别忽略查表法的多项式常量是反的、索引是右移还是左移靠人脑记迟早出错让脚本生成数组、粘贴进源码比手抄可靠得多。文件职责改动频率crc.h对外接口、参数结构体低crc_bitwise.c通用逐位算法对照标准用极低crc_table256.c量产用查表实现低crc_nibble.c小 RAM 场景的半字节实现低crc_params.c变种参数常量高新设备就加一行2.2 统一参数模型一个结构体装下 width、poly、init、refin、refout、xoroutCRC 之所以让人头大是因为同一个名字下面藏了六个自由参数。业界通用的描述方式来自那份被反复引用的 CRC 参数目录六个字段分别是宽度 width、生成多项式 poly省略最高位那个 1、寄存器初值 init、输入是否反射 refin、输出是否反射 refout、结果异或值 xorout。把这六个字段固化进结构体所有变种就变成同一套代码的不同实例/* crc.h参数模型六个字段决定一个 CRC 变种 */ typedef struct { uint8_t width; /* 校验宽度8 / 16 / 32决定掩码和移位量 */ uint32_t poly; /* 生成多项式省略最高位的 1如 0x1021 表示 x^16x^12x^51 */ uint32_t init; /* 寄存器初值Modbus 是 0xFFFFXMODEM 是 0x0000 */ uint8_t refin; /* 输入反射每个字节是否低位先入 */ uint8_t refout; /* 输出反射最终结果是否整体翻转 */ uint32_t xorout; /* 结果异或值CRC-32 靠它做全 1 翻转 */ } crc_params_t; /* 统一入口参数 数据 长度返回宽位掩码后的结果 */ uint32_t crc_compute(const crc_params_t *p, const uint8_t *data, size_t len);有了这个结构体参数表就变得很短看起来一目了然/* crc_params.c常用变种新增设备照着格式加一行 */ const crc_params_t CRC16_MODBUS {16, 0x8005, 0xFFFF, 1, 1, 0x0000}; const crc_params_t CRC16_CCITT_FALSE {16, 0x1021, 0xFFFF, 0, 0, 0x0000}; const crc_params_t CRC16_XMODEM {16, 0x1021, 0x0000, 0, 0, 0x0000}; const crc_params_t CRC32_ISO_HDLC {32, 0x04C11DB7, 0xFFFFFFFF, 1, 1, 0xFFFFFFFF};参数说明上要留神两个点。poly 是省略最高位的写法CRC-16 的多项式 x^16x^12x^51 记作 0x1021别写成 0x11021宽度 16 存不下。refin 和 refout 在绝大多数变种里取值相同但标准里允许不同通用实现必须分开处理不能图省事合成一个 flag。这两条如果写反表现出来的现象就是“算法看着没错结果就是差一位”。2.3 三种实现路径的取舍位运算、256 项查表、半字节查表同一套参数算法可以有三份实现各自对应不同场景。逐位实现最慢但最通用它不依赖查找表宽度从 8 到 32 都能算适合当“标准答案”用来对照256 项查表最快一个字节一步代价是 1KB 的表空间宽度 32 时半字节查表是折中一次处理 4 位表只有 16 项64 字节 RAM 就能放下速度大概是逐位的三倍多。选择上有个朴素的经验上位机、服务器侧、文件校验这些场景内存不是问题直接上 256 项表MCU 上的通信协议尤其 RAM 只有几 KB 的小片子用半字节表如果只是启动时校验一次固件头数据量不到几十字节逐位实现反而最省事连表初始化都省了。提示三种实现必须共用同一个参数结构和同一组测试向量否则改完查表版发现和逐位版对不上排查成本会翻倍。3. 核心算法拆解从 8 次移位到一张表3.1 位运算版一个字节到底被移了几次逐位实现的逻辑其实很短短到很多人第一次看会怀疑它是不是漏了什么。核心就三步把当前字节异或进寄存器高位、循环 8 次判断最高位决定是否异或多项式、最后按 refout 和 xorout 做收尾。/* crc_bitwise.c通用逐位实现所有变种共用 */ static uint32_t reflect_bits(uint32_t v, uint8_t bits) { uint32_t r 0; for (uint8_t i 0; i bits; i) { r (r 1) | (v 1u); /* 从最低位往最高位搬 */ v 1; } return r; } uint32_t crc_bitwise(const crc_params_t *p, const uint8_t *data, size_t len) { const uint32_t mask (p-width 32) ? 0xFFFFFFFFUL : ((1UL p-width) - 1); const uint32_t topbit 1UL (p-width - 1); uint32_t crc p-init mask; for (size_t i 0; i len; i) { uint8_t byte data[i]; if (p-refin) byte (uint8_t)reflect_bits(byte, 8); /* 输入反射 */ crc ^ (uint32_t)byte (p-width - 8); /* 对齐到高位 */ for (int b 0; b 8; b) { if (crc topbit) crc (crc 1) ^ p-poly; else crc (crc 1); crc mask; /* 每次移位后截断 */ } } if (p-refout) crc reflect_bits(crc, p-width); /* 输出反射 */ return (crc ^ p-xorout) mask; }逐行说明一下。mask用来把寄存器限制在声明宽度内宽度 16 时是 0xFFFF少了这一步左移会把高位残留进来结果必然错。topbit是被移出去的那一位它决定这一轮要不要异或多项式这是 CRC 多项式除法的本质。byte (width - 8)把输入字节对齐到寄存器最高字节宽度 8 时移位量是 0也成立。注意这个写法隐含 width ≥ 8 的前提CRC-5、CRC-7 这类窄宽度变种得单独处理对齐别直接套。反射的处理顺序也值得说一句。refin 是对每个输入字节单独翻转refout 是对最终结果整体翻转两者位置不能互换。有人图省事在返回前统一反射一次对 refin refout 的变种结果恰好一样但遇到两者不同的变种就翻车——这类 bug 往往等到换设备才暴露。3.2 256 项查表表里的每个数字是怎么来的查表法不是什么新算法它只是把“一个字节异或进寄存器再移 8 次”这个过程提前算好存成 256 个数。查表的时候一个字节一步移位 8 次的工作交给了表项。非反射版的表生成逻辑是这样的/* gen_table.c 的核心为指定多项式和宽度生成 256 项表 */ uint32_t table[256]; void build_table(uint32_t poly, uint8_t width) { uint32_t mask (width 32) ? 0xFFFFFFFFUL : ((1UL width) - 1); uint32_t topbit 1UL (width - 1); for (uint32_t i 0; i 256; i) { uint32_t crc (i (width - 8)) mask; /* 把索引放到高位 */ for (int b 0; b 8; b) { crc (crc topbit) ? ((crc 1) ^ poly) : (crc 1); crc mask; } table[i] crc; /* 存的是处理完这一字节后的贡献值 */ } } /* 查表计算一个字节一步 */ uint32_t crc_table(const crc_params_t *p, const uint8_t *data, size_t len) { uint32_t crc p-init mask_of(p-width); for (size_t i 0; i len; i) { uint8_t idx (uint8_t)((crc (p-width - 8)) ^ data[i]); crc ((crc 8) mask_of(p-width)) ^ table[idx]; } return (crc ^ p-xorout) mask_of(p-width); }索引的算法是(crc (width - 8)) ^ byte取的是寄存器最高字节和输入字节异或物理意义就是“当前余数的高 8 位参与下一轮除法”。移位量width - 8必须跟着宽度走16 位是右移 832 位是右移 24写死成 8 是新手最容易犯的错。反射版变种CRC-32、CRC-16/Modbus 这些的表不太一样表项生成时用的是反射后的多项式索引和移位方向也整个镜像过来/* 反射表CRC-32 用的多项式是 0x04C11DB7 的位反转 0xEDB88320 */ void build_table_reflected(uint32_t poly_reflected) { for (uint32_t i 0; i 256; i) { uint32_t crc i; for (int b 0; b 8; b) crc (crc 1u) ? ((crc 1) ^ poly_reflected) : (crc 1); table_ref[i] crc; } } /* 反射版查表右移 8 位索引取低字节 */ uint32_t crc32_table_reflected(const uint8_t *data, size_t len) { uint32_t crc 0xFFFFFFFFu; /* init */ for (size_t i 0; i len; i) crc (crc 8) ^ table_ref[(crc ^ data[i]) 0xFF]; return crc ^ 0xFFFFFFFFu; /* xorout */ }把这两份表放在一起看能明白一个关键点反射版不是“算完再翻转”而是从多项式到移位方向全部镜像。所以拿0x04C11DB7去生成反射表结果一定错必须换成位反转后的0xEDB88320。这个坑我见过太多次表现是 CRC-32 与标准库结果完全对不上但 CRC-16 又正常因为 16 位那份表是别人给现成的。3.3 半字节查表与参考实现小 RAM 场景和交叉验证半字节表的存在意义只有一个省空间。16 项表在 32 位宽度下占 64 字节比 256 项表小 16 倍对 RAM 紧张的 MCU 很友好代价是速度降到查表法的四成左右。写法上是把一次 8 位移位拆成两次 4 位/* crc_nibble.c16 项表一次处理半个字节 */ uint32_t crc_nibble(const crc_params_t *p, const uint8_t *data, size_t len) { uint32_t crc p-init; uint32_t mask (p-width 32) ? 0xFFFFFFFFUL : ((1UL p-width) - 1); for (size_t i 0; i len; i) { crc ^ (uint32_t)data[i] (p-width - 8); /* 字节先进高位 */ for (int n 0; n 2; n) { /* 两轮每轮 4 位 */ uint8_t idx (uint8_t)((crc (p-width - 4)) 0x0F); crc ((crc 4) mask) ^ nibble_table[idx]; } } return (crc ^ p-xorout) mask; }索引取的是寄存器高 4 位(width - 4)这个移位量同样跟宽度走。每轮先左移 4 再异或表项顺序不能反过来否则高位会被表项污染。再说参考实现。这类合集里一般会附一份 Python 版本功能上没什么性能追求唯一的作用是对拍同一组参数、同一段数据C 和 Python 两边结果必须一致不一致就说明有一边写错了。我常用的是这种纯参数化写法# crc_ref.py参数化参考实现用来和 C 版对拍 def reflect(value, bits): result 0 for _ in range(bits): result (result 1) | (value 1) value 1 return result def crc_generic(data, width, poly, init, refin, refout, xorout): mask (1 width) - 1 topbit 1 (width - 1) crc init mask for byte in data: if refin: byte reflect(byte, 8) crc ^ byte (width - 8) for _ in range(8): crc ((crc 1) ^ poly) mask if (crc topbit) else (crc 1) mask if refout: crc reflect(crc, width) return (crc ^ xorout) mask if __name__ __main__: print(hex(crc_generic(b123456789, 16, 0x8005, 0xFFFF, True, True, 0x0000))) # 0x4b37 print(hex(crc_generic(b123456789, 32, 0x04C11DB7, 0xFFFFFFFF, True, True, 0xFFFFFFFF))) # 0xcbf43926这段代码的价值不在跑得快而在“没有任何优化痕迹每一步都能对着标准文档核”。当 C 版查表实现和硬件 CRC 外设结果不一致时先跑这段 Python就能立刻判断是算法写错还是外设配置错。这个习惯救过我两次一次是外设配错了反射位一次是数据长度传成了字节数的一半。4. 把代码跑起来编译、测试向量与参数配置4.1 编译与最小可运行示例这套代码没有第三方依赖标准 C99 加一个 Makefile 就能编。上手第一步别急着接协议先编译跑通一个最小例子确认工具链和头文件路径没问题# 编译静态库和测试程序 make clean make # 或者手动编一个最小例子 gcc -stdc99 -Iinclude -o demo demo.c src/crc_bitwise.c src/crc_table256.c src/crc_params.c ./demodemo.c里不用写复杂逻辑算一段固定字符串、打印十六进制结果就够了#include stdio.h #include crc.h extern const crc_params_t CRC16_MODBUS; extern const crc_params_t CRC32_ISO_HDLC; int main(void) { const uint8_t msg[] 123456789; /* 9 字节标准测试串 */ uint32_t a crc_compute(CRC16_MODBUS, msg, 9); uint32_t b crc_compute(CRC32_ISO_HDLC, msg, 9); printf(CRC-16/Modbus 0x%04X (expect 0x4B37)\n, a); printf(CRC-32 0x%08X (expect 0xCBF43926)\n, b); return 0; }参数说明一下。crc_compute的第一个参数是参数结构体指针直接传常量地址不用拷第二个是数据首地址uint8_t*保证不会因为char的符号性出岔子第三个是字节数不是元素个数sizeof(msg)/sizeof(msg[0])这种写法没错但传字符串字面量时别把结尾的\0算进去。打印格式%04X和%08X用来对齐宽度调试时比看十进制直观得多。编译通过、输出和注释里的期望值对上这一步才算完。对不上就别往下走了先把参数表那一行核一遍——十有八九是 poly 或 init 抄错。4.2 标准测试向量一组 “123456789” 打天下CRC 圈子有个约定俗成的测试串就是 ASCII 的123456789九个字节每个变种都有一个官方 check 值。把这张表存进测试程序每次改动代码跑一遍比什么都靠谱变种polyinitrefinrefoutxoroutcheckCRC-80x070x00否否0x000xF4CRC-8/MAXIM0x310x00是是0x000xA1CRC-16/ARC0x80050x0000是是0x00000xBB3DCRC-16/Modbus0x80050xFFFF是是0x00000x4B37CRC-16/CCITT-FALSE0x10210xFFFF否否0x00000x29B1CRC-16/XMODEM0x10210x0000否否0x00000x31C3CRC-320x04C11DB70xFFFFFFFF是是0xFFFFFFFF0xCBF43926CRC-32C0x1EDC6F410xFFFFFFFF是是0xFFFFFFFF0xE3069283写回归测试的时候把这张表做成结构体数组循环跑三种实现断言三者结果都等于 check。数组里每加一个变种三种实现就自动被测一遍这比写一堆 if 判断省事得多typedef struct { const char *name; const crc_params_t *params; uint32_t check; /* 123456789 的期望值 */ } vector_t; static const vector_t vectors[] { {CRC-16/Modbus, CRC16_MODBUS, 0x4B37}, {CRC-16/CCITT-FALSE, CRC16_CCITT_FALSE, 0x29B1}, {CRC-32, CRC32_ISO_HDLC, 0xCBF43926}, }; for (size_t i 0; i sizeof(vectors)/sizeof(vectors[0]); i) { uint32_t r crc_compute(vectors[i].params, (const uint8_t *)123456789, 9); assert(r vectors[i].check); /* 三种实现各断言一次 */ }提示CRC-32C 的多项式是 0x1EDC6F41和 CRC-32 的 0x04C11DB7 完全不同check 值也不一样。iSCSI、ext4 这些场景用的是 C 版别混用。4.3 新增一个变种改参数还是加代码对接新设备时面对一份只写了“CRC16 校验”的协议文档第一步是从文档里把六个参数抠出来抠不出来的话就试。试的办法很土但有效拿设备返回的一段已知报文用测试向量表里所有 16 位变种跑一遍哪个对得上就用哪个。如果设备用的是表里没有的变种两种处理方式。参数能凑出来的直接往crc_params.c加一行常量算法层一行不动宽度不是 8 的整数倍比如某些 CAN 协议用 CRC-15才需要动位运算实现里的对齐逻辑。我一般会先加常量跑标准测试串看能不能对上某个已知 check 值对不上再判断是不是宽度问题避免一上来就改算法。配置层面还有个小细节如果项目里只用一个变种完全可以把参数结构体优化成宏省掉一次间接寻址。但别一开始就这么干等确认变种固定了再优化否则换设备时又要改回来。5. 踩坑与排查反射、掩码、字节序最容易翻车5.1 移植阶段四个高频翻车点现象一CRC-32 的结果和 zlib 里的 crc32 永远差一个固定值。原因通常是反射处理只做了一半——查表实现用了反射多项式 0xEDB88320但收尾时的 xorout 忘了加或者加了两次。解决方式是把最终返回值固定写成crc ^ 0xFFFFFFFF且只写一处中间过程不碰它再用标准测试串对一遍确认得到 0xCBF43926。现象二16 位变种在 MCU 上结果正确换到上位机就错。原因多半是类型问题。C 里int是 32 位左移 16 位以上不会溢出但有人为了省空间把寄存器声明成uint16_t移位后高位直接丢掉和不截断的算法就对不上了。解决方式是寄存器统一用uint32_t宽度靠 mask 控制别用窄类型硬扛。现象三查表版对半字节版偶发错。原因通常是索引没掩码。(crc (width - 4)) 0x0F里的 0x0F少写一次索引就可能超过 15读到表外面去了结果时对时错全看内存里放的是什么。解决办法是把表长定义成宏NIBBLE_TABLE_SIZE 16索引取完再做一次范围断言调试期把问题暴露出来。现象四Modbus 从机回帧校验总是不过抓包看 CRC 值是对的。这是字节序问题。Modbus 的 CRC 是低字节先发而很多人按习惯先发高字节值本身没错顺序反了。解决方式是在组帧函数里显式拆成两个字节buf[n] crc 0xFF; buf[n1] (crc 8) 0xFF;不要图省事直接 memcpy 一个 16 位变量过去那样结果依赖 CPU 大小端换平台就变。5.2 上线前自检增量校验和一次性校验必须对齐协议栈里经常需要边收边算也就是把数据分几段喂给 CRC最后出一个总结果。这种增量接口最容易出的问题是 init 和 xorout 被重复执行每段都初始化一次或者每段结束都异或一次 xorout分段算出来的值和一次性算出来的就对不上。正确的做法是把状态和收尾拆开/* 增量接口update 只推进状态final 才做反射和异或 */ typedef struct { const crc_params_t *p; uint32_t state; } crc_ctx_t; void crc_init(crc_ctx_t *ctx, const crc_params_t *p) { ctx-p p; ctx-state p-init; /* 初值只在这里赋一次 */ } void crc_update(crc_ctx_t *ctx, const uint8_t *data, size_t len) { /* 内部只推进 state不碰 xorout也不做 refout */ } uint32_t crc_final(crc_ctx_t *ctx) { uint32_t r ctx-state; if (ctx-p-refout) r reflect_bits(r, ctx-p-width); return (r ^ ctx-p-xorout) mask_of(ctx-p-width); /* 收尾只做一次 */ }自检方法很直接把同一段 100 字节数据分别按“一次性算”和“拆成 1990 三段算”跑一遍结果必须相同。这个用例我建议直接写进单元测试因为增量接口的 bug 往往在长时间跑数据时才暴露等到上线再查就贵了。6. 二次开发把 CRC 模块接到协议栈里的三个改造点第一处改造是帧格式。CRC 单独算没有意义它总是挂在一个帧结构上。我一般会把组帧和校验封成一对函数帧头、长度、载荷、CRC 四段拼装接收侧先按长度字段截取再只对“参与校验的那部分”算 CRC。这里有个容易忽略的点帧头要不要参与校验不同协议规定不一样参数化成一个 offset 和 length比写死灵活。/* 组帧载荷在前CRC 追加在后低字节先发Modbus 风格 */ size_t frame_pack(uint8_t *out, const uint8_t *payload, size_t len, const crc_params_t *p) { out[0] 0xAA; /* 帧头是否参与校验看协议 */ out[1] (uint8_t)len; memcpy(out[2], payload, len); uint32_t crc crc_compute(p, out, len 2); /* 帧头长度载荷一起算 */ out[2 len] (uint8_t)(crc 0xFF); out[2 len 1] (uint8_t)((crc 8) 0xFF); return len 4; }第二处是硬件 CRC 外设的交叉验证。很多 MCU 自带 CRC 单元速度快但配置项少反射和初值经常不支持自由设置。我的做法是保留软件实现作为基准上电自检时用同一段数据分别跑硬件和软件结果不一致就直接标记硬件不可用、回退到软件路径。这段自检代码不到二十行但能挡住“换了个型号外设行为不同”这类问题。第三处是测试闭环。参数表、标准向量、三种实现这三样凑齐之后二次开发的安全性就有底了。我的习惯是每接一个新协议先把参数写进表跑一遍123456789的标准 check再拿一段真实报文验证两步都过才接进主流程。从那以后我每次改 CRC 相关代码不管改动多小都强制走一遍“测试向量 分段一致性”这两道检查——血泪经验换来的一条规矩省下的调试时间远超写测试花的那点功夫。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑