Bootloader设计基础:分区、启动、跳转与看门狗
各位同学,今天我们来聊聊Bootloader。这东西说白了,就是固件升级的「守门员」。我做了这么多年嵌入式,见过太多因为Bootloader没写好,导致设备变砖的案例。嗯,咱们今天就把这块硬骨头啃下来。
一、Bootloader与App分区
先说说分区。你想想看,MCU的Flash就那么大一块,怎么安排才合理?我个人习惯把Flash分成三个区:Bootloader区、App区、参数区。
| 分区名称 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader区 | 0x08000000 | 32KB | 存放Bootloader代码 |
| App区 | 0x08008000 | 192KB | 存放应用程序 |
| 参数区 | 0x08038000 | 32KB | 存放校准参数、配置信息 |
为什么Bootloader要单独占一个区?因为它的代码不能跟App混在一起。我在项目中遇到过,有同事把Bootloader和App放在同一个区,结果升级时写错了地址,直接把Bootloader覆盖了。设备当场变砖,只能拆机用烧录器救回来。
二、启动流程
上电后,MCU先执行什么?当然是Bootloader。流程大概是这样的:
- 系统上电,MCU从0x08000000取复位向量
- 执行Bootloader的main函数
- 检查是否有升级请求(比如检测某个GPIO电平,或者检查标志位)
- 如果有升级请求,进入升级模式,等待接收固件
- 如果没有升级请求,校验App的完整性
- 校验通过,跳转到App;校验失败,停留在Bootloader等待升级
这里有个细节:启动时间。Bootloader不能跑太久,否则会影响设备的响应速度。我一般控制在200ms以内完成所有检查和跳转。
三、跳转逻辑
跳转是Bootloader的核心操作。说白了,就是把程序计数器PC指向App的起始地址。但这里有个坑:中断向量表。
App的代码是在Bootloader之后编译的,它的中断向量表地址跟Bootloader不一样。所以跳转前,必须重新设置中断向量表偏移。以STM32为例:
/* 跳转到App */
void jump_to_app(uint32_t app_addr)
{
uint32_t stack_ptr;
void (*app_entry)(void);
/* 1. 关闭全局中断 */
__disable_irq();
/* 2. 关闭所有外设时钟 */
RCC->AHBENR = 0;
RCC->APB1ENR = 0;
RCC->APB2ENR = 0;
/* 3. 从App起始地址读取栈指针 */
stack_ptr = *(uint32_t*)app_addr;
/* 4. 从App起始地址+4读取复位向量 */
app_entry = (void (*)(void))(*(uint32_t*)(app_addr + 4));
/* 5. 设置主栈指针 */
__set_MSP(stack_ptr);
/* 6. 跳转 */
app_entry();
}
我曾经犯过一个错误:跳转前忘了关中断。结果跳过去之后,中断还在响应,但中断向量表还没更新,直接跑飞了。嗯,从那以后,我每次跳转前都会把外设和中断清理干净。
四、看门狗喂狗策略
看门狗这东西,用好了是保护神,用不好就是催命符。Bootloader里怎么喂狗?我总结了几条原则:
- Bootloader启动时,先别开看门狗。因为Bootloader执行时间不确定,万一升级过程比较长,看门狗超时复位就麻烦了。
- 跳转到App前,确保看门狗是关闭的。App那边会重新初始化看门狗,如果Bootloader开着狗,App没来得及喂,又复位了。
- 如果一定要在Bootloader里开看门狗,那就把喂狗周期设长一点,比如10秒。然后在每个关键步骤后喂一次。
举个例子,我在做分拣机固件升级时,遇到过一个问题:升级过程中网络中断,Bootloader一直在等待数据包。看门狗没喂,3秒后复位了。复位后又进入Bootloader,又等数据包,又复位...无限循环。后来我改了策略:在等待数据包时,每500ms喂一次狗,同时加一个超时计数器,超过30秒没收到数据就退出升级模式,直接跳转到旧App。
/* 升级过程中的喂狗策略 */
void upgrade_loop(void)
{
uint32_t timeout = 0;
while(1)
{
/* 每500ms喂一次狗 */
if(get_tick() - last_feed > 500)
{
feed_watchdog();
last_feed = get_tick();
}
/* 检查是否有数据包 */
if(receive_packet())
{
process_packet();
timeout = 0;
}
else
{
timeout++;
if(timeout > 60) /* 30秒超时 */
{
/* 退出升级,跳转到旧App */
jump_to_app(OLD_APP_ADDR);
}
}
}
}
五、避坑指南
最后,我把自己踩过的坑总结一下,你们记好了:
- 分区地址要对齐。Flash的擦除是按扇区来的,分区起始地址必须是扇区对齐的。否则擦除时会擦到别的区。
- App的链接脚本要改。App的起始地址必须跟分区地址一致,中断向量表偏移也要设置正确。
- 跳转前清理外设。DMA、定时器、中断,一个都不能留。否则App那边初始化时会出问题。
- 校验App完整性。跳转前至少算个CRC,确保App代码没被破坏。我一般用CRC32,速度快,校验强度也够。
好了,Bootloader的基础设计就讲到这里。下一章我们聊聊固件包的格式设计,包括怎么打包、怎么校验、怎么加密。你们先把今天的内容消化一下,有问题随时问我。