在上一期(第39期),我们通过修改链接脚本(.ld)和 VTOR 寄存器,把 Flash 里的房产分好了,App 也能正确响应中断了。
现在,Bootloader 已经完成了固件搬运,校验也通过了。 到了最激动人心,也最容易“翻车”的时刻:移交控制权。
这不仅仅是一个函数调用,这是一次灵魂转移。Bootloader 必须把 CPU 的状态恢复到“出厂设置”,然后把接力棒交给 App。如果做得不干净,App 运行几秒钟就会莫名其妙地 HardFault。
让我们进入 第40期,完成这“惊险一跳”。
关中断、清外设、重设 MSP、汇编跳转 PC 的完整步骤
1. 为什么要“大扫除”?
很多新手的 Bootloader 跳转代码只有一行: ((void (*)(void))App_Addr)(); 然后发现 App 跑不起来。
原因: Bootloader 运行期间,可能开启了串口 DMA、开启了 SysTick 定时器、开启了 USB 中断。 当你跳转到 App 时,App 的 main 函数刚开始跑,还没有初始化自己的中断向量表(VTOR 还没改)。 就在这时,Bootloader 遗留的 SysTick 倒计时到了!触发中断! CPU 拿着旧的向量表(指向 Bootloader),却跑在新的环境里,或者干脆因为堆栈混乱,直接 HardFault。
专家法则: 在跳转之前,必须把单片机恢复到**“仿佛刚上电”**的纯净状态。
2. 准备工作:清理现场 (De-Init)
在执行跳转函数之前,必须按顺序执行以下操作:
关闭所有外设时钟: 尤其是 DMA 和 定时器。
关闭全局中断: __disable_irq()。虽然 App 启动后会重开,但跳转瞬间必须关掉。
关闭 SysTick: 这是最著名的“杀手”。必须停掉它,并清除挂起标志。
清除所有中断标志位: 防止跳转后立刻触发。
实战代码:
void Boot_DeInit(void) { // 1. 关闭 USB, UART, SPI 等外设 HAL_UART_DeInit(&huart1); HAL_PCD_DeInit(&hpcd_USB_OTG_FS); // 2. 关闭 SysTick (致命杀手!) SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0; // 3. 关闭全局中断 __disable_irq(); // 4. (可选) 将所有外设复位 // HAL_RCC_DeInit(); // ST的库函数,有时过于暴力,需慎用 }
3. 核心动作:读取新世界的坐标
App 程序的二进制文件(.bin)开头前 8 个字节,蕴含了最重要的信息:
-
0x00 – 0x03: App 的 MSP 初始值 (栈顶地址)。
-
0x04 – 0x07: App 的 Reset_Handler 地址 (代码入口)。
Bootloader 必须把这两个值读出来,塞给 CPU 寄存器。
4. 终极代码:Jump_To_App()
这是可以直接复制到你工程里的标准跳转代码。
// 定义一个函数指针类型 typedef void (*pFunction)(void);
// 假设 App 存在这个地址 #define APP_START_ADDR 0x08004000
void Jump_To_App(void) { uint32_t JumpAddress; pFunction Jump_To_Application;
// 1. 检查栈顶地址是否合法 // 栈必须在 RAM 范围内 (0x20000000 附近) // *(__IO uint32_t*)APP_START_ADDR 取出的是 MSP 的值 if (((*(__IO uint32_t*)APP_START_ADDR) & 0x2FF00000) == 0x20000000) { // 2. 取出 Reset_Handler 地址 (偏移 4 字节) JumpAddress = *(__IO uint32_t*) (APP_START_ADDR + 4); // 3. 将地址转换为函数指针 Jump_To_Application = (pFunction) JumpAddress;
// 4. 【关键步骤】设置主堆栈指针 (MSP) // 必须让 App 使用它自己定义的栈,而不是 Bootloader 的栈 __set_MSP(*(__IO uint32_t*) APP_START_ADDR);
// 5. 【最后一步】跳转! // 实际上就是把 PC 指针赋值为 JumpAddress Jump_To_Application(); } else { // App 地址非法(可能没烧录,或者固件损坏) printf("Error: No valid App found!\\r\\n"); } }
5. 常见死机原因分析 (Troubleshooting)
如果你用了上面的代码还是死机,请排查以下三点:
SysTick 没关干净: App 刚运行几条指令,还没来得及 SCB->VTOR,SysTick 中断就来了。一定要清空 SysTick->CTRL。
App 忘记设置 VTOR: App 跑起来了,但一开中断,CPU 又跑回 Bootloader 的中断向量表了。见第39期。
Bootloader 使用了 FreeRTOS:
-
RTOS 会把堆栈指针切换为 PSP。
-
而 __set_MSP 只改了 MSP。
-
解决: 跳转前必须把 CPU 切回 特权级 (Privileged) 并使用 MSP。这通常需要写几行汇编来复位 CONTROL 寄存器。
// 如果跑了 RTOS,跳转前必须加这一句 __set_CONTROL(0);
6. 总结
Bootloader 跳转 App 的本质,就是模拟一次 “软件复位”。 我们手动帮 CPU 做了上电时做的那两件事:
加载 MSP (__set_MSP)。
加载 PC (Jump_To_Application())。
只要现场清理得干净,App 就会以为自己是刚上电启动的,完全不知道 Bootloader 的存在。
但是, 如果在升级过程中(擦写 Flash 时)突然断电了怎么办? App 区变成了写到一半的乱码。 再次上电,Bootloader 尝试跳转,App 直接死机,设备变砖。
我们需要一套**“防变砖机制”**,也就是双分区备份。




