
1. 问题重现provisioning 到底在哪个环节失败先交代一下我手上的环境STM32H573I-DK 开发板STM32CubeH5 固件包示例路径是Projects/STM32H573I-DK/Applications/ROT/STiROT_OEMuROT。这块板子刚到手里的时候我还挺有信心毕竟 ST 的 ROT 例程在 H7、L5 上都跑过理论上就是生成镜像、烧进去、再跑一遍 provisioning 脚本的事。结果第一次跑完终端直接打出一行Error: Data download failed: Flash write error at 0x0C000040 Provisioning failed.我当时第一反应是烧录地址不对检查了脚本里的地址、重新编译了OEMuROT_Appli问题依旧。后来换了一台机器、换了一根 ST-Link 线还是同一个位置报错。直到我把板子当前的生命周期状态读出来才发现根本不是地址的问题——是这个芯片在进入 provisioning 之前已经被我上一次“半成功”的操作搞进了一个既不是开发态也不是完整封闭态的中间状态OTP 里已经有残留内容。后面所有写入都被 RSS 拒绝了。这个失败其实很典型。很多人搜到STM32H573 STiROTOEMuROT example failed provisioning这个关键词大概率就是在跑官方例程的过程中卡住。最麻烦的地方在于STM32H5 的安全启动链路不像普通 MCU 那样“编译-下载-运行”三步就结束了它涉及到生命周期、公钥注入、防回滚计数器和调试认证任何一步前置条件不对最终都会以 provisioning failed 收场。而且同一个失败现象背后的原因可能完全相反有些板子是条件没满足有些板子是条件已经烧过了头不能再用常规方式重来。先说结论避免后面看晕如果你的板子还在OPEN生命周期也就是刚出厂的默认状态那 provisioning 失败大概率是可以救回来的但如果已经烧进去了防回滚相关内容或者生命周期被推到了CLOSED往上那就不是重新插拔一下调试器能解决的问题了。我后面会按排查顺序把每一步讲清楚。2. STiROT 与 OEMuROT 各干什么排查前必须理清的两个角色2.1 这两个 ROT 不是同一级东西很多人看到目录名叫STiROT_OEMuROT以为这是一个二选一的选项要么用 STiROT要么用 OEMuROT。其实是两级串联的关系。STM32H5 系列在芯片内部有一段固定的启动 ROM也就是 RSSRobust Secure Storage鲁棒安全存储。芯片上电后CPU 先执行 RSSRSS 再根据 option byte 决定是否启动 STiROT。STiROT 是 ST 官方提供的第一级 ROT它负责验证第二级 ROT也就是 OEMuROT 的镜像。OEMuROT 则是你自己的二级 bootloader负责验证最终的用户 App。整个过程是一条信任链RSS - STiROT - OEMuROT - 用户 App每一级验证下一级的签名和完整性任何一级过不了系统就停在当前级不会往下走。2.2 provision 到底在“烧”什么东西这里的 provisioning 并不是往 Flash 里写一段配置那么简单它干的事情很“硬”把 STiROT 和 OEMuROT 需要的一组根公钥哈希烧到 OTP 里把安全生命周期从OPEN推到一个更受控的状态可能还会设置防回滚计数器。举个例子STiROT 在启动的时候需要校验 OEMuROT 镜像的签名。但校验用的公钥从哪里来不能是 Flash 里的因为 Flash 可以被改写攻击者把自己的公钥换上去就绕过验证了。所以这个公钥要么在 OTP 里要么是在工厂里用特定方式一次性写进去的。provisioning 干的就是这件事把你项目里指定的那组 OEM 公钥固定在芯片里确保以后只有持对应私钥的人才能发布可启动的 OEMuROT 镜像。这也就是说provisioning 失败和普通下载失败有本质区别。普通下载失败顶多是没有跑起来provisioning 失败则意味着你的板子已经经历了一次“半初始化”OTP 里可能已经有一部分内容被写进去了只是没有完成整个流程。这也是为什么不能简单重试。2.3 为什么 H573 这个例程特别容易失败STM32H573 是 H5 系列里偏中高端的一款它同时支持 STiROT 和 OEMuROT而且允许配置多块 ROT 区域。官方例程为了方便展示把 STiROT 也做成了可烧写的镜像而不是像某些老平台那样直接固化在 ROM 里。这个设计本身没问题但带来一个副作用烧写顺序和地址必须严格按照工程里的配置来否则就会出现我上面遇到的Flash write error at 0x0C000040。另外H573 的生命周期状态比老平台要多OPEN、CLOSED、LOCKED等状态之间的迁移有一些是不可逆的。官方 README 里通常会提醒你先在OPEN状态下完成开发但实际用的时候很多人会被随手执行的 option byte 写入脚本带偏把生命周期提前推走。等再回来跑 provisioning发现已经晚了。3. 从失败日志倒推根因我按这个顺序检查板卡状态3.1 第一步确认当前生命周期状态遇到 provisioning 失败不要急着换镜像、不要改脚本、更不要反复执行同一套操作。先用 STM32CubeProgrammer 把板子的状态读出来STM32_Programmer_CLI.exe -c portSWD modeHOTPLUG -ob displ重点看几个字段TZEN、SECBOOTADD0R、DBG_AUTH以及芯片当前的生命周期。H5 的生命周期不会直接以上面的形式显示出来有时要通过Option Bytes里的 RSS 相关位去判断。如果看到板子已经不在OPEN状态那之前的失败原因就基本锁定了。我那个板子第一次失败后就是在这条命令里看到TZEN1没问题但SECBOOTADD0R指向的地址和例程里STiROT_Appli的链接地址对不上。也就是说某一次操作把 option byte 里的安全启动地址改掉了而后面的 provisioning 还按默认地址去写自然写不进去。3.2 第二步区分是“烧不进”还是“验证不过”失败日志有两种典型表现千万别混在一起排查。一种是在烧写阶段就报错比如Flash write error、Data download failed。这类通常和地址、option byte、flash 擦除权限有关属于“根本没烧进去”。另一种是烧写看着成功了但复位后 UART 上没有输出或者输出了OEMuROT authentication failed。这类是烧进去了但签名验证没过问题出在密钥不匹配、镜像链接地址不对、或者 OTP 里的公钥和当前镜像对不上。把这两种情况分清楚排查范围就缩小了一半。前者往地址、生命周期、调试接口配置上查后者往密钥、证书、签名工具链上查。3.3 第三步查看 OTP 残留内容如果板子不是第一次跑 provisioning那么即便当前显示OPENOTP 里也可能残留了上一次的公钥哈希。H5 的 OTP 是按位烧写的很多位只能从 1 变 0不能翻回去。你第二次跑 provisioning 时如果脚本尝试写入一个已经是 0 的位可能不会直接报错但最终验证阶段会因为“OTP 中的哈希和当前工程配置不一致”而失败。所以一定要养成一个好习惯每次跑 provisioning 前先把工程里Keys目录下的公钥文件导出来和 OTP 里实际存的值做一次比对。比对方法不复杂STM32CubeProgrammer 可以读 OTPSTM32TrustedPackageCreator 可以看到你工程里当前使用的公钥哈希。两边哈希对得上再继续。失败现象最常见原因优先排查方向烧写阶段Flash write errorSECBOOTADD0R 或地址配置不对Flash 写入被 ROT 拒绝读取 option byte核对链接地址复位后 UART 无输出provisioning 中断生命周期卡在中间态读生命周期检查是否需要恢复出厂能启动 STiROT 但 OEMuROT 认证失败OTP 公钥与当前项目密钥不匹配核对 Keys 目录公钥与 OTP 哈希第二次运行 provisioning 时失败防回滚或 OTP 位已被永久烧写换板卡或确认当前板子是否支持完全擦除脚本执行到一半 ST-Link 断开复位时序问题或供电不稳定检查调试器连接关闭自动复位选项3.4 第四步检查你的 STM32CubeProgrammer 版本这个点容易被忽略但我这次踩了。H5 系列的 provisioning 脚本会调用 STM32CubeProgrammer 的新功能尤其是对 STiROT 区域进行操作时老版本可能不认识新的 OTP 布局或者不认识脚本里传入的.obk参数。表现就是脚本在同一个地方反复报错但你手动用命令行烧写却能成功。我的经验是跑 H573 的 STiROT_OEMuROT 例程前直接去 ST 官网下载最新版 STM32CubeProgrammer别用 IDE 自带的旧版本也别用之前调试其他芯片时装的版本。版本不匹配产生的错误提示可能非常离谱比如乱码、找不到文件、参数错误但它们背后其实是同一个问题。4. 示例工程里最容易动错的地方Key、模式、链接地址4.1 默认 Key 与替换 Key 的坑官方例程自带一组演示用 Key放在工程目录下。第一次跑通之前肯定是用这组 Key没问题。但很多人拿到例程后第一件事就是把 Key 换成自己的这本身是正确的但要注意替换的是一整套而不只是某一个文件。STiROT 在启动时要验证 OEMuROTOEMuROT 在启动时要验证用户 App。这中间涉及不只一把 Key至少包括 STiROT 用来验证 OEMuROT 的公钥、OEMuROT 用来验证 App 的公钥以及可能存在的调试认证密钥。你只替换了其中一把另外一把还是旧的那么 provisioning 写入 OTP 的哈希和后续镜像验证的哈希就会不一致表现出来就是烧写成功但启动失败。我自己吃过一次亏替换了 OEMuROT 的密钥但忘记更新OEMuROT_Appli工程里内置的那份根公钥。结果 provisioning 全流程跑完它告诉我成功可每次复位都停在 OEMuROT 认证失败。排查了很久才发现OTP 里写入的是新公钥的哈希但 OEMuROT 镜像里用来验证 App 的根公钥还是旧的两边谁都不认谁。4.2 Development 模式与 Product 模式例程源码里通常会有一个配置开关用来区分Development模式和Product模式。这两个模式的行为差异很大。在 Development 模式下provisioning 脚本会尝试保留调试能力允许你后续继续连接调试器方便开发迭代。而在 Product 模式下脚本会尽量关闭调试接口把芯片推向更封闭的状态最终的生产板就是用这个模式来 provisioning 的。如果你的目的是先在开发板上跑通例程那就一定要确认当前处于 Development 模式。否则你可能在第一次 provisioning 时就把调试口锁了后面每调试一次都要用调试认证证书解锁一次非常痛苦。更糟的是有些板子在 Product 模式下执行到一半失败调试口已经锁了但 provisioning 还没完成这时候你能做的事情非常有限。4.3 链接地址必须和脚本、XML 配置三处一致H5 例程的 Flash 布局相对固定一般是STiROT 位于0x0C000000附近OEMuROT 跟在后面用户 App 再往后。但具体偏移会随例程版本和板卡型号微调。这些地址不会只出现在一个地方链接脚本里有provisioning 脚本里有STiROT_Config.xml 配置文件里也有。如果只改了链接脚本没改 XML那 STiROT_Config 在打包镜像时会把 OEMuROT 镜像放在错误的偏移位置烧写进去自然不对。如果只改了 XML没改链接脚本镜像本身则可能是按旧的偏移编译的结果同样不对。所以动地址之前先搜索整个工程目录把所有相关的地址定义都找出来。至少包含STM32CubeIDE链接脚本里的FLASH_ORIGIN、provisioning 脚本里的烧录偏移、STiROT_Config 里的镜像槽位地址。三者保持一致才能进入下一步。4.4 镜像格式选对了吗H5 provisioning 用到的镜像不是直接烧原始.bin而是通过 STM32TrustedPackageCreator 打包成带签名头、带元数据的格式。如果 STM32CubeProgrammer 在烧写时报“无法识别文件格式”那大概率是打包这一步出了问题而不是烧录问题。打包时还要注意选对目标芯片型号。H573 和 H563 虽然同属 H5 系列但在某些安全特性的细节上有差异。选错目标可能导致包结构不匹配最终 provisioning 失败。5. 救回板卡的完整重跑顺序Open 状态下可用先说清楚这套顺序只适用于芯片生命周期还在OPEN或可以恢复到OPEN的情况。如果你的芯片已经进入LOCKED那就不是软件层能救的了下面操作请直接跳过。5.1 恢复出厂状态把 ST-Link 连接到板子打开 STM32CubeProgrammer选择HOTPLUG模式连接。注意不要用默认的 connect 模式HOTPLUG 模式可以避免触发芯片上电后的安全启动流程避免板子卡在一个半初始化状态里启动不了。连接成功后执行全片擦除和 option byte 恢复STM32_Programmer_CLI.exe -c portSWD modeHOTPLUG -ob erase接着执行STM32_Programmer_CLI.exe -c portSWD modeHOTPLUG -e all擦除之后确认一下生命周期状态是否回到了OPEN。如果还是不行可能需要在 option byte 里手动恢复TZEN1因为 STiROT 是基于 TrustZone 的安全方案TrustZone 必须使能。5.2 按顺序编译三个工程恢复出厂后打开STM32CubeH5包里的STiROT_OEMuROT例程按官方 README 的顺序编译一般是先编译STiROT_Appli再编译OEMuROT_Appli最后编译用户 App。不要跳步也不要并行编译。因为后一个工程生成镜像时可能会引用前一个工程的输出文件编译顺序乱了后面的镜像可能拿不到前面的产物。编译结果要放在 example 工程预期路径下。有些脚本会从固定路径读取镜像如果路径不对脚本不会提示“找不到文件”而是会拿着上一次编译的旧镜像去打包那个旧镜像可能用了旧的 Key 和旧的地址。5.3 重新生成密钥包如果不想用默认 Key如果你已经决定用自己的一套 Key那么在 provisioning 之前把所有密钥文件重新生成一遍然后确认 Key 目录下这几个文件同时被更新了STiROT 用来验证 OEMuROT 的公钥/私钥OEMuROT 的签名密钥调试认证密钥生成完密钥后重新编译那几个 App 工程。这一步不能省。很多人改完 Key 不重新编译还是拿旧镜像在跑结果 provisioning 写进 OTP 的是新 Key镜像里的旧 Key 和它完全不匹配。5.4 运行 provisioning 脚本官方脚本通常叫provisioning.bat或者由 STM32CubeProgrammer 直接执行的命令序列。运行前把板子上的跳线、供电、ST-Link 连接都确认一遍。provisioning 过程中会有几次复位操作这是一次非常容易造成调试器掉线的过程。我在跑的时候遇到过一次 ST-Link 掉线结果脚本停在一个很尴尬的位置最后只能重新擦除再来。跑完脚本后不要急着拔线先用 UART 或串口工具观察板子的启动输出。正常情况下你应该能看到 STiROT 启动、OEMuROT 启动、然后用户 App 启动的日志。看到这些输出才说明 provisioning 真的成功了。6. 防回滚、安全调试与第二次失败的真相6.1 防回滚计数器是怎么“坑”人的STM32H5 的 ROT 里有一个防回滚机制用来防止攻击者把系统降级回旧版本固件。每次升级 ROT 相关镜像时防回滚计数器的值会保存在 OTP 或一次性可编程区域里。一旦计数器的值增加了就不能再减回去。这就解释了为什么很多人在第一次 provisioning 失败后再跑一次还是失败而且失败的方式一模一样。如果你第一次已经烧掉了防回滚相关的 OTP 位那么第二次无论怎么改配置都会因为“当前计数器值已经高于镜像要求的值”而失败。这个失败不是操作问题是硬件状态问题。所以如果你在排查时发现自己的板子已经执行过不止一次 provisioning而且每次失败的位置都一样建议先停下来认真确认防回滚状态而不是继续重试。继续重试只会消耗板子上有限的 OTP 位甚至可能把本来还能用的板子彻底搞进LOCKED。6.2 安全调试与调试认证provisioning 成功后普通调试接口通常会被禁用。这是 STiROT 安全启动的设计目标之一防止攻击者通过调试口读取固件或篡改内存。但作为开发者你还是需要继续调试的能力所以 ST 提供了安全调试机制也就是用调试认证证书来解锁调试端口。这个机制需要提前把调试认证公钥烧进 OTP。如果之前没有配置那 provisioning 成功后调试口就真的打不开了只能回家搬救兵。所以如果你还要继续开发请一定在 provisioning 前确认调试认证相关配置已经就位。我建议在开发板上把调试认证密钥也一并配置好哪怕你现在不需要调试。否则后续想在真机上抓一个运行时的 bug会发现自己根本连不上设备。6.3 第一次成功之后别把 Key 文件留在工程目录里这是一个经验层面的提醒。provisioning 一旦成功芯片里已经刻下了你那套 Key 的哈希。此时工程目录里那堆私钥文件就成了最敏感的东西。私钥泄露意味着任何人拿到你的固件就可以签名出一份合法的 OEMuROT 镜像在支持相同公钥的设备上运行。在做产品批量烧录时更合理的做法是把私钥放到专门的签名服务器或 HSM 里工程开发机上只保留公钥和用于测试的演示密钥。生产环境的 provisioning server 上再使用最终私钥做一次签名。这样即使开发机被拷贝也不会直接泄露产品私钥。我自己现在跑这类安全启动例程时会专门维护一个“Key 清单”记录哪个板子烧了哪套 Key哪个目录里的 Key 是测试用的哪个是产品用的。这个习惯让我少踩了很多坑。因为很多时候你拿开发板的 OTP 哈希去和产品 Key 比对是永远也对不上的你会以为板子坏了其实只是 Key 用错了一套。6.4 如果一切恢复无望如何判断是换芯片还是换板子如果已经确定板子进了LOCKED状态并且现有的调试认证证书也无法解锁那你面临的就是一个“换板”决策。不过在此之前可以看看 STM32CubeProgrammer 里是否有“取消安全”或“开发模式恢复”的选项不同批次、不同样片的状态可能不一样有些样片允许通过特定方式回到开发状态但正式量产芯片基本不会给你这个后门。我个人的判断标准是如果板子还能通过 HOTPLUG 模式连接且 option byte 还能读出来那就还有尝试余地如果连 HOTPLUG 连接都失败基本可以确定纯硬件层已经锁死别浪费时间了。这时候把芯片拆下来换一片或者直接换一块开发板反而是成本最低的解决方案。最后再分享一个小技巧遇到 provisioning 失败先把 STM32CubeProgrammer 的完整日志保存下来。这个日志看起来啰嗦但它记录了每一次复位、每一次 OTP 写入、每一次擦除操作是定位问题的第一手资料。很多人只看最后一行报错然后就再也找不到根因了。其实往前翻几页往往能看到真正出问题的那一步。