嵌入式通信CRC选型与实现:CRC16还是CRC32?STM32+W5500实战复盘
2026/9/15 2:30:12 网站建设 项目流程

做嵌入式的小伙伴应该都遇到过这种场景:一个挂在以太网上的温湿度传感器,要定时把温度、湿度上报给上位机或者PLC,中间隔了交换机、网线、协议栈,谁都不敢保证每个字节都分毫不差。我在这个项目里用的是STM32 + W5500,传感器是SHT30,按自定义帧格式封装成私有UDP报文。上位机同事拿着协议文档,劈头就问一句:这段数据用CRC16还是CRC32?多项式多少?初值多少?RefIn、RefOut开不开?当时我就意识到,CRC校验远不是写个函数那么简单。

这篇文章就把我在以太网温湿度传感器通信里关于CRC16和CRC32的选型逻辑、代码实现、对端联调、踩坑复盘整个过程整理一遍。适合正在做嵌入式通信、需要给链路加校验,或者被“同一个CRC16咋算出来不一样”这类问题折磨过的开发者。内容不预设你已经精通CRC,我会从帧格式讲到参数配置,再讲到可以直接抄的代码,最后把实际项目中遇到的坑一个个拆开。

1. 选型之前,先搞清楚温湿度通信里的CRC到底在防什么

1.1 一个真实的以太网温湿度上报帧

先看我在项目里用的一帧数据,这是一个典型的私有以太网温湿度上报帧:

AA 55 00 08 01 02 20 13 03 10 12 34 CRCH CRCL
  • AA 55:两字节帧头,用来让接收端快速找到帧起始位置。
  • 00 08:两字节长度,表示后面还有8个字节。
  • 01:设备ID,现场挂多个传感器时用来区分设备。
  • 02:命令字,表示“上报温湿度数据”。
  • 20 13:温度数据,高位在前,比如这里代表0x2013,换算成实际温度需要乘以分辨率。
  • 03 10:湿度数据。
  • 12 34:应用层序列号,用来检测丢包、乱序和重复。
  • CRCH CRCL:CRC校验值,覆盖从“长度字段”到“序列号”之间的所有字节,不包括两字节帧头。

接收端拿到一帧数据后,先把CRC算出来和报文末尾的CRCH/CRCL比对,一致才认这帧有效。有人可能会说,以太网不是已经有FCS帧校验了吗?网卡底层不是有CRC32吗?怎么还要自己做CRC?道理很简单:底层以太网FCS负责的是“物理链路到物理链路”的完整性,而我们的传感器数据要经过TCP/IP协议栈、多级缓冲、应用层拷贝,任何一个中间环节出现错位、截断、覆盖,底层FCS都管不到。应用层再做一道CRC,属于端到端的完整性保障,两个校验解决的是不同层面的问题。

1.2 为什么不用校验和与奇偶校验

早期很多嵌入式协议喜欢用简单的累加校验和,就是把要保护的所有字节加起来,取低16位或者取反。这种方案实现起来确实简单,但问题也明显:两个字节互换位置时累加结果不变,一个字节多了0x01、另一个字节少了0x01时结果也可能互相抵消,数据位发生偶发翻转时漏检率相对较高。奇偶校验更弱,基本只能用在UART等单字节链路场景,对一帧几十字节的数据包来说,定位能力约等于零。

CRC不一样。它本质上是把整段二进制数据当作一个多项式,用另一个约定好的生成多项式去做模2除法,最后的余数就是CRC。数据里哪怕只有一位变化,最终余数也大概率不同;如果变化发生在某些特定位置,CRC的汉明距离又能保证一定抓得住。打个比方,校验和像是把所有行李重量加起来,丢了一件、捡到一件没准数字还对得上;CRC更像是给整段数据做了一道格式固定的除法题,商和余数对不上,代码立刻就能发现。对于温湿度传感器这种可靠性要求较高的通信场景,选CRC而不是简单校验和,是值得的。

1.3 CRC16与CRC32的本质差异

CRC16和CRC32最直观的区别是校验值宽度不一样,一个16位,一个32位。宽度不同,生成多项式的规模不同,错误检测能力也不同。但这不是说CRC32就一定比CRC16“高级”,而是要看数据长度和协议需求。

CRC16对短帧数据非常友好,像温湿度上报这种一帧只有十几个字节的场景,它能检测出所有1位、2位、3位错误,以及所有奇数位错误和长度不超过16位的突发错误。CRC32检测能力更强,对几十字节到几KB的数据也有很好的汉明距离,同时漏检率更低。缺点是寄存器更宽、查表内存更大、代码里出错时更不好排查。

另一个需要考虑的因素是“协议栈本身用的是什么”。以太网物理帧尾部那4字节FCS用的就是CRC-32/IEEE,所以如果只说“以太网通信里反正有CRC32了”,那确实说的是底层。但我们要保护的应用层温湿度数据,用的是自己的CRC配置。实际上我在项目里最后选择的是CRC-16/MODBUS,因为后续还要兼容Modbus工具链和部分PLC协议。CRC本身没有绝对好坏,关键是要和你的帧长、误码率要求、上位机协议保持统一。

2. CRC选型的核心考量:帧长、性能、协议兼容性

2.1 帧长越短,CRC16越够用;帧长越长,CRC32越稳妥

选CRC之前,先数一数你要保护的数据有多长。温湿度传感器最典型的上报帧大概十几到几十字节,后面如果要扩展成多通道数据、历史记录批量上传、固件升级分包,帧长会涨到几百字节甚至几KB。

CRC能保证检测多少位错误,用汉明距离HD来描述。对某个CRC算法,HD=4意味着它能检测出任意1位、2位、3位错误,超过3位的错误就不能打包票全部检出。CRC16在数据长度不超过一定范围时,汉明距离可以保持4;CRC32在同样长度下常常能到8甚至更高。换句话说,如果数据帧短,CRC16的检测能力已经完全覆盖温湿度传感器的可靠性需求;如果数据帧动不动上百字节,又对可靠性抠得细,那CRC32更让人放心。

我当时做了一个很简单的判断:温湿度每秒钟报一次,每次一帧20字节左右,链路误码率在没有CRC时已经是小概率事件,CRC16能拦住几乎全部可能出现的翻转错误。如果以后要在这个协议上跑固件升级,一包512字节,我就会把CRC32作为升级通道的校验方式。选型不是拍脑袋,而是先问“我最长的一帧有多长、我能容忍的漏检率是多少”。

2.2 计算开销:逐位法、查表法、硬件CRC怎么选

CRC实现方式大概有三条路:逐位计算、查表法、芯片内置硬件CRC外设。很多初学者觉得CRC很复杂,其实占用的计算资源远没有想象中那么大。

逐位法每处理一个字节要做8次位移和异或,逻辑最简单,代码只有十几行,不用额外内存,但速度最慢。查表法预先算出256项查找表,每处理一个字节只需要做一次异或、一次查表、一次移位,CRC-16查表表长256项,占512字节,CRC-32单表占1024字节,如果用slicing-by-4或者slicing-by-8优化,表会到4KB甚至8KB,但速度更快。硬件CRC外设,比如STM32系列里的CRC单元,可以做到一个周期或者几个周期算完一个数据块,但参数配置和不同芯片的兼容性需要仔细对照手册。

放到温湿度传感器这个具体场景里,MCU是72MHz的STM32F103,一帧20字节,就算用最慢的逐位法,算一遍也就几十微秒,而上报周期动不动就是100ms甚至1秒。说直白点,CRC计算根本不会成为瓶颈。选哪种实现,更多取决于代码整洁度和个人习惯。我建议只要RAM不紧张,就用查表法,代码统一、性能充足、逻辑也容易验证。

2.3 协议兼容性:别让上位机问你“你的CRC是哪个版本”

这是CRC选型里最容易掉坑的地方。很多工程师想当然地认为CRC16就应该是某一种固定算法,结果对上位机联调时发现两边算出来的校验值完全对不上。原因很简单:CRC是一族算法,不是一种算法。

同一个CRC16,有CRC-16/MODBUS、CRC-16/CCITT、CRC-16/XMODEM、CRC-16/IBM等一堆变种,多项式、初值、输入输出反射、结果异或各有不同。同一个CRC32,也有CRC-32/IEEE、CRC-32/MPEG-2、CRC-32C等。如果项目对接的是Modbus设备或者PLC,直接用Modbus RTU那套CRC-16/MODBUS是最省心的,整个工控圈子都认这个。如果协议本来就是私有协议,那就必须在协议文档里明确写出五个关键参数:多项式、初值、RefIn、RefOut、XorOut,顺便写清楚计算范围和字节序。

我这个项目最开始就是吃了这个亏。上位机同事用在线CRC计算器算出来的结果,和我们设备算出来的不一致,两边差点吵起来。后来拉了个表格逐项对参数,才发现他用的计算器默认是CRC-16/CCITT,而协议文档里根本没写我们用哪个。把文档补齐之后,半小时内联调就通了。协议兼容性不是说“功能上能通就行”,而是要让上下游看到同一个CRC定义时,心里想的完全是同一个东西。

3. CRC16与CRC32代码实现:从原理到工程落地

3.1 五个参数决定你用的是“哪一种CRC”

正式写代码前,必须先把五个参数吃透。现在随便打开网上任意一个CRC计算工具,都会让你填这几个值。这里我用最快的方式解释一遍:

  • Poly(多项式):CRC算法的生成多项式,通常省略最高位。比如CRC-16/MODBUS的完整多项式是0x18005,代码里常简写为0x8005。CRC-32/IEEE的完整多项式是0x104C11DB7,代码里简写为0x04C11DB7。
  • Init(初值):计算前CRC寄存器的起始值,常见0x0000、0xFFFF、0xFFFFFFFF。
  • RefIn(输入反射):数据字节进入算法前是否按位反转,即最低位先参与计算。
  • RefOut(输出反射):最终结果是否按位反转。
  • XorOut(结果异或):输出或反射后,再与一个固定值异或。

参数不同,结果天差地别。为了验证算法是否写对,行业里有一个通用测试向量:对ASCII字符串“123456789”计算CRC,结果必须是一个固定值。常用CRC参数列在下面这张表里,方便你对照:

算法名称PolyInitRefInRefOutXorOut对“123456789”的校验值
CRC-16/MODBUS0x80050xFFFFtruetrue0x00000x4B37
CRC-16/XMODEM0x10210x0000falsefalse0x00000x31C3
CRC-16/CCITT-FALSE0x10210xFFFFfalsefalse0x00000x29B1
CRC-32/IEEE0x04C11DB70xFFFFFFFFtruetrue0xFFFFFFFF0xCBF43926
CRC-32/MPEG-20x04C11DB70xFFFFFFFFfalsefalse0x000000000x0376E6E7

写完CRC代码后,先拿“123456789”算一遍,对不上表里这个值,说明参数或者代码有错,不用急着拿去联调。

3.2 CRC-16/MODBUS逐位法与查表法实现

我在温湿度传感器项目里最终用的就是CRC-16/MODBUS,原因前面说过,兼容Modbus生态。先看逐位法实现,理解它有助于排查问题:

#include <stdint.h> uint16_t crc16_modbus_bit(uint8_t *data, uint32_t len) { uint16_t crc = 0xFFFF; // Init = 0xFFFF for (uint32_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t bit = 0; bit < 8; bit++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; // 反射多项式 } else { crc >>= 1; } } } return crc; // XOROUT = 0x0000 }

这里稍微解释一下0xA001是怎么来的。CRC-16/MODBUS原始多项式是0x8005,逃不掉RefIn和RefOut都开,于是要把多项式按位反转。0x8005按16位逐位反转后就是0xA001。RefIn=true意味着数据字节最低位先进算法,RefOut=true意味着算完后的结果还要做一次16位反转。上面的代码因为从一开始就用反射后的多项式在右移算法里算,相当于把RefIn和RefOut都隐含进去了,所以不需要最后再额外反转。这个写法是Modbus圈子里最常见的实现。

逐位法虽然好懂,但在工程上我更推荐查表法。查表法依然从0xFFFF开始,每处理一个字节,把当前CRC的低8位和要处理的数据异或,作为查表索引,然后CRC右移8位和表中值异或。查表生成一次,以后每次调用都是固定开销:

uint16_t crc16_modbus_table[256]; void crc16_modbus_init_table(void) { for (uint16_t i = 0; i < 256; i++) { uint16_t crc = i; for (uint8_t j = 0; j < 8; j++) { if (crc & 1) { crc = (crc >> 1) ^ 0xA001; } else { crc >>= 1; } } crc16_modbus_table[i] = crc; } } uint16_t crc16_modbus_fast(uint8_t *data, uint32_t len) { uint16_t crc = 0xFFFF; for (uint32_t i = 0; i < len; i++) { uint8_t idx = (uint8_t)((crc ^ data[i]) & 0xFF); crc = (crc >> 8) ^ crc16_modbus_table[idx]; } return crc; }

用查表法之后,20字节的温湿度帧算一遍大概几微秒到十几微秒,在STM32F103这种72MHz的平台上,开销可以忽略。这张表总共512字节,RAM完全放得下。写完后别忘了用0x4B37这个测试向量验证一下。

3.3 CRC-32/IEEE逐位法与查表法实现

CRC-32/IEEE是Ethernet、ZIP、PNG等场景最常见的一种CRC32,也是以太网FCS的那一套。在温湿度传感器项目里,如果你决定上CRC32,多半会选这个。逐位法代码如下:

uint32_t crc32_ieee_bit(uint8_t *data, uint32_t len) { uint32_t crc = 0xFFFFFFFF; // Init for (uint32_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t bit = 0; bit < 8; bit++) { if (crc & 1) { crc = (crc >> 1) ^ 0xEDB88320; // 反射多项式 } else { crc >>= 1; } } } return crc ^ 0xFFFFFFFF; // XorOut }

0xEDB88320就是0x04C11DB7按32位逐位反转后的结果。因为CRC-32/IEEE的RefIn和RefOut都是true,所以用一个反射多项式就能写进右移算法,最终再异或0xFFFFFFFF。

查表法会更实用,直接生成一版单表实现:

uint32_t crc32_ieee_table[256]; void crc32_ieee_init_table(void) { for (uint32_t i = 0; i < 256; i++) { uint32_t crc = i; for (uint8_t j = 0; j < 8; j++) { if (crc & 1) { crc = (crc >> 1) ^ 0xEDB88320; } else { crc >>= 1; } } crc32_ieee_table[i] = crc; } } uint32_t crc32_ieee_fast(uint8_t *data, uint32_t len) { uint32_t crc = 0xFFFFFFFF; for (uint32_t i = 0; i < len; i++) { crc = (crc >> 8) ^ crc32_ieee_table[(crc ^ data[i]) & 0xFF]; } return crc ^ 0xFFFFFFFF; }

这段代码对字符串“123456789”算出来必须是0xCBF43926。需要注意的是,单表版速度已经不错,但追求极致性能的会在查表时一次性处理4个字节,也就是slicing-by-4,表会变大到4KB。温湿度上报场景根本用不到这种优化,单表足够。

3.4 在STM32+W5500温湿度传感器上接入CRC校验

具体到项目里,传感器数据链路是这样的:STM32通过I2C读取SHT30的温湿度数据,组装成前面说的私有帧,再通过SPI接口发给W5500,W5500负责发UDP包。发送端在组帧的最后一步追加CRC,接收端在帧解析前先做校验。

发送端的代码结构大致如下:

uint8_t frame[128]; uint16_t frame_len = build_sensor_frame(frame, temp, humi); // 计算CRC,范围从frame[2]开始,到frame[frame_len-1]结束 // 即长度字段、设备ID、命令、温湿度数据、序列号 uint16_t crc = crc16_modbus_fast(&frame[2], frame_len - 2); // 把CRC追加到帧尾,这里采用低字节在前 frame[frame_len++] = (uint8_t)(crc & 0xFF); frame[frame_len++] = (uint8_t)((crc >> 8) & 0xFF); w5500_send_udp(frame, frame_len);

接收端的校验逻辑是反过来的。收到一帧UDP数据后,先根据帧头帧尾和长度字段确认帧边界,然后取出末尾的CRC,和重新计算的CRC比较:

uint16_t recv_crc = (uint16_t)frame[frame_len - 1] << 8 | frame[frame_len - 2]; uint16_t calc_crc = crc16_modbus_fast(&frame[2], frame_len - 4); if (recv_crc == calc_crc) { update_temp_humi(frame); } else { sensor_stat.crc_error_cnt++; }

这里要特别提醒:CRC高低字节的组装顺序必须和发送端完全一致。发送端如果低字节在前,接收端组合时也要低字节在前。很多联调失败最后都出在这种对称性上,发送端写了低前高后,接收端却按高前低后去解析,一上来就CRC失败,还以为是算法错了。

4. 联调踩坑复盘与排查速查

4.1 坑位一:参数不匹配,算法本身并没有错

遇到最典型的问题就是“设备算的CRC和上位机算的CRC对不上”。这个绝大多数情况不是代码写错,而是两边用的CRC参数不是同一套。网络上有大量在线CRC计算器,默认算法各不相同,有人默认CRC-16/CCITT,有人默认CRC-16/MODBUS,直接拿着默认值去比对,很容易怀疑自己。

解决这个问题最有效的方法就是使用“123456789”标准测试向量。写一个自测函数,上电后对固定字符串算一遍CRC,和已知值比对。CRC-16/MODBUS必须是0x4B37,CRC-32/IEEE必须是0xCBF43926。自测过了之后,再和上位机联调时就把这五个参数截图或者写进文档,谁都不用猜。

4.2 坑位二:计算范围多一字节、少一字节

CRC算不对,除了参数问题,还有计算范围问题。你算了一帧的CRC,但文档里没说清从哪里开始、到哪里结束,结果发送端把帧头一起算进去了,接收端从长度字段开始算,两边肯定对不上。

我在排查这种问题时,会先在发送端和接收端各自把参与CRC计算的字节范围打印出来,逐字节对比。比如先打印“calc start = 2, calc len = 12”,再打印参与计算的每一字节,基本一眼就能看出差在哪里。协议文档里也应该明确写:“CRC计算范围从长度字段第一个字节开始,到序列号最后一个字节结束,不含帧头,不含CRC本身。”如果包结构后续加了版本号、时间戳之类的字段,记得同步更新文档。

4.3 坑位三:TCP粘包/半包导致CRC被错误计算

如果你用的是UDP,每个报文天然有边界,一个send对应一个recv,CRC校验相对简单。但如果你为了业务方便,把温湿度传感器接成了TCP,那就要小心粘包和半包问题。TCP是字节流,没有边界概念,一次读到的数据可能只有半帧,也可能是两帧粘在一起,直接在字节流里从某个固定位置取CRC来校验,大概率会失败。

处理办法不复杂,但必须做帧同步。用状态机解析:先找帧头AA 55,然后读长度字段,知道这帧完整需要多少字节,再往下收数据,直到凑满一帧才做CRC校验。同时加一个超时机制,如果收到半包后迟迟等不到剩余字节,就清空缓存重新找帧头,避免缓存区里一直留着坏数据。我之前在项目里就因为没有做“长度字段合理性检查”,遇到一个被干扰的0xFFFF长度值,接收缓存差点溢出,CRC自然怎么算都不对。

4.4 坑位四:STM32硬件CRC外设与软件结果不一致

有的朋友为了省CPU时间,会用到STM32内置的CRC外设。这里要特别提醒:不同STM32系列的硬件CRC外设能力差别很大。老一点的F1系列,CRC外设参数几乎是写死的,计算出来的结果和上面这份软件CRC-32/IEEE代码不一定完全一致;F4/H7系列支持配置多项式、初值、输入输出反射,但也要能正确配置配套寄存器才行,而且不是所有芯片的参数都一样。

最稳妥的做法,是先把软件CRC调通,用“123456789”验证结果,再尝试用同一个算法去配置硬件外设,同样跑一遍向量做对照。硬件外设算出来的值如果和软件不一样,不要本能地怀疑其中一边,先查数据手册,确认它是否支持你需要的RefIn/RefOut和XorOut配置。温湿度上报这种低频场景,CPU其实完全算得过来,硬件CRC外设更多是大数据量传输时的优化选项,不是必选项。

4.5 排查流程与避坑工具箱

最后把我常用的排查流程整理成一份速查表,照着走能省很多时间:

现象优先排查项验证方法
CRC结果和工具不一致算法参数不匹配用“123456789”标准向量验证
收发两端CRC不一致计算范围不一致打印首字节索引和参与计算的数据
偶尔CRC失败,大部分正常粘包/半包、干扰、缓冲区错位开启帧头+长度解析,添加超时清缓冲
接入硬件CRC外设后不一致外设参数不支持软件配置对照手册,回退软件CRC验证
上位机显示乱码但CRC通过字节序或数据解析错误确认高前低后,打印原始数据

项目里我还习惯把CRC错误次数、最近一帧错误时间戳、原始错误帧数据都保存下来,便于事后分析。CRC校验通过不奇怪,校验失败才是定位协议问题的最好线索。

这个项目做下来,我最大的体会是CRC16和CRC32没有绝对的好坏,只有适不适合当前场景。温湿度传感器这种短周期、低频、小帧的通信任务,CRC-16/MODBUS查表法已经提供了足够的可靠性;如果哪天要跑批量历史数据或者固件升级分包,我才会考虑把通道切到CRC-32/IEEE,并且保留双算法兼容。另外,把算法参数和校验范围写进协议文档,比调通代码本身更能避免返工。最后再分享一个小技巧:固件里一定要留一个CRC自测函数,上电后对“123456789”算一遍,和标准值比对,错了就直接报警。这个习惯帮我抓过不止一次编译器优化选项错误、Flash表被意外修改、内存越界覆盖CRC表这类隐藏问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询