STM32U5G9单.hex烧录:固件与选项字节合并实战指南 上周产线朋友把一版固件退回给我附了一条消息你这 hex 只烧了 flash没有选项字节我们上板后 RDP 还是 level 0。我愣了一下因为默认 CubeProgrammer 里 GUI 下载选项字节一直成功总觉得它跟着工程走了。其实不然——如果你把产物发给别人或者丢给工厂脚本很多情况下 hex 就是 hex选项字节是单独配置件。这次刚好新项目用了 STM32U5G9要交付单文件 .hex同时包含固件和选项字节索性把整个链路完整跑了一遍。这篇文章就把整条链路的原理、工具链和坑都整理出来适合正在做 STM32U5 量产烧录、或者给客户交付固件包的人参考尤其是你们也遇到“对方只收一个 hex”这种情况。我会从 Intel HEX 结构讲起然后给出三种生成单 .hex 的方式最后附上我在 STM32U5G9 上验证和量产时踩过的坑。核心结论先放在前面单 .hex 不是把固件 hex 和选项字节文本文件拼起来而是让一个 Intel HEX 文件里同时包含两个不连续的内存区段一个是用户闪存区0x08000000 起始另一个是选项字节区STM32U5 系列通常在 0x0BF90000 起始。工具链能识别这两个区段并在烧录时按不同策略处理才算真正成功。1. 为什么需要单 .hex交付场景里的两种误区1.1 “能烧进去”和“单文件交付”是两回事很多人在自己开发板上烧录时是用 STM32CubeProgrammer 的图形界面先加载固件 hex再切到 Option Bytes 页签手动勾几个选项点一下 Apply板子就跑起来了。这种操作方式非常常见也最容易造成一个错觉选项字节已经被打进固件包里了。实际上图形界面的每次操作都是独立的。固件 .hex 文件本身没有发生任何变化选项字节只是通过调试器的内存写接口写进了芯片的专用区域。你把那个 .hex 单独发给别人对方烧进去选项字节自然是默认值。另外还有一种误区是把固件 .hex 和选项字节 .ob 文件一起打包成 zip就对外宣称“单包交付”。严格来说这不算错误工厂那边用脚本也能烧但是多人交接、版本管理、扫码追溯的时候两个文件就多出两个同步问题固件匹配哪一版选项字节RDP 等级改了没有如果能把固件和选项字节放到同一个 Intel HEX 文件里整个交付物就是一个文件校验和、版本号、烧录记录都围绕这一个文件转误操作的概率会低很多。1.2 STM32U5G9 的存储布局和烧录特殊性STM32U5G9 属于 STM32U5 系列的高端型号Cortex-M33 内核带 TrustZone闪存容量可以到 4MB 量级双 Bank 结构内存映射布局和以前 F1/F4 很不一样。和单 .hex 合成直接相关的地址区域主要有三块区域起始地址说明用户闪存 Bank10x08000000常规固件存放区用户闪存 Bank20x08100000双 Bank 时的第二块固件区选项字节区0x0BF90000选项字节的实际内存映射地址OTP 区0x0BFA0000一次性可编程区域注意选项字节那个地址并不是“寄存器”而是映射到芯片存储空间的一个区域。你可以在 STM32CubeProgrammer 的 Memory 面板里直接敲 0x0BF90000 进去看内容也可以尝试把它当内存一样写入。但内部实现上选项字节使用的是电子熔丝/专用存储单元写入方式和普通 Flash 不一样。所以有一点很重要不要把 0x0BF90000 当成普通 Flash 那样想怎么写就怎么写。它有自己的对齐规则、补码校验机制和编程时序最好的办法是让官方工具或者官方库函数去操作而不是自己手工拼一长串字节。1.3 工厂为什么特别喜欢单 .hex我在配合代工厂做程序烧录时发现工厂的自动化测试台最喜欢的就是单文件方案。它们的流程通常是扫码枪扫下 PCB 条码自动加载对应固件烧录校验打标测试。如果固件包是多个文件烧录脚本里就要多写一段逻辑去判断哪个 .ob 配哪个 .hex一旦某条产线更新了选项字节而另一条没更新问题就会在出货后才暴露。单 .hex 方案下产线脚本只需要一个参数文件路径。固件和选项字节作为一个整体被校验和约束哪怕操作员拷错文件校验环节也能立刻发现。对客户交付也一样一个文件传过去对方打开就能烧减少了大量沟通成本。2. 合成前必须吃透 Intel HEX 的“非连续地址”能力2.1 扩展线性地址记录才是单文件合成的关键Intel HEX 并不是只能保存一段连续地址的数据。它用记录类型04Extended Linear Address Record来指定后续数据记录的高 16 位地址。举个例子:0200000408009A :10000000112233445566778899AABBCCDDEEFF00F2 :00000001FF第一行:020000040800中02表示后面有 2 个数据字节0000是记录地址固定为 004是记录类型接下来0800就是高 16 位地址意思是后续数据记录的完整地址是0x0800xxxx。第二行是普通数据记录地址0000合成完整地址就是0x08000000。最后一行:00000001FF是 EOF 记录。如果要在一个文件里同时放 0x08000000 的固件和 0x0BF90000 的选项字节只需要在适当位置再插入一条扩展线性地址记录:0200000408009A :10000000112233445566778899AABBCCDDEEFF00F2 :020000040BF98B :10000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 :00000001FF第二条:020000040BF9就把基地址切到了0x0BF9xxxx后面:10开头那条就是写到选项字节区的数据。原理不复杂但很多工具解析这类混合 hex 时处理方式不同。有的烧录器会自动识别所有分区有的只会把第一个扩展线性地址之后的数据当固件后面的直接忽略。这就是为什么合成之后还必须在真实芯片上做一遍完整烧录验证。2.2 工具对选项字节区域的识别逻辑ST-Link、J-Link、STM32CubeProgrammer 这类调试工具操作带地址映射的存储区域时通常会根据地址范围去判断目标区域类型。地址落在 0x08000000 到 0x081FFFFF 之间就按用户 Flash 方式写入地址落在 0x0BF90000 附近会走选项字节编程流程并触发对应的 option byte reload。但并不是所有烧录器都这样。有些第三方烧录器只支持普通 Flash 编程不处理选项字节区的特殊时序哪怕你给的 .hex 里带了 0x0BF9 段它也会报错。所以在给工厂选型时一定要提前确认烧录器固件版本是否支持 STM32U5 的选项字节编程而不是等到了量产阶段才去试。2.3 先通过工具确认目标芯片的 OB 基址不同系列的选项字节基址不一样。STM32F103 是 0x1FFFF800STM32L4 系列是 0x1FFF7800STM32U5 系列我这边测到的是 0x0BF90000。保险起见拿到新片子时先连接 STM32CubeProgrammer跑一下命令STM32_Programmer_CLI -c portSWD modeUR -ob read它会打印当前芯片的选项字节寄存器值同时也能从输出里看到工具识别出来的选项字节区域。建议把这段输出保存下来作为后续合成 hex 时的核对基准。3. STM32U5G9 选项字节里到底有什么要写哪些3.1 常用字段速查表STM32U5 的选项字节比 F1 复杂得多它不仅仅是 RDP 和 BOOT0 那么简单。我列一下 U5G9 上比较常用、也确实会影响固件运行的字段字段用途典型设置RDP读保护等级0xAALevel 00xBBLevel 10xCCLevel 2nBOOT0硬件 BOOT0 引脚使能0启用引脚1禁用nBOOT1启动介质选择与 BOOT0 组合SWAP_BANKBank 交换0不交换1交换SECWM1_START/ENDTrustZone 安全区范围按闪存页号配置HDP隐藏保护区可选隐蔽指定区域PCROP专有代码保护防止读出来注意别把自己的调试区也封了WPRPN写保护防止意外擦写RDP 是最容易踩坑的。Level 1 意味着调试器不能再直接读 flash但还能擦除重新写Level 2 一旦设置芯片的调试口和 bootloader 访问都将被永久限制这是不可逆的。如果你还处于开发调试阶段千万别在发给别人的 single hex 里直接设置 Level 2尤其是当整机没有预留离线升级通道时。3.2 在 CubeProgrammer 里配置出想要的 OB 组合我的习惯是先在图形界面里把选项字节配置好再想办法导出成可复用的数据而不是直接手写寄存器值。具体操作连接目标板进入 Option Bytes 页签按项目需求设置 RDP、BOOT 配置、安全区边界等点击 Apply让工具把选项字节写入芯片再切到 Memory 面板地址栏输入0x0BF90000读取一段数据确认读回来的值和页签里的设置一致。这个流程能保证你最终拿到的数据是真实有效、经过芯片确认的而不是自己脑补的寄存器组合。3.3 千万不要手动拼原始字节我曾经为了图省事想直接在脚本里构造选项字节区域的原始数据发现 U5G9 的选项字节并不像 F1 那样简单地把数据写在一个地址上。选项字节区域内部有补码校验机制某些位还会自动映射到多个存储单元你手动拼出来的数据看着对实际烧进去可能触发校验失败。更稳的做法是用 CubeProgrammer 的.ob文件作为中间格式或者用工具导出该地址范围的原始内存数据再把它作为 Intel HEX 的一部分合并到固件里。记住STM32U5 的选项字节是电子熔丝形式的专用存储不是普通 RAM 或者 Flash你把它当成内存去写地址本质上是让芯片内部的状态机去完成真正的烧录动作。4. 生成单 .hex 的三种实操方式4.1 方案一CubeProgrammer 图形界面手工合并适合验证这是最接近“所见即所得”的方式适合第一次跑通流程确认合成后的 hex 是否满足要求。先把固件工程编译出 .hex然后在 CubeProgrammer 中连接芯片把固件 .hex 下载进去。接着按上一节的方式配置好选项字节并应用再用 Memory 面板把0x0BF90000区域的数据导出成一个小型 .hex 文件。此时你手上有两个 hex固件 hex 和选项字节区 hex。用任意支持 Intel HEX 合成的工具把它们合并。没有工具的话甚至可以在文本编辑器里操作但容易出错不建议。这个方案本质上是借助芯片本身帮你算出选项字节区的有效原始数据而不是人肉计算。缺点是必须先有一块真实芯片没法纯离线生成。4.2 方案二srec_cat 命令行合成适合批量和 CIsrec_cat 是 SRecord 工具集里的核心命令专门用来处理各种 hex/bin/s-record 文件支持合并、切片、偏移、校验和计算。Ubuntu/Debian 下直接apt install srecordWindows 下也有对应二进制包。合成命令很简单srec_cat firmware.hex -Intel ob_data.hex -Intel -o combined.hex -Intel其中ob_data.hex是你在 4.1 里导出的选项字节区数据。srec_cat 会保留各自的扩展线性地址自动生成一个包含两个区段的完整 Intel HEX 文件。如果想把选项字节数据本身也做成可重复生成的脚本可以用 srec_cat 从一个二进制文件生成带指定地址的 hexsrec_cat ob_data.bin -Binary -offset 0x0BF90000 -o ob_data.hex -Intel这样的话你的版本库里只需要维护一个“选项字节原始数据.bin”每次构建固件时顺手生成合并 hex比到处传 GUI 导出文件规范得多。srec_cat 还支持生成 CRC 校验记录可以把整个 hex 的校验和算出来输出到另一个文件。这个特性在生产追溯里很有用。4.3 方案三Python intelhex 定制脚本适合复杂构建链如果你们的固件构建流程已经用 Python 串联或者需要额外处理 TrustZone 安全区、多 Bank 布局用 Python 的 intelhex 库更灵活。下面是我用的一个示例脚本它会读取固件 hex 和选项字节 bin按指定地址合并输出from intelhex import IntelHex # 读取用户固件 firmware IntelHex(build/fw.hex) # 读取选项字节原始数据假设文件里就是映射地址对应的字节序列 with open(build/ob_data.bin, rb) as f: ob_data f.read() # 把选项字节写入 0x0BF90000 起始地址 base 0x0BF90000 for i, b in enumerate(ob_data): firmware[base i] b # 输出合并后的单 hex firmware.write_hex_file(output/combined.hex)如果要从 CubeProgrammer 导出的.ob文件转换成 bin可以用脚本解析它的键值对然后填充到偏移地址。每个 STM32 系列的偏移定义都不同U5 的字段排布也比较复杂我建议初期还是先用工具导出 bin脚本只负责拼接。4.4 三种方式对比方式优点缺点适合场景GUI 手工合并直观适合验证概念依赖真实芯片难以重复执行项目初期确认 OB 数据srec_cat快速、可脚本化、跨平台不能直接生成 OB 原始数据CI 自动化批量构建Python intelhex灵活可集成进构建需要维护脚本和地址映射复杂安全区/多Bank工程我个人最推荐的是 4.2 4.3 组合用 GUI 在新片上生成一次 OB 原始数据存成 bin后续所有构建都走 srec_cat 或者 Python 自动合成不再人工参与。5. 合成之后怎么验证尤其是选项字节段5.1 先在 CubeProgrammer 里打开合成 hex 看分区合成后的 combined.hex 不要急着烧到量产板上先在 CubeProgrammer 的编程界面里加载它然后切到 Memory 区域看地址分布。正常情况下你应该能看到 0x08000000 和 0x0BF90000 两个段。如果你看到的只有 0x08000000说明工具在解析时把后一段忽略了这通常是 hex 文件里扩展线性地址记录格式不对或者某些工具对地址跳跃有特殊限制。这时要回头检查 srec_cat 的输出警告或者用文本方式打开 hex 确认存在:020000040BF9这一行。5.2 命令行烧录和回读验证我通常用命令行烧录来做完整验证STM32_Programmer_CLI -c portSWD modeUR -d combined.hex -v-v参数让工具在烧录后执行校验。日志里会显示 flash 写入地址范围和校验状态。如果选项字节段也被正确识别你会在日志里看到除了 flash 区之外还有 OB 区的写入动作。烧完后再读一遍选项字节STM32_Programmer_CLI -c portSWD modeUR -ob read对比输出的字段值是否与预期一致。这一步能确认选项字节不是只写进了缓存而是真的在芯片上生效。5.3 遇到工具不认 OB 段时怎么处理如果某些烧录场景实在不识别 hex 里的 OB 段退而求其次的方案是单独用-ob参数配合.ob文件烧写。例如STM32_Programmer_CLI -c portSWD modeUR -d firmware.hex -ob ob_config.ob这样至少能在一个脚本命令里完成所有动作虽然不是单一 .hex但比手工操作稳得多。真要交付单 .hex 给客户就需要提前确认对方用的烧录器型号和固件版本做一次联合验证。5.4 回读不一致的常见原因选项字节区回读结果和预期不一致最常见的是这几个原因烧录时芯片还在 Bank1 和 Bank2 切换状态OB 未完成 reload设置 RDP 后调试器权限发生变化读回来的结果被掩码目标板电源不稳导致 OB 编程时序中断某些选项字段之间互相约束比如 SECWM 和 PCROP 冲突工具没有提示。遇到不一致先不要重复烧复位一下板子重新连上再读。如果复位后依然不对用橡皮擦模式先全片擦除再重新下载。6. 量产阶段的细节版本管理、RDP 策略和双 Bank6.1 把版本信息和完整性校验写进 hex单 .hex 文件在产线上流转时靠文件名判断版本不可靠。我建议在固件里预留一个版本信息区把编译时间、Git commit、构建序号写进去选项字节区域也可以用固定偏移记录校验值。这样产线在烧录后可以做一次全片校验如果版本串不匹配就会直接报警避免把上一版本固件当新版本出货。6.2 RDP 等级应该什么时候设置如果你需要后期在线升级或者现场调试量产时别急着设 RDP Level 2。Level 1 能够阻止普通调试器读 flash又不影响整机升级是一个比较折中的选择。但要注意Level 1 状态下STM32CubeProgrammer 依然可以通过 sector erase 把芯片变回 Level 0只是 flash 内容会被清空。这并不会影响工厂重复烧录反而方便返修。只有当产品对安全要求很高且完全不需要现场维护时才考虑 Level 2。6.3 双 Bank 和 SWAP_BANK 导致的启动异常STM32U5G9 是双 Bank 结构如果选项字节里的 SWAP_BANK 和实际固件所在 Bank 不匹配芯片上电后可能从错误的 Bank 启动现象就是“固件明明烧进去了却跑不起来”。这种坑特别隐蔽因为固件 hex 烧录地址是对的编译也没有问题但一上电就进硬件错误。检查时优先看 SWAP_BANK 是否为 0再确认 Reset Vector 是否在 Bank1 起始位置。如果你要做 A/B 升级就需要刻意把两个 Bank 的固件放好然后在升级流程中切换 SWAP_BANK。这也不是简单改一个选项字节就能完事必须考虑固件加载地址、中断向量表地址和启动顺序。6.4 常见异常快速排查表异常现象可能原因解决方向烧录完芯片无法启动SWAP_BANK 不对读 OB检查 Bank 映射调试器无法连接RDP Level 2 已启用不可逆只能换芯片只能读 flash 不能写RDP Level 1 正常行为用 sector erase 后再写选项字节总是写不进去电源电压不稳或工具不支持换官方工具检查供电固件跳转到其他地址失败TrustZone 安全区配置不当核对 SECWM/PCROP说一个我在 U5G9 上体会最深的地方这类芯片的选项字节不是“设一次就完事”的配置它和固件加载、安全启动、双 Bank 升级都是耦合的。写进单 .hex 只是第一步真正要保证的是生产线上每一块板子烧完之后的运行状态和你手里这块验证板完全一致。我现在的做法是维护一个构建脚本每次编译固件时自动从固定版本号的 OB bin 文件生成 combined.hex再在 CI 里用真实开发板跑一遍烧录回读比对 OB 关键字段。整个流程稳定跑了一个多月产线那边再也没来问过“为什么板子烧出来没保护”。如果你们也遇到类似的交付问题建议先从自己的烧录脚本和版本管理下手把固件和配置当成一个整体来管理会比每次手工点界面可靠得多。