news 2026/6/9 11:41:23

STM32 BootLoader 实战(二):APP 偏移配置、固件校验与升级标志设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 BootLoader 实战(二):APP 偏移配置、固件校验与升级标志设计

摘要

上一篇文章主要讲了 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 = 448K

ORIGIN是 APP 起始地址,LENGTH是 APP 可用 Flash 大小。

如果芯片是 STM32F407VET6,内部 Flash 为 512KB,BootLoader 预留 64KB,则 APP 可用空间大约是:

512KB - 64KB = 448KB

所以可以写成:

FLASH (rx) : ORIGIN = 0x08010000, LENGTH = 448K

4.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_ADDR0x08010000U

APP 启动后,应尽早执行:

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->ICERNVIC->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_Handler

MSP 应该落在 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_SIZESRAM_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_flag

APP 本体仍然从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(&param);if(param.magic!=BOOT_PARAM_MAGIC){return0;}if(!Boot_CheckParamCrc(&param)){return0;}if(param.command==BOOT_CMD_UPGRADE){return1;}return0;}

APP 想主动进入 BootLoader,可以这样做:

voidApp_RequestUpgrade(void){BootParam_t param;Boot_ReadParam(&param);param.magic=BOOT_PARAM_MAGIC;param.command=BOOT_CMD_UPGRADE;param.param_crc32=Boot_CalcParamCrc(&param);Boot_WriteParam(&param);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 单片机 嵌入式 固件升级
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/9 11:39:23

计算器 VB.NET源码

使用的开发环境&#xff1a;微软Visual Studio Enterprise 2022 编程语言&#xff1a;VB.NET 框架&#xff1a;winform 生成的程序可运行的操作系统&#xff1a;生成的程序可运行于windows7及以上操作系统。 计算器简介&#xff1a;   1、支持以下运算&#xff1a;加&…

作者头像 李华
网站建设 2026/6/9 11:37:35

小程序毕设选题推荐:nodejs基于微信小程序的设备报修系统【附源码、mysql、文档、调试+代码讲解+全bao等】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/6/9 11:37:00

【课程设计/毕业设计】基于Springboot+微信小程序的健康管理微信小程序的设计与实现【附源码、数据库、万字文档】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华