背景
最近在给 PD305 项目(THA6510,ARC HS 五核,10MB PFlash)开发 A/B 双 Bank 备份刷写 + CAN 强刷方案。这套方案依赖芯片的硬件 Swap:低 8MB PFlash 分成两个 4MB Bank,SSW 上电时根据 DFlash 里的 UCB Swap 记录,把激活 Bank 重映射到逻辑 0x80000000 窗口,APP 永远按 0x80000000 基址链接,Boot 代码放在交换窗口之外(0x80800000+),两个 Bank 共用一份。
刷写侧的设计是:诊断仪下发的下载地址永远是链接地址,Bootloader 在擦写时加一个偏移,把数据落到非激活 Bank,刷完写有效标志、更新 Swap 记录、复位,硬件按新记录重映射,完成切换。
开发完成后,出现了一个非常规律的现象。
现象:第一次永远正常,第二次必挂
调试器刷入代码后:
- 第一次 CAN 刷写完全正常——擦除、烧写、CRC 校验、写有效标志、切区,一气呵成;
- 复位后 ECU 跑在新 Bank 上,诊断响应一切正常;
- 第二次 CAN 刷写,必挂在擦除命令(
31 01 FF 00)上——擦除例程发出后,ECU 约 52 秒无任何响应,然后疑似复位,回到 APP。
抓 trace 分析,有两个关键细节:
- 擦除挂起期间,ECU 对
90 01的响应只要 0.3ms(主循环空闲),但10 02要 734ms 才回50 02——这是 APP 收到编程会话请求后复位进 Boot 的耗时,证明挂起后跑的是 APP,不是 Boot; - 复位后旧 APP 完好无损,说明激活 Bank 一个字节都没被擦到。
也就是说:擦除例程执行过程中 ECU 异常复位了,但擦除实际上没有生效。
排查链:两个被证伪的假设
假设一:刷写偏移错了,擦到了激活 Bank(被证伪)
最初判断:映射 B 激活后,代码的偏移逻辑是"跑 B 时 offset=0",擦除地址直接打在逻辑 0x80000000——也就是激活 Bank 上,可能被硬件保护机制阻挡导致复位。
于是把偏移改成:Swap 使能时 offset 恒为 +0x400000。理由是无论映射 A 还是 B,非激活 Bank 在逻辑地址空间恒位于 +0x400000(映射 B 时逻辑 0x80400000 指向物理 Bank A)。
实测:依然挂,现象一模一样。
假设二:BS=0 流控移植引入的回归(被证伪)
第二次怀疑对象是前一天刚移植的 BS=0 优化——0x36 数据传输改成"一次流控收完整条报文、收齐再烧",流控帧从每 9 帧一个降到整条报文一个。改动量大,嫌疑也大。
把 BS=0 逻辑完整回退到原来的"每收满一个 BURST 烧一次、烧完发流控帧"(幸好移植前留了备份,按 diff 做了 13 处精准回退,保留了 31 03 切区等新功能)。
实测:依然挂。
两个假设都被证伪后,结论反而清晰了:问题与诊断接收路径无关,就是切区本身导致的——只要 ECU 跑在映射 B 状态下,擦除操作本身就必挂,跟擦哪个 Bank、用什么流控方式都没有关系。
根因:NVM 寄存器块号没跟着重映射
重新梳理硬件 Swap 的地址模型,问题浮出水面:
映射 A(恒等映射):
逻辑 0x80000000-0x803FFFFF → 物理 Bank A(块 0/1)
逻辑 0x80400000-0x807FFFFF → 物理 Bank B(块 2/3)
映射 B(两个 4MB 半区互换):
逻辑 0x80000000-0x803FFFFF → 物理 Bank B(块 2/3)
逻辑 0x80400000-0x807FFFFF → 物理 Bank A(块 0/1)
THA6510 的 NVM 控制器有 5 组 PFlash 控制寄存器(0xF00A0000 起,每 2MB 物理块一组),寄存器组按物理块编号,不参与 Swap 重映射。而擦除的触发方式是:先往块 N 的寄存器写擦除命令序列(ETYP + SDP 解锁),再往目标扇区地址写一个触发值,由地址译码决定擦哪个物理块——这个触发写会经过 Swap 重映射。
再看代码里的块号查找函数 GetBlockIndex():它用逻辑地址在静态块表(0x80000000、0x80200000……)里查出块号,然后用 PFLASH0_BASE + 块号 × 寄存器组大小 选寄存器组。
映射 A 下,逻辑地址 = 物理地址,块号永远正确——所以第一次刷写永远正常。
映射 B 下,两者必然错开。以 offset 恒 +0x400000 为例:擦除逻辑地址 0x80400000,GetBlockIndex 查出块 2,命令序列发给块 2 的寄存器;但触发写经硬件重映射后落在物理块 0(Bank A)。命令发给块 2、触发落在块 0,NVM 状态机直接错乱——挂死、等不到完成标志、最终复位。这完美解释了"擦除没生效、APP 完好、ECU 复位"的全部现象。
之前两种 offset 改法都失败也是同一个原因:问题从来不在"擦哪个 Bank",而在寄存器块号与物理目标块对不上。
修复:两处必须成对修改
修复 1:GetBlockIndex 按物理地址查块号
static uint8 Bootloader_FlashPFlashGetBlockIndex(uint32 addrUI)
{
uint8 idxBlockUB = 0;
Bootloader_Fls_infoBlockCfgS_TYPE *addrBlockCfgPS = NULL;
uint32 addrPhyUI = addrUI;
#if BOOTLOADER_CFG_FLASH_SWAP_ENABLE == STD_ON
/* Swap映射B激活时,低8MB窗口的两个4MB半区被硬件互换,NVM控制寄存器组
按物理块编号,必须先把逻辑地址换算成物理地址再查块号 */
if ((1U == Bootloader_bSwapEnUB) &&
(BOOTLOADER_SWAP_INDEX_BANK_B == Bootloader_idxSwapBankUB) &&
(addrUI >= BOOTLOADER_CFG_FLASH_ADDR_PFLASH_BEGIN) &&
(addrUI < (BOOTLOADER_CFG_FLASH_ADDR_PFLASH_BEGIN + (2UL*BOOTLOADER_CFG_FLASH_ADDR_OFFSET_SWAP))))
{
addrPhyUI = addrUI ^ BOOTLOADER_CFG_FLASH_ADDR_OFFSET_SWAP;
}
#endif
for (idxBlockUB = 0; idxBlockUB < Bootloader_Fls_infoCfgS.numPFlashBlockUB; idxBlockUB++)
{
addrBlockCfgPS = ...;
if ((addrPhyUI >= addrBlockCfgPS->addrBeginUI) &&
(addrPhyUI < (addrBlockCfgPS->addrBeginUI + addrBlockCfgPS->numSizeUI)))
{
break;
}
}
return idxBlockUB;
}
擦除、页写入、突发写入共 4 个调用点全部经过这个函数,一处修改全覆盖。映射 A 和 Swap 未使能时不做换算,行为与原来完全一致。
修复 2:刷写偏移恒为 +0x400000
if (Bootloader_SwapRead())
{
/* 无论当前映射A还是B,非激活Bank在逻辑地址空间恒位于+0x400000,
因此偏移恒为0x400000,始终刷写非激活Bank */
Bootloader_numSwapOffsetUI = BOOTLOADER_CFG_FLASH_ADDR_OFFSET_SWAP;
}
为什么必须成对? 只改修复 1 不改修复 2(保留 P560 的"跑 B 时 offset=0"):映射 B 下擦除逻辑 0x80000000,换算后块号、触发写都指向物理块 2——这次倒是"对齐"了,但物理块 2 是正在运行的激活 Bank,会把好端端的 APP 擦掉。只改修复 2 不改修复 1,就是本次排查的故障现象。两个修改合起来才闭环。
验证
修复后重新编译、调试器刷入,连续完成 A→B→A 多轮 CAN 刷写,擦除、烧写、CRC 校验、写标志、切区全部正常。之后再把 BS=0 优化叠加回来,多次刷写依然稳定。
总结
- 硬件 Swap 方案的通用坑:凡是"地址重映射 + 按块编号的 Flash 控制寄存器"的架构(THA6510、AURIX UCB Swap 同类),擦写路径上所有"由地址推导资源编号"的逻辑,都必须明确用的是逻辑地址还是物理地址。这类 bug 在映射 A(恒等映射)下完全隐形,只有切到 B 才暴露——而很多项目的 A/B 功能恰恰很少真正切到 B 测过。
- 实现逻辑:原实现逻辑里"跑 B 时 offset=0"与 CRC 读取固定
+0x400000的不对称,当时就存疑但没验证,最终还是不得不验证映射 B 下是否能真正跑通,喜提来自懒惰的教育。 - 排查方法论:这次两个假设(offset 错、BS=0 回归)都是"看起来最像"的方向,逐个证伪后才逼出真正的根因。关键证据始终是 trace 里的两个时间差——0.3ms 的
90 01和 734ms 的50 02,它们证明了"复位后跑的是 APP 且 APP 完好",把排查方向从"刷写流程"扭转到"擦除操作本身"。


