4、Bootloader设计(下):向量表重映射、跳转逻辑实现、看门狗喂狗策略
好,咱们接着聊Bootloader。上一节我们把启动流程和分区规划讲清楚了,这一节要动真格的了——向量表重映射、跳转逻辑、还有看门狗喂狗。这三个东西,任何一个出问题,你的OTA就是空中楼阁。
我见过太多人,Bootloader写好了,跳转也成功了,结果跑起来就死机。查了半天,原来是向量表没重映射。还有的人,升级到一半看门狗复位了,直接变砖。嗯,这些坑我都踩过,今天咱们一个一个填平。
4.1 向量表重映射:为什么必须做?
先问一个问题:MCU上电后,从哪里开始执行代码?
答案是复位向量。这个向量存在哪里?存在Flash的最开头,也就是0x08000000地址。这是硬件写死的,没得商量。
那我们的Bootloader放在0x08000000,应用程序放在0x08010000。当Bootloader跳转到应用程序时,CPU还是习惯性地去0x08000000找向量表。这就出问题了——应用程序的向量表在0x08010000啊!
所以,你必须告诉CPU:「嘿,向量表搬家了,新地址在0x08010000。」这个过程就叫向量表重映射。
核心要点:不重映射向量表,中断一个都进不去。你的定时器、串口、按键中断,全部失效。
具体怎么做?不同MCU厂商的寄存器不一样。以STM32为例,有个寄存器叫SCB->VTOR(Vector Table Offset Register)。你只需要把应用程序的基地址写进去就行:
// 假设应用程序起始地址为 0x08010000
#define APP_ADDRESS 0x08010000
// 关闭全局中断
__disable_irq();
// 设置向量表偏移
SCB->VTOR = APP_ADDRESS;
// 重新使能全局中断
__enable_irq();
就这么简单?对,就这么简单。但有个细节要注意:必须在跳转之前设置VTOR。我见过有人把VTOR写在应用程序的main函数里,结果Bootloader跳过去之后,还没跑到main就触发了中断,直接死机。
我的经验:VTOR设置最好放在应用程序的启动文件里,也就是Reset_Handler的最开头。这样一上电就生效,万无一失。
4.2 跳转逻辑实现:从Bootloader到App的最后一跃
向量表搞定了,接下来就是跳转。跳转的核心就两件事:
- 获取应用程序的栈指针(MSP)
- 获取应用程序的复位向量地址
- 跳转过去
说白了,就是模拟一次CPU复位。应用程序的起始地址处,前4个字节是栈指针,紧接着4个字节是复位向量。我们直接取出来,然后跳转。
看代码:
typedef void (*pFunction)(void);
// 应用程序起始地址
#define APP_ADDRESS 0x08010000
void jump_to_app(void)
{
uint32_t msp_value;
pFunction app_entry;
// 1. 关闭所有外设中断
// 这一步很重要,我后面会讲
deinit_all_peripherals();
// 2. 关闭全局中断
__disable_irq();
// 3. 获取应用程序的栈指针
msp_value = *(volatile uint32_t*)APP_ADDRESS;
// 4. 获取应用程序的复位向量
app_entry = (pFunction)(*(volatile uint32_t*)(APP_ADDRESS + 4));
// 5. 设置主栈指针
__set_MSP(msp_value);
// 6. 跳转
app_entry();
}
这里有个坑,我当年踩过。你看第1步,为什么要关闭所有外设中断?
你想啊,Bootloader里可能开了定时器、串口、ADC。跳转之前如果不关掉,这些中断还在跑。应用程序一启动,突然来个定时器中断,但应用程序的中断向量表还没初始化好——直接进HardFault。
警告:跳转前必须关闭所有外设时钟和中断。我建议写一个deinit_all_peripherals()函数,把用到的外设逐个关掉。不要偷懒只关全局中断,因为有些外设的DMA还在后台跑。
另外,还有一个容易被忽略的点:SysTick定时器。Bootloader里通常用SysTick做延时,跳转前一定要把SysTick关掉,否则应用程序的SysTick中断和Bootloader的冲突。
4.3 看门狗喂狗策略:OTA过程中的生死时速
看门狗这个东西,用好了是守护神,用不好就是催命符。
在OTA升级过程中,你正在擦除Flash、写入数据,这个过程可能持续几秒甚至几十秒。如果看门狗超时了,啪,复位了。升级到一半的固件就废了。
所以,喂狗策略必须精心设计。我总结了几种方案:
| 方案 | 描述 | 适用场景 | 风险 |
|---|---|---|---|
| 方案一:升级前关狗 | 进入OTA模式后,直接关闭看门狗 | 开发调试阶段 | 升级过程中如果死机,无法自动恢复 |
| 方案二:定时喂狗 | 在擦写循环中,每完成一个扇区就喂一次 | Flash擦写时间可控的场景 | 如果擦写出错卡死,喂狗也救不了 |
| 方案三:独立看门狗+备份区 | 使用独立看门狗,配合备份区做断点续传 | 对可靠性要求极高的产品 | 实现复杂,占用额外Flash空间 |
我个人最推荐方案二,但要做一些改进。来看代码:
// 假设Flash每页大小为2KB,擦写时间约20ms
// 看门狗超时时间设为500ms
void ota_write_firmware(uint32_t address, uint8_t *data, uint32_t len)
{
uint32_t page_count = 0;
while (len > 0)
{
// 擦除一个扇区
flash_erase_page(address);
// 写入数据
flash_write_page(address, data, PAGE_SIZE);
// 喂狗!每完成一页就喂一次
watchdog_feed();
address += PAGE_SIZE;
data += PAGE_SIZE;
len -= PAGE_SIZE;
page_count++;
// 进度上报(可选)
ota_progress_report(page_count);
}
}
这个策略的关键是:喂狗频率要远大于看门狗超时时间。比如看门狗设500ms,你每20ms喂一次,就算某次擦写卡了200ms,也还有300ms的余量。
避坑指南:我曾经在一个项目里,把看门狗超时设成了100ms,Flash擦写一次要80ms。结果每次擦写都卡在临界点上,偶尔一次超时就复位了。后来我把超时改成500ms,问题就解决了。记住,看门狗的超时时间至少要大于最长的单次操作时间,再留50%的余量。
还有一个进阶技巧:在进入OTA模式之前,先记录一个标志位到备份寄存器。这样如果升级过程中看门狗复位了,Bootloader启动时检测到这个标志,就知道上次升级失败了,可以进入恢复模式,而不是傻乎乎地再跳转到损坏的应用程序。
// 进入OTA前,设置标志
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0xA5A5);
// Bootloader启动时检测
if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1) == 0xA5A5)
{
// 上次升级未完成,进入恢复模式
enter_recovery_mode();
}
else
{
// 正常启动,检查应用程序是否有效
if (is_app_valid())
{
jump_to_app();
}
}
4.4 综合实战:一个完整的跳转流程
好了,我们把今天讲的东西串起来,写一个完整的Bootloader跳转函数:
void bootloader_jump_to_app(void)
{
uint32_t app_base = 0x08010000;
uint32_t msp;
pFunction entry;
// 1. 检查应用程序有效性(CRC校验等)
if (!is_app_valid(app_base))
{
// 应用程序无效,停留在Bootloader
return;
}
// 2. 关闭所有外设
deinit_all_peripherals();
// 3. 关闭全局中断
__disable_irq();
// 4. 关闭SysTick
SysTick->CTRL = 0;
// 5. 清除所有挂起的中断
NVIC->ICER[0] = 0xFFFFFFFF;
NVIC->ICER[1] = 0xFFFFFFFF;
NVIC->ICPR[0] = 0xFFFFFFFF;
NVIC->ICPR[1] = 0xFFFFFFFF;
// 6. 设置向量表
SCB->VTOR = app_base;
// 7. 获取栈指针和入口地址
msp = *(volatile uint32_t*)app_base;
entry = (pFunction)(*(volatile uint32_t*)(app_base + 4));
// 8. 设置栈指针
__set_MSP(msp);
// 9. 跳转
entry();
// 永远不会执行到这里
while(1);
}
这个函数我用了很多年,基本没出过问题。你拿去改改就能用。
最后说一句:跳转之前,记得把看门狗喂饱。不然刚跳过去,还没来得及初始化,看门狗就复位了,那可就太冤了。
本章小结:
- 向量表重映射是跳转的前提,用SCB->VTOR实现
- 跳转前必须关闭所有外设和中断,避免冲突
- 看门狗喂狗策略要保证每步操作都有足够余量
- 备份寄存器标志位可以实现断点续传和故障恢复
下一节我们讲固件包的生成与签名校验,那是OTA安全的第一道防线。到时候见。