资讯动态

STM32F051 IAP实战:Flash分区、向量表搬移与固件升级全解析

发布时间:2026/10/14 1:04:50 来源:尧图企业网站定制
1. 为什么要在 STM32F051 上折腾 IAP第一次接触 IAP 这个概念是在做一个需要现场升级的采集终端项目。设备装在现场机柜里每次改点逻辑都要派人带着仿真器去拆机烧录来回一趟成本高得离谱。后来咬牙把 IAP 做进去才算把这个坑填上。STM32F051 这颗芯片在入门级 Cortex-M0 里算是性价比很高的一颗Flash 从 16KB 到 64KB 不等SRAM 只有 8KB资源紧巴巴的但恰恰是这种资源受限的芯片做 IAP 才最能锻炼人对内存布局和启动流程的理解。IAP全称 In-Application Programming直译就是“在应用内编程”。说人话就是芯片里跑着一段程序这段程序能自己去擦写另一块 Flash 区域把新的固件写进去然后跳过去执行。它和 ICP用仿真器烧最大的区别在于整个升级过程不需要拆机、不需要专用工具只要设备还能通信串口、CAN、无线模块都行就能把新固件灌进去。STM32F051 做 IAP 有几个绕不开的现实约束必须先摆清楚。第一它没有像 F4/F7 那样的双 Bank Flash整个 Flash 是单 Bank 的意味着你没法“在 A 区跑着的同时写 B 区”只能把 Flash 人为切成 Bootloader 区和 App 区两块。第二Cortex-M0 的中断向量表重映射机制和 M3/M4 不一样没有 VTOR 寄存器跳转 App 前必须做向量表搬移。第三8KB 的 SRAM 决定了你的 Bootloader 不能太臃肿缓冲区开大了 App 就没地方放了。这篇文章适合谁看如果你手上正好有 STM32F051 的板子想搞明白 IAP 到底怎么落地或者你之前做过 F1/F4 的 IAP想看看 M0 上有什么不一样的地方那这篇内容应该能帮到你。我会把 Flash 分区、向量表搬移、跳转流程、固件传输协议、校验机制这些环节全部拆开讲代码给到能直接抄的程度同时把我在实际调试中踩过的坑一并交代清楚。2. Flash 分区与内存布局设计2.1 分区方案怎么定才合理做 IAP 的第一步不是写代码而是拿张纸把 Flash 的地址空间画出来。STM32F051 的 Flash 起始地址是 0x08000000假设我们用的是 64KB 版本那地址范围就是 0x08000000 到 0x0800FFFF。你要做的第一件事是决定 Bootloader 占多大、App 占多大。Bootloader 的职责很单一上电后判断是否需要升级需要就接收固件写进 App 区不需要就直接跳转 App。它不需要复杂的业务逻辑所以体积可以压得很小。我实测下来一个带串口接收、Flash 擦写、CRC 校验的 Bootloader用 Keil 开 -O2 优化大概在 6KB 到 8KB 之间。为了留点余量我一般把 Bootloader 区划成 12KB也就是 0x08000000 到 0x08002FFF。App 区就从 0x08003000 开始一直到 Flash 末尾。64KB 的芯片App 可用空间就是 64KB - 12KB 52KB。这个空间对于大多数应用足够了但如果你的 App 本身就接近 50KB那就得把 Bootloader 再压一压比如砍掉一些不必要的外设初始化或者把校验算法换成更精简的版本。这里有个容易被忽略的点STM32F051 的 Flash 页大小是 1KB有些型号是 2KB具体看参考手册的 Flash 章节。擦除操作必须以页为单位所以你的分区边界最好对齐到页边界。12KB 正好是 12 页对齐得很干净。如果你把 Bootloader 划成 10KB那 App 起始地址就是 0x08002800虽然也能用但擦除 App 第一页的时候要小心别把 Bootloader 的最后一页带进去。区域起始地址结束地址大小用途Bootloader0x080000000x08002FFF12KB升级逻辑、跳转App0x080030000x0800FFFF52KB用户应用参数区0x0800F8000x0800FFFF2KB升级标志、版本号参数区是我强烈建议单独划出来的一块。你可以把它放在 Flash 最末尾专门存升级标志位、固件版本、CRC 值这些元数据。这样 Bootloader 判断是否需要升级时直接读参数区就行不用去扫描整个 App 区。参数区的擦写频率比 App 区高单独管理也方便做磨损均衡虽然 Flash 擦写寿命有 1 万次一般项目根本用不完。2.2 中断向量表的搬移问题这是 M0 做 IAP 最容易翻车的地方。Cortex-M3/M4 有个 VTOR 寄存器你只要把 App 的向量表地址写进去中断就能正确响应。但 Cortex-M0 没有 VTOR向量表固定映射在 0x00000000 地址通过 Flash 别名映射到 0x08000000。那 App 的中断怎么办答案是在跳转前把 App 的向量表复制到 SRAM 起始处然后把 SRAM 重映射到 0x00000000。STM32F051 支持通过 SYSCFG_CFGR1 寄存器的 MEM_MODE 位把 SRAM 映射到 0x00000000。具体操作是先把 App 向量表的前 48 个字M0 有 16 个系统异常 32 个外部中断共 48 个向量拷贝到 SRAM 的 0x20000000 开始的位置然后设置 MEM_MODE 为 0x03SRAM 映射。这里有个细节SRAM 的前 192 字节48 个字 × 4 字节会被向量表占用你的 App 链接脚本里必须把 RAM 起始地址往后挪 192 字节否则 App 的全局变量会覆盖向量表一进中断就 HardFault。我当初就是没注意这个调了整整一个下午最后用调试器看 SRAM 内容才发现向量表被变量冲掉了。// 向量表搬移与重映射 #define APP_VECT_TAB_ADDR 0x08003000 #define SRAM_VECT_TAB_ADDR 0x20000000 #define VECT_TAB_SIZE 48 void IAP_RelocateVectorTable(void) { uint32_t i; uint32_t *pSrc (uint32_t *)APP_VECT_TAB_ADDR; uint32_t *pDst (uint32_t *)SRAM_VECT_TAB_ADDR; for (i 0; i VECT_TAB_SIZE; i) { pDst[i] pSrc[i]; } // 使能 SYSCFG 时钟 RCC-APB2ENR | RCC_APB2ENR_SYSCFGEN; // 将 SRAM 映射到 0x00000000 SYSCFG-CFGR1 ~SYSCFG_CFGR1_MEM_MODE; SYSCFG-CFGR1 | SYSCFG_CFGR1_MEM_MODE_0 | SYSCFG_CFGR1_MEM_MODE_1; }注意 SYSCFG_CFGR1 的 MEM_MODE 是两位0x03 表示 SRAM 映射。改完这个寄存器后0x00000000 地址访问的就是 SRAM 了中断向量自然就从 SRAM 里取。这个操作必须在跳转 App 之前完成而且跳转后 App 不能再改这个寄存器否则中断会乱套。2.3 App 的链接脚本怎么改Bootloader 和 App 是两个独立的工程App 工程的链接脚本必须告诉链接器“我的代码从 0x08003000 开始RAM 从 0x200000C0 开始”。以 Keil MDK 为例在 Target 选项里把 IROM1 的 Start 改成 0x08003000Size 改成 0xC00048KB按实际调整IRAM1 的 Start 改成 0x200000C0Size 改成 0x1F408KB - 192 字节。如果你用的是 GCC 工具链那就改 .ld 文件MEMORY { FLASH (rx) : ORIGIN 0x08003000, LENGTH 52K RAM (xrw) : ORIGIN 0x200000C0, LENGTH 8K - 192 }还有一处必须改App 的system_stm32f0xx.c里有个VECT_TAB_OFFSET宏默认是 0x00。你要把它改成 0x3000这样SystemInit()里设置向量表偏移时才会指向 App 的向量表。不过因为 M0 没有 VTOR这个宏实际上只在 M3/M4 上生效M0 上改了也没用但为了代码规范还是建议改掉免得以后移植到 M3 上忘了。3. Bootloader 核心逻辑实现3.1 上电后的判断流程Bootloader 的 main 函数逻辑其实很清晰我把它拆成几个步骤初始化时钟和外设、读取升级标志、判断是否进入升级模式、执行升级或跳转。升级标志我一般存在参数区的第一个字比如 0x0800F800 地址。约定 0x5A5A5A5A 表示“需要升级”其他值表示“正常启动”。上电后 Bootloader 先读这个地址如果是 0x5A5A5A5A就进入升级流程否则检查 App 区是否有有效固件比如读 App 起始地址的前两个字看栈顶指针是否在 SRAM 范围内有效就跳转无效就停留在 Bootloader 等待升级。还有一种情况是“强制升级”比如上位机发送特定命令或者某个按键被按住。我一般会留一个 GPIO 检测上电时如果检测到按键按下就无条件进入升级模式。这个功能在现场调试时特别有用万一 App 跑飞了按住按键上电就能救回来。#define UPGRADE_FLAG_ADDR 0x0800F800 #define UPGRADE_FLAG_VALUE 0x5A5A5A5A #define APP_START_ADDR 0x08003000 typedef void (*pFunction)(void); int main(void) { HAL_Init(); SystemClock_Config(); UART_Init(115200); // 检查按键是否按下强制升级 if (KEY_IsPressed()) { UART_SendString(Force upgrade mode\r\n); IAP_UpgradeProcess(); } // 检查升级标志 uint32_t flag *(volatile uint32_t *)UPGRADE_FLAG_ADDR; if (flag UPGRADE_FLAG_VALUE) { UART_SendString(Upgrade flag detected\r\n); IAP_UpgradeProcess(); } // 检查 App 是否有效 if (IAP_IsAppValid()) { UART_SendString(Jumping to app...\r\n); IAP_JumpToApp(); } else { UART_SendString(No valid app, waiting for upgrade\r\n); IAP_UpgradeProcess(); } while (1) {} }IAP_IsAppValid()的判断逻辑很简单读 App 起始地址的第一个字应该是栈顶指针检查它是否落在 0x20000000 到 0x20002000 之间再读第二个字应该是复位向量检查它是否落在 0x08003000 到 0x0800FFFF 之间。两个条件都满足就认为 App 有效。这个检查不能保证 App 完全正确但能过滤掉大部分“空片”或“擦除后未写入”的情况。3.2 Flash 擦写操作要点STM32F051 的 Flash 操作和 F1 系列类似但寄存器名字和位定义有些差别。关键步骤是解锁 Flash、擦除页、写入半字、等待操作完成、上锁。擦除一页的流程是检查 FLASH_SR 的 BSY 位确保没有正在进行的操作设置 FLASH_CR 的 PER 位和 PROG 位把页地址写入 FLASH_AR然后设置 FLASH_CR 的 STRT 位启动擦除最后轮询 BSY 位等待完成。写入半字则是设置 FLASH_CR 的 PG 位然后往目标地址写 16 位数据同样轮询 BSY。这里有个坑STM32F051 的 Flash 写入必须以半字16 位为单位不能按字节写。如果你从串口收到的是字节流必须凑成两个字节再写。我当初图省事直接按字节写结果写进去的数据全是乱的后来看参考手册才发现这个问题。static void Flash_WaitForLastOperation(void) { while (FLASH-SR FLASH_SR_BSY); } void Flash_ErasePage(uint32_t pageAddr) { Flash_WaitForLastOperation(); FLASH-CR | FLASH_CR_PER; FLASH-AR pageAddr; FLASH-CR | FLASH_CR_STRT; Flash_WaitForLastOperation(); FLASH-CR ~FLASH_CR_PER; } void Flash_WriteHalfWord(uint32_t addr, uint16_t data) { Flash_WaitForLastOperation(); FLASH-CR | FLASH_CR_PG; *(volatile uint16_t *)addr data; Flash_WaitForLastOperation(); FLASH-CR ~FLASH_CR_PG; }解锁和上锁的操作是往 FLASH_KEYR 写 0x45670123再写 0xCDEF89AB。上锁就是设置 FLASH_CR 的 LOCK 位。我习惯在每次擦写操作前解锁操作完立刻上锁避免程序跑飞时误擦 Flash。还有一个细节擦除和写入操作会阻塞 CPU因为 Flash 在擦写时不能同时读取指令。如果你的 Bootloader 代码本身就在 Flash 里跑擦写期间 CPU 取指会停顿这是正常的。但如果你在擦写时开了中断中断向量表又在 Flash 里那中断响应会延迟甚至出错。所以我在擦写期间会关掉全局中断擦完再开。3.3 跳转到 App 的完整流程跳转是 IAP 的最后一哆嗦也是最容易出问题的地方。完整的跳转流程是关闭所有外设中断、设置主栈指针、搬移向量表、重映射 SRAM、跳转到 App 的复位向量。关闭中断这一步不能省。Bootloader 里可能开了串口中断、定时器中断如果不关掉就跳过去App 里又没重新初始化这些中断一触发就 HardFault。我一般直接调用__disable_irq()关掉全局中断然后逐个关闭外设时钟。设置主栈指针用__set_MSP()函数参数是 App 起始地址的第一个字。搬移向量表和重映射 SRAM 前面已经讲过。最后跳转用函数指针void IAP_JumpToApp(void) { uint32_t appStack *(volatile uint32_t *)APP_START_ADDR; uint32_t appReset *(volatile uint32_t *)(APP_START_ADDR 4); pFunction jumpToApp; // 检查栈顶指针合法性 if ((appStack 0x2FFE0000) ! 0x20000000) { return; } // 关闭全局中断 __disable_irq(); // 关闭外设时钟根据实际情况调整 RCC-APB1ENR 0; RCC-APB2ENR 0; RCC-AHBENR 0; // 搬移向量表并重映射 IAP_RelocateVectorTable(); // 设置主栈指针 __set_MSP(appStack); // 跳转 jumpToApp (pFunction)appReset; jumpToApp(); }跳转后 App 的第一件事应该是重新初始化时钟和外设因为 Bootloader 把时钟都关了。App 的SystemInit()会做这件事但如果你用的是 HAL 库HAL_Init()里会重新配置 SysTick这个要注意别和 Bootloader 的配置冲突。4. 固件传输协议与校验机制4.1 串口传输协议设计Bootloader 和上位机之间需要一个简单的传输协议。我用得最多的是“帧头 命令 长度 数据 校验”的结构帧头用两个字节 0xAA 0x55命令占一个字节长度占两个字节小端数据区变长最后跟一个 CRC16 校验。字段长度说明帧头2 字节固定 0xAA 0x55命令1 字节0x01 握手、0x02 擦除、0x03 写数据、0x04 校验、0x05 跳转长度2 字节数据区长度小端数据N 字节命令相关CRC162 字节从命令到数据的 CRC16握手命令用于上位机确认设备处于 Bootloader 模式设备回复固件版本和 Flash 页大小。擦除命令携带起始页地址和页数设备执行擦除后回复成功或失败。写数据命令携带起始地址和数据设备写入后回复。校验命令携带固件总长度和 CRC32 值设备计算 App 区的 CRC32 并比对。跳转命令让设备执行跳转。这个协议的好处是简单、可靠CRC16 能过滤掉大部分传输错误。缺点是效率不高每帧都有额外开销。如果你传的是 50KB 的固件按每帧 256 字节数据算大概要 200 帧每帧额外开销 7 字节总共多传 1.4KB可以接受。4.2 CRC 校验的实现CRC 校验是保证固件完整性的关键。我在 Bootloader 里用两个 CRC帧校验用 CRC16固件整体校验用 CRC32。CRC16 用查表法实现速度快CRC32 用 STM32F051 没有硬件 CRC 外设只能软件算但 50KB 数据算下来也就几十毫秒不影响体验。static const uint16_t crc16_table[256] { ... }; // 省略表内容 uint16_t CRC16_Calc(uint8_t *data, uint32_t len) { uint16_t crc 0xFFFF; while (len--) { crc (crc 8) ^ crc16_table[((crc 8) ^ *data) 0xFF]; } return crc; } uint32_t CRC32_Calc(uint32_t addr, uint32_t len) { uint32_t crc 0xFFFFFFFF; uint8_t *p (uint8_t *)addr; while (len--) { crc ^ *p; for (int i 0; i 8; i) { crc (crc 1) ^ (0xEDB88320 -(crc 1)); } } return ~crc; }上位机在发送固件前先算好整个固件的 CRC32发完所有数据后把 CRC32 发给设备。设备收到后从 App 起始地址开始算同样长度的 CRC32比对一致就认为固件完整。如果不一致设备回复失败上位机重传。这里有个经验CRC32 的计算范围要包含整个 App 区但 App 区可能比实际固件大。我的做法是上位机在固件末尾填充 0xFF 到页边界然后按实际固件长度算 CRC32设备也按这个长度算。这样就不用擦除整个 App 区再算省时间。4.3 断点续传与重试机制现场升级最怕的是传了一半断线。如果每次断线都从头传50KB 的固件在低速串口上要传好几分钟体验很差。我的做法是在协议里加一个“查询进度”命令设备回复当前已写入的最高地址。上位机断线重连后先查询进度然后从断点继续传。实现断点续传的关键是设备要记录“已写入到哪个地址”。我一般把这个信息存在参数区的第二个字每写完一帧就更新一次。这样即使设备断电重启重新进入 Bootloader 后还能从上次的位置继续。重试机制也很重要。每帧数据发完后上位机等待设备的 ACK如果超时比如 500ms没收到就重发当前帧最多重试 3 次。3 次都失败就报错让用户检查连接。这个逻辑在上位机实现设备端只需要收到帧就处理并回复 ACK。5. 常见问题与排查技巧实录5.1 跳转后进不了中断这是 M0 做 IAP 最高频的问题。现象是 App 能跑起来主循环正常但一触发中断就死机。原因几乎都是向量表没搬移或 SRAM 没重映射。排查步骤是先用调试器看 0x00000000 地址的内容如果还是 Bootloader 的向量表说明重映射没生效再看 SRAM 0x20000000 处的内容如果全是 0说明搬移没执行。还有一种情况是搬移了但地址不对。App 的向量表在 0x08003000你搬移的时候要从这个地址读不能从 0x08000000 读。我有一次复制代码时忘了改地址搬了个 Bootloader 的向量表过去结果中断全跑到 Bootloader 里去了。注意SRAM 重映射后0x00000000 地址就变成 SRAM 了调试器如果还按 Flash 别名去读看到的可能是旧数据。用内存窗口直接看 0x20000000 更靠谱。5.2 Flash 写入失败或数据错乱Flash 写入失败一般有三个原因没解锁、没擦除、没按半字写。STM32F051 的 Flash 在写入前必须先擦除擦除后所有位都是 1写入只能把 1 变成 0。如果你往一个没擦除的地址写数据结果就是新旧数据按位与出来的东西完全不对。数据错乱的另一个原因是写入地址没对齐。半字写入要求地址是 2 的倍数如果你传了个奇地址硬件可能忽略最低位导致数据写到隔壁去了。我在协议里强制要求数据长度是偶数起始地址也是偶数从源头避免这个问题。现象可能原因排查方法写入后读出来全是 0xFF没解锁或没擦除检查 FLASH_CR 的 LOCK 位和 PER 位数据错位地址没对齐检查写入地址是否为偶数写入一半失败擦除不完整检查擦除页地址和页数校验总是不通过CRC 算法不一致对比上位机和设备的 CRC 参数5.3 App 能跑但串口不通这个问题通常出在时钟配置上。Bootloader 把时钟初始化成 48MHzApp 的SystemInit()可能又初始化了一遍如果配置不一致串口波特率就会偏。我的做法是 App 里不依赖 Bootloader 的时钟配置自己完整初始化一遍包括 HSE、PLL、AHB、APB 分频。还有一种情况是 Bootloader 里开了串口中断跳转前没关App 里又用了同一个串口但没重新配置中断优先级导致中断冲突。解决办法就是跳转前把所有外设中断关掉App 里重新初始化。5.4 升级过程中断电怎么办这是现场升级必须考虑的问题。如果升级到一半断电App 区就是半新半旧的状态直接跳过去肯定跑飞。我的做法是在参数区加一个“升级中”标志进入升级流程前先写这个标志升级完成并校验通过后清除。上电时如果发现“升级中”标志还在就强制进入升级模式等待上位机重新传固件。这个机制配合断点续传基本上能保证即使断电也不会变砖。最坏的情况是 Bootloader 自己升级时断电但 Bootloader 一般不做在线升级都是用仿真器烧所以风险可控。6. 一些实操心得和扩展思路Bootloader 的体积优化是个持续的过程。我一开始写的 Bootloader 有 14KB后来把 HAL 库换成 LL 库砍掉不用的外设初始化关掉 printf 浮点支持压到了 7KB。省下来的 Flash 都是 App 的对于 64KB 的芯片来说多 7KB 能多写不少业务逻辑。传输方式上串口是最简单的但速度慢。如果你用 CAN可以做到 1Mbps50KB 固件几秒钟就传完。如果用无线模块那就要考虑丢包重传和信号稳定性。我做过一个用 LoRa 升级的项目传输速率只有几 kbps50KB 固件传了将近两分钟所以协议里加了压缩把固件用 heatshrink 压到 30KB 左右省了不少时间。最后分享一个调试技巧在 Bootloader 里留一个“内存 dump”命令上位机可以读取任意地址的数据。调试跳转问题时用这个命令读 0x00000000 和 0x20000000 的内容比接调试器方便多了。这个命令在量产固件里记得删掉不然是个安全隐患。这个 IAP 框架后来被我移植到了好几款 M0 和 M3 的芯片上核心逻辑几乎没变只是向量表搬移和 Flash 操作寄存器换一下。把这一套吃透以后遇到任何 Cortex-M 的 IAP 需求基本都能半天搞定。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑