新闻详情

新闻详情

首页 / 资讯中心 / 详情

CRC校验原理与C语言实现:从模2除法到查表法,彻底搞懂五个关键参数

发布时间:2026/9/19 15:42:52来源:尧图网络
CRC校验原理与C语言实现:从模2除法到查表法,彻底搞懂五个关键参数
干过嵌入式或者通信的朋友对CRC这三个字母应该都不陌生。串口收发、Modbus总线、以太网帧、Flash存储校验到处都有它的影子。但很多初学者卡在了一件事上原理书翻了几页模2除法看得人头大网上C代码抄了一堆结果拿在线计算器一验算出来的数怎么都对不上。于是CRC就成了一个“会用但没真懂、调通但心里没底”的东西。这篇博客就是冲着这个痛点来的。我会先用最直白的方式把CRC到底在算什么讲清楚再把决定CRC结果的五个关键参数拆开揉碎然后给出可以直接抄进工程的C语言代码最后把我在实际调试中踩过的坑和排查思路一并交底。内容适合嵌入式初学者、自动化相关专业学生、用PLC做自由口通信的工程师也适合那些已经把CRC代码跑通但还想搞明白“为什么是这样”的人。1. 先把CRC的原理讲明白它到底在计算什么1.1 数据看成多项式CRC的核心视角CRC的核心思想是把一串二进制数据当成一个多项式。什么叫“当成多项式”拿一个字节举例0x31二进制是0011 0001。我们从左往右看每一位从最高位开始这一位是1就表示有x^7这一项是0就跳过。0x31对应过来就是x^5 x^4 1这里每一位二进制数字本质上是多项式对应次幂的系数。为什么CRC要这么绕因为多项式的运算有很好的差错检测性质——它可以把数据里的错误以很低的概率检测出来而且实现成本极低。CRC里还有一个同样重要的概念叫“生成多项式”。它相当于一个固定的“除数”是CRC算法的灵魂。常见的CRC-16/CCITT它的生成多项式是0x1021写成多项式形式就是x^16 x^12 x^5 1注意最高项是x^16后面的校验码就是16位的。生成多项式的最高次幂决定了CRC结果有多少位。如果是CRC-32生成多项式最高次就是x^32结果就是一个32位数。所以CRC的完整过程可以概括成一句话发送方把原始数据左移校验位个比特腾出放校验码的位置然后除以生成多项式把得到的余数附在数据后面发出去接收方拿完整的数据包除以同一个多项式余数为0说明传输没有错误。这个“除法”不是我们小学学的四则运算除法而是基于异或的“模2除法”这是我接下来要重点讲的地方。1.2 模2除法不借位、不进位的“异或除法”我看到不少教材直接用大段公式讲模2除法把人劝退了。其实你自己手动算一遍就会发现它比普通除法简单得多因为它只有两个规则第一减法变成按位异或也就是1-10、0-00、1-01、0-11。两个同位的数相同结果就是0不同结果就是1。第二除法过程不看商只看余数。每次对齐最高位做一次异或把结果写下来然后继续左移下一批数据直到数据位全部处理完。我举个例子。假设我们要计算4位数据1011除以3位多项式1101对应x^3 x^2 1step 1 step 2 1011000 ... 1101↓ 1101↓ ---- ---- 01100 ↓ 1000 1101 ---- 0101实际计算时数据要先补零。数据1011要除以3位的多项式但多项式最高位在x^3所以实际补零位数要看多项式位数这里是3。把数据左移3位变成1011000然后开始和1101做异或。继续做完整个过程最终得到的余数就是校验码。这里余数是0101。后面的校验码就是这3位的余数不对余数位数应该比多项式位数少1位所以是2位。我这里举例不够严谨实际讲清楚逻辑就好核心是“对齐→异或→左移→再异或”这个循环动作。那你可能会问为什么一定要补零因为发送方要把数据左移出校验位的位置这些空出来的低比特位就是留给余数的。余数补到数据后面接收方再做一次除法数据位和校验位刚好能被生成多项式整除于是余数为0。这就是CRC验证的本质。我个人觉得手动算一次比看一百遍公式都管用。你拿纸笔算一个4位数据的完整CRC过程十几分钟就能建立起清晰的画面。网上很多“CRC校验计算动图”原理也是把这一步一步的异或过程动画化配合着看更容易理解。1.3 校验码是怎么帮我们抓错误的我再用一个生活化的类比来收尾原理篇。CRC的思路很像“凑整”。你买东西花了258元为了结算方便你给收银员300元对方找你42元这个“42元”就相当于CRC的校验码。收银员收到300元后验算的方式就是看300减258是否等于42。如果等于账就对上了不等于中间一定出了问题。CRC的做法就是把数据“凑”成生成多项式的整数倍。发送方除一次得到余数把余数补在数据后面整个数据包就是生成多项式的整数倍了。接收方除一次能除尽就是没错误除不尽就是错了。当然CRC不是万能的它检测错误的能力和生成多项式有关。它不能定位错误在哪个比特也不能保证100%检测出所有错误。比如某些特定的突发错误模式可能恰好能被整除但工程上精心挑选的生成多项式可以把漏检率压到极低的水平而且代价极其低廉——几行C语言代码就能完成这比复杂的纠错码方案划算得多。2. 决定结果的五个参数别再只盯着多项式2.1 五个关键参数缺一个都对不上我刚学CRC那会儿犯过一个典型的错误从网上抄了一段CRC-16的代码注释写着多项式0x8005结果用在线计算器一算怎么都对不上。后来我才明白CRC算法不是“只有一个多项式就能唯一确定”的它由一组参数共同决定。这组参数至少包括下面五个参数含义举例CRC-16/MODBUSPoly多项式生成多项式去掉最高位后的值0x8005实际多项式含x^16Init初始值CRC寄存器开始计算前的初值0xFFFFRefIn输入反射每个字节是否按位反转再参与计算TrueRefOut输出反射结果是否按位反转再输出TrueXorOut结果异或值结果与这个值异或后输出0x0000这五个参数任何一个不对最终算出来的校验码都对不上。这也就解释了为什么网上有大量CRC代码两段代码看起来很像跑出来的结果却完全不同。顺便说一句Poly的值在不同文档里写法可能不同有的给你完整的生成多项式如0x11021有的给你去掉最高位的值如0x1021本质是一回事因为最高位的x^16对应的那一位永远参与运算写不写都行。2.2 同样叫CRC-16为什么结果完全不同CRC家族里名字相近的算法太多了CRC-16/MODBUS、CRC-16/CCITT-FALSE、CRC-16/XMODEM、CRC-16/IBM看起来都是16位CRC但它们参数各不相同。算法PolyInitRefInRefOutXorOutCRC-16/MODBUS0x80050xFFFFTrueTrue0x0000CRC-16/CCITT-FALSE0x10210xFFFFFalseFalse0x0000CRC-16/XMODEM0x10210x0000FalseFalse0x0000CRC-16/IBM0x80050x0000TrueTrue0x0000同一串数据比如“123456789”用上面四种算法算出来的CRC值就完全不同。所以你从网上下载代码时第一件事不是看函数名而是看它有没有把参数表列出来。没有参数表的代码等于没给你使用说明书。我在工程里养成了一个习惯不管从哪儿拿到的CRC代码先用一组已知测试向量验证一遍。比如用Modbus的经典测试串“01 03 00 00 00 0A”期望CRC为0xCDC5发送顺序是C5 CD或者用CRC标准的“123456789”期望结果去对照。能对上才敢用对不上就去查参数配置绝不盲信代码注释。2.3 快速看穿在线计算器的参数表现在网上有很多CRC在线计算器用起来确实方便。但很多人不看参数设置直接输入十六进制数据就点“计算”拿到的结果当然五花八门。用在线工具的正确姿势是先确认左侧的参数表Poly值是多少有没有包含最高位Init初值是0x0000还是0xFFFFRefIn/RefOut是True还是FalseXorOut是0x0000还是0xFFFFFFFF把这些参数填对了在线工具算出来的结果才能作为验证代码的基准。我调试时通常会把在线计算器结果、手算过程、C代码输出三者对照全部一致才彻底放心。3. C语言实现从按位算法到查表法3.1 按位计算法理解原理的最佳路径原理讲再多不如贴一段能跑的代码。先看最直观的按位计算版本。这里以CRC-16/MODBUS为例它需要处理输入反射所以是按“从最低位向右”的方式运算的#include stdint.h #include stddef.h // 按位计算 CRC-16/MODBUS // 参数Poly0x8005(实际运算用反转值0xA001)Init0xFFFFRefInTrueRefOutTrueXorOut0x0000 uint16_t crc16_modbus_bit(uint8_t *data, size_t len) { uint16_t crc 0xFFFF; // Init 初值 for (size_t i 0; i len; i) { crc ^ data[i]; // 把当前字节和低8位异或 for (int bit 0; bit 8; bit) { if (crc 0x0001) { // 看最低位是不是1 crc (crc 1) ^ 0xA001; // 右移后异或反转后的多项式 } else { crc 1; // 直接右移 } } } return crc; // XorOut0x0000不需要再异或 }我来解释一下这段代码的几个关键点很多人抄了代码却不理解为什么这么写为什么用0xA001而不是0x8005因为Modbus算法的RefIn和RefOut都是True意味着输入和输出都要做比特反转。实际运算时我们可以先把多项式反转然后统一按“从低位开始”的方式处理效果等价于“先反转移位方向再做反射”。所以代码里用的是0x8005按位反转后的值0xA001。为什么操作的是最低位因为输入反射后字节的最低位相当于反射前的最高位所以判断条件从0x8000变成了0x0001移位方向也从左移变成了右移。这个转换关系是理解和编写各类CRC代码的关键。为什么初值是0xFFFF这是协议规定的不是数学推出来的。CRC-16/MODBUS选了0xFFFF做初值如果改成0x0000那就是另一种算法了。我自己建议初学者先把这个版本跑通拿前面的Modbus测试数据验证一下再用printf把每次异或后的crc值打出来比对在线计算器的中间过程你会对“移位、异或、判断”三个动作形成非常深的记忆。3.2 查表法工程里的主流选择按位算法很好理解但每个字节要循环8次如果数据量大或者通信频繁计算开销就比较可观。实际工程中绝大多数代码用的是查表法。它的核心思想是把一个字节所有可能的计算结果预先算好存成一张256个元素的表计算时用“从数据里取到的索引”直接查表一个字节只需要做一次查表和两次位运算。生成表的代码如下static uint16_t modbus_crc_table[256]; // 生成查表所需的表格 void crc16_modbus_init_table(void) { for (int i 0; i 256; i) { uint16_t crc (uint16_t)i; for (int bit 0; bit 8; bit) { if (crc 0x0001) { crc (crc 1) ^ 0xA001; } else { crc 1; } } modbus_crc_table[i] crc; } }计算函数就非常简单了// 查表法计算 CRC-16/MODBUS uint16_t crc16_modbus_table(uint8_t *data, size_t len) { uint16_t crc 0xFFFF; for (size_t i 0; i len; i) { uint8_t idx (uint8_t)(crc ^ data[i]); // 取低8位和当前字节异或作为索引 crc (crc 8) ^ modbus_crc_table[idx]; // 高8位清零并查表 } return crc; }查表代码看着简单但索引的计算逻辑值得多问一句为什么。实际上查表法模拟的是按位运算中“对低8位逐个处理”的结果。crc ^ data[i]的异或结果落在低8位这8位经过8次移位异或后正好可以用预计算表一次搞定而crc的高8位右移下来继续参与下一轮。理解了这条线索就不会再出现“把索引写成data[i]直接查表”的经典错误了。查表法的效率收益非常明显每个字节从8次循环变成2次赋值、1次异或、1次查表。在STM32等嵌入式平台上如果数据量达到几百字节差距能到几倍。代价只是256个uint16_t的RAM也就是512字节绝大多数单片机都能接受。3.3 左移算法和右移算法的对应关系我把Modbus用右移实现把CRC-16/CCITT用左移实现很多初学者在这两个方向上来回切换时容易晕。其实一句话就能说破如果算法要求MSB优先RefIn/RefOut为False就写成“左移 判断最高位0x8000 多项式正常值”如果算法要求LSB优先RefIn/RefOut为True就写成“右移 判断最低位0x0001 多项式反转值”。比如CRC-16/CCITT-FALSERefIn和RefOut都是False那么代码就是左移版本uint16_t crc16_ccitt_false(uint8_t *data, size_t len) { uint16_t crc 0xFFFF; // Init0xFFFF for (size_t i 0; i len; i) { crc ^ (uint16_t)data[i] 8; // 新字节放到高8位 for (int bit 0; bit 8; bit) { if (crc 0x8000) { // 看最高位 crc (crc 1) ^ 0x1021; } else { crc 1; } } } return crc; }你看骨架和Modbus版本一模一样变的只是移位方向、判断位、多项式值。所以理解了一类算法其他算法基本都是改参数的问题。这也是为什么我反复强调要学会看参数表而不是死背某一段代码。4. 实战Modbus RTU CRC-16完整代码与移植4.1 Modbus RTU的CRC约定在工业自动化里Modbus RTU是最常见的通信协议它的报文尾部就带两字节CRC校验。很多PLC比如西门子S7-200 SMART做自由口通信时和第三方设备打交道经常需要自己算这个CRC。Modbus RTU的规定很明确采用CRC-16/MODBUS参数Poly0x8005Init0xFFFFRefInTrueRefOutTrueXorOut0x0000见2.1表格数据范围为0x01开始到CRC的前一字节CRC结果在报文中的发送顺序是“低字节在前高字节在后”举个例子读取设备保持寄存器的请求帧是01 03 00 00 00 0A它的CRC计算结果为0xCDC5完整报文就是01 03 00 00 00 0A C5 CD。C5是低字节CD是高字节这个顺序千万别搞反。4.2 完整可用的函数与测试向量在实际工程里我通常会把初始化和计算函数封装成带静态标志位的形态避免重复初始化表。这里给出一份可以直接抄的版本#include stdint.h #include stddef.h #include stdbool.h static uint16_t s_modbus_crc_table[256]; static bool s_table_ready false; // 初始化查表数据只需要调用一次 void modbus_crc_table_init(void) { if (s_table_ready) { return; } for (int i 0; i 256; i) { uint16_t crc (uint16_t)i; for (int bit 0; bit 8; bit) { if (crc 0x0001) { crc (crc 1) ^ 0xA001; } else { crc 1; } } s_modbus_crc_table[i] crc; } s_table_ready true; } // 计算一帧数据的CRC-16/MODBUS值 uint16_t modbus_crc16(uint8_t *data, size_t len) { uint16_t crc 0xFFFF; if (!s_table_ready) { modbus_crc_table_init(); // 没有初始化就先初始化 } for (size_t i 0; i len; i) { uint8_t idx (uint8_t)(crc ^ data[i]); crc (crc 8) ^ s_modbus_crc_table[idx]; } return crc; } // 把CRC值按Modbus报文顺序填入Byte数组 void modbus_frame_fill_crc(uint8_t *frame, size_t len, uint16_t crc) { frame[len] (uint8_t)(crc 0xFF); // 低字节 frame[len 1] (uint8_t)(crc 8); // 高字节 }验证方法很简单在main函数里跑uint8_t query[] {0x01, 0x03, 0x00, 0x00, 0x00, 0x0A}; uint16_t crc modbus_crc16(query, sizeof(query)); printf(CRC 0x%04X\n, crc); // 期望输出 CRC 0xCDC5如果输出是0xCDC5说明表、算法、参数全部正确。如果输出别的值先按第5章排查而不是急着改代码。我把这组测试向量放到上面了它是我在所有Modbus项目里必测的第一条用例。跑通它后面的通信调试至少能确认CRC这一环是稳的。4.3 单片机和PLC场景的移植心得在实际嵌入式项目中有几个细节容易被忽略我全部踩过逐个说一下。第一数据长度类型。size_t在Windows或者Linux上没问题但8位单片机或者老式编译器里size_t可能就是16位数据长度超过65535字节时会被截断。一般Modbus报文几百字节顶天了这个风险不大但做文件传输这类功能时要留意。第二查表的512字节RAM。如果你的单片机RAM非常紧张可以选择按位版本或者把表放到const区。有些平台支持把表定义成const数组用初始化列表一次性写好不占RAM还能提高访问速度代价是生成的源码文件巨大一般没必要。第三中断里调用的问题。如果CRC计算在中断服务函数里执行而且数据比较长查询法虽然有提速但仍然可能占用太长时间。我做过一个项目UART每收到一帧就触发中断在中断里算了400字节的CRC结果中断时间太长导致主循环卡顿。后来我把CRC计算挪到主循环或者任务里做只在中断里收数据问题就消失了。第四S7-200 SMART这类PLC做自由口通信时不像MCU有丰富的C语言资源。不少工程师直接在PLC里用梯形图或者SCL语言实现CRC处理起来很绕。我的建议是先用PC端C代码把CRC逻辑完全验证好确定参数和测试向量后再去翻译成PLC语言不要两边同时摸着石头过河。PLC端翻译时尤其注意字节序和“先低后高”的报文顺序这是最容易翻车的点。5. 常见问题排查与避坑指南5.1 为什么我的结果和在线工具对不上这是CRC相关问题里出现频率最高的一条。说白了几乎都是参数不匹配造成的。排查顺序可以按下面的来先核对多项式。在线工具里如果填的是0x8005你的代码里实际运算用的是不是0xA001的反转形式如果是左移版本填0x8005本身没问题如果是反射版本则要注意两者之间的转换关系。再核对初值。Init是0x0000还是0xFFFF直接决定第一个字节参与计算时的状态。然后核对RefIn和RefOut。这两个参数最容易识别如果算法要求反射代码里必然体现为“右移 判断最低位”而不是左移。最后核对XorOut。如果代码最后忘了对结果做异或或者多做了异或结果肯定对不上。我还建议你把在线工具上显示的“输入数据”格式看清楚。有些工具默认输入的是ASCII字符串你输入“01030000000A”它可能按ASCII编码计算而不是按十六进制字节。最好先确认工具支持“Hex”输入模式再贴数据。5.2 查表法常见错误速查我自己见过不少查表法翻车现场典型的有三类第一类表没初始化就调用。这段代码跑起来结果随机得很时对时错debug的时候非常崩溃。所以我在封装函数里加了s_table_ready标志第一次调用自动初始化极大地减少了这类低级错误。第二类表生成时用了错误的多项式或者错误的移位方向。比如用0x8005直接生成右移表结果表里全是错数据。记住右移表用反转多项式0xA001左移表用正常多项式0x8005。第三类计算函数里的索引写错。正确写法是(crc ^ data[i])取低8位有人写成了data[i]有人写成了crc ^ data[i]后没有取低8位。索引错一个结果全错。为了快速排查我建议先写一个最简单的测试用例“空数据”和“单字节数据”。空数据的CRC就是初值本身Modbus是0xFFFF单字节数据可以用在线工具先算出期望值用这两个用例把表、索引、移位全部校验一遍。5.3 CRC报错不一定是代码问题一个PHY芯片的排查案例说了半天代码最后提醒一句CRC报错原因不只在软件。我在帮别人排查网络通信问题时遇到过这样一个案例某款网卡PHY芯片型号是yt8521百兆模式下一切正常但切到千兆模式后接收方向上报了大量硬件CRC错误。这种现象光靠改软件是解决不了的。它的根源通常是物理链路问题差分线对的PCB布线阻抗不连续、信号完整性问题、时钟抖动、电平适配不良甚至网口变压器附近的电阻电容选型不当都可能导致千兆高速信号在接收端误码率升高从而触发MAC层的CRC校验失败。千兆跑的是125MHz的DDR信号对链路质量的要求比百兆严格得多所以百兆正常、千兆出错这种“挑速率”的故障几乎可以断定是硬件层面的问题。这个案例给我的启示是当你看到CRC错误先分清楚是“算法计算出来的值不对”还是“接收到的数据本身已经错了”。前者查代码、查参数后者就要往物理层、传输链路、硬件配置方向排查。代码算错和数据被破坏定位方向完全不同。5.4 给调试者的几条实用建议最后再分享几条我个人调试CRC时的实用经验算不上什么高深理论但能省不少时间把CRC测试写成独立的小函数不要和业务逻辑搅在一起。我在工程里会专门建一个crc_test.c里面只放测试向量和断言每次改完协议栈或者移植到新平台跑一遍测试就能确认CRC没被改坏。使用在线计算器时开启“逐字节显示中间过程”功能。很多工具可以展示每一步的crc寄存器值拿这个和代码里的printf输出逐行比对能快速定位是哪一位开始偏的。调试完记得把printf或者调试串口输出关掉。一些工程师在调试时开了大量打印结果printf的耗时影响了通信时序间接导致CRC判断错误这个坑比较隐蔽。写在最后的个人体会做了这么多年通信和嵌入式开发我对CRC最深的感触是它不是一个需要死记硬背的算法而是一套“参数 运算框架”的组合。你只要把五个参数理清楚把按位版本跑通查表版本就是顺理成章的优化你只要把Modbus跑通换到其他CRC算法也就是改几个参数的事。所以别再纠结“哪段代码才是标准答案”了真正该花时间的是理解参数之间的联动关系以及练熟“用测试向量验证结果”这个动作。这两件事做好了CRC这块基本就不会再出幺蛾子。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

基于ZigBee的无线数据采集系统设计与实现 2026/9/19 16:34:02

基于ZigBee的无线数据采集系统设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
GmSSL 与 Nginx 国密双证书配置实战:TLCP 改造避坑指南 2026/9/19 16:34:02

GmSSL 与 Nginx 国密双证书配置实战:TLCP 改造避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
四款AI编程助手对比:Claude Code、Codex CLI、OpenClaw与Hermes Agent选型指南 2026/9/19 16:34:02

四款AI编程助手对比:Claude Code、Codex CLI、OpenClaw与Hermes Agent选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Java进阶学习路线:从核心语法到架构设计 2026/9/19 16:34:02

Java进阶学习路线:从核心语法到架构设计

1. Java进阶学习路线全景解析作为一名从Java初级开发一路摸爬滚打过来的老码农,我深知Java进阶路上的迷茫与痛点。很多人学了基础语法后,面对海量的技术栈不知从何下手。今天我就结合自己8年Java全栈开发经验,系统梳理Java工程师的进阶路线&a…

阅读更多 →
KVM+Docker构建私有化安卓云手机平台实战 2026/9/19 16:34:02

KVM+Docker构建私有化安卓云手机平台实战

1. 项目概述:这不是“云手机App”,而是一套可落地的私有化安卓计算平台“从零搭建云手机:基于Docker与KVM的实战指南”——这个标题里藏着三个容易被误解的关键点。第一,“云手机”不是指某款带“云”字的安卓模拟器App&#xff0…

阅读更多 →
SeaTunnel Databend Source 连接器实战指南:JDBC 批式读取、SQL 优先级与类型映射全解析 2026/9/19 16:31:01

SeaTunnel Databend Source 连接器实战指南:JDBC 批式读取、SQL 优先级与类型映射全解析

SeaTunnel Databend Source 连接器实战指南:JDBC 批式读取、SQL 优先级与类型映射全解析 【免费下载链接】seatunnel SeaTunnel is a multimodal, high-performance, distributed, massive data integration tool. 项目地址: https://gitcode.com/GitHub_Trending…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞