ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32L4P5 OTFSPI实战:SPI Flash固件加密解密与避坑指南

STM32L4P5 OTFSPI实战:SPI Flash固件加密解密与避坑指南 做嵌入式这几年我拿到过不少次“新片调试两行泪”的剧本。最近一次是给客户产品做固件防抄方案他们指定了STM32L4P5理由是这颗料内置了一个叫 OTFSPI 的外设。当时我还在群里问“ocotspi 是啥”同行回复说就是 OTFSPIOn-the-fly SPI decryption engine顺手把数据手册丢了过来。看完第一遍我差点放弃因为它的配置流程比普通外设繁琐不少而且网上能查到的实战案例特别少几乎全是英文手册里干巴巴的寄存器描述。啃完手册再踩完硬件上的各种坑前后折腾了两周多终于把整套链路跑通了。这篇文章就是把我从“看懂原理”到“跑通解密启动”再到“排查各种疑难杂症”的完整过程记录下来。主要面向正在调 STM32L4P5 SPI NOR Flash 加密启动的工程师也适合准备用 OTFSPI 做固件安全保护、又不想在项目里反复试错的朋友。我会从 OTFSPI 到底是什么、为什么值得用到具体配置参数怎么算、代码怎么写再到我实际踩过的坑和排查手法完整走一遍。不敢说覆盖所有 issue但至少能帮你少走几天的弯路。1. OTFSPI 是什么为什么它值得折腾1.1 没有硬件解密引擎时固件安全有多难做先说个背景。很多产品用外部 SPI NOR Flash 存固件因为容量大、便宜、可现场升级。但这么做有个绕不开的问题Flash 里的固件是明文抄板的人只要拆机用烧录器把 Flash 内容读出来整个程序就裸奔了。更麻烦的是如果产品有联网功能别人拿到明文固件还能分析通信协议、挖漏洞等于把你的家底全看光了。常见的应对方案有几种。第一种是把固件在 PC 端用 AES 加密芯片上电后由 Bootloader 把密文读进 RAM再用软件解密最后跳转到 RAM 执行。这个方案能用但问题不少第一解密占 CPU 时间固件越庞大启动越慢第二需要足够大的 RAM 来容纳解密后的固件对低成本的 MCU 来说很奢侈第三Bootloader 本身还是明文的除非你连 Bootloader 也做安全启动否则攻击者可以直接改 Bootloader。所以就有了第二种方案硬件解密引擎。STM32L4P5 的 OTFSPI 就是干这个的。它的思路很直接把解密逻辑做成一个硬件模块挂在 CPU 访问 SPI Flash 的路径中间。CPU 照常从 0x90000000 这个 memory-mapped 地址读数据OTFSPI 发现这个地址命中了配置好的加密区域就从 SPI Flash 里把密文读出来解密之后把明文返回给 CPU。CPU 以为自己在读普通 Flash实际上数据已经被实时解密了。1.2 OTFSPI 的系统架构和工作流程我们看一张简化后的数据通路。在 STM32L4P5 里CPU 访问外部 SPI Flash 有两个路径一是通过 QUADSPI 的 memory-mapped 模式把 Flash 映射到 0x90000000 开始的地址空间二是通过 QUADSPI 的间接模式用寄存器读写。OTFSPI 只作用于前者。具体流程是这样的CPU 发起一个读请求访问地址落在 0x90000000 到 0x9FFFFFFF 的映射区间。OTFSPI 拿到这个地址和自己的 region 配置表比对。如果命中了某个加密区域OTFSPI 会通过 QUADSPI 从 Flash 中读取对应位置的密文然后在硬件内部用 AES-CTR 解密最后把明文数据返回给 CPU。如果地址没有命中任何 region那就原样透传CPU 读到的还是 Flash 里的原始数据。这个架构的最大好处是解密过程对 CPU 完全透明。你不需要在应用代码里手动调用解密函数也不需要把固件搬到 RAM 里。CPU 可以像执行普通 Flash 里的代码一样直接从 0x90000000 地址取指执行I-Cache 和 D-Cache 照常工作。只要第一次访问某个 cache line 时多等那么几个周期的解密延迟后续命中 cache 就完全无感了。1.3 OTFSPI 适合什么场景不适合什么场景从我实际应用看OTFSPI 最适合的场景是固件主体存放于外部 SPI Flash希望防抄板、防篡改同时对启动速度有一定要求的产品。比如带 GUI 的智能家电、工业控制器、采集终端等。因为固件可以在加密区域直接执行省掉了“复制到 RAM 软件解密”的时间。但也要泼一盆冷水。OTFSPI 保护的是 Flash 里的静态固件内容它不能替代安全启动Secure Boot也不能抵抗专业的侧信道攻击和芯片开盖分析。它解决的是“别人把 Flash 拆下来用编程器读数据”这种最常见的抄板威胁。如果你的产品要面对国家级攻击者那你需要的是一颗带专用安全子系统的芯片而不是靠 L4P5 这颗通用 MCU 硬扛。这个预期管理很重要我见过不少工程师以为开了 OTFSPI 就万无一失了结果被各种降维打击。另外如果你的需求只是防止别人篡改固件而不在意内容泄露那更合适的是加签名校验而不是加解密。OTFSPI 只负责解密不负责完整性校验这两件事在产品设计时要分开考虑。2. 配置 OTFSPI 前必须想清楚的几个关键参数2.1 地址映射和 region 规划是第一步配置 OTFSPI 最让人头疼的不是寄存器操作而是地址规划。你需要先想清楚三个地址明文地址、密文地址和 Flash 物理地址。QUADSPI 的 memory-mapped 地址从 0x90000000 开始。当一个加密区域配置好之后CPU 访问 0x90000000 offset 时OTFSPI 会去 Flash 的某个物理位置读取密文。这个偏移关系非常关键。举个例子。假设你的 SPI Flash 总容量是 16MB你打算在 Flash 开头放一个 64KB 的明文 Bootloader从偏移 0x10000 开始放加密的应用程序固件加密区域的大小是 256KB。那么明文地址CPU 访问的地址0x90000000 0x10000 0x90010000密文地址Flash 里实际存放密文的偏移即 SADDR0x10000这个映射关系里明文基址和密文基址的偏移可以一样也可以不一样。OTFSPI 的 region 配置里有一个 SADDR 字段用来指定密文在 Flash 里的偏移。如果你在加密固件时固件头部设计为“前 64KB 明文 后 256KB 密文”那你很可能需要配置两个 region或者干脆把布局设计成“独立的明文区和独立的密文区”。还有一个很容易踩的坑region 大小必须符合对齐要求。在 STM32L4P5 上OTFSPI 的 region 大小是按 2 的幂次来配置的比如 2KB、4KB、8KB……而且基地址也要和大小对齐。如果你的加密区域实际只有 100KB但配置成 128KB 或者 256KB 的 region那 region 覆盖范围会超出你的密文区域。超出的部分OTFSPI 会把它当作合法密文去解密如果 Flash 那边对应位置是 0xFF解密出来的就是一堆乱码然后你还得费劲排查半天。所以我的建议是在设计 Flash 布局时就把 Bootloader、加密固件、配置参数、文件系统这些分区按 2 的幂次来规划留足对齐余量后面能省掉很多麻烦。2.2 密钥怎么管理直接写死在代码里行不行密钥管理是 OTFSPI 方案里最容易被轻视的一环。很多人调通第一个版本之后为了图省事直接把 AES Key 明文写在初始化代码里。这种做法在开发阶段没问题但进入量产就非常危险因为你的固件一旦被反汇编密钥就漏了整个 OTFSPI 形同虚设。在 STM32L4P5 上OTFSPI 的密钥是存放在专用的寄存器区域里的硬件层面有写入保护。正常情况下软件在启动早期把密钥写入 OTFSPI 的 KEY 寄存器然后通过置位 LOCK 位把寄存器锁住避免后续程序意外修改。密钥寄存器的内容不能通过调试接口读取这点比把密钥放在普通 RAM 里要安全得多。量产时的密钥注入方案我见过几种一是工厂在产线上下发密钥通过烧录器或自定义通信协议写入片内 OTP/选项字节区域Bootloader 启动时从那里读取并配置给 OTFSPI二是同一产品所有设备共用一套密钥密钥只存储在 Bootloader 镜像里配合 RDP读保护防止别人读出 Bootloader。第一种安全性更高但产线流程复杂第二种胜在简单适合威胁模型不高的产品。这里特别提醒一下如果你打算在开发板上先用 ST-LINK 调试又同时开启了 RDP 保护那么密钥寄存器和 Flash 内容都会受到调试接口访问限制调试体验会比较痛苦。我的经验是开发阶段把 RDP 关掉所有功能调通后再做安全收紧。2.3 AES-CTR 模式的 counter 处理加密脚本必须和硬件对齐OTFSPI 在硬件内部使用的是 AES 的 CTR 模式。CTR 模式的原理是用 AES 加密一个递增计数器得到密钥流再把密钥流和明文按字节异或得到密文。解密时用同样的方式生成密钥流和密文异或恢复明文。这意味着你在 PC 端用脚本加密固件时必须保证密钥流生成方式和 OTFSPI 硬件完全一致。最简单、最容易验证的方案是使用全零的 16 字节 IV计数器从 0 开始按 16 字节一个 AES block为单位递增。加密时按地址顺序从低到高处理每个 block。OTFSPI 在解密时会自己根据 region 的基地址和偏移计算对应的计数器值。只要你的加密端约定 IV 为全零、计数器从 0 开始且密文在 Flash 里的排列顺序和地址递增方向一致就能正确解密。如果不一致会怎样最常见的现象是解密出来的前 16 字节是乱的后面的数据是正常的。这通常意味着加密端脚本的 IV 处理逻辑和硬件不完全对齐。还有一种是整个区域解出来的全是乱码但用没配置 key 的透传模式能读到正常的密文这种情况多半是 region 基地址的计数基准错了。所以我在项目里会把加密脚本单独抽成一个 Python 工具输入是明文固件 bin输出是密文 bin。脚本里写上详细的注释IV 全零、CTR 递增步长 16 字节、从偏移 0 开始。这样以后交给同事维护或者换项目的时候至少能少踩一次坑。2.4 多个 region 的适用场景OTFSPI 支持多个 region我印象中最多可以配置 8 个。每个 region 都有自己的基址、大小、SADDR 和密钥配置。多 region 有什么实际用处举个例子。你的产品需要远程升级升级时 Bootloader 先把新固件密文写入 Flash再跳转到应用。如果应用固件被 OTFSPI 解密执行了那么应用内部想再去读升级包的完整性校验值就不能直接读 Flash 的密文了只能通过 QUADSPI 间接模式去读。而 Bootloader 是明文的不受 OTFSPI 影响。这种场景下你至少需要两个 region 的规划思路一个是加密的 App 区域一个是可选的加密参数区域。不过我要说的是多 region 的复杂度成倍增加很多时候反而是个坑。我自己的习惯是“能用单 region 就不上多 region”只有确实需要区分不同安全等级的存储区域时才配多个。不要为了炫技把配置复杂度拉满后面调试起来真的会想哭。3. 实操过程从 PC 端加密到板端解密运行3.1 开发环境准备我这次用的硬件是 NUCLEO-L4P5ZG 开发板加一块外接的 SPI NOR Flash 模块Flash 型号是 W25Q128JVSIQ容量 16MB。软件方面用到了 STM32CubeMX 6.10、STM32CubeIDE、Python 3.10以及 PyCryptodome 库。烧录用 STM32CubeProgrammer调试用板载 ST-LINK。在 CubeMX 里需要配置的有三块一是 QUADSPI打开并设置为 Memory-mapped 模式二是 OTFSPI直接使能三是时钟树确认 QUADSPI 的时钟源和分频满足 Flash 的最高工作频率。这里有个细节OTFSPI 的时钟和 QUADSPI 共用一个时钟源如果分频配得不合适Flash 高速读会出错表现起来特别像解密失败。我当时就吃过这个亏后面会详细说。3.2 PC 端加密镜像的 Python 脚本加密脚本的核心逻辑不复杂但写的时候要严谨。我的脚本长这样关键部分做了注释from Crypto.Cipher import AES import sys def encrypt_firmware(plain_bin, enc_bin, key_hex, iv_hex00*16): key bytes.fromhex(key_hex) iv bytes.fromhex(iv_hex) # 使用 CTR 模式初始 counter 为 0 cipher AES.new(key, AES.MODE_CTR, initial_valueiv, nonceb) with open(plain_bin, rb) as f: data f.read() # 自动按 block 递增pycryptodome 内部处理 encrypted cipher.encrypt(data) with open(enc_bin, wb) as f: f.write(encrypted) if __name__ __main__: key 00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff encrypt_firmware(app_plain.bin, app_enc.bin, key)这里我用的是 AES-256密钥 32 字节。PyCryptodome 的 CTR 模式默认 counter 递增方式是按 16 字节 block 递增只要 IV 是全零就和 OTFSPI 的约定一致。实际项目中密钥我不会写死在脚本里而是从环境变量或独立配置文件读取避免密钥随脚本仓库泄露。加密完成后我需要用工具把镜像拼起来前 64KB 是明文 Bootloader偏移 0x10000 开始放加密后的 App。Flash 布局大概是下面这样Flash 偏移内容大小是否加密0x000000Bootloader明文64KB否0x010000App 密文256KB是0x050000参数区64KB否3.3 OTFSPI 初始化代码我用的是寄存器方式操作因为 CubeMX 生成的 HAL 库对 OTFSPI 的封装在不同版本里变化比较大。用寄存器方式虽然啰嗦但至少每个动作在干什么我都清楚。void OTFSPI_Init_Region(void) { // 1. 使能 OTFSPI 外设时钟 __HAL_RCC_OTFSPI_CLK_ENABLE(); // 2. 配置 region 0 // 明文基址: 0x90010000region 大小 256KB // 注意基址和大小都是 2 的幂次对齐 // SADDR 指向 Flash 中的偏移 0x10000 OTFSFI-R0CR (0x10000 16) | 0x0000; // 基址配置 OTFSFI-R0SZ 0x0000; // 256KB OTFSFI-R0SADDR 0x10000; // 密文偏移 // 3. 写入密钥 KEY0/KEY1 // 先确保 LOCK 被清除 OTFSFI-CR ~OTFSFI_CR_LOCK; OTFSFI-KEY0_0 key[0]; OTFSFI-KEY0_1 key[1]; OTFSFI-KEY0_2 key[2]; OTFSFI-KEY0_3 key[3]; OTFSFI-KEY1_0 key[4]; OTFSFI-KEY1_1 key[5]; OTFSFI-KEY1_2 key[6]; OTFSFI-KEY1_3 key[7]; // 4. 锁定密钥 OTFSFI-CR | OTFSFI_CR_LOCK; // 5. 使能 OTFSPI OTFSFI-CR | OTFSFI_CR_EN; }这段代码里寄存器名称在不同参考手册里可能略有差异我建议以你手头那份 RM0467 最新修订版为准。重要的是流程先开时钟、再配 region、再写 key、再锁 key、最后 enable。顺序尽量不要反尤其是先 enable 再写 key运行起来会有莫名其妙的行为。3.4 验证解密结果代码烧进去之后最简单的验证方式是直接读 0x90010000 地址和原始明文固件的前几个字节做对比。我习惯在调试器里看内存窗口或者写几行测试代码uint32_t *p (uint32_t*)0x90010000; printf(0x%08x\n, p[0]);如果输出和我 PC 端加密前的明文文件头一致说明解密链路是通的。如果不一样先不要慌按第 4 章的排查步骤走一遍。4. 那些让我熬了几个通宵的典型问题4.1 解密出来全是乱码先别怀疑硬件我遇到过最典型的“开始解出来全乱”的情况原因是加密脚本里用错了 IV。当时我在 Python 脚本里写成了nonceb0000000000000000PyCryptodome 把 nonce 当成 counter 初值的一部分导致每个 block 的计数器跟硬件对不上。结果就是整片数据解密失败。后来我把脚本改成手动管理 counter打印每个 block 的 counter 值和硬件对账终于发现问题。这个案例说明如果你在 PC 端加密后板端解出来是乱的第一优先级永远是检查脚本和硬件的 CTR 参数是否完全一致而不是怀疑芯片坏了。我的建议是写一个极小的测试镜像固定 16 字节明文用脚本加密成 16 字节密文固化到 Flash板端解密取回这 16 字节跟明文比对。如果这个都过不了问题基本就锁定在加密端约定或 region 地址配置上。再补充一个常见的原因region 配置里的明文基址算错了。QUADSPI memory-mapped 的基地址是 0x90000000如果我把 region 基址配成了 0x90020000而密文实际在 Flash 偏移 0x10000那 CPU 读 0x90020000 时OTFSPI 会去 Flash 的什么位置取密文呢答案是 0x20000。如果那个位置不是你加密的数据那解出来自然不对。这个错位在调试时非常隐蔽因为程序读起来不报错只是数据内容全错而且你还会一直怀疑是代码跑飞了。4.2 Cache 一致性动不动就让调试器“骗”了你Cortex-M4 内核有 I-Cache 和 D-Cache。正常工作情况下第一次访问解密区域时数据会经过 OTFSPI 解密然后缓存在 D-Cache 里。如果后续程序再次读取同一地址直接从 Cache 返回不会再走 OTFSPI。这对性能是好事但对调试和固件升级来说是灾难。我最严重的一次翻车是在调试器里改了 Flash 中的密文数据然后重新运行程序发现程序解码出来的还是旧数据。我一度以为是 OTFSPI 卡死没响应反复复位外设都没用。后来才意识到D-Cache 里缓存的是旧数据CPU 读 0x90010000 时根本没有走到 OTFSPI而是从 Cache 直接命中了。解决方法是在修改 Flash 密文、切换密钥或者更新固件后显式地做 Cache clean 和 invalidate。代码里用 CMSIS 自带的函数SCB_InvalidateDCache(); SCB_InvalidateICache();还可以配合__DSB()和__ISB()做屏障指令确保 cache 操作完成后再继续执行。另外如果固件放在 I-Cache 可缓存区域并且你正在执行解密区的代码重新烧录后必须断电重启才能让 I-Cache 失效。调试器复位有时候并不会自动清 I-Cache所以我后来养成了“改完 Flash 就拔掉调试器重新上电”的习惯。4.3 启动慢解密延迟在第一次访问时特别明显OTFSPI 的解密延迟在第一次访问某个 cache line 时会体现出来后面访问靠 cache 就很快。但冷启动时整个 App 区域几乎都要从 Flash 读一遍如果固件很大启动时间会明显拉长。我用 256KB 的 App 做过对比测试。明文情况下从跳转到 App 到进入 main 函数大约 0.8 秒开启 OTFSPI 解密后直接跳到加密区执行时间变成了 1.9 秒。对于用户体验敏感的产品这个差距不可忽略。优化的办法有几个。第一把启动初始化和高频中断函数放在明文区只把核心业务逻辑放在加密区。第二在进入 App 前由 Bootloader 主动把加密区的数据预读一遍比如从头到尾 memcpy 到一段不用的 RAM 缓冲区利用顺序读把 cache 命中率拉上去。第三调整 OTFSPI 的 region 属性如果 ST 支持配置为“非缓存”或“写通”根据实际需求权衡。我个人最常用的是第二种简单粗暴实测可以压缩 30% 的启动时间。4.4 DMA 读不了解密数据是一个绕不过去的设计限制很多工程师做完 OTFSPI 解密之后会自然想到既然 CPU 能从 0x90000000 读解密数据那 DMA 是不是也能从同一个地址读答案是在 STM32L4P5 上DMA 走的是 AHB 总线矩阵而 QUADSPI memory-mapped 区域并不向 DMA 的访问路径开放解密重映射。换句话讲DMA 读 0x90000000 地址要么拿不到数据要么行为未定义。这个限制意味着如果你的应用打算用 DMA 把加密 Flash 里的资源文件批量搬到内存这条路是堵死的。我的替代做法是用 CPU 做一次分段 memcpy把需要的数据从解密区搬到 SRAM然后再交给 DMA 处理。虽然多了一次 CPU 拷贝但至少数据是正确的。还有一种更彻底的方案把需要 DMA 读取的大块数据比如图片、字库放在明文区域只对代码段做加密。这样能规避 DMA 的限制但安全性会降低需要产品团队一起评估。4.5 低功耗模式唤醒后OTFSPI 配置丢了导致 HardFault我做低功耗测试时发现设备从 STOP 模式唤醒后只要一访问 0x90010000 地址就直接 HardFault。排查了很久才发现STOP 模式唤醒后 OTFSPI 外设的状态并没有自动恢复需要软件重新初始化一遍。这个问题的根源是 OTFSPI 和 QUADSPI 在低功耗模式下的行为取决于 RCC 的 reset 控制。有些模式下外设的配置寄存器会丢失有些模式下只是暂停时钟唤醒后还能用。具体行为要看手册里的“low-power mode”章节不同系列可能有差异。我建议的工程做法是把 OTFSPI 初始化逻辑封装成一个独立函数不仅在系统启动时调用也在低功耗唤醒后无条件调用一次。初始化是幂等的多调几次没有副作用这让代码更健壮。另外进入 STOP 模式之前把 OTFSFI_CR 的 EN 位读取出来保存一份唤醒后检查是否需要重新配置能减少不必要的初始化开销。4.6 RDP 和调试之间的矛盾处理不好会把自己锁死OTFSPI 的安全等级很高但它和调试器的关系处理不当坑很大。官方推荐的做法是把 RDP 开启到 Level 1 或以上然后用 OTFSPI 保护固件。但问题是当 RDP 级别大于 0 时调试器对 Flash 和 OTFSPI 密钥寄存器的访问会受到限制你没法在 IDE 里实时调试解密后的代码也没法用烧录器直接读 Flash。有一次我图省事直接在量产配置里把 RDP 设成了 Level 1然后忘了烧录新的 App 密文结果设备启动后 Bootloader 发现 App 校验失败但又没法通过调试器把 Flash 擦干净最后只能走 ST 的 RDP 回退流程才救回来。那种感觉真的很崩溃。给新手的建议是开发调试阶段 RDP 保持 Level 0不要开保护所有功能验证通过后再按量产流程配置 RDP。同时Bootloader 里一定要预留进 recovery 模式的手段比如检测特定 GPIO 引脚电平或者通过串口指令进入升级模式。否则哪天密钥或密文写错设备变砖你只能眼睁睁看着拆焊换芯片。4.7 常见问题速查表现象可能原因处理建议解密数据前 16 字节乱加密端 IV/CTR 与硬件不一致检查脚本 IV 为全零counter 从 0 按 16 字节递增整片数据乱region 基址或 SADDR 偏移错误核对明文基址、密文偏移确认对齐程序跑飞HardFaultCache 残留旧数据 / OTFSPI 配置丢失更新密文或 key 后做 Cache invalidate低功耗唤醒后重新初始化读取速度异常慢OTFSPI 解密延迟 Cache miss启动时预读或仅加密关键代码区DMA 读 0x90000000 无效DMA 不支持 OTFSPI 解密路径改用 CPU memcpy 搬运到 SRAM开启 RDP 后无法调试RDP 保护了 Flash 和密钥寄存器开发阶段关 RDP量产前再开启保留 recovery 通道5. 排查工具与实战技巧5.1 用 STM32CubeProgrammer 直接看内存排查 OTFSPI 问题我最常用的工具是 STM32CubeProgrammer 的 memory dump 功能。连接开发板之后直接在地址栏输入 0x90010000读取 64 字节跟 PC 端明文固件头做对比。如果读出来的是明文说明 OTFSPI 解密生效如果读出来是乱码说明要么加密约定不对要么硬件没有正确配置。这个方法的好处是绕过了应用代码直接看硬件层的输出定位问题更快。我强烈建议在调试初期就做这一步不要等到整个系统跑起来才查。另外Stm32CubeProgrammer 还能修改选项字节方便你切换 RDP 状态但注意这个操作有风险操作前务必先备份 Flash 内容。5.2 在 RAM 里跑调试代码绕开 Flash 引导依赖有一种死锁场景特别隐蔽Bootloader 是明文跳转到 App 时开了 OTFSPI 解密但 App 初始化代码里又调用了依赖解密区数据的外设配置函数。如果 OTFSPI 配置有问题App 一跑就 HardFault而你连打印的机会都没有。为了排查我会把一段测试代码直接放在 SRAM 里运行。具体做法是在调试器里把测试函数加载到 RAM 地址然后修改 PC 指针跳过去执行。这样即使 Flash 里的 App 完全没法跑也能单独验证 OTFSPI 寄存器状态和数据通路。具体操作是在 CubeIDE 里新建一个 RAM 调试配置或者直接用命令行 GDB 设置 PC 和 SP。如果没有经验也可以改为在 Bootloader 里加一个调试命令比如串口收到特定字节后在 RAM 里执行一段测试逻辑。5.3 逻辑分析仪配合抓取 SPI 时序当怀疑 Flash 的连线或 QUADSPI 时序有问题时逻辑分析仪是排查利器。抓取 CS、CLK、IO0-IO3 的波形确认 OTFSPI 发起读操作的地址与预期是否一致。正常的 trace 会显示CPU 访问 0x90010000 地址QUADSPI 从 flash offset 0x10000 开始发起连续读。如果看到地址不对或者 CLK 频率异常问题往往不在 OTFSPI而在 QUADSPI 配置或 Flash 选型。不过逻辑分析仪采样率要够高。W25Q128 在高速模式下跑 80MHz 甚至更高如果你用便宜的 24MHz 采样率的设备抓到波形也是糊的看不出个所以然。我建议至少 200MHz 采样率的逻辑分析仪或者用示波器看关键信号。5.4 分级调试是 OTFSPI 项目最重要的工程习惯整个 OTFSPI 链路涉及四层QUADSPI 时序、OTFSPI 透传模式、OTFSPI 解密模式、应用代码执行。每一层都有可能出问题如果试图一次调通遇到问题你会完全无从下手。我的建议是严格执行下面的分级调试流程不使能 OTFSPI只把 QUADSPI 配成 memory-mapped 模式CPU 直接读 Flash 明文内容先确认 Flash 时序和映射没问题。使能 OTFSPI但不配置 region也不写密钥。此时 OTFSPI 应该透传数据CPU 读到的还是 Flash 原始内容。这一步验证 OTFSPI 不会破坏普通访问路径。配置 region但用固定 key 加密 16 字节测试数据。验证硬件加解密链路是否正确。正式加密整个 App跳转执行验证系统功能。最后再配置 RDP 和量产密钥做安全收尾。每一步都有明确的验证点出了问题能立刻定位到具体层。我就是靠这个流程把一开始各种“莫名其妙”的问题一个一个揪出来的。最后再分享一个小技巧做 OTFSPI 方案时我强烈建议把镜像头部设计成固定 16 字节的明文魔数区比如开头的 arm 向量表保留 128 字节不用。Bootloader 跳转 App 前先读 0x90010000 的前 16 字节判断它和预期的魔数是否匹配。如果匹配说明 OTFSPI 解密路径正常如果不匹配就自动进入升级模式等待新固件。这个小技巧解决了我一个长期痛点总是担心设备出厂后固件被误刷成密文错乱导致变砖。有了这个检测机制Bootloader 可以在跳转前发现问题给现场烧录留一条后路。毕竟 OTFSPI 这把锁一旦锁上要重新打开真的很折腾。希望这篇文章能让你在 STM32L4P5 的 OTFSPI 上少走些弯路顺利拿下这个让人又爱又恨的外设。
返回列表