4. 串口通信协议设计:数据帧格式定义、帧头帧尾、命令字与超时重传
串口通信协议,说白了就是主机(你的PC)和从机(Bootloader)之间约定好的“黑话”。没有这套黑话,两边数据对不上,烧录就是瞎忙活。我早期做项目时,就吃过协议没定义好的亏——帧头帧尾随便选,结果数据错位,查了三天才发现是0xAA和0x55在特定波特率下容易误码。嗯,从那以后,我对协议设计就特别较真。
4.1 数据帧格式:从比特到帧的封装
一个完整的数据帧,就像一封信。你得有信封(帧头帧尾)、收件人地址(设备ID)、信件内容(数据域),还得有个防伪标记(校验码)。我个人习惯把帧结构设计成固定长度+可变长度的混合模式,这样解析起来既简单又灵活。
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 帧头 | 2 | 固定值 0xAA 0x55,用于同步 |
| 设备ID | 1 | 多设备组网时区分目标 |
| 命令字 | 1 | 擦除/写入/校验/跳转等 |
| 数据长度 | 2 | 小端模式,最大1024字节 |
| 数据域 | N | 实际有效数据 |
| CRC16校验 | 2 | 从帧头到数据域的CRC |
| 帧尾 | 2 | 固定值 0x0D 0x0A |
4.2 帧头帧尾:同步与定界的艺术
帧头的作用是告诉接收端:“注意,我要开始发数据了!”帧尾则是:“这包结束了,赶紧处理!”你想想看,如果没有帧尾,接收端怎么知道一帧数据什么时候收完?靠超时?那效率太低了。
我设计的帧头是2字节,帧尾也是2字节。为什么不用1字节?因为单字节太容易在数据域里出现了。比如你传输的固件数据里正好有个0xAA,接收端就会误以为这是帧头,然后整个解析就乱套了。
4.3 命令字定义:擦除、写入、校验、跳转
命令字是协议的“灵魂”。Bootloader通过命令字知道上位机想让它干什么。我一般用单字节命令字,0x01到0x0F,留够扩展空间。
| 命令字 | 名称 | 功能描述 | 数据域内容 |
|---|---|---|---|
| 0x01 | CMD_ERASE | 擦除指定扇区 | 起始地址(4B) + 扇区数(1B) |
| 0x02 | CMD_WRITE | 写入数据到Flash | 起始地址(4B) + 数据(NB) |
| 0x03 | CMD_VERIFY | 校验已写入的数据 | 起始地址(4B) + 长度(2B) + CRC16(2B) |
| 0x04 | CMD_JUMP | 跳转到应用程序 | 跳转地址(4B) |
| 0x05 | CMD_GET_INFO | 获取Bootloader版本/芯片信息 | 空 |
| 0x06-0x0F | 保留 | 预留扩展 | — |
每个命令执行后,Bootloader必须回复一个应答帧。应答帧结构简单:帧头 + 命令字 + 状态码(1B) + CRC + 帧尾。状态码0x00表示成功,0x01表示失败,0x02表示命令不支持。
4.4 超时重传机制:让通信更可靠
串口通信不是100%可靠的。线缆干扰、波特率偏差、缓冲区溢出,都可能导致丢帧或错帧。所以,超时重传机制是必须的。
我一般设计两级超时:
- 字节超时(Byte Timeout):接收两个字节之间的间隔超过一定时间(比如10ms),就认为帧不完整,丢弃当前缓冲区。
- 帧超时(Frame Timeout):发送完一帧后,等待应答帧的最长时间(比如100ms)。超时未收到应答,就重传。
重传次数我通常设为3次。超过3次还没成功,就向上位机报告“通信失败”,让用户检查硬件连接。
4.5 协议状态机:接收端的灵魂
接收端怎么解析这些帧?靠状态机。我画了一个简单的状态转换图,你看看就明白了。
这个状态机看起来简单,但实际调试时坑不少。我记得有一次,接收端在DATA_RECV状态时,上位机突然发了一个帧头过来,结果状态机直接跳到HEADER_RECV,导致当前帧数据丢失。后来我加了一个“帧头保护”逻辑:在DATA_RECV状态下收到帧头,先缓存起来,等当前帧处理完再处理新的帧头。
4.6 代码实现要点
最后,给你看一段状态机核心代码的伪代码框架。实际项目中,我会把状态机放在定时器中断里轮询,或者用DMA+空闲中断的方式接收。
// 串口接收状态机
typedef enum {
STATE_IDLE,
STATE_HEADER,
STATE_DATA,
STATE_CRC,
STATE_DONE,
STATE_ERROR
} rx_state_t;
rx_state_t state = STATE_IDLE;
uint8_t rx_buffer[1024];
uint16_t rx_index = 0;
uint16_t expected_len = 0;
void uart_rx_byte_handler(uint8_t byte) {
switch(state) {
case STATE_IDLE:
if (byte == 0xAA) {
rx_buffer[0] = byte;
rx_index = 1;
state = STATE_HEADER;
}
break;
case STATE_HEADER:
rx_buffer[rx_index++] = byte;
if (rx_index == 4) { // 帧头(2) + 设备ID(1) + 命令字(1)
// 解析数据长度(第5-6字节)
expected_len = rx_buffer[4] | (rx_buffer[5] << 8);
state = STATE_DATA;
}
break;
case STATE_DATA:
rx_buffer[rx_index++] = byte;
if (rx_index == (6 + expected_len)) { // 6字节头 + 数据
state = STATE_CRC;
}
break;
case STATE_CRC:
rx_buffer[rx_index++] = byte;
if (rx_index == (8 + expected_len)) { // 加上2字节CRC
// 校验CRC
if (crc16_check(rx_buffer, rx_index - 2) ==
(rx_buffer[rx_index-2] | (rx_buffer[rx_index-1] << 8))) {
state = STATE_DONE;
} else {
state = STATE_ERROR;
}
}
break;
default:
state = STATE_IDLE;
break;
}
}