
简介面向STM32嵌入式开发者的官方IAP升级程序适用于F10x系列基于Cortex-M3内核的MCU解决无外部编程器条件下的串口固件更新与固件读取需求。压缩包共350个文件资源包约1.63MB文件类型覆盖C/H源代码、汇编启动文件、链接脚本以及Keil/IAR等IDE工程文件便于直接查看Ymodem协议实现与中断向量重定位等关键逻辑。已有622人学习。资料包含完整的串口下载与读取固件流程涉及固件分区、向量表重定位、传输错误处理等核心机制同时提供参考示例与批处理文件适合需要评估、移植或二次开发IAP方案的开发者快速上手。通过研究官方示例可以理解Ymodem握手、块传输与校验流程并在此基础上增加校验和或加密机制提升远程维护的安全性。 在嵌入式产品里混久了的人都有体会样机阶段调试固件有多痛快量产之后升级固件就有多痛苦。尤其用的还是 STM32F10x 这种老而弥坚的 Cortex-M3 系列没有以太网、没有无线机箱一封想更新程序只能拆壳、接仿真器、擦除、烧录一轮下来半小时没了。更别提现场设备散落在不同地方售后工程师光跑一趟就得赔上半天差旅。所以 IAPIn-Application Programming这个东西做量产产品的人基本绕不开。STM32F10x 系列本身在出厂时固化了一段 System Memory 里的 Bootloader支持串口、USB 等几种方式下载程序——这就是常说的 ISP。但 ISP 的通信协议是出厂写死的你没法改成自己的加密流程也没法定制私有通信接口更没法在 APP 运行过程中触发升级。IAP 的思路完全不同你自己写一段 Bootloader放在 Flash 起始区域上电先跑 Bootloader它再通过串口、SPI、CAN 或者任何你定义的通道接收新固件把固件写到 APP 区然后跳转执行。说白了ISP 是芯片厂给你准备的安全通道IAP 是你自己搭的专属电梯。这篇文章我会从方案选型、Flash 分区、Bootloader 端实现、APP 端适配、调试翻车实录这几个角度把 STM32F10x 系列做官方 IAP 升级程序这条路上的核心细节一次性讲透。不管你打算参考 ST 官方的 X-CUBE-IAP 快速起步还是想完全自己写一套精简 Bootloader这篇文章都能帮你少踩几个坑。1. IAP升级方案与官方资源怎么选1.1 为什么IAP是量产产品的标准答案IAP 的核心价值不在“能升级”这三个字而在“不用拆机、不用工具、不用专业人士在场”。我在前一个项目里做过一套工业控制板控制器装在配电柜深处四周都是强电接线让现场电工去插仿真器烧程序人家手都在抖。后来改成 IAP 方案维护人员只需要用一根串口线接到调试口或者干脆通过咱们的上位机远程下发固件5 分钟完成升级体验完全不一样。从技术原理上说IAP 之所以可行是因为 Cortex-M3 内核允许你把中断向量表映射到任意 Flash 地址。STM32F10x 上电后默认从 0x08000000 取栈顶指针和复位向量但你可以通过 SCB-VTOR 寄存器在 F1 系列上这个是标准外设库和 HAL 库都支持的把向量表搬到 0x08004000、0x08008000 甚至更高。这个偏移一旦生效所有中断响应都会自动去新的向量表里找入口地址APP 程序就像跑在一颗“只有这么大 Flash”的单片机上一样运行起来几乎无感知。那为什么不直接用芯片自带的 System Memory BootloaderISP原因很现实第一ISP 的通信协议是出厂固定的数据格式、握手流程都没法定制产品上想加个加密或者校验逻辑加不了第二ISP 只能在上电时通过 BOOT0 引脚来选择启动模式现场不可能每次升级都去拨跳线帽第三ISP 没法实现在 APP 运行过程中主动进入升级模式你总不能为了升级专门给用户发个“断电-拨线-上电-下载”的操作手册吧。IAP 把这些限制全部打破协议自己定触发方式自己定加密自己定整个升级流程被完全掌控在自己手里。1.2 官方与第三方方案如何取舍ST 官方针对 IAP 有一套完整的参考实现X-CUBE-IAP。这个是 STM32Cube 生态里的 IAP 扩展包基于 X-CUBE 框架、配合 CubeMX 图形化配置支持通过 UART、USB、SPI、I2C 甚至 CAN 等多种通信接口烧写固件。如果你搜“x cube iap 下载”在 ST 官网的 Tools Software 分类下就能找到下载后导入 CubeMX 就能看到完整的工程模板。我的建议是看官方例程可以别照搬整个工程。原因很简单X-CUBE-IAP 为了覆盖各种 MCU 和通信外设代码量和抽象层次都比较重丢到实际产品里会有大量用不上的东西。我第一版 IAP 是拿官方 UART 例程改的改到最后发现去掉 FIFO、去掉协议扩展、去掉各种回调机制之后剩下的核心逻辑还不如自己重新写一遍来得干净。所以更合理的路径是用官方例程理解整个状态机和 Flash 操作流程然后用自己项目的通信协议重写一套精简版 Bootloader代码控制在几百行以内逻辑清晰出问题也容易排查。第三方方案也不是没有比如一些国产厂商提供的移植版 IAP 组件、各种开源 Bootloader比如基于 Ymodem 协议的串口 Bootloader但质量和维护情况参差不齐。考虑到 STM32F10x 的资料池子足够大我建议还是以官方参考实现 自己改编为主毕竟出了问题你能找到的数据手册、勘误表、论坛讨论都是最多的。2. Flash分区与跳转流程先画好施工图2.1 地址分区怎么规划才不打架做 IAP 之前的第一件事不是写代码而是把 Flash 地址空间的分区图画明白。以常见的 STM32F103C8T6 为例它内部 Flash 共 64KB地址从 0x08000000 到 0x0800FFFF。你要在这里面划分出至少两块区域Bootloader 区和 APP 区。我个人习惯再加一块参数存储区用来放升级标志、固件版本号、校验和这类关键信息。这样分区的好处是写升级标志和读升级标志都只需要操作一个固定的页不会误伤代码区。典型分区如下区域地址范围大小用途Bootloader0x08000000 - 0x08003FFF16KBIAP 引导程序上电先执行APP0x08004000 - 0x0800BFFF32KB用户应用程序可被覆盖升级参数区0x0800C000 - 0x0800FFFF16KB升级标志、固件版本、校验信息注意F103 中容量产品每个 Flash 页大小是 1KB大容量产品如 F103ZET6每页是 2KB具体要看数据手册的 Flash 章节别凭感觉分配。划区时尽量让 APP 起始地址对齐到页边界这样擦除和写入操作会简单不少。16KB 的 Bootloader 看似很小但装下串口驱动、Flash 驱动、协议解析、CRC 校验模块完全够用我的第一版精简 Bootloader 编译出来只有 8KB 不到。这里还有一个隐藏问题参数区不要和 Bootloader 放在同一页。如果升级标志位和 Bootloader 代码共享一个 Flash 页那每次写标志位都要先擦除整页一旦擦除后代码没来得及恢复就断电Bootloader 直接变砖。把参数区独立成一个区域这个问题就从根本上不存在了。2.2 启动流程里最容易忽略的细节Bootloader 的启动流程看起来很简单检查升级标志有就等着收固件没有就跳转到 APP。但实际落地时顺序和细节决定了稳定性。我推荐的标准流程是这样初始化时钟、串口、看门狗、GPIO保证最基本的通信能力。读取参数区里的升级标志。如果升级标志有效进入升级模式等待上位机发送固件包边收边写 Flash收完做整体校验校验通过后清除升级标志软件复位校验失败就保留标志等待重发。如果升级标志无效直接跳过升级逻辑关掉 Bootloader 用到的外设中断设置好向量表跳转到 APP。第 4 步里“关掉外设中断”这个细节特别容易踩坑。Bootloader 里初始化了串口接收开了接收中断。如果跳转到 APP 之前你只复位串口、不关闭 NVIC 里已经使能的中断APP 启动后一旦串口收到数据MCU 会进入中断向量表——但 APP 里对应的串口中断服务函数可能在它自己的初始化里还没准备好或者中断处理逻辑跟 Bootloader 的预期完全不一样结果就是进入 HardFault 或者程序行为鬼畜。所以我跳转之前一定会把 Bootloader 用到的外设全部 DeInit并且显式关闭对应中断线。2.3 跳转函数的正确打开方式跳转函数是 Bootloader 里最核心的一段代码网上版本很多但有一些写法是错的。直接看一个能用的写法typedef void (*pFunction)(void); void JumpToApp(uint32_t app_addr) { /* 1. 检查 APP 区起始地址是否合理栈顶指针必须在 RAM 范围内 */ uint32_t app_sp *(volatile uint32_t *)app_addr; uint32_t app_pc *(volatile uint32_t *)(app_addr 4); if ((app_sp 0xFFF00000) ! 0x20000000) { return; /* 栈顶指针不在 0x20000000 范围内说明 APP 区没有有效程序 */ } /* 2. 跳到 APP 前把 Bootloader 的外设恢复默认 */ __disable_irq(); SysTick-CTRL 0; SysTick-LOAD 0; SysTick-VAL 0; /* 3. 重新设置中断向量表 */ SCB-VTOR app_addr; /* 4. 取 APP 复位向量跳转 */ pFunction jump_to_app; jump_to_app (pFunction)app_pc; __set_MSP(app_sp); jump_to_app(); }这段代码里有三个关键点。第一跳转前读一下 APP 区首地址的栈顶指针判断它是否落在 RAM 区域内。这一步能有效防止 Bootloader 跳到一个被擦除的空白 Flash 区域导致 HardFault。第二先关闭全局中断再设置 VTOR最后跳转。顺序反了跳转瞬间来一个中断就会炸。第三__set_MSP(app_sp)是把主栈指针切到 APP 的栈顶这一步不能省略否则 APP 用的是 Bootloader 的栈空间栈大小和位置都不对。3. Bootloader端实现要点逐帧逐页抠细节3.1 通信帧格式怎么定义最稳Bootloader 的通信协议没有一个绝对标准但有几个设计原则是通用的帧结构要简单、校验要可靠、传输要有确认机制。我在项目里用的是一套很朴素的帧格式附在下面字段长度说明帧头2 字节固定 0xAA 0x55用于同步命令字1 字节0x01 擦除请求0x02 数据包0x03 结束确认固件包序号2 字节当前数据包的序号从 0 开始数据长度1 字节数据域长度最大 244 字节数据域N 字节固件内容或擦除参数CRC162 字节从命令字到数据域的 CRC 校验值这个帧的一个关键点是每帧数据长度限制在 244 字节以内。为什么因为 Bootloader 里的接收缓冲区通常不会开太大F103 的 RAM 也就 20KBFlash 读写还得用一部分内存。244 字节数据域 8 字节帧头帧尾一帧总共 252 字节缓冲区开 256 字节刚好放下RAM 开销非常小。而且每次收到一帧就可以直接写入 Flash 页对应的缓冲区写完立即擦除原页、写入新数据不需要攒整个固件在 RAM 里。上位机发完一个数据包后Bootloader 写入 Flash 并回复 ACK0x06或者 NAK0x15上位机收到 ACK 才发下一帧。这种停等协议虽然吞吐量不算高但胜在实现简单、出错容易定位。我实测过用 115200 波特率的串口升级一个 32KB 的固件也就是 130 帧左右总耗时大概 10 秒出头完全在可接受范围内。3.2 Flash擦写与校验两条命脉STM32F10x 的 Flash 写操作有几个硬性要求写之前必须擦除、写入的最小单位是半字16 位、Flash 擦写过程中 CPU 会暂停执行、不允许在擦写 Flash 的同时读取 Flash 里的代码。这最后一条最坑人因为 Bootloader 本身就在 Flash 里运行擦写 APP 区时虽然不会覆盖 Bootloader 自己的代码但 Flash 控制器在操作期间会挂起对 Flash 的访问如果你的擦写函数里面有中断服务函数也要访问 Flash那中断就会卡顿甚至引发问题。所以擦写期间最好关中断至少保证没有中断上下文会干扰。擦写流程我习惯封装成“读-改-写”三步的接口uint8_t IAP_WritePage(uint32_t page_addr, uint8_t *data, uint32_t len) { uint32_t pos 0; HAL_StatusTypeDef status; /* 1. 擦除目标页 */ FLASH_ErasePage(page_addr); /* 2. 按半字写入 */ while (pos len) { uint16_t word data[pos] | (data[pos 1] 8); status HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, page_addr pos, word); if (status ! HAL_OK) { return 1; } pos 2; } return 0; }写完 Flash 之后一定要做读回校验不能只依赖 HAL_FLASH_Program 的返回值。我遇到过一种情况FLASH_Program 返回 HAL_OK但读回来数据对不上原因是写入地址越界到了保留区域。所以写完后把刚写入的区域逐字节读出来跟源数据比对一次全部一致才回 ACK返则回 NAK 让上位机重发。整个过程多花几毫秒但避免了“写完才发现数据坏了”的尴尬。整体校验我推荐在全部数据传输完成后对 APP 区做一次 CRC32 校验或者累加和校验。每帧的 CRC16 能防单帧传输错误整体校验能防 Flash 写入错误和固件文件本身不完整两道保险缺一不可。3.3 超时重传与容错设计串口传输不是什么高可靠通道尤其在工业现场电磁干扰、线缆接触不良、上位机软件卡顿都可能导致丢帧。Bootloader 的容错能力直接决定升级成功率。我在设计里做了三层意外兜底。第一层是帧级校验CRC16 校验不过就回 NAK上位机立即重传当前帧。第二层是超时机制Bootloader 每收到一帧就重置一个 2 秒超时计时器如果 2 秒内没收到下一帧就放弃本次升级把升级标志清掉软件复位跳转到旧 APP。旧 APP 还在设备不会变砖用户损失只是一次失败的升级尝试。第三层是重传次数的上限同一帧连续错误 5 次就放弃防止上位机死循环空转。这套容错设计的一个核心思路是“失败要能退出退出要有退路”。如果上位机升级中断了Bootloader 等超时后自动跳旧 APP相当于用旧版本继续跑业务等网络好了再重新升级。这种对异常的处理比单纯追求“一次升级必须成功”要实际得多。4. APP端配套改造不改这几点必翻车4.1 链接脚本与启动文件修改Bootloader 写好了APP 端不改的话依然没法跑。最基础的一步就是把 APP 的链接地址从默认的 0x08000000 改到 APP 区起始地址比如 0x08004000。在 Keil MDK 里这个配置在 Options for Target - Target 页面的 IROM1 区域。假设你的 APP 区是 0x08004000 开始、大小 32KB那 IROM1 Start 填 0x08004000Size 填 0x800032KB。如果你用的是 GCC 工具链对应的就是链接脚本里的 FLASH 起始地址和长度。改完链接地址只是第一步还要检查启动文件里的堆栈设置。虽然 APP 程序的启动文件一般是标准模板堆栈大小默认 0x4001KB但如果你在 APP 里用了比较大的局部变量、递归、或者 FreeRTOS堆栈就得适当加大否则 APP 跑起来会莫名其妙死机。我习惯在 APP 里加一个开机自检函数读一下 SP 是否在 0x20000000 到 RAM 末尾范围内不在就说明 Bootloader 跳转过来时栈没设对。4.2 中断向量表重映射的正确写法链接地址改好了Bootloader 跳过去之后 APP 还是不响应中断原因就在向量表。APP 程序里必须显式把中断向量表偏移到 0x08004000否则发生中断时 CPU 仍然从 0x08000000 取中断入口——那个位置是 Bootloader 的向量表里面的中断服务函数地址指向 Bootloader 代码APP 里注册的中断处理函数永远不会被调用。F10x 系列设置向量表的标准姿势是SCB-VTOR 0x08004000;注意在标准外设库时代有这么一个函数NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x4000)但在 HAL 库和实际应用里直接操作SCB-VTOR更直观也更好理解。要强调一点这行代码必须在系统初始化阶段、任何外设中断使能之前执行。如果放在 main 函数后面、串口初始化之后一旦在设置之前来了中断程序就乱了。我的习惯是在 startup 文件里SystemInit之后、进入 main 之前或者在 main 函数的最开头第一句就设置。为什么 F1 系列只需要设置 SCB-VTOR因为 Cortex-M3 的向量表地址是由这个寄存器直接决定的不像 F2/F4 系列还要额外开启 SYSCFG 时钟、调用 SYSCFG_MemoryRemapConfig。但你要是从 F4 的工程迁移代码过来注意别画蛇添足。4.3 升级标志与软件复位指令的前后呼应APP 里必须有一个“主动进入升级模式”的入口。最常见的方式是收到上位机的升级命令后在参数区写入升级标志然后执行软件复位让 CPU 重启后直接进入 Bootloader 的升级流程。void EnterBootloader(void) { /* 写升级标志到参数区 */ IAP_WriteFlag(0x55AA); /* 软件复位 */ NVIC_SystemReset(); }这里有个细节写标志和复位之间要做 Flash 写操作完成的确认。HAL_FLASH_Program 返回 HAL_OK 不代表数据已经稳定落盘最好再把读回来跟写入值比对一次确认无误再执行复位。否则复位指令发出去了标志位还没真正写进去Bootloader 启动时检查标志发现无效直接跳转到旧 APP升级没进入。另外升级完成后 Bootloader 清标志、软件复位、跳转到新 APP新 APP 运行时也应该维护自己的版本号并把它写到参数区。这样上位机可以随时查询设备当前运行的版本判断是否需要升级。这个功能听起来简单但在实际维护中非常有用——你知道现场设备跑的固件是几个月前编译的哪个版本比什么都重要。5. 调试实录5个高频翻车点与排查建议5.1 跳转后直接进HardFault这是 IAP 做出来之后遇到的第一个问题大概率出在跳转函数对栈顶指针的检查不够严谨或者 APP 区对应的 Flash 地址里根本没有有效程序。排查方法很简单用调试器在跳转函数里打断点查看读取到的 app_sp 和 app_pc 是否合理。如果 app_pc 不是 0x08xxxxxx 开头的地址说明你烧录到 APP 区的程序要么没编译成功、要么链接地址和你想的不一致。切记升级前先往 APP 区烧录一版能跑的测试程序。我踩过一次最蠢的坑Keil 里烧录程序时忘记把 Flash Download 起始地址改成 0x08004000结果 APP 还是烧到 0x08000000把 Bootloader 覆盖了设备直接变砖。5.2 串口中断“失灵”跳转成功了APP 跑起来了但串口收到数据不进入中断或者进了中断处理逻辑混乱。十有八九是SCB-VTOR设置得不够早。我遇到过一种更隐蔽的情况APP 用标准外设库初始化串口初始化函数内部会操作 NVIC如果 VTOR 是在这个初始化之后才设置的那这期间的任何中断都可能是去 Bootloader 的向量表找处理函数。这在高频率串口数据进来时很容易触发。排查手段在 APP 的最开始处加打印确认 VTOR 设置的时间点再对比中断响应是否正常。5.3 升级写一半断电成砖升级最怕的就是这个场景——Flash 擦除了一些页新固件还没写完断电了。下次上电 Bootloader 一看没有完整的 APP直接跳转旧 APP结果旧 APP 已经被擦了一半。设备就成了砖。解决办法是引入备份区或者至少引入“固件完成标志”。备份区方案是把新固件先完整写入一个独立的备份区域全部接收校验通过后再一次性把旧 APP 区整体擦除、把新固件复制过去。这么做需要额外的 Flash 空间对 F103C8T6 这种 64KB 的芯片来说比较紧张所以我更推荐第二种方案在参数区维护两个标志——upgrade_pending和upgrade_done。每次升级完成后才写upgrade_doneBootloader 只认upgrade_done有效才跳转 APP。如果 APP 区已经损坏但upgrade_done无效Bootloader 就停留在升级模式等待重传固件至少设备的 Bootloader 还在不会彻底变砖。5.4 官方例程移植后无法通过校验如果你参考 X-CUBE-IAP 的 UART 例程下载之后改了 PHUProgram Hardware Unit的参数或者换了自己的协议结果老是在 CRC 校验这一步失败建议先排查两件事。第一确认协议里 CRC 计算的覆盖范围和你上位机一致是从命令字开始还是从帧头开始。第二确认字节序CRC16 是高字节先发还是低字节先发两端不一致就会导致每个包都失败。这两个问题我在第一次移植官方例程时全踩过调了整整一个晚上才找到原因。5.5 一些调试工具和手段最后分享几个调试 IAP 的顺手工具。串口助手只是入门真正好用的还是能自定义协议的工具推荐 SerialPort Utility 这类支持脚本的串口调试软件可以按帧格式自动组帧、自动校验、循环发送。再加上一个逻辑分析仪用来抓串口波形判断是发送端问题还是接收端问题基本能覆盖绝大多数调试场景。如果嫌上位机开发麻烦可以先用 Python 写一个几十行的脚本模拟上位机发送固件还能顺便做自动化测试连续升级 50 次、断电测试、随机丢帧测试。这一步做好产品交付前的可靠性心里就有底了。我自己的习惯是把 IAP 升级逻辑拆成两个部分Bootloader 只做“收固件-写 Flash-跳转”这一件事APP 里则负责“查询版本-请求升级-确认结果”。这么拆之后Bootloader 可以单独测试APP 升级逻辑也方便替换两个模块各管各的耦合度降到最低。另外还有一个建议别等到产品做完了再开始设计 IAP最好在硬件设计阶段就把升级用的通信接口引出来把 Flash 分区画好把 Bootloader 和 APP 的地址约定写进项目文档。有很多项目是转产了才发现没预留升级通道那后面改起来就是脱层皮的事。做 IAP 没有太多玄学核心就是三件事分区要清晰、流程要容错、跳转要干净。把这三点守住你的 STM32F10x 产品就能真正实现“永不拆机”的远程升级体验。本文还有配套的精品资源点击获取