简介:面向 STM32F407 嵌入式开发者的一份远程固件升级(IAP)方案资源,基于 DTU 透传实现应用运行中的在线编程,省去现场拆机与外部编程器,适合维护 Bootloader、需要批量升级或研究 Flash 分区管理的项目参考。资源包共 663 个文件,压缩后约 21.88MB,以 C 源码与头文件为核心,辅以 Keil 工程配置(.uvproj/.uvprojx)、编译产物(.axf/.hex/.map)及备份说明文件,可完整看到工程从编辑、编译到生成固件的全过程。目前已有 669 人学习下载。包内提供 Bootloader 与 App 分区跳转、Flash 擦写与写入、DTU 串口数据透传、固件接收与 CRC/MD5 校验等关键代码,工程结构和代码注释便于对照 IAP 流程逐段理解;同时附带多个 hex/bin 固件及编译中间文件,可用于验证升级结果、排查链接或烧写问题。对于正在搭建 STM32F407 远程升级机制或准备移植 IAP 方案的开发者而言,这份打包完整的资料能直接提供可参考的代码框架与清晰的目录结构,节省大量从零开始摸索的时间。
1. STM32F407 远程升级先过 IAP 这一关
把编译好的固件通过网络或串口发到设备里,让 STM32F407 自己把 Flash 擦了再写进去,这就是远程升级。而 F407 的远程升级绕不开 IAP(In Application Programming),因为它的 Flash 虽然有一兆字节,但出厂并没有自带一个能更新自身的引导程序。多数人第一个坑也在这里:直接对 0x08000000 地址所在的整个 Flash 做整片擦除,把引导区也抹了,板子当场变砖,只能接 SWD 救回来。
IAP 的思路是让固件分成两块甚至三块区域,Bootloader 管接收和写入,App 跑业务逻辑,升级时把新固件放进预留的 Flash 空间,校验通过后再跳过去执行。这篇文章按我自己的做法来写:先用 Linker 脚本把 F407 的 Flash 分区讲清楚,再给出 Bootloader 跳转和 App 中断向量重映射的可抄代码,最后把远程下载、CRC 校验和回滚这三个关键点逐一落地。适合已经在用 STM32 标准库或 HAL 库做产品、想把 OTA 能力加进去的嵌入式工程师。
2. 给 stm32f407 的 IAP 分区:Bootloader 与 App 怎么分 Flash
2.1 先看 F407 的 Flash 扇区表再动手
STM32F407 的 Flash 是 1MB 的型号时,扇区划分不是均等的。看参考手册 RM0090 的 Flash 章节,0x08000000 到 0x080FFFFF 一共 12 个扇区,前四个都是 16KB,第五个是 64KB,后面六个是 128KB。这个不对称的扇区结构直接影响分区方案,因为 IAP 升级时擦除的最小单位是扇区,不是字节。
| 扇区 | 地址范围 | 大小 | 常见用途 |
|---|---|---|---|
| Sector 0 | 0x08000000 - 0x08003FFF | 16KB | Bootloader |
| Sector 1 | 0x08004000 - 0x08007FFF | 16KB | 参数存储或 Bootloader 扩展 |
| Sector 2 | 0x08008000 - 0x0800BFFF | 16KB | App 起始区域 |
| Sector 3 | 0x0800C000 - 0x0800FFFF | 16KB | App |
| Sector 4 | 0x08010000 - 0x0801FFFF | 64KB | App / 升级暂存 |
| Sector 5-11 | 0x08020000 - 0x080FFFFF | 128KB x 7 | App 主体 |
我一般把 Bootloader 放在 Sector 0,也就是从 0x08000000 开始,占用 16KB 就够一个最简引导程序了。App 从 0x08010000(Sector 4 的起始地址)开始,正好对齐扇区边界,擦除和写入都不牵扯前面几个小扇区。App 编译出来的 bin 放在 0x08010000 到 0x080FFFFF 之间,大约 960KB 可用,对绝大多数业务固件绰绰有余。
2.2 分区方案里必须给回滚留位置
做远程升级如果只留一个 App 区,升级过程中断电或写入校验失败,设备就只能停留在半旧的固件上,甚至完全无法启动。给回滚留位置的意思是:双备份区。常见做法是把 Flash 分成 Boot + App + App_Backup,App 和 App_Backup 各占足够大的块。
以 F407 为例,可以把 App 放在 Sector 4 到 Sector 7,App_Backup 放在 Sector 8 到 Sector 11,各自 448KB 左右。升级流程变成:新固件先整体写入 App_Backup,全部写完并校验 CRC 通过后,把 App_Backup 的内容搬到 App 区,再跳转执行。如果搬移过程中掉电,Bootloader 检测到 App 区首地址不是合法的栈顶值,就不跳转,等待下一次升级指令,设备最多停留在旧版本,不会被彻底锁死。
2.3 Bootloader 跳转 App 的代码才算真正分完区
分区只是 Linker 脚本里改了地址,真正让 IAP 转起来的是跳转代码。Bootloader 里跳转到 App 的代码要处理两个关键点:关闭全局中断,以及重新设置主堆栈指针。
typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t app_msp = *(volatile uint32_t *)app_addr; // App 起始处放的是栈顶地址 pFunction app_reset = (pFunction)(*(volatile uint32_t *)(app_addr + 4)); // 第二位是 Reset_Handler if ((app_msp & 0x2FFE0000) == 0x20000000) { __disable_irq(); // 跳转前必须关中断 SCB->VTOR = app_addr; // 设置向量表偏移 __set_MSP(app_msp); // 切换主栈指针 app_reset(); // 跳转 } }这段代码的逻辑是:F407 上电后从 0x08000000 读取第一个 32 位数据作为 MSP,第二个 32 位数据作为 PC 的初始值。跳转前先检查 App 首地址是否像合法的 RAM 地址,防止 Flash 为空或数据损坏时跳到一个非法地址。SCB->VTOR写 App 地址,是为了让中断向量表跟随 App 走,否则任何中断事件都会去 Bootloader 的向量表里找处理函数,直接进 HardFault。
提示:
__disable_irq()只能关掉 NVIC 里的中断,SysTick 等通过异常机制触发的处理器事件仍然可能介入,跳转前最好把 SysTick 也停掉,并清空挂起的中断标志。
3. 改造 stm32f407 的 App 工程:中断向量重映射与 bin 生成
3.1 链接脚本的起始地址决定 App 烧到哪
App 工程拿到手时默认链接地址是 0x08000000,这样的固件烧进 Flash,只能从复位向量执行,配合 Bootloader 跳转根本跑不起来。改造的第一步是把链接脚本里的 Flash 起始地址改到和 Bootloader 约定好的分区位置。
以 GCC 的链接脚本为例,修改前后对比:
/* 修改前 */ FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K /* 修改后 */ FLASH (rx) : ORIGIN = 0x08010000, LENGTH = 960K把 ORIGIN 和 LENGTH 同时改掉,LENGTH 不能超过从 0x08010000 到 Flash 末尾的距离。如果链接脚本没改,App 里所有函数地址和全局变量的位置都按 0x08000000 起算,Bootloader 跳转过去后,PC 指向的指令根本不是 App 的 Reset_Handler,几乎必然跑飞。
3.2 启动文件里要处理向量表偏移
App 工程使用标准外设库或 HAL 库时,SystemInit 函数会做一次时钟初始化,但不会主动设置向量表。较新的 Cortex-M4 内核支持运行时修改 SCB->VTOR,但 F407 要求向量表地址必须是 0x400 字节对齐,也就是低 10 位必须为 0。0x08010000 的十六进制低 10 位是 0,所以不需要额外对齐处理。
在 App 的 main 函数最前面加上这一句:
int main(void) { SCB->VTOR = FLASH_BASE | 0x10000; // 0x08010000 HAL_Init(); SystemClock_Config(); // ... 业务初始化 }注意:如果把这句话放在 HAL_Init 之后,HAL_Init 内部可能已经开了 SysTick 等中断,向量表切换时如果有中断发生,仍会走旧的向量表查地址。经验是放在进入 main 后的第一行执行,而且在使用任何外设中断之前完成。
3.3 生成 bin 文件时记得指定起始地址
Keil MDK 用 AC6 编译器时,调试器加载 axf 文件没有影响,但远程升级要用的是纯二进制 bin 文件。生成 bin 的 User 选项卡命令是:
fromelf.exe --bin --output=app.bin Objects\app.axf如果用的是 GCC 工具链,对应的命令是:
arm-none-eabi-objcopy -O binary app.elf app.binfromelf 和 objcopy 生成的都是无地址信息的原始数据,bin 文件的内容从链接脚本里设置的 ORIGIN 开始。也就是说,app.bin 的第一个字节就是对 0x08010000 处数据的拷贝。Bootloader 接收这个 bin 文件时,直接把它搬运到 0x08010000 即可,不需要额外填一个偏移地址字段。
以 bin 格式传输的固件还包含链接阶段确定的全部向量表和常量数据,App 里所有中断向量、函数入口、字面量池都已经在编译时定好了绝对地址,运行时靠 PC 跳转访问,和 Flash 的物理位置一一对应。远程下发时如果不小心把 axf 或 hex 文件当作 bin 发过去,Bootloader 写入的是带地址信息的对象格式,跳到 0x08010000 执行时会发现那只是一段记录头,设备立刻死机。这一点在搭建上位机或服务器端时最容易踩,务必在固件文件命名或下载请求里明确标识 bin 后缀。
3.4 App 里用 CCM RAM 也要注意启动拷贝
F407 有一块 64KB 的 CCM RAM,地址在 0x10000000,只能由内核访问,DMA 访问不到。如果把某些高频变量放到 CCM RAM,链接脚本里要单独指定一个内存区域,并且启动文件里负责把 data 段拷贝到 RAM 的循环需要认识这块区域。简单做法是只放零初始化变量,不放带初值的变量,这样启动代码不用增加额外的拷贝段,CCM RAM 的 bss 清零逻辑在 GCC 下通常不用额外处理,但使用不同启动文件时要在移植后实际测试一遍。
4. 远程升级的协议处理:固件下发、CRC 校验与回滚策略
4.1 用 Ymodem 或自定义协议接收固件文件
远程升级到了真正接收固件这一步,方式不只一种。产品在局域网内,可以通过以太网 TCP 把固件拉下来;走现成的串口调试工具,最常见的方案是 Ymodem 协议。IAR 和 Keil 的串口助手插件普遍支持 Ymodem,直接把 bin 文件选中就传完了,不用自己设计帧结构。
如果固件已经放在 TF 卡或 U 盘里,升级流程就是读文件、校验、写入。这里的主流程逻辑是一致的:
int upgrade_from_buffer(uint8_t *fw_buf, uint32_t fw_len) { if (check_crc32(fw_buf, fw_len, expected_crc) != 0) { return -1; } if (erase_flash_sectors(APP_BACKUP_START, fw_len) != 0) { return -2; } for (uint32_t i = 0; i < fw_len; i += FLASH_WRITE_UNIT) { uint32_t write_len = MIN(FLASH_WRITE_UNIT, fw_len - i); if (flash_write_bytes(APP_BACKUP_START + i, fw_buf + i, write_len) != 0) { return -3; } } copy_backup_to_app(); NVIC_SystemReset(); return 0; }先校验再擦写,这个顺序很重要。如果先擦后校验,固件校验失败时原 App 已经被擦掉一大片,现场回天无术。Flash 写入单位用 8 字节或 32 字节都行,F407 的双字写入要求地址按 8 字节对齐,固件长度如果不是 8 的倍数,最后一段要单独处理。
4.2 CRC32 校验要连版本号一起算
CRC32 是嵌入式固件完整性的常见校验方式,STM32F407 的硬件 CRC 外设能加速计算,但要注意它算出的多项式是 0x04C11DB7,初始值 0xFFFFFFFF,结果不做异或反转,和 Zlib 的 CRC32 并不一致。大多数上位机工具用的是 Zlib 的 CRC32,两边对不上时校验必然失败。
uint32_t crc32_zlib(uint32_t crc, const uint8_t *data, uint32_t len) { crc = ~crc; while (len--) { crc ^= *data++; for (int i = 0; i < 8; i++) { crc = (crc >> 1) ^ (0xEDB88320 & (uint32_t)(-(crc & 1))); } } return ~crc; }参数说明:crc 参数初始传入 0xFFFFFFFF,len 是字节数,返回的是标准 Zlib CRC32 值。写入 Flash 使用的固件文件,应该在文件末尾附加 4 字节 CRC 值,上位机生成 bin 时统计整个文件字节算出 CRC,追加在末尾,Bootloader 收完文件后先取最后 4 字节作为期望值,再对前面的数据重新计算 CRC 比对。
提示:版本号也应该参与校验。两个设备固件版本不一致时,Bootloader 直接拒绝写入,避免高版本固件被低版本覆盖导致功能回退。做法是在 bin 文件头部预留版本号字段,或在升级指令里单独携带版本字段。
4.3 回滚策略:搬到 App 区之前再验一次
第 2 章分区时预留了 App_Backup,现在说它怎么用。新固件写入 App_Backup 后,不要急着搬,先在 Bootloader 里对 App_Backup 再做一次完整 CRC 校验。为什么做两遍:接收过程中写入 API 可能会意外出错,第一遍校验只证明数据完整,第二遍校验才证明 Flash 里的内容和内存里的一致。
static int verify_flash_region(uint32_t addr, uint32_t len, uint32_t expect_crc) { uint32_t crc = 0xFFFFFFFF; for (uint32_t i = 0; i < len; i += 4) { uint32_t word = *(volatile uint32_t *)(addr + i); crc = crc32_step(crc, (uint8_t *)&word, 4); } return (crc == expect_crc) ? 0 : -1; }搬移函数仍用前面的flash_write_bytes,从 App_Backup 读出来写到 App 区。如果搬移过程掉电,Flash 里可能留了一块不完整的 App,这就要靠下一章讲的启动校验来兜底。
4.4 硬件 IIC 和其他外设冲突时怎么处理
升级过程中 Bootloader 如果遇到硬件 IIC 卡死或某个外设初始化时间过长,直接在超时函数里重启会陷入升级失败死循环。处理方法是把 Bootloader 全程的看门狗喂狗时间调到 1 秒以上,给整片 Flash 擦写预留充足时间。
F407 的硬件 IIC 是出了名的难伺候,如果产品主系统里用了硬件 IIC 读外部 EEPROM 或传感器,在 Bootloader 阶段尽快把用不到的外设 Deinit 掉,避免意外中断打断 Flash 擦写。擦写 Flash 期间任何中断都可能导致操作时序被拉长,最坏情况是擦写正在进行时 CPU 进入低功耗模式,Flash 控制器报错。
5. 用调试器验证 IAP 升级链路:断点、看门狗与现场恢复技巧
5.1 第一次联调先别急着跑远程
拿到一块崭新的 F407 板子,我会先做一次有线 IAP 的最小验证:Bootloader 烧到 0x08000000,App 烧到 0x08010000,然后断电重启。这一步就两个目的,确认链接脚本地址没改错、确认跳转能成功。用 SWD 连接时把断点打在jump_to_app()的app_reset()那一行,单步走进去,能看到 PC 跳到 0x0801xxxx 附近,再继续跑 App 里的串口打印。
5.2 看门狗与升级超时的配合逻辑
远程升级最怕设备在固件写入过程中被看门狗复位。独立看门狗 IWDG 一旦开启就无法关闭,超时时间通常设成几百毫秒到几秒,整片 Flash 擦除需要几十毫秒,写一个扇区最多也就几百毫秒,看起来够用,但 Ymodem 或 TCP 传大固件时,每包间隔的等待时间远比单个扇区擦写长。正确做法是把 IWDG 喂狗放在升级主循环里,每收到一包数据就喂一次,而不是放在 Flash 写入函数里。
while (upgrade_state != UPGRADE_DONE) { int ret = await_packet_with_timeout(1000); if (ret == TIMEOUT) { watchdog_feed(); continue; } if (ret == PACKET_OK) { write_packet_to_backup(); watchdog_feed(); } }5.3 启动校验失败时的恢复技巧
Bootloader 在每次上电后、跳转前都会检查 App 区的合法性。检查标准一般是栈顶地址落在 RAM 范围内,以及首条 Reset_Handler 地址落在 Flash 的 App 区内。
static int check_app_valid(uint32_t addr) { uint32_t msp = *(volatile uint32_t *)addr; uint32_t reset_vec = *(volatile uint32_t *)(addr + 4); if ((msp & 0x2FFE0000) != 0x20000000) return 0; if ((reset_vec & 0x2FFE0000) != 0x08010000) return 0; return 1; }校验失败时 Bootloader 不跳转,原地等待串口或网络重新发送固件。这个兜底逻辑让设备在升级断电后仍然能够进入恢复模式。实际项目中我还在 App 里加了一个升级标志位:App 启动后跑三秒,如果业务初始化失败,就把 Flash 里某个专门存储系统状态的扇区写成“请求回滚”,然后软复位。Bootloader 看到这个标志,就把 App_Backup 重新搬到 App 区。这比单纯依赖外部指令触发回滚更稳,因为设备往往在无人值守的现场,等不到服务器来救。
5.4 给升级失败留一条串口逃生门
无论回滚做得再完善,也有连 Bootloader 都进不去的极端情况,比如 Bootloader 本身被误擦。此时只能靠 SWD 接口重新烧录。量产产品如果外壳密封,把 BOOT0 引脚通过电阻拉到 3.3V,配合串口 ISP 模式就能救回来,这是 STM32 的系统存储器自带功能,不需要外部编程器。
验证手段上,我会在 Bootloader 和 App 里各放一组不同的串口打印,确认每次重启后进入的到底是哪个固件。升级完成后用bin文件的 SHA256 与服务器端记录比对,确定写入的字节和服务器下发的版本完全一致,才算一次远程升级真正闭环。
本文还有配套的精品资源,点击获取