STM32F051串口IAP实战:Bootloader与App双区升级详解 1. 项目缘起与整体设计思路STM32F051 是 ST 家 Cortex-M0 内核里性价比很高的一颗芯片48MHz 主频、64KB Flash、8KB RAM在很多小型控制器、传感器节点、手持设备里都能见到它的身影。但 Flash 只有 64KB一旦产品装到现场想改个功能、修个 Bug如果还得把设备拆回来接 SWD 烧录那维护成本就非常难受了。IAPIn-Application Programming就是解决这个问题的让设备自己通过某个通信接口串口、CAN、无线模块等接收新的固件数据自己擦写自己的 Flash自己跳转执行。说白了就是让单片机具备“自己给自己升级”的能力。我这次做的这个 IAP 方案目标很明确基于 STM32F051用串口作为传输通道实现一个最小可用的 Bootloader App 双区结构。Bootloader 常驻在 Flash 起始位置负责上电判断、接收固件、写入 App 区、校验、跳转App 区就是正常的业务程序编译时把起始地址偏移到 Bootloader 之后。整个方案不依赖外部存储不需要文件系统固件通过上位机分帧发送Bootloader 边收边写。适合谁看如果你手上有 STM32F0/F1/F4 系列的板子想搞明白 IAP 到底怎么落地或者你已经在做类似的东西但卡在跳转、中断向量、校验这些坑上这篇内容应该能帮你把整条链路走通。为什么选串口而不是别的因为串口最通用USB 转 TTL 模块几块钱一个PC 端工具也好写。CAN 适合汽车电子无线适合远程但作为第一次把 IAP 跑通的方案串口是最容易调试的。你可以在串口助手里一帧一帧看数据出问题也好定位。等串口方案跑稳了换成 CAN 或者无线只是换传输层的事核心的 Flash 操作和跳转逻辑完全不用动。整体分区上我采用的是最经典的双区方案Bootloader 占 0x08000000 开始的 12KBApp 从 0x08003000 开始往后到 0x0800F800 左右最后留 2KB 给参数区存固件长度、CRC、升级标志等。为什么 Bootloader 给 12KB因为 STM32F051 的 Flash 页大小是 1KB12KB 就是 12 页对于串口接收 Flash 写入 简单校验来说绰绰有余。App 区从 0x08003000 开始这个地址要 1KB 对齐因为 F0 系列擦除最小单位就是 1KB。参数区放最后避免和 App 冲突也方便单独擦写。注意STM32F051 的 Flash 擦除是按页1KB进行的写入是按半字16bit进行的。这意味着你接收固件时必须做缓冲凑够 1KB 或者至少凑够 2 字节再写。直接一个字节一个字节写会非常慢而且容易出错。2. 核心细节解析与实操要点2.1 Flash 分区与地址规划先把这个表列清楚后面所有代码都围绕这个来区域起始地址结束地址大小用途Bootloader0x080000000x08002FFF12KB引导程序永不被擦除App0x080030000x0800F7FF50KB用户应用程序参数区0x0800F8000x0800FFFF2KB升级标志、固件长度、CRCApp 区 50KB 对于 F051 的 64KB Flash 来说留了 12KB 给 Bootloader2KB 给参数刚好 64KB。如果你觉得 Bootloader 用不了 12KB可以压缩到 8KBApp 就能多 4KB。但我不建议压得太狠因为 Bootloader 里还要放串口驱动、Flash 操作、CRC 校验甚至以后想加个简单的菜单或者超时机制都需要空间。12KB 是个比较舒服的平衡点。参数区的设计有个小技巧不要每次升级都擦整个参数页。你可以把参数页分成多个 slot每个 slot 存一份升级信息轮流写这样避免频繁擦写导致 Flash 寿命下降。F051 的 Flash 擦写寿命大概是 1 万次虽然不少但如果你做频繁升级测试还是注意点好。我一般用两个 slot 轮换写之前先读一遍哪个空就用哪个。2.2 中断向量表的重映射这是 IAP 里最容易翻车的地方。Cortex-M0 的中断向量表默认在 0x00000000但 STM32F051 通过 Flash 映射实际复位后从 0x08000000 取向量。Bootloader 在 0x08000000所以它正常跑。但 App 在 0x08003000它的向量表也在 0x08003000如果不做重映射App 里一旦发生中断CPU 还是会去 0x08000000 找中断服务函数那就跳到 Bootloader 的中断里去了必死无疑。解决办法是在 App 的 main 函数开头尽早调用SCB-VTOR 0x08003000;。注意Cortex-M0 的 VTOR 寄存器是存在的但有些早期的 M0 芯片可能不支持偏移STM32F051 是支持的放心用。另外App 的链接脚本里要把 Flash 起始地址改成 0x08003000这样编译出来的向量表才在正确的位置。实操心得我习惯在 App 的SystemInit()之后、main()之前就设置 VTOR越早越好。如果你用的是 HAL 库HAL_Init()里会调用SystemInit()所以可以在HAL_Init()之后立刻设置。但更稳妥的做法是直接在SystemInit()里改不过那样会污染库文件不推荐。我一般是在main()的第一行就写SCB-VTOR APP_ADDR;实测下来没问题。2.3 跳转前的现场清理从 Bootloader 跳转到 App 之前必须把 Bootloader 用过的外设全部关掉中断全部禁用。为什么因为 Bootloader 里可能开了串口中断、SysTick 中断如果不清跳到 App 后这些中断还在触发但中断向量已经指向 App 的向量表了而 App 可能根本没初始化这些外设结果就是硬件错误。具体操作关闭所有中断__disable_irq();关闭 SysTickSysTick-CTRL 0;关闭 Bootloader 里用到的外设时钟比如 USART1、GPIOA 等用RCC-APB2ENR ~RCC_APB2ENR_USART1EN;之类。清除所有挂起的中断标志NVIC-ICPR[0] 0xFFFFFFFF;M0 只有一个 ICPR 寄存器。设置主栈指针__set_MSP(*(__IO uint32_t*)APP_ADDR);跳转((void (*)(void))(*((__IO uint32_t*)(APP_ADDR 4))))();这几步顺序不能乱。尤其是设置 MSP 必须在跳转之前因为 App 的栈顶值存在它的向量表第一个字里。如果你先跳转再设 MSP那 App 一开始用的还是 Bootloader 的栈可能会踩坏数据。3. 实操过程与核心环节实现3.1 Bootloader 的串口接收协议设计协议不用太复杂但要有帧头、长度、数据和校验。我用的格式如下字节内容说明00xAA帧头110x55帧头22CMD命令0x01 开始升级0x02 数据帧0x03 结束升级3LEN_H数据长度高字节4LEN_L数据长度低字节5~NDATA实际数据N1CRC_HCRC16 高字节N2CRC_LCRC16 低字节CRC16 我用的是 Modbus 多项式 0xA001计算范围从 CMD 到 DATA 最后一个字节。为什么用 CRC16 而不是简单的累加和因为串口传输在工业环境里容易受干扰累加和检错能力太弱CRC16 能检出绝大多数错误。计算量对于 48MHz 的 M0 来说完全不是问题1KB 数据算 CRC 也就几百微秒。上位机发送时每帧数据建议 256 字节或 512 字节。太小了帧数多效率低太大了 Bootloader 的接收缓冲要开很大8KB RAM 吃不消。我一般用 512 字节Bootloader 里开一个 1KB 的缓冲区收满 1KB 就擦一页写一页。这样 RAM 占用可控Flash 写入也整齐。3.2 Flash 擦写的关键代码STM32F051 的 Flash 操作寄存器不多但顺序很严格。擦除一页的流程void Flash_ErasePage(uint32_t page_addr) { while (FLASH-SR FLASH_SR_BSY); // 等待空闲 FLASH-CR | FLASH_CR_PER; // 页擦除模式 FLASH-AR page_addr; // 设置页地址 FLASH-CR | FLASH_CR_STRT; // 开始擦除 while (FLASH-SR FLASH_SR_BSY); // 等待完成 FLASH-CR ~FLASH_CR_PER; // 退出页擦除模式 }写入半字的流程void Flash_WriteHalfWord(uint32_t addr, uint16_t data) { while (FLASH-SR FLASH_SR_BSY); FLASH-CR | FLASH_CR_PG; // 编程模式 *(__IO uint16_t*)addr data; // 写入半字 while (FLASH-SR FLASH_SR_BSY); FLASH-CR ~FLASH_CR_PG; // 退出编程模式 }注意写之前必须确保目标地址已经被擦除也就是内容是 0xFFFF。如果没擦就写写进去的数据是错的而且可能触发 PGERR 错误。我踩过这个坑当时忘了擦参数页就直接写升级标志结果标志一直是乱的查了半天。还有一个细节F0 系列的 Flash 在擦写时CPU 从 Flash 取指会暂停。也就是说如果你的擦写代码本身也在 Flash 里运行擦写期间 CPU 会停住等擦写完成才继续。这没问题但如果你开了中断中断响应会延迟。所以我在擦写前会关中断擦完再开。对于 IAP 来说擦写期间本来就不需要响应中断关了更安全。3.3 App 的链接脚本修改以 Keil MDK 为例默认的 IROM1 起始是 0x08000000大小 0x10000。你要改成起始 0x08003000大小 0x0C80050KB。具体在 Options for Target - Target 里改。改完之后编译出来的 hex 文件里向量表就在 0x08003000 了。如果你用 GCC改链接脚本.ld文件MEMORY { FLASH (rx) : ORIGIN 0x08003000, LENGTH 50K RAM (xrw) : ORIGIN 0x20000000, LENGTH 8K }改完链接脚本后记得在system_stm32f0xx.c里把VECT_TAB_OFFSET改成 0x3000。不过这个宏在 F0 的库里有定义改起来方便。但我不建议依赖这个宏因为不同版本的库可能不一样直接在 main 里写SCB-VTOR 0x08003000;最保险。3.4 上位机发送流程上位机我用 Python 写了一个简单的脚本读 hex 文件解析成二进制然后分帧发送。关键步骤读 hex 文件转成 bin。可以用objcopy或者自己解析 hex 格式。计算 bin 文件大小发给 Bootloader 作为总长度。按 512 字节分帧每帧加帧头、命令、长度、CRC。发送完所有数据后发结束命令Bootloader 校验整个固件的 CRC。如果校验通过Bootloader 写升级标志然后复位或者直接跳转。Python 里用pyserial库设置波特率 115200超时 1 秒。每发一帧等 Bootloader 回一个 ACK收到 ACK 再发下一帧。这样虽然慢一点但可靠。如果你追求速度可以连续发Bootloader 用环形缓冲接收但那样容易丢帧调试麻烦。我建议第一次做的时候用 ACK 机制稳。4. 常见问题与排查技巧实录4.1 跳转后跑飞或者 HardFault这是最典型的问题。原因通常有三个一是 VTOR 没设对二是 MSP 没设对三是 Bootloader 的外设没关干净。排查方法在 App 的main()第一行点亮一个 LED如果 LED 亮了说明跳转成功问题在 App 内部如果没亮说明跳转本身有问题。然后检查SCB-VTOR的值是不是 0x08003000检查__set_MSP有没有执行检查 Bootloader 里是不是还有中断在使能。我遇到过一次跳转后 App 跑几秒就死最后发现是 Bootloader 里开了 SysTick 中断跳转前忘了关。SysTick 一直在触发但中断向量已经指向 App 的向量表而 App 的 SysTick 处理函数还没初始化结果就 HardFault 了。所以跳转前一定要SysTick-CTRL 0;并且__disable_irq();。4.2 串口接收丢数据115200 波特率下如果 Bootloader 用查询方式接收很容易丢数据因为 Flash 擦写的时候 CPU 停住串口数据就丢了。解决办法是用中断接收把数据存到环形缓冲里主循环再从缓冲里取。但擦写的时候中断也停了所以还是可能丢。更稳妥的做法是加流控或者上位机每发一帧等 ACK。我用的就是等 ACK 的方式虽然慢但从来没丢过。如果你非要用高速连续发送那 Bootloader 的接收缓冲要开大一点比如 4KB然后擦写的时候先把数据收完再擦。但 8KB RAM 的 F051 开 4KB 缓冲有点奢侈所以还是建议 ACK 机制。4.3 CRC 校验失败CRC 失败通常是数据在传输过程中出错了。排查步骤先看是固定某一帧失败还是随机失败。固定失败可能是上位机计算 CRC 的范围和 Bootloader 不一致比如上位机算了帧头Bootloader 没算。随机失败可能是波特率误差太大或者串口线太长受干扰。我一般会在 Bootloader 里把收到的数据原样回传上位机对比这样能快速定位是传输问题还是 CRC 计算问题。还有一个坑CRC16 的初始值和多项式要上下位机一致。我用的是初始值 0xFFFF多项式 0xA001结果不取反。如果你用别的参数两边一定要对好。4.4 升级标志误触发参数区如果没擦干净或者写标志的时候掉电可能导致升级标志乱掉设备一上电就进 Bootloader 不跑 App。解决办法是给标志加一个“魔数”比如 0x5A5A只有标志等于 0x5A5A 且 CRC 正确才认为需要升级。另外升级完成后要立刻把标志清掉避免重复升级。我一般是在跳转前清标志这样即使跳转失败下次上电也不会再进 Bootloader。问题现象可能原因排查方法解决措施跳转后无反应VTOR 未设检查 SCB-VTOR在 App main 开头设置跳转后 HardFaultMSP 未设检查 __set_MSP跳转前设置 MSP串口丢数据擦写时中断关闭看丢数据位置用 ACK 机制CRC 失败计算范围不一致对比上下位机统一 CRC 参数反复进 Bootloader标志未清读参数区跳转前清标志4.5 实操心得与避坑清单每次擦写前先读一下目标地址确认是 0xFFFFFFFF 再写否则先擦。Bootloader 里不要用 printf 之类的重定向串口只用于升级避免冲突。App 里如果也要用串口记得重新初始化不要依赖 Bootloader 的配置。升级过程中如果断电下次上电 Bootloader 应该能检测到不完整的固件重新进入升级模式。我一般会在参数区存一个“升级中”标志升级完成后改成“升级完成”如果上电看到“升级中”就认为上次升级失败等待重新升级。测试的时候先用小固件几 KB跑通流程再试大固件。大固件容易暴露缓冲和擦写边界问题。如果你用的是 HAL 库注意HAL_FLASH_Unlock()和HAL_FLASH_Lock()的配对别锁了忘了开。这个 IAP 方案我在几个小项目里都用过最远的现场设备通过串口升级了十几次没出过问题。核心就是分区清晰、跳转前清理干净、传输加校验。你把这三件事做好剩下的就是调试细节了。后面如果想加无线升级只需要把串口接收换成无线模块的接收协议和 Flash 操作完全不用动。