4. IAP性能瓶颈分类
串口通信协议,说白了就是收发双方约定好的「说话方式」。你发一个字节,对方得知道这个字节是数据、是命令、还是校验码。我在Bootloader项目里吃过不少亏——协议没定好,升级到一半就卡死,板子变砖。后来我学乖了,老老实实把协议层做扎实。
这一章,我们重点讲三件事:Ymodem协议的裁剪与定制、帧结构怎么定义才靠谱、以及CRC校验到底怎么算。
4.1 为什么选Ymodem?
市面上串口协议很多:Xmodem、Ymodem、Zmodem、还有各种私有协议。我个人习惯在Bootloader里用Ymodem,原因很简单:
- 支持批量传输:Ymodem可以传多个文件,适合固件+配置一起烧录
- 有完整的应答机制:每个包都有ACK/NAK确认,不会丢包
- 128字节/1024字节可变包长:小包适合低速链路,大包适合高速链路
- 开源实现多:网上能找到现成的C代码,改改就能用
4.2 自定义Ymodem协议:裁剪与增强
标准的Ymodem协议有完整的文件传输流程,但Bootloader场景下我们不需要那么复杂。我一般会做以下裁剪:
| 标准Ymodem功能 | Bootloader裁剪方案 | 原因 |
|---|---|---|
| 多文件传输 | 保留,但限制最多2个文件(固件+配置) | 减少协议复杂度 |
| 文件名/时间戳 | 只保留文件名,去掉时间戳 | Bootloader不需要时间信息 |
| 1024字节包 | 固定使用128字节包 | RAM有限,128字节更安全 |
| 取消/暂停 | 去掉,Bootloader不需要交互 | 简化状态机 |
| CRC16校验 | 升级为CRC32 | 提高检错能力 |
你想想看,Bootloader的ROM通常只有几KB到几十KB,塞不下完整的Ymodem协议栈。所以我们要做的,是取其精华,去其糟粕。
4.3 帧结构定义:每一比特都要算清楚
帧结构是协议的核心。我见过很多新手把帧结构定义得特别复杂,结果调试的时候自己都搞不清楚。我的原则是:够用就好,别炫技。
下面是我在项目中常用的帧结构:
// 帧结构定义
typedef struct {
uint8_t soh; // 帧起始符 (0x01)
uint8_t seq; // 包序号 (0x00-0xFF)
uint8_t seq_inv; // 包序号取反
uint8_t data[128]; // 数据负载
uint32_t crc; // CRC32校验值
} YmodemFrame_t;
每个字段的含义:
- soh (0x01):帧起始符,告诉接收端「我要发数据了」
- seq:包序号,从0开始递增,用于检测丢包
- seq_inv:包序号取反,用于双重校验
- data[128]:实际数据,不够128字节的用0xFF填充
- crc:对整个帧(从soh到data末尾)的CRC32校验
4.4 控制帧:不只是数据
除了数据帧,我们还需要控制帧来管理传输流程。常用的控制帧有:
| 控制符 | 值 | 含义 |
|---|---|---|
| SOH | 0x01 | 数据帧起始 |
| EOT | 0x04 | 传输结束 |
| ACK | 0x06 | 确认收到 |
| NAK | 0x15 | 未确认,请求重发 |
| CAN | 0x18 | 取消传输 |
| C | 0x43 | 请求发送(接收端发起) |
传输流程是这样的:
- 接收端发送字符 'C'(0x43),表示「我准备好了,开始吧」
- 发送端收到 'C' 后,发送第一个数据帧(包序号0)
- 接收端校验CRC,正确则回复ACK,错误则回复NAK
- 发送端收到ACK后发下一帧,收到NAK则重发当前帧
- 所有数据发完后,发送端发送EOT
- 接收端回复ACK,传输结束
4.5 CRC校验实现:别用软件算,用硬件
CRC校验是协议的最后一道防线。很多MCU都内置了CRC硬件模块,速度比软件快几十倍。我个人习惯优先使用硬件CRC,只有在硬件不支持时才用软件实现。
下面是一个硬件CRC32的实现示例(以STM32为例):
// 硬件CRC32初始化
void CRC_Init(void) {
RCC->AHB1ENR |= RCC_AHB1ENR_CRCEN; // 使能CRC时钟
CRC->CR = CRC_CR_RESET; // 复位CRC计算器
}
// 计算CRC32
uint32_t CRC_Calculate(uint8_t *data, uint32_t len) {
uint32_t i;
CRC->CR = CRC_CR_RESET; // 每次计算前复位
for (i = 0; i < len; i++) {
CRC->DR = data[i]; // 逐字节写入
}
return CRC->DR; // 读取结果
}
如果MCU没有硬件CRC,可以用查表法实现软件CRC32:
// 软件CRC32查表法
static const uint32_t crc32_table[256] = {
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
// ... 省略中间数据,实际使用时需要完整256项
};
uint32_t CRC32_Software(uint8_t *data, uint32_t len) {
uint32_t crc = 0xFFFFFFFF;
uint32_t i;
for (i = 0; i < len; i++) {
crc = crc32_table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
}
return crc ^ 0xFFFFFFFF;
}
4.6 协议状态机:让通信有条不紊
协议实现的核心是一个有限状态机。我画了一张图,帮你理清整个流程:
状态机的实现代码:
// 协议状态机
typedef enum {
STATE_IDLE,
STATE_WAIT_C,
STATE_RECEIVE,
STATE_CHECK_CRC,
STATE_SEND_ACK,
STATE_SEND_NAK,
STATE_COMPLETE,
STATE_ERROR
} ProtocolState_t;
ProtocolState_t state = STATE_IDLE;
void Protocol_Process(uint8_t byte) {
switch (state) {
case STATE_IDLE:
if (byte == 'C') {
state = STATE_WAIT_C;
}
break;
case STATE_WAIT_C:
if (byte == SOH) {
// 开始接收数据帧
state = STATE_RECEIVE;
}
break;
case STATE_RECEIVE:
// 接收完整帧后,进入CRC校验
state = STATE_CHECK_CRC;
break;
case STATE_CHECK_CRC:
if (CRC_Check(frame) == SUCCESS) {
state = STATE_SEND_ACK;
} else {
state = STATE_SEND_NAK;
}
break;
case STATE_SEND_ACK:
UART_SendByte(ACK);
state = STATE_WAIT_C;
break;
case STATE_SEND_NAK:
UART_SendByte(NAK);
state = STATE_WAIT_C;
break;
case STATE_COMPLETE:
// 传输完成,跳转到固件
JumpToApplication();
break;
case STATE_ERROR:
// 错误处理
Error_Handler();
break;
}
}
4.7 超时处理:别让协议卡死
协议最怕的就是卡死。发送端发了数据,接收端没回复,两边都在等,然后就死锁了。所以,超时机制必不可少。
我一般会设置两个超时:
- 帧间超时:等待下一个字节的时间,通常设为100ms
- 帧超时:等待完整一帧的时间,通常设为1秒
4.8 实战建议:先仿真,再烧录
协议开发最容易出问题的地方是边界条件。比如:
- 包序号从0xFF跳转到0x00时,接收端能不能正确处理?
- 连续收到两个NAK时,发送端会不会死循环?
- 最后一帧数据不足128字节时,填充的0xFF会不会被误认为有效数据?
我的建议是:先用PC仿真工具测试协议,确认所有边界条件都覆盖到了,再烧录到目标板。我常用的工具是串口调试助手+Python脚本,模拟发送端和接收端的行为。