STM32F103 IAP升级实战:官方AN2557例程拆解与避坑指南
2026/9/11 4:53:29 网站建设 项目流程

简介:STM32F10x系列官方IAP升级程序面向嵌入式开发与维护人员,提供基于Cortex-M3内核微控制器的在应用编程方案,用户无需外部编程器,即可通过串口完成固件更新与远程维护。压缩包共350个文件,大小约1.63MB,以C/H源文件、汇编启动文件、Keil与IAR工程文件及链接脚本为主,覆盖从启动代码到应用层的主要模块,方便直接对照工程进行二次开发。程序支持Ymodem协议传输固件,可读取现有闪存内容,并配合中断向量表重定位、闪存分区等机制,为升级过程提供错误检测与稳定保障。工程包内包含AN2557相关参考配置,适合正在搭建IAP引导程序或需要处理固件升级流程的开发者参考。该方案的常见使用场景包括产品量产时的固件灌装、设备现场升级与远程维护,开发者可从中获得完整的串口IAP实现思路。目前已有622人学习下载,适合熟悉STM32基础开发、希望进阶掌握Bootloader与固件更新技术的读者。 做单片机开发的人,迟早要面对一个问题:设备已经发出去了,客户现场说程序要加功能,怎么办?派人出差刷写固件,成本高得离谱;让客户自己拿调试器烧,客户不会也不放心。这时候,Stm32F10x系列官方IAP升级程序就是最经典的解法。IAP(In-Application Programming)让设备通过串口、CAN、USB这些现成接口,在运行状态下完成固件自升级,不用拆机,不用调试器,整个流程对操作人员的要求也极低。这篇文章我就拿STM32F103这个最普及的型号开刀,把官方AN2557例程的工程结构、Flash分区、中断向量表、YMODEM协议这些核心东西全部拆开讲清楚,再附上我自己移植和量产过程中踩过的坑。适合刚接触IAP的嵌入式新人,也适合正在把IAP往产品里落地的老手参考。

1. 项目价值拆解:为什么盯着官方IAP例程不放

1.1 IAP到底解决什么问题:现场升级不再拆机

IAP的核心思想是在设备里烧两段程序:一段是Bootloader,负责接收升级数据并写入Flash;一段是真正的App应用代码,负责干实际业务。设备上电后先跑Bootloader,判断有没有升级需求,没有就直接跳转到App执行。以后要升级,只需要把新固件通过串口发过去,重启之后设备就跑新程序了。

我见过不少团队一开始自己写升级逻辑,结果各种翻车:有的把Flash擦写时序搞错,有的忘了处理中断向量表,升级一次死一次。其实ST官方早就把整套方案做成了一对现成的例程,配套应用笔记AN2557,代码量不大但该有的环节全都覆盖了。对绝大多数产品来说,与其自己从零写,不如先在官方例程基础上跑通,再按产品需求做裁剪和加固。

1.2 “官方”二字的价值:文档、协议、代码都能兜底

官方例程最大的优势不是代码写得有多华丽,而是它把“正确性”替你验证过了。ST给的这套IAP方案默认走USART1,使用YMODEM协议做文件传输,Flash写入、擦除、跳转逻辑全部基于标准外设库实现。YMODEM是个带CRC校验的文件传输协议,能传文件名、文件大小,还支持128字节和1024字节两种包长,比裸传裸收要可靠得多。

另外这套例程对STM32F10x全系列(F103、F105、F107等)都做了兼容,不同容量的Flash扇区大小也仔细处理过。你不需要自己翻几百页参考手册去看扇区结构,代码里已经替你整理好了。这一点在量产项目中非常值钱,因为代码出错可以改,硬件上电变砖的代价可没人愿意承担。

1.3 从标准外设库到Cube生态:这一套还值得学吗

我估计有人会问:ST现在主推的是STM32CubeMX加HAL库,标准外设库都停止维护了,学这套老例程还有意义吗?我的看法是:对F103这个型号来说,这套例程仍然是绕不开的参考。Cube生态里对应的东西叫X-CUBE-IAP,是个基于HAL库的IAP扩展包,支持F4、F7、L4这些后来的型号,你下载X-CUBE-IAP之后可以找到不少新的示例工程。但对F1系列,很多人还在用标准外设库维护存量产品,AN2557这套例程的协议流程、Flash操作逻辑、跳转细节,依然值得逐行读一遍。

更重要的是,你如果理解了这套老代码里的原理,再去看X-CUBE-IAP会轻松很多。IAP这个东西的难点从来不在哪个库,而在分区规划、中断向量表、通信协议稳定性和异常兜底,这些知识是通用的。

2. 核心原理:看懂这三个点,IAP就通了一半

2.1 Flash分区:给Bootloader和App划好地盘

STM32F103的Flash从0x08000000开始,容量根据型号从16KB到512KB不等。做IAP第一件事就是把这片空间切成几个区,常见分区是Bootloader区、App区和预留数据区。Bootloader区放升级程序本身,App区放业务代码,中间最好留出一点空间不分配,防止边界溢出互相踩踏。

我举个例子,假设芯片是STM32F103ZET6,512KB Flash,Flash起始0x08000000。Bootloader一般给16KB到32KB就足够了,因为升级逻辑本身不复杂,加上协议和串口驱动也就几KB。App区从0x08004000(偏移16KB)或0x08008000(偏移32KB)开始,剩下的空间全部给App。如果还想做“先下载到暂存区,校验通过后再搬移”的安全策略,还得专门留一个备份区。

分区起始地址大小用途
Bootloader0x0800000016KB~32KB引导、接收升级包、Flash写入
App0x08004000或0x08008000剩余Flash的前大部分业务应用代码
备份区/参数区Flash末尾可选暂存升级包、保存版本号等参数

分区大小这里面有个细节:STM32F10x不同容量的Flash扇区大小不一样。小容量和中容量的扇区是1KB,大容量和互联型是2KB。擦除只能按扇区整块擦,所以App区和Bootloader区的边界最好对齐到扇区边界,否则你擦一个扇区会把邻居的代码也擦掉。官方例程在flash_if.c里放了一个扇区大小表,不同型号会自动匹配,这是它想得周到的地方。

2.2 中断向量表重映射:让App“认住”新家

这是IAP最容易出事的地方,我当年第一次移植就被这个坑搞到怀疑人生。Cortex-M3内核的中断机制是:每次中断响应,CPU都去地址0x00000000区域读取中断向量表,找到对应的中断处理函数入口。在STM32F10x上,0x00000000默认映射的是主Flash,也就是0x08000000。

问题来了:如果App编译时设置起始地址为0x08004000,它的中断向量表也编译到了0x08004000,但CPU发生中断时还是去0x08000000找向量表。这个地址放的是Bootloader的向量表,里面根本没有App的中断处理函数地址,结果就是中断一触发,程序直接跑飞或者进HardFault。

解决办法是在App启动早期把向量表偏移到App区。标准外设库的misc.c里提供了NVIC_SetVectorTable函数,本质是操作SCB->VTOR寄存器。在实际的F103工程里,我一般直接在SystemInit或main最开头写上:

#define APPLICATION_ADDR 0x08004000 SCB->VTOR = APPLICATION_ADDR;

这就是把CPU的中断向量表从默认的0x08000000指向App区。需要提醒的是,F1是老内核,早期某些A版、B版芯片对VTOR的支持不完美,如果遇到设置VTOR仍然不起作用的情况,就得考虑在Bootloader里做中断转发,或者干脆把App设计成尽量少用中断。不过我在后期C版芯片上实测,SCB->VTOR写法是稳定有效的。

2.3 升级流程设计与YMODEM协议:数据怎么安全地流进Flash

官方AN2557的升级流程总体分四步:Bootloader启动后初始化串口并主动发送字符C等待握手;上位机用支持YMODEM的软件打开文件并发送;Bootloader逐包接收、校验CRC、擦写Flash;全部收完后跳转App。

这套流程里YMODEM协议承担了“可靠传输”的职责。它把一个文件拆成128字节或1024字节的块,每块都带块编号、块编号反码和CRC校验。接收方收到一个包后先验证块编号连续,再算CRC,任何一个环节不对就回复NAK,要求发送方重传。YMODEM还在正式数据之前先传一个包含文件名和文件大小的头部信息包,Bootloader可以利用这包信息判断固件大小是否超过App区容量,提前拦截错误升级。

官方跳转函数的实现很经典,直接看代码:

typedef void (*pFunction)(void); void Jump_To_Application(uint32_t app_addr) { uint32_t JumpAddress; pFunction Jump_To_Application; /* 检查栈顶指针是否落在SRAM区间,防止App区为空导致跳飞 */ if (((*(__IO uint32_t*)app_addr) & 0x2FFE0000) == 0x20000000) { JumpAddress = *(__IO uint32_t*)(app_addr + 4); Jump_To_Application = (pFunction)JumpAddress; /* 先关闭全局中断,再设置MSP栈指针 */ __disable_irq(); __set_MSP(*(__IO uint32_t*)app_addr); Jump_To_Application(); } }

跳转前的那个if判断很多人不理解,其实它是在检查App起始地址处的第一个4字节值。如果App区真有程序,这个位置存放的是初始栈顶指针,必然落在SRAM地址范围内(0x20000000开头);如果App区是空的或者数据无效,这个值就大概率不是合法的SRAM地址。这个判断能有效避免程序跳到一个空白区域然后跑飞,是官方例程里非常实用的一笔。

3. 实操:用官方例程搭建最小可用的IAP升级

3.1 官方工程结构梳理:先搞清楚每个文件的作用

从ST官网下载AN2557的配套例程后,解压看到的工程是以标准外设库为基础的多个子工程。核心文件其实就四个:common.c负责公共数据结构和串口收发辅助函数,iap.c是Flash擦写和跳转的主逻辑,ymodem.c是实现YMODEM协议收包和解包的协议栈,main.c是串口初始化和升级状态机的入口。

文件职责学习优先级
common.h / common.c文件信息结构体、串口字节收发
iap.h / iap.cFlash扇区擦除、写入、跳转App
ymodem.h / ymodem.cYMODEM协议处理、CRC校验、分包组包
main.c初始化、主循环状态机
flash_if.h / flash_if.c部分版本提供,Flash底层操作封装
stm32f10x_xx.c标准外设库外设驱动

很多人刚开始看ymodem.c会被变量名和状态分支搞得头晕,我的经验是先不要逐行抠协议代码,先把main.c的流程走通,再回头理解YMODEM的收包状态机,效率会高很多。协议本身只是工具,真正的核心是Flash操作和跳转逻辑。

3.2 关键代码逐段拆解:跳转、擦写、接收一个都不能少

看芯片手写Flash是这个例程最核心的动作。STM32F103的Flash写入前必须先解锁、擦除扇区,再按16位半字为单位写入。官方封装好的写函数思路非常清晰,最终调用库函数完成写入:

uint32_t FLASH_If_Write(uint32_t destination, uint8_t *p_source, uint32_t length) { uint32_t i; FLASH_Status status = FLASH_COMPLETE; FLASH_Unlock(); for (i = 0; (i < length) && (status == FLASH_COMPLETE); i += 2) { status = FLASH_ProgramHalfWord(destination, *(uint16_t *)p_source); destination += 2; p_source += 2; } FLASH_Lock(); return 0; }

写Flash有几个硬性规则:写入地址必须是偶数,写入数据长度必须是半字对齐,写入区域必须是已经擦除过的状态,否则结果不可预期。官方例程在接收YMODEM包之前会先把目标扇区全部擦掉,这是正确姿势,千万不要图省事跳过擦除。

再往深处讲,接收循环是在main.c里完成的。主循环里调用了YMODEM协议的接收函数,每拿到一批数据就通过FLASH_If_Write写入到App区地址。这里有个很多人忽略的小技巧:YMODEM的包大小是128字节或1024字节,但Flash最小擦除单位是扇区,所以最好把接收缓冲区大小设置为扇区大小的整数倍,避免每次收到一个包就频繁切换擦写状态,影响效率和Flash寿命。

整个升级完成后,上位机软件会发送一个EOT(结束符),YMODEM协议栈处理完EOT后会把文件的信息填入一个结构体里。Bootloader拿到这个结构体,确认文件长度和校验都没问题,就可以执行跳转了。官方代码里还给了一层CRC校验做确认,这是量产固件最后的一道保险。

3.3 App工程配合:修改链接地址和中断向量偏移

Bootloader单独能跑还不够,App工程那边也必须配合改两个地方,否则跳转过去还是白搭。第一处是链接脚本或者IDE里的ROM起始地址,第二处是中断向量表偏移。

在Keil MDK里,App工程的Options for Target中,Linker标签页里要把IROM1的Start地址从0x08000000改成0x08004000(对应你Bootloader的实际占用大小),Size相应改小。如果用的是STM32CubeIDE或GCC,就改链接脚本.icf或.ld文件里的Flash起始地址。这一步是告诉编译器,App的代码和只读数据都要放到偏移后的地址上。

改完链接地址,还要在App初始化代码里加上向量表重定向。最简单的办法是在SystemInit函数开始处直接调用标准外设库的NVIC_SetVectorTable:

NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x4000);

第二个参数就是App区相对Flash起始地址的偏移量。如果你的芯片版本对VTOR支持不友好,就要考虑把向量表复制到SRAM,再用SYSCFG重映射到0x00000000,这样做兼容性更好但代码复杂度更高。基础产品建议先试SCB->VTOR方案,跑不通再上SRAM映射或者中断跳板。

还有一件事容易被忽略:Bootloader跳转前要关闭自己用到的外设时钟和中断,别把一堆打开的串口、定时器状态带给App。官方例程的跳转函数里虽然只做了关中断和重设MSP,我的习惯是在跳转前把用到的串口显式去初始化,把外设中断标志位全部清掉,确保App起来时外设是干净状态。

4. 常见问题与排查经验

4.1 跳转后死机:先查向量表,再查时钟和外设

跳转后死机是IAP最典型的问题,我在无数论坛帖子里看到过这种求助,最后九成都是向量表偏移没设置对。判断方法很简单:把App单独直接烧到0x08000000地址,如果App能正常运行,说明代码自身没问题,问题出在Bootloader跳转或者向量表偏移上。

排查顺序我一般是这样:先确认App链接地址和Bootloader分区边界是否一致,再确认App里是否确实执行了SCB->VTOR偏移,最后检查Bootloader跳转时是否关闭了外设中断。另外,如果Bootloader里用了操作系统或者复杂的定时器,跳转前光关全局中断还不够,一定要把SysTick和NVIC里挂的异常入口都清理干净。

4.2 升级中途变砖:分区策略和校验兜底

官方例程的升级策略是直接往App区写,这有一个隐患:如果升级到一半串线断了、断电了,App区已经被擦掉一部分,设备就变砖了。官方代码在演示场景没问题,但量产产品不能这样裸奔。

我的做法是把Flash多分一个备份区,升级包先完整下载到备份区,接收过程中每个包都做CRC校验,全部接收完成后再对整体做校验,校验通过才把备份区的内容整体搬到App区。代价是App可用空间会少了备份区那一块,换来的是“升级失败设备还能接着跑老程序”的安全体验。如果硬件允许,也可以用外部Flash存升级包,逻辑一样。

4.3 YMODEM传输异常:115200只是看起来稳

不少人复现官方例程时发现,用SecureCRT或XShell发送文件,有时候传输到一半就卡住,或者提示CRC错误。这种问题绝大多数不是协议本身的问题,而是上位机软件没有选对YMODEM模式。一定要确认发送端选的是YMODEM而不是XMODEM或ZMODEM,三者的帧格式和握手方式差异很大。

波特率方面,官方默认115200,但具体能不能稳定跑还得看你板子的晶振精度和串口线质量。ST-Link的虚拟串口一般没问题,但一些PL2303、CH340山寨线在高波特率下丢包率很高。我一般量产产品用115200,如果发现丢包严重就降一档到57600或38400,IAP本来就不是追求速度的场景,稳定压倒一切。

问题现象常见原因解决办法
发送文件后Bootloader无反应握手字符C发送时序不匹配确认上位机选YMODEM,先手动发送一次字符C
传一半卡死串口丢包或USB转串口线质量问题降低波特率、换线,检查接线距离
CRC错误频繁波特率误差大、数据线干扰用外置晶振,检查地线,降低波特率
接收完成后跳转死机向量表偏移未设置App内加SCB->VTOR,确认链接地址
升级失败重启跑不了老程序直接写App区,无备份策略增加备份分区,先校验后搬移

4.4 从例程迁移到产品时要注意的几件事

官方例程本身就只是“最小可用”的参考实现,离产品还有几段路要走。第一,串口协议要加固,YMODEM虽然自带CRC,但最好在应用层再加一包版本信息和固件大小校验,防止客户传错文件。第二,固件版本管理要做起来,Flash里存一个版本号,Bootloader升级时判断新版本是否比当前版本新,太老的要拒绝。第三,升级入口要设计好,常见做法是App运行中收到升级指令,设置一个标志位到备份寄存器或Flash,然后软复位进Bootloader,而不是每次上电都傻等几秒让Bootloader超时再跳App,后者在开机速度敏感的产品上体验很差。

最后再补充一点个人经验:我们在一款F103的仪表产品上用了这套方案,最初也是直接在官方例程上改,后来发现量产时有两台设备升级完卡死。排查到最后,原因是那批芯片的Flash扇区边界和我在代码里写的扇区表不完全一致,因为Bootloader容量占得比较小,App区的起始扇区算错了一位。从那以后我给自己定了个规矩:开发阶段一定先把扇区表用调试器读出来核对一遍,不要盲目相信记忆里的编号。IAP这种功能,平时用不上,一用就是远程救命的通道,值得把每一处细节都较真到底。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询