fix(storage): dhara 移植层 is_free 整页精度 + HAL 越界守卫;日志读写批量化
登记簿 N-21(报告 N-12/N-13)与 N-24(报告 N-16)两项未闭环问题修复并销项。 N-12:dhara_nand_is_free() 旧实现只读页首 64 B 判全 0xFF,会把"页首 0xFF、后段有数据"的已编程页误判空闲,dhara 遂往已编程页再编程 = 位碰撞、 journal/map 指向垃圾且不报任何错。改整页扫描(栈上 64 B 分块、页首命中即 早退,故"已编程且页首有数据"的绝大多数页成本与旧实现同价)。 N-13:报告原建议经核对不成立——本仓裁剪版 dhara 既无 DHARA_NO_PAGE(哨兵 实为 DHARA_PAGE_NONE,journal.h:46)也无 DHARA_E_UNSUPPORTED(error.h 枚举 里没有,照抄直接编译失败),且 map.c 三处 dhara_journal_copy 调用点各自已 挡掉哨兵。真风险是移植层零上界校验:越界页号被芯片地址位截断成某个真实页 → 静默读写错页。改为六个 HAL 入口统一几何上界守卫 + DHARA_E_CORRUPT_MAP。 错误码选择是刻意的:BAD_BLOCK 会让 journal.c:904 标坏无辜好块,RECOVER 会 让 map.c:275 启动回收;无错误通道的 is_free/is_bad/mark_bad 一律取保守值。 N-16 写侧:一次排空合并成"每文件一次 open/write/close"(≤8 条 = 512 B 静态缓冲)。掉电语义不变——批次仍在同一次 drain 调用内 close,而 close 回写目录项这个不同 LPN 会把 FTL 单页写回缓存逼出到 NAND;失败尾部回投 队列,条目不丢(旧实现 -2 卷忙会直接丢掉那一条)。容量轮转仍只在按条慢 路径做,批量路径发现装不下一批就返回 -3 退回。 N-16 读侧:s_read_raw() 加 8 条滑动读窗口 + 写者代数失效,API 与 UI 零改动。 验证:@build 0 错 0 警(Code 213,540 / ER_IROM1 232,280 B = 22.2%, RW_IRAM1 75,232 B = 57.4%,ZI +1,640 B 全为静态缓冲、不吃任务栈); @test PASS,且台架 journal 恢复点与修复前同轨——连续四次启动 sectors used 恒 14,094、Map head 每轮 +128 单调推进、epoch 稳定 3 → 整页 is_free 未 改变恢复语义、无双编程、无回退;stack_report 9 个任务全 PASS(defaultTask 静态最坏链 3,024 B / 余 40%,实测峰值 2,164 B / 余 57%)。 新增主机侧移植层单测 test/host/nand_port/(第 16 个 -unit-test 钩子): RAM 假芯片 + 真实 dhara map/journal 源,149 断言,含"旧算法只看页首 64 B" 对照用例(将来有人把 is_free 改回去会立刻红)与 dhara 级 resume 双编程计数 为 0 的场景。L0.5 由 15 runner / 955 断言 → 16 runner / 1,104 断言。 文档:存储架构说明 §5.3 加移植层两条硬约束;运行与自检记录设计说明 §4.4/§4.5 写批量化与读窗口;GD5F Trap Records 新增 Trap 07(含三条遗留 代价与一条待办编译期断言);登记簿 N-21/N-24 删行销项并同步 README §1/§2.0/§2.1、排期详述 C3、报告 §0.1/§0.2/§0.4/§4.x/§7 与去处表; 钩子计数 33→34、主机侧单测 15→16 全库同步。
This commit is contained in:
@@ -12,6 +12,15 @@
|
||||
* "复位后所有写入丢失"时 sectors used 不足以判定
|
||||
* journal 回退(覆盖写不改 count),head/tail/epoch 是
|
||||
* 唯一能暴露 resume 锚错 checkpoint 的证据
|
||||
* 2026-10-15 王建锋 N-21(报告 N-12/N-13):① is_free 改**整页扫描**——
|
||||
* 旧实现只看页首 64 B,会把"页首 0xFF、后段有数据"的
|
||||
* 已编程页误判空闲,dhara 遂往已编程页再编程 = 数据丢失
|
||||
* (页首有数据的绝大多数页走快路径,成本与旧实现同价);
|
||||
* ② HAL 六个入口全部加几何上界守卫——越界页/块号(含
|
||||
* DHARA_PAGE_NONE)旧会被地址位截断成真实页 → 静默读写
|
||||
* 错页,现报 DHARA_E_CORRUPT_MAP(不能用 BAD_BLOCK:会
|
||||
* 误标无辜好块;不能用 RECOVER:会触发回收);
|
||||
* ③ 补主机侧移植层单测 test/host/nand_port/(L0.5)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -45,6 +54,38 @@
|
||||
static inline uint32_t ftl_phy_block(dhara_block_t b) { return b + FTL_START_BLOCK; }
|
||||
static inline uint32_t ftl_phy_page(dhara_page_t p) { return p + FTL_START_PAGE; }
|
||||
|
||||
/*
|
||||
* N-21(2026-10-15,报告 N-13):移植层地址守卫
|
||||
*
|
||||
* 旧实现把 dhara 传来的页/块号直接加偏移送进 SPI 命令,**不做任何上界校验**。
|
||||
* 越界页号(含 dhara 的"无页"哨兵 DHARA_PAGE_NONE = 0xffffffff,journal.h:46)
|
||||
* 经 ftl_phy_page() 相加后会被芯片地址位截断成某个**真实存在的页** → 静默读/写
|
||||
* 错页,比报错严重得多(数据错而传输"成功",与 CCM-DMA 那条同族)。
|
||||
*
|
||||
* 为什么报 DHARA_E_CORRUPT_MAP 而不是报告原建议的 DHARA_E_UNSUPPORTED:
|
||||
* 本仓裁剪版 dhara 的错误枚举(Lib/dhara/error.h:20-31)里**没有**
|
||||
* E_UNSUPPORTED,写上去直接编译失败。
|
||||
* 为什么不用 DHARA_E_BAD_BLOCK:journal.c:904 的 recover_from 只对 BAD_BLOCK
|
||||
* 走"标坏块 + 跳块重试"——对一个凭空的地址标坏块 = 毁掉一个无辜的好块。
|
||||
* 为什么不会触发静默清卷:map.c:275 的 try_recover 只对 E_RECOVER 启动回收,
|
||||
* 其余错误一律原样上抛(E-31 教训:单次瞬态毛刺不得销毁整卷数据)。
|
||||
* is_free / is_bad 没有错误通道 → 一律保守取值(判非空 / 判坏),与 E-42 同族
|
||||
* 口径一致:宁可少用一个块,不可把不可信的内容当有效。
|
||||
*/
|
||||
static int ftl_page_in_range(const struct dhara_nand *n, dhara_page_t p) {
|
||||
if ((n == NULL) || (n->num_blocks == 0u)) {
|
||||
return 0;
|
||||
}
|
||||
return p < ((dhara_page_t)n->num_blocks << n->log2_ppb);
|
||||
}
|
||||
|
||||
static int ftl_block_in_range(const struct dhara_nand *n, dhara_block_t b) {
|
||||
if ((n == NULL) || (n->num_blocks == 0u)) {
|
||||
return 0;
|
||||
}
|
||||
return b < n->num_blocks;
|
||||
}
|
||||
|
||||
/* ======================== 私有变量 ======================== */
|
||||
|
||||
/* dhara 核心数据结构 */
|
||||
@@ -103,18 +144,28 @@ static int ftl_read_page(dhara_sector_t lpn) {
|
||||
/* ======================== dhara NAND HAL ======================== */
|
||||
|
||||
int dhara_nand_is_bad(const struct dhara_nand *n, dhara_block_t b) {
|
||||
(void)n;
|
||||
if (ftl_block_in_range(n, b) == 0) {
|
||||
return 1; /* 越界块:保守判坏,让 dhara 跳过 */
|
||||
}
|
||||
return gd5f2gq5ue_is_block_bad(ftl_phy_block(b));
|
||||
}
|
||||
|
||||
void dhara_nand_mark_bad(const struct dhara_nand *n, dhara_block_t b) {
|
||||
(void)n;
|
||||
if (ftl_block_in_range(n, b) == 0) {
|
||||
return; /* 越界块:绝不标坏,避免毁掉无辜好块 */
|
||||
}
|
||||
gd5f2gq5ue_mark_block_bad(ftl_phy_block(b));
|
||||
}
|
||||
|
||||
int dhara_nand_erase(const struct dhara_nand *n, dhara_block_t b, dhara_error_t *err) {
|
||||
(void)n;
|
||||
int ret = gd5f_block_erase(ftl_phy_block(b));
|
||||
int ret;
|
||||
|
||||
if (ftl_block_in_range(n, b) == 0) {
|
||||
dhara_set_error(err, DHARA_E_CORRUPT_MAP);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = gd5f_block_erase(ftl_phy_block(b));
|
||||
if (ret == GD5F_ERASE_FAIL || ret != GD5F_OK) {
|
||||
dhara_nand_mark_bad(n, b);
|
||||
dhara_set_error(err, DHARA_E_BAD_BLOCK);
|
||||
@@ -125,7 +176,13 @@ int dhara_nand_erase(const struct dhara_nand *n, dhara_block_t b, dhara_error_t
|
||||
|
||||
int dhara_nand_prog(const struct dhara_nand *n, dhara_page_t p, const uint8_t *data, dhara_error_t *err) {
|
||||
int ret;
|
||||
uint32_t phy_pg = ftl_phy_page(p);
|
||||
uint32_t phy_pg;
|
||||
|
||||
if ((ftl_page_in_range(n, p) == 0) || (data == NULL)) {
|
||||
dhara_set_error(err, DHARA_E_CORRUPT_MAP);
|
||||
return -1;
|
||||
}
|
||||
phy_pg = ftl_phy_page(p);
|
||||
|
||||
gd5f_write_enable();
|
||||
ret = gd5f_program_load(0, data, FTL_PAGE_SIZE);
|
||||
@@ -149,38 +206,61 @@ int dhara_nand_prog(const struct dhara_nand *n, dhara_page_t p, const uint8_t *d
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* N-21(2026-10-15,报告 N-12):整页扫描
|
||||
*
|
||||
* 旧实现只看页首 64 B。dhara 的 journal 用 is_free 判断"检查点组是否未编程"
|
||||
* (journal.c:416 cp_free、journal.c:520 找下一空闲页),其正确性前提是
|
||||
* **is_free 答"空闲"的页必须真的全 0xFF**——journal.c:405-414 的注释论证了
|
||||
* "只要组内被编程过的页含非 0xFF 字节就能识别出来",可若那一页恰好页首 64 B
|
||||
* 是 0xFF、后段才有数据,旧实现就会误判空闲。误判的后果不是报错,而是
|
||||
* **往已编程的页再编程一次** → 位碰撞、journal/map 指向垃圾 = 数据丢失。
|
||||
* 本卷上跑的是 FatFS 数据页,"页首一段 0xFF、后段有数据"完全常见(刚格式化的
|
||||
* 卷、稀疏文件块、日志尾部),所以必须整页判。
|
||||
*
|
||||
* 成本处理:先查页首 64 B——页首就有数据的页(绝大多数已编程页)当场返回非空,
|
||||
* 与旧实现同价;只有"目前读到的全是 0xFF"的页才继续扫后段。分块用栈上 64 B
|
||||
* 缓冲,不新增 2 KB 静态 RAM(页缓冲三块不可合并的评估见报告 R-3)。
|
||||
*/
|
||||
int dhara_nand_is_free(const struct dhara_nand *n, dhara_page_t p) {
|
||||
(void)n;
|
||||
uint32_t phy_pg = ftl_phy_page(p);
|
||||
int fre;
|
||||
uint8_t buf[64];
|
||||
uint32_t phy_pg;
|
||||
uint32_t off;
|
||||
uint32_t i;
|
||||
|
||||
if (ftl_page_in_range(n, p) == 0) {
|
||||
return 0; /* 越界页号:保守判非空 */
|
||||
}
|
||||
phy_pg = ftl_phy_page(p);
|
||||
|
||||
if (gd5f_page_read(phy_pg) != GD5F_OK) {
|
||||
fre = 0;
|
||||
} else {
|
||||
uint8_t buf[64];
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (gd5f_read_from_cache(0, buf, sizeof(buf)) != GD5F_OK) {
|
||||
for (off = 0; off < FTL_PAGE_SIZE; off += sizeof(buf)) {
|
||||
if (gd5f_read_from_cache((uint16_t)off, buf, sizeof(buf)) != GD5F_OK) {
|
||||
/* E-42 同族(2026-10-03):读失败 → buf 可能为陈旧/垃圾内容,
|
||||
* 保守判为非空(块不可复用),避免把"没读到内容的块"判成空闲。 */
|
||||
fre = 0;
|
||||
} else {
|
||||
uint32_t i;
|
||||
|
||||
fre = 1;
|
||||
for (i = 0; i < sizeof(buf); i++) {
|
||||
if (buf[i] != 0xFF) {
|
||||
fre = 0;
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
for (i = 0; i < sizeof(buf); i++) {
|
||||
if (buf[i] != 0xFFu) {
|
||||
return 0; /* 页首命中即返回 = 快路径,不读后段 */
|
||||
}
|
||||
}
|
||||
}
|
||||
return fre;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int dhara_nand_read(const struct dhara_nand *n, dhara_page_t p, size_t offset, size_t length, uint8_t *data, dhara_error_t *err) {
|
||||
(void)n;
|
||||
uint32_t phy_pg = ftl_phy_page(p);
|
||||
uint32_t phy_pg;
|
||||
|
||||
if ((ftl_page_in_range(n, p) == 0) || (data == NULL) ||
|
||||
(offset >= FTL_PAGE_SIZE) || (length > (FTL_PAGE_SIZE - offset))) {
|
||||
dhara_set_error(err, DHARA_E_CORRUPT_MAP);
|
||||
return -1;
|
||||
}
|
||||
phy_pg = ftl_phy_page(p);
|
||||
|
||||
if (gd5f_page_read(phy_pg) != GD5F_OK) {
|
||||
dhara_set_error(err, DHARA_E_ECC);
|
||||
@@ -201,8 +281,23 @@ int dhara_nand_read(const struct dhara_nand *n, dhara_page_t p, size_t offset, s
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* N-21(2026-10-15,报告 N-13):src / dst 双端守卫
|
||||
*
|
||||
* 报告原判断"dhara map 在回收/擦除路径会用 DHARA_NO_PAGE 试探"经逐点核对
|
||||
* **不成立**:本仓裁剪版没有 DHARA_NO_PAGE,哨兵叫 DHARA_PAGE_NONE
|
||||
* (journal.h:46),而 map.c 三处 dhara_journal_copy 调用点都已先挡掉它
|
||||
* (map.c:260 挡 pad_queue 的 p、map.c:434/440 挡 alt_page、map.c:242 的 src
|
||||
* 来自 trace_path 的真实页)。但"移植层不做任何上界校验"这个风险是真的:
|
||||
* 哨兵一旦从别处漏进来(未来改动、被污染的 meta),ftl_phy_page() 会把
|
||||
* 0xffffffff 截断成某个真实页 → **静默搬走无辜数据**。故 src/dst 各挡一次,
|
||||
* dst 越界尤其致命——那是一次写入。
|
||||
*/
|
||||
int dhara_nand_copy(const struct dhara_nand *n, dhara_page_t src, dhara_page_t dst, dhara_error_t *err) {
|
||||
(void)n;
|
||||
if ((ftl_page_in_range(n, src) == 0) || (ftl_page_in_range(n, dst) == 0)) {
|
||||
dhara_set_error(err, DHARA_E_CORRUPT_MAP);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (dhara_nand_read(n, src, 0, FTL_PAGE_SIZE, s_copy_buf, err) < 0) {
|
||||
return -1;
|
||||
|
||||
Reference in New Issue
Block a user