STM32 OTA升级实战:BootLoader与Ymodem实现详解 📅 发布时间:2026/9/20 9:47:17 👁 浏览次数: 1. 为什么STM32产品必须要有OTA能力做过量产项目的兄弟都清楚产品一旦装到现场哪怕只是一个LED闪烁逻辑写错了想改就得派人跑一趟。我之前做过一个工业采集终端装在郊区的一个配电房里为了改一个Modbus寄存器的地址映射来回打车花了三百多改代码只用了五分钟。从那次以后我给自己定了个规矩凡是带MCU的产品只要硬件上留了通信口就必须把OTA功能做进去。STM32的OTA说白了就是让芯片自己给自己换程序。核心思路不复杂把Flash分成两块一块放BootLoader一块放应用程序。BootLoader永远不动它负责检查有没有新固件、要不要搬移、搬完了跳不跳过去。应用程序就是你的业务代码它可以通过串口、CAN、4G模组、甚至USB把新固件收下来写到一个暂存区然后通知BootLoader下次启动时执行升级。这里面有几个关键词需要先理清楚。IAP全称In-Application Programming指的是程序在运行过程中对Flash进行擦写的能力。STM32的Flash控制器允许你在代码里调用擦除和写入函数这就是IAP的硬件基础。BootLoader是一段特殊的程序它通常放在Flash的最前面上电后最先运行负责判断是跳转到应用程序还是进入升级模式。Ymodem是一种文件传输协议它把固件按1024字节分块每块带CRC校验接收方确认后才发下一块非常适合串口这种容易丢数据的场景。适合看这篇内容的人我大致分三类。第一类是刚接触STM32不久听说过OTA但不知道从哪下手的第二类是做过IAP但踩过坑比如中断向量表没偏移导致跳转后死机的第三类是做产品方案的需要评估OTA方案选型比如到底用双区备份还是单区升级。不管你是哪一类下面的内容都会从原理到代码、从分区设计到Ymodem协议细节一步步拆开讲。2. Flash分区设计与BootLoader方案选型2.1 分区方案的核心考量STM32的Flash分区不是随便切的切错了要么空间不够要么升级过程中断电直接变砖。我见过最离谱的一个项目BootLoader和应用程序共用了一个扇区结果擦除应用程序的时候把BootLoader也擦掉了芯片直接报废。所以第一步必须把BootLoader和应用程序放在不同的Flash扇区里。以STM32F103C8T6为例它的Flash总共64KB扇区大小是1KB一页。我的建议是BootLoader占前16KB应用程序从0x08004000开始剩下的48KB给应用程序用。为什么是16KB因为BootLoader里要放Ymodem协议解析、Flash擦写驱动、串口驱动再加上一些状态机逻辑16KB是比较宽裕的。如果你的BootLoader还要支持CAN或者USB那可能需要32KB。对于STM32F407这种大容量芯片Flash有1MB扇区大小不均匀前四个扇区是16KB后面是64KB和128KB。这时候分区就要更小心。我通常把BootLoader放在第一个16KB扇区应用程序从0x08004000开始然后在Flash末尾留一个64KB的扇区作为固件暂存区。暂存区的作用是应用程序收到新固件后先写到暂存区校验通过后再由BootLoader搬移到应用程序区。这样即使搬移过程中断电原来的应用程序还在不会变砖。2.2 双区备份与单区升级的取舍双区备份的方案是Flash里有两个应用程序区一个运行区一个备份区。升级时新固件写到备份区BootLoader负责把备份区的内容搬到运行区。这种方案的好处是升级失败可以回滚坏处是Flash空间要翻倍。对于F103C8T6这种只有64KB的芯片双区备份基本不现实。单区升级的方案是只有一个应用程序区新固件先写到暂存区校验通过后BootLoader擦除应用程序区再把暂存区的内容搬过去。这种方案节省空间但搬移过程中断电会导致应用程序区不完整。为了解决这个问题可以在BootLoader里加一个标志位搬移前先写一个“升级中”的标志搬移完成后清除。如果BootLoader启动时发现这个标志还在说明上次搬移没完成就重新从暂存区搬一次。我个人的选择是Flash小于128KB的芯片用单区升级加暂存区Flash大于256KB的芯片用双区备份。这个分界线不是绝对的主要看你的固件大小和成本敏感度。2.3 BootLoader的启动流程设计BootLoader的启动流程决定了整个OTA的可靠性。我设计的流程是这样的上电后先初始化时钟和串口然后检查一个特定的RAM地址或者Flash标志位判断是否需要进入升级模式。这个标志位通常是一个魔术数字比如0x5A5A5A5A应用程序在需要升级时把这个数字写到备份寄存器或者Flash的特定位置然后软复位。BootLoader读到这个数字就进入Ymodem接收模式等待上位机发送固件。如果不需要升级BootLoader就检查应用程序区的栈顶地址是否合法。STM32的应用程序区第一个字是栈顶地址第二个字是复位向量。合法的栈顶地址应该在RAM范围内比如0x20000000到0x20005000之间。如果这个地址不合法说明应用程序区是空的或者损坏了BootLoader就自动进入升级模式等待上位机发送固件。跳转到应用程序之前BootLoader必须做三件事关闭所有中断设置主栈指针为应用程序的栈顶地址设置向量表偏移寄存器SCB-VTOR为应用程序的起始地址。这三件事缺一不可尤其是VTOR不设置的话中断会跳到BootLoader的向量表导致程序跑飞。3. Ymodem协议在STM32上的实现细节3.1 Ymodem的帧格式与握手过程Ymodem协议是Xmodem的增强版支持1024字节的数据块和批量文件传输。它的帧格式是这样的起始字节SOH表示128字节数据块STX表示1024字节数据块然后是包序号、包序号的反码、数据区、CRC校验。接收方收到一帧后先检查包序号和反码是否匹配再检查CRC都通过后回复ACK否则回复NAK要求重发。握手过程是这样的接收方先发送字符C表示自己支持CRC校验。发送方收到C后发送第一个包这个包的数据区是文件名和文件大小比如firmware.bin\0加上文件长度。接收方收到后回复ACK和C表示文件名接收成功可以开始发数据了。发送方接着发送数据包每个包1024字节直到文件发完。文件发完后发送方发送一个全零的包表示结束接收方回复ACK。在STM32上实现Ymodem接收最关键的是超时处理。串口接收不能死等必须用状态机加超时计数器。我通常用1ms的定时器中断来递减超时计数器如果在规定时间内没有收到完整的包就回复NAK或者重新发送C。超时时间一般设为1秒太短了容易误判太长了升级速度慢。3.2 Flash擦写与数据缓冲Ymodem接收到的数据不能直接往应用程序区写因为应用程序区可能正在运行。正确的做法是先写到暂存区。暂存区的地址要提前规划好比如0x08010000。每收到一个1024字节的包先检查这个包对应的暂存区地址是否已经擦除。STM32的Flash擦除是按扇区的F103的扇区是1KBF407的扇区是16KB到128KB不等。所以收到数据后要先判断当前地址是否在扇区边界如果是就先擦除这个扇区。擦除和写入的时候要注意STM32的Flash在擦写期间不能执行代码所以擦写函数必须放在RAM里执行或者确保擦写期间没有中断。我通常的做法是在擦写前关闭总中断擦写完成后再打开。这样虽然会影响实时性但升级过程本身就不要求实时性安全第一。写入的时候要用半字写入也就是每次写16位。STM32的Flash控制器要求写入地址必须是半字对齐的写入的数据也是半字。如果Ymodem包的长度是奇数最后一个字节要特殊处理补一个0xFF凑成半字。3.3 CRC校验与断点续传Ymodem的CRC校验是16位的多项式是0x1021。STM32的硬件CRC单元不支持这个多项式所以要用软件查表法或者逐位计算。我通常用查表法提前生成一个256项的CRC表计算的时候查表加移位速度很快。每收到一个包先算CRC再和包里的CRC比较不匹配就回复NAK。断点续传是Ymodem的一个可选功能但实际项目中很有用。如果升级过程中串口断了重新连接后可以从上次断掉的地方继续传不用从头开始。实现方法是在暂存区里记录已经接收到的包序号重新握手时把这个序号告诉发送方发送方从下一个包开始发。不过这个功能需要上位机配合不是所有串口工具都支持。我常用的SecureCRT和Xshell都支持Ymodem但断点续传需要额外配置。4. 应用程序端的OTA触发与状态管理4.1 如何从应用程序跳转到BootLoader应用程序在运行过程中如果收到升级指令不能直接跳转到BootLoader因为BootLoader可能已经被擦除了。正确的做法是应用程序先把升级标志写到备份寄存器或者Flash的特定位置然后执行软复位。软复位后BootLoader先运行它读取标志位发现需要升级就进入Ymodem接收模式。写标志位的位置有讲究。STM32的备份寄存器BKP在掉电后由VBAT供电数据不会丢失但需要先使能PWR和BKP时钟。如果不想用BKP也可以写在Flash的最后一个扇区但要注意这个扇区不能被应用程序的擦除操作覆盖。我通常用BKP的DR1寄存器写一个魔术数字0x5A5ABootLoader启动时读这个寄存器如果是0x5A5A就进入升级模式否则跳转到应用程序。软复位的实现很简单调用NVIC_SystemReset()函数即可。但在复位之前要确保所有外设都已经关闭尤其是正在写Flash的操作。如果应用程序正在写Flash突然复位会导致Flash内容不完整。所以升级指令的处理要放在一个安全的状态下比如主循环的空闲时刻。4.2 升级过程中的状态反馈升级过程中用户最关心的是进度。如果没有任何反馈用户不知道是在升级还是死机了。所以应用程序在发送升级指令后应该通过串口或者LED指示升级状态。比如收到升级指令后LED快闪BootLoader进入接收模式后LED慢闪接收完成后LED常亮升级失败LED常灭。如果产品有显示屏可以在屏幕上显示进度条。进度信息可以从BootLoader通过串口发给应用程序但这时候应用程序已经不在运行了所以进度信息只能由BootLoader直接驱动显示外设。这就要求BootLoader里也要有显示驱动会增加BootLoader的体积。我的做法是BootLoader只控制一个LED进度用LED的闪烁频率表示简单可靠。4.3 升级失败的回滚机制升级失败的原因很多固件文件损坏、串口干扰、Flash擦写错误、断电。如果没有回滚机制设备就变砖了。回滚机制的核心是在搬移新固件之前先把旧的应用程序备份到另一个区域。如果搬移失败BootLoader重新把备份区的内容搬回来。对于单区升级的方案没有备份区回滚就无从谈起。这时候只能靠暂存区。如果搬移过程中断电BootLoader启动时发现“升级中”标志还在就重新从暂存区搬一次。但如果暂存区的数据也损坏了那就真的没救了。所以单区升级的方案必须在暂存区数据校验通过后才开始搬移搬移过程中不能擦除暂存区。我个人的经验是如果产品对可靠性要求极高比如医疗设备或者工业控制一定要用双区备份。如果只是消费类产品单区升级加暂存区就够了成本更低。5. 常见问题排查与实战避坑指南5.1 跳转后死机或HardFault这是IAP最经典的问题。应用程序跳转后第一条指令就HardFault或者运行几秒后死机。原因通常是三个栈顶地址不合法、向量表偏移没设置、中断没关闭。栈顶地址不合法的情况应用程序编译后第一个字是栈顶地址这个地址必须在RAM范围内。如果应用程序的链接脚本设置错了栈顶地址可能指向Flash或者外设区域跳转后一压栈就HardFault。检查方法是用调试器看应用程序区的第一个字确认它在0x20000000到0x20005000之间。向量表偏移没设置的情况STM32的中断向量表默认在0x08000000也就是BootLoader的向量表。应用程序跳转后如果发生了中断CPU会去BootLoader的向量表找中断服务函数结果跳到BootLoader的代码里行为不可预测。解决方法是在应用程序的main函数开头设置SCB-VTOR 应用程序起始地址。中断没关闭的情况BootLoader在跳转前如果还有中断使能跳转后中断可能立即触发但此时向量表还没切换就会跑飞。解决方法是在跳转前调用__disable_irq()关闭总中断跳转后再由应用程序重新使能。5.2 Ymodem传输中途失败Ymodem传输中途失败通常表现为进度条卡住、串口工具报超时、或者接收方一直回复NAK。原因可能是串口波特率太高、数据包太大、Flash擦写太慢。串口波特率太高的情况115200是比较稳妥的921600虽然快但对线材和干扰很敏感。如果传输距离超过一米建议降到57600。我实测过在115200下Ymodem传输1MB固件大约需要90秒这个速度对于大多数场景够用了。数据包太大的情况Ymodem支持1024字节的包但如果接收方的缓冲区只有512字节就会溢出。STM32的串口接收缓冲区要根据包大小来定至少要比包大小多一倍留出处理时间。我通常用2KB的环形缓冲区配合DMA接收这样CPU不用频繁中断。Flash擦写太慢的情况STM32的Flash擦除一个扇区需要20到40毫秒写入一个字需要几十微秒。如果Ymodem的发送方不等ACK就连续发接收方来不及擦写就会丢包。解决方法是接收方在擦写期间不回复ACK发送方自然会等待。或者接收方先回复ACK再擦写但这样如果擦写失败就没法补救了。我倾向于前者安全第一。5.3 升级后程序不运行升级完成后BootLoader跳转到应用程序但程序不运行或者运行的是旧程序。原因可能是搬移没完成、跳转地址不对、应用程序编译时设置了错误的起始地址。搬移没完成的情况BootLoader在搬移暂存区到应用程序区时如果中途断电搬移只完成了一部分。下次启动时BootLoader应该检测“升级中”标志如果标志还在就重新搬移。但如果标志被清除了BootLoader就会跳转到不完整的应用程序导致死机。跳转地址不对的情况应用程序的起始地址要和BootLoader里定义的地址一致。如果BootLoader里写的是0x08004000但应用程序编译时设置的起始地址是0x08005000跳转后就会跑飞。检查方法是对比BootLoader里的宏定义和应用程序的链接脚本。应用程序编译设置错误的情况Keil里要在Target选项卡设置IROM1的起始地址和大小IAR里要在Linker配置里设置。如果忘了改应用程序还是从0x08000000开始链接跳转后就会覆盖BootLoader。5.4 常见问题速查表问题现象可能原因排查方法解决方案跳转后HardFault栈顶地址不合法查看应用程序区第一个字修改链接脚本确保栈顶在RAM范围跳转后中断跑飞向量表偏移未设置检查SCB-VTOR的值在main开头设置VTORYmodem传输卡住串口波特率太高降低波特率测试降到115200或57600接收方一直NAKCRC校验失败检查CRC计算代码用查表法重新实现CRC升级后运行旧程序搬移未完成检查“升级中”标志重新搬移或恢复备份升级后不运行跳转地址不匹配对比BootLoader和应用程序的地址统一起始地址Flash擦写失败未解锁或未关闭中断检查FLASH-CR寄存器擦写前解锁关闭中断串口接收丢数据缓冲区太小增大环形缓冲区用DMA加2KB缓冲区6. 上位机工具选择与自动化升级思路6.1 常用上位机工具对比做STM32 OTA上位机工具的选择直接影响开发效率。我常用的有三个SecureCRT、Xshell、以及自己写的Python脚本。SecureCRT的Ymodem功能很稳定支持断点续传但它是收费的。Xshell免费版也支持Ymodem但断点续传需要专业版。如果只是开发阶段用这两个都够。如果是量产阶段需要自动化升级那就得自己写脚本。Python的ymodem库有不少开源实现比如ymodem和xmodem。用Python写升级脚本的好处是可以集成到生产测试流程里比如自动检测串口、自动发送固件、自动校验版本号。我写过一个脚本用pyserial加ymodem库配合一个简单的GUI产线工人只需要点一个按钮就能完成升级。6.2 自动化升级的流程设计自动化升级的流程是这样的上位机先发送一个握手命令比如“ATOTA”设备回复“OK”后进入升级模式。然后上位机用Ymodem发送固件文件发送完成后等待设备回复“SUCCESS”。如果超时或者收到“FAIL”就重试或者报警。这个流程的关键是握手指令的设计。握手指令要足够简单不能依赖复杂的协议解析因为此时应用程序可能已经处于不稳定状态。我通常用固定的字符串比如“OTA_START”设备收到后立即写升级标志并软复位。复位后BootLoader启动发送字符C上位机检测到C后开始Ymodem传输。自动化升级还要考虑版本管理。每次升级前上位机先读取设备的当前版本号和固件文件的版本号比较如果相同就跳过避免重复升级。版本号可以存在Flash的特定位置应用程序和BootLoader都能读取。6.3 量产阶段的注意事项量产阶段最怕的是升级过程中断电或者串口接触不良。我的做法是在产线上用带供电的USB转串口模块确保供电稳定串口线用带屏蔽的长度不超过50厘米升级前先擦除暂存区确保没有旧数据干扰升级后读取设备的版本号确认升级成功。另外量产阶段不建议用Ymodem因为速度太慢。如果产量大可以考虑用SD卡升级或者USB升级。SD卡升级的思路是把固件文件放到SD卡里BootLoader启动时检测SD卡如果有新固件就自动升级。USB升级类似用U盘或者USB线连接PC。这两种方式的速度都比串口快得多。7. 不同STM32系列的OTA适配要点7.1 F1系列的Flash限制与应对STM32F1系列的Flash扇区是1KB一页擦除粒度小适合做精细的分区。但F1的Flash写入速度较慢而且不支持双Bank所以双区备份方案在F1上基本不可行。F1的另一个问题是RAM较小比如C8T6只有20KB RAMYmodem的缓冲区不能太大否则会影响应用程序的运行。在F1上做OTA我的建议是BootLoader尽量精简只保留串口和Flash驱动Ymodem的缓冲区用512字节分两次接收一个1024字节的包。应用程序区从0x08004000开始暂存区放在Flash末尾比如0x0800C000。升级标志写在BKP的DR1寄存器不用Flash避免擦写冲突。7.2 F4系列的扇区不均匀问题STM32F4系列的Flash扇区大小不均匀前四个扇区是16KB第五个是64KB后面是128KB。这种不均匀性给分区带来麻烦。比如你想把BootLoader放在前16KB应用程序从0x08004000开始但0x08004000是第二个16KB扇区的起始没问题。但如果你想在Flash末尾留一个暂存区就要注意最后一个扇区是128KB可能太大了。F4的另一个特点是支持双Bank比如F407有1MB Flash分成两个512KB的Bank。双Bank的好处是可以在一个Bank运行程序同时擦写另一个Bank实现真正的后台升级。但双Bank的配置比较复杂需要设置选项字节而且不是所有F4型号都支持。7.3 H7系列的双Bank与后台升级STM32H7系列是高性能MCUFlash通常有2MB分成两个1MB的Bank。H7支持真正的后台升级应用程序在Bank1运行新固件写到Bank2写完后设置一个标志复位后BootLoader把Bank2的内容搬到Bank1。整个过程应用程序不用停止用户体验最好。但H7的Flash编程比较复杂需要配置Flash的等待周期、使能指令缓存和数据缓存。如果配置不对擦写速度会非常慢甚至失败。我建议参考ST官方的H7 Flash编程手册仔细配置时钟和等待周期。8. 我踩过的坑与实战经验总结第一个坑BootLoader里用了printf。早期做BootLoader的时候为了调试方便我在里面加了printf结果发现串口发送数据的时候Ymodem接收会丢包。原因是printf是阻塞式的发送一个字符串要等很久期间串口接收中断被耽误了。后来我把printf去掉了只在关键节点用LED指示问题解决。第二个坑Flash擦除时没关中断。有一次升级过程中定时器中断触发了中断服务函数在Flash里执行但此时Flash正在擦除CPU取不到指令直接HardFault。后来我在擦除前加了__disable_irq()擦除后再__enable_irq()再也没出过问题。第三个坑Ymodem的包序号溢出。Ymodem的包序号是8位的从0到255循环。我一开始用uint8_t存包序号结果到255之后回0判断逻辑写错了导致第256个包被当成第0个包数据错位。后来改成用uint16_t存只在发送和接收的时候转成uint8_t问题解决。第四个坑应用程序的栈顶地址没对齐。STM32要求栈顶地址是8字节对齐的我有个项目的链接脚本没注意栈顶地址是4字节对齐跳转后偶尔HardFault。后来在链接脚本里加了ALIGN(8)问题消失。第五个坑升级标志没清除。应用程序在写升级标志后软复位BootLoader进入升级模式升级完成后跳转到应用程序。但应用程序启动后没有清除升级标志下次复位时BootLoader又进入升级模式导致设备一直停在BootLoader。后来我在应用程序的main函数开头加了清除标志的代码问题解决。这些坑看起来都是小问题但每一个都可能导致升级失败甚至设备变砖。我的经验是OTA功能开发完后一定要做断电测试。在升级的各个阶段随机断电看设备能不能恢复。如果能恢复说明方案可靠如果不能就要加保护机制。这个测试我通常做100次以上确保万无一失。最后分享一个小技巧在BootLoader里加一个串口命令行可以通过串口发送特定指令来查看当前Flash的状态、擦除应用程序区、或者强制进入升级模式。这个命令行不用太复杂几个简单的命令就行但在调试和售后的时候非常有用。比如现场设备升级失败售后人员可以通过串口发送“erase”命令擦除应用程序区然后重新升级不用拆机。