第4章:Bootloader核心功能实现

好,咱们今天聊点实在的。Bootloader说白了就是一段上电先跑的代码,它的任务只有一个:把真正的固件安全地写进Flash,然后跳过去执行。我做了这么多年嵌入式,见过太多因为Bootloader写得糙导致产品变砖的案例。这一章,咱们就把串口Ymodem接收、Flash擦写、CRC校验这三个核心功能彻底讲透。

本章核心三件事:
  • 用Ymodem协议从串口把固件包收进来
  • 把收到的数据可靠地写入Flash
  • 用CRC32确保固件没被改过

4.1 Ymodem协议接收——别小看这个老协议

Ymodem是Xmodem的升级版,支持128字节和1024字节两种包长,还能传文件名和大小。我最早接触它是在一个卫星接收机项目里,当时觉得这协议太老了,结果发现工业设备里遍地都是。为什么?因为它简单、可靠、有重传机制。

Ymodem的传输流程其实不复杂:

  1. 发送方先发一个文件名包(SOH + 序号0 + 文件名 + 文件大小)
  2. 接收方回应ACK
  3. 发送方开始发数据包(STX或SOH开头)
  4. 每包收完,接收方回ACK或NAK
  5. 发完所有数据,发EOT结束

这里有个坑——超时处理。我曾经在一个项目里没做超时,结果发送方卡住了,Bootloader就一直死等,连看门狗都救不回来。后来我加了个10秒超时,收不到包就复位重来,稳得很。

我的习惯:Ymodem接收状态机用枚举实现,每个状态一个case,清晰好调。别用if-else堆,否则出bug你哭都来不及。
typedef enum {
    YM_STATE_IDLE,
    YM_STATE_WAIT_SOH,
    YM_STATE_RECV_HEADER,
    YM_STATE_RECV_DATA,
    YM_STATE_RECV_CRC,
    YM_STATE_WAIT_EOT,
    YM_STATE_COMPLETE,
    YM_STATE_ERROR
} YmodemState_t;

接收缓冲区我建议用双缓冲——一个收当前包,一个存上一包。这样在Flash写入期间,串口还能继续收,不会丢数据。嗯,这个技巧是我从一次OTA升级失败里学来的。

4.2 Flash擦除与写入——手把手教你操作寄存器

Flash操作是Bootloader的命门。写错了,轻则程序跑飞,重则芯片锁死。STM32的Flash控制器有几个关键寄存器:FLASH_CR(控制)、FLASH_SR(状态)、FLASH_AR(地址)。

操作流程其实就三步:

  • 解锁:向FLASH_KEYR写入两个密钥字
  • 擦除:按扇区擦除,每次擦除前要检查BSY位
  • 编程:按字或半字写入,同样要等BSY清空
注意:擦除操作不可逆!我曾经在调试时忘了关中断,结果擦除到一半被中断打断,Flash控制器锁死,只能复位重来。所以擦除前一定要关全局中断。

我一般把Flash驱动封装成三个函数:

HAL_StatusTypeDef Flash_EraseSector(uint32_t sector);
HAL_StatusTypeDef Flash_WriteWord(uint32_t addr, uint32_t data);
HAL_StatusTypeDef Flash_WriteBuffer(uint32_t addr, uint8_t *buf, uint32_t len);

这里有个性能问题——逐字写入太慢。1024字节的包,一个字一个字写要写256次。我建议用Flash_WriteBuffer做批量写入,内部用循环展开,速度能快3-5倍。你想想看,OTA升级时用户可没耐心等。

操作 典型耗时 注意事项
扇区擦除(1KB) ~20ms 必须关中断
逐字写入(1KB) ~15ms CPU占用高
批量写入(1KB) ~5ms 推荐使用

4.3 CRC32校验——别让坏固件跑起来

固件完整性校验是Bootloader的最后一道防线。CRC32虽然不如SHA256安全,但用在Bootloader里足够了——计算快、硬件支持好、碰撞概率极低。

STM32有个硬件CRC计算单元,用起来很简单:

uint32_t CRC32_Calculate(uint8_t *data, uint32_t len) {
    __HAL_RCC_CRC_CLK_ENABLE();
    CRC->CR = CRC_CR_RESET;
    for (uint32_t i = 0; i < len; i++) {
        CRC->DR = data[i];
    }
    return CRC->DR;
}

但要注意——硬件CRC的初始值和多项式是固定的(0xFFFFFFFF和0x04C11DB7)。如果你的上位机用的软件CRC算法不一样,算出来的值就对不上。我踩过这个坑,后来统一用0xEDB88320查表法,两边一致。

校验策略:
  • 每包校验:收到一包就算一次CRC,和包尾的CRC比较
  • 整体校验:全部收完后,对整个固件区算一次CRC
  • 我建议两个都做——包校验防传输错误,整体校验防Flash写入错误

4.4 整体流程——把三件事串起来

下面这张图展示了Bootloader接收固件的完整流程。我画这张图时特意把状态和关键判断都标出来了,你调试时对着看,能省不少时间。

Bootloader固件接收完整流程 上电启动 检测升级请求 Ymodem接收固件包 包CRC校验 Flash擦除+写入 整体CRC32校验 跳转执行新固件 CRC失败→重发 校验失败→报错 关键:每一步失败都要有明确的错误码和重试机制,别让Bootloader死在那里

4.5 避坑指南——我踩过的那些雷

做Bootloader这么多年,有些坑我几乎每个项目都见一次。列出来,你遇到了直接翻这页:

  • Flash写入未对齐:STM32要求写入地址必须是半字或字对齐,不对齐直接HardFault。我习惯在写入前做地址对齐检查。
  • 中断向量表没重映射:跳转前要把SCB->VTOR设成新固件的起始地址,否则中断一个都进不去。
  • 栈指针没恢复:跳转前要从新固件开头读栈顶指针并写入MSP,不然函数调用就崩。
  • Ymodem包序号乱序:发送方可能重传旧包,接收方要检查序号,别把旧数据当新的写进去。
我的调试技巧:在Bootloader里加一个串口打印开关,用宏控制。调试时打开,看每一包的状态;量产时关掉,省资源。这个习惯帮我省了至少一半的调试时间。

好了,这一章的内容就是这些。Ymodem接收、Flash擦写、CRC校验,这三个功能你吃透了,Bootloader的核心骨架就有了。剩下的就是往上加功能——比如双备份、回滚、加密——这些后面章节再聊。