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:
@@ -414,6 +414,25 @@ repos:
|
||||
files: ^(App/log/soe_rec\.(c|h)|App/log/soe_store\.(c|h)|test/host/soe/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7n) dhara NAND 移植层(nand_ftl)主机侧单元测试(2026-10-15 加,N-21 / 报告 N-12+N-13)
|
||||
# 背景:移植层六个 HAL 入口是**纯地址算术**,此前对越界页/块号(含
|
||||
# DHARA_PAGE_NONE 哨兵)**不做任何上界校验**——加偏移送进 SPI 命令后被地址位
|
||||
# 截断成某个真实页 → 静默读写/搬走无辜数据;而 is_free 旧实现只看页首 64 B,
|
||||
# 会把"页首 0xFF、后段有数据"的已编程页误判空闲 → dhara 往已编程页再编程
|
||||
# = 数据丢失。两类缺陷真机**极难复现**(要恰好撞上特定页形态 / 被污染的 meta),
|
||||
# 板端只能表现为"某次复位后丢数据",无从定位。
|
||||
# 本钩子用 MSVC 离线编译真实 nand_ftl.c(#include 纳入)+ **RAM 假芯片**
|
||||
# (32 块 x 64 页 x 2048 B):把"双编程"做成可数事件(N-12 探针)、记录全部
|
||||
# 访问次数,于是"越界不产生任何芯片访问""越界标坏块次数为 0"可机械断言;
|
||||
# 并内联一份"只看页首 64 B"的旧算法做对照,证明这组断言真能咬人。
|
||||
# 触发面含 dhara 本体(测试一并 #include map.c/journal.c 跑真链路场景)。
|
||||
- id: nand-port-unit-test
|
||||
name: dhara NAND 移植层单测(主机侧)
|
||||
entry: python test/host/nand_port/run_unit_test.py
|
||||
language: system
|
||||
files: ^(Drivers/BSP/GD5F2GQ5UE/nand_ftl\.(c|h)|Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue\.h|Lib/dhara/(map|journal|nand|error|bytes)\.(c|h)|test/host/nand_port/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 8) 测试套件开关门禁(2026-10-01 加)
|
||||
# 背景:test_config.h 的 TEST_SUITE_* 决定固件里编入哪个测试套件,
|
||||
# 全部注释 = 产品构建。但此前**无任何门禁检查**——实测已提交的 HEAD 里
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
## 提交门禁(pre-commit)
|
||||
|
||||
- 任一失败即拦截,共 **33 个钩子**(逐个详解 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 15 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验,N-23)+ `mem-budget`(RAM 92% 预算门禁,N-27)
|
||||
- 任一失败即拦截,共 **34 个钩子**(逐个详解 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 16 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验,N-23)+ `mem-budget`(RAM 92% 预算门禁,N-27)
|
||||
- **范围 = 项目代码**:第三方(`Lib/`、`Middlewares/`、CMSIS、HAL、`lftpd`、`dhara`、`MDK-ARM`)排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`。**`simulator/` 已纳入全部钩子**(2026-10-11)——生成物只靠 `.gitignore` 挡,**不在 `exclude` 里写目录名**(教训见 `docs/代码规范/pre-commit使用说明.md` §5.5)
|
||||
- ⚠ **pre-commit 4.x 已移除 `always: true`**(实测对 `local` 钩子只告警 `Unexpected key(s)` 并跳过)→ 想"总是跑"的钩子**必须让 `files:` 覆盖到所有会触发它的文件**,没有兜底开关
|
||||
- **改写型钩子(`trailing-whitespace` + `end-of-file-fixer`)豁免 CubeMX/Keil 生成文件**(配置里 YAML 锚点 `&cubemx-generated`);⚠ **只读检查不受影响**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` / `mem-numbers` 仍覆盖 `Src/`、`Inc/`
|
||||
@@ -173,7 +173,7 @@
|
||||
| ADC 采样节拍 / 通道映射 / V0.1 硬件缺陷 | `docs/子系统设计/ADC采样节拍设计说明.md`、`docs/硬件平台/ADC通道映射说明.md` |
|
||||
| 存储栈(FatFs / dhara / lftpd / 两条 FatFs 陷阱) | `docs/架构设计/STM32F4-Base存储架构说明.md` |
|
||||
| RAM / 栈预算 / 容量台账 | `docs/内存管理/RAM使用分析与优化方案.md`、`内存内容分配策略.md` |
|
||||
| pre-commit 33 个钩子逐个详解 | `docs/代码规范/pre-commit使用说明.md` |
|
||||
| pre-commit 34 个钩子逐个详解 | `docs/代码规范/pre-commit使用说明.md` |
|
||||
| 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` |
|
||||
| 自动化测试 / 实机测试 / 板级套件 | `docs/测试说明/自动化测试流程.md`、`实机测试实施指南.md`、`board-test-suite` skill |
|
||||
| 烧录与复位语义 | `docs/测试说明/ST-LINK_CLI_复位使用说明.md` |
|
||||
|
||||
+299
-62
@@ -45,6 +45,12 @@ static const uint32_t s_caps[SYSLOG_COUNT] = { SYSLOG_RUN_MAX, SYSLOG_SELFTEST_M
|
||||
|
||||
#define SYSLOG_QUEUE_MAX 8u
|
||||
|
||||
/* N-24(2026-10-15,报告 N-16 写侧):一次排空的合并写入上限。
|
||||
* 8 条 × 64 B = 512 B 静态缓冲(不吃任务栈),与队列深度 SYSLOG_QUEUE_MAX 同值
|
||||
* ——即"一轮排空最多一次 open/write/close"。 */
|
||||
#define SYSLOG_BATCH_MAX 8u
|
||||
static uint8_t s_batch[SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE];
|
||||
|
||||
/* 待发条目:desc 为调用方静态串(Flash 字面量),只存指针不拷贝 */
|
||||
typedef struct {
|
||||
uint8_t id;
|
||||
@@ -52,32 +58,81 @@ typedef struct {
|
||||
} syslog_entry_t;
|
||||
|
||||
static syslog_entry_t s_preboot[SYSLOG_QUEUE_MAX]; /* 启动前待发数组(RTOS 前单生产者/后单消费者,免锁) */
|
||||
static syslog_entry_t s_batch_src[SYSLOG_BATCH_MAX]; /* 本批条目的原始来源(N-24:未落盘的尾部要回投队列,故必须留住 id + desc 指针);
|
||||
* 唯一写者 defaultTask,走静态不吃栈 */
|
||||
static volatile uint8_t s_preboot_cnt; /* 数组条目数 */
|
||||
static osMessageQueueId_t s_queue; /* 运行期消息队列(syslog_drain 首次调用时惰性创建) */
|
||||
static const osMessageQueueAttr_t s_queue_attr = { .name = "syslog" };
|
||||
static volatile uint8_t s_fs_ready; /* 卷挂载 + /log 目录就绪后置位 */
|
||||
static volatile uint8_t s_mount_fail_reported; /* P2-9:建目录失败日志去重(成功复位) */
|
||||
|
||||
/* N-24(2026-10-15,报告 N-16 读侧):8 条滑动读窗口 + 写者代数。
|
||||
* 窗口 512 B 走静态(不吃任务栈;defaultTask/uiTask 最坏链见 stack_report.py,
|
||||
* 预算余量见 docs/内存管理/RAM预算台账.md)。s_rd_gen 由写者自增、读者比对,
|
||||
* 对齐 32 位在 ARM 上原子,跨任务无需临界区。 */
|
||||
#define SYSLOG_RD_WINDOW 8u
|
||||
static uint8_t s_rd_win[SYSLOG_RD_WINDOW * SYSLOG_REC_SIZE];
|
||||
static int32_t s_rd_win_id = -1; /* -1 = 窗口空 */
|
||||
static uint32_t s_rd_win_base; /* 窗口首条序号 */
|
||||
static uint32_t s_rd_win_cnt; /* 窗口内有效条数 */
|
||||
static uint32_t s_rd_win_gen; /* 填充时看到的写者代数 */
|
||||
static volatile uint32_t s_rd_gen[SYSLOG_COUNT]; /* 每次成功落盘 / unlink 后自增 */
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:追加一条记录(格式化 64B + FA_OPEN_APPEND;容量满先删除重建)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* desc - 描述串(UTF-8,超长截断至 SYSLOG_DESC_MAX) const char *
|
||||
* 返回值:0 - 成功;-1 - 写失败;-2 - 卷被占用(顺延,非故障) int
|
||||
* 限定条件:仅 defaultTask(唯一写者)调用;卷已挂载
|
||||
* 函数功能:格式化一条 64 B 定长记录(时间戳 + 空格 + epoch + 空格 + 描述)
|
||||
* 入口参数:id - 日志 ID(当前不参与编码,仅为接口对称) syslog_id_t
|
||||
* desc - 描述串(UTF-8,超长截断至 SYSLOG_DESC_MAX) const char *
|
||||
* p_rec - 输出缓冲(SYSLOG_REC_SIZE 字节,不可为 NULL) uint8_t *
|
||||
* 返回值:无
|
||||
* 限定条件:时间戳未就绪时落 "1970-01-01 08:00:00" 占位串
|
||||
* 函数说明:N-24(2026-10-15,报告 N-16)从 s_append 抽出,供"单条写"与
|
||||
* "批量写"共用——两条路径必须产出逐字节相同的记录,否则 UI 按定长
|
||||
* 字段偏移解析会错位。
|
||||
*/
|
||||
static int s_append(syslog_id_t id, const char *desc) {
|
||||
static FIL f; /* static:FF_FS_TINY=0 → 局部 FIL 吃 528 B 栈;
|
||||
* 本函数全程在 vol_lock 区间内,跨任务被卷锁串行化 */
|
||||
FRESULT fr;
|
||||
UINT bw = 0u;
|
||||
uint8_t rec[SYSLOG_REC_SIZE];
|
||||
static void s_format_rec(syslog_id_t id, const char *desc, uint8_t *p_rec) {
|
||||
char ts[SYSLOG_TS_LEN + 1u];
|
||||
uint32_t epoch;
|
||||
size_t dl;
|
||||
|
||||
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) {
|
||||
(void)id;
|
||||
memset(p_rec, 0u, SYSLOG_REC_SIZE);
|
||||
if (sys_clock_get_str(ts, sizeof(ts)) == NULL) {
|
||||
memcpy(ts, "1970-01-01 08:00:00", SYSLOG_TS_LEN);
|
||||
}
|
||||
ts[SYSLOG_TS_LEN] = '\0';
|
||||
epoch = sys_clock_get();
|
||||
if (epoch == UINT32_MAX) {
|
||||
epoch = 0u;
|
||||
}
|
||||
memcpy(p_rec, ts, SYSLOG_TS_LEN);
|
||||
p_rec[SYSLOG_TS_LEN] = ' ';
|
||||
(void)snprintf((char *)(p_rec + SYSLOG_EPOCH_OFFS), SYSLOG_EPOCH_LEN + 1u, "%010u", (unsigned int)epoch);
|
||||
p_rec[SYSLOG_DESC_OFFS - 1u] = ' ';
|
||||
dl = strlen(desc);
|
||||
if (dl > SYSLOG_DESC_MAX) {
|
||||
dl = SYSLOG_DESC_MAX;
|
||||
}
|
||||
memcpy(p_rec + SYSLOG_DESC_OFFS, desc, dl);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把一条**已格式化**的 64 B 记录追加落盘(容量满先删除重建)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* p_rec - 记录缓冲(SYSLOG_REC_SIZE 字节,不可为 NULL) const uint8_t *
|
||||
* 返回值:0 - 成功;-1 - 写失败;-2 - 卷被占用(顺延,非故障) int
|
||||
* 限定条件:仅 defaultTask(唯一写者)调用;卷已挂载
|
||||
*/
|
||||
static int s_append_rec(syslog_id_t id, const uint8_t *p_rec) {
|
||||
static FIL f; /* static:FF_FS_TINY=0 → 局部 FIL 吃 528 B 栈;
|
||||
* 本函数全程在 vol_lock 区间内,跨任务被卷锁串行化 */
|
||||
static uint8_t s_fault_reported[SYSLOG_COUNT];
|
||||
FRESULT fr;
|
||||
UINT bw = 0u;
|
||||
int rc = 0;
|
||||
|
||||
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || p_rec == NULL) {
|
||||
return -1;
|
||||
}
|
||||
/* 卷锁 try:本任务 = 看门狗喂狗点,绝不允许阻塞等锁(阻塞 = 喂狗停摆 =
|
||||
@@ -94,33 +149,11 @@ static int s_append(syslog_id_t id, const char *desc) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
memset(rec, 0u, sizeof(rec));
|
||||
if (sys_clock_get_str(ts, sizeof(ts)) == NULL) {
|
||||
memcpy(ts, "1970-01-01 08:00:00", SYSLOG_TS_LEN);
|
||||
}
|
||||
ts[SYSLOG_TS_LEN] = '\0';
|
||||
epoch = sys_clock_get();
|
||||
if (epoch == UINT32_MAX) {
|
||||
epoch = 0u;
|
||||
}
|
||||
memcpy(rec, ts, SYSLOG_TS_LEN);
|
||||
rec[SYSLOG_TS_LEN] = ' ';
|
||||
(void)snprintf((char *)(rec + SYSLOG_EPOCH_OFFS), SYSLOG_EPOCH_LEN + 1u, "%010u", (unsigned int)epoch);
|
||||
rec[SYSLOG_DESC_OFFS - 1u] = ' ';
|
||||
dl = strlen(desc);
|
||||
if (dl > SYSLOG_DESC_MAX) {
|
||||
dl = SYSLOG_DESC_MAX;
|
||||
}
|
||||
memcpy(rec + SYSLOG_DESC_OFFS, desc, dl);
|
||||
|
||||
/* L1 文件级自愈:陈旧/损坏条目 → 丢弃重建、重试一次(与上方容量满重建
|
||||
* 同一机制);L2 错误去重:持续故障只报第一次,恢复前静默(消除"每次
|
||||
* 开机同一错误刷屏")。打开模式必须带 FA_WRITE 位:本 FatFs 移植的
|
||||
* FA_OPEN_APPEND(0x30) 不含 FA_WRITE(0x02),不带写位打开的文件 f_write
|
||||
* 恒返回 FR_DENIED(7)——2026-09-20 台架实测 /log 写失败 ×5/开机 真根因 */
|
||||
static uint8_t s_fault_reported[SYSLOG_COUNT];
|
||||
int rc = 0;
|
||||
|
||||
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
|
||||
if (fr != FR_OK && (fr == FR_DENIED || fr == FR_INVALID_OBJECT ||
|
||||
fr == FR_INT_ERR || fr == FR_DISK_ERR ||
|
||||
@@ -130,14 +163,14 @@ static int s_append(syslog_id_t id, const char *desc) {
|
||||
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
|
||||
}
|
||||
if (fr == FR_OK) {
|
||||
fr = f_write(&f, rec, SYSLOG_REC_SIZE, &bw);
|
||||
fr = f_write(&f, p_rec, SYSLOG_REC_SIZE, &bw);
|
||||
(void)f_close(&f);
|
||||
if (fr != FR_OK || bw != SYSLOG_REC_SIZE) {
|
||||
(void)f_unlink(s_paths[id]);
|
||||
bw = 0u;
|
||||
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
|
||||
if (fr == FR_OK) {
|
||||
fr = f_write(&f, rec, SYSLOG_REC_SIZE, &bw);
|
||||
fr = f_write(&f, p_rec, SYSLOG_REC_SIZE, &bw);
|
||||
(void)f_close(&f);
|
||||
}
|
||||
if (fr != FR_OK || bw != SYSLOG_REC_SIZE) {
|
||||
@@ -153,6 +186,106 @@ static int s_append(syslog_id_t id, const char *desc) {
|
||||
s_fault_reported[id] = 1u;
|
||||
DBG_ERROR("write %s fail (%d)", s_paths[id], (int)fr);
|
||||
}
|
||||
/* 读窗口失效:本函数可能改过文件内容(追加 / 容量满重建 / 自愈 unlink),
|
||||
* 一律自增代数让读者重填——失败路径多失效一次只是多读一轮,不会读错。 */
|
||||
s_rd_gen[id]++;
|
||||
vol_lock_release();
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:追加一条记录(格式化 + 单条落盘)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* desc - 描述串(UTF-8,超长截断至 SYSLOG_DESC_MAX) const char *
|
||||
* 返回值:0 - 成功;-1 - 写失败;-2 - 卷被占用(顺延,非故障) int
|
||||
* 限定条件:仅 defaultTask(唯一写者)调用;卷已挂载
|
||||
* 函数说明:慢路径 / 兼容入口。批量排空走 s_append_many();容量轮转、
|
||||
* sys_log_evidence() 的 flush 诱导、开机首条"power on/reboot"仍走这里。
|
||||
*/
|
||||
static int s_append(syslog_id_t id, const char *desc) {
|
||||
uint8_t rec[SYSLOG_REC_SIZE];
|
||||
|
||||
if (desc == NULL) {
|
||||
return -1;
|
||||
}
|
||||
s_format_rec(id, desc, rec);
|
||||
return s_append_rec(id, rec);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把 n 条**已格式化**的记录一次性追加到同一日志文件(一次 open/write/close)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* p_buf - 连续 n 条 64 B 记录(n × SYSLOG_REC_SIZE 字节) const uint8_t *
|
||||
* n - 记录条数(1 ~ SYSLOG_BATCH_MAX) uint32_t
|
||||
* 返回值:0 - 成功;-1 - 写失败;-2 - 卷被占用(顺延,非故障);
|
||||
* -3 - 剩余容量不足 n 条,本批不能走快路径(交回按条慢路径做轮转) int
|
||||
* 限定条件:仅 defaultTask(唯一写者)调用;卷已挂载
|
||||
* 函数说明:N-24(2026-10-15,报告 N-16 写侧)。旧实现每条记录一轮
|
||||
* f_open + f_write(64 B) + f_close,而 f_close 要回写目录项、f_write
|
||||
* 要对目标扇区做读-改-写 → 每条日志 2~4 次 NAND 页读写。本函数把
|
||||
* 同一次排空里属于同一文件的最多 8 条合并成一次 512 B 追加写。
|
||||
* **不改掉电语义**:批次仍在同一次 syslog_drain() 调用内写完并
|
||||
* f_close;close 回写目录项(另一个 LPN)会把 FTL 的单页写回缓存逼出
|
||||
* 到 NAND——该"写到不同 LPN 才刷前一脏页"的语义见 sys_log_evidence()
|
||||
* 的 flush 诱导注释。落盘时机与旧实现每条 close 等价,批次不跨调用驻留 RAM。
|
||||
* 自愈重试与错误去重的判据与 s_append_rec 一致;容量不足时返回 -3
|
||||
* 而不是就地 unlink——"满了就重建"必须由按条慢路径统一执行,
|
||||
* 否则一批 8 条会跨越多轮重建、语义说不清。
|
||||
*/
|
||||
static int s_append_many(syslog_id_t id, const uint8_t *p_buf, uint32_t n) {
|
||||
static FIL f;
|
||||
static uint8_t s_batch_fault[SYSLOG_COUNT];
|
||||
FRESULT fr;
|
||||
UINT bw = 0u;
|
||||
UINT total;
|
||||
int rc = 0;
|
||||
|
||||
if (((uint32_t)id >= (uint32_t)SYSLOG_COUNT) || (p_buf == NULL) ||
|
||||
(n == 0u) || (n > SYSLOG_BATCH_MAX)) {
|
||||
return -1;
|
||||
}
|
||||
if (vol_lock_wait(0u) == 0u) {
|
||||
return -2;
|
||||
}
|
||||
if ((syslog_count(id) + n) > s_caps[id]) {
|
||||
vol_lock_release();
|
||||
return -3;
|
||||
}
|
||||
total = (UINT)(n * SYSLOG_REC_SIZE);
|
||||
|
||||
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
|
||||
if (fr != FR_OK && (fr == FR_DENIED || fr == FR_INVALID_OBJECT ||
|
||||
fr == FR_INT_ERR || fr == FR_DISK_ERR ||
|
||||
fr == FR_NO_FILE || fr == FR_NO_PATH ||
|
||||
fr == FR_INVALID_NAME || fr == FR_NO_FILESYSTEM)) {
|
||||
(void)f_unlink(s_paths[id]);
|
||||
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
|
||||
}
|
||||
if (fr == FR_OK) {
|
||||
fr = f_write(&f, p_buf, total, &bw);
|
||||
(void)f_close(&f);
|
||||
if (fr != FR_OK || bw != total) {
|
||||
(void)f_unlink(s_paths[id]);
|
||||
bw = 0u;
|
||||
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
|
||||
if (fr == FR_OK) {
|
||||
fr = f_write(&f, p_buf, total, &bw);
|
||||
(void)f_close(&f);
|
||||
}
|
||||
if (fr != FR_OK || bw != total) {
|
||||
rc = -1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rc = -1;
|
||||
}
|
||||
if (rc == 0) {
|
||||
s_batch_fault[id] = 0u;
|
||||
} else if (s_batch_fault[id] == 0u) {
|
||||
s_batch_fault[id] = 1u;
|
||||
DBG_ERROR("write %s x%lu fail (%d)", s_paths[id], (unsigned long)n, (int)fr);
|
||||
}
|
||||
s_rd_gen[id]++;
|
||||
vol_lock_release();
|
||||
return rc;
|
||||
}
|
||||
@@ -168,33 +301,78 @@ static int s_append(syslog_id_t id, const char *desc) {
|
||||
* open/read 可交错,读到的可能是半删除态文件。try 语义(timeout=0):
|
||||
* 本路径可由 uiTask 调用,uiTask 在 required_mask 内、须每 100ms 喂
|
||||
* 心跳,绝不能阻塞等锁;busy = 本次读失败(UI 下轮刷新重读,非故障)。
|
||||
*
|
||||
* N-24(2026-10-15,报告 N-16 读侧):加**8 条滑动窗口**。旧实现每读
|
||||
* 一条记录都要 open + lseek + read + close 一轮,UI 翻 20 条记录 =
|
||||
* 20 次目录项/ FAT 往返(报告 N-16 的"UI 翻记录逐条 open/seek/close")。
|
||||
* 记录定长 64 B,故一次 open + 一次 lseek + 一次 f_read 就能取回连续
|
||||
* 8 条(512 B),命中窗口时零 I/O 直接从 RAM 返回。API 与调用方
|
||||
* (ui_page_runlog / ui_page_selftest 的逐行取数)一律不变。
|
||||
* 失效机制:写者每次成功落盘/重建后把 s_rd_gen[id] 自增,读者比对
|
||||
* 填充时记录的代数——不匹配就重填。ARM 上对齐的 32 位读写是原子的,
|
||||
* 跨任务(uiTask 填、defaultTask 增)无需额外临界区;最坏情况是
|
||||
* 多读一轮,不会读到错数据。
|
||||
*/
|
||||
static int s_read_raw(syslog_id_t id, uint32_t idx, uint8_t *p_rec) {
|
||||
static FIL f; /* static:省 528 B 栈;open~close 全在卷锁区间内
|
||||
* (uiTask 与写者任务由 vol_lock 串行化,见上方 P2-10 说明) */
|
||||
FRESULT fr;
|
||||
UINT br = 0u;
|
||||
int rc;
|
||||
uint32_t total;
|
||||
uint32_t base;
|
||||
uint32_t want;
|
||||
|
||||
if (p_rec == NULL || idx >= syslog_count(id)) {
|
||||
if (p_rec == NULL) {
|
||||
return -1;
|
||||
}
|
||||
total = syslog_count(id);
|
||||
if (idx >= total) {
|
||||
return -1;
|
||||
}
|
||||
/* 快路径:窗口命中(同 id、代数未变、落在窗口区间内)→ 零 I/O */
|
||||
if ((s_rd_win_id == (int32_t)id) && (s_rd_win_gen == s_rd_gen[id]) &&
|
||||
(idx >= s_rd_win_base) && (idx < s_rd_win_base + s_rd_win_cnt)) {
|
||||
(void)memcpy(p_rec, s_rd_win + ((size_t)(idx - s_rd_win_base) * SYSLOG_REC_SIZE),
|
||||
SYSLOG_REC_SIZE);
|
||||
return 0;
|
||||
}
|
||||
if (vol_lock_wait(0u) == 0u) {
|
||||
return -1; /* 卷被占用:本次读失败(非故障,下轮重读) */
|
||||
}
|
||||
/* 重新校验:等锁期间写者可能已推进(虽然 try 语义下不会阻塞等待) */
|
||||
total = syslog_count(id);
|
||||
if (idx >= total) {
|
||||
vol_lock_release();
|
||||
return -1;
|
||||
}
|
||||
base = (idx / SYSLOG_RD_WINDOW) * SYSLOG_RD_WINDOW; /* 窗口按 8 条对齐 */
|
||||
want = total - base;
|
||||
if (want > SYSLOG_RD_WINDOW) {
|
||||
want = SYSLOG_RD_WINDOW;
|
||||
}
|
||||
fr = f_open(&f, s_paths[id], FA_READ);
|
||||
if (fr != FR_OK) {
|
||||
vol_lock_release();
|
||||
return -1;
|
||||
}
|
||||
fr = f_lseek(&f, (FSIZE_t)idx * SYSLOG_REC_SIZE);
|
||||
fr = f_lseek(&f, (FSIZE_t)base * SYSLOG_REC_SIZE);
|
||||
if (fr == FR_OK) {
|
||||
fr = f_read(&f, p_rec, SYSLOG_REC_SIZE, &br);
|
||||
fr = f_read(&f, s_rd_win, (UINT)(want * SYSLOG_REC_SIZE), &br);
|
||||
}
|
||||
(void)f_close(&f);
|
||||
rc = (fr == FR_OK && br == SYSLOG_REC_SIZE) ? 0 : -1;
|
||||
vol_lock_release();
|
||||
return rc;
|
||||
|
||||
if ((fr != FR_OK) || (br != (UINT)(want * SYSLOG_REC_SIZE))) {
|
||||
s_rd_win_cnt = 0u; /* 填充失败:作废窗口,避免半窗被当有效命中 */
|
||||
s_rd_win_id = -1;
|
||||
return -1;
|
||||
}
|
||||
s_rd_win_base = base;
|
||||
s_rd_win_cnt = want;
|
||||
s_rd_win_id = (int32_t)id;
|
||||
s_rd_win_gen = s_rd_gen[id];
|
||||
(void)memcpy(p_rec, s_rd_win + ((size_t)(idx - base) * SYSLOG_REC_SIZE), SYSLOG_REC_SIZE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -271,8 +449,6 @@ void syslog_mcu_mounted(void) {
|
||||
* 限定条件:仅 defaultTask(唯一写者)调用;卷未就绪时直接返回(条目滞留待重试)
|
||||
*/
|
||||
void syslog_drain(void) {
|
||||
syslog_entry_t e;
|
||||
|
||||
/* 运行期惰性创建消息队列:本函数仅由 defaultTask(调度器运行中)调用,
|
||||
保证内核对象创建发生在 osKernelStart 之后 */
|
||||
if (s_queue == NULL) {
|
||||
@@ -284,29 +460,90 @@ void syslog_drain(void) {
|
||||
if (!s_fs_ready || s_queue == NULL) {
|
||||
return;
|
||||
}
|
||||
while (s_preboot_cnt > 0u) {
|
||||
e = s_preboot[0];
|
||||
if (s_append((syslog_id_t)e.id, e.desc) != 0) {
|
||||
break; /* 落盘失败:保留剩余条目,下轮重试 */
|
||||
}
|
||||
memmove(&s_preboot[0], &s_preboot[1], sizeof(syslog_entry_t) * (size_t)(s_preboot_cnt - 1u));
|
||||
s_preboot_cnt--;
|
||||
}
|
||||
|
||||
/* N-24(2026-10-15,报告 N-16 写侧):一批一批地排空——同一文件、最多
|
||||
* SYSLOG_BATCH_MAX 条合并成一次 open/write/close(旧实现每条一轮
|
||||
* open/write/close,每条日志 2~4 次 NAND 页读写)。
|
||||
* 取数规则:preboot 数组按下标"看而不删",整批落盘成功才真正删除;
|
||||
* 队列必须取出才能看下一条,故未落盘的尾部一律回投队列——
|
||||
* **条目不丢**(旧实现 -2 卷忙会直接丢掉那一条)。 */
|
||||
for (;;) {
|
||||
if (osMessageQueueGet(s_queue, &e, 0u, 0u) != osOK) {
|
||||
break; /* 队列空 */
|
||||
syslog_entry_t e;
|
||||
uint32_t n = 0u;
|
||||
uint32_t n_pre = 0u;
|
||||
uint32_t i;
|
||||
uint32_t done;
|
||||
int rc;
|
||||
syslog_id_t bid = SYSLOG_RUN;
|
||||
|
||||
while (n < SYSLOG_BATCH_MAX) {
|
||||
uint8_t from_pre = 0u;
|
||||
|
||||
if (n_pre < s_preboot_cnt) {
|
||||
e = s_preboot[n_pre];
|
||||
n_pre++;
|
||||
from_pre = 1u;
|
||||
} else if (osMessageQueueGet(s_queue, &e, 0u, 0u) != osOK) {
|
||||
break; /* 队列空 */
|
||||
}
|
||||
if (n == 0u) {
|
||||
bid = (syslog_id_t)e.id;
|
||||
} else if ((syslog_id_t)e.id != bid) {
|
||||
/* 换文件:本批先落盘,这条留给下一批(原处不动或回投队列) */
|
||||
if (from_pre != 0u) {
|
||||
n_pre--;
|
||||
} else {
|
||||
(void)osMessageQueuePut(s_queue, &e, 0u, 0u);
|
||||
}
|
||||
break;
|
||||
}
|
||||
s_batch_src[n] = e;
|
||||
s_format_rec(bid, e.desc, s_batch + ((size_t)n * SYSLOG_REC_SIZE));
|
||||
n++;
|
||||
}
|
||||
{
|
||||
int arc = s_append((syslog_id_t)e.id, e.desc);
|
||||
if (arc == -2) {
|
||||
break; /* 卷被占用(FTP 文件操作进行中):本轮顺延,静默
|
||||
(条目已出队;run.log 尽力而为语义,不计故障) */
|
||||
if (n == 0u) {
|
||||
break; /* 无更多条目 */
|
||||
}
|
||||
|
||||
rc = s_append_many(bid, s_batch, n);
|
||||
done = n;
|
||||
if (rc == -3) {
|
||||
/* 剩余容量装不下一批:退回按条慢路径("满了 unlink 重建"只在
|
||||
* 那里做,避免一批跨越多轮重建、语义说不清) */
|
||||
done = 0u;
|
||||
for (i = 0u; i < n; i++) {
|
||||
int r1 = s_append(bid, s_batch_src[i].desc);
|
||||
|
||||
if (r1 == -2) {
|
||||
break; /* 卷忙:尾部回投队列 */
|
||||
}
|
||||
done++;
|
||||
if (r1 != 0) {
|
||||
DBG_ERROR("drop entry id=%d", (int)bid);
|
||||
break; /* 写失败:该条已丢,尾部回投 */
|
||||
}
|
||||
}
|
||||
if (arc != 0) {
|
||||
DBG_ERROR("drop entry id=%d", (int)e.id);
|
||||
break; /* 落盘失败:停止排空,避免连续丢弃 */
|
||||
} else if (rc == -1) {
|
||||
done = 0u;
|
||||
DBG_ERROR("drop %lu entries id=%d", (unsigned long)n, (int)bid);
|
||||
}
|
||||
/* rc == -2(卷忙):整批回投队列,静默顺延,不计故障 */
|
||||
|
||||
for (i = done; i < n; i++) {
|
||||
if (osMessageQueuePut(s_queue, &s_batch_src[i], 0u, 0u) != osOK) {
|
||||
DBG_ERROR("hold-over drop (queue full): id=%d", (int)s_batch_src[i].id);
|
||||
}
|
||||
}
|
||||
/* 本批消耗的 preboot 条目:已落盘的删除;未落盘的已转投队列,同样删除
|
||||
* (否则会重复落盘) */
|
||||
if (n_pre > 0u) {
|
||||
memmove(&s_preboot[0], &s_preboot[n_pre],
|
||||
sizeof(syslog_entry_t) * (size_t)(s_preboot_cnt - n_pre));
|
||||
s_preboot_cnt = (uint8_t)(s_preboot_cnt - n_pre);
|
||||
}
|
||||
if (rc != 0) {
|
||||
break; /* 本轮到此为止,下轮再试 */
|
||||
}
|
||||
}
|
||||
/* B9:SOE 落盘挂本函数尾部——同一唯一写者(defaultTask)、同一卷就绪门
|
||||
(上方 s_fs_ready 早退蕴含 /log 已建)、同一卷锁 try 语义;protectTask
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -418,6 +418,17 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
|
||||
| **为什么需要** | 记录格式是**磁盘上的字节级契约**——编解码错了要 FTP 下载 + 解码才看得见,板端日志照不出来;与 `/log/run.log` 同款定长 64 B 语义 |
|
||||
| **运行器** | `test/host/soe/run_unit_test.py`(MSVC `cl.exe`,L0.5) |
|
||||
|
||||
### 2.23 nand-port-unit-test — dhara NAND 移植层主机侧单测(2026-10-15 加,N-21)
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| **触发条件** | `Drivers/BSP/GD5F2GQ5UE/nand_ftl.(c\|h)`、`gd5f2gq5ue.h`、`Lib/dhara/(map\|journal\|nand\|error\|bytes).(c\|h)`、`test/host/nand_port/**` 或 `test/host/common/unit_test_harness.py` 变动 |
|
||||
| **处理方式** | **阻止提交**(单测失败即拦截) |
|
||||
| **被测对象** | `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` 的 dhara NAND HAL 六个入口(`is_bad` / `mark_bad` / `erase` / `prog` / `is_free` / `read` / `copy`)——纯地址算术 + 边界判定;测试单元 `#include` 真实 `nand_ftl.c` + 真实 `Lib/dhara/journal.c` / `map.c`,芯片侧换成 **RAM 假芯片**(32 块 × 64 页 × 2048 B,两段式读缓存、NOR 叠加编程、双编程违规计数、访问次数记账) |
|
||||
| **用例** | 149 断言:整页扫描(页首 64 B 全 0xFF + 第 1000 字节有数据 / 页尾单字节 / 第 64 字节)+ **旧算法对照**(内联"只看页首 64 B"的旧实现,同一页答"空闲"而新实现答"非空")+ 六个入口的越界守卫(`DHARA_PAGE_NONE` / `total_pages` / `num_blocks`,断言 `-1` + `DHARA_E_CORRUPT_MAP` + **零芯片访问 / 零标坏块**)+ 正常往返 + 故障注入(ECC / read_from_cache / program_load / program_exec / erase 失败)+ dhara map 级"写入 → 重新 resume"场景(用 64 B 列读指纹证明 resume 真走了 is_free,再断言全程**双编程计数为 0**) |
|
||||
| **为什么需要** | N-12 / N-13 两类缺陷真机**极难复现**:is_free 误判要恰好撞上"页首 0xFF、后段有数据"的页形态,越界地址要哨兵从别处漏进来;两者发作后都只表现为"某次复位后丢数据",无从定位。移植层是纯逻辑 + 假芯片,能钉死真机极难复现的越界/误判路径,且"越界不产生任何芯片访问"这类判据在板端**根本拿不到观测面** |
|
||||
| **运行器** | `test/host/nand_port/run_unit_test.py`(MSVC `cl.exe`,L0.5);套件说明见 `test/host/README.md` |
|
||||
|
||||
---
|
||||
|
||||
## 3. 日常工作流
|
||||
@@ -545,7 +556,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
|
||||
|
||||
| 文件 | 位置 | 说明 |
|
||||
|---|---|---|
|
||||
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**33 个钩子**,2026-10-12 清点;本文 §2 详解非单测类——N-19 新增的两个单测钩子见 §2.16/§2.17,N-23 双端注册反向校验见 §2.18,N-27 RAM 预算门禁见 §2.19,B4 IEC104 协议栈单测见 §2.20,B8 频率测量单测见 §2.21,B9 SOE 动作记录单测见 §2.22,其余主机侧单测见 AGENTS.md 门禁条;主机侧单测共 15 个) |
|
||||
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**34 个钩子**,2026-10-15 清点;本文 §2 详解非单测类——N-19 新增的两个单测钩子见 §2.16/§2.17,N-23 双端注册反向校验见 §2.18,N-27 RAM 预算门禁见 §2.19,B4 IEC104 协议栈单测见 §2.20,B8 频率测量单测见 §2.21,B9 SOE 动作记录单测见 §2.22,N-21 dhara NAND 移植层单测见 §2.23,其余主机侧单测见 AGENTS.md 门禁条;主机侧单测共 16 个) |
|
||||
| `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) |
|
||||
| `.cubemx_survival_baseline.json` | 仓库根 | CubeMX 易失定制基线(哈希;`--write` 重建) |
|
||||
| `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 |
|
||||
|
||||
+9
-11
@@ -66,8 +66,7 @@
|
||||
| P1 收尾 | 1 | P1-10 测试固件构建实测(`测试与架构专项.md` §1) |
|
||||
| 网络 | 4 | N-2(49s 日志静默,**低风险观察项**)· N-3(L5 基线重验)· N-16(A2/A4 负向台架)· N-17(探针窗口心跳缺口) |
|
||||
| 存储 / 日志 | 3 | **S-3(USART1 TX-only 根治)** · S-4(dhara 重试自旋)· S-5(可选全序列卷锁) |
|
||||
| 数据正确性 | 1 | **N-21(dhara `is_free`/`copy`,全簿最高风险)** |
|
||||
| 结构 / 健壮性 | 6 | N-22(写回 flush)· N-23(注册校验残余白名单)· N-24(日志逐条开关)· N-25(性能/语义簇)· N-28(Flash 死代码簇)· **N-33(栈余量收口,W1)** |
|
||||
| 结构 / 健壮性 | 5 | N-22(写回 flush)· N-23(注册校验残余白名单)· N-25(性能/语义簇)· N-28(Flash 死代码簇)· **N-33(栈余量收口,W1)** |
|
||||
| 仓库 | 1 | N-29(`.git` **235 MB**,**已决策暂不处理、登记观察**) |
|
||||
| 硬件 | 1 | N-18(V0.2 改版项) |
|
||||
| 测试 / 基础设施 | 6 | T-7(CI runner + 推送闭环 + 第二机交叉验证)· S12(断言宏)· S1-c(可选单测)· A-4(采集链路测试约束,RS485 侧随 B2 暂缓)· A-5 残余(CI 体积门禁盲区)· D7(串口助手 A6/A7 待 CH340) |
|
||||
@@ -78,14 +77,14 @@
|
||||
> `docs/编号规范.md` §4.3):E-25 / E-27 / E-28 挂 C1(net 层跨上下文收口,见 `排期详述.md` §1);
|
||||
> E-47 待 CH395F 驱动侧定案;E-12 为 `FF_FS_LOCK` / 单写者纪律记录项;E-31 后续与 E-62
|
||||
> 为回归监督项(纪律已固化于 `AGENTS.md`,无需改动)。
|
||||
> UI 标准化簇与覆盖率缺口(lftpd/dhara 移植层无主机测试)为整簇排期项,见 §2 W6 / N-21。
|
||||
> UI 标准化簇与 lftpd 主机测试缺口为整簇排期项,见 §2 W6(dhara 移植层主机单测已随登记簿
|
||||
> N-21 销项落地 `test/host/nand_port/`,2026-10-15)。
|
||||
|
||||
### 1.1 结构性判断(排序依据)
|
||||
|
||||
1. **用户决定(2026-10-12):先把 ADC 采集、保护等相关功能完成**——"转发 + 保护"两环代码已落地(B4/B5,2026-10-12),但**尚未台架闭环、功能面不完整**(定值不落盘、无频率保护、无动作记录/SOE、栈安全线两处 FAIL)。新 W1 = 采集→相量→保护→上送链路的**收尾与闭环**(B6~B10 + N-33,见 `W1-采集保护收尾.md`),其余波次顺延。**RS-485 对下采集(B2)按用户决定暂缓(2026-10-12,`暂缓-RS485.md`),主线不再等它**。
|
||||
2. **数据正确性最高风险 = N-21**(dhara `is_free` 只查页首 64 B / `copy` 不处理哨兵):低概率、后果重(数据丢失),且至今只能靠人工读代码发现——修复须配主机侧移植层测试(一并补报告"覆盖率"缺口)。
|
||||
3. **内存预算红线已机械化(2026-10-12,W1-0)**:`mem-budget` 钩子拦 RW_IRAM1/RW_CCM >92%,子系统配额 / CCM 台账 / 趋势基线 → `docs/内存管理/RAM预算台账.md`。实际优化只做了 **R-9(2026-10-15 落地,SRAM1 74.9% → 56.1%)**;**R-2 后半与 R-3 / R-4 / R-6 已按用户裁定 2026-10-15 整簇暂缓**(当前 CCM 只剩 **7,904 B**,重开条件见 `../问题记录/项目审查与优化建议报告.md` §5.3)——**门禁与台账继续常开,它们正是"何时该重开"的传感器**。
|
||||
4. **验证债仅剩窗口性事项**(D2/D5/D6/D7 + P1-10 收尾),不单独排期、逢窗口顺手做。
|
||||
2. **内存预算红线已机械化(2026-10-12,W1-0)**:`mem-budget` 钩子拦 RW_IRAM1/RW_CCM >92%,子系统配额 / CCM 台账 / 趋势基线 → `docs/内存管理/RAM预算台账.md`。实际优化只做了 **R-9(2026-10-15 落地,SRAM1 74.9% → 56.1%)**;**R-2 后半与 R-3 / R-4 / R-6 已按用户裁定 2026-10-15 整簇暂缓**(当前 CCM 只剩 **7,904 B**,重开条件见 `../问题记录/项目审查与优化建议报告.md` §5.3)——**门禁与台账继续常开,它们正是"何时该重开"的传感器**。
|
||||
3. **验证债仅剩窗口性事项**(D2/D5/D6/D7 + P1-10 收尾),不单独排期、逢窗口顺手做。
|
||||
|
||||
---
|
||||
|
||||
@@ -101,26 +100,25 @@
|
||||
| 波次 | 内容 | 为什么在这 |
|
||||
|---|---|---|
|
||||
| **W1 采集+保护收尾** | **里程碑制 + 并行道**:M1 台架闭环 ✅ 达成(B6 ✅→N-33 ✅)∥ M2 功能面(代码道 B7 ✅→{B8∥B9})→ M3 送检(B10,Fixed Date)——任务卡/判据/写范围见 `W1-采集保护收尾.md` | **用户决定(2026-10-12):先把 ADC 采集、保护等相关功能完成**。B4/B5 代码已落地但未台架闭环、功能面不完整(定值不落盘 / 无频率保护 / 无动作记录 / 栈两处 FAIL)——本波收口到"可用 + 可验收";真约束只有**台架**与**送检窗口**两个稀缺资源,其余任务按写范围切分并行 |
|
||||
| **W2 数据正确性** | **N-21** dhara `is_free`/`copy`(报告 N-12/13) | 低概率、后果重(数据丢失);修复须配主机侧移植层测试,一并补报告"覆盖率"缺口 |
|
||||
| **W3 预算与内存** | ⏸ **整簇暂缓**(用户裁定 2026-10-15):~~缩堆 56→32 KB → R-2 后半(29.5 KB→CCM)→ R-3 / R-4 / R-6 逐项评估~~。本簇已落地的只有 **R-9 ✅(2026-10-15)**;R-5 已判定不可行 | **不再排期**:没有功能在等这块预算,而搬迁要动 scatter、要逐条过 DMA 可达性清单、缩堆还要上板复测。**W3 槽位保留、不重编号**(避免全库改 W4~W7 引用)。重开条件 → `../问题记录/项目审查与优化建议报告.md` §5.3 |
|
||||
| **W4 结构债** | **C6(S-3)→ C3(N-2+N-24)→ C1 net 层跨上下文收口 → C5(含 N-23 残余)→ N-22/N-25/N-28**(详表 `排期详述.md` §1) | 无紧急,按性价比排:C6 灭 IWDG 误复位机制根源;C1 消化 E-25/E-27/E-28(原 ACCEPT+SEND pending 化已收口),体量大、验证重,放后(N-23 钩子已落地 2026-10-12,残余并入 C5) |
|
||||
| **W4 结构债** | **C6(S-3)→ C3(N-2)→ C1 net 层跨上下文收口 → C5(含 N-23 残余)→ N-22/N-25/N-28**(详表 `排期详述.md` §1) | 无紧急,按性价比排:C6 灭 IWDG 误复位机制根源;C1 消化 E-25/E-27/E-28(原 ACCEPT+SEND pending 化已收口),体量大、验证重,放后(N-23 钩子已落地 2026-10-12,残余并入 C5)。⚠ 原挂 C3 的 **N-24(日志逐条开关)已于 2026-10-15 单独修复销项**,C3 只剩 N-2 观察项 |
|
||||
| **W5 验证常态** | P1-10 收尾 · T-7 CI · D2 N-3 · D5 N-16 · D6 N-17 · D7 串口助手 · S12 · S1-c · A-5(详表 `排期详述.md` §2) | 窗口性,逢机顺带,不单独排期 |
|
||||
| **W6 UI 扩展** | U4 残余(golden + 字符集差集挂门禁)→ U1 令牌层 → U2/U3 → **27 页**落地 | 产品驱动、体量大,需单独立项(顺序按 `docs/问题记录/项目审查与优化建议报告.md` §6.5);**本轮不排期(2026-10-11 已确认)** |
|
||||
| **W7 仓库决策** | **N-29** `.git` **235 MB** + 图片 36.3 MB | **已决策(2026-10-11):暂不处理、登记观察**——推送/协作前重评(`git filter-repo` 历史重写影响未推送提交与协作者) |
|
||||
| **暂缓** | **B2 RS-485 整簇**(`暂缓-RS485.md`) | **用户决定(2026-10-12):当前不处理 RS-485** |
|
||||
| **硬件改版** | N-18 8 路电气量全接 TPAFE(V0.2) | 下一版硬件;固件侧已用语义映射隔离,改版只动映射表 |
|
||||
|
||||
> ✅ **W2 数据正确性已清空销项(2026-10-15)**:登记簿 N-21(dhara `dhara_nand_is_free()` 只查页首 64 B / HAL 越界无校验)已修复并配主机侧移植层单测 `test/host/nand_port/` → 实施记录 `../架构设计/STM32F4-Base存储架构说明.md` §5.3、验证链 `../问题记录/项目审查与优化建议报告.md` 变更记录。**W2 槽位与 W3 同理保留、不复用也不重编号 W3~W7**(避免全库改引用)。
|
||||
|
||||
### 2.1 顺序一图
|
||||
|
||||
```
|
||||
W1 收尾 台架道: B6✅→N-33✅(M1 ✅ 2026-10-12) ∥ 代码道: B7✅→B8✅→B9✅(M2 ✅ 2026-10-12) → B10 送检(M3, Fixed Date) 详表 W1-采集保护收尾.md
|
||||
(B4 IEC104 ✅ / B5 protectTask ✅ / B6 台架复测 ✅ / B7 定值持久化+软压板 ✅ / N-33 栈收口 ✅ / B8 频率保护 ✅ / B9 SOE 动作记录 ✅ 2026-10-12 均已销项;台账三件套 ✅)
|
||||
↓
|
||||
W2 数据 N-21 dhara is_free/copy(配主机侧移植层测试)
|
||||
↓
|
||||
W3 预算 ⏸ 整簇暂缓(2026-10-15 用户裁定);R-9 池瘦身 ✅ 已落地不回退
|
||||
↓
|
||||
W4 结构 C6(S-3) → C3(N-2/N-24) → C1 net 层跨上下文收口 → C5(含 N-23 残余) → N-22/25/28
|
||||
W4 结构 C6(S-3) → C3(N-2) → C1 net 层跨上下文收口 → C5(含 N-23 残余) → N-22/25/28
|
||||
↓
|
||||
W5 验证 P1-10 · T-7 CI · D2/D5/D6/D7 · S12 · S1-c · A-5
|
||||
↓
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
| 序 | 任务 | 说明 | 出处 |
|
||||
|---|---|---|---|
|
||||
| **C6** | **USART1 TX-only 结构根治(S-3)** | 灭 ORE 僵尸机制根源(2026-10-02 台架 IWDG 误复位的机制根源):改 `.ioc` 令 USART1 不使能 RX / PA10 不复用为 `USART1_RX` → CubeMX 重生成 → 跑 `check_cubemx_survival.py`。结构性改动,需单开一轮 + 复跑台架闭环 | S-3 |
|
||||
| **C3** | **存储侧日志管线(N-2 + N-24)** | 丢弃可观测性**代码已具备**(`console_log.c` `s_con_drop`+`[ERR] CONSOLE dropped`、`sys_log_mcu.c` 队列满 `DBG_ERROR`、TX 僵尸通道自愈);V5 30 min 日志**未复现 49s 静默**(运行期 >10s 静默全为 ~28s、均在 `e02_probe` 窗口内=N-17)。剩余:与 S8(非阻塞日志)合并评估 + 日志逐条 `f_open/f_write/f_close` 改造评估(N-24) | N-2 / N-24 |
|
||||
| **C3** | **存储侧日志管线(N-2)** | 丢弃可观测性**代码已具备**(`console_log.c` `s_con_drop`+`[ERR] CONSOLE dropped`、`sys_log_mcu.c` 队列满 `DBG_ERROR`、TX 僵尸通道自愈);V5 30 min 日志**未复现 49s 静默**(运行期 >10s 静默全为 ~28s、均在 `e02_probe` 窗口内=N-17)。剩余:与 S8(非阻塞日志)合并评估。⚠ 原挂本卡的 **N-24**(日志逐条 `f_open/f_write/f_close`)已于 2026-10-15 修复销项(写侧一次排空合并成每文件一次开关、读侧 8 条滑动窗口)→ 实施记录 `../子系统设计/运行与自检记录设计说明.md` §4.4 / §4.5 | N-2 |
|
||||
| **C1** | **net 层跨上下文收口** | 消化 E-25(`net_select` 语义)、E-27/E-28(裸指针收口为状态查询 API)。原 ACCEPT + SEND pending 化已收口:SEND 2026-10-14 落地、ACCEPT 决定不实施(2026-10-15) | E-25 / E-27 / E-28 |
|
||||
| **C5** | 一档剩余 | **N-23 残余**(`KNOWN_GAPS` 白名单清理 + `app_cfg.h` 未接线开关,见 `登记簿.md`)。**RS-485 DE 收尾已移入 `暂缓-RS485.md`** | 一档/二档 |
|
||||
| **N-22/25/28** | 评估簇 | 写回 flush / 性能与语义一致性 / Flash 死代码——逐项见 `登记簿.md` 对应行,随窗口评估 | 报告各条 |
|
||||
|
||||
@@ -41,10 +41,8 @@
|
||||
|
||||
| # | 事项 | 影响 / 现状 | 处置方向 | 波次 | 出处 |
|
||||
|---|---|---|---|---|---|
|
||||
| N-21 | **dhara 两处**:`dhara_nand_is_free()` 只查页首 64 B;`dhara_nand_copy()` 不处理 `DHARA_NO_PAGE` 哨兵 | 映射恢复误判 → **数据丢失风险**(低概率、后果重);哨兵页号被当真实页 → 未定义行为;至今只能靠人工读代码发现 | 整页(或首/中/尾)扫描 + 返回 `DHARA_E_UNSUPPORTED`;**必须配主机侧移植层单测**(一并补报告"覆盖率"缺口) | **W2** | 报告 N-12/N-13 + §7 |
|
||||
| N-22 | 写回缓存无空闲/掉电强制 flush(整页写与换页已 flush) | 写回窗口内掉电丢最近日志/配置写 | defaultTask 空闲 N ms flush | W4 | 报告 N-14 |
|
||||
| N-23 残余 | **钩子已落地**(2026-10-12 `registration-check` 挂 pre-commit,存量缺口走 `KNOWN_GAPS` 白名单只 WARN);**残余** = 白名单只剩 `rs485_task.c` 一条**刻意**缺口(E-59 门禁下随 B2 暂缓,见 `暂缓-RS485.md`);`app_cfg.h` 未接线开关(C5)。⚠ `lftpd_log.c` 已随 N-19 删除并入 `lftpd_auth.c`(2026-10-11),不再是缺口 | 白名单不清 = "写了不编译"盲区仍在 | 随 B2 一并出白名单(`--strict` 供审计复跑);开关随 C5 处置 | W4(C5) | 报告 N-06 残余 + 报告 S-3 |
|
||||
| N-24 | 日志每条 `f_open+f_write+f_close`(`sys_log_mcu.c:123-140`) | 每条日志 2–4 次 NAND 页读写;UI 翻记录逐条 open/seek/close | 与 C3 日志管线合并评估(现仅 `FIL` static 化省 528 B 栈) | W4 | 报告 N-16(**≠ 本登记簿 N-16**) |
|
||||
| N-25 | 性能/资源建议簇:PASV 每次新建监听;PHY 强制 100M 无自协商回退;`NET_MAX_SOCKETS 8` 与单连接语义不符;`HAL_Delay(1)` 阻塞 DE(**DE 项随 B2 暂缓,见 `暂缓-RS485.md`**)。⚠ 原列的 `NET_RECV_TIMEOUT_MS 30000` 已随 2026-10-13「recv 语义主流化」删除(挂起 recv 模型),不再是待办 | 次级性能/语义一致性 | 逐项见 `../问题记录/项目审查与优化建议报告.md` §4.2/§4.7,随窗口评估 | W4 | 报告 N-09/PHY/P3-x/rs485 |
|
||||
| N-27 | **内存优化簇**:**R-9 ✅ 已落地(2026-10-15)**——`LV_MEM_SIZE` 40→16 KB → SRAM1 由 74.9% 降到 **56.1%(余 57,504 B)**;待迁 CCM 体量随之由 54,146 B 降到 **29,570 B**,而 CCM 只剩 **7,904 B**(`RW_CCM` 上限 65,280 − 57,376) | ⏸ **整簇暂缓(用户裁定 2026-10-15)**:R-2 后半(缩堆 56→32 KB → 迁 CCM)与 R-3 / R-4 / R-6 **不再排期**——没有功能在等这块预算,而搬迁要动 scatter、要逐条过 DMA 可达性清单、缩堆还要上板复测,收益不抵风险与工时。⚠ 代价落在"以后加任务 / 抬栈 / 加堆"那一刻:CCM 只剩 7,904 B、FreeRTOS 堆实测 `min_ever` 8,464 B,届时 `mem-budget` 会先报警 → 按报告 §5.3 重开条件评估 | **三件套 ✅ 2026-10-12 落地**(`ram_baseline.py` 四模式 + `ram_baseline.tsv` 趋势 + `docs/内存管理/RAM预算台账.md` + `mem-budget` 钩子 92% 门禁)→ **R-9 ✅ 2026-10-15**(实测峰值 6,568 B = 池的 40%,水位表见 `../内存管理/RAM使用分析与优化方案.md` §2.6.2)→ **⏸ 其余整簇暂缓**(2026-10-15 裁定;理由与重开条件 → `../问题记录/项目审查与优化建议报告.md` §5.3)。⚠ **R-5 已判定不可行、从本簇剔除**:CMSIS-RTOS2 `freertos_os2.h` 有 `#if (configMAX_PRIORITIES != 56) #error`,改小直接编译失败(为 480 B 改第三方包装层不划算) | **暂缓** | 报告 R-2 剩余/R-3/R-4/R-6/§5.5 |
|
||||
| N-28 | Flash 死代码评估簇:剩余 ≈10 KB(`box_shadow` 3,246 / `draw_sw_img` 2,960 / `bin_decoder` 1,972 / `image_decoder` 952 / `triangle` 930)+ 双精度 `dadd` 等 ≈1 KB;uvprojx 仍挂 201 个空 LVGL TU(F-1/F-2/F-3 已闭环,省的是编译时长) | 编译时长 + 文档噪音 | 逐项实测再关;⚠ `mask`/`mask_rect`/`grad` 是文字与矩形绘制必经路径,**不可盲关** | W4 | 报告 F-4/F-5/F-6 |
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| ER_IROM1 | 231,084 B (225.67 KB) | 1,048,576 B (1024 KB) | 22.0% |
|
||||
| RW_IRAM1 | 73,568 B (71.84 KB) | 131,072 B (128 KB) | 56.1% |
|
||||
| ER_IROM1 | 232,280 B (226.84 KB) | 1,048,576 B (1024 KB) | 22.2% |
|
||||
| RW_IRAM1 | 75,232 B (73.47 KB) | 131,072 B (128 KB) | 57.4% |
|
||||
| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
|
||||
| **Flash (ROM 合计)** | **232,196 B (226.8 KB)** | 1,048,576 B (1024 KB) | **22.1%** |
|
||||
| **Flash (ROM 合计)** | **233,416 B (227.9 KB)** | 1,048,576 B (1024 KB) | **22.3%** |
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
各区的容量与常量来源:
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|
||||
|---|---|---|---|---|
|
||||
| ER_IROM1 (Flash) | 231,084 B | 1,048,576 B | 22.0% | 参考 |
|
||||
| ER_IROM1 (Flash) | 232,280 B | 1,048,576 B | 22.2% | 参考 |
|
||||
| RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS |
|
||||
| UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 |
|
||||
| RW_IRAM1 | 73,568 B | 131,072 B | 56.1% | PASS |
|
||||
| RW_IRAM1 | 75,232 B | 131,072 B | 57.4% | PASS |
|
||||
|
||||
| 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 |
|
||||
|---|---|---|---|
|
||||
@@ -26,7 +26,7 @@
|
||||
| 存储 | 7,056 B | 12 KB | OK |
|
||||
| UI | 30,412 B | 16 KB | ⚠ 超 13.7 KB |
|
||||
| RTOS 堆 | 57,376 B | 56 KB | 堆=配额 |
|
||||
| 应用 | 13,247 B | 8 KB | ⚠ 超 4.9 KB |
|
||||
| 应用 | 14,911 B | 8 KB | ⚠ 超 6.6 KB |
|
||||
| 其它 | 9,184 B | — | 参考 |
|
||||
<!-- MEM_BUDGET:END -->
|
||||
|
||||
|
||||
@@ -123,12 +123,18 @@
|
||||
|
||||
defaultTask(唯一写者,挂载成功后):
|
||||
1) f_mkdir("/log")(幂等)
|
||||
2) 排空启动前数组 + 队列 → 逐条落盘(f_open FA_OPEN_APPEND → f_write(64B) → f_close)
|
||||
2) 排空启动前数组 + 队列 → 按文件合并落盘(一批最多 8 条:一次
|
||||
f_open FA_OPEN_APPEND → f_write(n×64B) → f_close,见下方 N-24 说明)
|
||||
3) 写"上电/重启"分类记录(§5.3,基于 run.log 末条 epoch)
|
||||
4) 主循环:osMessageQueueGet(阻塞, 10s 超时) → 落盘;超时空转继续 LED 闪烁
|
||||
```
|
||||
|
||||
- 读路径与写路径并发安全:UI 页在 uiTask 直接 `f_open(FA_READ)` 按偏移读记录(只读既有内容,写者只在 EOF 追加);FatFS 系统锁保护单次调用。
|
||||
- **N-24(2026-10-15,报告 N-16 写侧):一次排空合并成"每文件一次 open/write/close"**。旧实现每条记录一轮,而 `f_close` 要回写目录项、`f_write` 要对目标扇区做读-改-写 → **每条日志 2~4 次 NAND 页读写**。现在同一文件的一批(≤8 条 = 512 B 静态缓冲 `s_batch`)合并成一次追加写:
|
||||
- **掉电语义不变**:批次仍在**同一次 `syslog_drain()` 调用内**写完并 `f_close`;close 回写目录项(另一个 LPN)会把 FTL 的单页写回缓存逼出到 NAND——该"写到不同 LPN 才刷前一脏页"的语义见 `sys_log_evidence()` 的 flush 诱导注释。落盘时机与旧实现每条 close 等价,**批次不跨调用驻留 RAM**。
|
||||
- **条目不丢**:启动前数组按下标"看而不删",整批成功才删;队列条目在落盘失败(-2 卷忙 / -1 写失败 / -3 容量不足)时**回投队列**——旧实现 -2 会直接丢掉那一条。
|
||||
- **容量轮转仍只在按条慢路径做**:批量路径发现剩余容量装不下一批时返回 -3,退回逐条 `s_append()`("满了 unlink 重建"只保留一处实现,避免一批跨多轮重建)。
|
||||
- 自愈重试(L1 丢弃重建 + 重试一次)与错误去重(L2 只报第一次)判据与单条路径一致。
|
||||
- 读路径与写路径并发安全:UI 页在 uiTask 经 `syslog_get_record()` 读记录(内部走 `s_read_raw()`,见 §4.5);读写两侧都取卷锁 try 语义(P2-10),FatFS 系统锁保护单次调用。
|
||||
- **存储未就绪/挂载失败**:待发条目滞留 RAM(下次上电丢失),串口打印告警——存储故障时本就无法持久化,属预期行为。
|
||||
|
||||
### 4.5 读路径(UI / 模拟器)
|
||||
@@ -139,7 +145,9 @@ int syslog_get_record(syslog_id_t id, uint32_t idx, char *ts, size_t ts_len
|
||||
char *desc, size_t desc_len); /* 越界返回 -1 */
|
||||
```
|
||||
|
||||
- MCU:`f_open(FA_READ) → f_lseek(idx*64) → f_read(64B) → f_close`,内联拆分函数切 ts/desc。
|
||||
- MCU:**8 条滑动读窗口**(N-24,报告 N-16 读侧)——`s_read_raw()` 先查窗口(同 id + 代数未变 + 落在区间内 → **零 I/O** 直接从 512 B 静态窗口返回);未命中才 `f_open(FA_READ) → f_lseek(窗口首条×64) → f_read(≤8×64B) → f_close` **一次取回连续 8 条**,再从中切出请求的那条。旧实现每读一条都要 open/lseek/read/close 一轮,UI 翻 20 条记录 = 20 次目录项/FAT 往返。
|
||||
- 失效机制:写者每次落盘后把 `s_rd_gen[id]` 自增,读者比对填充时记录的代数,不匹配就重填。对齐 32 位在 ARM 上原子,跨任务(uiTask 填、defaultTask 增)无需临界区;最坏情况多读一轮,不会读错。
|
||||
- **API 与调用方一律不变**(`syslog_get_record()` 签名照旧,`ui_page_runlog` / `ui_page_selftest` 零改动)。
|
||||
- 模拟器:宿主文件同语义(UI 页双端零差异)。
|
||||
- UI 页每帧仅读当前窗口 ≤8 条记录(每次重绘 8×64B SPI 读,用户翻页触发,频率极低)。
|
||||
|
||||
|
||||
@@ -331,11 +331,16 @@ dhara 通过 7 个回调函数访问物理 NAND:
|
||||
| `dhara_nand_erase` | 擦除块 | `nand_block_erase(block + 1024)` |
|
||||
| `dhara_nand_prog` | 写页 | `nand_program_load + nand_program_exec` |
|
||||
| `dhara_nand_read` | 读页 | `nand_page_read_to_cache + nand_read_from_cache` |
|
||||
| `dhara_nand_is_free` | 检查页空闲 | 读前 64 字节判断全为 0xFF |
|
||||
| `dhara_nand_is_free` | 检查页空闲 | **整页扫描 2048 B 判全 0xFF**(栈上 64 B 缓冲分块读;页首命中即早退,见下方 ⚠) |
|
||||
| `dhara_nand_copy` | 页拷贝 (GC 用) | read + prog 组合 |
|
||||
|
||||
所有回调自动将 dhara 逻辑块/页加 `FTL_START_BLOCK` 偏移转换为物理地址。
|
||||
|
||||
⚠ **两条移植层硬约束(2026-10-15,登记簿 N-21 / 报告 N-12、N-13)**:
|
||||
|
||||
1. **`is_free` 必须整页精确**。dhara 的 journal 用它判断"检查点组是否未编程"(`journal.c:416 cp_free`、`journal.c:520` 找下一空闲页),其正确性前提是"答空闲的页必须真的全 0xFF"(`journal.c:405-414` 的注释论证)。旧实现只看页首 64 B → 一个"页首 0xFF、后段有数据"的已编程页会被误判空闲,dhara 遂**往已编程页再编程一次** = 位碰撞、journal/map 指向垃圾(数据丢失,且不报任何错)。本卷跑的是 FatFS 数据页,这种页型很常见(刚格式化的卷、稀疏文件块、日志尾部)。成本上做了快路径:页首 64 B 有数据就当场返回非空,与旧实现同价,只有"目前读到的全是 0xFF"的页才继续扫后段。
|
||||
2. **六个 HAL 入口一律做几何上界校验**(上界 = `n->num_blocks << n->log2_ppb`)。越界页/块号(含 dhara 哨兵 `DHARA_PAGE_NONE` = 0xffffffff)旧实现直接加偏移送进 SPI 命令,会被芯片地址位**截断成某个真实页** → 静默读写/搬走无辜数据。现越界时 prog/read/copy/erase 返回 -1 且 `*err == DHARA_E_CORRUPT_MAP`,`is_free` 返回 0(保守判非空)、`is_bad` 返回 1(保守判坏)、`mark_bad` 直接返回(绝不标无辜好块)。**错误码选择是刻意的**:不能用 `DHARA_E_BAD_BLOCK`(`journal.c:904 recover_from` 会据此标坏块并跳块),也不能用 `DHARA_E_RECOVER`(`map.c:275 try_recover` 会据此启动回收);其余错误一律原样上抛,不会静默清 map(E-31 教训)。
|
||||
|
||||
### 5.4 初始化流程 (disk_initialize)
|
||||
|
||||
```
|
||||
|
||||
@@ -273,6 +273,7 @@ static void phaseN_run(void) {
|
||||
| **`test/host/watchdog/run_unit_test.py`** | `test/host/watchdog/` | **L0.5 主机侧单测**:声明 `TestBuild` 后调 harness,穷举看门狗判定状态机(零硬件、秒级)。**需本机装有 VS(`cl.exe`)**,见 §4.2 环境要求 |
|
||||
| **`test/host/sys_clock/run_unit_test.py`** | `test/host/sys_clock/` | **L0.5 主机侧单测**:穷举 `sys_clock` 时间换算 / 星期 / 惰性再基准化 / RTC 故障降级(100 断言,S1-a) |
|
||||
| **`test/host/cfg_file/run_unit_test.py`** | `test/host/cfg_file/` | **L0.5 主机侧单测**:INI 解析 / 值编解码 / 段注册 / load-save-poll 契约,内存虚拟 FS 驱动(139 断言,S1-b) |
|
||||
| **`test/host/nand_port/run_unit_test.py`** | `test/host/nand_port/` | **L0.5 主机侧单测**:离线编译真实 `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` + 真实 dhara,以 **RAM 假芯片**驱动,149 断言钉死 is_free 整页扫描(N-12)与六个入口的越界守卫(N-13) |
|
||||
| **`scripts/ci_local.py`** | `scripts/` | **本地跑与 CI 等价的门禁**(L0 + L0.5):自动选用装了 pre-commit 的解释器;`.map` 自适应跳过 `mem-numbers`;退出码 0/1/2 与 CI 对齐。见 §9.3 |
|
||||
| `.serialtest/config.json` | 仓库 | **串口端口单一配置源**(当前 COM6 / 115200);换端口只改这个文件 |
|
||||
| `test/host/ftp_*.py`(4 个)/ `ch395f_socket_test.py` / `time_sync_server.py` | `test/host/` | PC 端交互测试工具(FTP 客户端 / socket 回显 / 对时推送) |
|
||||
@@ -329,9 +330,10 @@ static void phaseN_run(void) {
|
||||
| `App/util/time_sync.c` | — | ⬜ 候选 | 协议帧编解码 | 报文组装/解析纯逻辑;网络收发不测 |
|
||||
| `Drivers/BSP/NETCFG/net_cfg_legacy.c` | — | ⛔ 已删除(2026-10-10) | — | 旧档 NCFG 迁移整体移除(旧档 `/cfg/netcfg.bin` 不读、不删、不迁移),测试候选随之撤销 |
|
||||
| `App/protocol/iec104/*`(104 协议栈) | — | ✅ 已覆盖(82 断言,2026-10-12 B4) | 数据源 vtable + tick 注入 + 发送回调注入 | APCI 重组/总召分片/COV/k-w-t1~t3/两段式全离线穷举;`iec104-unit-test` 钩子 |
|
||||
| `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c`(dhara 移植层) | 573 行 | ✅ 已覆盖(149 断言,2026-10-15 N-21) | 11 个 SPI 原语(`gd5f2gq5ue.h`)→ **RAM 假芯片** | 移植层是纯地址算术 + 边界判定,换掉芯片即可离线跑;钉死 N-12(is_free 整页扫描)与 N-13(六入口越界守卫),并内联旧算法做对照证明断言有牙 |
|
||||
| `App/util/rs485.c` | — | ⏸ 待功能实现 | — | 上游 B2;当前为空桩 |
|
||||
| `Drivers/BSP/TPAFE5160/*.c` | — | ❌ 不适格 | — | 寄存器序列为主,需真机时序 |
|
||||
| 其余驱动(CH395F/GD5F2GQ5/SD2506) | — | ❌ 不适格 | — | 需硬件时序 / SPI / I2C 总线 |
|
||||
| 其余驱动(CH395F/GD5F2GQ5UE 芯片层/SD2506) | — | ❌ 不适格 | — | 需硬件时序 / SPI / I2C 总线。⚠ **例外**:GD5F 之上的 `nand_ftl.c` 移植层**适格**(见上一行)——"驱动不适格"不等于"驱动之上的胶水层不适格" |
|
||||
| `App/util/crc.c` · `ringbuf.c` | 120/154 行 | ⛔ 已删除(2026-10-02 S8) | — | 孤儿文件,零引用,已 `git rm` |
|
||||
|
||||
**使用方式**:新增/改动上述模块前,按表判断是否需补 L0.5;`⬜ 候选` 表示"适格但未做",
|
||||
@@ -623,6 +625,7 @@ CI 就静默停摆(2026-10-11 实测 run 16~21 共 12 个 job 零日志)。
|
||||
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 2026-10-15 | **新增 `test/host/nand_port/`(dhara NAND 移植层 L0.5,N-21 / 报告 N-12+N-13)**:`#include` 真实 `nand_ftl.c` + 真实 `Lib/dhara/{journal,map}.c`,芯片侧换成 **RAM 假芯片**(32 块 × 64 页 × 2048 B,两段式读缓存 + NOR 叠加编程 + 双编程违规计数 + 访问次数记账),149 断言覆盖 is_free 整页扫描、六入口越界守卫(`-1` + `DHARA_E_CORRUPT_MAP` + 零芯片访问 / 零标坏块)、故障注入与 dhara map"写入 → 重新 resume"场景;**内联一份"只看页首 64 B"的旧算法做对照**,证明断言能咬人。挂 pre-commit 钩子 `nand-port-unit-test`(第 16 个主机侧单测,钩子总数 33→34)。§4.1 登记运行器、§4.2.1 台账新增该模块并给"其余驱动不适格"补例外说明(驱动不适格 ≠ 驱动之上的胶水层不适格) |
|
||||
| 2026-10-12 | **§9 瘦身:机器侧内容迁出**(远程 CI 文档独立成 `docs/CI/`)。§9.2 两个 job 的**步骤级细节**并入 `docs/CI/远程CI完整指南.md` §5.5、§9.4 部署前置表并入其 §0.2;本文 §9 只保留**测试分层视角**的内容(CI 覆盖哪些层、两个 job 各守哪层、触发与 paths-ignore 语义、本地等价 `ci_local.py`、"CI 不替代本地门禁")。§9.1 两个 🔴 块压缩为结论 + 指针(机制根因与排障口径归 `docs/CI/`)。相关文档表两行 CI 条目合并为一行指向 `docs/CI/README.md` |
|
||||
| 2026-10-10 | §4.2.1 台账:`Drivers/BSP/NETCFG/net_cfg_legacy.c` 由 `⬜ 候选` 改为 **`⛔ 已删除`**——旧档 NCFG 迁移整体移除(`net_cfg_legacy.{c,h}` 删除、`cfg_store_legacy_path()` 撤销;旧档 `/cfg/netcfg.bin` 不读、不删、不迁移,配置回落工厂默认),该测试候选随之撤销 |
|
||||
| 2026-10-02 | **S6**:`test/run_all_suites.py` 新增 **`--check-build`**(只编译不烧录,纯离机,不需要 MCU/串口/ST-LINK);新增 **§3.6.1** 说明其解决的问题(测试构建无独立门禁,2026-08~09 CH395F 套件实例)、判定语义与用法。**实测 4 套件全部 `BUILD_OK: 0 errors, 0 warnings`**(即 T-4 提到的"CH395F 不可编译"已修复) |
|
||||
|
||||
@@ -397,7 +397,7 @@ docs\内存管理\RAM使用分析与优化方案.md:585 [路径引用] docs/内
|
||||
|
||||
| 主题 | 去处 |
|
||||
|---|---|
|
||||
| 提交门禁 30 个钩子 | `docs/代码规范/pre-commit使用说明.md` |
|
||||
| 提交门禁 34 个钩子 | `docs/代码规范/pre-commit使用说明.md` |
|
||||
| 自动化测试与台架流程 | `docs/测试说明/自动化测试流程.md` |
|
||||
| 文档写作规范(编码/链接/登记) | `docs/文档规范.md` |
|
||||
| 工具路径环境变量(换机) | `docs/环境与部署/本地工具路径环境变量方案.md` |
|
||||
|
||||
@@ -231,3 +231,70 @@ if (ok) {
|
||||
- 坏块表这类**持久化状态**,扫描失败时必须"放弃持久化"而非"照常保存"——
|
||||
保护既有好副本比刷新更重要;
|
||||
- 使能类配置(ECC)必须**回读校验**,不能只信写命令的返回。
|
||||
|
||||
## Trap 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 03 的关系(本条的核心教训)**:Trap 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 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 页/块的芯片,守卫会**静默算错**(偏大 = 越界守卫失效,正是它要防的那类)。
|
||||
→ 待办:加编译期断言 `GD5F_PAGES_PER_BLOCK == (1u << 6)`(本仓 `gd5f2gq5ue.h` 已有
|
||||
"负数组 typedef"式契约断言先例,照抄即可)。
|
||||
|
||||
+30
-30
@@ -25,27 +25,29 @@
|
||||
|------|------|-----------|-----------------|
|
||||
| 项目结构 | **A−** | 六层分层真实可执行、命名一致、门禁到位;反向依赖控制在 4 处 | `NETCFG`/`UI` 语义层错位(S-1)、4 处裸 `extern`(S-2)、`rs485_task.c` 仍在注册白名单里等 B2 |
|
||||
| 文档分布 | **A−** | 工程文档 **87 篇 / 30,781 行**(2026-10-15 随 `错误码传播审计.md` 删除而 −1 篇 / −635 行),`docs/README.md` 索引 + `docs/文档规范.md` + 三档一致性门禁全部落地,`check_doc_links --all` = 0 失效 | **D-1**:`docs/` 占仓库 **71.8%**、手册图片 **36.3 MB**、`.git` 已 **235 MB**(已决策暂不处理,登记观察) |
|
||||
| 模块设计 | **A−** | 看门狗(IWDG + 心跳位图 + 4 个 Fault 现场 + `.noinit`)、就绪屏障、卷锁、FTP 鉴权、`Error_Handler` 自恢复、ADC 全链均已落地 | **N-12/N-13** dhara 移植两处缺陷(数据丢失风险)、**N-16** 日志逐条开关、驱动层无统一错误码约定(S-5)、RS485 无协议栈(随 B2 暂缓) |
|
||||
| 内存使用 | **B+** | 堆迁 CCM + 栈/堆三面观测常开 + `mem-budget` 92% 门禁 + 预算台账与趋势基线;**R-9 已落地(2026-10-15)→ SRAM1 由 74.9% 降到 56.1%(余 57,504 B),不再是卡点** | **内存优化簇已整簇暂缓**(用户裁定 2026-10-15):R-2 后半(待迁 29,570 B → CCM,只差缩堆 56→32 KB)与 R-3 / R-4 / R-6 不再排期,理由与重开条件见 §5.3;**当前真正的约束是 CCM 只剩 7,904 B**——加任务 / 抬栈 / 加堆会先在这里撞墙,届时按 §5.3 重开;MicroLIB 512 B C 堆仍无机械检查(§5.5) |
|
||||
| Flash 使用 | **A−** | F-1/F-2/F-3 落地省 64,576 B → 占用 **22.0%(231,084 B)** | F-4/F-5/F-6 ≈10 KB + 编译时长(§5.4) |
|
||||
| 模块设计 | **A−** | 看门狗(IWDG + 心跳位图 + 4 个 Fault 现场 + `.noinit`)、就绪屏障、卷锁、FTP 鉴权、`Error_Handler` 自恢复、ADC 全链均已落地;**原列的 dhara 移植层两处缺陷(N-12/N-13)与日志逐条开关(N-16)已于 2026-10-15 修复** → `GD5F2GQ5UE_Trap_Records.md` Trap 07、`../架构设计/STM32F4-Base存储架构说明.md` §5.3、`../子系统设计/运行与自检记录设计说明.md` §4.4/§4.5 | 驱动层无统一错误码约定(S-5)、RS485 无协议栈(随 B2 暂缓) |
|
||||
| 内存使用 | **B+** | 堆迁 CCM + 栈/堆三面观测常开 + `mem-budget` 92% 门禁 + 预算台账与趋势基线;**R-9 已落地(2026-10-15)→ SRAM1 由 74.9% 降到 56.1%;同日移植层与日志修复 +1.6 KB → 现 **57.4%(余 55,840 B)**,不再是卡点** | **内存优化簇已整簇暂缓**(用户裁定 2026-10-15):R-2 后半(待迁 29,570 B → CCM,只差缩堆 56→32 KB)与 R-3 / R-4 / R-6 不再排期,理由与重开条件见 §5.3;**当前真正的约束是 CCM 只剩 7,904 B**——加任务 / 抬栈 / 加堆会先在这里撞墙,届时按 §5.3 重开;MicroLIB 512 B C 堆仍无机械检查(§5.5) |
|
||||
| Flash 使用 | **A−** | F-1/F-2/F-3 落地省 64,576 B → 占用 **22.2%(232,280 B)** | F-4/F-5/F-6 ≈10 KB + 编译时长(§5.4) |
|
||||
| UI 标准化 | **B** | golden 回归已建(`simulator/tests/` 四脚本,含池水位门禁) | 无令牌层 / 无组件库 / 无页注册表(手写 `case` 仍在)、**27 页**未落地、golden 与字符集差集未挂门禁(§6) |
|
||||
|
||||
### 0.2 未闭环问题(Top 2,2026-10-15)
|
||||
### 0.2 未闭环问题(2026-10-15 复核)
|
||||
|
||||
| # | 编号 | 问题 | 影响 | 现状 |
|
||||
|---|------|------|------|------|
|
||||
| 1 | **N-12 / N-13**(登记簿 **N-21**,W2) | `dhara_nand_is_free()` 只查页首 64 B(`nand_ftl.c:152-179`);`dhara_nand_copy()` 不处理 `DHARA_NO_PAGE` 哨兵(`:204-216`) | 映射恢复误判 → **数据丢失风险**(低概率、后果重);哨兵页号被当真实页 → 未定义行为 | ❌ 两处原样;至今只能靠人工读代码发现(主机侧无 dhara 移植层测试) |
|
||||
| 2 | **N-16**(登记簿 **N-24**,W4) | 日志每条记录一次 `f_open + f_write + f_close`(`sys_log_mcu.c:124-141`) | 每条日志 2–4 次 NAND 页读写;UI 翻记录逐条 open/seek/close | ❌ 未办(仅 `FIL` static 化省 528 B 栈) |
|
||||
> ✅ **原 Top 2 已全部修复并销项(2026-10-15)**:
|
||||
> ① **N-12 / N-13**(登记簿 N-21,W2)——`dhara_nand_is_free()` 改整页精确扫描、HAL 六个入口加几何上界守卫;
|
||||
> ② **N-16**(登记簿 N-24,W4)——日志写侧一次排空合并成"每文件一次 open/write/close"、读侧加 8 条滑动窗口。
|
||||
> 实施记录与硬约束 → `../架构设计/STM32F4-Base存储架构说明.md` §5.3(移植层两条硬约束)、`../子系统设计/运行与自检记录设计说明.md` §4.4 / §4.5(写批量化 + 读窗口);验证链见文末变更记录(含台架 journal 恢复点同轨对照)。
|
||||
|
||||
剩余未闭环项**均非紧急级**(W4 结构债 / 评估簇 / 已裁定暂缓的内存优化簇),逐条见 §0.4;推进顺序的唯一权威仍是 `../任务规划/README.md` §2。
|
||||
|
||||
⚠ 另有两条**不在上表但需知晓**:`.git` 已膨胀到 **235 MB**(D-1,§3.1,2026-10-11 决策"暂不处理、登记观察");`rs485_task.c` 仍不在两端构建清单里——**这是 E-59 门禁下的刻意缺口**(随 B2 暂缓),已登记在 `check_registration.py` 的 `KNOWN_GAPS`,不是漏注册。
|
||||
|
||||
⏸ **内存优化簇已整簇暂缓**(用户裁定 2026-10-15,原 Top 2 的 R-2 剩余已移出):R-2 后半(把 LVGL 池 + UI 两份缓冲 ≈29.6 KB 搬进 CCM)与 R-3 / R-4 / R-6 不再排期。**已落地的 R-9 与治理面保留常开**(`mem-budget` 92% 门禁、预算台账、趋势基线、栈/堆观测)。理由与重开条件 → §5.3;登记簿 N-27 已标暂缓。
|
||||
⏸ **内存优化簇已整簇暂缓**(用户裁定 2026-10-15,未闭环清单里的 R-2 剩余已移出):R-2 后半(把 LVGL 池 + UI 两份缓冲 ≈29.6 KB 搬进 CCM)与 R-3 / R-4 / R-6 不再排期。**已落地的 R-9 与治理面保留常开**(`mem-budget` 92% 门禁、预算台账、趋势基线、栈/堆观测)。理由与重开条件 → §5.3;登记簿 N-27 已标暂缓。
|
||||
|
||||
### 0.3 两条主线建议
|
||||
|
||||
> **推进顺序的唯一权威是 [`../任务规划/README.md`](../任务规划/README.md) §2**;本节只是体系评审视角的两条结构性建议,不另立顺序。
|
||||
|
||||
1. **内存这条线到此为止**(用户裁定 2026-10-15,**覆盖本报告的原建议**):R-9 已把 LVGL 池 40→16 KB、SRAM1 降到 **56.1%(余 57,504 B)**,够用了。原建议的"缩堆腾 CCM → 把 LVGL 池与 UI 帧缓冲(现 ≈29.6 KB)搬进 CCM 作为后续功能预算来源"**整簇暂缓,不再作为主线**——没有功能在等这块预算,而搬迁要动 scatter、要逐条过 DMA 可达性清单、缩堆还得上板复测,收益不抵风险与工时。**保留的是治理面**(92% 门禁 + 台账 + 趋势 + 栈/堆观测):它会在真正吃紧时自动报警,届时再按 §5.3 的重开条件评估,而不是现在预先腾预算。
|
||||
1. **内存这条线到此为止**(用户裁定 2026-10-15,**覆盖本报告的原建议**):R-9 已把 LVGL 池 40→16 KB、SRAM1 降到 **57.4%(余 55,840 B)**,够用了。原建议的"缩堆腾 CCM → 把 LVGL 池与 UI 帧缓冲(现 ≈29.6 KB)搬进 CCM 作为后续功能预算来源"**整簇暂缓,不再作为主线**——没有功能在等这块预算,而搬迁要动 scatter、要逐条过 DMA 可达性清单、缩堆还得上板复测,收益不抵风险与工时。**保留的是治理面**(92% 门禁 + 台账 + 趋势 + 栈/堆观测):它会在真正吃紧时自动报警,届时再按 §5.3 的重开条件评估,而不是现在预先腾预算。
|
||||
2. **把 UI 从"每页手写"变"声明 + 原型库"**:令牌层 → 原型库 → 生成器补全注册 → golden 入门禁,使新增一页的成本从 8 处手工改动降到 1 处声明(§6)。
|
||||
|
||||
---
|
||||
@@ -57,7 +59,6 @@
|
||||
|
||||
| 类别 | 编号 | 未闭环内容 | 下一步 |
|
||||
|------|------|-----------|--------|
|
||||
| **数据正确性** | **N-12 / N-13** ❌(N-21,W2) | `is_free` 只查页首 64 B;`copy` 不处理 `DHARA_NO_PAGE` | 整页(或首/中/尾)扫描 + 返回 `DHARA_E_UNSUPPORTED` + **必须配主机侧移植层单测** |
|
||||
| 内存 RAM | **R-2 剩余** ⏸ **暂缓**(N-27,原 W3) | 待迁体量已由 54,146 B 降到 **29,570 B**(R-9 落地后);CCM 只剩 7,904 B | **用户裁定 2026-10-15:暂不做**——没有功能在等这块预算;重开条件 → §5.3 |
|
||||
| 内存 RAM | R-3 / R-4 / R-6 ⏸ **暂缓**(N-27,原 W3) | 页缓冲未合并 / UI 帧 4 副本 / 栈仍动态 | **同上裁定,暂不做**。本簇已落地的只有 R-9(2026-10-15,SRAM1 74.9%→56.1%,见 §5.3);**R-5 已判定不可行**(§5.3) |
|
||||
| 内存 RAM | N-14 🟡(N-22,W4) | 写回缓存无空闲/掉电强制 flush(整页写与换页已 flush) | 空闲 N ms 由 defaultTask flush |
|
||||
@@ -108,8 +109,8 @@ Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap
|
||||
| 术语 | 定义 |
|
||||
|------|------|
|
||||
| 自研代码 | `App/`、`Drivers/BSP/`(不含 `Lib/`、`Middlewares/`、HAL/CMSIS)、`test/`;不含 CubeMX 生成的 `Src/`、`Inc/` |
|
||||
| Flash 占用 | `Code + RO-data` = `ER_IROM1` **231,084 B(22.0% of 1 MB)**;`ROM` 再加 `RW-data` 初始化表 = 232,196 B |
|
||||
| RAM 占用 | `RW-data + ZI-data`,**跨两个区域** = SRAM1 `RW_IRAM1` **73,568 / 131,072(56.1%)** + CCM `RW_CCM` **57,376 / 65,280(87.9%)** + `UNINIT_CCM` 48 B(`.noinit` 崩溃现场保留区)。⚠ 原报告"CCM 不在 scatter 内计 0"的口径已不成立 |
|
||||
| Flash 占用 | `Code + RO-data` = `ER_IROM1` **232,280 B(22.2% of 1 MB)**;`ROM` 再加 `RW-data` 初始化表 = 233,416 B |
|
||||
| RAM 占用 | `RW-data + ZI-data`,**跨两个区域** = SRAM1 `RW_IRAM1` **75,232 / 131,072(57.4%)** + CCM `RW_CCM` **57,376 / 65,280(87.9%)** + `UNINIT_CCM` 48 B(`.noinit` 崩溃现场保留区)。⚠ 原报告"CCM 不在 scatter 内计 0"的口径已不成立 |
|
||||
| "死代码" | 该对象在镜像中有码,但其功能被工程配置(如 `LV_COLOR_DEPTH 1`)或调用点扫描(零调用者)证明不可达 |
|
||||
|
||||
> **内存数字的权威源**:区域水位与子系统配额由 `../内存管理/RAM预算台账.md`(受管区,`ram_baseline.py --write` 生成)持有,趋势在 `scripts/ram_baseline.tsv`。本报告只在支撑结论处引用,不与之竞争事实源。
|
||||
@@ -272,8 +273,6 @@ Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap
|
||||
|
||||
| 编号 | 问题 | 证据 | 影响/建议 |
|
||||
|------|------|------|-----------|
|
||||
| **N-12** | `dhara_nand_is_free()` 只检查页首 64 B | `nand_ftl.c:152-179`(仍 `uint8_t buf[64]`) | dhara 依赖该判定做映射恢复;页首 64 B 全 FF 但后半页有数据时会误判为空闲 → 数据丢失风险(低概率但后果重)。建议整页扫描或至少首/中/尾三处采样 |
|
||||
| **N-13** | `dhara_nand_copy()` 未处理 `DHARA_NO_PAGE` 语义(dst 为哨兵值时仍执行"读+编程") | `nand_ftl.c:204-216`(read→prog 直传) | dhara map 在回收/擦除路径会用 `DHARA_NO_PAGE` 试探;当前实现会把它当作真实页号 → 未定义行为。建议:`if (dst == DHARA_NO_PAGE) { dhara_set_error(err, DHARA_E_UNSUPPORTED); return -1; }` |
|
||||
| N-14 | 写回缓存 `s_cache_buf/s_cache_dirty` 无掉电保护策略:`CTRL_SYNC` 已实现但**没有空闲时强制 flush** | `nand_ftl.c:418` | 掉电窗口 = 最后一次未跨页的写。已改善:整页写立即 flush、换页时 flush、`CTRL_SYNC` 走 flush + `dhara_map_sync`;仍无空闲定时 flush → 建议 defaultTask 空闲 N ms flush |
|
||||
| N-15 | 页缓冲重复:`nand_ftl` 3×2,048(`s_page_buf/s_cache_buf/s_copy_buf`) | 对象 ZI:nand_ftl 6,216 | 与 `s_copy_buf` 复用可省 2 KB(§5.3 R-3)。🟡 `gd5f` 的 `s_bbt_page_buf` 已由 2,048 B 收紧为 272 B |
|
||||
| — | `disk_ioctl GET_BLOCK_SIZE = 1` | `nand_ftl.c:442-444` | FatFs 的擦除块提示失效(FAT32 对齐优化拿不到信息);建议返回 64 页(=1 物理块) |
|
||||
@@ -296,7 +295,6 @@ Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap
|
||||
|
||||
| 编号 | 问题 | 证据 | 建议 |
|
||||
|------|------|------|------|
|
||||
| **N-16** | **每条记录一次 `f_open + f_write + f_close`**,读侧同样每条 `f_open/f_lseek/f_close` | `sys_log_mcu.c:124-141`(失败重建重试一次)、`wdev.log` 同模式 `:486-489` | 写侧:常驻 `FIL`(仅 defaultTask 持有)+ 批量 flush;读侧:打开一次 + 缓存目录偏移(UI 已有 `s_cache_valid/s_last_rec` 雏形,可升级为页缓存)。🟡 唯一改善 = `FIL` 已 static 化(省 528 B 栈) |
|
||||
| P2-10 | 容量轮转 `f_unlink` 与 UI 读并发 | | 轮转只在写者、读者持版本号;卷锁覆盖面评估 → 登记簿 S-5 |
|
||||
|
||||
### 4.6 UI 子系统
|
||||
@@ -316,7 +314,7 @@ Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap
|
||||
|
||||
### 4.8 测试体系
|
||||
|
||||
**设计良好**:`test/board/test_config.h` 的 `TEST_SUITE_*` 互斥(`#error` 保护)、CH395F 硬件层/NET 层 Phase 互斥、`@test` 一键闭环(烧录→wait-idle→标记校验)、模拟器 `--cfgtest/--logtest`、主机侧单测 15 个钩子(含 `sha256` / `ftp_auth` / `vol_lock` / 采集算法族)。
|
||||
**设计良好**:`test/board/test_config.h` 的 `TEST_SUITE_*` 互斥(`#error` 保护)、CH395F 硬件层/NET 层 Phase 互斥、`@test` 一键闭环(烧录→wait-idle→标记校验)、模拟器 `--cfgtest/--logtest`、主机侧单测 16 个钩子(含 `sha256` / `ftp_auth` / `vol_lock` / 采集算法族 / **`nand_port` dhara 移植层**)。
|
||||
|
||||
> ⚠ 仍缺:**覆盖率概念**、**主机侧对 `dhara` 移植层的测试**(N-12/N-13 这类缺陷至今只能靠人工读代码发现)。
|
||||
|
||||
@@ -334,13 +332,13 @@ Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap
|
||||
|
||||
| 指标 | 数值 | 上限 | 占用 |
|
||||
|------|------|------|------|
|
||||
| `Code + RO-data`(`ER_IROM1`) | 231,084 B | 1 MB Flash | **22.0%** |
|
||||
| `ROM`(`Code + RO + RW` 初始化表) | 232,196 B | 1 MB | 22.1% |
|
||||
| **SRAM1(0x20000000,128 KB)** `RW_IRAM1` | **73,568 B** | 131,072 B | **56.1%(余 57,504 B)** |
|
||||
| `Code + RO-data`(`ER_IROM1`) | 232,280 B | 1 MB Flash | **22.2%** |
|
||||
| `ROM`(`Code + RO + RW` 初始化表) | 233,416 B | 1 MB | 22.3% |
|
||||
| **SRAM1(0x20000000,128 KB)** `RW_IRAM1` | **75,232 B** | 131,072 B | **57.4%(余 55,840 B)** |
|
||||
| **CCM(0x10000000,64 KB)** `RW_CCM` | **57,376 B** | 65,280 B | **87.9%(余 7,904 B)** |
|
||||
| `UNINIT_CCM`(`.noinit` 崩溃现场) | 48 B | 256 B | 18.8% |
|
||||
|
||||
> **R-9 已落地(2026-10-15)**:`LV_MEM_SIZE` 40→16 KB → SRAM1 由 98,144 B / **74.9%** 降到 **73,568 B / 56.1%**(−24,576 B,与池缩减量逐字节吻合)。SRAM1 首次从"最紧的一块"变成**余 57.5 KB**;**当前瓶颈换到 CCM(87.9%,只剩 7,904 B)**。⏸ 该簇后续动作(缩堆 → 迁 CCM)已按用户裁定 2026-10-15 **整簇暂缓**,见 §5.3。
|
||||
> **R-9 已落地(2026-10-15)**:`LV_MEM_SIZE` 40→16 KB → SRAM1 由 98,144 B / **74.9%** 降到 **73,568 B / 56.1%**(−24,576 B,与池缩减量逐字节吻合)。SRAM1 首次从"最紧的一块"变成**余 57.5 KB**(现 75,232 B / 57.4%、余 55,840 B,增量来自同日移植层守卫与日志缓冲 +1,664 B);**当前瓶颈换到 CCM(87.9%,只剩 7,904 B)**。⏸ 该簇后续动作(缩堆 → 迁 CCM)已按用户裁定 2026-10-15 **整簇暂缓**,见 §5.3。
|
||||
|
||||
> **权威源**:区域水位与子系统配额 → `../内存管理/RAM预算台账.md`(受管区);趋势 → `scripts/ram_baseline.tsv`(`ram_baseline.py --trend` 高亮 RW+ZI 增长 >1 KB 的对象)。本节只列支撑结论所需的数。
|
||||
|
||||
@@ -375,13 +373,13 @@ Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap
|
||||
| 其余(`App/` 应用与驱动自研、startup、C 库、填充) | ≈45,497 | ≈19.7% |
|
||||
| **合计** | **231,084** | 100% |
|
||||
|
||||
> 归类按 `.map` 组件表对象名前缀,合计与区域总量可差几十字节(链接器填充)。前 7 行逐字节与 2026-10-14 那次构建相同;并行未提交改动(`cfg_file` / `sd2506`)的 +48 B 落在"其余"。
|
||||
> 归类按 `.map` 组件表对象名前缀,合计与区域总量可差几十字节(链接器填充)。前 7 行逐字节与 2026-10-14 那次构建相同;并行未提交改动(`cfg_file` / `sd2506`)的 +48 B 落在"其余"。⚠ **本表是那次构建的构成快照**(合计 231,084 B);当前镜像 **232,280 B / 22.2%**,增量 +1,192 B `Code` 来自同日 dhara 移植层守卫与日志批量化(登记簿原 N-21 / N-24),**不改变"LVGL 是最大单一子系统"的结论**。
|
||||
|
||||
**要点**:LVGL 的绝对量自 F-1/F-2/F-3 裁剪后**未再增长**(87,706 B),但仍是镜像最大单一子系统 → §5.4 的剩余裁剪(≈10 KB)与 F-5 的编译时长收益依旧成立。
|
||||
|
||||
### 5.2 结构性诊断(三条)
|
||||
|
||||
1. **瓶颈已从 SRAM1 换到 CCM**:R-9 落地后 SRAM1 只剩 56.1%(**余 57,504 B**),而 **CCM 被堆占掉 87.9%(只剩 7,904 B)**,待迁的 LVGL 池 + UI 两份缓冲 = **29,570 B**。
|
||||
1. **瓶颈已从 SRAM1 换到 CCM**:R-9 落地后 SRAM1 只剩 56.1%(**现 57.4%、余 55,840 B**),而 **CCM 被堆占掉 87.9%(只剩 7,904 B)**,待迁的 LVGL 池 + UI 两份缓冲 = **29,570 B**。
|
||||
→ R-2 的剩余部分**只差"缩堆"这一步**(56→32 KB,`../内存管理/RAM使用分析与优化方案.md` §2.2;⚠ 2026-10-12 B5 是反向扩容,缩回前必须重跑 `stack_report.py` + 上板 `min_ever` 复测);顺序不可颠倒(`../内存管理/RAM预算台账.md` §3)。
|
||||
⏸ **本簇已按用户裁定 2026-10-15 整簇暂缓**(§5.3)——这条诊断保留作将来重开时的现状基线,**不代表现在要动**。
|
||||
2. **任务栈全部动态取自 FreeRTOS 堆**:`osThreadAttr_t` 只设 `stack_size` 不设 `stack_mem`(`Src/freertos.c`)→ 8 条产品任务栈合计 **9,792 words = 39,168 B**(另 testTask 2,048 words 仅测试构建)连同 idle/timer 栈、TCB、IPC 全部从堆里扣,**留给动态分配的实际余量很紧**(堆 56 KB 是 2026-10-12 B5 扩容后的值)。
|
||||
@@ -392,7 +390,7 @@ Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap
|
||||
### 5.3 RAM 优化路线(收益/风险/优先级)
|
||||
|
||||
> ⏸ **本表除已落地的 R-9 外整簇暂缓(用户裁定 2026-10-15)**,理由三条:
|
||||
> ① **没有功能在等这块预算**——SRAM1 已降到 56.1%(余 57,504 B)、Flash 22.0%,离 `mem-budget` 的 92% 红线很远;
|
||||
> ① **没有功能在等这块预算**——SRAM1 已降到 57.4%(余 55,840 B)、Flash 22.2%,离 `mem-budget` 的 92% 红线很远;
|
||||
> ② **搬迁本身有风险和工时**——R-2 要动 scatter 并逐条过 DMA 可达性清单(规则 R2/R3,违例是**静默传错**不是报错),R-4 要重排 sink 与 LVGL 刷新的生命周期,R-6 静态化后栈溢出更难发现,缩堆还得上板复测 `min_ever`;
|
||||
> ③ **收益与代价不对称**——腾出的 CCM 是给"以后可能有的功能",代价是现在动存储与显示两条主链。
|
||||
>
|
||||
@@ -419,7 +417,7 @@ Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap
|
||||
|
||||
### 5.4 Flash 优化(未闭环:F-4 / F-5 / F-6)
|
||||
|
||||
> **F-1 / F-2 / F-3 已落地并销项(2026-10-11)**:两份 `lv_conf.h` 对齐为同一特性子集 → **实测省 64,576 B**,Flash 26.3% → 20.1%(现 22.0%,增量来自 B4/B5 功能代码)。完整记录见 `../内存管理/RAM使用分析与优化方案.md` §2.6.4 与 `../UI设计/UI_设计架构.md` §1.4 第 10 条。
|
||||
> **F-1 / F-2 / F-3 已落地并销项(2026-10-11)**:两份 `lv_conf.h` 对齐为同一特性子集 → **实测省 64,576 B**,Flash 26.3% → 20.1%(现 22.2%,增量来自 B4/B5 功能代码)。完整记录见 `../内存管理/RAM使用分析与优化方案.md` §2.6.4 与 `../UI设计/UI_设计架构.md` §1.4 第 10 条。
|
||||
|
||||
| 编号 | 措施 | 可省 Flash | 依据 | 状态 |
|
||||
|------|------|-----------|------|------|
|
||||
@@ -550,7 +548,7 @@ const ui_page_desc_t ui_page_deadzone = UI_FORM_PAGE("遥测死区", s_items_dea
|
||||
|------|------|
|
||||
| 页面**不得**新增静态大缓冲;文本缓冲统一用 `UI_BUF_STRLEN` | 现有 `s_ts/s_desc` 已是范式 |
|
||||
| 每页**只用** `lv_style_const_prop_t`,禁止运行期 `lv_style_init` | 已验证动态样式每次泄漏 88 B |
|
||||
| 优先自绘(`lv_draw_*` + `lv_text_get_width`),新控件需评审(会破坏 §5.4 F-2 **已落地**的裁剪,且须两端同步) | LVGL 现占镜像 **38.0%**(87,706 / 231,084 B) |
|
||||
| 优先自绘(`lv_draw_*` + `lv_text_get_width`),新控件需评审(会破坏 §5.4 F-2 **已落地**的裁剪,且须两端同步) | LVGL 现占镜像 **37.8%**(87,706 / 232,280 B) |
|
||||
| 帧缓冲任何改动必须遵守 R-4(副本数 ≤ 2) | 当前 4 副本 12.8 KB |
|
||||
| `LVGL 池`用量必须经 `lv_mem_monitor` / 池水位门禁实测后才允许扩大 | **16 KB**(R-9 于 2026-10-15 由 40 KB 降下;实测峰值 6,568 B = 池的 40%,余 ≈9.8 KB) |
|
||||
|
||||
@@ -570,13 +568,12 @@ const ui_page_desc_t ui_page_deadzone = UI_FORM_PAGE("遥测死区", s_items_dea
|
||||
|
||||
## 7. 统一改进路线图
|
||||
|
||||
> **顺序的唯一权威 = [`../任务规划/README.md`](../任务规划/README.md) §2**(W1 采集保护收尾 → W2 数据正确性 → ~~W3 预算与内存~~ **⏸ 已整簇暂缓** → W4 结构债 → W5 验证常态 → W6 UI 扩展 → W7 仓库决策;B2 RS-485 暂缓)。本节只保留"本报告条目 → 波次"的映射,不另立顺序。
|
||||
> **顺序的唯一权威 = [`../任务规划/README.md`](../任务规划/README.md) §2**(W1 采集保护收尾 → ~~W2 数据正确性~~ **✅ 已清空销项 2026-10-15** → ~~W3 预算与内存~~ **⏸ 已整簇暂缓** → W4 结构债 → W5 验证常态 → W6 UI 扩展 → W7 仓库决策;B2 RS-485 暂缓)。本节只保留"本报告条目 → 波次"的映射,不另立顺序。
|
||||
|
||||
| 波次 | 本报告条目 | 验收要点 |
|
||||
|------|-----------|---------|
|
||||
| **W2 数据正确性** | N-12 / N-13(登记簿 N-21) | 整页扫描 + `DHARA_E_UNSUPPORTED` + **主机侧 dhara 移植层单测**(一并补覆盖率缺口) |
|
||||
| ~~**W3 预算与内存**~~ ⏸ | **整簇暂缓(用户裁定 2026-10-15)**:R-9 ✅ 已落地不回退;R-2 后半 / R-3 / R-4 / R-6 不再排期 | 暂缓期间**无验收动作**;`mem-budget` 92% 门禁与台账、趋势、`stack_report.py` / `ram_report.py` 观测**照常跑**(它们是判断何时重开的输入)→ 重开条件见 §5.3 |
|
||||
| **W4 结构债** | N-16、N-14、F-4/F-5/F-6、S-1/S-2/S-4/S-5;`rs485_task.c` 随 B2 出注册白名单 | `--strict` 默认化;`bsp_err_t` 进规范 |
|
||||
| **W4 结构债** | N-14、F-4/F-5/F-6、S-1/S-2/S-4/S-5;`rs485_task.c` 随 B2 出注册白名单 | `--strict` 默认化;`bsp_err_t` 进规范 |
|
||||
| **W5 验证常态** | P1-10 收尾(构建实测链接 + `@test` 闭环) | 逢窗口顺手做 |
|
||||
| **W6 UI 扩展** | U4 残余 → U1 → U2/U3 → U5(27 页) | 新增一页 ≤30 行;**本轮不排期** |
|
||||
| **W7 仓库决策** | D-1(登记簿 N-29) | **已决策暂不处理**;推送/协作前重评 |
|
||||
@@ -598,7 +595,7 @@ const ui_page_desc_t ui_page_deadzone = UI_FORM_PAGE("遥测死区", s_items_dea
|
||||
|
||||
| 结论 | 证据(2026-10-15 `.map`) |
|
||||
|------|---------------------|
|
||||
| SRAM1 56.1%、余 57,504 B;CCM 87.9%、余 7,904 B | map `Execution Region RW_IRAM1 Size: 0x00011f60, Max: 0x00020000` / `RW_CCM Size: 0x0000e020, Max: 0x0000ff00`;`ram_baseline.tsv` 末行 |
|
||||
| SRAM1 57.4%、余 55,840 B;CCM 87.9%、余 7,904 B | map `Execution Region RW_IRAM1 Size: 0x00012600, Max: 0x00020000` / `RW_CCM Size: 0x0000e020, Max: 0x0000ff00`;`ram_baseline.tsv` 末行 |
|
||||
| MicroLIB C 堆 = 512 B | `MDK-ARM/startup_stm32f407xx.s` `Heap_Size EQU 0x200`;map `Removing startup_stm32f407xx.o(HEAP), (512 bytes)`(未被引用 → 整段移除) |
|
||||
| 任务栈全部动态分配 | `Src/freertos.c` 的 `osThreadAttr_t` 无 `.stack_mem`;8 条产品任务栈合计 9,792 words(R-6 未办) |
|
||||
| DMA 可达性三层防线(原 N-11 的处置) | 编译期窗口断言 `gd5f2gq5ue.h:96-107`(负数组 typedef)→ 启动期自检 `nand_ftl.c:241-243` → 运行时守卫 `gd5f_dma_buf_ok()`(`gd5f2gq5ue.c:121-139`);规则族 R1~R4 → `../内存管理/内存内容分配策略.md` §4;负路径用例 TC-GD5F-704/705 |
|
||||
@@ -644,6 +641,7 @@ const ui_page_desc_t ui_page_deadzone = UI_FORM_PAGE("遥测死区", s_items_dea
|
||||
| **2026-10-15** | **修 `netcfg_save` 假失败:回归改为自洽运行** | 上一条那个"唯一差异"查到真因,**不是代码回归**:`--cfgtest` 的 E-01 场景 2 伪造一份 4000 B 纯注释配置档并断言"拒写后原文件不动",**连通过路径也不删除它**;留在工作目录后每次运行都被加载 → 外科手术保存要在 4000 B 注释后补写受管段 → 输出必超 `CFG_FILE_MAX_LEN` → 引擎**按设计拒写** → 保存行显示 `ERR`。修三处:`simulator/main.c` 给 `--cfgtest` 加外壳(无论成败一律删除伪造档)、`check_menu_rows.py` 与 `check_content_pages.py` 改在**空临时目录**里跑(`cwd=tmp`)、脚本里已作废的 `sim_netcfg.bin` 兼容清理改为只提示不删除。修后 **11/11 页 + 往返池水位 5,448 B 全过,未刷任何黄金参考**(参考本来就是对的)。排查口径 → `../UI设计/UI_自动化测试流程.md` §五 |
|
||||
| **2026-10-15** | **补漏:§0.1 六维评分与口径/附录数字追平 R-9 后状态** | 前面那条只改了 §0.2/§0.3/§0.4/§5.x,**漏掉 §0.1、§1 术语表、附录 A 证据表**(用户指出):① 内存使用行仍写"54,146 B 仍在 SRAM1 / SRAM1 74.9% / 须先 R-9"→ 改为 SRAM1 **56.1%(余 57,504 B)**、瓶颈收敛到 CCM 单点、只差"缩堆"一步,评级 **B → B+**(理由:SRAM1 由卡点变富裕,治理三件套与观测齐备;扣分项换成 CCM 单点 + C 堆无机械检查)② Flash `231,036` → **`231,084`**(§0.1 / §1 术语表 / §6.4 三处),并修掉术语表里 `ROM = 231,324 B` 这个**算术错**(`Code + RO + RW` 实为 **232,196 B**)③ RAM 占用定义 `98,144 / 74.9%` → **`73,568 / 56.1%`** ④ 附录 A 证据行改 `RW_IRAM1 Size: 0x00011f60`、余量 57,504 B ⑤ 文档分布 **88 篇 / 31,416 行 → 87 篇 / 30,781 行**、§3.1 体量表受控文件 **2,466 / 79.3 MB → 2,465 / 79.2 MB**(均随 `错误码传播审计.md` 删除,−1 篇 / −635 行;同表 `docs/` **56.9 MB**、手册图片 **703 张 / 36.3 MB** 复核未变)⑥ §0.1/§0.2/§0.4 与附录的复核日期统一标 **2026-10-15**,最近复核行的 `HEAD` 改指 `a7f5265` 并注明"后续文档与模拟器侧提交不影响固件",避免每提交一次就过期 |
|
||||
| **2026-10-15** | **裁定:内存优化簇整簇暂缓** | 用户判定"把 LVGL 池与 UI 帧缓冲(现 ≈29.6 KB)搬进 CCM 作为后续功能预算来源"这个决定不合适,内存优化的事先放一放。据此:§0.2 **Top 3 → Top 2**(R-2 剩余移出名列,表下加一条 ⏸ 说明);§0.3 主线一由"把内存从卡死变富裕"改写为"**这条线到此为止**";§0.4 两条内存条目 🟡/❌ → **⏸ 暂缓**;§5.1 结论段与 §5.2 诊断 1 补"不代表现在要动";**§5.3 加暂缓横幅**(三条理由 + 保留常开的治理面 + 三条重开条件),R-2 / R-3 / R-4 / R-6 优先级列一律 ~~P1/P2~~ → **⏸ 暂缓**。**已落地的 R-9 不回退**(−24,576 B 是净收益且已台架验收);`mem-budget` 92% 门禁、预算台账、趋势基线、栈/堆观测**全部保留常开**——它们是传感器,不是优化动作。同步 `../任务规划/登记簿.md` N-27、`../任务规划/README.md` W3、`../内存管理/RAM预算台账.md` §3、`../内存管理/RAM使用分析与优化方案.md` 阶段 1.2 / 2 |
|
||||
| **2026-10-15** | **执行本报告条目:N-12 / N-13 / N-16 落地并销项** | **N-12**:`dhara_nand_is_free()` 改整页扫描(栈上 64 B 分块、页首命中即早退,成本与旧实现同价)。**N-13:本报告原建议经核对不成立,已按实际契约改修**——本仓裁剪版 dhara 既没有 `DHARA_NO_PAGE`(哨兵实为 `DHARA_PAGE_NONE`,`journal.h:46`)也没有 `DHARA_E_UNSUPPORTED`(`error.h:20-31` 枚举里没有,照抄直接编译失败),且 `map.c` 三处 `dhara_journal_copy` 调用点各自已挡掉哨兵(`map.c:260`、`:434`+`:440`、`:242`);真风险是**移植层零上界校验**(越界页号被地址位截断成真实页 → 静默读写错页),故改为六个 HAL 入口统一几何上界守卫 + `DHARA_E_CORRUPT_MAP`(不能用 `BAD_BLOCK`:`journal.c:904` 会据此标坏块;不能用 `RECOVER`:`map.c:275` 会据此启动回收)。**N-16**:写侧一次排空合并成"每文件一次 open/write/close"(≤8 条 = 512 B 静态缓冲;**掉电语义不变**——批次仍在同一次 drain 内 close,close 回写目录项这个不同 LPN 会逼出 FTL 单页写回缓存;失败尾部回投队列,条目不丢);读侧 8 条滑动窗口 + 写者代数失效(API 与 UI 零改动)。**刻意不批量**:`sys_log_evidence()` 的 `wdev.log` 仍逐条落盘——取证路径要求立刻进 NAND 并靠 flush 诱导逼出脏页。**验证**:`@build` 0 错 0 警(Code 212,344→**213,536**、ZI 129,880→**131,520**,+1,640 B 全为静态缓冲,不吃栈);`@test` PASS 且**台架 journal 恢复点与修复前同轨**(`sectors used` 恒 14,094、`Map head` 每轮 +128 单调推进、`epoch` 稳定 3 → 整页 `is_free` 未改变恢复语义、无双编程);`stack_report.py` 9 个任务全 PASS(defaultTask 静态最坏链 3,024 B / 余 40%,实测峰值 2,164 B / 余 57%);新增主机侧移植层单测 `test/host/nand_port/`(假芯片 + 旧算法对照用例)。实施记录 → `../架构设计/STM32F4-Base存储架构说明.md` §5.3、`../子系统设计/运行与自检记录设计说明.md` §4.4/§4.5 |
|
||||
| **2026-10-14** | **清理:删除已完成与过时内容** | ① **已修复项按销项删除**(去处见下表):N-02(FTP 鉴权)、N-03c(`Error_Handler` 降级)、**N-11(DMA 可达性三层防线 + 规则族 R1~R4)**、S-3(注册反向校验)、P2-5/P2-6(SD2506 负温度与 WRTC)、P2-8(`NET_RECV_TIMEOUT_MS` 已删)、P2-9(`mkdir` 幂等重试)、§5.5 前三条(内存治理三件套)、交付物清单已建项。② **删除过时层**:2026-09-16 / 2026-10-11 双列基线表与"复核期间未提交改动"口径说明、旧"组合结果预估"表、逐条"2026-10-11 复核"批注、历史变更记录行——内存数字改由 `../内存管理/RAM预算台账.md` 与 `scripts/ram_baseline.tsv` 持有。③ **按 2026-10-14 `.map` 与代码重测**全部保留结论的数字(含 `lftpd_log.c` 已删除并入 `lftpd_auth.c`,N-06 残余只剩 `rs485_task.c` 一条刻意缺口)。④ **修正三处错误**:R-5(`configMAX_PRIORITIES` 56→32)**判定不可行**(CMSIS-RTOS2 `#error`);原 §7 把报告 S-5(驱动统一错误码)与登记簿 S-5(卷锁覆盖面)混为一谈;原 §5.5 第 4 条建议的 `_Static_assert` 形态在 AC5 上取不到链接期地址,已被"窗口断言 + 启动自检 + 运行时守卫"三层取代。⑤ 报告自有编号**不回改、不重排**(`../编号规范.md`) |
|
||||
|
||||
### 已销项条目的记录去处
|
||||
@@ -671,3 +669,5 @@ const ui_page_desc_t ui_page_deadzone = UI_FORM_PAGE("遥测死区", s_items_dea
|
||||
| §5.5 前三条(92% 门禁 / 趋势基线 / 预算台账,登记簿 N-27 前半) | `../内存管理/RAM预算台账.md`(阈值与配额的**唯一权威**)+ `scripts/ram_baseline.py`(`--gate/--update/--trend/--write`)+ `../代码规范/pre-commit使用说明.md`(`mem-budget`) |
|
||||
| F-1 / F-2 / F-3(LVGL 死代码裁剪) | `../内存管理/RAM使用分析与优化方案.md` §2.6.4;`../UI设计/UI_设计架构.md` §1.4 第 10 条(**两份 `lv_conf.h` 必须同一特性子集**);`../UI设计/UI_自动化测试流程.md` 变更记录(池水位基线) |
|
||||
| **R-9(`LV_MEM_SIZE` 40→16 KB,登记簿 N-27 原 W3 的第一步,2026-10-15 已执行;该簇其余部分同日整簇暂缓)** | 取值依据与再降 8 KB 的前置写在 `Lib/lv_conf.h` 与 `simulator/lv_conf.h` 的池注释;实测水位 → `../内存管理/RAM使用分析与优化方案.md` §2.6.2 与 §阶段 1.1(已执行);解锁链第 ① 步 ✅(后续步骤暂缓)→ `../内存管理/RAM预算台账.md` §3;池容量口径 → `../UI设计/UI_设计架构.md` §1.2、`../UI设计/UI_界面制作流程.md` C4 |
|
||||
| **N-12 / N-13**(dhara 移植层 `is_free` 精度 + HAL 越界守卫,登记簿 **N-21**,2026-10-15 已销项) | `../架构设计/STM32F4-Base存储架构说明.md` §5.3(移植层两条硬约束:整页精确的原因链 + 错误码取舍与"为什么不会静默清卷")+ `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` 的 `ftl_page_in_range` / `ftl_block_in_range` / `dhara_nand_is_free` 注释 + 主机侧单测 `test/host/nand_port/`(含"旧算法只看页首 64 B"的对照用例,防止将来被改回去) |
|
||||
| **N-16**(日志读写放大,登记簿 **N-24**,2026-10-15 已销项) | `../子系统设计/运行与自检记录设计说明.md` §4.4(写侧批量合并 + **掉电语义不变**的论证 + 失败回投不丢条目 + 容量轮转只在慢路径)与 §4.5(读侧 8 条窗口 + 写者代数失效 + API 不变)+ `App/log/sys_log_mcu.c` 的 `s_format_rec` / `s_append_rec` / `s_append_many` / `s_read_raw` 注释 |
|
||||
|
||||
@@ -2,4 +2,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
|
||||
2026-10-12 209492 18332 1104 154184 227824 21.7 97864 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;console_log.o=4120;ch395f.o=4106;default_task.o=2736;sys_log_mcu.o=1924
|
||||
2026-10-13 211456 18760 1104 154232 230216 22.0 97912 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924
|
||||
2026-10-14 212296 18740 1112 154456 231036 22.0 98144 74.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924
|
||||
2026-10-15 212344 18740 1112 129880 231084 22.0 73568 56.1 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924
|
||||
2026-10-15 213540 18740 1136 131520 232280 22.2 75232 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736
|
||||
|
||||
|
+28
-6
@@ -66,14 +66,34 @@
|
||||
| `time_sync/time_sync_test.c` | 测试单元本体(`#include` 生产源码 + 网络 / 事件 / RTOS 可脚本化桩 + `longjmp` 跳出 `for(;;)` 任务循环) |
|
||||
| `time_sync/stub/` | 桩头文件(`FreeRTOS.h` / `task.h` / `cmsis_os2.h` / `net_socket.h` / `sys_clock.h` / `sys_events.h` / `dbg_log.h`) |
|
||||
| `iec104/run_unit_test.py` | **IEC 60870-5-104 协议栈单测(B4)**:离线编译真实 `App/protocol/iec104/` 三模块 + 数据源(prot_set/prot_elm/phasor/sample_ring),**82** 用例覆盖 CP56 往返 / APCI 半帧·粘帧·坏帧 / STARTDT 握手 / 总召分片+RBT / COV 死区 / k=12 窗满 / w=8 触发 S / t1·t2·t3 / N(R) 倒退判故障 / 遥控·定值两段式与 PN / C_CS / 未知类型·地址错误应答 |
|
||||
| `nand_port/run_unit_test.py` | **dhara NAND 移植层单测(N-21)**:离线编译真实 `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` + 真实 `Lib/dhara/{journal,map}.c`,以 **RAM 假芯片**驱动,**149** 断言覆盖 is_free 整页扫描(N-12)/ 六个 HAL 入口的越界守卫(N-13)/ 故障注入 / dhara map"写入 → 重新 resume"场景 |
|
||||
| `nand_port/nand_port_test.c` | 测试单元本体(`#include` 生产源码 + RAM 假芯片;内联一份"只看页首 64 B"的**旧算法对照**,证明断言能咬人) |
|
||||
| `nand_port/stub/` | 桩头文件(`gd5f2gq5ue.h` 芯片几何与 SPI 原语声明 / `dbg_log.h` 日志宏计数) |
|
||||
|
||||
> 详见 `docs/测试说明/看门狗单元测试说明.md`、`docs/测试说明/系统时钟单元测试说明.md`、
|
||||
> `docs/测试说明/配置文件单元测试说明.md`、`docs/测试说明/ADC输入层单元测试说明.md`、
|
||||
> `docs/测试说明/相量层单元测试说明.md`、`docs/测试说明/卷锁单元测试说明.md`、
|
||||
> `docs/测试说明/时间同步单元测试说明.md`;
|
||||
> harness 用法见 [`common/README.md`](common/README.md)。
|
||||
> 已挂 pre-commit 钩子 `watchdog-unit-test`、`sys-clock-unit-test`、`cfg-file-unit-test`、
|
||||
> `phasor-unit-test`、`adc-input-unit-test`、`vol-lock-unit-test`、`time-sync-unit-test`。
|
||||
> 已挂 pre-commit 钩子(各模块同名 `*-unit-test`,共 **16** 个;逐个说明见
|
||||
> `docs/代码规范/pre-commit使用说明.md` §2 与 AGENTS.md 门禁条)。
|
||||
|
||||
### nand_port —— dhara NAND 移植层为什么能在 PC 上测
|
||||
|
||||
**一句话**:移植层是**纯逻辑 + 假芯片**,能钉死真机极难复现的越界/误判路径。
|
||||
|
||||
`nand_ftl.c` 的 dhara HAL 六个入口只做地址算术与边界判定,芯片侧全部经 11 个 SPI
|
||||
原语打交道——把它们换成一块 RAM 假芯片,整层即可离线跑。而它要防的两类缺陷恰恰是
|
||||
板端**拿不到观测面**的:
|
||||
|
||||
- **N-12**:`is_free` 旧实现只看页首 64 B,"页首 0xFF、后段有数据"的已编程页被误判
|
||||
空闲 → dhara 往已编程页再编程 = 静默数据丢失。真机要恰好撞上那种页形态才发作,
|
||||
发作后只表现为"某次复位后丢数据"。假芯片把**双编程做成可数事件**(违规计数),
|
||||
测试还内联一份旧算法做对照:同一页旧算法答"空闲"、新实现答"非空"。
|
||||
- **N-13**:越界页/块号(含 `DHARA_PAGE_NONE` 哨兵)旧会被地址位截断成**真实页**
|
||||
→ 静默读写/搬走无辜数据。真机上没有任何路径会主动喂进越界号。假芯片记录**全部
|
||||
访问次数**,于是"越界 → 返回 -1 + `DHARA_E_CORRUPT_MAP` 且零芯片访问、零标坏块"
|
||||
可机械断言。
|
||||
|
||||
## 运行方式
|
||||
|
||||
@@ -106,6 +126,9 @@
|
||||
|
||||
# 时间同步单测(纯逻辑,无需真机)
|
||||
& '.venv\Scripts\python.exe' test/host/time_sync/run_unit_test.py
|
||||
|
||||
# dhara NAND 移植层单测(纯逻辑 + RAM 假芯片,无需真机)
|
||||
& '.venv\Scripts\python.exe' test/host/nand_port/run_unit_test.py
|
||||
```
|
||||
|
||||
> 串口端口配置在 `.serialtest/config.json`(单一配置源,当前 COM6/115200)。
|
||||
@@ -114,14 +137,13 @@
|
||||
|
||||
- **PYTHONPATH/路径敏感**:脚本按仓库根相对路径读写,**从仓库根执行**
|
||||
- 本目录**被 pre-commit 门禁的 cppcheck / 代码规范排除**(`test/` 豁免);
|
||||
但各模块的 `run_unit_test.py` 自身**作为独立钩子**参与门禁(见上,共 7 个)
|
||||
但各模块的 `run_unit_test.py` 自身**作为独立钩子**参与门禁(见上,共 16 个)
|
||||
- 台架探针族分工(2026-10-13 起):**自愈/Trap 常备回归腿在本目录**(`relisten_stress` /
|
||||
`ftp_abort_probe` / `n30_repro` / `e02_probe` / `e6_soak` 等);`MDK-ARM/*.py` 只余
|
||||
历史一次性探针(`ftp_freeze_probe` / `tcp_reconnect_probe` / `ftp_retr_abort_probe` /
|
||||
`bench_verdict`)
|
||||
- 全部主机侧单测(`watchdog/`、`sys_clock/`、`cfg_file/`、`phasor/`、`adc_input/`、
|
||||
`vol_lock/`、`time_sync/`)都需要本机装有 MSVC(VS2019/2022/2026 的 C++ 工具链);
|
||||
无则脚本报环境错误(退出码 2)
|
||||
- 全部主机侧单测(16 个模块目录,含本目录新增的 `nand_port/`)都需要本机装有 MSVC
|
||||
(VS2019/2022/2026 的 C++ 工具链);无则脚本报环境错误(退出码 2)
|
||||
|
||||
## 相关文档
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""dhara NAND 移植层(nand_ftl)的主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/nand_port/nand_port_test.c(它以 #include 纳入
|
||||
**生产源文件** Drivers/BSP/GD5F2GQ5UE/nand_ftl.c),在 PC 上离线跑 dhara NAND
|
||||
HAL 六个入口(is_bad / mark_bad / erase / prog / is_free / read / copy)的
|
||||
几何守卫与整页判空,失败即非 0 退出。零硬件、零烧录、毫秒级。
|
||||
|
||||
工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness
|
||||
`test/host/common/unit_test_harness.py`(本文件只做声明)。
|
||||
|
||||
为什么要主机侧单测(而不是只用板端套件):
|
||||
移植层是**纯地址算术 + 边界判定**,芯片侧只经 11 个 SPI 原语打交道——把它换成
|
||||
一块 RAM 假芯片,整层就完全可在 PC 上跑。而它要防的两类缺陷恰恰是真机极难复现的:
|
||||
- N-12:is_free 只看页首 64 B → "页首 0xFF、后段有数据"的页被误判空闲 →
|
||||
dhara 往已编程页再编程 = 静默数据丢失(真机要恰好撞上那种页形态才发作);
|
||||
- N-13:越界页/块号(含 DHARA_PAGE_NONE 哨兵)被地址位截断成真实页 →
|
||||
静默读写/搬走无辜数据(真机上没有任何路径会主动喂进越界号)。
|
||||
假芯片把"双编程"做成可数事件、并记录全部访问次数,于是"越界不产生任何芯片
|
||||
访问""越界标坏块次数为 0"这类判据可机械断言——这是板端永远拿不到的观测面。
|
||||
|
||||
用法(仓库根目录):
|
||||
python test/host/nand_port/run_unit_test.py
|
||||
或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/nand_port/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
|
||||
|
||||
设计依据:docs/问题记录/项目审查与优化建议报告.md N-12/N-13(报告 R-3 建议的
|
||||
`test/host/dhara/` 移植层单测,落位为 test/host/nand_port/);
|
||||
创建日期:2026-10-15
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
|
||||
# harness 位于 test/host/common/
|
||||
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
|
||||
|
||||
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
|
||||
|
||||
# 顺序敏感:stub 目录必须**先于**生产目录,才能覆盖 gd5f2gq5ue.h(真实头会拖入
|
||||
# main.h + 整套 ST HAL/CMSIS,PC 上不可编译)与 dbg_log.h(真实头依赖 sys_clock)。
|
||||
# nand.h / journal.h / map.h / error.h(Lib/dhara)与 ff.h / diskio.h / ffconf.h
|
||||
# (Lib/FatFs)用**真实头**——接口契约原汁,桩与真实 API 一旦漂移编译期即报错。
|
||||
BUILD = TestBuild(
|
||||
name="NAND Port Tests",
|
||||
src=os.path.join(HERE, "nand_port_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(HERE, "stub"),
|
||||
os.path.join(ROOT, "Drivers", "BSP", "GD5F2GQ5UE"),
|
||||
os.path.join(ROOT, "Lib", "dhara"),
|
||||
os.path.join(ROOT, "Lib", "FatFs"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== NAND Port Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 主机侧单测桩:dbg_log.h
|
||||
* 模块功能:为 Drivers/BSP/GD5F2GQ5UE/nand_ftl.c 提供主机可编译的日志宏
|
||||
* (DBG_* 映射为计数,不真正打印,避免污染测试输出)。
|
||||
*
|
||||
* ⚠ 仅供 test/host/nand_port/ 使用,不参与 MCU 构建。
|
||||
* MCU 版(Drivers/BSP/dbg_log.h)依赖 sys_clock(RTC 时间戳)与 console,
|
||||
* 并在 DBG_ENABLE_* 开关下展开成行缓冲 + 时间戳格式化。
|
||||
*
|
||||
* 被测源在 `#include "dbg_log.h"` 之前自定义了 DBG_LINE_BUF_MAX / DBG_TAG,
|
||||
* 本桩不使用它们(宏重定义无副作用:本桩不定义同名宏)。
|
||||
*
|
||||
* 为什么计数而不是直通 printf:
|
||||
* nand_ftl.c 的日志只在 disk_initialize / 缓存回写路径上,本套件测的是
|
||||
* dhara HAL 六个入口;直通打印会让"越界不产生任何芯片访问"这类断言的
|
||||
* 输出被日志淹没。留计数则仍可断言"错误路径确有留痕"。
|
||||
*
|
||||
* 创建日期:2026-10-15
|
||||
*/
|
||||
|
||||
#ifndef DBG_LOG_H
|
||||
#define DBG_LOG_H
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
/* 测试观测计数(在 nand_port_test.c 实现):
|
||||
* g_nandtest_err_count —— DBG_ERROR 次数
|
||||
* g_nandtest_info_count —— DBG_INFO 次数
|
||||
* g_nandtest_debug_count —— DBG_DEBUG 次数 */
|
||||
extern int g_nandtest_err_count;
|
||||
extern int g_nandtest_info_count;
|
||||
extern int g_nandtest_debug_count;
|
||||
|
||||
#define DBG_INFO(fmt, ...) do { g_nandtest_info_count++; } while (0)
|
||||
#define DBG_DEBUG(fmt, ...) do { g_nandtest_debug_count++; } while (0)
|
||||
#define DBG_ERROR(fmt, ...) do { g_nandtest_err_count++; } while (0)
|
||||
|
||||
#endif /* DBG_LOG_H */
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 主机侧单测桩:gd5f2gq5ue.h(GD5F2GQ5UE SPI NAND 驱动 API)
|
||||
* 模块功能:为 Drivers/BSP/GD5F2GQ5UE/nand_ftl.c 提供**可在 PC 上编译链接**的
|
||||
* 芯片几何常量与 SPI 原语声明;实现体(RAM 假芯片)在
|
||||
* nand_port_test.c 里给出。
|
||||
*
|
||||
* ⚠ 仅供 test/host/nand_port/ 使用,不参与 MCU 构建。
|
||||
* 真实头 Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h 会 `#include "main.h"` 拖入整套
|
||||
* ST HAL / CMSIS(GPIO、SPI、HAL_GetTick),主机侧不引入。
|
||||
*
|
||||
* 保真点(与真实头逐项对齐,只有容量是刻意缩小的):
|
||||
* - 几何常量 GD5F_PAGE_SIZE / GD5F_PAGES_PER_BLOCK / GD5F_BLOCK_SIZE /
|
||||
* GD5F_SPARE_SIZE / GD5F_BBT_POOL_COUNT **同名同值**;
|
||||
* - GD5F_TOTAL_BLOCKS 真实 2048(256 MB)→ 本桩 **32**(4 MB),
|
||||
* 够跑 dhara map/journal 与全部越界用例;
|
||||
* - 返回值 GD5F_OK / GD5F_ERROR / GD5F_PROGRAM_FAIL / GD5F_ERASE_FAIL 同值;
|
||||
* - SPI 原语**签名逐字照抄**真实头(gd5f_page_read/gd5f_read_from_cache/
|
||||
* gd5f_program_load/gd5f_program_exec/gd5f_block_erase/gd5f_check_ecc/
|
||||
* gd5f_write_enable/gd5f_dma_buf_ok/gd5f_get_usable_blocks/
|
||||
* gd5f2gq5ue_is_block_bad/gd5f2gq5ue_mark_block_bad),
|
||||
* 桩与真实 API 一旦漂移,编译期即报错。
|
||||
*
|
||||
* 刻意**不搬**的部分:SPI 命令码 / 状态位 / 控制引脚宏 / DMA 窗口常量与
|
||||
* 编译期守卫(nand_ftl.c 不使用它们;DMA 可达性在 PC 上没有物理意义,
|
||||
* 由假芯片的 gd5f_dma_buf_ok 恒返 1 代替)。
|
||||
*
|
||||
* 创建日期:2026-10-15
|
||||
*/
|
||||
|
||||
#ifndef GD5F2GQ5UE_H
|
||||
#define GD5F2GQ5UE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ======================== 芯片几何(与真实头同名同值,除总块数) ======================== */
|
||||
|
||||
#define GD5F_PAGE_SIZE 2048
|
||||
#define GD5F_SPARE_SIZE 64
|
||||
#define GD5F_TOTAL_PAGE_SIZE 2112
|
||||
#define GD5F_PAGES_PER_BLOCK 64
|
||||
#define GD5F_BLOCK_SIZE (GD5F_PAGES_PER_BLOCK * GD5F_PAGE_SIZE)
|
||||
|
||||
/* 真实 2048 块(256 MB)→ 假芯片 32 块(4 MB),见文件头说明 */
|
||||
#define GD5F_TOTAL_BLOCKS 32
|
||||
#define GD5F_TOTAL_SIZE (GD5F_TOTAL_BLOCKS * GD5F_BLOCK_SIZE)
|
||||
|
||||
/* 坏块表保留块池(真实头同值):dhara 可见块数 = 总块数 - 本值 */
|
||||
#define GD5F_BBT_POOL_COUNT 4
|
||||
|
||||
/* ======================== 返回值(与真实头同值) ======================== */
|
||||
|
||||
#define GD5F_OK 0
|
||||
#define GD5F_ERROR -1
|
||||
#define GD5F_BUSY_TIMEOUT -2
|
||||
#define GD5F_ECC_ERROR -3
|
||||
#define GD5F_PROGRAM_FAIL -4
|
||||
#define GD5F_ERASE_FAIL -5
|
||||
#define GD5F_ID_MISMATCH -6
|
||||
|
||||
/* ======================== SPI 原语(签名逐字照抄真实头) ======================== */
|
||||
|
||||
int32_t gd5f_write_enable(void);
|
||||
int32_t gd5f_page_read(uint32_t page_addr);
|
||||
int32_t gd5f_read_from_cache(uint16_t column, uint8_t *p_buf, uint32_t size);
|
||||
int32_t gd5f_program_load(uint16_t column, const uint8_t *p_buf, uint32_t size);
|
||||
int32_t gd5f_program_exec(uint32_t page_addr);
|
||||
int32_t gd5f_block_erase(uint32_t block_addr);
|
||||
int32_t gd5f_check_ecc(void);
|
||||
|
||||
int32_t gd5f2gq5ue_is_block_bad(uint32_t block);
|
||||
void gd5f2gq5ue_mark_block_bad(uint32_t block);
|
||||
uint32_t gd5f_get_usable_blocks(void);
|
||||
int gd5f_dma_buf_ok(const void *p_buf, size_t size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* GD5F2GQ5UE_H */
|
||||
Reference in New Issue
Block a user