摘要
上一篇文章主要讲了 STM32 BootLoader 的基础原理:为什么 BootLoader 要放在0x08000000,Flash 如何划分,以及 BootLoader 如何跳转到 APP。
这一篇继续往实战推进,重点解决三个问题:
- APP 工程从
0x08010000运行时,工程配置到底要改哪里 - BootLoader 如何判断 APP 是否有效,避免跳转到空地址导致死机
- 固件升级时,升级标志、固件包头、CRC 校验应该怎么设计才不容易踩坑
本文适合已经知道 BootLoader 基本跳转原理,准备真正做 STM32 IAP、串口升级、网口升级、OTA 升级的开发者。
目录
- 1. 本文要解决的问题
- 2. 推荐的 BootLoader 与 APP 工程结构
- 3. APP 起始地址如何确定
- 4. APP 工程链接地址配置
- 5. APP 中断向量表重定位
- 6. BootLoader 跳转 APP 前要做哪些清理
- 7. 如何判断 APP 是否有效
- 8. 固件包头不要直接写到 APP 起始地址
- 9. 固件 CRC 校验流程
- 10. 升级标志位如何设计
- 11. 一个完整的升级流程
- 12. 常见问题
- 13. 总结
1. 本文要解决的问题
加入 BootLoader 之后,STM32 的程序结构不再是一个单独的 APP 工程,而是至少两个工程:
BootLoader 工程 APP 工程BootLoader 固定从0x08000000启动,APP 放到后面的某个地址,例如:
BootLoader: 0x08000000 APP: 0x08010000很多 BootLoader 问题不是跳转代码写错,而是 APP 工程配置不完整:
- APP 仍然按
0x08000000链接 - APP 没有设置
SCB->VTOR - BootLoader 跳转前没有关闭中断
- APP 起始地址被固件包头覆盖
- 固件没有校验,写坏一半也照样跳转
- 升级标志位和 BootLoader 代码放在同一个擦除页里
这些问题在调试时非常折磨,所以本文把它们单独拆出来讲清楚。
2. 推荐的 BootLoader 与 APP 工程结构
建议把 BootLoader 和 APP 当成两个独立工程维护:
Project |-- BootLoader | |-- Core | |-- Drivers | |-- boot_jump.c | |-- boot_flash.c | |-- boot_protocol.c | |-- App | |-- Core | |-- Drivers | |-- app_main.c | |-- Tools | |-- pack_firmware.exe | |-- upgrade_tool.exe这样做的好处是职责清楚:
- BootLoader 只负责升级、校验、跳转
- APP 只负责业务逻辑
- 上位机工具负责给 APP 固件增加包头、版本号、CRC 等信息
不要把 BootLoader 和 APP 强行塞进一个工程里靠宏切换,短期看起来省事,后期非常容易混乱。
3. APP 起始地址如何确定
APP 起始地址由 BootLoader 占用空间决定。
例如 BootLoader 预留 64KB:
0x08000000 - 0x0800FFFF BootLoader 区域 0x08010000 - ... APP 区域那么 APP 起始地址就是:
#defineAPP_BASE_ADDR0x08010000U选择 APP 起始地址时,要同时考虑:
- BootLoader 当前实际大小
- 后续升级协议、通讯栈、加密校验可能继续增大
- Flash 擦除单位
- 是否需要单独预留参数页
对于 STM32,不同系列的 Flash 擦除单位不一样:
- STM32F103:按 Page 擦除,常见 Page 大小为 1KB
- STM32F407:按 Sector 擦除,前几个扇区大小是 16KB、64KB、128KB 不等
- STM32G473:按 Page 擦除,并且还要注意 Bank/Page 的组织方式
所以 APP 地址不能只看“字节够不够”,还要对齐到对应芯片的擦除边界。
一个比较常见的划分方式如下:
0x08000000 - 0x0800DFFF BootLoader 代码 0x0800E000 - 0x0800FFFF Boot 参数区 0x08010000 - ... APP 区域注意:上面只是示例,具体地址必须根据芯片容量和擦除单位调整。
4. APP 工程链接地址配置
APP 既然不再从0x08000000运行,就必须修改 APP 工程的链接地址。
否则编译器仍然会认为 APP 从0x08000000开始,生成的中断向量表、函数地址都会以错误地址为基准,BootLoader 跳转后很容易 HardFault。
下面以APP_BASE_ADDR = 0x08010000为例。
4.1 Keil MDK 配置
如果使用 Keil MDK,可以进入:
Options for Target -> Target -> IROM1修改:
Start: 0x08010000 Size: 根据芯片 Flash 剩余空间填写例如 STM32F103C8T6 标称 64KB Flash,如果 BootLoader 已经占用 20KB,那 APP 剩余空间大约是:
0x08005000 - 0x0800FFFF如果用 64KB BootLoader 分区,那么 F103C8T6 标称容量下就没有 APP 空间了。因此 F103C8T6 做 BootLoader 时,BootLoader 通常要控制在 16KB、20KB、24KB 这类更小范围内。
如果使用 scatter file,可以类似这样配置:
LR_IROM1 0x08010000 0x00070000 { ER_IROM1 0x08010000 0x00070000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00020000 { .ANY (+RW +ZI) } }其中:
0x08010000是 APP 起始地址0x00070000是 APP 可用 Flash 大小,需要按实际芯片修改- RAM 起始地址和大小也需要按具体芯片修改
4.2 STM32CubeIDE / GCC 配置
如果使用 STM32CubeIDE 或 GCC,需要修改 linker script,例如.ld文件:
MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K FLASH (rx) : ORIGIN = 0x08010000, LENGTH = 448K }重点是这两项:
ORIGIN = 0x08010000 LENGTH = 448KORIGIN是 APP 起始地址,LENGTH是 APP 可用 Flash 大小。
如果芯片是 STM32F407VET6,内部 Flash 为 512KB,BootLoader 预留 64KB,则 APP 可用空间大约是:
512KB - 64KB = 448KB所以可以写成:
FLASH (rx) : ORIGIN = 0x08010000, LENGTH = 448K4.3 IAR 配置
如果使用 IAR,需要修改.icf文件里的 ROM 区域:
define symbol __ICFEDIT_region_ROM_start__ = 0x08010000; define symbol __ICFEDIT_region_ROM_end__ = 0x0807FFFF;本质仍然一样:让 APP 按新的 Flash 地址链接。
5. APP 中断向量表重定位
APP 链接地址改完之后,还需要设置中断向量表地址。
STM32 Cortex-M 内核通过SCB->VTOR指定当前中断向量表位置。加入 BootLoader 后,APP 的向量表不在0x08000000,而是在 APP 起始地址。
例如:
#defineAPP_BASE_ADDR0x08010000UAPP 启动后,应尽早执行:
SCB->VTOR=APP_BASE_ADDR;__DSB();__ISB();建议放在main()一开始,或者放在SystemInit()中,且必须在开启中断之前完成。
示例:
intmain(void){SCB->VTOR=0x08010000U;__DSB();__ISB();HAL_Init();SystemClock_Config();while(1){/* APP 业务逻辑 */}}有些 STM32Cube 工程会在system_stm32xxx.c中提供类似配置:
#defineVECT_TAB_OFFSET0x00010000U如果使用这种方式,要确认最终SCB->VTOR的值确实是:
0x08010000常见错误是只改了链接地址,但没有改SCB->VTOR。这种情况下,主循环可能能跑,但一进中断就飞。
6. BootLoader 跳转 APP 前要做哪些清理
BootLoader 跳转 APP 不是简单调用一个函数。
在跳转前,BootLoader 最好清理自己的运行环境:
- 关闭全局中断
- 停止 SysTick
- 关闭或反初始化外设
- 清除 NVIC 使能和挂起标志
- 设置 APP 的中断向量表地址
- 设置 MSP 为 APP 的栈顶
- 跳转到 APP 的 Reset_Handler
示例代码:
#include"main.h"/* 按实际芯片工程包含对应 HAL/CMSIS 头文件 */#defineAPP_BASE_ADDR0x08010000Utypedefvoid(*AppEntryFunc)(void);staticuint8_tBoot_IsAppValid(uint32_tapp_addr);voidBoot_JumpToApp(void){uint32_tapp_msp;uint32_tapp_reset;AppEntryFunc app_entry;if(!Boot_IsAppValid(APP_BASE_ADDR)){return;}app_msp=*(__IOuint32_t*)APP_BASE_ADDR;app_reset=*(__IOuint32_t*)(APP_BASE_ADDR+4U);app_entry=(AppEntryFunc)app_reset;__disable_irq();SysTick->CTRL=0;SysTick->LOAD=0;SysTick->VAL=0;HAL_RCC_DeInit();HAL_DeInit();for(uint32_ti=0;i<8;i++){NVIC->ICER[i]=0xFFFFFFFFU;NVIC->ICPR[i]=0xFFFFFFFFU;}SCB->VTOR=APP_BASE_ADDR;__DSB();__ISB();__set_MSP(app_msp);app_entry();}说明:
APP_BASE_ADDR要和 APP 工程链接地址一致NVIC->ICER和NVIC->ICPR的循环次数可根据芯片中断数量调整- 如果 BootLoader 使用了 USB、ETH、CAN、串口 DMA 等外设,跳转前要特别注意外设和 DMA 的关闭
- 如果 APP 自己会重新初始化所有外设,BootLoader 尽量不要留下半初始化状态
7. 如何判断 APP 是否有效
BootLoader 不能看到 APP 地址就直接跳。
如果 APP 区域还是空的,Flash 内容通常是:
0xFFFFFFFF这时如果直接跳转,基本会进入 HardFault。
最基础的 APP 有效性检查可以判断两个值:
APP_BASE_ADDR + 0:APP 栈顶 MSP APP_BASE_ADDR + 4:APP Reset_HandlerMSP 应该落在 SRAM 范围内,Reset_Handler 应该落在 Flash 范围内。
示例:
#defineFLASH_BASE_ADDR0x08000000U#defineFLASH_SIZE(512U*1024U)#defineSRAM_BASE_ADDR0x20000000U#defineSRAM_SIZE(128U*1024U)#defineAPP_BASE_ADDR0x08010000Ustaticuint8_tBoot_IsAddressInRange(uint32_taddr,uint32_tstart,uint32_tsize){return(addr>=start)&&(addr<(start+size));}staticuint8_tBoot_IsAppValid(uint32_tapp_addr){uint32_tapp_msp;uint32_tapp_reset;uint32_tapp_reset_addr;app_msp=*(__IOuint32_t*)app_addr;app_reset=*(__IOuint32_t*)(app_addr+4U);if((app_msp==0xFFFFFFFFU)||(app_reset==0xFFFFFFFFU)){return0;}if((app_msp&0x3U)!=0U){return0;}if(!Boot_IsAddressInRange(app_msp,SRAM_BASE_ADDR,SRAM_SIZE)){return0;}if((app_reset&0x1U)==0U){return0;}app_reset_addr=app_reset&0xFFFFFFFEU;if(!Boot_IsAddressInRange(app_reset_addr,FLASH_BASE_ADDR,FLASH_SIZE)){return0;}return1;}注意:
FLASH_SIZE、SRAM_SIZE要按具体芯片修改- Cortex-M 的函数入口地址最低位通常为 1,表示 Thumb 状态
- 这个检查只能判断 APP 向量表“像不像一个程序”,不能证明固件完整
如果要更可靠,还需要增加固件长度、版本号、CRC32 等校验。
8. 固件包头不要直接写到 APP 起始地址
这是 BootLoader 开发里非常常见的坑。
有些升级协议会给固件增加包头:
magic version size crc32 timestamp payload然后 BootLoader 把整个文件直接写到APP_BASE_ADDR。
这样做是错误的,除非你专门修改了 APP 的链接布局。
原因是 APP 起始地址的前 8 个字节必须是:
APP_BASE_ADDR + 0:MSP APP_BASE_ADDR + 4:Reset_Handler如果把固件包头写到 APP 起始地址,Flash 会变成:
APP_BASE_ADDR + 0:magic APP_BASE_ADDR + 4:version APP_BASE_ADDR + 8:size APP_BASE_ADDR + 12:crc32 ...BootLoader 再去读取 MSP 和 Reset_Handler,读到的就是包头字段,跳转一定会出问题。
推荐做法有三种。
8.1 固件包头只用于传输,不写入 APP 区域
上位机发送:
[FirmwareHeader][APP bin payload]BootLoader 先接收FirmwareHeader,解析出:
目标地址 固件长度 版本号 CRC32然后只把APP bin payload写入:
APP_BASE_ADDR也就是说,包头参与通讯和校验,但不写入 APP 起始地址。
这是最简单、最推荐的方式。
8.2 固件元信息放到单独参数页
可以单独预留一个 Flash 参数页:
Boot 参数区: magic app_addr app_size app_crc32 app_version upgrade_flag valid_flagAPP 本体仍然从APP_BASE_ADDR开始,参数页只保存描述信息。
这种方式适合量产项目,因为 BootLoader 可以通过参数页知道当前 APP 是否有效、版本是多少、是否需要升级。
8.3 包头放在 APP 内部固定偏移位置
也可以把固件信息放在 APP 内部,例如放在向量表后面的固定位置。
但是这种方式需要配合链接脚本,确保该区域不会被普通代码覆盖。对初学者来说不如前两种直观。
9. 固件 CRC 校验流程
固件写入 Flash 后,BootLoader 应该做 CRC 校验。
最基本的流程:
1. 上位机计算 APP bin 的 CRC32 2. 上位机把 size 和 crc32 发给 BootLoader 3. BootLoader 擦除 APP 区域 4. BootLoader 分包写入 APP bin 5. BootLoader 重新读取 Flash 中的 APP 内容 6. BootLoader 计算 Flash 中 APP 的 CRC32 7. 对比上位机给的 crc32 8. 校验通过后标记 APP 有效不要只校验串口接收过程中每一包是否正确,还要校验最终写入 Flash 的完整 APP。
示例固件包头:
#defineFW_MAGIC0x46574D47U/* FWMG */typedefstruct{uint32_tmagic;uint32_ttarget_addr;uint32_timage_size;uint32_timage_crc32;uint32_tversion;uint32_theader_crc32;}FirmwareHeader_t;BootLoader 收到包头后先检查:
staticuint8_tBoot_CheckFirmwareHeader(constFirmwareHeader_t*header){if(header->magic!=FW_MAGIC){return0;}if(header->target_addr!=APP_BASE_ADDR){return0;}if(header->image_size==0U){return0;}if(header->image_size>APP_MAX_SIZE){return0;}return1;}写入完成后再计算 Flash 中的 APP CRC:
uint32_tflash_crc;flash_crc=Boot_CalcCrc32((uint8_t*)APP_BASE_ADDR,header.image_size);if(flash_crc==header.image_crc32){Boot_SetAppValidFlag(&header);}else{Boot_ClearAppValidFlag();}Boot_CalcCrc32()可以使用软件 CRC,也可以使用 STM32 的硬件 CRC 外设。关键是上位机和下位机必须使用同一种 CRC 参数,例如:
- 多项式
- 初始值
- 输入是否反转
- 输出是否反转
- 最终异或值
否则两边算法都没错,但结果就是对不上。
10. 升级标志位如何设计
BootLoader 通常需要判断是否进入升级模式。
常见触发方式:
- 上电时按住某个按键
- APP 收到升级命令后写入升级标志,然后软件复位
- 上位机在 BootLoader 等待窗口内发送升级命令
- APP 运行异常多次后,BootLoader 强制停留升级模式
对于实际项目,推荐设计一个 Boot 参数区。
示例:
#defineBOOT_PARAM_MAGIC0x424F4F54U/* BOOT */#defineBOOT_CMD_NONE0x00000000U#defineBOOT_CMD_UPGRADE0xA55A0001U#defineBOOT_APP_VALID0x5AA55AA5Utypedefstruct{uint32_tmagic;uint32_tcommand;uint32_tapp_addr;uint32_tapp_size;uint32_tapp_crc32;uint32_tapp_version;uint32_tapp_valid;uint32_tparam_crc32;}BootParam_t;Boot 参数区可以放在单独 Flash Page 或 Sector 中。
例如 F103 按 Page 擦除,可以预留最后一个 BootLoader Page:
0x08000000 - 0x0800EFFF BootLoader 0x0800F000 - 0x0800FFFF Boot 参数区 0x08010000 - ... APP对于 STM32F407 这类按 Sector 擦除的芯片,要特别注意:不能把参数区和 BootLoader 代码混在同一个 Sector 中。因为擦除参数区时会擦掉整个 Sector,如果这个 Sector 里还有 BootLoader 代码,设备可能直接变砖。
升级标志判断示例:
staticuint8_tBoot_ShouldEnterUpgrade(void){BootParam_t param;Boot_ReadParam(¶m);if(param.magic!=BOOT_PARAM_MAGIC){return0;}if(!Boot_CheckParamCrc(¶m)){return0;}if(param.command==BOOT_CMD_UPGRADE){return1;}return0;}APP 想主动进入 BootLoader,可以这样做:
voidApp_RequestUpgrade(void){BootParam_t param;Boot_ReadParam(¶m);param.magic=BOOT_PARAM_MAGIC;param.command=BOOT_CMD_UPGRADE;param.param_crc32=Boot_CalcParamCrc(¶m);Boot_WriteParam(¶m);NVIC_SystemReset();}注意:APP 和 BootLoader 如果都要读写 Boot 参数区,结构体定义必须保持一致。最好把boot_param.h做成公共头文件。
11. 一个完整的升级流程
一个单 APP 区的 BootLoader 升级流程可以这样设计:
STM32 上电 | v BootLoader 启动 | v 读取 Boot 参数区 | +-- 有升级标志 -> 进入升级模式 | +-- 无升级标志 -> 检查 APP 是否有效 | +-- 有效 -> 跳转 APP | +-- 无效 -> 停留升级模式升级模式内部流程:
等待上位机连接 | v 接收固件包头 | v 检查 magic、目标地址、固件长度 | v 擦除 APP 区域 | v 分包接收 APP bin 并写入 Flash | v 计算 Flash 中 APP 的 CRC32 | +-- 校验通过 -> 写入 APP 有效标志,清除升级命令,复位 | +-- 校验失败 -> 清除 APP 有效标志,停留升级模式这种设计的优点是即使升级过程中断电,BootLoader 仍然存在。下次上电后,由于 APP 无效,设备会继续停留在升级模式。
缺点是只有一个 APP 区,升级时会擦掉旧 APP。对于特别强调升级不中断和断电回滚的项目,可以进一步设计 A/B 双 APP 分区。
12. 常见问题
12.1 APP 明明下载进去了,为什么跳转就 HardFault
优先检查:
- APP 链接地址是否改成了
APP_BASE_ADDR - APP 是否设置了
SCB->VTOR - BootLoader 跳转前是否正确设置 MSP
- APP 起始地址前 8 字节是否真的是 MSP 和 Reset_Handler
- 固件包头是否被错误写入 APP 起始地址
可以在调试器里直接查看:
APP_BASE_ADDR + 0 APP_BASE_ADDR + 4如果这两个值不像 SRAM 地址和 Flash 函数地址,说明 APP 镜像或写入位置有问题。
12.2 为什么 APP 主循环能跑,但串口中断、定时器中断不进
大概率是中断向量表没有重定位。
检查 APP 中是否执行:
SCB->VTOR=APP_BASE_ADDR;并且这句代码要在开启中断之前执行。
12.3 F407 上参数区为什么不能随便放
STM32F407 的 Flash 是按 Sector 擦除,不是按 1KB 或 2KB 小 Page 擦除。
如果参数区和 BootLoader 代码在同一个 Sector 中,擦除参数区时会把该 Sector 的 BootLoader 代码也擦掉。
所以 F407 上要按 Sector 完整预留参数区,例如单独预留 Sector 3 或其他合适 Sector。
12.4 F103C8T6 做 BootLoader 空间不够怎么办
F103C8T6 标称 Flash 是 64KB,做 BootLoader 时空间比较紧张。
建议:
- BootLoader 功能尽量简单
- 优先使用串口升级,不要塞太重的协议栈
- 减少 HAL 中不必要模块
- BootLoader 分区控制在 16KB 到 24KB 左右
- APP 起始地址不要一上来就设到
0x08010000
对于量产项目,不建议依赖某些 F103C8T6 实际可用 128KB 的情况。应以芯片标称容量和官方规格为准。
12.5 固件 bin、hex、elf 应该传哪个
BootLoader 最常用的是.bin。
原因是.bin就是纯二进制镜像,适合直接写入 Flash 的 APP 区域。
.hex里面带地址信息,也可以解析,但 BootLoader 端实现复杂一些。
.elf或.axf是调试文件,包含符号表和调试信息,不适合直接作为 BootLoader 升级固件。
12.6 BootLoader 和 APP 都需要 Flash 驱动吗
BootLoader 一定需要 Flash 擦写驱动。
APP 是否需要取决于功能:
- 如果 APP 只运行,不主动请求升级,可以不写 Flash
- 如果 APP 需要写升级标志,就需要能写 Boot 参数区
- 如果 APP 要保存业务参数,也需要独立的数据 Flash 区
建议把 Boot 参数区和业务参数区分开,不要混用。
13. 总结
STM32 BootLoader 能不能稳定运行,关键不只是跳转代码,而是整个链路都要一致:
- BootLoader 的 APP 地址配置
- APP 工程的链接地址
- APP 的中断向量表重定位
- 固件包头与 APP 镜像的关系
- Flash 擦除单位和参数区规划
- APP 有效性检查和 CRC 校验
- 升级标志位的读写规则
最重要的一句话:
APP_BASE_ADDR 处必须永远保持 APP 的向量表,不能被固件包头、参数结构体或其他数据覆盖。只要把这条规则守住,再配合链接地址、SCB->VTOR、MSP 设置和 CRC 校验,STM32 BootLoader 的稳定性就会提升很多。
参考标签
STM32 BootLoader IAP 单片机 嵌入式 固件升级