步骤 3/4(IWDG V2/V4/V5 + 堆低水位 S11):
- V2 任务饿死 stall 后 ≤2.5s IWDG 复位;V4 关中断死循环 ~4.9s 稳定复位;
V5 30min 120×FTP 全 PASS、零 IWDG 复位。堆告警全程未触发(min_ever=4728B)。
- task_watchdog.c:stall 证据写盘会经 nand_ftl_wd_feed 喂狗 → 增 s_stall_evidenced
每次 stall 仅取一次证据,避免"取证据"本身阻止复位。
- default_task.c:堆低水位告警(HEAP_LOW_WATER_B=3072)。
步骤 5(GD5F 错误通道 S9):gd5f2gq5ue.c/.h + nand_ftl.c 错误码统一为 int32_t 并
全量检查返回值;套件回归 16/16 PASSED。
- test/run_all_suites.py:--forbidden 改为按套件覆盖(GD5F 用 "HardFault;assert")——
TC-GD5F-704 故意触发 [ERR] 回落轮询,统一默认值会误判 FAIL。
步骤 6(网络稳定性):7.1 正常路径 ✅(V5 证据);7.2 N-2 丢弃可观测性代码已具备、
49s 静默在 V5 未复现(降级低风险观察项);7.3 N-3 待 PC peer。
步骤 7(工程卫生):
- S5:新增 test-cppcheck 钩子(仅 warning,排除模拟器桩)。
- S4:新增 scripts/check_test_traceability.py(默认仅告警)。
- 待核实项 TEST_SUITE_SELECTED 实机核实 = 死代码(无使用点);RTC 套件实际正常执行,
真缺口 = 缺套件级汇总行 → sd2506_test_task.c 补 === RTC Tests: N/M PASSED === 并
修正 Phase 级汇总累计问题;test_config.h 补齐 RTC 并注释"当前无引用"。
验证:产品 @build 0/0;pre-commit --all-files 17/17 Passed;
run_all_suites.py --suites RTC → PASS。
文档:实机测试实施指南 / SD2506_Test_Guide / 待处理内容 / 测试与架构改进建议 同步。
13 KiB
GD5F2GQ5UE 陷阱记录
Trap 01 — 块擦除地址错误(有效)
发现时间:2026-07-21
现象:
gd5f_block_erase() 使用 byte_addr = block × 128KB(字节地址)作为 D8h 命令的入参,但 GD5F2GQ5UE 要求的是页地址(block × 64)。
只有 block 0 的 byte_addr=0 与 page_addr=0 巧合一致,其余块全部指向了错误的物理块。导致:
- 验证流程从未擦除过大部分块,厂标坏块每次重启都恢复
- 基于错误擦除的实验结论全部无效
根因:
gd5f_block_erase(block_addr)用block_addr × GD5F_BLOCK_SIZE计算地址- 芯片 D8h 命令将 24-bit 地址作为页地址(行地址)解析:
RA<16:6>选块,RA<5:0>选页内页 - 对于 block=1,发送
{0xD8, 0x00, 0x02, 0x00}→ 芯片视为 page=512 → 擦除 block=8 - 导致 block 1
7、915、17~23…等大量块从未被擦除
修复:改用 page_addr = block_addr × GD5F_PAGES_PER_BLOCK(块首页地址)
验证结果(修复后):
- 全部 2048 块正确擦除,厂标全部清除,重启后 BBT scan 为 0
- ECC 开启时 BBT rescan 正常工作
- ECC 校验码正确生成(ECCS=0)
- Block 0 无特殊行为
经验教训:
- D8h 和 13h/10h 都使用 24-bit 行地址,格式一致
- 块擦除不使用
block × block_size(字节偏移),而用块首页地址 - 芯片手册的 memory mapping 必须严格遵守
Trap 02 — 测试缓冲越界导致跨页比对误判(有效)
发现时间:2026-08-26
现象:
gd5f_test_task.c 中 s_wbuf / s_rbuf 按单页大小定义:
static uint8_t s_wbuf[GD5F_PAGE_SIZE]; /* 2048 字节 */
static uint8_t s_rbuf[GD5F_PAGE_SIZE]; /* 2048 字节 */
TC-GD5F-302 跨页写读用 gd5f2gq5ue_write(base + 1000, s_wbuf, 3000) /
read(..., s_rbuf, 3000)。驱动实际把 3000 字节正确写入了 NAND,但测试缓冲只有
2048 字节,发生数组越界:
- 前 2048 字节写入合法区(page0 全 + page1 前 1000 字节)→ 比对正确;
- page1 剩余 952 字节被写到
s_rbuf[2048..2999](越界),落入相邻内存,丢失; - 比对时读
s_rbuf[2048..2999]为越界垃圾 → 误判cross-page MISMATCH。
根因: 测试缓冲未覆盖实际最大读写长度(跨页场景超过单页 2048 字节)。
修复:
将缓冲放大到 ≥ 最大读写长度(如 GD5F_PAGE_SIZE * 2,即 4096):
static uint8_t s_wbuf[GD5F_PAGE_SIZE * 2];
static uint8_t s_rbuf[GD5F_PAGE_SIZE * 2];
验证结果: 缓冲放大后 TC-GD5F-302 一次通过,不再误判。
经验教训:
- 任何“跨页 / 大于单页”的读写测试,缓冲必须按实际长度上限而非单页大小分配
- 越界写会污染相邻全局变量,可能引起其它用例莫名失败,定位时优先怀疑缓冲尺寸
Trap 03 — 驱动读写擦缺越界检查(有效 / 健壮性缺口)
发现时间:2026-08-26
现象:
TC-GD5F-1001 用 offset = GD5F_TOTAL_SIZE(恰好超出末尾 1 字节)做 read/write,
期望驱动拒绝(返回错误),但 gd5f2gq5ue_read / write / erase 原实现无任何边界校验,
直接计算 page_addr = offset / PAGE_SIZE 后继续操作,返回 GD5F_OK → 测试 FAIL。
同时这也意味着正常调用若传入越界参数,会静默访问到回绕后的非法页。
根因:
gd5f2gq5ue_read / write / erase 入口未校验 offset / size 是否在
[0, GD5F_TOTAL_SIZE] 容量范围内。
修复: 在三个函数入口统一加边界检查(FTL/diskio 均在容量内访问,不受影响):
if (offset < 0 || (unsigned long)offset + size > (unsigned long)GD5F_TOTAL_SIZE) {
return GD5F_ERROR;
}
erase 在原有的块对齐检查之后、循环之前加同一判断即可。
验证结果:
- TC-GD5F-1001 read/write 越界均被拒绝(ret=-1),测试 PASS
- TC-GD5F-1002 size=0 仍返回
GD5F_OK(offset 合法且 size=0 不越界),符合预期 - 该检查还顺带逮到 Trap 04 的测试越界擦除(见下)
经验教训:
- 面向字节偏移的裸 NAND 接口必须做容量边界检查,调用方传错参数时尽早失败
- 无越界检查的驱动容易“静默回绕”,问题极难定位
Trap 04 — gd5f_find_run 返回语义与多块擦除窗口错位(测试 bug)
发现时间:2026-08-26
现象:
TC-GD5F-402 多块擦除(gd5f_find_run(3) 后擦 3 块)返回 ret=-1。
经 Trap 03 的边界检查定位:实际要擦的块超出了设备末尾。
根因:
gd5f_find_run(n) 原实现返回的是连续好块的末尾块号 top
(校验 top, top-1, ..., top-(n-1) 共 n 块),但调用方把返回值当作起始块号使用:
base3 = (long)blk3 * GD5F_BLOCK_SIZE;
gd5f2gq5ue_erase(base3, 3 * GD5F_BLOCK_SIZE); /* 实际擦 blk3, blk3+1, blk3+2 */
当 gd5f_find_run(3) 返回的末尾块贴到设备末尾(例如 2047,即块 2045/2046/2047 为好块)
时,调用方实际要擦 2047, 2048, 2049,块 2048/2049 越界。
n=1 时首尾块号相同所以不暴露,仅 n>1 暴露。
修复:
统一让 gd5f_find_run 返回起始块号,与 n=1 的用法一致,并仍避开块 0:
if (ok) {
return top - (uint32_t)(n - 1); /* 返回连续好块的起始块号 */
}
循环条件保持 top >= (uint32_t)(n - 1) + 1,保证起始块 ≥ 1。
验证结果: 修复后 TC-GD5F-402 多块擦除(ret=0)及“擦后全 0xFF”均 PASS; 全套用例最终 60/60 PASSED。
经验教训:
- 返回“连续区间”的辅助函数,其返回值是起点还是终点必须在注释/命名中明确, 调用方与实现必须一致
- 这类错位 bug 往往只在“区间贴到设备边界”时才触发,正常情况能过,建议测试覆盖边界块
Trap 05 — FatFs 探针文件名非法 → 无限重格式化(已修复,2026-09-20)
发现时间:2026-09-20(症状自卷健康探针引入之日起长期存在)
现象:
每次软件复位(ST-LINK Reset&Run / 上电)开机日志都出现
[FS] volume FAT invalid (fr=6), reformatting...,/config.ini 永远
"file missing, factory template generated";journal head 每 boot 前进
~300 页(持续磨损);in-boot 一切正常(挂载/写文件都好)。
真根因(纯软件,NAND/驱动/电源全部无罪):
卷健康探针 f_open("/.fschk", FA_WRITE|FA_OPEN_ALWAYS) 的名字非法——
本移植 FF_USE_LFN=0,ff.c create_short 把 .fschk 点后的 "FSCHK"
当扩展域解析(5 字符 > 3 上限)→ f_open 返回 FR_INVALID_NAME(6),
纯 RAM 名字判定、根本不碰盘(FF_RPATH≠0 时点开头还会走 ./..
特殊目录分支同样拒绝)。而 FR_INVALID_NAME 被误归入"结构性错误集合"
→ 每 boot 假失败 → 触发 f_mkfs → 无限重格式化。
修复(commit 81a11c8):
- 探针文件名改
/fschk.tmp(严格 8.3:名≤8、扩展≤3、不以点开头); FR_INVALID_NAME移出重格式化触发集合(名字语法错误与卷健康无关)。
验证(gen 45~48,@test 连续复位):
Map resumed: 197→198→199→200 sectors used(卷随 boot 正常增量持久)、
无 reformat 行、[CFG] load: OK (523B)(config.ini 跨复位存活)、VERDICT PASS。
调查中被证伪的假设(重要反面记录):
- "幻影编程"假设——证伪(低 VDD 归因同时作废):当时怀疑"3.1V 欠压供电"下 program 状态报成功但 cell 未 latch。裸 NAND 持久性自检(块 2040/2041:擦→写 4 页图案→ 立即回读→跨复位再回读 seq 递增)实测 4/4 全过:芯片、驱动原语、供电 在同一台架上的持久性完全正常。2026-09-21 更正:实测供电为 3.3V 标称, "3.1V 欠压"前提不成立(3.1V 数字源自 ST-LINK VDD 感应脚未接线的悬空读数), 本假设的低 VDD 归因作废;4/4 结果本身不受影响。
- "journal 混合页/数据损坏"假设——误读:所谓"LPN16 恒为
RRaA垃圾、LPN0 全零、LPN15 全 FF"实为合法卷内容——本移植f_mkfs(FM_FAT32)创建的是 MBR 分区卷(分区起点 LBA 63): LPN0=MBR 页(前 446 字节引导区本来就全零)、LPN15 页尾(扇区 63)=BPB (页首扇区 60-62 未用=FF)、LPN16 页首=备份 FSInfo(52 52 61 41= 0x41615252 "RRaA" 是合法签名!)。判定"损坏"前默认了几何=整盘 FAT32, 把合法结构当成了花屏。 - 三个先前修复(mkfs 前清 FTL 树 ac2087e、page_read 二次发令 38b70ad、 读后 2ms settle dc6c206)都没有终结循环——它们是合理硬化, 但不是根因。(2026-09-21:2ms settle 已移除——其"低 VDD"前提与 "混合页"证据双双证伪,见本条 1 更正与 ST-LINK 文档命令表注记; 二次发令同日移除——Phase 11 对照复现(16 轮编程擦除风暴 × 48 次立即/延迟整页字节比对)零复现 13h 空操作 / WIP 提前清零,页读回归主线单发令形状;mkfs 清树保留。)
定界决定性手段:
- 裸 NAND 跨复位持久性自检 → 排除介质/驱动/电源,指向文件系统层;
- glue 层 wsrc/flush/proglog 写链路追踪 → 暴露 mkfs 真实落盘几何 (boot 记录写向扇区 63/64/69/70/95 = 标准 MBR 布局);
fr=6=FR_INVALID_NAME枚举语义 → "路径名格式非法"不可能来自盘。
经验教训:
FF_USE_LFN=0时一切文件名必须严格 8.3,.hidden风格点开头名字 完全不可用;新建文件的代码路径先按create_short解析规则推演一遍- 磁盘健康探针必须按错误语义分诊:名字/参数类纯软件错误 (FR_INVALID_NAME / FR_INVALID_PARAMETER 等)绝不能触发整卷擦除
- 判定"存储数据损坏"前必须先确认实际卷几何(f_mkfs 默认 MBR 分区、 b_vol=63;全零/全 FF/RRaA/EB FE 90 都可能是合法结构而非花屏)
- 分层定界优先做最小闭环自下而上测试(裸介质跨复位持久性), 一次实验即可排除/坐实整个故障域,远胜在上层猜机制
- 探针自身也可能有 bug:错误码要记录"是哪次调用返回的 + 枚举语义"—— 本例 fr=6 在日志里挂了几十个 boot 没人查过枚举表
Trap 06 — 轮询路径"伪完成":HAL 失败被伪装成操作完成(已修复,2026-10-03)
发现时间:2026-10-03(错误码传播审计 E-10/E-42/E-53/E-54/E-56/E-57 复核 + 台架套件回归)
现象/机制:
gd5f_wait_busy 内 3 次 HAL 调用(HAL_SPI_Transmit / HAL_SPI_Receive)返回值全部丢弃,
失败时局部 status 保持初值 0 → (status & GD5F_STATUS_OIP) == 0 立即成立 →
"操作完成"被伪造出来。SPI 故障下后续命令在未就绪的芯片上执行,
最终表现为"静默报成功但数据没落盘"。
同族缺口:
gd5f_bbt_scan丢弃gd5f_page_read/gd5f_read_from_cache返回值,且spare[8]跨轮不清零 → 读失败时用陈旧内容判坏块;整片扫描全失败仍返回GD5F_OK,bbt_rebuild照常gd5f_bbt_save()→ 可能把错表持久化覆盖正确副本(数据安全优先项)。- init 全链路(扫坏块 / 使能 ECC / 解块保护 / 定位保留池)大量忽略返回值 → 返回
GD5F_OK但 ECC 未真正打开 → 存储栈在无 ECC 保护下运行(数据完整性底线静默降级)。 - 公开 API 返回类型
int/int32_t混用(码值均为小负数,无功能影响)。
修复(2026-10-03):
gd5f_wait_busy检查三次 HAL 调用,失败即返GD5F_ERROR(与GD5F_BUSY_TIMEOUT区分)。gd5f_bbt_scan返回错误码、每轮spare清零、读失败中止;bbt_rebuild扫描失败 放弃持久化(gd5f_bbt_load()回滚 RAM 后直接返回,不bbt_save)。- init 全链路检查返回值;init 后回读 B0h 校验 ECC 使能(bit4),未使能即报错。
gd5f2gq5ue_read的 raw 契约在头文件显式文档化("0 = SPI 传输完成,正确性由 ECC 层保证")。- 公开 API 统一
int32_t;nand_ftl.c2 处未检查的gd5f_read_from_cache补检查。
验证:台架套件回归 === GD5F2GQ5UE Tests: 16/16 PASSED (failed=0) ===(只读模式)。
经验教训:
- "看起来被检查了"(调用点写了
ret = wait_busy(...))不等于真检查—— 函数内部不区分"超时"与"HAL 失败"时,伪完成会优先于超时返回成功; - 坏块表这类持久化状态,扫描失败时必须"放弃持久化"而非"照常保存"—— 保护既有好副本比刷新更重要;
- 使能类配置(ECC)必须回读校验,不能只信写命令的返回。