简介:这是一份基于MATLAB环境的CRC循环冗余校验仿真程序包,面向通信工程、数据存储及嵌入式方向的初学者与研发人员,用于解决数据传输完整性校验的原理理解与仿真实现问题。资源共包含4个M文件,涵盖主程序、编码函数、解码函数与完整校验流程,压缩包仅2KB,代码精简,便于逐行阅读与二次扩展。程序围绕生成多项式、预处理左移、模2除法及余数校验等核心步骤展开,并给出了从原始数据附加校验码、发送、接收端再次相除并判定余数是否为零的完整链路;同时支持人为翻转比特位模拟信道干扰,直观检验CRC的检错能力。除常规生成、添加与验证流程外,也可对照代码分析不同生成多项式对检错效果的影响,适合串联起《通信原理》《计算机网络》课程实验与毕业设计。已有940人学习下载,对于希望快速掌握CRC算法并完成MATLAB仿真的读者,是一份轻量而实用的参考资料。 做通信和嵌入式调试的朋友,应该都遇到过这个场景:串口收到一包数据,没人敢保证这包数据在传输过程中没被干扰过;固件升级文件拷贝到一半,也没人能肉眼看出文件是不是少了几字节。CRC(循环冗余校验)就是用来解决这类问题的经典方案,也是我这些年调试链路、设计通信协议时用得最多的校验手段之一。之前我整理过一个基于MATLAB的CRC仿真程序,把CRC-8、CRC-16、CRC-32的生成、拼接、校验、错误注入全都在一个脚本里跑通了。这篇文章就把这个仿真程序的设计思路、完整代码和踩过的坑拆开讲,适合做通信、嵌入式、上位机开发的工程师,也适合正在学数据链路层课程但被教材绕晕的在校生。
这套仿真最大的价值在于:先用MATLAB把算法和协议参数验证清楚,等逻辑完全没问题了,再往C语言、Verilog或者单片机上移植,可以省掉大量在硬件上反复烧录调试的时间。下面从原理开始,一步步带你搭一个能跑的CRC仿真环境。
1. 项目整体思路:为什么需要CRC,为什么用MATLAB
1.1 校验的本质:给数据加一道“冗余保险”
任何校验算法,本质上干的都是同一件事:发送端在有效数据后面附加一段冗余信息,这段冗余信息由原始数据通过某种规则计算得出;接收端拿到完整数据后,用同样的规则重新计算,再把计算结果和收到的冗余信息对比,一致就认为数据没问题,不一致就说明传输过程中发生了错误。
这就像寄快递时,发件人会在包裹上贴一张写了重量的标签。收件人收到快递后,如果把包裹放到秤上一称,重量和标签对不上,就知道包裹要么在半路被调包了,要么内部东西坏了。不过“重量校验”太粗糙了,数据包里哪怕只翻转了一个比特,如果刚好有另一个比特也跟着翻转,总量可能不变,这就会出现漏检。CRC比这种简单校验强的地方在于,它不是简单加和,而是把数据当成一个大的二进制数,通过多项式除法把错误特征“摊开”到整个数据长度上,单比特翻转、双比特翻转这类常见错误基本都能抓住。
1.2 CRC的数学原理:二进制多项式除法
CRC的全称是Cyclic Redundancy Check,核心思想听起来有点吓人,其实拆开了很简单。把待发送的数据看成一个二进制多项式,比如字节0x0B(二进制00001011)可以对应成多项式“x^3 + x + 1”。发送端把这个数据多项式左移r位(r是校验位宽度),然后除以一个发送端和接收端约定好的“生成多项式”,取余数,这个余数就是CRC校验码。
这里的除法不是普通十进制除法,而是模2除法,说人话就是按位异或。整个过程只有0和1的异或运算,没有进位也没有借位。因为运算规则简单,硬件上只需要移位寄存器和异或门就能实现,这也是CRC在串口通信、网卡、存储设备里被用了这么多年依然不过时的原因。
生成多项式的选择很有讲究,常见的CRC-8、CRC-16、CRC-32都有标准定义。比如CRC-16常用的多项式0x8005,写成二进制就是“1000 0000 0000 0101”,代表“x^16 + x^15 + x^2 + 1”。选用标准多项式的好处是:检错能力经过充分验证,而且不同设备之间能互相兼容。我自己见过不少人在实际项目里自己“发明”多项式,结果检错效果一塌糊涂,这个问题后面专门说。
1.3 为什么选MATLAB做仿真而不是直接上单片机
做CRC校验,最后肯定要落到C语言或者硬件电路里,但在起步阶段,用MATLAB做仿真有三个明显优势。
第一个优势是调试直观。MATLAB里数组、矩阵、位操作都是原生支持,变量值随时可以在工作区点开看,还能用disp直接打印每步的中间结果。相比之下,C语言里面做位操作,得时刻小心数据类型溢出、无符号整型的移位方向,调试起来心智负担重很多。
第二个优势是测试方便。MATLAB写脚本不需要交叉编译,不需要下载到板子,改一行代码立刻能看到结果。做错误注入实验时,只要用bitxor把数据里的某几个比特翻转一下,就能快速验证CRC的检错能力,这在硬件环境里做要麻烦得多。
第三个优势是知识迁移容易。把MATLAB里的算法逻辑理清楚之后,移植到C语言就是机械翻译,连变量名都能一一对应。我后面给出的一段查表法代码,就几乎可以直接照搬到嵌入式平台上。
2. 仿真程序的功能设计与模块拆分
2.1 程序整体要解决什么问题
这套仿真程序一开始定下的目标很明确:输入任意字节数组,能输出对应的CRC校验码;接收端能通过重新计算校验码来確認数据是否被篡改;同时支持人为翻转数据比特,用来演示CRC的检错过程。
围绕这个目标,我把程序拆成了四个模块:
| 模块 | 功能说明 | 对应函数 |
|---|---|---|
| 参数配置模块 | 设定生成多项式、初始值、结果异或值、是否反转 | 脚本头部参数区 |
| CRC计算模块 | 实现逐位法和查表法两种算法 | crc16_calc、crc_table函数 |
| 收发链路模块 | 模拟发送端附加校验码、接收端重新校验 | 主脚本流程 |
| 错误注入模块 | 翻转单个或多个比特,测试检错效果 | 主脚本中的位操作 |
这种拆法的好处是:CRC计算函数是纯算法,不依赖任何外部环境,拿出去就能复用到其他项目;收发链路和错误注入只是用来验证算法正确性的驱动代码。模块之间通过函数接口隔离,不会因为改了一个地方的参数导致别的模块跟着出问题。
2.2 为什么用函数封装而不是一个脚本从头写到尾
很多初学者喜欢把所有代码堆在一个脚本里,这样跑起来确实快,但一旦程序复杂起来就麻烦了。我这次特意把CRC计算封装成独立函数,因为在实际工程里,CRC计算往往要嵌入到不同的业务流程里,比如上位机发送文件、下位机解析报文、存储系统校验数据块,每个场景调用的方式都不一样。把算法封装成函数,参数传进去、结果返回来,完全不用关心内部实现细节。
另一个考虑是代码复用。逐位法和查表法虽然算法思路不同,但函数签名可以保持一致,这样主脚本里只要换一行调用,就能对比两种算法的结果是否一致。函数封装好了,后面想再加CRC-32或者CRC-8,只需要复制函数改几个参数即可。
3. 核心代码实现与逐段讲解
3.1 先写一个最直观的逐位法(非反射模型)
逐位法是最接近CRC原理的写法:每处理一个字节,就把这个字节左移8位,放到16位CRC寄存器的低8位上,然后逐位判断寄存器最高位是否为1,如果是,就左移一位后异或生成多项式,否则只左移一位。
这里用MATLAB实现,有一个地方特别提醒:MATLAB的位操作函数(bitxor、bitshift、bitand)默认要求输入是整数类型。如果你直接往函数里塞一个double类型,运算结果很可能不是你想要的。所以进入函数后,第一步就是把数据强制转成uint16类型。
function crc = crc16_calc(data, poly, init) % 非反射模型,MSB-first % data: 待计算CRC的字节数组,uint16或double均可 % poly: 生成多项式,例如0x8005 % init: CRC寄存器初始值 crc = uint16(init); data = uint16(data(:)); poly = uint16(poly); for i = 1:length(data) crc = bitxor(crc, bitshift(data(i), 8)); for bit = 1:8 if crc >= 32768 % 判断最高位是否为1 crc = bitxor(bitshift(crc, 1), poly); else crc = bitshift(crc, 1); end end end end代码里判断最高位用crc >= 32768而不是bitand(crc, 0x8000),是因为对uint16类型的变量来说,最高位是第15位(从0开始计),这一位为1时,数值必然大于等于32768。这样写避免了bitand函数里类型不匹配的麻烦,逻辑也更清晰。
函数跑完后,返回的crc值就是CRC-16校验码。但是要注意,这个函数实现的是最简模型:没有做输入反转,也没有做结果异或,所以它算出来的结果和某些协议标准里的CRC-16可能对不上。对不上不代表算法错了,而是因为协议参数不同,下一小节会细说。
3.2 查表法:把8次循环换成1次查表
逐位法虽然好懂,但效率确实不高。每处理一个字节,内部要循环8次判断,处理1000个字节就要循环8000次。在MATLAB仿真里无所谓,但到了单片机上,这会白白消耗大量CPU时间。
查表法的优化思路很巧妙:既然CRC计算就是对每个字节重复做“异或后左移”的操作,而一个字节的取值范围只有0到255共256种可能,那不如预先算出这256种情况下CRC寄存器的变化值,存成一张表。后面每次处理字节时,用查表代替循环,处理一个字节只需要几次异或运算。
function table = crc16_table_gen(poly) table = zeros(256, 1, 'uint16'); poly = uint16(poly); for i = 0:255 crc = uint16(bitshift(i, 8)); for bit = 1:8 if crc >= 32768 crc = bitxor(bitshift(crc, 1), poly); else crc = bitshift(crc, 1); end end table(i + 1) = crc; end end function crc = crc16_table_calc(data, table, init) crc = uint16(init); data = uint16(data(:)); for i = 1:length(data) idx = double(bitxor(bitshift(crc, -8), data(i))) + 1; crc = bitxor(bitshift(crc, 8), table(idx)); end end这段代码里有几个细节非常考验基本功。
第一个细节是,MATLAB的数组索引从1开始,而表的序号从0到255,所以取表的时候idx必须加1。这个坑我当年踩过,用C语言的思维写MATLAB代码,结果查表总是错一位,最后打印中间值才发现索引问题。
第二个细节是,表里的每一项代表“当前字节对CRC寄存器的影响值”。计算时,先取出CRC寄存器的高8位,和当前字节做异或,得到一个0到255的索引值,查表得到对应的变换值,再把CRC寄存器左移8位和这个变换值异或。这比逐位法少了一个内层循环,处理一个字节的时间基本固定,性能提升非常明显。
第三个细节是double(...)强转。因为bitxor返回的是uint16,直接加1再作为索引也没有问题,但MATLAB偶尔会给提示,干脆转成double,索引运算更干净。
3.3 完整CRC模型:初值、反转、结果异或一个都不能少
真正做项目时你会发现,光是“CRC-16”这个名字根本没法确定最终校验码是什么。同样是CRC-16,MODBUS协议、CCITT协议、IBM协议算出来的结果完全不同,原因就是参数表不同。
一个完整的CRC模型需要五个参数:多项式poly、初始值init、输入反转refin、输出反转refout、结果异或值xorout。以CRC-16/MODBUS为例,它的完整模型是poly=0x8005,init=0xFFFF,refin=true,refout=true,xorout=0x0000。refin和refout为true,意味着计算前对每个输入字节按位反转,计算后对结果也按位反转。这种看似很小的差别,会让最终校验码天差地别。
针对refin和refout为true的情况,工程上有一个等价实现:把整个算法反过来处理,判断最低位而不是最高位,右移而不是左移,多项式也要预先反转。下面这段代码就是CRC-16/MODBUS的完整实现,也是最常见的反射算法:
function crc = crc16_modbus(data) % CRC-16/MODBUS % poly=0x8005, init=0xFFFF, refin=true, refout=true, xorout=0x0000 poly = uint16(0xA001); % 0x8005的位反转 crc = uint16(0xFFFF); data = uint16(data(:)); for i = 1:length(data) crc = bitxor(crc, data(i)); for bit = 1:8 if bitand(crc, uint16(1)) % 判断最低位 crc = bitxor(bitshift(crc, -1), poly); else crc = bitshift(crc, -1); end end end % xorout为0,无需额外异或 end这段代码的正确性验证很简单。网上公开的CRC-16/MODBUS测试向量是:对ASCII字符串“123456789”计算,结果应该是0x4B37。你在MATLAB命令行输入以下代码验证:
data = uint16('123456789'); crc = crc16_modbus(data); fprintf('0x%04X\n', crc);如果能打印出0x4B37,说明参数没配错。这也是我强烈建议的做事方式:写任何CRC代码,先找公开测试向量验证,别拿自己的数据盲测。测试向量都没过的话,后面全白搭。
常见CRC模型参数如下表,方便你对照调试:
| 协议名称 | 多项式 | 初始值 | refin | refout | xorout |
|---|---|---|---|---|---|
| CRC-16/MODBUS | 0x8005 | 0xFFFF | true | true | 0x0000 |
| CRC-16/CCITT | 0x1021 | 0xFFFF | false | false | 0x0000 |
| CRC-16/XMODEM | 0x1021 | 0x0000 | false | false | 0x0000 |
| CRC-32 | 0x04C11DB7 | 0xFFFFFFFF | true | true | 0xFFFFFFFF |
4. 仿真验证:从正确链路到错误注入
4.1 收发链路基线验证
算法写好了,先别急着做花活,第一件事是验证一条完全正确的数据链路能不能通过校验。这一步是用来确认“CRC计算本身没毛病”的。
我写了一个简单的主脚本:发送端定义一组测试数据,调用crc16_calc或crc16_table_calc算出校验码,然后模拟接收端拿到同样的数据后再算一次,两次结果一致就打印“校验通过”。
data = uint16([0x01, 0x02, 0x03, 0x04, 0x05]); poly = 0x8005; init = 0x0000; table = crc16_table_gen(poly); crc_tx = crc16_table_calc(data, table, init); crc_rx = crc16_table_calc(data, table, init); if crc_tx == crc_rx disp('校验通过'); else disp('校验失败'); end这个脚本跑通之后,你可以顺手做一个小实验:把CRC校验码按高字节在前、低字节在后的顺序拼接在数据后面,然后对“数据+校验码”整体重新计算一次CRC,结果会变成0。这是CRC的一个数学性质,可以用来快速验证自己的拼接逻辑是否正确,不过这个方法只适用于非反射模型,反射模型要反过来拼。
4.2 错误注入:模拟单比特翻转和突发错误
链路验证通过,接下来就该测试CRC的检错能力了。真实通信链路里最常见的错误有两种:单比特翻转和突发错误。单比特翻转就是数据中某一个比特由0变成1或由1变成0;突发错误是连续几个比特都被干扰,比如电机启动瞬间串口收到的乱码。
在MATLAB里模拟这两种情况非常方便:
data_correct = uint16([0x01, 0x02, 0x03, 0x04, 0x05]); crc_original = crc16_table_calc(data_correct, table, init); % 模拟单比特翻转:翻转第3字节的第4位 data_error = data_correct; data_error(3) = bitxor(data_error(3), uint16(0x08)); crc_error = crc16_table_calc(data_error, table, init); if crc_error == crc_original disp('错误未被检出'); else disp('单比特翻转已被检出'); end % 模拟突发错误:连续翻转第2字节到第4字节的部分比特 data_burst = data_correct; data_burst(2) = bitxor(data_burst(2), uint16(0xF0)); data_burst(3) = bitxor(data_burst(3), uint16(0x3C)); data_burst(4) = bitxor(data_burst(4), uint16(0x0F)); crc_burst = crc16_table_calc(data_burst, table, init); if crc_burst == crc_original disp('突发错误未被检出'); else disp('突发错误已被检出'); end运行下来你会发现,CRC对这两种错误基本都能检出来。原因在于模2除法的数学特性:只要错误多项式不是生成多项式的倍数,余数就不为零,最终CRC值就会变化。CRC能检测所有单比特错误、所有双比特错误、所有奇数个错误,以及所有长度不超过校验位宽度的突发错误。但要注意,CRC不是万能的,如果错误多项式恰好是生成多项式的整数倍,CRC值就会阴差阳错地保持一致,这就是所谓“漏检”。虽然概率很低,但你心里得有个数。
4.3 用测试向量校准协议参数
在调试真实项目时,还会遇到一种情况:CRC代码逻辑完全正确,但和对方的协议对不上。这种问题八成是参数表不匹配。我在帮一个朋友调MODBUS-RTU通信时,就发现他代码里用的多项式是0x8005,但初始值写成了0x0000,而MODBUS标准要求init=0xFFFF。结果就是通信双方都以为自己在算CRC,可算出来的校验码永远对不上。
解决办法是,先找一个权威的在线CRC计算工具,或者用公开的测试向量校准自己的代码。我测试CRC-16/MODBUS通用做法,就是直接跑“123456789”这个向量,一旦输出0x4B37,就说明参数全对,接下来再去查业务层逻辑。
5. 常见问题与踩坑实录
5.1 结果和参考工具不一致怎么办
这是被问得最多的问题。拿到一个参考CRC结果,用自己程序算出来不一样,第一反应别急着怪算法,先核对五个参数:多项式、初始值、输入反转、输出反转、结果异或。五个参数里只要有一个不一样,结果肯定不一样。
还有个隐蔽问题:有些资料里多项式会省略最高位的1。以CRC-16为例,完整多项式是x^16 + x^15 + x^2 + 1,写作二进制是1 1000 0000 0000 0101,最高位这个1是固定的,十六进制就是0x18005。但实际使用时,因为最高位参与运算最后一定被消掉,所以很多资料直接写0x8005。如果你看到0x8005和0x18005,别慌,它们是同一个多项式。
5.2 下载或解压时遇到的7-zip CRC error
很多人在搜CRC相关资料时,会搜到“安装显卡驱动提示7-zip CRC error”这类问题。这里多说一句,7-Zip在解压时也会用CRC校验来确认压缩包是否完整,如果解压时报出CRC error,通常意味着压缩包下载不完整、存储介质有坏道,或者内存不稳定导致数据读取出错。解决办法一般是重新下载安装包,或者换一个镜像源下载,顺便检查一下硬盘和内存。这也说明CRC在文件完整性校验中的应用远比我们日常关注到的要广泛。
5.3 MATLAB位操作的数据类型坑
MATLAB的位操作函数对数据类型极其敏感。bitxor(uint16(1), double(1))这种写法会直接报错,提示两个输入类型不一致。所以写MATLAB的CRC代码,一定要养成“所有参与位操作的数据先统一转成uint16或uint32”的习惯。
另外,bitshift对于无符号整数的“溢出”处理是静默截断。比如bitshift(uint16(0xFFFF), 1)的结果不是0x1FFFE,而是0xFFFE,高位丢掉了。这个行为在CRC算法里其实正是我们需要的,因为左移后最高位本来就不参与计算,但如果你没意识到这一点,中间调试时会晕头转向。这里建议你打印每一轮循环的crc值,亲眼看一下数据变化规律,跑通一遍之后,CRC在脑子里就不再是抽象公式了。
5.4 查表法索引错位
前面提到过,MATLAB数组下标从1开始,而查表法的索引是从0开始。把C语言代码翻译成MATLAB时,这个差异最容易引发bug。我在代码里专门用了idx = double(bitxor(bitshift(crc, -8), data(i))) + 1,就是为了把索引从0到255映射到1到256。如果你得到的CRC结果忽大忽小,而且明显没有规律,优先检查这一行是不是少了加1。
还有个相关细节:bitshift(crc, -8)对uint16类型是右移8位,得到的是高8位值,范围0到255,和当前字节异或后也还是0到255,所以索引是安全的。
6. 个人使用心得与后续扩展建议
这个MATLAB CRC仿真程序我陆陆续续用了一年多,从最初的逐位法到后来的查表法,再到支持refin/refout的完整模型,每次迭代都让我对CRC的认识更深一层。我最大的体会是:CRC这个技术看起来“老”,但它在串口通信、以太网帧校验、NFV文件系统完整性校验里都是底层基石,值得花时间把原理彻底搞透。
如果你想把这套仿真真正用到自己的项目里,我建议你往两个方向扩展。
第一个方向是增加多协议支持。现在函数里写死了16位宽度,你可以把crc16_calc改成通用的参数化函数,传入poly、init、refin、refout、xorout,宽度也作为参数,这样CRC-8、CRC-16、CRC-32一套代码全搞定。
第二个方向是自动做错误注入统计。我现在会写一个循环,随机翻转数据里的多个比特,统计CRC成功检出的次数和漏检次数,用来评估不同生成多项式的检错能力。这个对比实验在做协议设计时很实用,可以验证自己选的多项式是否靠谱。
最后再分享一个调试技巧:无论你用什么语言写CRC,第一版代码一定要先跑公开测试向量,别拿自己的业务数据试。业务数据出错了,你不知道是算法问题还是参数问题,而测试向量能帮你把算法和参数一次性隔离出来。这个习惯帮我节省了大量排查时间,今天也一并写在这里。
本文还有配套的精品资源,点击获取