Files
STM32F4-Base/docs/问题记录/GD5F2GQ5UE_Trap_Records.md
T
wandering1 78248a1cdf feat(S-5): 收编 SD2506/TPAFE 并给 dhara 失败分类,门禁基线 19→17
前一笔立契约与门禁(`Drivers/BSP/bsp_err.h` + `bsp-err` 钩子 + RS485 试点);这一笔按用户
指定顺序 C3 → C1 → C2 把三个模块真正收编,并让存储失败第一次变得可读、可分类。

**C3 · SD2506 整模块(11 个公共 API + 6 个内部 helper)**:三枚私有宏 `SD2506_OK`/
`SD2506_ERROR`/`SD2506_I2C_ERROR` **删除而不是留别名**——留别名等于新代码还能用旧形状,
把迁移洗掉;编译器替我们找全所有调用点。新增 `sd2506_err_from_hal()`,**把 I2C 超时从
"和没人应答共用一个 −2"里拆出来归成 `BSP_ERR_TIMEOUT`(可重试)**——SD2506 是启动链第一站、
I2C1 上电时序敏感,这是本波唯一真正的语义改善,不是改名。`sd2506_init` 的 charge 寄存器
**回读校验不符**原返参数类错误,改归 `BSP_ERR_IO`(那不是调用方的错)。

**C1 · 同形两义**:`GD5F_BUSY_TIMEOUT` → `GD5F_WAIT_EXPIRED`(**值 −2 不变**,纯消"名字叫
TIMEOUT 其实是返回码")。**GD5F 私有码集整体收编刻意没做**:169 处引用里 48 处在板级套件、
还要动 Trap 记录用词,属另一波带真机+套件回归的工作,不夹带。TPAFE5160 收编 23 处:
`BUSY_TIMEOUT → BSP_ERR_TIMEOUT`、`ERROR → BSP_ERR_PARAM`(**同值 −1,行为零变化**)、
`FRSTDATA_ERR → BSP_ERR_NO_DEVICE`(E-44 的本义就是"芯片没驱动 DB 总线",归到器件无响应比
私有 −3 有用,且避开与 `BSP_ERR_TIMEOUT` 撞值)。实测 `tpafe5160.o`/`gd5f2gq5ue.o` 的 Code
增量为 **0**——改名不产生一条指令。

⚠ **`tpafe5160_snapshot()` 刻意不套成败码**:它是 1=有新帧 / 0=无新帧 / −1=参数错 的三态
**状态**返回,0 不是错误而是"这一拍没有新数据",强行收编会把慢消费者的正常路径变成失败。
顺带订正我上一版在 `bsp_err.h` 的断言——"本层只有 `net_*` 把数量与错误挤同一返回值"是错的,
`snapshot`/`adc_input_poll` 就是第二族;两族现都已写明并各注保留理由。

**C2 · FTL 坍缩点分类**:`ftl_flush_cache`/`ftl_read_page` 不再把 dhara 失败压成
  `GD5F_ERROR`,
按**处置差异**分三类,并在最早同时知道 LPN 与原始码的位置就地打印:`ECC`/`TOO_BAD` →
`BSP_ERR_CRC`(页级,重试或换页,不代表卷坏);`JOURNAL_FULL`/`MAP_FULL` →
`BSP_ERR_NO_SPACE`(介质健康但块池/日志见底,此时重格式化只是把问题推回去);
`BAD_BLOCK`/`RECOVER`/`CORRUPT_MAP`/越界 → `BSP_ERR_IO`(完整性故障,保卷 + 上报,不许自愈
重格式化)。这与 **E-31** 是同一条纪律的两端。可读名用自建短表:**本仓 dhara 没有编
`error.c`,用 `dhara_strerror()` 会直接链接失败**。`disk_read`/`disk_write` 仍回
  `RES_ERROR`
(FatFs 契约);要让 defaultTask 卷健康探针按类分支还差跨调用存活的快照 → C4 待还。

**两次靠核实拦住的自我错误**:① 我一度认定 `DBG_ERROR("... err=%d", err)` 是把结构体塞进
varargs 的格式 bug——核 `Lib/dhara/error.h` 后确认 `dhara_error_t` 是 `enum`(标量),那行日志
本来合法,改了反而是引入错误;② `bsp-err` 一次 `replace_all` 越界把 8 处 **GD5F 码空间**的
判据改成 `!= BSP_OK`(数值同为 0、无行为变化),但那正是"把两个码空间混起来",已逐一还原。
`cppcheck` 亦抓到 `cls` 声明作用域过宽,已收进失败分支。

**验证**:`@build` 0 错 0 警告(`Code` 213,648、RO 18,860、`ER_IROM1` 232,508 B /
  22.2%、
`ROM 合计` 233,696;**RAM 三项零增量**);`@test` PASS(692 ms,烧入戳 `Oct 23 01:35:32`)
且逐条读到被改模块的锚点:`SD2506 init: OK (ret=0)` 与 ID/时间/温度/电池四条读回正常、
`ADC init: OK`、`NAND init: OK`、`Map resumed: 14094/97504`、`adc=` 帧计数持续递增(证明
snapshot 三态未被改坏)、`[ERR]`/Fault/FAIL 0 条。主机单测:系统时钟 100/100、dhara NAND
移植层 149/149(两者都因本次改动需补 `Drivers/BSP` include 路径,补后在钩子内实跑 Passed)。
`pre-commit run` 全量 RC=0(20 个实跑钩子 Passed);`bsp-err` 基线 **19 → 17**(还掉两个
`*_BUSY_TIMEOUT = -2`)、`--prune` 干净;`doc-links --all --sections-strict` 147 篇
  0 问题;
`id-check --strict`、`layer-deps`、`mem-numbers`、`mem-budget`、`ui-golden` 全过;33
  个文件
字节卫生 0 异常(无 CR / 无 U+FFFD / 无行尾空格 / 均有尾换行)。

**口径与数字联动**:新增 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.4(三类码与处置
理由);`pre-commit使用说明.md` §2.27 与 `AGENTS.md` 基线计数 19 → 17;TPAFE README、
GD5F 测试指南与 Trap 记录均**保留旧名留痕**以便对旧日志;配图桶按 .map 对象级增量重算
(`dhara/FTL` 10,931 → 11,222,+291 全部落在 `nand_ftl.o`;应用代码/UI +36 落在
`app_main.o`;其余散量入"其他"7,557;桶和 = Code+RO 断言通过、文本溢出 0);
`ram_baseline --update/--write`、`ram_report_20261023.md` 快照、报告 §1.3/§5.1/§5.4
  与
部署文档 §3.3 数字全部同步;报告 §0.4/§9 的 S-5 状态改为 🟡 主体已落地。
2026-10-23 07:42:02 +08:00

18 KiB
Raw Blame History

GD5F2GQ5UE 陷阱记录

TRAP-GD5F-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 17、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-GD5F-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-GD5F-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-GD5F-04 的测试越界擦除(见下)

经验教训:

  • 面向字节偏移的裸 NAND 接口必须做容量边界检查,调用方传错参数时尽早失败
  • 无越界检查的驱动容易“静默回绕”,问题极难定位

TRAP-GD5F-04 — gd5f_find_run 返回语义与多块擦除窗口错位(测试 bug)

发现时间:2026-08-26

现象: TC-GD5F-402 多块擦除(gd5f_find_run(3) 后擦 3 块)返回 ret=-1。 经 TRAP-GD5F-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-GD5F-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):

  1. 探针文件名改 /fschk.tmp(严格 8.3:名≤8、扩展≤3、不以点开头);
  2. 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。

调查中被证伪的假设(重要反面记录):

  1. "幻影编程"假设——证伪(低 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 结果本身不受影响。
  2. "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, 把合法结构当成了花屏。
  3. 三个先前修复(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-GD5F-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_WAIT_EXPIRED 区分;后者旧名 GD5F_BUSY_TIMEOUT,2026-10-23 按 S-5 R2 改名——返回码不许叫 *_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.c 2 处未检查的 gd5f_read_from_cache 补检查。

验证:台架套件回归 === GD5F2GQ5UE Tests: 16/16 PASSED (failed=0) ===(只读模式)。

经验教训:

  • "看起来被检查了"(调用点写了 ret = wait_busy(...))不等于真检查—— 函数内部不区分"超时"与"HAL 失败"时,伪完成会优先于超时返回成功;
  • 坏块表这类持久化状态,扫描失败时必须"放弃持久化"而非"照常保存"—— 保护既有好副本比刷新更重要;
  • 使能类配置(ECC)必须回读校验,不能只信写命令的返回。

TRAP-GD5F-07 — dhara 移植层:is_free 只看页首 + HAL 无几何校验(已修复,2026-10-15)

发现时间:2026-10-15(项目审查与优化建议报告.md N-12 / N-13 复核,登记簿原 N-21)

现象 / 机制(两个独立缺陷,同一层):

  1. dhara_nand_is_free() 只读页首 64 B 判全 0xFF。dhara journal 用它论证"检查点组尚未编程" (journal.c:405-414 的前提是组内最后一页一旦编程必然含非 0xFF 字节),而 cp_free(journal.c:422)与"找下一空闲页"(journal.c:520)都直接信这个返回值。 一个"页首恰好全 0xFF、后段有数据"的已编程页(刚格式化的卷、稀疏文件块、日志尾部都很常见) 会被误判空闲 → dhara 往已编程页再编程一次 = 位碰撞,journal / map 指向垃圾, 且不报任何错。
  2. 六个 HAL 入口(is_bad / mark_bad / erase / prog / read / copy)对页号、块号 零上界校验。越界值(含 dhara 哨兵 DHARA_PAGE_NONE = 0xffffffff,journal.h:46) 加完 FTL_START_BLOCK 偏移送进 SPI 命令后,被芯片地址位截断成某个真实页 → 静默读写、甚至把无辜页的数据 GC 搬走。

与 TRAP-GD5F-03 的关系(本条的核心教训):TRAP-GD5F-03 已为裸驱动字节偏移 API (gd5f2gq5ue_read/write/erase)补过容量检查,但dhara 移植层是另一个入口,同一教训在那儿 原样重演。边界校验必须逐层做——"下层查过了"推不出"上层调用也安全",何况本例下层查的是 字节偏移、上层传的是页号,两者根本不是同一个量。

修复(2026-10-15):

  • is_free 改整页扫描(栈上 64 B 缓冲分块读满 2048 B;页首命中即早退,所以"已编程且 页首有数据"的绝大多数页成本与旧实现同价,只有前缀全 0xFF 的页才多读几块)。
  • 新增 ftl_page_in_range() / ftl_block_in_range()(上界 = n->num_blocks << n->log2_ppb), 六个入口统一守卫。
  • 错误码选择是刻意的:越界返回 DHARA_E_CORRUPT_MAP。不能用 DHARA_E_BAD_BLOCK ——journal.c:904 recover_from 只对该码执行"标坏块 + 跳块",会把无辜好块标掉; 不能用 DHARA_E_RECOVER——map.c:275 try_recover 只对该码启动回收。其余错误一律 原样上抛,不会静默清 map(E-31 教训)。
  • 无错误通道的两个函数取保守值:is_free 越界返回 0(判非空,最多少用一页)、 is_bad 越界返回 1(判坏,绝不读写)、mark_bad 越界直接返回(绝不标无辜好块)。
  • is_free 内 gd5f_page_read / gd5f_read_from_cache 任一失败 → 返回 0 (TRAP-GD5F-06 同族:读失败不得伪装成"空闲")。

验证:

  • @build 0 错 0 警;@test PASS,且台架 journal 恢复点与修复前同轨——连续四次启动 sectors used 恒 14,094、Map head 每轮 +128 单调推进(37,472 → 37,600 → 37,728 → 37,856)、 epoch 稳定 3 → 整页 is_free 未改变恢复语义、无双编程、无回退。
  • 主机侧移植层单测 test/host/nand_port/(RAM 假芯片 + "旧算法只看页首 64 B"对照用例, 将来有人把 is_free 改回去会立刻红)。

经验教训:

  • 移植层的"廉价近似"会被上游当成精确契约:dhara 已在注释里论证 is_free 必须精确,端口却 按"页首大概会有数据"实现。契约类回调不许采样式实现——要嘛精确,要嘛按保守值失败。
  • 擦除态检测(判"空闲")的正确做法是扫全页;性能用早退快路径去拿,而不是靠缩小扫描范围赌。
  • 哨兵值(DHARA_PAGE_NONE)在 C 里就是个普通整数,下层不校验就一定会被当地址用。

遗留与代价(2026-10-15 主机侧单测落地时复核提出,未修):

  1. is_free 读失败返 0 的方向性代价:比"误判空闲 → 再编程"安全(dhara 会去读 checkpage, 被 magic / CRC 门挡住),但一次瞬态 SPI 毛刺就可能让整次 mount 失败——旧实现反而会被 "页首恰好读到 0xFF"的快路径掩盖过去。这是刻意选择:宁可挂载失败,不可误判空闲 (E-31 纪律:绝不拿销毁数据换可用性)。排障时记住"挂载失败 + 有 SPI 错误"先看这里。
  2. 空白卷首启成本:dhara resume 扫到的每个检查点页都是全 0xFF → 每页要 32 次列读 (旧实现 1 次)。量级仍小(O(log N) × ppc),但首次挂载(新格式化卷)会明显变慢。 台架若观察到该现象,根因在此,不是回归。
  3. 守卫上界依赖硬编码 log2_ppb = 6 ✅ 同日已收口:ftl_page_in_range() 的上界 = n->num_blocks << n->log2_ppb,而这两个 log2_* 原本在 disk_initialize 里是裸字面量。 将来换 128 页/块的芯片,守卫会静默算错(偏大 = 越界守卫失效,正是它要防的那类)。 → 收口方式比"加一条断言"更彻底:字面量收成 FTL_LOG2_PAGE_SIZE / FTL_LOG2_PPB 单一真源(disk_initialize 与守卫同取此宏,杜绝两处各写一遍再各自漂移), 再加两条负数组编译期守卫 FTL_PAGE_SIZE == (1 << FTL_LOG2_PAGE_SIZE) / GD5F_PAGES_PER_BLOCK == (1 << FTL_LOG2_PPB)——改 GD5F 常量而不同步几何即编译错。 AC5 不支持 C11 _Static_assert,负数组是本项目惯用法(同本目录 gd5f2gq5ue.h 的 N-26 守卫)。