4. 串口通信协议设计:Ymodem协议详解、自定义协议帧格式、CRC校验实现、超时重传机制
串口通信协议,说白了就是Bootloader和上位机之间约定好的“黑话”。双方得按照同一套规则来收发数据,否则就是鸡同鸭讲。这一章我们重点聊四个东西:Ymodem协议怎么用、怎么自己定义一套帧格式、CRC校验怎么写、以及超时重传怎么处理。
我个人习惯,在资源允许的情况下优先用Ymodem。但如果MCU的RAM太小,或者你想完全掌控传输过程,那就得自己设计协议了。下面我们一个一个拆开讲。
4.1 Ymodem协议详解
Ymodem是Xmodem的改进版,支持批量文件传输,而且每个数据包带128字节或1K字节的数据。它的核心流程其实不复杂:发送方先发一个文件名和大小,然后一包一包发数据,最后发结束包。
我记得第一次调Ymodem的时候,被它的握手顺序搞晕了。后来画了个流程图才理清楚。你看下面这张图:
Ymodem的包结构是这样的:
| 字节偏移 | 内容 | 说明 |
|---|---|---|
| 0 | SOH (0x01) 或 STX (0x02) | SOH=128字节包,STX=1024字节包 |
| 1 | 包序号 (0x00~0xFF) | 从0开始,循环使用 |
| 2 | 包序号取反 | 用于校验序号正确性 |
| 3~N+2 | 数据 | 128或1024字节 |
| N+3~N+4 | CRC16校验 | 高字节在前 |
4.2 自定义协议帧格式
Ymodem虽好,但有时候太臃肿了。比如你的Bootloader只有4KB空间,塞不下Ymodem的解析代码。或者你想加入一些私有功能,比如加密、压缩、断点续传。这时候就得自己设计协议了。
我设计自定义协议时,一般遵循“短帧头、定长数据、强校验”的原则。下面是我在某个项目里用过的帧格式:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 帧头 | 2 | 固定为0xAA 0x55,用于同步 |
| 命令字 | 1 | 0x01=开始升级,0x02=传输数据,0x03=结束升级 |
| 数据长度 | 2 | 小端模式,表示数据域字节数 |
| 数据域 | 0~1024 | 实际有效数据 |
| CRC16 | 2 | 从帧头到数据域的CRC校验 |
| 帧尾 | 1 | 固定为0x0A,用于帧结束检测 |
你想想看,为什么帧头要用两个字节?因为单字节容易误判。串口上随便一个噪声字节如果是0xAA,就会导致帧同步错乱。两个字节的组合概率就低多了。
4.3 CRC校验实现
CRC校验说白了就是多项式除法。但你别真去手算除法,有现成的查表法,又快又准。我一般用CRC16-CCITT,多项式是0x1021。
下面是我常用的CRC16实现,查表法,速度很快:
// CRC16-CCITT 查表法
static const uint16_t crc16_table[256] = {
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
// ... 省略中间数据,实际使用时需要完整256项
};
uint16_t crc16_calculate(const uint8_t *data, uint32_t len)
{
uint16_t crc = 0x0000; // 初始值
for (uint32_t i = 0; i < len; i++) {
crc = (crc << 8) ^ crc16_table[((crc >> 8) ^ data[i]) & 0xFF];
}
return crc;
}
关键点:
- 初始值:Ymodem用0x0000,有些协议用0xFFFF,要统一
- 结果异或:Ymodem不异或,有些协议最后异或0xFFFF
- 字节序:发送时高字节在前还是低字节在前,双方要约定好
我曾经在一个项目里,上位机用Python的binascii.crc_hqx,下位机用自己写的查表法,结果怎么都对不上。后来发现是Python的crc_hqx初始值是0x0000,而我用的初始值是0xFFFF。改过来就通了。
4.4 超时重传机制
串口通信不像以太网有硬件流控,丢包是家常便饭。没有超时重传,传输就是碰运气。
我一般设计三层超时:
- 字节超时:接收一个字节后,超过10ms没收到下一个字节,认为帧不完整,丢弃
- 帧超时:发送一帧后,超过500ms没收到应答,认为丢包,重传
- 整体超时:整个升级过程超过30秒没有进展,强制退出
重传策略我建议用“指数退避”。第一次重传等500ms,第二次等1秒,第三次等2秒,最多重传5次。为什么?因为如果连续丢包,说明信道质量很差,频繁重传只会让情况更糟。
下面是一个简单的超时重传伪代码:
#define MAX_RETRY 5
#define TIMEOUT_MS 500
int send_frame_with_retry(uint8_t *frame, uint32_t len)
{
for (int retry = 0; retry < MAX_RETRY; retry++) {
uart_send(frame, len);
uint32_t wait = TIMEOUT_MS << retry; // 指数退避
if (wait_ack(wait) == SUCCESS) {
return SUCCESS;
}
// 喂看门狗,防止死锁
watchdog_feed();
}
return ERROR_TIMEOUT;
}
嗯,这里要注意:wait_ack函数里不能是死等。要用定时器或者硬件超时中断,否则整个系统都被阻塞了。我习惯用SysTick做超时基准,每毫秒检查一次。
最后总结一下这一节的核心:协议设计的关键不是“用哪个”,而是“怎么用稳”。Ymodem适合快速实现,自定义协议适合深度定制。CRC是保底防线,超时重传是最后一道保险。把这四个东西配合好,你的串口升级就成功了一大半。