48、Bootloader设计:启动流程、分区管理、固件升级
Bootloader,说白了就是系统上电后第一个跑的程序。它负责把真正的应用程序从Flash里搬出来,然后跳过去执行。很多人觉得Bootloader就是个跳板,没啥技术含量。嗯,我刚开始也这么想,直到有一次产品量产时发现固件刷死了,整批货卡在产线……那滋味,真不好受。
今天咱们就聊聊Bootloader设计的三个核心:启动流程怎么走、分区怎么规划、升级怎么保证不掉坑。
启动流程:从复位向量到跳转
芯片上电后,CPU会从复位向量指向的地址开始执行。这个地址,通常就是Bootloader的入口。我个人习惯把Bootloader放在Flash的起始位置,比如0x08000000。
启动流程大致分这几步:
- 硬件初始化:关看门狗、设置时钟、初始化堆栈。这一步要快,别磨蹭。
- 外设初始化:串口、Flash控制器、可能还有I2C或SPI。我建议只初始化升级会用到的外设,别一股脑全开。
- 检查升级标志:看看有没有升级请求。比如某个GPIO被拉低,或者Flash里某个标志位被置位。
- 跳转或等待:有升级请求就进入升级模式,没有就直接跳转到应用程序。
核心要点:跳转前一定要关中断、关SysTick、复位外设状态。否则应用程序一启动就可能跑飞。
跳转的代码其实很简单,但容易出错。我贴一段我常用的跳转函数:
typedef void (*app_func_t)(void);
void jump_to_app(uint32_t app_addr)
{
// 关全局中断
__disable_irq();
// 关SysTick
SysTick->CTRL = 0;
// 复位所有外设时钟(根据芯片调整)
RCC->AHB1RSTR = 0xFFFFFFFF;
RCC->AHB1RSTR = 0;
RCC->APB1RSTR = 0xFFFFFFFF;
RCC->APB1RSTR = 0;
RCC->APB2RSTR = 0xFFFFFFFF;
RCC->APB2RSTR = 0;
// 设置主堆栈指针
__set_MSP(*(uint32_t *)app_addr);
// 跳转到复位向量+4(即Reset_Handler)
app_func_t app = (app_func_t)(*(uint32_t *)(app_addr + 4));
app();
}
这里有个坑:__set_MSP必须在跳转前执行,而且跳转函数要用__attribute__((noreturn))声明。我曾经因为没加这个属性,编译器优化后把堆栈搞乱了,查了两天才找到原因。
分区管理:Flash怎么分才合理
分区设计直接决定了升级的灵活性和可靠性。我见过不少产品,Bootloader和应用程序各占一块,中间留个空白区就算完事。这种设计,说实话,太粗糙了。
我个人推荐的分区方案是这样的:
| 分区名称 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64KB | 启动、升级逻辑 |
| 参数区 | 0x08010000 | 16KB | 升级标志、配置参数 |
| App A(运行区) | 0x08014000 | 256KB | 当前运行的固件 |
| App B(备份区) | 0x08054000 | 256KB | 升级接收的新固件 |
| 日志区 | 0x08094000 | 16KB | 升级日志、错误记录 |
你想想看,为什么要有两个App区?这就是A/B升级(也叫双备份升级)。升级时新固件写到B区,校验通过后把标志位一改,下次启动就从B区启动。万一升级失败,还能回滚到A区。
小技巧:参数区最好单独划出来,别和代码混在一起。我习惯用结构体管理参数,末尾加CRC校验。每次读取时先校验,校验失败就用默认值。
分区大小怎么定?我的经验是:Bootloader给64KB足够了,除非你要支持复杂的加密或压缩。App区大小取决于你的固件大小,建议留出30%的余量。日志区16KB够用,记录最近几次升级结果就行。
固件升级:稳字当头
升级过程最容易出问题。掉电、传输错误、Flash写入失败……任何一个环节出岔子,设备就可能变砖。所以升级逻辑的核心就一个字:稳。
我设计的升级流程大致如下:
- 接收固件头:包含固件大小、CRC、版本号、目标分区等信息。先校验头部CRC,不对就直接拒绝。
- 分块接收数据:每块256字节或512字节,每块带序号和CRC。收到一块写一块,写完后读回校验。
- 整体校验:所有块收完后,对整个固件做一次CRC或SHA256校验。
- 切换标志:校验通过后,在参数区写入“下次从B区启动”的标志。
- 复位重启:Bootloader读到标志,从新分区启动。
警告:千万不要在写Flash的过程中断电!如果无法避免,至少要在写之前备份关键参数。我曾经遇到过客户在升级时拔电源,结果参数区被写坏,设备连串口都打不开了。
这里有个细节:分块写入时,每块写完要立即读回验证。别等到最后再校验,万一中间某块写坏了,你都不知道是哪块。我习惯在每块写入后加一句:
if (memcmp(buffer, (void *)flash_addr, block_size) != 0) {
// 写入失败,记录错误并重试
log_error("Block %d verify failed", block_num);
return -1;
}
重试机制也很重要。我一般允许每块重试3次,3次都失败就终止升级,并回滚到旧固件。别无限重试,否则坏块会卡死整个流程。
SVG流程图:Bootloader核心逻辑
避坑指南:我踩过的几个雷
做Bootloader这些年,我踩过的坑不少。挑几个典型的说说:
- 中断向量表偏移:应用程序的中断向量表必须偏移到它的起始地址。我见过有人忘了设置SCB->VTOR,结果一按按键就触发HardFault。
- 看门狗干扰:升级过程中如果看门狗没喂,写到一半就复位了。我建议在进入升级模式后,把看门狗超时设长一点,或者干脆关掉。
- Flash擦除时间:擦除一个扇区可能要几百毫秒,这段时间如果串口有数据进来,缓冲区可能溢出。我习惯在擦除前先通知上位机暂停发送。
- 版本兼容性:新固件可能改了分区大小或Bootloader接口。我建议在固件头里带上兼容性版本号,Bootloader先检查再决定要不要升级。
我的习惯:每次升级完成后,在日志区写一条记录,包含时间、版本号、校验结果。这样出了问题还能回溯,知道是哪次升级导致的。
最后说一句:Bootloader是系统的最后一道防线。设计时多花点心思,生产时就能少流点汗。别嫌麻烦,该加的校验加,该做的备份做。等你遇到一次批量变砖的事故,就知道这些功夫没白费。
公众号:蓝海资料掘金营,微信deep3321