做电子开发这些年,被问最多的一个问题不是“怎么画板子”,而是“通讯协议能不能自定义”。尤其是碰到黑通道协议、工业总线上跑私有数据、或者想把一段裸通道改造成带安全校验的业务通道时,很多人会愣一下:这玩意儿到底是硬件规定的,还是可以自己定的?今天就顺着这个“电子知识”话题,聊聊黑通道协议能不能自定义,以及如果要做,应该从哪里下手。
先说结论:能,而且在实际项目里,绝大多数“好用”的黑通道协议,都是开发团队针对具体场景自己揉出来的。所谓黑通道,本质上是一段不额外提供安全防护的透明传输通道,它只负责把数据“相对可靠”地搬过去,至于怎么验错、怎么防重放、怎么确认收没收到,都要靠更上层的协议来兜底。正是这个“靠上层兜底”的特性,给了自定义极大的发挥空间:你可以按自己的业务需求定义帧头、定义校验、定义重传规则,甚至是定义一套全新的命令交互机制。
这篇文章不是纯理论,我会把自己在串口、RS-485、以太网这些常见物理链路上折腾自定义黑通道协议的经验拆开讲,包括设计思路、帧结构、校验算法、状态机解析、常见坑和处理办法。适合三类人看:一是正在做设备间通讯的嵌入式工程师,二是想给现有业务加私有协议的产品经理和技术负责人,三是刚入门、想搞懂“协议到底是怎么被设计出来的”同学。
1. 先搞清楚什么是“黑通道协议”
1.1 黑通道的本来面目
很多第一次接触“黑通道”概念的人,会误以为这是一种具体的通讯协议,其实不是。它更偏向一种安全架构里的角色划分思路。
举个生活化的例子:你让快递小哥送一个保险箱,快递公司的运输网络只负责把保险箱完好地送到目的地,路上被打开了、被拆了、被掉包了,运输网络本身不一定知道。保险箱里头到底怎么锁、密码是多少、箱体防不防撬,这些全部由寄件人和收件人自己约定。在这个模型里,快递网络就是“黑通道”,保险箱本身以及双方的验货约定,就是上层协议。
放到电子通讯里,黑通道就是那条只做数据搬运、不附加任何安全机制的链路。比如说两个单片机之间拉了一根串口线,如果直接把原始字节丢过去,不做CRC校验、不编号、不确认,那这根串口线就是一条标准的黑通道。真正的安全任务要交给应用层,比如给每一帧加校验码、加时间戳、加序列号,收端发现校验不对就丢弃,发现序列号跳变就知道中间丢了帧。
这个模型在很多安全相关的工业标准里非常常见。安全要求高的系统,往往会明确要求“传输系统按黑通道来处理”,也就是说,上层不能假设物理层一定不出错,必须自己做足够的检错和纠错设计。理解了这一点,你就能明白:黑通道协议的关键不是“通道是什么样”,而是“上层协议能扛多少责任”。
1.2 黑通道和常见总线的“血缘关系”
有人会问:UART、CAN、以太网这些总线,算不算黑通道?这还真得看你怎么用。
拿最普及的UART举例。它定义了波特率、数据位、停止位,最多再加个奇偶校验,这些只是让两个设备能把字节流对上。但字节流里每一帧从哪儿开始、到哪儿结束、哪个字节是命令、哪个字节是数据、数据坏了怎么办,UART一概不管。从协议分层的角度看,UART只是提供了原始字节流,它之上必须有一个更高层的协议来定义消息结构。这个我们自己的消息协议,就是在黑通道上定义东西。
CAN总线稍微聪明一点,它有ID、有DLC、有CRC校验,还能自动重发。但CAN的CRC只能保证单个帧在物理传输中没有被破坏,它并不能告诉你这条报文是给谁的、这条报文是第几条、这个温度值是不是过期的。你照样得在上层定义一套数据字典和交互规则。工业上还有更经典的做法:在CAN之上跑一个CANopen或J1939,再在它们之上做私有应用层。再加一个串行总线,比如Modbus RTU,它本身有从站地址、功能码、CRC,算是一种标准化协议。但你完全可以只借用它的物理层,重新定义一套自己的帧结构和交互逻辑,本质上还是在做自定义黑通道协议。
所以别被“协议”两个字吓到。总线标准通常只负责“这一段怎么传比特”,协议逻辑是你可以自由发挥的地方。
1.3 为什么非要自定义,不能直接拿现成的用
如果标准协议这么好用,为什么大家还要自造轮子?我在实际项目里遇到的自定义理由,基本可以归纳成这几类:
第一类是业务数据结构太特殊。有些设备的监控数据是定长的、高频的,比如每5毫秒上报一个16字节的结构体。很多标准协议为了兼容各种情况,帧头、地址、长度、功能码一堆字段占用,实际有效数据占比很低。你要是跑在115200波特率的串口上,带宽本来就不宽裕,再用标准协议套一层,数据就塞不下了。这时候自定义一个精简帧结构,可能只需要2个字节的帧头加1个字节长度,效率立刻上来了。
第二类是安全需求太具体。标准协议通常只提供基础的CRC校验,但很多业务还需要知道“这条命令是几秒前发的”“这条数据是不是被人重放了一遍”。协议里不加时间戳、不加序列号,这些功能就做不了。你只能在标准协议之外再包一层,或直接把自定义协议写好。
第三类是命令交互不在常规范围内。我做过一个设备,上位机需要随时插入一个“优先级极高的急停命令”,要能在数据流中间马上被识别出来。标准协议一般按照先来后到的顺序处理,很难体现这种优先级。我自己设计协议的时候,直接在帧头后面留了一个优先级字段,接收端看到高优先级帧就立刻打断当前任务去执行。这个灵活性,标准协议很难给你。
说白了,自定义黑通道协议不是为了炫技,是为了让协议结构跟业务模型严丝合缝。这也是为什么“自定义”在电子知识领域经久不衰的原因。
2. 动手设计前,必须敲定的四个核心问题
2.1 帧结构设计:这个东东真的“动一发牵全身”
协议最核心的骨架就是帧结构。我一般把它拆成几个区域:帧头、控制域、长度域、数据域、校验域。
帧头的作用是让接收方知道“一帧从这里开始”。在串口这类没有起始同步机制的总线上,帧头尤其关键。很多人会选0xAA、0x55这种交替比特型字节,因为它们二进制能产生比较明显的跳变沿,配合逻辑分析仪很容易抓。我习惯用两个字节做帧头,比如0xAA 0x55,这样单个字节发生错位误判成帧头的概率会低很多。
控制域里面可以放协议版本号、命令字、序列号这些信息。版本号看似可有可无,但等到你的设备已经卖出去几百台,再想改协议格式,版本号就是平滑升级的救生圈。命令字用来区分这是一次查询、还是一条下发、还是一帧心跳。序列号则是我强烈建议每一项都加的字段,每发一帧递增一次,这能让你很轻松地发现丢帧和乱序。
长度域和整个帧结构的设计强相关。有了长度域,接收端就可以知道要收多少个字节才算完整。但我的个人习惯是:长度域只统计“数据域+校验域”的长度,不统计帧头和控制域。这样固定开销和可变开销分开,解析时逻辑更清晰,也方便算最大帧长。
校验域就是整帧数据的“指纹”。这里不只校验数据域,控制域和长度域也要一并带上,因为一旦长度域或命令字被干扰,整帧数据的解析方向直接就跑偏了。校验范围宁可多不可少。
我自己常用的一个精简帧格式长这样:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 帧头1 | 1 | 固定0xAA |
| 帧头2 | 1 | 固定0x55 |
| 版本号 | 1 | 例如0x01 |
| 命令字 | 1 | 0x01读、0x02写 |
| 序列号 | 1 | 每帧递增 |
| 数据长度 | 2 | 数据域+校验域的长度 |
| 数据域 | N | 实际业务数据 |
| CRC16 | 2 | 从版本号到数据域的CRC |
这个结构里,N最大可以到多少?我建议根据你的缓存区和波特率倒推。比如单片机里只有256字节接收缓存,那N最多给200字节,别卡得太极限,留出接收和处理的余量。
2.2 校验算法怎么选:CRC16真的是万能解吗
做协议设计的时候,校验算法是最容易“闭眼选”的环节。一说到验错,很多人直接砸一个CRC16上去,这在大方向没错,但选哪个CRC多项式、CRC初始值是多少、输出高低字节的顺序是什么,都要定清楚。
我见过不少“两边都说自己是CRC16,但联调死活对不上”的情况。原因十有八九是:一方用的是CRC16/MODBUS,另一方用的是CRC16/CCITT,两者多项式、初始值、输入输出处理都不一样。所以只要是自定义协议,第一件事就是把校验算法明确到“版本级别”,最好在协议文档里直接写明初始化值0xFFFF、多项式0xA001、输出低字节在前。比如下面这段CRC16/MODBUS实现,就属于可以直接抄作业的版本:
uint16_t crc16_modbus(const uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x01) crc = (crc >> 1) ^ 0xA001; else crc >>= 1; } } return crc; }展开计算一下:假设你要校验5个字节,第一个字节是0x01,crc初始值是0xFFFF,第一步crc异或0x01变成0xFFFE,然后按位右移8次。如果不进参数表,这种逐位算法在一个几十MHz的单片机上算几十个字节,开销并不大,除非你在一个中断里高频处理成千上万帧,否则性能完全可以接受。真的想要更快的速度,就把CRC表提前算好,做成查表法。
但CRC不是唯一选择,也不要神化它。有些场景里,比如数据长度只有几个字节、干扰又主要呈“突发毛刺”形态,一个简单的异或校验和也能扛住。当然,安全等级要求高的场景,我会再加一步:把序列号、时间戳、甚至一个预共享密钥一起参与校验。这样收到的帧如果不是合法发送端算出来的,校验直接就过不了。这个做法本质上是给黑通道临时加了一道简单认证,成本极低,能挡掉很多意外报文。
2.3 确认和重传机制:让黑通道从“只管发”变成“确认送达”
原始形态的黑通道是只管发不管收没收到,这在很多工业场合远远不够。你要是给一台设备下发了一条“设置电压”的命令,结果报文在半路被干扰了,设备没执行,而上位机以为自己已经设置成功了,那整个系统状态就是错的。
所以自定义协议里,我强烈建议给关键命令加上应答帧。最简单的模型是这样:
- 发送端发出命令帧,携带序列号,比如0x01。
- 接收端处理完,回一个应答帧,应答帧里带上原始序列号0x01,并填一个状态码表示成功或失败。
- 发送端收到应答,发现序列号和自己发出去的序列号一致,才认为这次命令执行完成。
如果发送端在一定时间内没收到应答,就重发。这个“超时重传”的时间怎么定?得看链路特性。在115200波特率串口上,一帧几十字节,毫秒级就能传完,所以我把超时定在50毫秒到100毫秒比较合适。但如果走的是低速无线模块或者云端中转,时间就要重算,定太短会疯狂误重发,定太长用户又会觉得卡顿。实操中我会加一个可配置项,出厂默认一个值,到了现场根据实际链路调整。
不过别一发不可收拾,重传次数要封顶。我最常用的策略是重传3次,3次都失败就向上层汇报“链路异常”,而不是无限重发,否则链路上全是重复帧,新命令反而发不出去,这就是所谓的“重传风暴”。
2.4 防重放和时效性:别让一条旧数据把系统带偏
有些业务里,数据的新鲜度比准确度更重要。比如一个温度巡检系统,就算你收到一帧校验正确的温度值,但如果它是10分钟前的数据,对当前决策来说依然没有意义。
解决这个问题,最简单的办法是在帧里加时间戳或序列号。接收端记录上一帧的序列号,新帧序列号必须比上一帧大,否则就视为旧帧直接丢弃。如果链路经常出现重置,发送端的序列号有时会被清零,那就要再加一个“设备重启计数”或者干脆只比较序列号差值是否合理,给一个窗口。
比如我做过一个项目,发送端每100毫秒发一帧,接收端就设定“序列号差值超过10就丢弃整帧”。这样即使中间偶发丢帧,只要差值在合理范围,就照样接受;一旦出现异常跳变,说明链路已经不稳定,可以触发告警。这种“带窗口的序列号判断”,比单纯地“后一帧必须等于前一帧+1”要实用很多。
3. 实操:从零搭一个简化版自定义黑通道协议
3.1 场景定义和运行环境
为了让这个过程不那么抽象,我给一个具体场景:有两块板子用UART直连,波特率115200,8位数据位,1位停止位,无硬件流控。主控板A定时采集传感器数据,通过串口发给从板B,同时B还能主动向A下发控制命令。
在这个场景里,我会把协议拆成两条主要路径:上行数据帧(A到B,定时上报),下行命令帧(B到A,随时下发)。虽然都在同一条串口上,但命令字的定义会让接收端清楚地知道该把数据交给哪个处理逻辑。
3.2 帧结构定义细化和打包函数
我直接用前面那张格式表来落地。定义这套常量:
- 帧头1:0xAA
- 帧头2:0x55
- 版本号:0x01
- 命令字:0x01表示上报,0x02表示下发,0x03表示应答
- 序列号:上行和下行分别维护独立的计数器
- 数据长度:两个字节,存储“数据域长度+2”,这里的2就是CRC域长度
- 数据域:业务数据
- CRC16:覆盖版本号、命令字、序列号、数据长度、数据域
写一个打包函数,把上面的字段按顺序灌进发送数组里:
uint16_t build_frame(uint8_t cmd, uint8_t seq, const uint8_t *payload, uint16_t payload_len, uint8_t *out_buf) { uint16_t index = 0; out_buf[index++] = 0xAA; out_buf[index++] = 0x55; out_buf[index++] = 0x01; // version out_buf[index++] = cmd; // command out_buf[index++] = seq; // sequence out_buf[index++] = (payload_len + 2) >> 8; out_buf[index++] = (payload_len + 2) & 0xFF; memcpy(&out_buf[index], payload, payload_len); index += payload_len; uint16_t crc = crc16_modbus(&out_buf[2], index - 2); out_buf[index++] = crc & 0xFF; out_buf[index++] = (crc >> 8) & 0xFF; return index; }函数返回的是整个帧的字节数。注意,CRC算的是从版本号开始一直到数据域为止,不包含帧头。这里有一个容易被忽视的细节:CRC在通信协议里的字节序到底先送低字节还是高字节。我建议统一成“低字节在前”,因为很多单片机的串口发送是从缓冲区低地址开始的,这样排布比较省事。两头约定一致就不会出问题。
3.3 接收端状态机:不要动不动丢数据
串口接收的经典难题是不知道一帧从哪儿开始、到哪儿结束。如果用DMA接收,很容易把两帧数据混在一起,处理起来很难看。我推荐的做法是逐字节驱动一个状态机,每来一个字节就喂给状态机,让它自己完成“找帧头、收头部、收数据、收校验”的完整过程。
简单说就是:
typedef enum { ST_IDLE, ST_HDR1, ST_HDR2, ST_FIXED, ST_DATA, ST_CRC0, ST_CRC1 } parse_state_t; uint8_t rx_buf[256]; uint16_t rx_len = 0; uint16_t expect_len = 0; parse_state_t state = ST_IDLE; void uart_rx_byte_handler(uint8_t byte) { switch (state) { case ST_IDLE: rx_len = 0; if (byte == 0xAA) state = ST_HDR1; break; case ST_HDR1: if (byte == 0x55) state = ST_FIXED; else state = ST_IDLE; break; case ST_FIXED: rx_buf[rx_len++] = byte; if (rx_len == 4) { // version/cmd/seq/lenH/lenL 共5字节,此处简化,4字节后再咬定 expect_len = ((uint16_t)rx_buf[3] << 8) | rx_buf[4]; if (expect_len > 250) state = ST_IDLE; else state = ST_DATA; } break; case ST_DATA: rx_buf[rx_len++] = byte; if (rx_len == expect_len + 4) state = ST_CRC0; break; case ST_CRC0: rx_buf[rx_len++] = byte; state = ST_CRC1; break; case ST_CRC1: rx_buf[rx_len++] = byte; // 此时可校验rx_buf[0..expect_len]再和最后两个CRC字节比对 state = ST_IDLE; break; } }这段代码是为了说明思路,没有做完整校验,真实项目里你会把帧头到数据域的所有字节都存下来,再调用CRC函数算一遍。为了更健壮,我还会额外做一个超时判断:状态机在非IDLE状态下停留超过“几个字符时间”还没收到新字节,就强制复位回IDLE。这么做能有效避免“半包”卡死状态机的情况。
3.4 联调验证:写个脚本快速测通
硬件协议写完,最怕的就是手动拿串口工具一帧一帧发,费时费力。我习惯写一个简单的Python脚本,直接调用串口,一边发封包,一边把收到的数据做CRC校验,自动化验证协议正确性。脚本里要复刻同一条CRC函数,确保主控端和验证端用的是同一套规则。
import serial def crc16_modbus(data: bytes) -> int: crc = 0xFFFF for b in data: crc ^= b for _ in range(8): if crc & 0x01: crc = (crc >> 1) ^ 0xA001 else: crc >>= 1 return crc & 0xFFFF payload = bytes([0x01, 0x02, 0x03]) frame = b'\xAA\x55\x01\x01\x00' + bytes([len(payload)+2]) + payload crc = crc16_modbus(frame[2:]) frame += bytes([crc & 0xFF, (crc >> 8) & 0xFF]) ser = serial.Serial('COM10', 115200, timeout=1) ser.write(frame) resp = ser.read(32) print(resp.hex())脚本输入的是一个固定的测试包,能把自定义协议从打包到收包的整个链路快速过一遍。等协议稳定后,再把这个脚本扩展成自动化回归测试,每次修改完协议定义就跑一遍,能省下大量人工测试时间。这也是我在热词里看到“自定义脚本”时特别想强调的一点:协议工程也要自动化,不要靠手测。
4. 联调现场:那些绕不开的坑和排查思路
4.1 半包和粘包:串口最容易踩的雷
串口数据是一个字节一个字节来的,如果发送端分两次间隔很小地发了两个帧,接收端可能把它们连在一起,看起来像一个“大包”,这叫粘包。反过来,如果接收端恰好在数据途中开始接收,一帧数据分成几次到达,这叫半包。
解决办法说起来不复杂:有了长度域和帧头,半包不是问题,状态机会一直等到攒齐长度域规定的字节数才判帧;粘包也不会太恶心,因为接收完一帧后状态机会重新回到IDLE,去等下一个0xAA 0x55,不会把上一帧的结尾误认为新帧头。
但比较烦的是数据域内部本身就含有0xAA 0x55字节。比如你要传输一个固件升级包,里面可能任意字节都有。这就会导致状态机在数据域内部误判出一个“假帧头”。这种情况我有两种处理思路:第一种,如果数据域长度是已知的,状态机在ST_DATA状态里已经知道自己要收多少字节了,那就别去匹配帧头,直接把字节存下来就行,帧头匹配只在IDLE状态生效。第二种,对固定帧头协议仍然发生误判时,可以在业务层判断长度是否合理,或者干脆选择更长的三字节帧头。
4.2 CRC正确但业务数据却不对:查长度域
我有一个印象很深的排查经历。设备联调时,接收端校验CRC一直通过,但上位机解析出来的温度值是乱的。折腾了将近两个小时,最后发现是发送端在长度域里填的数据长度和实际数据域字节数不一致。CRC计算范围包含了长度域和后面的数据域,所以LEN填错,CRC计算结果其实也跟着变了,两边如果用的同一套错误算法,CRC当然能对上。
这提醒我一件事:协议里的每个字段都要当成“参与者”,长度域不只是给接收端报个应收字节数,它同时也是CRC的输入。调试时如果CRC对,但数据不对,第一件事就是打印原始帧,用十六进制人工核对一遍长度域和实际字节数。另外,LEN建议固定用大端(高位在前),整个团队里统一,否则两个人看同一帧解析结果可能完全相反。
4.3 重传风暴:确认帧丢了,发端疯了
重传机制看似简单,但有一个陷阱:接收端其实已经处理完命令了,应答帧也在返回的路上,但发送端因为超时没等到应答,就重发了一次完全相同的命令。结果接收端收到了两条一模一样的命令,执行了两次,这在控制类设备上是不能接受的。
所以我设计协议时,会明确区分两种情况:一种是“命令本身幂等”,重复执行没有副作用,那重传是安全的;另一种是“命令不幂等”,比如“下发一次电机正转”,重复下发等于二次启动。针对后一种情况,接收端必须维护一个“最近处理过的序列号缓存”,只要新帧的序列号和最近几个序列号重复,就直接丢弃或只回缓存中的应答,不再执行命令。这个“去重队列”一般长度三五个就够了,能覆盖网络层重传的时间范围。
4.4 调试工具怎么选:能不能可视化解析是关键
自定义协议调试,最怕拿着一串十六进制裸数据在脑子里人肉解码。我强烈建议两步走。
第一步是物理层观察。用逻辑分析仪抓UART波形,确认波特率对不对、有没有毛刺、字节间隙是不是过大。很多时候,你以为协议写错了,实际是线没接好、波特率标称值和实际偏差太大。115200的波特率误差超过2%就可能开始乱码,这在天冷天热、晶振不准时特别容易犯。
第二步是协议层观察。如果只是为了快速验证帧结构,直接用串口助手看十六进制也行;但如果项目周期长,我建议写一个带协议解析的调试小工具,或者直接用开源的协议分析软件,把自定义的帧格式填进去,让工具自动标出帧头、命令字、数据域和CRC。这能极大提高联调效率,尤其是当帧长很长、字节很多的时候。我在“自定义组件”上花的功夫往往都不亏,把协议解析做成一个可视化组件,后续接上位机、写测试脚本都能复用。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查建议 |
|---|---|---|
| 完全收不到数据 | 波特率不一致/接线错/方向引脚没切换 | 先用逻辑分析仪看波形 |
| 收到垃圾数据,偶有正常帧 | 帧头选择太短或数据域里有假帧头 | 用双字节帧头,严谨处理ST_DATA状态 |
| CRC常对不上 | CRC多项式/初值/字节序不一致 | 统一到明确的标准,参照3.2代码 |
| 校验对但数据错 | 长度域填错或字节序混乱 | 打印原始帧逐字节核对 |
| 命令执行了两次 | 应答丢帧后重发导致重复执行 | 接收端加序列号去重 |
| 长时间运行后卡死 | 状态机没有超时复位 | 添加字节间隔超时强制回IDLE |
5. 自定义协议的原则:能做但不等于要全做完
5.1 正确评估“自定义深度”
黑通道协议确实可以自定义,但从业者也需要克制。我见过一个项目,连物理层的电平定义都想自己来,结果做出来只能在自家一小片设备里用,外购设备根本兼容不了。适度的自定义,应该停留在“帧结构和交互流程”这一层;物理接口、波特率、基础电气特性,尽量沿用行业通用标准,这就像盖房子时水管用标准口径,室内怎么走管线可以自己规划。
以UART为例,物理层用标准的UART电平,链路上用标准的串口参数,自定义只发生在数据帧层面。这样做的好处是,以后任何单片机、PC、WiFi模块都能通过串口接入,你不需要为每种接入方单独设计物理适配。这也是最快见效的自定义方式。
5.2 能借产品时就别完全自造
有的团队一提自定义协议,连协议栈都要从零开始写。如果你只是做一个简单的数据采集设备,完全可以考虑先站在现成协议的肩膀上。比如Modbus RTU已经有成熟的功能码设计、CRC校验、从站寻址,你可以直接用它的外壳,只是在功能码里定义自己业务的寄存器表。这就算一种轻量级自定义。
再比如在CAN总线上做运动控制器,我更不会一上来就自造协议。我会先看CANopen能不能覆盖需求,覆盖不了,再在CANopen的应用层里预设私有对象,让标准部分走标准,私有部分走自定义。整体的工程量和工作量会少很多,调试环境也更成熟。
“黑通道协议能不能自定义”真正的价值不在于“能”,而在于“怎么做才是对的”。刚入门的同学不要动辄把协议做成十层嵌套,先定一个精简帧、算好CRC、加序列号、做好超时去重,这套组合已经能覆盖大部分采控业务了。等你真的把这套流程跑通,再回头去看标准协议,就会发现那些协议的设计其实也是这么一笔一画抠出来的。
5.3 后续还可以往哪些方向扩展
如果你手头的自定义协议已经稳定运行,下一步有几件事值得做。
一是加轻量级加密。不需要上AES那种重量级算法,可以用一个可配置的初始向量,把整帧数据和CRC计算范围做一次异或混淆,至少能挡掉外部设备直接伪造合法帧。注意,这块跟真正的信息安全还不是一回事,别吹得太过,它能干的就是把“裸奔”变成“穿了件外套”。
二是区分优先级通道。在帧头里预留一个优先级位,让急停、告警、心跳这类关键帧可以被接收端优先处理。协议能跑通只是第一步,跑得聪明才是后面几年的打磨方向。
三是做协议版本兼容。现在帧头里有版本字段,后续增加新字段时尽量加在数据域内部,别动帧头和固定控制域的结构。如果实在要变,版本号可以让新旧设备各自选择解析方式,实现平滑升级。
我在多个项目里的体会是,自定义协议最大的坑往往不在代码,而在“没有文字定义”。所以无论协议多简单,请把帧格式表格、CRC细则、序列号规则、超时参数全部写进文档里。这份文档就是团队里的“法律”,后期改一行字段,大家一起评审,才能少踩坑。