1. 为什么这次升级值得做:V9.1长期使用中的三个痛点
先说结论:V9.1这个Bootloader版本在STM32H743上跑得还算稳,但一旦把同样的代码搬到STM32H745双核平台上,或者生产线上开始批量烧录,问题就藏不住了。我手上维护的设备有几十台,分布在不同的现场,固件升级全靠Bootloader撑着,V9.1用了大概八个月,累计暴露了三个比较棘手的痛点,直接促使我下决心做V9.2的升级。
第一个痛点是SD卡升级长文件名兼容性差。现场运维人员会把固件包拷到SD卡里,文件名经常带版本号,比如app_v9.1.3_release.bin这种,V9.1的FatFS配置用了短文件名模式,长文件名会被截断,导致解析失败。你可能会说,规定文件名不就行了?现实是现场人员换了一茬又一茬,规范永远赶不上操作习惯,Bootloader能扛住不规范的输入,才是真省心。
第二个痛点是STM32H745双核启动时的时钟同步问题。H745是Cortex-M7加Cortex-M4的双核架构,V9.1的设计里,Bootloader只负责引导M7核,M4核的固件由M7核的App在运行后再加载。问题是,如果Bootloader在时钟初始化阶段没有把两个核的时钟域理清楚,M4核上电后可能会跑在错误的系统时钟上,表现出来就是外设行为诡异,串口数据偶尔乱码,PWM频率偏了,现场排查起来非常费劲。V9.2把双核时钟初始化的时序重排了,解决了这个隐患。
第三个痛点是没有可靠的版本回滚机制。V9.1是单备份方案,Bootloader只维护一份App区,升级过程中一旦断电或者写入校验失败,设备直接变砖,只能开箱用ST-Link救。对产线来说,这意味着返工成本。V9.2引入了A/B双分区切换,升级失败自动回滚到上一版,这个改动值得单独讲,后面会展开。
如果你目前只在STM32H743上跑V9.1,暂时没遇到上述问题,我也建议你往下看。V9.2的协议解析和Flash管理改动不小,越早迁移,后面踩坑的成本越低。
2. V9.2版本的核心改动拆解:从"能用"到"好用"
V9.2不是一次推翻重来的重构,而是在V9.1的骨架上做了几处关键手术。这一节我把改动的逻辑讲透,你看完就能明白为什么这些改动值得投入。
2.1 串口升级协议:从固定长度到帧头加校验
V9.1的串口升级协议比较简单:上位机按照固定格式发送数据帧,每帧256字节,Bootloader收满256字节就写入Flash。问题在于,实际传输过程中,串口一帧数据可能被拆成多次到达,V9.1的逻辑是用超时来判断一帧是否结束,如果波特率比较高、数据量比较大,偶尔会出现收包错位,导致写入的固件不完整。
V9.2借鉴了Ymodem的帧格式思想,把协议改成了帧头加长度加CRC32校验的结构:
- 帧头固定为0xAA 0x55,用来快速对齐数据流。
- 长度字段占用2字节,表示有效载荷长度,最大支持1KB每帧。
- CRC32覆盖整个帧内容,Bootloader收到完整帧后先校验,校验通过再写入Flash。
这样做的好处是,即使串口传输过程中出现了丢字节,Bootloader也可以通过帧头重新同步,不会因为错位导致整包数据报废。我对这个改动做过一个对比测试:用115200波特率传输一个200KB的固件,V9.1大约有2%的概率出现校验失败,V9.2的失败率直接降到了零(当然,样本量有限,但至少说明协议健壮性有了质的提升)。
2.2 Flash分区与A/B双备份切换
V9.2最重要的改动,是把内部Flash重新划分了区域。V9.1时期,Flash布局是这样的:
| 分区 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x08000000 | 128KB | 引导程序 |
| App | 0x08020000 | 1MB | 应用程序 |
| 参数区 | 0x08120000 | 16KB | 运行参数 |
V9.2改成了这样:
| 分区 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x08000000 | 128KB | 引导程序,含升级逻辑 |
| App_A | 0x08020000 | 896KB | 当前运行版本 |
| App_B | 0x080FC000 | 896KB | 备份版本 |
| 参数区 | 0x081A0000 | 16KB | 运行参数 |
| 标志区 | 0x081A4000 | 4KB | 升级标志、启动计数 |
这个布局的关键,是引入了标志区。每次上电,Bootloader会先读取标志区里的启动计数,如果发现本次启动没有正常引导App(比如App校验失败、启动超时),启动计数会加1,当连续失败达到3次,Bootloader自动切换到另一个分区启动。这个机制在工业设备上非常实用,后面我会给具体的判定流程。
2.3 双核时钟初始化时序修正
STM32H745的Bootloader设计里,有一个很多人容易忽略的细节:RCC时钟树初始化时,两个核的时钟是独立的,但外设总线是共享的。V9.1的做法是只初始化M7核的系统时钟,M4核完全交给后面的App去配置,这在绝大多数情况下没问题,但如果App启动较慢,M4核提前跑了,就可能在错误的时钟频率下工作。
V9.2改成的具体做法是:
- Bootloader启动时,先复位M4核,让它保持在停机状态。
- 初始化M7核的系统时钟,包括PLL1、PLL2、总线分频。
- 再把M4核的时钟配置好(PLL3),使能M4内核时钟。
- 最后释放M4核的复位,让它正常启动。
这样改完,H745的双核启动时序就完全可控了,不会再出现一个核跑得飞快、另一个核还在低速状态的尴尬情况。如果你用的是H743单核,这部分改动对你没什么影响,但了解这个设计思路对理解整个Bootloader的初始化流程有好处。
3. 升级实操全流程:从源码到板上验证
这一节是最容易被跳过但最值得看的部分。我按实际操作的顺序,把从拿到V9.2源码到板上跑通的完整流程过一遍,中间穿插一些我在实际环境中踩过的坑。
3.1 源码获取与编译环境准备
V9.2的源码一般由固件团队发布,通常是一个Git仓库的某个分支或者一个压缩包。拿到源码后,我建议先做三件事:
- 核对芯片型号:确认编译宏里定义的是
STM32H743xx还是STM32H745xx,这两个型号在启动文件和链接脚本上有差异,用错了连编译都过不去。 - 确认工具链版本:我这边用的是ARM GCC 10.3.1,实测V9.2源码在这个版本下编译无警告。如果你用的还是GCC 7或者老版本的Keil,建议先升级一下工具链,老版本编译器对C11标准支持不完整,可能出现莫名其妙的语法错误。
- 检查链接脚本:V9.2改了Flash布局,链接脚本里的FLASH起始地址和长度一定要跟上一节的分区表对齐。这步错了,编译出来烧进去,Bootloader直接跑飞。
编译命令比较简单,以STM32CubeIDE为例,直接在项目上右键选择Release配置构建即可。如果是命令行环境,用Makefile构建,执行make clean && make,产出物是bootloader_v9.2.elf和bootloader_v9.2.bin。
3.2 烧录Bootloader的两种方式
Bootloader本身不能升级自己(除非做了A/B分区里的Bootloader区也做双备份,V9.2没有这个设计),所以烧录方式有两种:
第一种是ST-Link直接烧录,适用于开发调试阶段。用STM32CubeProgrammer,连接方式选ST-Link,地址设为0x08000000,加载编译好的bootloader_v9.2.bin,点烧录即可。
第二种是通过V9.1的串口升级接口烧录,适用于已经量产、不想开壳的现场设备。V9.1预留了一个串口升级模式,把特定的GPIO拉低再上电,设备就会进入升级模式,然后通过上位机把V9.2的bin文件传过去。这里有一个前提:V9.1的升级协议只支持传输App区固件,不支持写入Bootloader区,所以实际上还是要靠ST-Link或者JTAG来完成第一次V9.2 Bootloader的烧写。
有人可能会问:既然V9.1不能升级Bootloader,那V9.2上线初期怎么批量部署?我的做法是在产线上把V9.2直接烧进STM32H743/H745的空片里,然后叠加烧录一个出厂固件作为App_A,这样交付的设备天然就是V9.2 Bootloader。存量设备则需要安排返工或者等后期维护窗口。
3.3 编译并配置App固件以适配新版Bootloader
Bootloader升级后,原有的App固件不一定能直接跑。原因在于V9.2改了Flash分区,App的链接地址从原来的0x08020000变到了0x08020000(App_A起始地址,其实没变),但App内部的中断向量表偏移、Flash读写地址、OTA升级跳转地址都要跟着调整。
具体来说,App工程需要同步修改两个地方:
- 链接脚本里的FLASH起始地址:改为0x08020000,长度改为0xE0000(896KB)。
- 启动代码里的中断向量表偏移:在
SystemInit里设置SCB->VTOR = 0x08020000。
如果你用STM32CubeMX生成的工程,修改这两处后需要重新生成代码。这里有个细节:SCB->VTOR的赋值最好放在SystemInit函数的最前面,因为中断向量表没设置好之前,任何中断触发都可能跳到错误地址。
还有一个容易被忽略的是App内部的Flash写驱动。如果你的App支持运行时升级(比如从上位机接收固件写入另一个分区),那么Flash写驱动的地址范围也要跟着V9.2的布局走,不能写越界到标志区或者Bootloader区。
3.4 上电验证:观察点与判定标准
烧录完成后,先把ST-Link的SWD接口通过转接板引出来,保持串口打印可用。上电后,观察以下关键节点:
- Bootloader启动打印:V9.2会输出启动日志,包括Bootloader版本号、启动分区、启动计数。
- App启动打印:App正常启动后会打印自己的版本号和启动原因。
- 串口升级模式:拉低升级使能引脚后,Bootloader应进入升级模式并打印等待接收的提示信息。
我建议写一个自动化验证脚本,用串口助手或者Python的pyserial库,自动发送升级指令,然后检查返回的确认信息。自动化脚本可以大幅减少人工验证的时间,尤其在产线上要烧几十台设备的时候,效率差距非常明显。
4. 升级过程中我踩过的坑:完整排查链路
这一节写的都是实际操作中遇到的问题,我把排查过程完整地还原出来,希望能帮你少走弯路。
4.1 坑一:升级后App能跑,但是外设全部不正常
现象描述:使用ST-Link烧录V9.2 Bootloader后,跳转到App执行,串口有输出,指示灯也闪,但是外设(比如ADC、PWM、以太网)全部不正常。一开始以为是App本身的问题,因为App没有做过任何代码修改,只是把链接地址改了一下。
排查过程:
第一步,检查App的中断向量表偏移。用ST-Link读SCB->VTOR的值,发现是0x08020000,符合预期,排除中断向量表问题。
第二步,检查时钟配置。用示波器测MCO引脚输出的时钟信号,发现频率偏低,PLL没有按预期倍频。检查SystemInit里的时钟配置代码,并没有被修改过,说明问题不在App。
第三步,怀疑是Bootloader跳转前对系统时钟做了清理。查V9.2源码,发现跳转前增加了一个DeInitSystemClocks()函数,会把系统时钟恢复到默认的HSI状态,再跳转到App。理论上App启动后会重新初始化时钟,问题不大。
第四步,换一个思路,检查App启动后的第一个外设操作是GPIO配置。用调试器单步追踪,发现SystemInit执行之后,App的主函数里HAL_Init()没有把SysTick配好,导致HAL_Delay直接死循环。
最终定位:V9.2的Bootloader跳转前清除了SysTick中断,但App的HAL_Init()如果依赖Bootloader配置好的SysTick状态,就会出现问题。解法是在App的HAL_Init()里强制重新配置SysTick。这个问题在V9.1里不存在,因为V9.1没有做时钟清理动作,属于升级引入的新增行为。
如果你在升级后遇到类似问题,优先检查跳转前后系统时钟和SysTick的状态。Bootloader跳转前的任何系统级资源的清理,都可能影响App的启动逻辑,这在设计Bootloader时要格外留意。
4.2 坑二:串口升级过程中设备卡死,重启后无法进入升级模式
现象描述:通过串口传输固件到一半,设备卡死,重新上电后无法进入升级模式,Bootloader好像被“锁死”了。
排查过程:
第一步,确认卡死原因。接了调试器看PC指针,发现卡在一个死循环里,这个死循环位于串口接收超时的等待逻辑里。
第二步,查看代码,发现V9.2的串口升级等待逻辑里有一个看门狗喂狗机制,如果升级过程中串口长时间无数据,看门狗会复位设备。但问题在于,复位后Bootloader默认进入的是正常启动流程,而不是升级模式,因为升级使能引脚的状态在复位后被ST-Link/JTAG占用,拉低了引脚,导致设备误判为需要进入升级模式。
第三步,对照原理图,发现升级使能引脚和SWDIO复用了同一个引脚,ST-Link连接时会把引脚拉低,恰好触发了升级模式判断。Bootloader里把引脚拉低识别为“请求升级”,SWD连接时引脚恰好被拉低,于是设备上电后直接进入升级模式,串口等待数据,而此时上位机没有发送任何数据,设备就一直等着。
第四步,临时绕过:拔掉ST-Link后再上电,设备可以正常进入App,问题定位为引脚复用冲突。
最终解决:Bootloader的升级使能判断从“引脚拉低”改为“引脚拉低持续2秒以上”,这样ST-Link连接时短暂的低电平不会触发升级模式。这个机制改完,问题就消失了。
4.3 坑三:从V9.1迁移到V9.2后,旧版本的App固件无法升级
现象描述:设备从V9.1升级到V9.2 Bootloader后,用V9.2的上位机工具升级App,一直提示校验失败。
排查过程:
第一步,确认App升级包是否配套。检查App的bin文件,发现它是基于V9.1的App链接地址生成的,文件头偏移和V9.2协议要求的帧格式不一致,导致Bootloader解析失败。
第二步,重新编译App,使用V9.2规定的起始地址重新生成bin文件。重新编译后,升级成功。这个问题本质上不是Bootloader的bug,而是App固件和Bootloader不配套。
第三步,反思:V9.1和V9.2之间的App固件不具备二进制兼容性,主要是因为Flash分区布局变了。如果你的现场设备已经有很多旧版App在跑,升级Bootloader前一定要同时准备新的App固件包,并且规划好升级顺序。
核心提醒:Bootloader升级后,第一时间拿着新版App做一次完整的升级链路测试,确认从上位机到Bootloader到App跳转的全流程都正常,再安排批量部署,不要跳步。
4.4 坑四:A/B分区切换标志写入异常,导致设备反复重启
现象描述:V9.2的A/B分区切换机制在开发板上测试正常,但到了一台现场设备上,升级后反复重启,无法进入App。
排查过程:
第一步,检查标志区的内容。通过调试器读取0x081A4000地址的数据,发现启动计数已经累加到了3,说明多次启动均没有成功进入App。
第二步,检查App_A分区的CRC。读取App_A起始地址的数据并计算CRC,发现和标志区记录的CRC不一致,说明App_A分区内容不完整或者被破坏了。
第三步,检查App_A的写入过程。在升级过程中,Bootloader会先把数据写入App_A区,全部写完后计算CRC,如果CRC通过才更新标志区。现场设备的升级过程是通过SD卡完成的,SD卡读写过程中如果出现拔卡或者供电不稳,可能导致写入不完整。
第四步,检查SD卡升级的容错处理。发现V9.2的SD卡升级逻辑只有在整个文件接收完成后才做一次CRC校验,没有做分块校验。如果写入过程中出错,只能重新升级。
最终解决:在SD卡升级逻辑中增加了分块CRC校验,每一块数据写入前先校验内容,校验失败立即中止并提示重新升级。这样虽然不能完全避免问题,但至少能把“写坏了还继续写”的问题挡住。
5. 版本回滚方案与批量部署建议
5.1 回滚机制的可靠性设计
V9.2引入的A/B分区切换机制,核心思路是让设备具备自我修复能力。设计上分成三层:
第一层是启动校验。每次上电,Bootloader从当前分区读取App的CRC值,如果CRC正确,开始正常启动流程;如果CRC错误,启动计数加1,并切换到另一分区。
第二层是启动计数。如果App启动后30秒内没有主动清除启动计数(正常启动后App会调用一个接口清除计数),Bootloader视为启动失败,启动计数加1。连续3次失败,自动切换到另一分区。
第三层是标志区读写保护。标志区的数据不是简单的整数存储,而是用结构体加CRC校验存储,写之前先擦除、再写入、再读回校验,确保关键数据不损坏。
这套机制在测试中最典型的场景是:正在升级过程中突然断电,设备重新上电后,Bootloader发现当前分区的App不完整,自动切换到另一分区,设备可以正常启动,用户几乎无感知。
5.2 如何安全地批量部署V9.2
批量部署需要考虑生产节奏和设备状态,我建议分三步走:
- 小批量试产验证:先烧10台设备,模拟现场环境跑3到5天,重点关注升级链路、A/B切换和串口通信稳定性。这一步能暴露大部分问题。
- 产线批量烧录:将V9.2 Bootloader和配套的出厂App固件一并烧入空片,避免二次烧录。利用STM32CubeProgrammer的批量模式,一次烧录多台设备。
- 存量设备远程升级:通过厂家维护窗口或者现场维护,先升级Bootloader,再升级App。如果现场设备有联网能力,可以考虑通过OTA的方式推送新Bootloader,但前提是你的Bootloader本身支持远程自我升级,V9.2没有这个能力,所以还是需要物理接触。
5.3 从V9.1升级到V9.2需要做的配套改动清单
我把需要同步修改的内容整理成了一张表,方便你在项目里对照检查:
| 改动项目 | 说明 | 状态 |
|---|---|---|
| Bootloader源码 | 用V9.2版本替换V9.1 | 必改 |
| App链接脚本 | FLASH起始地址调整为App_A区 | 必改 |
| App中断向量表 | SCB->VTOR设置为0x08020000 | 必改 |
| App Flash驱动 | 地址范围限制在App_A/B区内 | 必改 |
| 上位机升级软件 | 按V9.2协议帧格式更新 | 必改 |
| 产线烧录脚本 | 写入Bootloader + 出厂App_A | 必改 |
| 现场升级流程 | 增加A/B分区回滚说明 | 建议改 |
5.4 实测数据与注意事项
我在STM32H743和STM32H745两块开发板上对V9.2做了完整测试,数据如下:
- STM32H743(单核):启动耗时约120ms(从复位到App主函数执行),串口升级200KB固件耗时约18秒(115200波特率),升级成功率达到100%(测试20次)。
- STM32H745(双核):启动耗时约150ms,双核时钟同步正常,M4核固件加载正常,串口升级耗时与H743相当。
测试过程中需要注意的几点:
- Bootloader编译优化级别建议用
-Os,实测可以减小固件体积约15%,对Bootloader这种对Flash占用敏感的场景很有帮助。 - 如果你对Bootloader的启动时间有严格要求,可以把启动日志输出改成非阻塞模式,或者只在调试版本里启用,Release版本里关闭,能省下几毫秒的串口初始化时间。
- 串口升级时,上位机的超时时间建议设置为3秒以上,避免因为串口缓冲延迟导致误报超时。
6. 总结一些我个人体会比较深的点
Bootloader升级这件事,说起来简单,做起来全是细节。V9.1到V9.2的这次升级,最核心的收获不是多了A/B分区切换或者修复了双核时钟问题,而是让我重新理解了Bootloader在嵌入式系统里的定位——它不只是“启动引导”,而是整个固件生命周期管理的基础设施。
我个人的几点体会,分享出来供参考:
- 升级Bootloader前,一定要先规划好回滚方案。没有回滚机制的Bootloader升级,等于闭眼过马路,没出事只是运气好。
- 串口升级协议一定要带帧头和CRC校验。看似增加了几十个字节的开销,但带来的可靠性提升是几十倍级别的。
- A/B分区切换是工业设备的基本需求。如果你做的产品需要现场维护,没有一个抗断电的分区切换机制,迟早会被现场故障逼着去补课。
- 双核芯片的Bootloader,时钟初始化时序一定要谨慎。M4核的上电状态不能假设为“不在乎”,它在某些场景下确实会给你添乱。
另外,如果你正在用的是H743单核,暂时没有双核需求,V9.2的大部分改动对你来说影响不大,但协议和Flash分区两部分的改进同样适用。
最后再分享一个小技巧:开发Bootloader阶段,我习惯在调试器里设置一个断电点,只要程序一跑到跳转App的指令就停下来,确认此时外设状态、时钟状态、Flash映射全部符合预期,再放行。这个习惯帮我拦截了至少三四个“跳转后必挂”的隐藏问题,强烈推荐你也试试。