公司动态

STM32串口IAP远程升级:Ymodem协议与Bootloader实现全解析

📅 2026/9/3 1:52:22
STM32串口IAP远程升级:Ymodem协议与Bootloader实现全解析
简介面向嵌入式开发者这份资料围绕Ymodem协议串口IAP远程升级完整覆盖STM32F103C8T6与STM32F407ZGT6两个平台包含IAP bootloader与对应APP源码。F103侧配有呼吸灯和亮暗灯两个点灯程序F407侧配有点灯APP均已通过SecureCRT上位机以Ymodem方式发送bin文件完成实测升级可直接还原整个远程升级链路。代码由作者结合个人思路整理而成部分来自移植部分为手写适合刚接触固件远程升级、想快速搭建IAP工程的开发者学习也可按项目需求裁剪使用。压缩包共1103个文件、约40.34MB内容以C/H源码、uvprojx/uvoptx工程文件、axf/hex/bin固件及bat辅助脚本为主同时包含调试日志与备份文件工程结构完整便于在Keil MDK中打开、编译和烧录验证。目前已有1641人浏览学习对理解IAP跳转、APP地址配置、串口Ymodem传输机制有直接参考价值是固件远程升级方向较为实用的参考资料。 很多产品做出来以后最头疼的一件事就是设备铺到现场之后想改个bug或者加个功能只拎着烧录器到处跑。这次项目做的是基于STM32F103C8T6和STM32F407ZGT6两个平台的Ymodem串口IAP远程升级把Bootloader刷进芯片以后后续固件更新走串口就行不用开壳接调试器。全文会从实际需求出发把协议选型、Flash分区、Bootloader工程搭建、App端配合改造以及我在调试过程中踩过的各种坑一整套讲清楚给正在做IAP、或者想在同一套代码里兼容多个STM32型号的工程师做个参考。IAPIn-Application Programming说白了就是芯片里先放一段独立的Boot程序上电先跑BootBoot通过某种通信接口拿到新的App固件写进Flash最后跳转执行。这样产品在现场就能通过串口、CAN、网口甚至无线通道完成固件更新。这个项目选择串口加Ymodem协议原因很直接串口普及率最高几乎每个产品都会引出UART口Ymodem协议本身是成熟的面向文件传输的协议有校验、有重传、带文件名和大小信息非常适合做固件传输。先说明一点F103C8T6和F407ZGT6一个是Cortex-M3内核一个是带FPU的Cortex-M4IAP实现逻辑上大部分共通差异主要在Flash扇区、向量表重映射方式、启动文件和时钟配置这些细节。后面每一章我都会把两个平台的不同点单独拎出来讲。1. 整体方案为什么这样选1.1 为什么选Ymodem而不是Xmodem或自造协议有人可能觉得串口传固件而已自己定个协议不就行了比如一帧数据带长度、带CRC、带帧号写完Flash回个ACK。这个思路没问题短距离调试、量小的时候确实够用。但一旦产品需要批量交付、多人维护或者要对接不同上位机工具自造协议的问题就出来了上位机得从头写校验策略容易漏断点续传、包长协商这些功能都得自己折腾。Ymodem是从Xmodem演化来的最大改进是支持一次传多个文件并且在文件传输前会先发送文件名和文件大小信息。对IAP来说文件名和大小这个头信息特别重要。比如升级包叫app_f407.bin大小是145820字节Ymodem第一帧就会把这些信息告诉接收端Bootloader拿到后可以校验文件名后缀、检查剩余Flash空间够不够、估算传输时间。这些在Xmodem里统统做不到。再看可靠性设计Ymodem几乎是为半双工通信量身定做的。每发一帧数据接收端都要回ACK或NAK发送端只有在收到NAK或超时后才重传。CRC校验默认用CRC16出错概率已经很低。实际用普通USB转串口线在车间环境测试也很少出现固件写坏、升完级设备变砖的情况。1.2 串口IAP配合点先收整包再写FlashIAP有两个关键动作接收固件、写入Flash。这两个动作放在一起很容易出问题因为串口按字节来Flash按页擦除、按半字或字写入。如果边收边写一次只收1个字节就去写Flash整个流程会非常慢而且擦除Flash期间CPU被占用串口数据来不及收就丢了。Ymodem的设计刚好解决了这个问题。它固定按128字节或1024字节打包这个包大小正好可以配合Flash编程单位。F103C8T6的Flash是64KB页大小1KBF407ZGT6的Flash是1MB但扇区大小从16KB到128KB不等。如果使用1024字节的Ymodem包收满整包再写效率会高很多。更合理的是做一个环形缓冲区串口中断或DMA把数据搬进来主循环提取Ymodem帧整包校验通过后调用Flash写函数。这样协议解析和Flash擦写完全解耦。1.3 F103C8T6和F407ZGT6的IAP差异对比这两个芯片不能只在Flash容量上看差异。F103C8T6主频72MHzFlash只有64KB如果Bootloader占用8KB剩余56KB给AppF407ZGT6主频168MHzFlash有1MBBootloader可以留大一点比如编译出来几十KB也不心疼。更关键的是Flash扇区结构不同对比项STM32F103C8T6STM32F407ZGT6内核Cortex-M3Cortex-M4F主频72MHz168MHzFlash容量64KB1MBFlash最小擦除单位1KB页16KB扇区起步SRAM20KB192KB向量表重映射方式复制到SRAM设置SYSCFG_MEMRMP直接写VTOR寄存器向量表这行值得单独解释。F407有专门的向量表偏移寄存器VTOR写一个地址就能把中断向量表挪到App区。F103没有VTOR需要先关闭中断、把SRAM里的向量表复制过去再设置SYSCFG_MEMRMP寄存器把内存映射重定向到SRAM操作上比F407麻烦一点。这两行差异直接影响了代码结构F103和F407的Flash驱动函数是两套跳转App前的向量表重映射代码也是两套。如果项目还有别的型号务必在写码之前把这些差异点抽象出来。2. Ymodem协议核心拆解格式、流程、CRC162.1 帧格式与必须记住的控制字符Ymodem协议里单帧数据最核心的格式是SOH或STX开头、帧序号、帧序号取反、数据块、CRC16校验码。SOH对应128字节的数据块STX对应1024字节的数据块。传输第一个帧文件名帧固定用128字节后续数据帧根据接收端的建议包长决定常见是1024字节。协议里几个控制字符必须记住SOH0x01128字节帧头STX0x021024字节帧头EOT0x04文件传输结束ACK0x06确认NAK0x15否认、请求重发CAN0x18取消传输C0x43要求发送端开始发送ASCII里就是大写字母C如果手头没有专用工具用超级终端或者SecureCRT也能凑合跑Ymodem。但从实际体验来说强烈建议用一个能显示接收日志的串口调试助手把波特率、包长、校验开关都暴露出来。调试IAP时能看到每一帧的交互过程能省下大量时间。2.2 一次完整传输流程和“双EOT”处理整个Ymodem传输过程大致是这样接收端Bootloader进入接收模式后每隔一段时间发送一个字符C表示准备好接收。发送端收到C以后先发送一个文件名帧帧序号0128字节数据部分前面是文件名和文件大小不足128字节用0x00填充。接收端收到文件名帧后如果校验通过回ACK然后继续发C。发送端收到ACK加C后开始发送数据帧。如果后续用1024字节包帧头是STX帧序号从1开始递增到255后回绕到0。接收端每收到一帧校验CRC16正确回ACK错误回NAK。文件发完后发送端发EOT接收端回ACK最后发送端再发一个空的文件名帧帧序号0只有文件名结束符0x00接收端回ACK整个传输结束。最容易踩坑的是第6步的“双EOT”流程。很多初学者实现的时候只处理一个EOT就结束结果发送端迟迟等不到下一个ACK整个流程卡死。我用Xshell和SecureCRT都遇到过这种细微差异最后统一按“收到EOT回ACK、再等多一个EOT或空帧”的方式处理兼容性就好很多。2.3 CRC16计算与查表法实现Ymodem帧尾的CRC16采用CRC-CCITT多项式0x1021初始值0x0000对数据区的所有字节进行计算不包含帧头、帧序号。实现CRC16的代码网上非常多我自己习惯用查表法。1024字节的数据用查表法计算在几十微秒到一百多微秒这个量级对168MHz的F407毫无压力。要注意的是有些串口工具发出来的Ymodem用1024字节块有些固定用128字节块接收端要同时支持SOH和STX两种帧头不要一看到STX就报错。3. Bootloader端完整实现分区、接收、擦写、跳转3.1 Flash分区怎么定F103和F407完全不同的思路写Bootloader之前第一件事就是定Flash分区。这个分区不能随便拍脑袋要结合App的编译地址、Bootloader自身代码大小、以及App后续会不会继续增大来综合考虑。对F103C8T6我是这样分的0x08000000 ~ 0x08001FFFBootloader区8KB0x08002000 ~ 0x0800FFFFApp区56KBApp起始地址就是0x08002000编译时要把IROM1的Start改成这个地址Size改成0xE000也就是56KB。8KB给Bootloader比较紧如果代码里加了完整Ymodem协议解析、Flash驱动、跳转逻辑开启-O2优化基本能放下但建议日志打印精简一点。如果还想加个简易命令行建议分到12KB或16KB更保险。对F407ZGT6Flash容量大分区可以宽松一些0x08000000 ~ 0x0800FFFFBootloader区64KB0x08010000 ~ 0x080FFFFFApp区960KBF407这里有个结构特点必须注意它的Flash扇区大小不均匀。从0x08000000开始前4个扇区每个16KB然后是1个64KB扇区再后面是7个128KB扇区。Bootloader正好占满前4个16KB扇区App从扇区464KB开始扇区边界和分区边界刚好对齐这是比较舒服的情况。如果想把Boot从64KB压缩到48KB、甚至32KBApp起始地址就会落在16KB扇区里擦除覆盖关系就会变得复杂。所以F407的分区设计建议让App起始地址对齐到至少16KB的扇区边界。3.2 串口环形缓冲区与Ymodem接收状态机串口这边建议用中断加环形缓冲区不推荐在Ymodem解析代码里直接阻塞读串口。阻塞读会卡住整个流程一旦发送端因为超时重传接收端的超时处理会变得很复杂。环形缓冲区用一个简单的结构体#define RX_BUF_SIZE 2048 typedef struct { uint8_t buf[RX_BUF_SIZE]; volatile uint16_t head; volatile uint16_t tail; } ring_buf_t;串口中断里只做一件事把收到的字节写入环形缓冲区更新head指针。主循环里Ymodem状态机从环形缓冲区取字节解析帧头、帧序号、CRC、数据块。这样收数据和协议解析完全解耦即使主循环里要花时间擦除Flash也不会因为没来得及读串口寄存器而丢数据因为数据已经在环形缓冲区里了。Ymodem接收状态机一般至少有这几个状态等待开始、接收文件名帧、接收数据帧、等待EOT、结束。我习惯用枚举定义每个状态处理对应的控制字符收到错误帧回NAK。实测下来这个状态机跑在72MHz的F103上也完全流畅瓶颈只在Flash擦写时间上。3.3 边收边写Flash的正确姿势接收写Flash的流程建议“先收帧、后擦写”。每收满一个数据帧并且CRC校验通过后立刻把这帧数据一次性写入当前Flash地址然后地址累加等待下一帧。对1024字节一帧来说这个操作在F407上可能涉及擦除一个16KB或64KB扇区耗时为毫秒级而F103按1KB页擦除则快得多。F103的页擦除和编程使用标准库可以这样void f103_flash_write(uint32_t addr, uint8_t *data, uint16_t len) { FLASH_Unlock(); FLASH_ErasePage(addr); for (uint16_t i 0; i len; i 2) { uint16_t tmp data[i] | (data[i 1] 8); FLASH_ProgramHalfWord(addr i, tmp); } FLASH_Lock(); }这里要特别提醒F103的Flash编程是半字16位编程不是字节编程必须对齐。如果Ymodem帧是128字节数据长度是偶数没问题如果长度是奇数最后一字节要补0处理否则写Flash会触发HardFault。F407的写法类似但API换成了HAL库的FLASH_Program参数按64位数据进行。擦除策略上有一个优化点并不是每一帧都要擦除扇区。擦除操作只发生在当前写入地址跨越了新的扇区边界时。如果判断当前帧数据还在已擦除的扇区里就可以直接跳过擦除只做Program操作能显著缩短整包升级时间。3.4 跳转App前必须做的合法性检查Bootloader写完固件以后需要跳转到App。跳转的核心操作是typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t jump_addr *(volatile uint32_t *)(app_addr 4); pFunction jump (pFunction)jump_addr; __disable_irq(); SCB-VTOR app_addr; // F4/F7必须设置VTOR __set_MSP(*(volatile uint32_t *)app_addr); jump(); }跳转前一定要检查App区的起始地址是不是合法的栈指针值。简单检查就是确认首字的值在SRAM范围内比如F103是0x20000000到0x20004FFFF407是0x20000000到0x2002FFFF。如果首字不在这个范围说明Flash里根本没有有效固件跳过去必死。我第一次写的时候没加这个检查擦除完Flash、还没写完固件就断电再上电Bootloader直接跳到一个全是0xFF的地址结果HardFault。从App跳回Bootloader的方法也很简单把一个固定标志位写到一个不会被编译器优化掉的全局变量然后软件复位。Bootloader启动时检查这个标志等于预设值就进入Ymodem接收流程否则直接跳App。建议用SRAM末尾地址或备份寄存器来存标志不要用普通全局变量因为普通全局变量在软件复位后可能被启动代码清零。4. App端配合改动与双平台代码复用4.1 向量表重映射F103和F407两种写法App端第一步要做的就是把中断向量表从0x08000000改到App的实际起始地址。F407直接在main函数最开始设置VTORSCB-VTOR 0x08010000;F103没有VTOR需要把向量表复制到SRAM再设置SYSCFG_MEMRMP重映射#define APP_VECTOR_TABLE 0x20000000 void f103_vector_table_reinit(void) { memcpy((void *)APP_VECTOR_TABLE, (void *)0x08002000, 0x100); SYSCFG-MEMRMP SYSCFG_MEMRMP_MEM_MODE_0; }这里有个细节复制向量表后__set_MSP不需要改因为F103的启动代码已经在启动文件里把SP设置好了。但如果你从Bootloader跳过来时设置了MSP为App首字理论上也没问题。重点是要保证所有用到的中断向量都从SRAM里取复制大小建议覆盖项目实际用到的最大中断号。0x40只覆盖前16个如果用了多个外设中断建议复制到0x100甚至更多。4.2 Keil链接地址设置与启动文件注意点App工程的链接地址必须改成App分区起始地址。Keil里就是Options for Target - Target - IROM1的Start和Size。F103改0x08002000、Size 0xE000F407改0x08010000、Size 0xF0000。注意ROM起始地址不要覆盖Bootloader。容易漏的地方是启动文件里的中断向量表。启动文件里定义的向量表位置是由链接脚本决定的如果你只改了链接地址但没处理重映射跳过去以后中断来了会跑飞。F4通过VTOR解决F1通过MEMRMP解决但App启动时要先知道自己实际运行地址再处理中断。另外如果App代码里用到OTA标志位、版本号等参数建议放在固定Flash地址比如App区末尾。Bootloader升级前可以读旧版本号校验新版本号避免把旧固件反复刷回设备。这在量产维护时非常有用。4.3 Ymodem协议库抽取与多型号复用方式写了两套代码以后最痛苦的是维护两份相似的逻辑。我最后把公共部分抽成一个Ymodem协议解析库只依赖外界提供三个函数串口读一个字节、写一个字节、写一帧数据到Flash。三个函数在F103和F407上各自实现协议库完全不用动。再配合#if defined(STM32F103xx)和#if defined(STM32F407xx)宏来区分芯片编译时自动选取对应Flash驱动和向量表重映射代码。新加一个型号只要实现底层三个接口再处理该型号特有的Flash分区和擦除逻辑剩下协议处理全部复用。这个抽象还有一个隐形好处如果想把串口换成CAN或者以太网只需要把底层三个函数换成CAN或网络驱动Ymodem协议解析部分不用改。项目二期如果要支持蓝牙升级也只需要增加一个蓝牙驱动的适配层。5. 实战问题排查与调试技巧记录5.1 一直收不到C字符先查串口通路最常见的问题就是Bootloader发了C上位机Ymodem发送端却完全没反应。排查思路分三段串口驱动、Bootloader发送逻辑、上位机工具配置。实际遇到的情况里大多数是USB转串口线驱动没装好或者串口号被其他程序占用。本文还有配套的精品资源点击获取