Files
STM32F4-Base/test/host/nand_port/nand_port_test.c
T
super_agent_bot 5538af829b
CI / 静态门禁 (pre-commit) (push) Failing after 43s
CI / 主机侧单测 (MSVC) (push) Failing after 45s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 44s
CI / 主机侧单测 (MSVC) (pull_request) Failing after 43s
fix(nand-ftl): 传输失败不再误标坏块 + format 不再被脏缓存挡死(#79)
根因(真机定案,问题 #79):旧 dhara_nand_erase/prog() 把一切失败一律
mark_bad,而 mark_block_bad 每次**立即持久化**板级 BBT(不可撤销)——
一次传输层/等忙抖动就把好块永久记成坏块。台架 129 个好块(1016–1142
连续段)被固化,journal prepare_head() 连续 8 次撞 is_bad → TOO_BAD →
Cache flush FAIL → reformat 又被同一页挡住 → 整卷禁用、重启不恢复。

- 修复①:只有芯片明确回 E_FAIL/P_FAIL(状态位证据)才标坏;
  传输/超时失败只报 DHARA_E_BAD_BLOCK 让 dhara 重试,不标坏
- 修复②:nand_ftl_format() 尽力刷一次脏缓存,失败即丢弃并继续
  dhara_map_clear()(格式化语义本就是销毁映射;旧实现第一步返回,
  恢复动作在同一页死锁,f_mkfs 全在旧映射上必失败)
- C4 还债:新增跨调用失败类快照 nand_ftl_fault_get/clear(20 B,
  resume/读/刷/同步四点位记账);defaultTask 探针按类分流,
  BSP_ERR_NO_SPACE 跳过重格式化(容量问题不该用 mkfs 解)
- 哨兵与诊断:init 解锁块保护(A0h)后补回读校验(与 B0h ECC 同族
  缺口,实测本板 A0h=0,非本次根因,留作哨兵);gd5f_bbt_load()
  合并后坏块数 > 出厂扫描时打「BBT after merge」,现场可区分
  真坏与误标固化
- 测试:主机单测 149→178 全绿;新增 format 死锁(TC-NANDPORT-800..
  813b)、传输失败不标坏(207 语义翻转 + 208/505);牙齿检查过——
  把修复回退即见红
- 真机:恢复动作 A 重建 BBT 清除已固化误标(3/3 PASSED,BBT
  version=71);产品镜像复烧 [FS] FATFS mounted、配置出厂模板
  落盘、0 条 flush FAIL——#79 真机闭环(旧卷内容随 map clear
  作废,台架卷为可弃数据)
- 文档:存储架构说明 §10.4 口径修正 + 新增 §10.4.1/§10.4.2(页级
  失败升级链与误标持久化机理),存储维护与容量恢复 §2 补板级 BBT
  独立持久化注记 + §2.1(动作 A 此前静默失效),登记簿 C4 销项
- Keil 0 错 0 警(Code=255552,与烧录验证镜像一致);doc-links 213
  文档通过
2026-11-04 18:58:13 +08:00

1242 lines
55 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:dhara NAND 移植层(nand_ftl)主机侧单元测试
* 被测对象:真实源文件 Drivers/BSP/GD5F2GQ5UE/nand_ftl.c(以 #include 原样纳入,
* 保证测的是生产代码而非副本)。底层 SPI 原语由本文件提供的
* **RAM 假芯片**(stub/gd5f2gq5ue.h 声明的同名 API)实现。
*
* 为什么要主机侧单测(而不是只用板端套件):
* 移植层(dhara HAL 六个入口)是**纯地址算术 + 边界判定**,与 SPI 时序无关;
* 而它要防的两类缺陷恰恰是真机**极难复现**的:
* 1. is_free 只看页首 64 B → "页首 0xFF、后段有数据"的页被误判空闲
* → dhara 往已编程页再编程 = 静默数据丢失。真机上要恰好撞上那种页形态
* 才发作,且发作后只表现为"某次复位后丢数据",无从定位。
* 2. 越界页/块号(含 DHARA_PAGE_NONE 哨兵)被地址位截断成真实页
* → 静默读写/搬走无辜数据。真机上没有任何一条路径会主动喂进越界号,
* 只有被污染的 meta 或将来改动才会触发——主机侧一次构造即钉死。
* 假芯片把"双编程"做成**可数事件**(is_free 精度回归探针),并记录全部访问次数,
* 于是"越界不产生任何芯片访问""越界标坏块次数为 0"这类判据可机械断言
* ——这是板端永远拿不到的观测面。
*
* 编码:字符串字面量**纯 ASCII**(中文只进注释),与 cfg_file_test.c 同口径;
* 主机侧由 harness 的 /utf-8 保证注释里的中文被正确解码。
*
* 用例前缀:TC-NP-NNN(NP = NAND Port);分组:
* TC-NANDPORT-000 假芯片自洽(几何 / 上电态)
* TC-NANDPORT-100..108 dhara_nand_is_free 整页扫描
* TC-NANDPORT-110..112 dhara_nand_is_free 越界守卫
* TC-NANDPORT-200..207 dhara_nand_prog(越界 / NULL / 往返 / 双编程探针 / 失败注入)
* TC-NANDPORT-300..307 dhara_nand_read(越界 / 越窗 offset+length / 部分读 / ECC / 读失败保守判据)
* TC-NANDPORT-400..405 dhara_nand_copy(src 与 dst 双端守卫 / 正常搬页)
* TC-NANDPORT-500..504 dhara_nand_erase(越界块零标坏块 / 整块回 0xFF / 失败注入)
* TC-NANDPORT-600..605 dhara_nand_is_bad / dhara_nand_mark_bad(越界保守判坏、BBT 不变)
* TC-NANDPORT-700 dhara map 级场景(写入 -> 重新 resume,双编程计数为 0)
*
* 通过:全部 CHECK 通过(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-15
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ===================== 平台桩(先纳入,覆盖真实头) ===================== */
#include "dbg_log.h"
#include "gd5f2gq5ue.h"
/* DBG_* 计数(stub/dbg_log.h 声明) */
int g_nandtest_err_count;
int g_nandtest_info_count;
int g_nandtest_debug_count;
/* ======================== RAM 假芯片 ========================
*
* 形态:一块 32 块 x 64 页 x 2048 B = 4 MB 的字节数组,上电全 0xFF。
* 语义保真点:
* - 读:gd5f_page_read(pg) 把该页**锁进内部缓存**,之后必须由
* gd5f_read_from_cache(column, buf, len) 按列取数;未锁页就取数、
* 或列区间越过页尾 → 返回失败(真实芯片同样是"先 13h 后 0Bh"两段式);
* - 写:gd5f_program_load 只写"数据寄存器",gd5f_program_exec 才落到阵列,
* 且落盘是 **NOR 叠加**(new = old & written)——真实 NAND 的位语义;
* - 严格探针:exec 时若目标页已有非 0xFF 字节,记一次**双编程违规**
* (g_fake_double_program_cnt)。这是抓 is_free 整页精度回归的核心:任何
* "把已编程页判成空闲"的实现都会让它立刻非 0;
* - 擦除:整块回 0xFF;坏块表是 RAM 位图,mark_bad 记录次数与块号序列。
*/
#define FAKE_BLOCKS ((uint32_t)GD5F_TOTAL_BLOCKS) /* 32 */
#define FAKE_PAGES_PER_BLOCK ((uint32_t)GD5F_PAGES_PER_BLOCK) /* 64 */
#define FAKE_PAGE_SIZE ((uint32_t)GD5F_PAGE_SIZE) /* 2048 */
#define FAKE_TOTAL_PAGES (FAKE_BLOCKS * FAKE_PAGES_PER_BLOCK) /* 2048 */
#define FAKE_USABLE_BLOCKS (FAKE_BLOCKS - (uint32_t)GD5F_BBT_POOL_COUNT) /* 28 */
/* 阵列 + 数据寄存器 + 内部读缓存锁 */
static uint8_t s_chip[FAKE_TOTAL_PAGES][FAKE_PAGE_SIZE];
static uint8_t s_load_reg[FAKE_PAGE_SIZE];
static int32_t s_cache_page = -1; /* gd5f_page_read 锁进的页;-1 = 无 */
static int s_load_valid;
/* 坏块表(RAM) */
static uint8_t s_bad[FAKE_BLOCKS];
/* 访问计数(供"越界不产生任何芯片访问"断言) */
static unsigned long g_fake_write_enable_cnt;
static unsigned long g_fake_page_read_cnt;
static unsigned long g_fake_read_cache_cnt;
static unsigned long g_fake_check_ecc_cnt;
static unsigned long g_fake_program_load_cnt;
static unsigned long g_fake_program_exec_cnt;
static unsigned long g_fake_block_erase_cnt;
static unsigned long g_fake_is_bad_cnt;
static unsigned long g_fake_mark_bad_cnt;
static unsigned long g_fake_isfree_scan_cnt; /* 64 B 列读次数(is_free 指纹) */
/* 双编程探针:往已编程页再编程的次数与页号 */
static unsigned long g_fake_double_program_cnt;
static uint32_t g_fake_last_double_program_page;
/* mark_bad 记账(次数 + 块号序列) */
#define FAKE_MARKBAD_LOG 16u
static uint32_t g_fake_mark_bad_log[FAKE_MARKBAD_LOG];
static unsigned long g_fake_mark_bad_log_n;
/* 故障注入(复现真机极难构造的 I/O 失败) */
static int s_fail_page_read;
static int s_fail_read_cache;
static int s_fail_check_ecc;
static int s_fail_program_load;
static int s_fail_program_exec;
static int s_fail_block_erase;
/* 定向坏页注入(问题 #79):非 0 时,**只有**该物理页的 program_exec 失败 */
static uint32_t s_fail_program_page = 0u;
/* 传输层失败注入(问题 #79):非 0 时 erase / program_exec 返回传输/超时错误
* (GD5F_ERROR,芯片**没有**回 E_FAIL/P_FAIL),用于验证"不得标坏块"。 */
static int s_fail_erase_transport;
static int s_fail_exec_transport;
/* ---- SPI 原语(stub/gd5f2gq5ue.h 声明,签名与真实驱动逐字一致) ---- */
int32_t gd5f_write_enable(void) {
g_fake_write_enable_cnt++;
return GD5F_OK;
}
int32_t gd5f_page_read(uint32_t page_addr) {
g_fake_page_read_cnt++;
if (page_addr >= FAKE_TOTAL_PAGES) {
return GD5F_ERROR;
}
if (s_fail_page_read != 0) {
return GD5F_ERROR;
}
s_cache_page = (int32_t)page_addr;
return GD5F_OK;
}
int32_t gd5f_read_from_cache(uint16_t column, uint8_t *p_buf, uint32_t size) {
g_fake_read_cache_cnt++;
/* 64 B 列读 = dhara_nand_is_free 分块扫描的形状指纹。
* dhara 其它读路径的尺寸是 16(header)/ 132(meta)/ 2048(整页),
* 故本计数可用来证明场景用例确实走到了 is_free(而不是"没跑到也算过")。 */
if (size == 64u) {
g_fake_isfree_scan_cnt++;
}
if (p_buf == NULL) {
return GD5F_ERROR;
}
if (s_cache_page < 0) {
return GD5F_ERROR; /* 未先 page_read 就取缓存 = 无数据 */
}
if (s_fail_read_cache != 0) {
return GD5F_ERROR;
}
if (((uint32_t)column + size) > FAKE_PAGE_SIZE) {
return GD5F_ERROR; /* 列越界 */
}
(void)memcpy(p_buf, &s_chip[(uint32_t)s_cache_page][column], size);
return GD5F_OK;
}
int32_t gd5f_check_ecc(void) {
g_fake_check_ecc_cnt++;
return (s_fail_check_ecc != 0) ? GD5F_ECC_ERROR : GD5F_OK;
}
int32_t gd5f_program_load(uint16_t column, const uint8_t *p_buf, uint32_t size) {
g_fake_program_load_cnt++;
if (p_buf == NULL) {
return GD5F_ERROR;
}
if (s_fail_program_load != 0) {
return GD5F_ERROR;
}
if (((uint32_t)column + size) > FAKE_PAGE_SIZE) {
return GD5F_ERROR;
}
if (column == 0u) {
(void)memset(s_load_reg, 0xFF, sizeof(s_load_reg));
}
(void)memcpy(&s_load_reg[column], p_buf, size);
s_load_valid = 1;
return GD5F_OK;
}
int32_t gd5f_program_exec(uint32_t page_addr) {
uint32_t i;
g_fake_program_exec_cnt++;
if (page_addr >= FAKE_TOTAL_PAGES) {
return GD5F_ERROR;
}
if (s_load_valid == 0) {
return GD5F_ERROR; /* 没装载就 exec = 命令序列错 */
}
if (s_fail_program_exec != 0) {
return GD5F_PROGRAM_FAIL;
}
if (s_fail_exec_transport != 0) {
return GD5F_ERROR; /* 传输/等忙失败:芯片没回 P_FAIL */
}
/* 定向坏页注入(问题 #79):现场是**某一个 PPA 恒写不进去**,其余页正常。
* 全局 s_fail_program_exec 无法复现"只有那一页坏"的形态——它连格式化
* 之后的新写入也一起打死,反而测不出"换个页就能写"这个关键结论。 */
if ((s_fail_program_page != 0u) && (page_addr == s_fail_program_page)) {
return GD5F_PROGRAM_FAIL;
}
/* 严格语义探针:目标页已有非 0xFF 字节 = 双编程(is_free 误判的现场证据) */
for (i = 0; i < FAKE_PAGE_SIZE; i++) {
if (s_chip[page_addr][i] != 0xFFu) {
g_fake_double_program_cnt++;
g_fake_last_double_program_page = page_addr;
break;
}
}
/* NOR 叠加:真实 NAND 只能把 1 写成 0,双编程的结果是位碰撞 */
for (i = 0; i < FAKE_PAGE_SIZE; i++) {
s_chip[page_addr][i] = (uint8_t)(s_chip[page_addr][i] & s_load_reg[i]);
}
s_load_valid = 0;
return GD5F_OK;
}
int32_t gd5f_block_erase(uint32_t block_addr) {
g_fake_block_erase_cnt++;
if (block_addr >= FAKE_BLOCKS) {
return GD5F_ERROR;
}
if (s_fail_block_erase != 0) {
return GD5F_ERASE_FAIL;
}
if (s_fail_erase_transport != 0) {
return GD5F_ERROR; /* 传输/等忙失败:芯片没回 E_FAIL */
}
(void)memset(&s_chip[block_addr * FAKE_PAGES_PER_BLOCK][0], 0xFF,
(size_t)FAKE_PAGES_PER_BLOCK * FAKE_PAGE_SIZE);
return GD5F_OK;
}
int32_t gd5f2gq5ue_is_block_bad(uint32_t block) {
g_fake_is_bad_cnt++;
if (block >= FAKE_BLOCKS) {
return 1; /* 越界:保守判坏(与真实驱动同口径) */
}
return (s_bad[block] != 0u) ? 1 : 0;
}
void gd5f2gq5ue_mark_block_bad(uint32_t block) {
g_fake_mark_bad_cnt++;
if (g_fake_mark_bad_log_n < FAKE_MARKBAD_LOG) {
g_fake_mark_bad_log[g_fake_mark_bad_log_n] = block;
}
g_fake_mark_bad_log_n++;
if (block < FAKE_BLOCKS) {
s_bad[block] = 1u;
}
}
uint32_t gd5f_get_usable_blocks(void) {
return FAKE_USABLE_BLOCKS;
}
int gd5f_dma_buf_ok(const void *p_buf, size_t size) {
/* PC 上没有"DMA 不可达内存"这种物理形态;恒返 1 让 disk_initialize 继续。
* 该守卫本身由 gd5f2gq5ue.c 负责,不在本套件射程内。 */
(void)p_buf;
(void)size;
return 1;
}
/* ---- 假芯片辅助(测试脚手架,非驱动 API) ---- */
/* 复位到"刚上电":阵列全 0xFF、坏块表清空、全部计数与注入清零 */
static void fake_chip_reset(void) {
(void)memset(s_chip, 0xFF, sizeof(s_chip));
(void)memset(s_load_reg, 0xFF, sizeof(s_load_reg));
(void)memset(s_bad, 0, sizeof(s_bad));
(void)memset(g_fake_mark_bad_log, 0, sizeof(g_fake_mark_bad_log));
s_cache_page = -1;
s_load_valid = 0;
g_fake_write_enable_cnt = 0u;
g_fake_page_read_cnt = 0u;
g_fake_read_cache_cnt = 0u;
g_fake_check_ecc_cnt = 0u;
g_fake_program_load_cnt = 0u;
g_fake_program_exec_cnt = 0u;
g_fake_block_erase_cnt = 0u;
g_fake_is_bad_cnt = 0u;
g_fake_mark_bad_cnt = 0u;
g_fake_isfree_scan_cnt = 0u;
g_fake_mark_bad_log_n = 0u;
g_fake_double_program_cnt = 0u;
g_fake_last_double_program_page = 0xFFFFFFFFu;
s_fail_page_read = 0;
s_fail_read_cache = 0;
s_fail_check_ecc = 0;
s_fail_program_load = 0;
s_fail_program_exec = 0;
s_fail_block_erase = 0;
s_fail_program_page = 0u; /* 定向坏页注入复位(0 = 不注入,0 号页本身也不允许当靶) */
s_fail_erase_transport = 0;
s_fail_exec_transport = 0;
g_nandtest_err_count = 0;
g_nandtest_info_count = 0;
g_nandtest_debug_count = 0;
}
/* 全部芯片侧访问次数之和("越界不产生任何芯片访问"的判据量) */
static unsigned long fake_chip_access_total(void) {
return g_fake_write_enable_cnt + g_fake_page_read_cnt + g_fake_read_cache_cnt +
g_fake_check_ecc_cnt + g_fake_program_load_cnt + g_fake_program_exec_cnt +
g_fake_block_erase_cnt + g_fake_is_bad_cnt + g_fake_mark_bad_cnt;
}
/* 绕过驱动语义直写一页(构造"已编程页"的初始形态,不计双编程) */
static void fake_poke_page(uint32_t page, const uint8_t *p_src, uint32_t len) {
if (page < FAKE_TOTAL_PAGES) {
(void)memcpy(&s_chip[page][0], p_src, len);
}
}
/* 整页是否仍为擦除态 */
static int fake_page_all_ff(uint32_t page) {
uint32_t i;
if (page >= FAKE_TOTAL_PAGES) {
return 0;
}
for (i = 0; i < FAKE_PAGE_SIZE; i++) {
if (s_chip[page][i] != 0xFFu) {
return 0;
}
}
return 1;
}
/* 整页是否与给定内容一致 */
static int fake_page_equals(uint32_t page, const uint8_t *p_src, uint32_t len) {
if (page >= FAKE_TOTAL_PAGES) {
return 0;
}
return memcmp(&s_chip[page][0], p_src, len) == 0;
}
/* 块内所有页是否都是擦除态 */
static int fake_block_all_ff(uint32_t block) {
uint32_t i;
if (block >= FAKE_BLOCKS) {
return 0;
}
for (i = 0; i < FAKE_PAGES_PER_BLOCK; i++) {
if (!fake_page_all_ff(block * FAKE_PAGES_PER_BLOCK + i)) {
return 0;
}
}
return 1;
}
/* 生成可复现的伪随机页内容(非 0xFF 密集,用于往返比对) */
static void fake_fill_pattern(uint8_t *p_buf, uint32_t len, uint32_t seed) {
uint32_t i;
uint32_t x = seed | 1u;
for (i = 0; i < len; i++) {
x = (x * 1664525u) + 1013904223u;
p_buf[i] = (uint8_t)(x >> 24);
}
}
/* 坏块表快照 / 比对(断言"越界 mark_bad 后坏块表不变") */
static uint8_t s_bad_snapshot[FAKE_BLOCKS];
static void fake_bad_snapshot(void) {
(void)memcpy(s_bad_snapshot, s_bad, sizeof(s_bad_snapshot));
}
static int fake_bad_unchanged(void) {
return memcmp(s_bad_snapshot, s_bad, sizeof(s_bad_snapshot)) == 0;
}
/* 复位假芯片并在页 64(块 1 首页)铺一份已知内容:read / copy 用例的公共前置。
* 每次 fake_chip_reset() 后阵列回到 0xFF,**必须重新铺数据**,否则"内容比对"
* 会拿全 0xFF 去比模式串——测试自己假绿。 */
#define UT_SRC_PAGE 64u
static void prep_content_page(uint8_t *p_buf, uint32_t seed) {
fake_chip_reset();
fake_fill_pattern(p_buf, FAKE_PAGE_SIZE, seed);
fake_poke_page(UT_SRC_PAGE, p_buf, FAKE_PAGE_SIZE);
}
/* ===================== 断言 ===================== */
static int s_total;
static int s_passed;
static int s_failed;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
/* ===================== 被测代码(原样纳入生产源) ===================== */
/*
* MSVC /W4 下的**主机平台专属**噪音,与 MCU 构建(ARMCC,size_t = 32 bit)无关,
* 只在这几处 include 周围关闭;本测试单元自身仍保持 /W4 全开:
* 4267 nand_ftl.c:277 把 size_t length 交给 uint32_t 形参——x64 上 size_t 是
* 64 位才成立,ARM 上同宽(ARMCC 构建 0 错 0 警);
* 4244 / 4334 / 4100 / 4701 第三方 Lib/dhara(本仓裁剪版,只读、cppcheck 亦排除)。
*/
#if defined(_MSC_VER)
#pragma warning(push)
#pragma warning(disable : 4267)
#endif
#include "nand_ftl.c"
#if defined(_MSC_VER)
#pragma warning(pop)
#endif
/* dhara 核心(真实源,非桩):nand_ftl.c 引用 dhara_map_*,必须一并纳入才能链接。
* 纳入后本编译单元 = 移植层 + 它服务的 FTL 本体,故 dhara map 级场景
* (TC-NANDPORT-700 写入 -> 重新 resume)测的是真链路。顺序:journal 在 map 之前。 */
#if defined(_MSC_VER)
#pragma warning(push)
#pragma warning(disable : 4100 4244 4334 4701)
#endif
#include "journal.c"
#include "map.c"
#if defined(_MSC_VER)
#pragma warning(pop)
#endif
/* ===================== 测试几何 =====================
* 与 disk_initialize 里给 s_nand 的赋值逐项一致(log2_page_size=11 -> 2048 B、
* log2_ppb=6 -> 64 页/块),块数取假芯片的 dhara 可见块数(32 - 4 保留块池)。
* FTL_START_BLOCK = 0 -> 逻辑页号 == 物理页号,越界期望值可直接算。 */
static struct dhara_nand s_ut_nand;
#define UT_TOTAL_PAGES ((dhara_page_t)s_ut_nand.num_blocks << s_ut_nand.log2_ppb)
#define UT_OOB_PAGE (UT_TOTAL_PAGES) /* 第一个越界页 */
#define UT_OOB_BLOCK ((dhara_block_t)s_ut_nand.num_blocks)
/* ===================== 旧算法对照(is_free 误判的"牙齿") =====================
*
* 整页精度修复**之前**的 dhara_nand_is_free:只读页首 64 B 就回答"空闲"。
* 本函数按当时的形状内联一份(同样经假芯片的两段式读),用来证明本套件**有牙**:
* 对同一页,旧算法答 1(误判空闲)、新实现答 0。将来有人把 is_free 改回只看
* 页首,TC-NANDPORT-105 立刻变红——而不是"测试跟着实现一起改回去"。
*/
static int old_is_free_first64(const struct dhara_nand *n, dhara_page_t p) {
uint8_t buf[64];
uint32_t i;
(void)n;
if (gd5f_page_read(ftl_phy_page(p)) != GD5F_OK) {
return 0;
}
if (gd5f_read_from_cache(0u, buf, sizeof(buf)) != GD5F_OK) {
return 0;
}
for (i = 0; i < sizeof(buf); i++) {
if (buf[i] != 0xFFu) {
return 0;
}
}
return 1;
}
/* ===================== TC-NANDPORT-100..108:dhara_nand_is_free(整页扫描) =====================
*
* 旧实现只看页首 64 B,会把"页首 0xFF、后段有数据"的已编程页误判空闲
* -> dhara 往已编程页再编程 = 数据丢失。本组把"必须扫完整页"钉死,
* 并用 TC-NANDPORT-105 的旧算法对照证明这组断言真的能咬人。
*/
static void tc_is_free(void) {
static uint8_t page_buf[FAKE_PAGE_SIZE];
struct dhara_nand zeroed;
/* TC-NANDPORT-100:整页擦除 -> 1 */
printf("[TC-NANDPORT-100] erased page is free\n");
fake_chip_reset();
CHECK(dhara_nand_is_free(&s_ut_nand, 0u) == 1, "erased page -> is_free = 1");
CHECK(g_fake_page_read_cnt == 1u, "one page_read only (no per-byte commands)");
CHECK(g_fake_read_cache_cnt == (FAKE_PAGE_SIZE / 64u),
"erased page scans the whole page = 32 column reads (whole-page scan evidence)");
CHECK(g_fake_isfree_scan_cnt == (FAKE_PAGE_SIZE / 64u),
"64B column-read fingerprint calibrated (proves scenario really calls is_free)");
/* TC-NANDPORT-101:第 0 字节有数据 -> 0(快路径) */
printf("[TC-NANDPORT-101] byte 0 programmed -> not free\n");
fake_chip_reset();
(void)memset(page_buf, 0xFF, sizeof(page_buf));
page_buf[0] = 0x00u;
fake_poke_page(1u, page_buf, FAKE_PAGE_SIZE);
CHECK(dhara_nand_is_free(&s_ut_nand, 1u) == 0, "byte 0 programmed -> is_free = 0");
CHECK(g_fake_read_cache_cnt == 1u,
"early exit on first chunk = fast path (same cost as the old impl)");
/* TC-NANDPORT-102:is_free 整页精度回归用例
* 页首 64 B 全 0xFF、第 1000 字节 = 0x00 -> 必须答"非空"。
* 旧实现只看页首 64 B 会误判空闲,dhara 遂往这一页再编程。 */
printf("[TC-NANDPORT-102] is_free regression: hole in first 64B, data at byte 1000\n");
fake_chip_reset();
(void)memset(page_buf, 0xFF, sizeof(page_buf));
page_buf[1000] = 0x00u;
fake_poke_page(2u, page_buf, FAKE_PAGE_SIZE);
CHECK(dhara_nand_is_free(&s_ut_nand, 2u) == 0,
"first 64B all 0xFF, byte 1000 = 0x00 -> is_free = 0 (whole-page scan)");
CHECK(g_fake_read_cache_cnt > 1u,
"scan continues past the first 64B (old impl stopped here = root cause)");
/* TC-NANDPORT-103:只有最后一个字节非 0xFF -> 0 */
printf("[TC-NANDPORT-103] only last byte programmed -> not free\n");
fake_chip_reset();
(void)memset(page_buf, 0xFF, sizeof(page_buf));
page_buf[FAKE_PAGE_SIZE - 1u] = 0x5Au;
fake_poke_page(3u, page_buf, FAKE_PAGE_SIZE);
CHECK(dhara_nand_is_free(&s_ut_nand, 3u) == 0,
"only the very last byte non-0xFF -> is_free = 0");
CHECK(g_fake_read_cache_cnt == (FAKE_PAGE_SIZE / 64u),
"full scan to the page tail before answering (32 column reads)");
/* TC-NANDPORT-104:只有第 64 字节非 0xFF -> 0(旧窗口右边界外的第一个字节) */
printf("[TC-NANDPORT-104] only byte 64 programmed -> not free\n");
fake_chip_reset();
(void)memset(page_buf, 0xFF, sizeof(page_buf));
page_buf[64u] = 0x00u;
fake_poke_page(4u, page_buf, FAKE_PAGE_SIZE);
CHECK(dhara_nand_is_free(&s_ut_nand, 4u) == 0,
"only byte 64 (first byte outside the old window) -> is_free = 0");
/* TC-NANDPORT-105:旧算法对照(证明本套件有牙)
* 同一页形态:旧算法答 1 = 误判空闲,新实现答 0。
* 任何人把 is_free 改回"只看页首",这条立刻红。 */
printf("[TC-NANDPORT-105] old-algorithm contrast (test has teeth)\n");
fake_chip_reset();
(void)memset(page_buf, 0xFF, sizeof(page_buf));
page_buf[1000] = 0x00u;
fake_poke_page(5u, page_buf, FAKE_PAGE_SIZE);
CHECK(old_is_free_first64(&s_ut_nand, 5u) == 1,
"old 64B-only algorithm misjudges the same page as free -> 1");
CHECK(dhara_nand_is_free(&s_ut_nand, 5u) == 0,
"new implementation answers not-free -> 0 (opposite verdict = teeth)");
/* TC-NANDPORT-106:读失败保守判非空 */
printf("[TC-NANDPORT-106] read failure is answered conservatively\n");
fake_chip_reset();
s_fail_page_read = 1;
CHECK(dhara_nand_is_free(&s_ut_nand, 6u) == 0,
"page_read failure -> not free (never treat unreadable as free)");
fake_chip_reset();
s_fail_read_cache = 1;
CHECK(dhara_nand_is_free(&s_ut_nand, 7u) == 0,
"read_from_cache failure -> not free (conservative)");
/* TC-NANDPORT-107:NULL / num_blocks = 0 的几何守卫 */
printf("[TC-NANDPORT-107] NULL geometry / zero blocks guard\n");
fake_chip_reset();
CHECK(dhara_nand_is_free(NULL, 0u) == 0, "n = NULL -> not free, no crash");
CHECK(fake_chip_access_total() == 0u, "n = NULL causes zero chip access");
zeroed.log2_page_size = 11u;
zeroed.log2_ppb = 6u;
zeroed.num_blocks = 0u;
CHECK(dhara_nand_is_free(&zeroed, 0u) == 0, "num_blocks = 0 -> not free");
CHECK(fake_chip_access_total() == 0u, "num_blocks = 0 causes zero chip access");
/* TC-NANDPORT-108:擦除后重新判空(假芯片语义自洽,防"擦除没生效"假绿) */
printf("[TC-NANDPORT-108] fake chip erase really clears the array\n");
fake_chip_reset();
fake_fill_pattern(page_buf, FAKE_PAGE_SIZE, 7u);
fake_poke_page(8u, page_buf, FAKE_PAGE_SIZE);
CHECK(dhara_nand_is_free(&s_ut_nand, 8u) == 0, "programmed page -> not free");
CHECK(gd5f_block_erase(0u) == GD5F_OK, "erase the containing block (helper)");
CHECK(dhara_nand_is_free(&s_ut_nand, 8u) == 1, "after erase -> free (fake chip sane)");
}
/* ===================== TC-NANDPORT-110..112:is_free 越界守卫 ===================== */
static void tc_is_free_oob(void) {
printf("[TC-NANDPORT-110] is_free out-of-range pages\n");
fake_chip_reset();
CHECK(dhara_nand_is_free(&s_ut_nand, DHARA_PAGE_NONE) == 0,
"DHARA_PAGE_NONE -> is_free = 0");
CHECK(fake_chip_access_total() == 0u,
"DHARA_PAGE_NONE causes zero chip access (no truncated address)");
printf("[TC-NANDPORT-111] is_free at total_pages boundary\n");
fake_chip_reset();
CHECK(dhara_nand_is_free(&s_ut_nand, UT_OOB_PAGE) == 0,
"page == total_pages -> is_free = 0");
CHECK(fake_chip_access_total() == 0u, "page == total_pages -> zero chip access");
printf("[TC-NANDPORT-112] is_free beyond total_pages\n");
fake_chip_reset();
CHECK(dhara_nand_is_free(&s_ut_nand, UT_OOB_PAGE + 1u) == 0,
"page == total_pages + 1 -> is_free = 0");
CHECK(fake_chip_access_total() == 0u, "page == total_pages + 1 -> zero chip access");
}
/* 无任何写侧芯片访问("越界零写入"的判据量) */
static int fake_no_write_access(void) {
return (g_fake_write_enable_cnt == 0u) && (g_fake_program_load_cnt == 0u) &&
(g_fake_program_exec_cnt == 0u) && (g_fake_block_erase_cnt == 0u);
}
/* ===================== TC-NANDPORT-200..205:dhara_nand_prog ===================== */
static void tc_prog(void) {
static uint8_t page_buf[FAKE_PAGE_SIZE];
dhara_error_t err;
/* TC-NANDPORT-200:越界页 -> -1 + CORRUPT_MAP + 零写入 */
printf("[TC-NANDPORT-200] prog out-of-range page -> -1, zero write\n");
fake_chip_reset();
fake_fill_pattern(page_buf, FAKE_PAGE_SIZE, 11u);
err = DHARA_E_NONE;
CHECK(dhara_nand_prog(&s_ut_nand, UT_OOB_PAGE, page_buf, &err) == -1,
"prog page == total_pages -> -1");
CHECK(err == DHARA_E_CORRUPT_MAP, "prog out-of-range -> DHARA_E_CORRUPT_MAP");
CHECK(fake_no_write_access(), "prog out-of-range -> zero write access");
CHECK(g_fake_mark_bad_cnt == 0u, "prog out-of-range -> no block marked bad");
/* TC-NANDPORT-201:DHARA_PAGE_NONE -> -1 + CORRUPT_MAP + 零写入
* 旧实现会把 0xffffffff 加偏移送进 SPI 命令,被地址位截断成某个真实页
* -> 静默写坏无辜数据;现在必须在守卫处就挡住。 */
printf("[TC-NANDPORT-201] prog DHARA_PAGE_NONE -> -1, zero write\n");
fake_chip_reset();
err = DHARA_E_NONE;
CHECK(dhara_nand_prog(&s_ut_nand, DHARA_PAGE_NONE, page_buf, &err) == -1,
"prog DHARA_PAGE_NONE -> -1");
CHECK(err == DHARA_E_CORRUPT_MAP, "prog DHARA_PAGE_NONE -> DHARA_E_CORRUPT_MAP");
CHECK(fake_chip_access_total() == 0u, "prog DHARA_PAGE_NONE -> zero chip access");
/* TC-NANDPORT-202:data == NULL -> -1(守卫顺带挡住空指针) */
printf("[TC-NANDPORT-202] prog with NULL data -> -1\n");
fake_chip_reset();
err = DHARA_E_NONE;
CHECK(dhara_nand_prog(&s_ut_nand, 0u, NULL, &err) == -1, "prog data = NULL -> -1");
CHECK(err == DHARA_E_CORRUPT_MAP, "prog data = NULL -> DHARA_E_CORRUPT_MAP");
CHECK(fake_no_write_access(), "prog data = NULL -> zero write access");
/* TC-NANDPORT-203:正常页 -> 0 且回读一致 */
printf("[TC-NANDPORT-203] prog a valid page -> 0, read-back matches\n");
fake_chip_reset();
err = DHARA_E_NONE;
CHECK(dhara_nand_prog(&s_ut_nand, 64u, page_buf, &err) == 0, "prog page 64 -> 0");
CHECK(err == DHARA_E_NONE, "prog success leaves err = NONE");
CHECK(fake_page_equals(64u, page_buf, FAKE_PAGE_SIZE), "prog page 64 content on chip");
CHECK(g_fake_double_program_cnt == 0u, "prog onto an erased page -> no violation");
/* TC-NANDPORT-204:最后一个合法页(上界内边界) */
printf("[TC-NANDPORT-204] prog last valid page (boundary inside)\n");
fake_chip_reset();
err = DHARA_E_NONE;
CHECK(dhara_nand_prog(&s_ut_nand, UT_TOTAL_PAGES - 1u, page_buf, &err) == 0,
"prog page == total_pages - 1 -> 0");
CHECK(fake_page_equals(UT_TOTAL_PAGES - 1u, page_buf, FAKE_PAGE_SIZE),
"last valid page content on chip");
/* TC-NANDPORT-205:双编程探针自检——同一页再编程必须被记违规(防探针是死码) */
printf("[TC-NANDPORT-205] double-program probe really fires\n");
fake_chip_reset();
err = DHARA_E_NONE;
(void)dhara_nand_prog(&s_ut_nand, 128u, page_buf, &err);
(void)dhara_nand_prog(&s_ut_nand, 128u, page_buf, &err);
CHECK(g_fake_double_program_cnt == 1u, "reprogramming a programmed page -> 1 violation");
CHECK(g_fake_last_double_program_page == 128u, "violation records the offending page");
/* TC-NANDPORT-206:编程失败 -> BAD_BLOCK + 标坏该块(错误分类不被守卫改动破坏) */
printf("[TC-NANDPORT-206] program failure -> BAD_BLOCK + mark bad\n");
fake_chip_reset();
s_fail_program_exec = 1;
err = DHARA_E_NONE;
CHECK(dhara_nand_prog(&s_ut_nand, 2u * 64u, page_buf, &err) == -1,
"program_exec failure -> -1");
CHECK(err == DHARA_E_BAD_BLOCK, "program_exec failure -> DHARA_E_BAD_BLOCK");
CHECK(g_fake_mark_bad_cnt == 1u, "program failure marks exactly one block bad");
CHECK(g_fake_mark_bad_log[0] == 2u, "the block marked bad is the page's own block");
/* TC-NANDPORT-207:program_load 失败(装载阶段)-> BAD_BLOCK,但**不标坏块**。
* 【#79 修复】装载失败是传输层问题(02h 都没送达芯片),芯片从未回 P_FAIL,
* 把块标坏等于把"线抖了一下"固化成永久坏块。旧实现在此标坏,本用例把守。 */
printf("[TC-NANDPORT-207] program_load failure -> BAD_BLOCK, NO mark bad\n");
fake_chip_reset();
s_fail_program_load = 1;
err = DHARA_E_NONE;
CHECK(dhara_nand_prog(&s_ut_nand, 3u * 64u, page_buf, &err) == -1,
"program_load failure -> -1");
CHECK(err == DHARA_E_BAD_BLOCK, "program_load failure -> DHARA_E_BAD_BLOCK");
CHECK(g_fake_program_exec_cnt == 0u, "load failure short-circuits before program_exec");
CHECK(g_fake_mark_bad_cnt == 0u, "load failure must NOT mark bad (transport, not media)");
CHECK(fake_bad_unchanged(), "load failure leaves the BBT untouched");
/* TC-NANDPORT-208:program_exec 传输/超时失败(芯片未回 P_FAIL)-> 不标坏块。
* 【#79 核心】真机现场:出厂 0 坏块、底层写全部成功,而持久化 BBT 把
* 1016–1142 连续段判坏——正是这类失败被旧实现标坏并固化所致。 */
printf("[TC-NANDPORT-208] program_exec transport failure -> BAD_BLOCK, NO mark bad\n");
fake_chip_reset();
s_fail_exec_transport = 1;
err = DHARA_E_NONE;
CHECK(dhara_nand_prog(&s_ut_nand, 4u * 64u, page_buf, &err) == -1,
"program_exec transport failure -> -1");
CHECK(err == DHARA_E_BAD_BLOCK, "program_exec transport failure -> DHARA_E_BAD_BLOCK");
CHECK(g_fake_mark_bad_cnt == 0u, "exec transport failure must NOT mark bad");
CHECK(fake_bad_unchanged(), "exec transport failure leaves the BBT untouched");
}
/* ===================== TC-NANDPORT-300..307:dhara_nand_read ===================== */
static void tc_read(void) {
static uint8_t page_buf[FAKE_PAGE_SIZE];
static uint8_t rd[FAKE_PAGE_SIZE];
dhara_error_t err;
/* TC-NANDPORT-300/301:越界页 -> -1 + CORRUPT_MAP + 零访问 */
printf("[TC-NANDPORT-300] read out-of-range page -> -1, zero access\n");
fake_chip_reset();
err = DHARA_E_NONE;
CHECK(dhara_nand_read(&s_ut_nand, UT_OOB_PAGE, 0u, 512u, rd, &err) == -1,
"read page == total_pages -> -1");
CHECK(err == DHARA_E_CORRUPT_MAP, "read out-of-range -> DHARA_E_CORRUPT_MAP");
CHECK(fake_chip_access_total() == 0u, "read out-of-range -> zero chip access");
printf("[TC-NANDPORT-301] read DHARA_PAGE_NONE -> -1, zero access\n");
fake_chip_reset();
err = DHARA_E_NONE;
CHECK(dhara_nand_read(&s_ut_nand, DHARA_PAGE_NONE, 0u, 512u, rd, &err) == -1,
"read DHARA_PAGE_NONE -> -1");
CHECK(err == DHARA_E_CORRUPT_MAP, "read DHARA_PAGE_NONE -> DHARA_E_CORRUPT_MAP");
CHECK(fake_chip_access_total() == 0u, "read DHARA_PAGE_NONE -> zero chip access");
/* TC-NANDPORT-302:offset + length 越过 2048 -> -1 */
printf("[TC-NANDPORT-302] read spanning past the page end -> -1\n");
fake_chip_reset();
err = DHARA_E_NONE;
CHECK(dhara_nand_read(&s_ut_nand, 0u, 2000u, 100u, rd, &err) == -1,
"read offset 2000 + length 100 (> 2048) -> -1");
CHECK(err == DHARA_E_CORRUPT_MAP, "read overrun -> DHARA_E_CORRUPT_MAP");
CHECK(fake_chip_access_total() == 0u, "read overrun -> zero chip access");
err = DHARA_E_NONE;
CHECK(dhara_nand_read(&s_ut_nand, 0u, FAKE_PAGE_SIZE, 0u, rd, &err) == -1,
"read offset == 2048 (even with length 0) -> -1");
err = DHARA_E_NONE;
CHECK(dhara_nand_read(&s_ut_nand, 0u, 0u, FAKE_PAGE_SIZE + 1u, rd, &err) == -1,
"read length 2049 -> -1");
/* TC-NANDPORT-303:NULL data -> -1 */
printf("[TC-NANDPORT-303] read with NULL data -> -1\n");
fake_chip_reset();
err = DHARA_E_NONE;
CHECK(dhara_nand_read(&s_ut_nand, 0u, 0u, 512u, NULL, &err) == -1,
"read data = NULL -> -1");
CHECK(fake_chip_access_total() == 0u, "read data = NULL -> zero chip access");
/* TC-NANDPORT-304:正常部分读 -> 0 且内容正确 */
printf("[TC-NANDPORT-304] partial read returns the right slice\n");
prep_content_page(page_buf, 21u);
err = DHARA_E_NONE;
CHECK(dhara_nand_read(&s_ut_nand, UT_SRC_PAGE, 1024u, 512u, rd, &err) == 0,
"partial read (offset 1024, len 512) -> 0");
CHECK(memcmp(rd, &page_buf[1024], 512u) == 0, "partial read returns the right bytes");
CHECK(g_fake_page_read_cnt == 1u, "partial read issues one page_read");
/* TC-NANDPORT-305:整页读 -> 0 且内容正确(边界 length == 2048) */
printf("[TC-NANDPORT-305] full page read\n");
prep_content_page(page_buf, 21u);
err = DHARA_E_NONE;
CHECK(dhara_nand_read(&s_ut_nand, UT_SRC_PAGE, 0u, FAKE_PAGE_SIZE, rd, &err) == 0,
"full page read (length == 2048) -> 0");
CHECK(memcmp(rd, page_buf, FAKE_PAGE_SIZE) == 0, "full page read matches the chip");
/* TC-NANDPORT-306:ECC 不可纠正 -> -1 + DHARA_E_ECC */
printf("[TC-NANDPORT-306] uncorrectable ECC -> -1 + DHARA_E_ECC\n");
prep_content_page(page_buf, 21u);
s_fail_check_ecc = 1;
err = DHARA_E_NONE;
CHECK(dhara_nand_read(&s_ut_nand, UT_SRC_PAGE, 0u, 512u, rd, &err) == -1,
"check_ecc failure -> -1");
CHECK(err == DHARA_E_ECC, "check_ecc failure -> DHARA_E_ECC");
/* TC-NANDPORT-307:同一保守判据——read_from_cache 失败必须 -1(旧实现返回 0 交垃圾) */
printf("[TC-NANDPORT-307] read_from_cache failure must not report success\n");
prep_content_page(page_buf, 21u);
s_fail_read_cache = 1;
err = DHARA_E_NONE;
CHECK(dhara_nand_read(&s_ut_nand, UT_SRC_PAGE, 0u, 512u, rd, &err) == -1,
"read_from_cache failure -> -1 (not 0 with garbage data)");
CHECK(err == DHARA_E_ECC, "read_from_cache failure -> DHARA_E_ECC");
}
/* ===================== TC-NANDPORT-400..405:dhara_nand_copy ===================== */
static void tc_copy(void) {
static uint8_t page_buf[FAKE_PAGE_SIZE];
dhara_error_t err;
/* TC-NANDPORT-400:src 越界 -> -1 + CORRUPT_MAP + 零写入 */
printf("[TC-NANDPORT-400] copy with out-of-range src -> -1, zero write\n");
fake_chip_reset();
err = DHARA_E_NONE;
CHECK(dhara_nand_copy(&s_ut_nand, UT_OOB_PAGE, 100u, &err) == -1,
"copy src == total_pages -> -1");
CHECK(err == DHARA_E_CORRUPT_MAP, "copy bad src -> DHARA_E_CORRUPT_MAP");
CHECK(fake_no_write_access(), "copy bad src -> zero write access");
CHECK(fake_chip_access_total() == 0u, "copy bad src -> zero chip access at all");
/* TC-NANDPORT-401:src = DHARA_PAGE_NONE -> -1 + CORRUPT_MAP */
printf("[TC-NANDPORT-401] copy with DHARA_PAGE_NONE src -> -1\n");
fake_chip_reset();
err = DHARA_E_NONE;
CHECK(dhara_nand_copy(&s_ut_nand, DHARA_PAGE_NONE, 100u, &err) == -1,
"copy src = DHARA_PAGE_NONE -> -1");
CHECK(err == DHARA_E_CORRUPT_MAP, "copy DHARA_PAGE_NONE src -> DHARA_E_CORRUPT_MAP");
CHECK(fake_chip_access_total() == 0u, "copy DHARA_PAGE_NONE src -> zero chip access");
/* TC-NANDPORT-402:dst 越界 -> -1 + CORRUPT_MAP + 零写入 + 零标坏块
* dst 越界最致命:那是一次写入,旧实现会静默写进被截断出来的无辜页。 */
printf("[TC-NANDPORT-402] copy with out-of-range dst -> -1, zero write, zero mark-bad\n");
prep_content_page(page_buf, 31u);
err = DHARA_E_NONE;
CHECK(dhara_nand_copy(&s_ut_nand, UT_SRC_PAGE, UT_OOB_PAGE, &err) == -1,
"copy dst == total_pages -> -1");
CHECK(err == DHARA_E_CORRUPT_MAP, "copy bad dst -> DHARA_E_CORRUPT_MAP");
CHECK(fake_no_write_access(), "copy bad dst -> zero write access");
CHECK(g_fake_mark_bad_cnt == 0u, "copy bad dst -> no block marked bad");
/* TC-NANDPORT-403:dst = DHARA_PAGE_NONE(哨兵漏进来的真实形态) */
printf("[TC-NANDPORT-403] copy with DHARA_PAGE_NONE dst -> -1, zero write\n");
prep_content_page(page_buf, 31u);
err = DHARA_E_NONE;
CHECK(dhara_nand_copy(&s_ut_nand, UT_SRC_PAGE, DHARA_PAGE_NONE, &err) == -1,
"copy dst = DHARA_PAGE_NONE -> -1");
CHECK(err == DHARA_E_CORRUPT_MAP, "copy DHARA_PAGE_NONE dst -> DHARA_E_CORRUPT_MAP");
CHECK(fake_no_write_access(), "copy DHARA_PAGE_NONE dst -> zero write access");
CHECK(g_fake_mark_bad_cnt == 0u, "copy DHARA_PAGE_NONE dst -> zero mark_bad");
/* TC-NANDPORT-404:正常 copy -> 0 且目标页与源一致 */
printf("[TC-NANDPORT-404] normal copy moves the page content\n");
prep_content_page(page_buf, 31u);
err = DHARA_E_NONE;
CHECK(dhara_nand_copy(&s_ut_nand, UT_SRC_PAGE, 128u, &err) == 0,
"copy src -> erased dst -> 0");
CHECK(err == DHARA_E_NONE, "copy success leaves err = NONE");
CHECK(fake_page_equals(128u, page_buf, FAKE_PAGE_SIZE), "dst page equals src content");
CHECK(g_fake_double_program_cnt == 0u, "copy onto an erased page -> no violation");
/* TC-NANDPORT-405:copy 到已编程页 -> 探针记下双编程(场景层的判据基础) */
printf("[TC-NANDPORT-405] copy onto a programmed page is recorded as a violation\n");
prep_content_page(page_buf, 31u);
err = DHARA_E_NONE;
(void)dhara_nand_copy(&s_ut_nand, UT_SRC_PAGE, 128u, &err);
(void)dhara_nand_copy(&s_ut_nand, UT_SRC_PAGE, 128u, &err);
CHECK(g_fake_double_program_cnt == 1u, "second copy onto the same dst -> 1 violation");
}
/* ===================== TC-NANDPORT-500..504:dhara_nand_erase ===================== */
static void tc_erase(void) {
static uint8_t page_buf[FAKE_PAGE_SIZE];
dhara_error_t err;
uint32_t i;
/* TC-NANDPORT-500:越界块 -> -1 + CORRUPT_MAP + 零标坏块 */
printf("[TC-NANDPORT-500] erase out-of-range block -> -1, zero mark-bad\n");
fake_chip_reset();
err = DHARA_E_NONE;
CHECK(dhara_nand_erase(&s_ut_nand, UT_OOB_BLOCK, &err) == -1,
"erase block == num_blocks -> -1");
CHECK(err == DHARA_E_CORRUPT_MAP, "erase out-of-range -> DHARA_E_CORRUPT_MAP");
CHECK(g_fake_mark_bad_cnt == 0u, "erase out-of-range -> no block marked bad");
CHECK(fake_chip_access_total() == 0u, "erase out-of-range -> zero chip access");
/* TC-NANDPORT-501:块号 = 0xffffffff(哨兵当块号用) */
printf("[TC-NANDPORT-501] erase block 0xffffffff -> -1\n");
fake_chip_reset();
err = DHARA_E_NONE;
CHECK(dhara_nand_erase(&s_ut_nand, 0xFFFFFFFFu, &err) == -1,
"erase block = 0xffffffff -> -1");
CHECK(err == DHARA_E_CORRUPT_MAP, "erase block 0xffffffff -> DHARA_E_CORRUPT_MAP");
CHECK(g_fake_mark_bad_cnt == 0u, "erase block 0xffffffff -> zero mark_bad");
CHECK(fake_chip_access_total() == 0u, "erase block 0xffffffff -> zero chip access");
/* TC-NANDPORT-502:正常擦除 -> 0 且该块全页回 0xFF */
printf("[TC-NANDPORT-502] erase a valid block clears every page\n");
fake_chip_reset();
fake_fill_pattern(page_buf, FAKE_PAGE_SIZE, 41u);
for (i = 0; i < FAKE_PAGES_PER_BLOCK; i++) {
fake_poke_page(2u * FAKE_PAGES_PER_BLOCK + i, page_buf, FAKE_PAGE_SIZE);
}
CHECK(!fake_block_all_ff(2u), "block 2 programmed before erase");
err = DHARA_E_NONE;
CHECK(dhara_nand_erase(&s_ut_nand, 2u, &err) == 0, "erase block 2 -> 0");
CHECK(err == DHARA_E_NONE, "erase success leaves err = NONE");
CHECK(fake_block_all_ff(2u), "after erase every page of block 2 reads 0xFF");
CHECK(g_fake_block_erase_cnt == 1u, "erase issues one block-erase command");
/* TC-NANDPORT-503:最后一个合法块(上界内边界) */
printf("[TC-NANDPORT-503] erase last valid block (boundary inside)\n");
fake_chip_reset();
err = DHARA_E_NONE;
CHECK(dhara_nand_erase(&s_ut_nand, UT_OOB_BLOCK - 1u, &err) == 0,
"erase block == num_blocks - 1 -> 0");
/* TC-NANDPORT-504:芯片回 E_FAIL 的擦除失败 -> BAD_BLOCK + 标坏该块(正确处置) */
printf("[TC-NANDPORT-504] erase E_FAIL -> BAD_BLOCK + mark bad\n");
fake_chip_reset();
s_fail_block_erase = 1;
err = DHARA_E_NONE;
CHECK(dhara_nand_erase(&s_ut_nand, 3u, &err) == -1, "block_erase failure -> -1");
CHECK(err == DHARA_E_BAD_BLOCK, "block_erase failure -> DHARA_E_BAD_BLOCK");
CHECK(g_fake_mark_bad_cnt == 1u, "erase E_FAIL marks exactly one block bad");
CHECK(g_fake_mark_bad_log[0] == 3u, "the block marked bad is the one that failed");
/* TC-NANDPORT-505:擦除传输/超时失败(芯片未回 E_FAIL)-> 不标坏块。
* 【#79 核心】这是把 129 个好块误标并持久化的那条路径;标坏是**不可撤销**
* 的持久化动作,只有芯片明确回 E_FAIL 才允许。 */
printf("[TC-NANDPORT-505] erase transport failure -> BAD_BLOCK, NO mark bad\n");
fake_chip_reset();
s_fail_erase_transport = 1;
err = DHARA_E_NONE;
CHECK(dhara_nand_erase(&s_ut_nand, 5u, &err) == -1, "erase transport failure -> -1");
CHECK(err == DHARA_E_BAD_BLOCK, "erase transport failure -> DHARA_E_BAD_BLOCK");
CHECK(g_fake_mark_bad_cnt == 0u, "erase transport failure must NOT mark bad");
CHECK(fake_bad_unchanged(), "erase transport failure leaves the BBT untouched");
}
/* ===================== TC-NANDPORT-600..605:is_bad / mark_bad ===================== */
static void tc_bad_block(void) {
/* TC-NANDPORT-600:越界块 is_bad -> 1 且不碰驱动 */
printf("[TC-NANDPORT-600] is_bad out-of-range block -> 1, zero access\n");
fake_chip_reset();
CHECK(dhara_nand_is_bad(&s_ut_nand, UT_OOB_BLOCK) == 1,
"is_bad block == num_blocks -> 1 (conservative)");
CHECK(dhara_nand_is_bad(&s_ut_nand, 0xFFFFFFFFu) == 1,
"is_bad block = 0xffffffff -> 1 (conservative)");
CHECK(g_fake_is_bad_cnt == 0u, "is_bad out-of-range never reaches the driver");
/* TC-NANDPORT-601:NULL 几何 is_bad -> 1 */
printf("[TC-NANDPORT-601] is_bad with NULL geometry -> 1\n");
fake_chip_reset();
CHECK(dhara_nand_is_bad(NULL, 0u) == 1, "is_bad n = NULL -> 1, no crash");
CHECK(g_fake_is_bad_cnt == 0u, "is_bad n = NULL -> zero driver access");
/* TC-NANDPORT-602:合法好块 -> 0(透传驱动) */
printf("[TC-NANDPORT-602] is_bad on a valid block passes through\n");
fake_chip_reset();
CHECK(dhara_nand_is_bad(&s_ut_nand, 5u) == 0, "good block -> is_bad = 0");
CHECK(g_fake_is_bad_cnt == 1u, "valid block reaches the driver once");
/* TC-NANDPORT-603/604:越界 mark_bad -> 坏块表不变、计数为 0 */
printf("[TC-NANDPORT-603] mark_bad out-of-range leaves the BBT untouched\n");
fake_chip_reset();
fake_bad_snapshot();
dhara_nand_mark_bad(&s_ut_nand, UT_OOB_BLOCK);
CHECK(g_fake_mark_bad_cnt == 0u, "mark_bad block == num_blocks -> driver not called");
CHECK(fake_bad_unchanged(), "mark_bad out-of-range -> BBT unchanged");
CHECK(fake_chip_access_total() == 0u, "mark_bad out-of-range -> zero chip access");
printf("[TC-NANDPORT-604] mark_bad with 0xffffffff leaves the BBT untouched\n");
fake_chip_reset();
fake_bad_snapshot();
dhara_nand_mark_bad(&s_ut_nand, 0xFFFFFFFFu);
CHECK(g_fake_mark_bad_cnt == 0u, "mark_bad block = 0xffffffff -> driver not called");
CHECK(fake_bad_unchanged(), "mark_bad 0xffffffff -> BBT unchanged");
dhara_nand_mark_bad(NULL, 0u);
CHECK(g_fake_mark_bad_cnt == 0u, "mark_bad n = NULL -> driver not called, no crash");
/* TC-NANDPORT-605:合法块 mark_bad -> 驱动收到一次、块号正确、随后 is_bad = 1 */
printf("[TC-NANDPORT-605] mark_bad on a valid block really marks it\n");
fake_chip_reset();
dhara_nand_mark_bad(&s_ut_nand, 7u);
CHECK(g_fake_mark_bad_cnt == 1u, "valid mark_bad reaches the driver once");
CHECK(g_fake_mark_bad_log[0] == 7u, "driver receives the expected block number");
CHECK(dhara_nand_is_bad(&s_ut_nand, 7u) == 1, "marked block now reports bad");
}
/* ===================== TC-NANDPORT-700:dhara map 级场景(真链路) =====================
*
* 用真实 dhara_map_* 跑一轮"写入 -> sync -> 重新 resume -> 读回",断言:
* - 数据往返一致;
* - **全程双编程计数为 0**:is_free 一旦退回"只看页首 64 B",journal 会把
* 已编程的检查点页当空闲再编程一次,这个计数立刻非 0(场景级证据)。
*/
static void tc_dhara_scenario(void) {
static uint8_t wr[FAKE_PAGE_SIZE];
static uint8_t rd[FAKE_PAGE_SIZE];
dhara_error_t err;
int rc;
printf("[TC-NANDPORT-700] dhara map write -> resume -> read round-trip\n");
fake_chip_reset();
s_initialized = 0;
CHECK(nand_ftl_init() == 0, "first init on a blank chip creates the volume");
CHECK(s_nand.num_blocks == (unsigned int)FAKE_USABLE_BLOCKS,
"dhara sees gd5f_get_usable_blocks() blocks");
fake_fill_pattern(wr, FAKE_PAGE_SIZE, 0x5EEDu);
err = DHARA_E_NONE;
rc = dhara_map_write(&s_map, 0u, wr, &err);
CHECK(rc == 0, "dhara_map_write sector 0 -> 0");
err = DHARA_E_NONE;
rc = dhara_map_write(&s_map, 1u, wr, &err);
CHECK(rc == 0, "dhara_map_write sector 1 -> 0");
err = DHARA_E_NONE;
rc = dhara_map_sync(&s_map, &err);
CHECK(rc == 0, "dhara_map_sync -> 0");
CHECK(dhara_map_size(&s_map) == 2u, "map size = 2 sectors after two writes");
CHECK(g_fake_double_program_cnt == 0u, "write phase: zero double-program violations");
/* 模拟掉电重启:FTL 状态复位后重新 resume(同一块假芯片,内容保留) */
nand_ftl_deinit();
CHECK(nand_ftl_init() == 0, "re-init resumes the volume");
CHECK(g_fake_isfree_scan_cnt > 0u,
"resume really consulted dhara_nand_is_free (64B scan fingerprint > 0)");
CHECK(dhara_map_size(&s_map) == 2u, "resumed map still holds 2 sectors");
memset(rd, 0, sizeof(rd));
err = DHARA_E_NONE;
rc = dhara_map_read(&s_map, 0u, rd, &err);
CHECK(rc == 0, "read sector 0 after resume -> 0");
CHECK(memcmp(rd, wr, FAKE_PAGE_SIZE) == 0, "sector 0 data survives the resume");
memset(rd, 0, sizeof(rd));
err = DHARA_E_NONE;
rc = dhara_map_read(&s_map, 1u, rd, &err);
CHECK(rc == 0, "read sector 1 after resume -> 0");
CHECK(memcmp(rd, wr, FAKE_PAGE_SIZE) == 0, "sector 1 data survives the resume");
CHECK(g_fake_double_program_cnt == 0u,
"write + resume scenario: zero double-program violations (scenario guard)");
}
/* ===================== TC-NANDPORT-800..813:问题 #79 回归 =====================
*
* 现场(2026-11-04 台架,两次独立捕获):
* 探针写 /fschk.tmp → disk_write → ftl_flush_cache 对 LPN=211 反复 TOO_BAD
* → 判卷损坏 → storage_bring_up 调 nand_ftl_format() 想重建
* → **旧版 format 先 flush 同一脏页、失败即 return** → map 从没被清过
* → f_mkfs 在旧映射上跑 → 3 轮重试耗尽 → storage disabled(每次启动必现)。
*
* 本组测三件事:
* ① 脏缓存写不进去时,format **仍然成功**并清掉映射(修复点);
* ② format 后脏缓存被丢弃,不会带着旧 LPN 幽灵继续写(配套不变量);
* ③ 失败快照(C4)真的把"操作 / 失败类 / LPN / 次数"带出函数边界(新能力)。
*
* 假芯片的失败注入是**按页**的(s_fail_program_exec 是全局开关),而现场是
* "某个 LPN 的写回恒定失败"。这里用"让 program_exec 恒失败"逼近同一形态:
* 脏页写不进去 → flush 必失败,正是旧版 return 的那个分支。
*/
/* 把脏缓存喂进去:直接经 FatFs 叶子的 disk_write 不可行(那会先触发 init),
* 故手工构一个与 disk_write 相同的前置状态:缓存里有一页待写数据。 */
static void prep_dirty_cache(uint32_t lpn, uint8_t seed) {
fake_chip_reset();
s_cached_lpn = (dhara_sector_t)lpn;
fake_fill_pattern(s_cache_buf, FTL_PAGE_SIZE, seed);
s_cache_dirty = 1u;
}
static void tc_format_survives_flush_failure(void) {
uint8_t before[FTL_PAGE_SIZE];
/* ---- 前置:一个已建好、里面真有数据的卷(模拟台架"卷原本可用") ---- */
fake_chip_reset();
s_ut_nand.log2_page_size = 11u;
s_ut_nand.log2_ppb = 6u;
s_ut_nand.num_blocks = (unsigned int)FAKE_USABLE_BLOCKS;
dhara_map_init(&s_map, &s_ut_nand, s_page_buf, 4);
memset(s_cache_buf, 0, FTL_PAGE_SIZE);
s_cached_lpn = (dhara_sector_t)-1;
s_cache_dirty = 0;
s_initialized = 1;
(void)dhara_map_clear(&s_map);
{
dhara_error_t e;
(void)dhara_map_sync(&s_map, &e);
}
fake_fill_pattern(s_cache_buf, FTL_PAGE_SIZE, 0xA5u);
{
dhara_error_t e;
CHECK(dhara_map_write(&s_map, 0u, s_cache_buf, &e) == 0,
"TC-NANDPORT-800 precondition: sector 0 written");
}
fake_fill_pattern(s_cache_buf, FTL_PAGE_SIZE, 0x5Au);
{
dhara_error_t e;
CHECK(dhara_map_write(&s_map, 1u, s_cache_buf, &e) == 0,
"TC-NANDPORT-801 precondition: sector 1 written");
}
CHECK(dhara_map_size(&s_map) == 2u, "TC-NANDPORT-802 precondition: 2 sectors used");
/* ---- 关键形态:缓存脏着(脏页写不出去),此时调 format() ----
* 阶段 A 用"介质整体写不动"逼近最坏情况:program_load 全失败 ⇒ dhara 反复
* 标坏块 + 重试 8 次后 TOO_BAD,正是 `Cache flush FAIL: LPN=211 … TOO_BAD`
* 那条日志的来源。这一阶段**只断言死锁被打破**,不断言 format 返回 OK——
* 介质真的全写不动时它本来就落不了盘,报错是诚实的。 */
memset(s_cache_buf, 0xC3, FTL_PAGE_SIZE);
s_cached_lpn = (dhara_sector_t)211u; /* 与现场 LPN=211 同形 */
s_cache_dirty = 1u;
memcpy(before, s_cache_buf, FTL_PAGE_SIZE);
s_fail_program_load = 1;
(void)nand_ftl_format();
/* ★ 本单的核心断言:**旧实现到这里 map 一个字节都没清**
* (它在 ftl_flush_cache() 失败那行就 return 了,dhara_map_clear 从未执行)。
* 新实现必须已经把 map 清空、缓存作废——这正是"恢复动作本身被永久挡住"
* 这条死锁的破点。 */
CHECK(dhara_map_size(&s_map) == 0u,
"TC-NANDPORT-804 format clears the map even when dirty-cache flush fails (fix for #79)");
CHECK(s_cache_dirty == 0u, "TC-NANDPORT-805 format discards the dirty cache flag");
CHECK(s_cached_lpn == (dhara_sector_t)-1,
"TC-NANDPORT-806 format invalidates cached LPN (no ghost page reuse)");
CHECK(memcmp(s_cache_buf, before, FTL_PAGE_SIZE) == 0,
"TC-NANDPORT-807 format does not fabricate page content");
/* ---- 阶段 B:介质可用后(新片上电 = 无坏块积累、map 重新 resume),
* format 必须产出真正可写的空卷。现场对应"下次可写的格式化"。 ---- */
fake_chip_reset(); /* 同时复位全部失败注入 */
dhara_map_init(&s_map, &s_ut_nand, s_page_buf, 4);
memset(s_cache_buf, 0, FTL_PAGE_SIZE);
s_cached_lpn = (dhara_sector_t)-1;
s_cache_dirty = 0;
s_initialized = 1;
nand_ftl_fault_clear();
{
int rc = nand_ftl_format();
CHECK(rc == GD5F_OK, "TC-NANDPORT-808 on recovered media: format returns OK");
CHECK(dhara_map_size(&s_map) == 0u,
"TC-NANDPORT-808b on recovered media: map is empty after format");
}
{
dhara_error_t e;
int wrc;
fake_fill_pattern(s_cache_buf, FTL_PAGE_SIZE, 0x11u);
wrc = dhara_map_write(&s_map, 0u, s_cache_buf, &e);
CHECK(wrc == 0, "TC-NANDPORT-809 after format: new volume accepts writes");
CHECK(dhara_map_size(&s_map) == 1u, "TC-NANDPORT-810 after format: map counts 1 sector");
}
/* ---- 失败快照(C4):新能力,把失败类带出函数边界 ---- */
nand_ftl_fault_clear();
{
nand_ftl_fault_t f;
nand_ftl_fault_get(&f);
CHECK(f.active == 0u, "TC-NANDPORT-811 fault snapshot clear -> inactive");
CHECK(f.count == 0u, "TC-NANDPORT-811b fault snapshot clear -> count reset");
}
/* 造一次可归类的失败:脏缓存 + 写不动 */
fake_chip_reset();
memset(s_cache_buf, 0x77, FTL_PAGE_SIZE);
s_cached_lpn = (dhara_sector_t)211u;
s_cache_dirty = 1u;
s_fail_program_load = 1;
{
bsp_err_t cls = ftl_flush_cache();
nand_ftl_fault_t f;
CHECK(cls != BSP_OK, "TC-NANDPORT-812a injected flush failure returns an error class");
nand_ftl_fault_get(&f);
CHECK(f.active == 1u, "TC-NANDPORT-812 fault snapshot becomes active after a failure");
CHECK(f.op == NAND_FTL_OP_FLUSH, "TC-NANDPORT-812b snapshot records the failing operation");
CHECK(f.cls == (int32_t)cls, "TC-NANDPORT-812c snapshot records the failure class");
CHECK(f.lpn == 211u, "TC-NANDPORT-812d snapshot records the failing LPN");
CHECK(f.count == 1u, "TC-NANDPORT-812e snapshot counts the failure");
}
/* 再失败一次:count 累计(现场是同一页撞 15 次) */
{
nand_ftl_fault_t f;
(void)ftl_flush_cache();
nand_ftl_fault_get(&f);
CHECK(f.count == 2u, "TC-NANDPORT-813 repeated failures accumulate (same-page retry)");
CHECK(f.lpn == 211u, "TC-NANDPORT-813b repeated failures keep the same LPN");
}
s_fail_program_load = 0;
s_fail_program_page = 0u;
nand_ftl_fault_clear();
s_initialized = 0;
s_cached_lpn = (dhara_sector_t)-1;
s_cache_dirty = 0;
}
/* ===================== main ===================== */
int main(void) {
s_ut_nand.log2_page_size = 11u;
s_ut_nand.log2_ppb = 6u;
s_ut_nand.num_blocks = (unsigned int)FAKE_USABLE_BLOCKS;
printf("=== NAND Port Tests ===\n");
printf(" fake chip: %u blocks x %u pages x %u B; dhara sees %u blocks = %lu pages\n",
FAKE_BLOCKS, FAKE_PAGES_PER_BLOCK, FAKE_PAGE_SIZE,
s_ut_nand.num_blocks, (unsigned long)UT_TOTAL_PAGES);
fake_chip_reset();
CHECK(FAKE_TOTAL_PAGES == 2048u, "TC-NANDPORT-000 fake chip geometry (32 x 64 pages)");
CHECK(fake_page_all_ff(0u), "TC-NANDPORT-000b power-on state is all 0xFF");
CHECK(UT_TOTAL_PAGES == ((dhara_page_t)FAKE_USABLE_BLOCKS * FAKE_PAGES_PER_BLOCK),
"TC-NANDPORT-000c guard bound = num_blocks << log2_ppb");
printf("-- dhara_nand_is_free: whole-page scan --\n");
tc_is_free();
printf("-- dhara_nand_is_free: out-of-range guard --\n");
tc_is_free_oob();
printf("-- dhara_nand_prog --\n");
tc_prog();
printf("-- dhara_nand_read --\n");
tc_read();
printf("-- dhara_nand_copy --\n");
tc_copy();
printf("-- dhara_nand_erase --\n");
tc_erase();
printf("-- dhara_nand_is_bad / dhara_nand_mark_bad --\n");
tc_bad_block();
printf("-- dhara map scenario (real dhara, fake chip) --\n");
tc_dhara_scenario();
printf("-- format survives dirty-cache flush failure + fault snapshot (issue #79) --\n");
tc_format_survives_flush_failure();
printf("\n=== NAND Port Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}