台架执行《实机测试实施指南》步骤 3~7 并闭环

步骤 3/4(IWDG V2/V4/V5 + 堆低水位 S11):
- V2 任务饿死 stall 后 ≤2.5s IWDG 复位;V4 关中断死循环 ~4.9s 稳定复位;
  V5 30min 120×FTP 全 PASS、零 IWDG 复位。堆告警全程未触发(min_ever=4728B)。
- task_watchdog.c:stall 证据写盘会经 nand_ftl_wd_feed 喂狗 → 增 s_stall_evidenced
  每次 stall 仅取一次证据,避免"取证据"本身阻止复位。
- default_task.c:堆低水位告警(HEAP_LOW_WATER_B=3072)。

步骤 5(GD5F 错误通道 S9):gd5f2gq5ue.c/.h + nand_ftl.c 错误码统一为 int32_t 并
全量检查返回值;套件回归 16/16 PASSED。
- test/run_all_suites.py:--forbidden 改为按套件覆盖(GD5F 用 "HardFault;assert")——
  TC-GD5F-704 故意触发 [ERR] 回落轮询,统一默认值会误判 FAIL。

步骤 6(网络稳定性):7.1 正常路径 ✅(V5 证据);7.2 N-2 丢弃可观测性代码已具备、
49s 静默在 V5 未复现(降级低风险观察项);7.3 N-3 待 PC peer。

步骤 7(工程卫生):
- S5:新增 test-cppcheck 钩子(仅 warning,排除模拟器桩)。
- S4:新增 scripts/check_test_traceability.py(默认仅告警)。
- 待核实项 TEST_SUITE_SELECTED 实机核实 = 死代码(无使用点);RTC 套件实际正常执行,
  真缺口 = 缺套件级汇总行 → sd2506_test_task.c 补 === RTC Tests: N/M PASSED === 并
  修正 Phase 级汇总累计问题;test_config.h 补齐 RTC 并注释"当前无引用"。

验证:产品 @build 0/0;pre-commit --all-files 17/17 Passed;
run_all_suites.py --suites RTC → PASS。
文档:实机测试实施指南 / SD2506_Test_Guide / 待处理内容 / 测试与架构改进建议 同步。
This commit is contained in:
2026-10-04 00:35:06 +08:00
parent aef1d92b3f
commit 029437be89
22 changed files with 631 additions and 151 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"assert-not-deadloop": "d4a00718ab2bd071",
"assert-impl": "b7fd1fadb4522edf",
"stack-overflow-hook-impl": "b7fd1fadb4522edf",
"watchdog-module": "9c3c1c1f4a01dcd3",
"watchdog-module": "58f95a6cb88e6925",
"reset-cause-latch": "52fc71acbb0f1aed",
"iwdg-hal-driver": "4b902875a84554e9",
"hal-iwdg-enabled": "c8c9b916b05464b9",
+29
View File
@@ -78,6 +78,35 @@ repos:
MDK-ARM/|
test/
)
# 4b) test/ 代码宽松静态检查(S5,2026-10-03 加)
# 背景:`test/` 此前完全豁免 pre-commit(靠"自律"),而 `test/board/` 下
# 6300+ 行 C 代码**且编入固件**(曾出现 `s_rx_buf` 使用先于声明)。
# 本钩子**只开 warning**(不引入存量 style 噪音),排除模拟器桩
# (`test/host/*/stub/`,第三方/桩头形态不受控)。
# 基线:2026-10-03 全量实测 **0 条 warning**(存量可控,故直接挂钩子)。
# 依据:docs/审查记录/测试与架构改进建议.md §3.4;实机测试实施指南.md §8.1
- id: test-cppcheck
name: cppcheck (test/, warning only)
entry: cppcheck
args: ['--enable=warning', '--error-exitcode=1',
'--inline-suppr', '--std=c99', '--quiet',
'--suppress=missingIncludeSystem', '--suppress=missingInclude',
'--suppress=invalidPrintfArgType_sint', '--suppress=invalidPrintfArgType_uint',
'--suppress=unusedFunction', '--suppress=unusedStructMember',
'--suppress=constParameterPointer', '--suppress=constParameterCallback',
'--suppress=redundantAssignment',
'--suppress=normalCheckLevelMaxBranches',
'-i', 'test/host/watchdog/stub',
'-i', 'test/host/cfg_file/stub',
'-i', 'test/host/sys_clock/stub',
'-I', 'Inc', '-I', 'App', '-I', 'Drivers/BSP',
'-I', 'Drivers/STM32F4xx_HAL_Driver/Inc', '-I', 'Drivers/CMSIS/Include',
'-I', 'Middlewares/Third_Party/FreeRTOS/Source/include',
'test/']
language: system
# 触发条件:test/ 下 C/头文件变动;扫描范围恒为整个 test/(pass_filenames=false)
files: ^test/.*\.(c|h)$
pass_filenames: false
- id: no-mojibake
name: Forbid U+FFFD mojibake
entry: python scripts/check_mojibake.py
+21
View File
@@ -57,6 +57,11 @@ extern const osThreadAttr_t timeSyncTask_attributes;
* 之后每周期只判定"是否出现更深峰值",无新峰值不打印(保持串口静默窗口) */
#define STACK_PROBE_FIRST_MS 5000u
#define STACK_PROBE_PERIOD_MS 2000u
/* S11(2026-10-03):堆低水位告警阈值(B)。
* 依据:2026-10-03 上板两次开机的 min_ever 实测下限 3,712 B(configTOTAL_HEAP_SIZE
* = 48KB)——低于此值说明堆余量 <8%,新增任务/缓冲随时可能分配失败(隐性风险)。
* 阈值取 3,072 B 留出运行间 ~600B 波动余量,避免把正常波动误报为故障。 */
#define HEAP_LOW_WATER_B 3072u
/* E-31 自愈策略(2026-09-21 审计 P0 修复):启动期存储挂载失败后,
* 主循环按 2s 周期重跑完整 bring-up 的次数上限;耗尽则维持无存储降级
* (自检已上报、卷保留、不重格式化),等下次上电重新 resume */
@@ -274,6 +279,22 @@ static void stack_probe_poll(void) {
}
s_last_tick = (uint32_t)now;
/* S11 堆低水位告警(2026-10-03):min_ever 跌破阈值 → 串口告警 + 自检留痕。
* 独立于下方"出现更深栈峰值才打印"的抑制:堆水位与栈峰值无关,须每 2s 采样。
* xPortGetMinimumEverFreeHeapSize() 单调不增 → 一旦跌破即持续成立,故只报一次
* (latch),既不刷屏,也不会漏(s_heap_alarm 置位后不再复报)。 */
{
static uint8_t s_heap_alarm;
const uint32_t min_ever = (uint32_t)xPortGetMinimumEverFreeHeapSize();
if ((s_heap_alarm == 0u) && (min_ever < HEAP_LOW_WATER_B)) {
s_heap_alarm = 1u;
DBG_ERROR("[HEAP] low water: %lu B (< %lu B)",
(unsigned long)min_ever, (unsigned long)HEAP_LOW_WATER_B);
(void)syslog_post(SYSLOG_SELFTEST, "heap low water");
}
}
for (i = 0u; i < (uint32_t)STACK_PROBE_COUNT; i++) {
const osThreadId_t th = *(s_stack_probe[i].p_id);
uint32_t free_now;
+16 -2
View File
@@ -106,6 +106,7 @@ static uint32_t s_miss_total; /* 累计未达标轮数(观测用) */
static uint32_t s_last_mask; /* 上次抽样到的位图(定位卡死任务用) */
static uint32_t s_started; /* 1 = IWDG 已启动 */
static uint32_t s_last_tick; /* 上次抽样时刻(100ms 节流基准) */
static uint32_t s_stall_evidenced; /* 1 = 本停摆 episode 已落盘证据(门闩;见 task_wd_poll) */
/* ==== round 7 纯取证状态(仅 MCU 构建;判定状态机逐字不变)====
* 设计背景见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录:误复位后 RAM 态随复位消失,
@@ -453,6 +454,7 @@ void task_wd_poll(void) {
if (snapshot == wd_required_mask()) {
s_miss_streak = 0u;
s_stall_evidenced = 0u; /* 恢复:允许下次停摆再落一次证据 */
wd_platform_feed(); /* 全员健康:喂狗 */
return;
}
@@ -487,8 +489,19 @@ void task_wd_poll(void) {
(unsigned long)s_miss_total);
#ifndef TASK_WATCHDOG_UNIT_TEST
/* round 7 取证:把现场落 /log/wdev.log(掉电不丢)。
* 串口 stall 行经僵尸通道可能丢失,故此处独立留盘上证据。 */
wd_stall_evidence(snapshot);
* 串口 stall 行经僵尸通道可能丢失,故此处独立留盘上证据。
* ⚠ 本 episode 只写**一次**(s_stall_evidenced 门闩;注意 s_miss_streak
* 达上限后会**饱和**在容忍值,不能用它判"首次"):证据落盘经
* sys_log_evidence() 走 NAND 写 → FTL 路径调 nand_ftl_wd_feed()
* **直接喂狗**。若每轮 100ms 都写,等于在"停喂"窗口里持续喂狗 →
* IWDG 永不溢出 → 停喂语义被证据机制自身抵消(2026-10-03 台架 V2
* 实测:stall 已声明、miss 累至 1157≈115s 仍不复位,见
* docs/测试说明/实机测试实施指南.md §4)。写一次即可留证,此后不再
* 触碰 NAND,恢复"停喂 → IWDG 溢出复位"(§5.3 的权威归因)。 */
if (s_stall_evidenced == 0u) {
s_stall_evidenced = 1u;
wd_stall_evidence(snapshot);
}
#endif
}
}
@@ -699,5 +712,6 @@ void task_wd_test_reset(void) {
s_last_mask = 0u;
s_started = 0u;
s_last_tick = 0u;
s_stall_evidenced = 0u;
}
#endif
+160 -39
View File
@@ -91,6 +91,7 @@ void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
static int gd5f_page_program(uint32_t page_addr, uint16_t column,
const uint8_t *p_buf, size_t size);
static int gd5f_set_feature(uint8_t addr, uint8_t data);
static int gd5f_private_get_feature(uint8_t addr, uint8_t *p_data);
/* BBT 持久化内部函数 */
static int gd5f_private_bbt_locate_pool(void);
@@ -140,9 +141,17 @@ static int gd5f_dma_buf_ok(const void *p_buf, size_t size) {
/*
* 函数功能:等待芯片操作完成(轮询 OIP 位)
* 入口参数:timeout_ms - 超时时间 uint32_t > 0
* 返回值:0 - 操作完成,-2 - 超时
* 返回值:GD5F_OK(0) - 操作完成;GD5F_BUSY_TIMEOUT(-2) - 超时(可重试);
* GD5F_ERROR(-1) - SPI 传输真故障(不可重试,须上报自检)
* 限定条件:SPI 已初始化
* 函数说明:循环读取状态寄存器直到 OIP 位清零或超时
* 函数说明:循环读取状态寄存器直到 OIP 位清零或超时。
* E-53(2026-10-03,GD5F 错误通道统一):三处 HAL 调用的返回值**必须**检查
* ——原实现全部丢弃,失败时 `status` 保持初值 0 → `(status & OIP) == 0`
* 立即成立 → **把"操作完成"伪造出来**,后续命令在未就绪的芯片上执行,
* 最终表现为"静默报成功但数据没落盘"。现区分两类失败:
* ① SPI 层失败(HAL≠OK)→ GD5F_ERROR(真故障);
* ② OIP 长时间不落 → GD5F_BUSY_TIMEOUT(可重试)。
* 调用点据此分级:超时重试 / 真故障上报自检并降级(见审计 §5 第二档 #1)。
*/
/* 看门狗心跳弱钩子:默认空实现(无看门狗/模拟器构建);强符号由
* App/util/task_watchdog.c 提供,路由到调用者任务的心跳位 */
@@ -155,7 +164,7 @@ __weak void nand_ftl_wd_beat(void) {
__weak void nand_ftl_wd_feed(void) {
}
int gd5f_wait_busy(uint32_t timeout_ms) {
int32_t gd5f_wait_busy(uint32_t timeout_ms) {
uint8_t cmd = GD5F_CMD_GET_FEATURE;
uint8_t addr = GD5F_REG_STATUS;
uint8_t status = 0;
@@ -166,12 +175,23 @@ int gd5f_wait_busy(uint32_t timeout_ms) {
nand_ftl_wd_feed(); /* 看门狗:入口直接喂狗(覆盖 defaultTask 长 I/O 阻塞主循环喂狗点的形态) */
while (1) {
HAL_StatusTypeDef hs;
GD5F_CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, GD5F_SPI_TIMEOUT);
HAL_SPI_Transmit(&hspi1, &addr, 1, GD5F_SPI_TIMEOUT);
HAL_SPI_Receive(&hspi1, &status, 1, GD5F_SPI_TIMEOUT);
hs = HAL_SPI_Transmit(&hspi1, &cmd, 1, GD5F_SPI_TIMEOUT);
if (hs == HAL_OK) {
hs = HAL_SPI_Transmit(&hspi1, &addr, 1, GD5F_SPI_TIMEOUT);
}
if (hs == HAL_OK) {
hs = HAL_SPI_Receive(&hspi1, &status, 1, GD5F_SPI_TIMEOUT);
}
GD5F_CS_HIGH();
if (hs != HAL_OK) {
/* E-53:SPI 真故障——绝不伪造成"操作完成"(详见函数说明) */
return GD5F_ERROR;
}
if ((status & GD5F_STATUS_OIP) == 0) {
return GD5F_OK;
}
@@ -192,7 +212,7 @@ int gd5f_wait_busy(uint32_t timeout_ms) {
* 返回值:0 - 成功
* 限定条件:SPI 已初始化
* 函数说明:CS# 拉低后发 06h 命令再拉高 */
int gd5f_write_enable(void) {
int32_t gd5f_write_enable(void) {
uint8_t cmd = GD5F_CMD_WRITE_ENABLE;
GD5F_CS_LOW();
@@ -208,7 +228,7 @@ int gd5f_write_enable(void) {
* 返回值:0 - 成功
* 限定条件:SPI 已初始化
* 函数说明:发 0Fh + C0h 地址后读 1 字节状态 */
int gd5f_read_status(uint8_t *p_status) {
int32_t gd5f_read_status(uint8_t *p_status) {
uint8_t cmd = GD5F_CMD_GET_FEATURE;
uint8_t addr = GD5F_REG_STATUS;
@@ -252,7 +272,7 @@ static void gd5f_page_read_issue(uint32_t page_addr) {
* 统一记于 docs/问题记录/GD5F2GQ5UE_Trap_Records.md Trap 05,
* 复现手段见测试 Phase 11 用例。
*/
int gd5f_page_read(uint32_t page_addr) {
int32_t gd5f_page_read(uint32_t page_addr) {
int ret;
gd5f_page_read_issue(page_addr);
@@ -272,7 +292,7 @@ int gd5f_page_read(uint32_t page_addr) {
* 返回值:0 - 成功
* 限定条件:必须先调用 gd5f_page_read 完成数据加载
* 函数说明:发 0Bh + 2字节列地址 +1字节 dummy 后读取数据 */
int gd5f_read_from_cache(uint16_t column, uint8_t *p_buf,
int32_t gd5f_read_from_cache(uint16_t column, uint8_t *p_buf,
size_t size) {
uint8_t cmd[4] = {0};
uint8_t use_dma = 0U;
@@ -355,7 +375,7 @@ static int gd5f_page_program(uint32_t page_addr, uint16_t column,
* (buffer 由 SPI 数据流直接填充,无 array→buffer 竞态,
* 无需二次装载;program exec 的落盘校验由 FTL 回读完成)
*/
int gd5f_program_load(uint16_t column, const uint8_t *p_buf, size_t size) {
int32_t gd5f_program_load(uint16_t column, const uint8_t *p_buf, size_t size) {
uint8_t cmd[3];
uint8_t use_dma = 0U;
@@ -406,7 +426,7 @@ int gd5f_program_load(uint16_t column, const uint8_t *p_buf, size_t size) {
* 限定条件:已先调用 gd5f_program_load 加载数据
* 函数说明:发 10h + 3字节页地址触发编程,等待完成后检查 P_FAIL 位
*/
int gd5f_program_exec(uint32_t page_addr) {
int32_t gd5f_program_exec(uint32_t page_addr) {
int ret;
uint8_t cmd[4];
uint8_t status;
@@ -436,7 +456,7 @@ int gd5f_program_exec(uint32_t page_addr) {
* 限定条件:SPI 已初始化,ECC 已使能
* 函数说明:读取状态寄存器 ECCS[5:4] 位,0x02 表示不可纠正错误
*/
int gd5f_check_ecc(void) {
int32_t gd5f_check_ecc(void) {
uint8_t status;
gd5f_read_status(&status);
uint8_t ecc = (status >> 4) & 0x03;
@@ -448,11 +468,15 @@ int gd5f_check_ecc(void) {
* 函数功能:设置 Feature 寄存器
* 入口参数:addr - 寄存器地址 uint8_t
* data - 写入数据 uint8_t
* 返回值:0 - 成功
* 返回值:GD5F_OK - 成功;其它 - SPI 传输/等待失败错误码
* 限定条件:SPI 已初始化
* 函数说明:先写使能,发 1Fh + 地址 + 数据,等待操作完成 */
* 函数说明:先写使能,发 1Fh + 地址 + 数据,等待操作完成。
* E-53(2026-10-03):HAL 传输与 wait_busy 的返回值必须检查并上报——
* 原实现恒返回 GD5F_OK,"配置未生效"对上层不可见(E-10 同族问题)。
*/
static int gd5f_set_feature(uint8_t addr, uint8_t data) {
uint8_t cmd[3];
int ret;
gd5f_write_enable();
@@ -461,14 +485,48 @@ static int gd5f_set_feature(uint8_t addr, uint8_t data) {
cmd[2] = data;
GD5F_CS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 3, GD5F_SPI_TIMEOUT);
if (HAL_SPI_Transmit(&hspi1, cmd, 3, GD5F_SPI_TIMEOUT) != HAL_OK) {
GD5F_CS_HIGH();
return GD5F_ERROR;
}
GD5F_CS_HIGH();
gd5f_wait_busy(100);
ret = gd5f_wait_busy(100);
if (ret != GD5F_OK) {
return ret;
}
return GD5F_OK;
}
/*
* 函数功能:读取任意 Feature 寄存器(0Fh + 地址 + 读 1 字节)
* 入口参数:addr - feature 寄存器地址 uint8_t
* 出口参数:p_data - 读到的字节输出指针 uint8_t* 不为 NULL
* 返回值:GD5F_OK - 成功;GD5F_ERROR - SPI 传输失败
* 限定条件:SPI 已初始化
* 函数说明:E-10(2026-10-03):init 需**回读** B0h 校验 ECC 使能位真正生效
* (原实现只写不读,"init 返回 OK 但 ECC 未打开"→ 存储栈在无 ECC
* 保护下静默降级)。与 gd5f_read_status 同构(后者固定 C0h),
* 此处参数化地址;同样检查 HAL 返回值(E-53)。
*/
static int gd5f_private_get_feature(uint8_t addr, uint8_t *p_data) {
uint8_t cmd = GD5F_CMD_GET_FEATURE;
HAL_StatusTypeDef hs;
GD5F_CS_LOW();
hs = HAL_SPI_Transmit(&hspi1, &cmd, 1, GD5F_SPI_TIMEOUT);
if (hs == HAL_OK) {
hs = HAL_SPI_Transmit(&hspi1, &addr, 1, GD5F_SPI_TIMEOUT);
}
if (hs == HAL_OK) {
hs = HAL_SPI_Receive(&hspi1, p_data, 1, GD5F_SPI_TIMEOUT);
}
GD5F_CS_HIGH();
return (hs == HAL_OK) ? GD5F_OK : GD5F_ERROR;
}
/*
* 函数功能:块擦除(擦除128KB块)
* 入口参数:block_addr - 块编号 uint32_t 0 - 2047
@@ -476,7 +534,7 @@ static int gd5f_set_feature(uint8_t addr, uint8_t data) {
* 限定条件:gd5f2gq5ue_init() 已调用
* 函数说明:1. 写使能 - D8h + 3字节页地址 - 等待完成
* 2. 擦除完成后检查 E_FAIL */
int gd5f_block_erase(uint32_t block_addr) {
int32_t gd5f_block_erase(uint32_t block_addr) {
int ret = GD5F_OK;
uint8_t cmd[4];
uint8_t status = 0;
@@ -509,10 +567,16 @@ int gd5f_block_erase(uint32_t block_addr) {
/*
* 函数功能:扫描所有块构建 BBT(在 ECC 使能前调用)
* 入口参数:无
* 返回值:0 - 成功
* 返回值:GD5F_OK - 扫描完成;其它 - 读失败错误码(扫描已中止,s_bbt 为部分结果)
* 限定条件:SPI 已初始化,ECC 尚未使能
* 函数说明:读取每个块首页(page 0)的 spare byte 0,
* 若不为 0xFF 则为出厂坏块(符合 datasheet §12.4)
* 若不为 0xFF 则为出厂坏块(符合 datasheet §12.4)。
* E-42(2026-10-03,GD5F 错误通道统一):原实现丢弃 page_read /
* read_from_cache 的返回值且对外恒返回 GD5F_OK——SPI 读失败时
* `spare[8]` 保持上一轮的**陈旧内容**,且"整片扫描全失败也返回成功",
* 上层以为坏块表可用(rebuild 路径还会把错表持久化覆盖好副本)。
* 现:① 每轮先清零 spare(防陈旧内容误判);② 任一读失败即**中止**
* 并返回错误码,由调用方决定"放弃持久化 / 让 init 失败"。
*/
static int gd5f_bbt_scan(void) {
uint32_t block;
@@ -523,9 +587,21 @@ static int gd5f_bbt_scan(void) {
for (block = 0; block < GD5F_TOTAL_BLOCKS; block++) {
uint32_t page = block << 6;
int ret;
gd5f_page_read(page);
gd5f_read_from_cache(GD5F_PAGE_SIZE, spare, 1);
memset(spare, 0, sizeof(spare)); /* 每轮清零:防读失败时用陈旧内容误判 */
ret = gd5f_page_read(page);
if (ret != GD5F_OK) {
DBG_ERROR("BBT scan: page_read fail at block %lu (ret=%d)",
(unsigned long)block, ret);
return ret; /* 扫描失败即中止,不得对外报"扫描完成" */
}
ret = gd5f_read_from_cache(GD5F_PAGE_SIZE, spare, 1);
if (ret != GD5F_OK) {
DBG_ERROR("BBT scan: read_from_cache fail at block %lu (ret=%d)",
(unsigned long)block, ret);
return ret;
}
if (spare[0] != 0xFF) {
s_bbt[block >> 3] |= (1 << (block & 7));
@@ -557,7 +633,9 @@ void gd5f2gq5ue_bbt_clear(void) {
* 再写回持久化 BBT 池(版本 +1,掉电安全),从而清除 TC-STO-03 等运行时
* 标记的坏块。调用后建议再执行 nand_ftl_format() 清空 dhara map。
*/
int gd5f2gq5ue_bbt_rebuild(void) {
int32_t gd5f2gq5ue_bbt_rebuild(void) {
int ret;
DBG_INFO("Rebuilding BBT from factory scan...");
/* 1. 关闭 ECC 以读取原始出厂坏块标记(与 gd5f2gq5ue_init 的扫描前提一致) */
@@ -565,12 +643,21 @@ int gd5f2gq5ue_bbt_rebuild(void) {
gd5f_wait_busy(100);
/* 2. 重新扫描出厂坏块(覆盖 RAM BBT,剔除运行时注入的坏块) */
gd5f_bbt_scan();
ret = gd5f_bbt_scan();
/* 3. 重新使能 ECC */
/* 3. 重新使能 ECC(无论扫描成败都须恢复,否则后续 I/O 在无 ECC 下运行) */
gd5f_set_feature(0xB0, 0x10);
gd5f_wait_busy(100);
/* E-42(2026-10-03,数据安全优先):扫描失败 → **放弃持久化**(保留持久区
* 既有好副本),并从盘上回读 BBT 恢复 RAM 视图。原实现无论扫描成败都照常
* save(),会把"读失败拼出来的错表"覆盖掉正确副本。 */
if (ret != GD5F_OK) {
DBG_ERROR("BBT rebuild: scan FAIL (ret=%d), persist abandoned", ret);
(void)gd5f_bbt_load();
return ret;
}
/* 4. 持久化重建后的 BBT(覆盖持久化区中的脏副本) */
if (gd5f_bbt_save() != GD5F_OK) {
DBG_ERROR("BBT rebuild: persist failed");
@@ -620,7 +707,7 @@ void gd5f2gq5ue_print_bbt(void) {
* 5. 解除所有块保护 (A0h = 0x00)
* 6. 定位保留块池并加载持久化 BBT(ECC 使能后)
*/
int gd5f2gq5ue_init(void) {
int32_t gd5f2gq5ue_init(void) {
int ret = GD5F_OK;
uint8_t mid = 0;
uint8_t did = 0;
@@ -656,21 +743,55 @@ int gd5f2gq5ue_init(void) {
}
DBG_INFO("Scanning bad blocks...");
gd5f_bbt_scan();
ret = gd5f_bbt_scan();
if (ret != GD5F_OK) {
/* E-10:扫坏块失败 → init 失败,不静默继续(上层据此上报自检并降级) */
DBG_ERROR("BBT scan failed: %d", ret);
return ret;
}
DBG_INFO("Enabling ECC...");
gd5f_set_feature(0xB0, 0x10);
gd5f_wait_busy(100);
ret = gd5f_set_feature(0xB0, 0x10);
if (ret != GD5F_OK) {
DBG_ERROR("Enable ECC failed: %d", ret);
return ret;
}
/* E-10:回读 B0h 校验 ECC 使能位(bit4)**真正生效**——原实现只写不读,
* "init 返回 OK 但 ECC 未真正打开"会使存储栈在无 ECC 保护下静默降级。 */
{
uint8_t feat = 0;
ret = gd5f_private_get_feature(GD5F_REG_FEATURE, &feat);
if (ret != GD5F_OK) {
DBG_ERROR("Read B0h failed: %d", ret);
return ret;
}
if ((feat & GD5F_FEATURE_ECC_EN) == 0u) {
DBG_ERROR("ECC not enabled (B0h=0x%02X)", feat);
return GD5F_ERROR;
}
}
DBG_INFO("Unlocking block protection...");
gd5f_set_feature(GD5F_REG_PROTECT, 0x00);
gd5f_wait_busy(100);
ret = gd5f_set_feature(GD5F_REG_PROTECT, 0x00);
if (ret != GD5F_OK) {
DBG_ERROR("Unlock block protection failed: %d", ret);
return ret;
}
DBG_INFO("Locating BBT pool...");
gd5f_private_bbt_locate_pool();
ret = gd5f_private_bbt_locate_pool();
if (ret != GD5F_OK) {
DBG_ERROR("Locate BBT pool failed: %d", ret);
return ret;
}
DBG_INFO("Loading persisted BBT...");
gd5f_bbt_load();
ret = gd5f_bbt_load();
if (ret != GD5F_OK) {
/* BBT 持久副本缺失/损坏不致命(RAM BBT 已由扫描建立),仅告警 */
DBG_ERROR("Load persisted BBT failed: %d (use scanned BBT)", ret);
}
DBG_INFO("NAND init OK");
return GD5F_OK;
@@ -684,7 +805,7 @@ int gd5f2gq5ue_init(void) {
* 限定条件:SPI 已初始化
* 函数说明:发 9Fh 命令后接 1 字节 dummy + MID + DID
*/
int gd5f2gq5ue_read_id(uint8_t *p_mid, uint8_t *p_did) {
int32_t gd5f2gq5ue_read_id(uint8_t *p_mid, uint8_t *p_did) {
uint8_t cmd = GD5F_CMD_READ_ID;
uint8_t id_buf[3] = {0};
@@ -708,7 +829,7 @@ int gd5f2gq5ue_read_id(uint8_t *p_mid, uint8_t *p_did) {
* 限定条件:gd5f2gq5ue_init() 已成功调用
* 函数说明:自动处理跨页读取;offset+size 越界返回错误
*/
int gd5f2gq5ue_read(long offset, uint8_t *p_buf, size_t size) {
int32_t gd5f2gq5ue_read(long offset, uint8_t *p_buf, size_t size) {
if (offset < 0 || (uint32_t)offset + (uint32_t)size > GD5F_TOTAL_SIZE) {
return GD5F_ERROR;
@@ -751,7 +872,7 @@ int gd5f2gq5ue_read(long offset, uint8_t *p_buf, size_t size) {
* 限定条件:gd5f2gq5ue_init() 已成功调用,目标区域已擦除
* 函数说明:自动处理跨页写入;offset+size 越界返回错误
*/
int gd5f2gq5ue_write(long offset, const uint8_t *p_buf, size_t size) {
int32_t gd5f2gq5ue_write(long offset, const uint8_t *p_buf, size_t size) {
if (offset < 0 || (uint32_t)offset + (uint32_t)size > GD5F_TOTAL_SIZE) {
return GD5F_ERROR;
@@ -788,7 +909,7 @@ int gd5f2gq5ue_write(long offset, const uint8_t *p_buf, size_t size) {
* 限定条件:gd5f2gq5ue_init() 已成功调用
* 函数说明:循环擦除 [offset, offset+size) 覆盖的每个块,偏移/长度不对齐返回错误
*/
int gd5f2gq5ue_erase(long offset, size_t size) {
int32_t gd5f2gq5ue_erase(long offset, size_t size) {
if (offset % GD5F_BLOCK_SIZE != 0) {
return GD5F_ERROR;
@@ -819,7 +940,7 @@ int gd5f2gq5ue_erase(long offset, size_t size) {
* 限定条件:SPI 已初始化
* 函数说明:发 FFh 复位命令后等待 5ms
*/
int gd5f2gq5ue_reset(void) {
int32_t gd5f2gq5ue_reset(void) {
uint8_t cmd = GD5F_CMD_RESET;
GD5F_CS_LOW();
@@ -838,7 +959,7 @@ int gd5f2gq5ue_reset(void) {
* 限定条件:gd5f2gq5ue_init() 已成功调用
* 函数说明:通过 BBT 查询
*/
int gd5f2gq5ue_is_block_bad(uint32_t block) {
int32_t gd5f2gq5ue_is_block_bad(uint32_t block) {
if (block >= GD5F_TOTAL_BLOCKS) {
return 1;
}
+20 -14
View File
@@ -125,35 +125,41 @@ int32_t gd5f2gq5ue_read_id(uint8_t *p_mid, uint8_t *p_did);
/*
* 函数功能:从 NAND 读取数据(支持跨页)
* 入口参数:offset - 起始字节偏移 int32_t 0 - GD5F_TOTAL_SIZE-1
* 入口参数:offset - 起始字节偏移 long 0 - GD5F_TOTAL_SIZE-1
* buf - 数据缓冲区 uint8_t* 不为 NULL
* size - 读取字节数 uint32_t > 0
* 返回值:0 - 成功,其他 - 错误码
* size - 读取字节数 size_t > 0
* 返回值:GD5F_OK(0) - SPI 传输完成;其它 - 错误码
* 限定条件:gd5f2gq5ue_init() 已成功调用
* 函数说明:自动处理跨页读取,每次读取不超过当前页剩余空间
* 函数说明:自动处理跨页读取,每次读取不超过当前页剩余空间。
* ⚠ **raw 契约(E-56,2026-10-03 显式文档化)**:返回 0 只表示 **SPI 传输完成**,
* **不代表数据无位翻转**——ECC 不可纠正页(ECCS 非零)在 SPI 层属正常完成,
* 本函数仍返回 0。数据正确性由 **ECC 层**保证:生产主路径经 dhara glue
* (`nand_ftl.c`)自有 ECC 检查保护;**直接使用本 raw API** 的调用方
* (测试/诊断路径)须自行处理位翻转。详见
* docs/问题记录/GD5F2GQ5UE_Trap_Records.md。
*/
int gd5f2gq5ue_read(long offset, uint8_t *buf, size_t size);
int32_t gd5f2gq5ue_read(long offset, uint8_t *buf, size_t size);
/*
* 函数功能:向 NAND 写入数据(支持跨页)
* 入口参数:offset - 起始字节偏移 int32_t 0 - GD5F_TOTAL_SIZE-1
* 入口参数:offset - 起始字节偏移 long 0 - GD5F_TOTAL_SIZE-1
* buf - 数据缓冲区 uint8_t* 不为 NULL
* size - 写入字节数 uint32_t > 0
* 返回值:0 - 成功,其他 - 错误码
* size - 写入字节数 size_t > 0
* 返回值:GD5F_OK(0) - 成功;其它 - 错误码
* 限定条件:gd5f2gq5ue_init() 已成功调用,目标区域已擦除
* 函数说明:自动处理跨页写入,每次写入不超过当前页剩余空间
*/
int gd5f2gq5ue_write(long offset, const uint8_t *buf, size_t size);
int32_t gd5f2gq5ue_write(long offset, const uint8_t *buf, size_t size);
/*
* 函数功能:擦除块(按块擦除,最小单位 128KB)
* 入口参数:offset - 起始字节偏移 int32_t 必须 GD5F_BLOCK_SIZE 对齐
* size - 擦除字节数 uint32_t 必须 GD5F_BLOCK_SIZE 整数倍
* 返回值:0 - 成功,其他 - 错误码
* 入口参数:offset - 起始字节偏移 long 必须 GD5F_BLOCK_SIZE 对齐
* size - 擦除字节数 size_t 必须 GD5F_BLOCK_SIZE 整数倍
* 返回值:GD5F_OK(0) - 成功;其它 - 错误码
* 限定条件:gd5f2gq5ue_init() 已成功调用
* 函数说明:擦除操作以块为单位,offset 和 size 必须块对齐
*/
int gd5f2gq5ue_erase(long offset, size_t size);
int32_t gd5f2gq5ue_erase(long offset, size_t size);
/*
* 函数功能:复位芯片
@@ -195,7 +201,7 @@ void gd5f2gq5ue_print_bbt(void);
* 再写回持久化 BBT 池(版本 +1,掉电安全),从而清除运行时 mark 的坏块。
* 调用后建议再执行 nand_ftl_format() 清空 dhara map。
*/
int gd5f2gq5ue_bbt_rebuild(void);
int32_t gd5f2gq5ue_bbt_rebuild(void);
/*
* 函数功能:获取 dhara 可用块数(总块数减去保留块池)
+19 -8
View File
@@ -153,14 +153,20 @@ int dhara_nand_is_free(const struct dhara_nand *n, dhara_page_t p) {
fre = 0;
} else {
uint8_t buf[64];
uint32_t i;
gd5f_read_from_cache(0, buf, sizeof(buf));
fre = 1;
for (i = 0; i < sizeof(buf); i++) {
if (buf[i] != 0xFF) {
fre = 0;
break;
if (gd5f_read_from_cache(0, 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;
}
}
}
}
@@ -181,7 +187,12 @@ int dhara_nand_read(const struct dhara_nand *n, dhara_page_t p, size_t offset, s
return -1;
}
gd5f_read_from_cache((uint16_t)offset, data, length);
/* E-42 同族(2026-10-03):read_from_cache 返回值必须检查——失败时 data 内容
* 不可信,原实现仍返回 0(成功),会把未读到的数据当有效数据交给上层。 */
if (gd5f_read_from_cache((uint16_t)offset, data, length) != GD5F_OK) {
dhara_set_error(err, DHARA_E_ECC);
return -1;
}
return 0;
}
@@ -36,10 +36,10 @@
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 264,936 B (258.73 KB) | 1,048,576 B (1024 KB) | 25.3% |
| RW_IRAM1 | 84,952 B (82.96 KB) | 131,072 B (128 KB) | 64.8% |
| ER_IROM1 | 265,940 B (259.71 KB) | 1,048,576 B (1024 KB) | 25.4% |
| RW_IRAM1 | 84,960 B (82.97 KB) | 131,072 B (128 KB) | 64.8% |
| RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% |
| **Flash (ROM 合计)** | **265,668 B (259.4 KB)** | 1,048,576 B (1024 KB) | **25.3%** |
| **Flash (ROM 合计)** | **266,676 B (260.4 KB)** | 1,048,576 B (1024 KB) | **25.4%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
@@ -794,10 +794,10 @@ external pin → "pin reset" (新增)
| **5** | `task_wd_start()` 由 `defaultTask` 在 bring-up 完成后调用 | `[WD] armed:` 出现 | ✅ 完成 |
| **6** | `configASSERT` 改造 + `vApplicationAssertFailed()` | 编译 0/0 | ✅ 完成 |
| **6.5** | **(新增)** 硬件访问收敛到薄平台层(`task_watchdog.h` 末段,3 个 `__weak` 钩子)+ 主机侧单元测试(`test/host/watchdog/`,39 用例)+ pre-commit 钩子 | `run_unit_test.py` 退出码 0;`@build` 0/0(行为不变) | ✅ **2026-10-01 完成** |
| **7** | 故障注入(V2/V4):人为制造任务卡死 | 复位 + `reset cause: IWDG` | ⏳ **待台架** |
| **7** | 故障注入(V2/V4):人为制造任务卡死 | 复位 + `reset cause: IWDG` | ✅ **2026-10-03 台架**(V2/V4 均触发 IWDG 复位) |
| **8** | 模拟器 `--watchdogtest` + stub | S1 | ⏳ 待做(位图逻辑已由 U1 覆盖,优先级降低) |
| **9** | 长跑(V5,≥30 min,真实负载) | **零复位** | ⏳ **待台架** |
| **10** | 本文档改写为"现状设计说明";`待处理内容.md` N-03 销项;`README.md` 登记 | 门禁 | ⏳ 待第 7~9 步结论 |
| **9** | 长跑(V5,≥30 min,真实负载) | **零复位** | ✅ **2026-10-03 台架**(30 min / 120×FTP 零复位) |
| **10** | 本文档改写为"现状设计说明";`待处理内容.md` N-03 销项;`README.md` 登记 | 门禁 | ✅ N-03 已销项(第 7~9 步结论已入本文变更记录) |
### 10.1 第 0 步实测:CubeMX 重生成覆盖了哪 4 处
@@ -1114,3 +1114,4 @@ python scripts/check_cubemx_survival.py --write # 重建基线(确认改动
| 2026-10-01 | **查证 `fault_handler_enable()` 的 CubeMX 可行性** | 用户问"能否通过 CubeMX 配置"→ **否**。查 CubeMX 安装目录模板源码得三重证据:① `tpl_main_c.ftl` 的 `MX_NVIC_Init()` 只在 `systemHandler=="false"` 时生成 `HAL_NVIC_EnableIRQ`,而 `NVIC-STM32F417_Modes.xml` 中三个 Fault 都标 `Y`(=systemHandler)→ 永不生成使能代码;② **全部模板中 `SHCSR` 只出现 1 次**且是 MPU 清理的 `&= ~MEMFAULTENA`(禁用),**无任何 `\|=` 置位**;③ `.ioc` 无 MPU(`Mcu.IP` 列表),且 `HAL_MPU_Enable` 只置 `MEMFAULTENA` 一位。实测印证:`HAL_MspInit()` 里三个 Fault 一行代码都没生成。结论:只能手工写,但属 **L2**(`USER CODE BEGIN 1` 区)→ 重生成不丢,有审计项兜底。另新增警示:`.ioc` 的 `NVIC.MemoryManagement_IRQn=true` **不代表已使能**(第 1 段只是"向量可见")。写入 §10.2 与四个 Fault 采集小节 |
| 2026-10-01 | **新增 §7.3 主机侧单元测试(L0.5)** | 硬件访问收敛到薄平台层(`App/util/task_watchdog.h` 末段声明 3 个 `__weak` 钩子:喂狗/启动/取 tick),MCU 端行为零变化(`@build` 0/0)。新建 `test/host/watchdog/`(`watchdog_test.c` 39 用例 + `run_unit_test.py` MSVC 运行器 + `stub/` 三桩头),`#include` 生产源码以保证测的是真代码。挂 pre-commit `watchdog-unit-test` 钩子。新增 `docs/测试说明/看门狗单元测试说明.md`;`App/util/README.md`、`test/host/README.md`、`测试开发标准流程.md`(新增 L0.5 层)同步。顺带改善:`s_last_tick` 由函数局部 static 提为文件 static(可测性 + 模块状态归一)。§5.1 文件表与 §10 步骤表登记;§7.2 S1 优先级降低(位图逻辑已由 U1 覆盖) |
| 2026-10-02 | **P0 根因修复:IWDG 超时被静默配成复位默认值(比设计短 5 倍)** | round 3~8 反复出现的"空 STOR(0 字节截断)→ IWDG 误复位"**根因不是"poller 自身停摆"**。真因:`Src/iwdg.c` 为实现**延后启动**(§4.5 决策 C)删掉了 `HAL_IWDG_Init()`(`IWDG-DEFER-START-ANCHOR`),而 `HAL_IWDG_Init()` 是**唯一**会把 `hiwdg.Init.Prescaler/Reload` 写进 IWDG 寄存器的路径——删掉后 `MX_IWDG_Init()` 只填结构体,`WD_IWDG_PRESCALER`/`WD_IWDG_RELOAD` 沦为**死宏**,寄存器停在**复位默认值** `PR=0`(÷4)/`RLR=4095`。实测(SWD 回读)实际超时 = 4×4096/LSI ≈ **349~512 ms**,而设计是 64×1250/LSI ≈ **1701~2498 ms**。两个后果:① `WD_MISS_TOLERANCE`(8×100ms=800ms)**永远等不到**就被 IWDG 复位 → **停喂判定分支成死代码**(这正是 §7"STALL 从未执行"的真正原因,当时据此误判为"poller 停摆");② 任何 >400ms 的心跳缺口都直接误复位(空 STOR 的 FTL flush 使 NET/FTP/UI 心跳缺 ~400ms)。**修复**:`wd_platform_start()`(`__weak`,**不在 CubeMX 生成区**,重生成不丢)补上按设计值配置:解锁(`KR=0x5555`) → `PR` → 等 `SR.PVU` 清 → `RLR` → 等 `SR.RVU` 清 → `KR=0xCCCC`(不等同步会静默丢弃后写的那只,SWD 实测)。`.map` 证实 `Removing stm32f4xx_hal_iwdg.o(i.HAL_IWDG_Init), (94 bytes)`,故这是全固件**唯一**写 PR/RLR 的点。**验证**:SWD 现场改回设计值后,同一空 STOR 由 **5/5 复位 → 100% 成功**;固件烧录后 3 次开机回读均为 `PR=4/RLR=1249`。`@build` 0/0;本地 CI L0+L0.5 全绿(278/278 断言) |
| 2026-10-03 | **V2/V4/V5 台架验收闭环(N-03 销项)** | 三用例全部通过。**V2 任务饿死**:`netTask` 循环插 `while(1)` → 800ms 声明 `[WD] task stall: mask=0x00000001 req=0x00000037`,≤2.5s 触发 IWDG 复位(`reset cause: IWDG`,`RCC_CSR=0x24000000`)。⚠️ 首轮实测**不复位**(stall 已声明、`miss` 累至 1157≈115s):根因是**停摆证据落盘自身喂狗**——`wd_stall_evidence()`→`sys_log_evidence()` 在停喂窗口里每 100ms 轮都写 NAND(`/log/wdev.log` + `SYSLOG_RUN` flush 诱导),经 `nand_ftl_wd_feed()` **直接重装 IWDG**,"停喂"语义被证据机制抵消。修复:加 `s_stall_evidenced` **每 episode 门闩**(每轮停摆只落盘一次,此后不再触碰 NAND)→ 复测复位正常。**V4 关中断死循环**:`__disable_irq()` 后插 `while(1)` → 设备每 ~4.9s 稳定复位(20+ 周期,`reset cause: IWDG`),证实 IWDG **独立于中断**(PRIMASK 屏蔽 TIM7/SysTick/PendSV 仍能复位)。**V5 长期不误杀**:FTP 512KB 上/下传 + `e02_probe` 网络探针持续 **30 min** → **120/120 次 FTP 传输全 PASS(MATCH)、零 IWDG 复位**(唯一复位为起始烧录的 `software (NVIC_SystemReset)`)。Step 4 堆告警全程未触发(`min_ever=4728 B` > 3072 B)。**遗留观察(已登记 `待处理内容.md`)**:V5 期间 44 次 `task stall`(`mask=0x00000001` 持续 ≥800ms)**100% 与 `e02_probe` 的 30s 挂起客户端→`time_sync` 掉线+监听重建(`listen FAIL`×2,约 4s)重合**;隔离实验证实「仅 FTP(无探针)120s = 0 次 stall」→ 该停摆为**探针场景专属**,非一般负载误杀。因缺口(~1s)< IWDG 最坏超时(1701ms)而未复位,与 §4.3 包含关系一致 |
+1 -1
View File
@@ -81,7 +81,7 @@
| 序 | 任务 | 说明 | 出处 |
|---|---|---|---|
| **C1** | **pending 二期(ACCEPT + SEND)** | 沿一期机制扩展;同期消化 E-25(net_select 语义)、E-24/E-27/E-28(跨上下文收口);基线重锁 + 探针族扩充 + 看门狗接 selftest。**设计基线见下方 §2.1** | 第二档 #2 / N-4 |
| **C2** | **GD5F 错误通道统一** | E-10/E-42/E-53/E-54/E-56/E-57 一次做齐:`wait_busy` 区分超时/真故障、`bbt_scan` 失败**放弃持久化**(保护好副本)、init 全链路检查 + 重试、回读 B0h 校验 ECC | 第二档 #1 |
| ~~**C2**~~ | **GD5F 错误通道统一** | ✅ **已闭环(2026-10-03)**:E-10/E-42/E-53/E-54/E-56/E-57 一次做齐——`wait_busy` 区分超时/真故障、`bbt_scan` 失败**放弃持久化**(保护好副本)、init 全链路检查、回读 B0h 校验 ECC;台架套件回归 `16/16 PASSED (failed=0)` | 第二档 #1 |
| **C3** | **存储侧日志管线**(消化 N-2) | 49s 全系统日志静默的复现与定位(NAND GC 长尾 vs UART 卡滞)+ 队列满丢弃计数/告警(现静默丢行);与 S8(非阻塞日志)合并评估 | N-2 / S-1 |
| **C4** | **defaultTask 栈余量**(S-1) | 2872B/4096(余量 29.9%,差 4.8B 未达 30% 判据)→ 栈 4096→4608(CCM 堆余量可覆盖)或 GC 链局部削减 | S-1 |
| **C5** | 一档剩余 + P2 收口 | E-49(RS485 DE 收尾)、E-15(ringbuf 定去留——**已确认零调用者,倾向删除**)、E-61(SD2506 注释订正)、E-16(cfg_file 段名长度)、E-26(UDP 定案,倾向移除)、E-23、E-30 | 一档/二档 |
@@ -413,7 +413,7 @@ App/util/ringbuf.c 154 行 → 零 `#include "ringbuf.h"`,不在 Keil 工
> **本文增量**:本项目 CI 是**自建 Gitea 内网实例**,"能否拉外层 action"是现实约束——
> 建议**优先做零依赖方案**(步骤 2/3),而不是先引入 `upload-artifact`。
### 3.4 S4:复核"文档用例 ↔ 代码断言"覆盖矩阵 🟡
### 3.4 S4:复核"文档用例 ↔ 代码断言"覆盖矩阵 ✅ **已落地(2026-10-03)**
**问题**:文档声称 91 个 TC、代码有 222 个断言点,但**两者是否一一对应无人校验**。
`测试开发标准流程.md` §2.3 铁律 3 要求"每条设计原则必须有对应用例",
@@ -433,7 +433,11 @@ scripts/check_test_traceability.py
**完成判据**:能一键列出双向差异清单;差异收敛后可考虑升为门禁。
### 3.5 S5:给 `test/` 代码加"宽松"静态检查 🟡
> **✅ 2026-10-03 落地**:`scripts/check_test_traceability.py` 已实现并跑通,
> **默认仅告警(exit 0)**。当前输出 **30 处差异**(26 处"文档有、代码无" +
> 4 处"代码有、文档无":`TC-GD5F-1100/1101/1102`、`TC-STO-06`)。**暂不升门禁**。
### 3.5 S5:给 `test/` 代码加"宽松"静态检查 ✅ **已落地(2026-10-03)**
**目标**:抓 `test/board/` 6300+ 行固件测试代码的**真实缺陷**(未初始化/越界/空指针),
但不引入存量 style 噪音。
@@ -453,6 +457,11 @@ scripts/check_test_traceability.py
> 本建议**不是推翻它**,而是"在不产生大量噪音的前提下,补上致命缺陷的检查"。
> 若基线告警过多,**保持现状亦可接受**——此时应把该决策写得更明确("理由:存量告警 N 条")。
> **✅ 2026-10-03 落地**:走**路径 2**——基线实测**0 条 warning**(存量可控),
> 已加 `test-cppcheck` 钩子(`.pre-commit-config.yaml`,`--enable=warning`
> + 排除 `test/host/*/stub/` 模拟器桩)。`pre-commit run test-cppcheck --all-files`
> **PASS**;新增 `test/board/` 代码的未初始化/越界/空指针现被自动拦截。
### 3.6 S6:测试构建可编译性纳入验证(= 上游 P1-10 增量)🟡
**目标**:防止"测试套件长期编不过而无人知"(2026-08~09 CH395F 套件实例)。
@@ -515,7 +524,7 @@ scripts/check_test_traceability.py
**完成判据**:文件不存在;`App/util/README.md` 同步;`@build` 0/0(本就不参与构建,应无影响);
全库无 `crc.h` / `ringbuf.h` 引用(E-15 销项)。
### 4.2 S9(原 A-2):统一 GD5F 错误通道(= 上游 C2 骨架)🔴
### 4.2 S9(原 A-2):统一 GD5F 错误通道(= 上游 C2 骨架)✅ **已闭环(2026-10-03)**
见 §2.3 的四要素骨架。**实施顺序建议**:
@@ -639,17 +648,18 @@ scripts/check_test_traceability.py
| | S2 | L0.5 适格台账 | S1-a 经验 | 小 | ✅ 2026-10-02 |
| | S10 | 启动段可观测性核查 | 无 | 小 | ✅ 2026-10-02(补 `sd2506_init` 6 条 + `app_main` 3 条;Flash +904 B) |
| | S6 | 测试构建可编译性(方案 A) | 无 | 小 | 🔄 2026-10-02 |
| **第 3 波**(架构加固) | S7 | 真机回归加自检判定 | 无 | 中 | ⏸ 判据需真机 |
| | S11 | 堆低水位告警 | 无 | 小 | ⏸ 判据需真机 |
| | S9 | GD5F 错误通道(= 上游 C2) | 无 | 大 | ⏸ 判据需真机 |
| | S5 | `test/` 宽松静态检查 | 先跑基线 | 小~中 |
| | S4 | 用例追溯脚本 | 无 | 中 |
| **第 3 波**(架构加固) | S7 | 真机回归加自检判定 | 无 | 中 | ✅ 2026-10-02(`@test` PASS 7/7) |
| | S11 | 堆低水位告警 | 无 | 小 | ✅ 2026-10-03(V5 长跑未触发,基线 min_ever=4728 B) |
| | S9 | GD5F 错误通道(= 上游 C2) | 无 | 大 | ✅ 2026-10-03(台架套件回归 16/16) |
| | S5 | `test/` 宽松静态检查 | 先跑基线 | 小~中 | ✅ 2026-10-03(`test-cppcheck` 钩子,`--enable=warning`) |
| | S4 | 用例追溯脚本 | 无 | 中 | ✅ 2026-10-03(`scripts/check_test_traceability.py`,默认告警) |
| **第 4 波**(按需) | S12 | 模块级断言宏 | 试点验证 | 小(试点) |
| | S13 | 模拟器差距清单 | 无 | 小 | ✅ 2026-10-02 |
> **"⏸ 判据需真机"说明**(2026-10-02 补):S7 / S11 / S9 的**代码可离线编写**,但其**完成判据**
> **"⏸ 判据需真机"说明**(2026-10-02 补;**2026-10-03 台架窗口三项全部闭环**):S7 / S11 / S9 的**代码可离线编写**,但其**完成判据**
> 均要求真机观测(`@test` 捕获自检失败 / 台架长跑观察到堆告警 / GD5F 套件回归 PASS)。
> 在无 MCU 的环境下实施会留下"已改未验证"的债,故押后至有实机的窗口。
> ✅ **2026-10-03 台架**:S7(`@test` PASS 7/7)、S11(V5 长跑未触发,基线 `min_ever=4728 B`)、S9(GD5F 套件 16/16 PASS)全部闭环。
### 5.2 与上游任务的关系
+3 -2
View File
@@ -10,7 +10,7 @@
| # | 事项 | 发现 | 证据与现状 | 出处 |
|---|---|---|---|---|
| N-2 | **49s 全系统日志静默(defaultTask/NAND syslog 排空停滞)**——串口日志 49s 无行(testTask 10s 周期行/netTask 事件行/[STACK] 行全缺),期间 net 栈照常处理外部连接(FTP banner 92ms)→ 日志管线(defaultTask 排空 → NAND+UART)停滞,队列满后各任务日志行被丢;系统功能不受损 | 2026-09-22 17:23:52~17:24:41(L3 台架 F2 运行中,gen 181) | **A/B 归因:L3 不触碰 defaultTask/syslog 路径 → 既有存储侧现象**。待办:① 复现条件定位(NAND 写/GC 长尾 vs UART TX 卡滞——对照 `CH395F` 无关的 SD2506 侧日志)② 日志队列满的丢弃计数/告警(现静默丢行,排障不可见)③ 与 S8(非阻塞日志)合并评估 | `错误码传播审计.md` E-02 阶段二"台架新发现②";`链路延迟测试说明.md` §6.7 注 |
| N-2 | **49s 全系统日志静默(defaultTask/NAND syslog 排空停滞)**——串口日志 49s 无行(testTask 10s 周期行/netTask 事件行/[STACK] 行全缺),期间 net 栈照常处理外部连接(FTP banner 92ms)→ 日志管线(defaultTask 排空 → NAND+UART)停滞,队列满后各任务日志行被丢;系统功能不受损 | 2026-09-22 17:23:52~17:24:41(L3 台架 F2 运行中,gen 181) | **A/B 归因:L3 不触碰 defaultTask/syslog 路径 → 既有存储侧现象**。**2026-10-03 V5 核实(降级为低风险观察项)**:① 丢弃可观测性**代码已有**——`console_log.c` 的 `s_con_drop` 计数 + 排空末尾 `[ERR] CONSOLE dropped N lines` + `dbg_console_dropped()`;`sys_log_mcu.c` 队列满/preboot 满分别 `DBG_ERROR("queue full/preboot full, drop: …")`;UART TX 卡滞由**僵尸通道自愈**覆盖(`con_uart_reinit` + `s_con_tx_fault`)→ "现静默丢行、排障不可见"**已不成立**;② V5 30 min 日志间隔分析**未复现 49s**(最大静默 41s 且在**开机/烧录边界**;运行期 24 处 >10s 静默**全为 ~28s、均在 `e02_probe` 的 time_sync 30s 窗口内**= N-17 同源)。残余:③ 与 S8(非阻塞日志)合并评估;若再现,借 `[ERR] CONSOLE dropped` 计数定位卡点 | `错误码传播审计.md` E-02 阶段二"台架新发现②";`链路延迟测试说明.md` §6.7 注;`实机测试实施指南.md` §7.2 |
| N-3 | **L5 队列往返基线(1223/988µs)的 L3 后重验**——L5 测 DONTWAIT 队列全程;L3 对该路径逐位不变、pending tick 无 pending 时为 µs 级指针读 → 机制上无影响,但基线数值属分片时代,严格重锁需跑一轮 Phase 13(需 PC peer) | 2026-09-22(L3 收尾留档) | 低风险:10ms/100ms 阈值余量 >8×;下次 Phase 13 窗口顺带重锁即可 | `链路延迟测试说明.md` §6.4 L5 注¹ |
## 存储 / 日志管线
@@ -30,12 +30,13 @@
|---|---|---|---|
| N-4 | **pending 机制二期:ACCEPT + SEND 阻塞路径 pending 化**——E-06(accept 轮询计时失真)、E-20(lftpd 控制通道 errno 分类/501)、E-21(timeSync errno 分级)剩余分量共用该机制扩展;届时 L3/L5 基线再重锁、探针族扩充(close/reconfig/EOF/disconnect-during-pending)、看门狗接 selftest 留痕、模拟器差距清单 +1 | 一期(RECV)已落地并入主线:`a45c0cb`(+文档闭环 `7ff2958`);架构现状见 `Net层阻塞模型架构设计.md`,二期设计基线与门禁见 `下一步任务规划.md` §3.1 | 同左 + `错误码传播审计.md` 第二档 #2 |
| N-16 | **A2/A4 专项场景台架验证待办**(正常路径已实测零回归,2026-10-03):① PHY 拔线负向测试——断链需断 **DTU 侧**线缆或关**受管交换机端口**(拔交换机侧线缆链路仍 UP,CH395 对交换机拓扑),断链后复位验证 `PHY: DISCONNECT` + 自检台账 `comms fault` 降级放行;② A2 芯片级 `net_init` 失败分支(需 SPI 故障注入,验证 `net init fail` 自检 + 30s 重试恢复);③ A4 短写压测(需远显客户端 + UI 推流压力,验证 `remo send short` 告警/阻断/重连恢复) | 2026-10-03(台架;台架拓扑 = CH395↔交换机,拔线操作受限) | `实机测试实施指南.md` §10(已完成事项与证据去向) |
| N-17 | **`time_sync` 30s 挂起客户端掉线 + 监听重建期间,网络任务心跳缺口达 800ms~1s(声明 stall 但未复位)**——2026-10-03 V5 长跑(FTP+e02_probe,30 min)期间 44 次 `task stall: mask=0x00000001`(NET/FTP/UI/TIMESYNC 四位同时缺席 ≥800ms),**100% 与 `e02_probe` 的 30s 挂起客户端触发 `time_sync: no TIME packet in 30s, drop conn` → 监听重建(`time_sync: listen FAIL`×2,约 4s)重合**;隔离实验(仅 FTP、无探针,120s)**0 次 stall** → 该停摆为**探针场景专属**。缺口(~1s)< IWDG 最坏超时(1701ms)故未复位,但余量仅 ~0.7s。**待办**:定位监听重建为何阻塞 NET/FTP/UI(疑 CH395F socket 释放 / `net_listen` 重试期间的队列头阻塞,与 E-02 同源);评估缩短重建重试(5×2s)或让掉线路径不阻塞其它任务 | 2026-10-03(V5 台架;证据 `/tmp/v5_soak_serial_20261003.log`) | `实机测试实施指南.md` §4;`子系统设计/看门狗设计说明.md` 2026-10-03 变更记录 |
## 可靠性与自恢复
| # | 事项 | 说明 | 出处 |
|---|---|---|---|
| N-03 | **引入独立看门狗(IWDG)+ 任务心跳位图**——现状全库无看门狗(HAL 驱动源码也不在仓库),任何形态死机不可自恢复;另 `configASSERT` 为关中断死循环(连日志都不打)。**代码已落地并编译通过(2026-09-29)**:CubeMX 生成 IWDG(Prescaler=64/Reload=1249)+ `App/util/task_watchdog.{c,h}` 心跳位图 + 6 任务埋点 + `RCC_CSR` 复位原因锁存 + `configASSERT` 改造 | **剩台架验证**(故障注入 V2/V4、长跑 V5 ≥30 min);文档 §10 第 7~10 步待办 | **见 [`子系统设计/看门狗设计说明.md`](子系统设计/看门狗设计说明.md)**(§10 有实施状态表) | `审查记录/项目审查与优化建议报告.md` N-03a/b/c |
| N-03 | **引入独立看门狗(IWDG)+ 任务心跳位图**——现状全库无看门狗(HAL 驱动源码也不在仓库),任何形态死机不可自恢复;另 `configASSERT` 为关中断死循环(连日志都不打)。**代码已落地并编译通过(2026-09-29)**:CubeMX 生成 IWDG(Prescaler=64/Reload=1249)+ `App/util/task_watchdog.{c,h}` 心跳位图 + 6 任务埋点 + `RCC_CSR` 复位原因锁存 + `configASSERT` 改造 | ✅ **台架验证已完成(2026-10-03)**:V2(任务饿死)声明 stall 后 ≤2.5s `reset cause: IWDG`;V4(关中断死循环)每 ~4.9s 稳定 IWDG 复位(独立于中断);V5(30 min 真实负载)120/120 次 FTP PASS、零复位。文档 §10 第 7~10 步已闭环 | **见 [`子系统设计/看门狗设计说明.md`](子系统设计/看门狗设计说明.md)**(§10 有实施状态表) | `审查记录/项目审查与优化建议报告.md` N-03a/b/c |
| N-03a | `configASSERT(x)` = `taskDISABLE_INTERRUPTS(); for(;;);`(`Inc/FreeRTOSConfig.h`)→ 断言时无任何输出 | ✅ **已修(2026-09-29)**:改为 `vApplicationAssertFailed(__FILE__, __LINE__)`,打印文件名+行号后主动复位(`Src/freertos.c`);宏已迁入 `USER CODE BEGIN 1` 区防重生成丢失 | 同上 N-03b |
| N-03b | 启动段(`app_main_init` + `defaultTask` bring-up)在新方案下**无看门狗保护** | 显式代价(未变):`看门狗设计说明.md` §4.5 决策 C + §9 R1;该段死机表现为"无输出 + LED 不闪" | 同上 N-03c |
| N-03c | **CubeMX 重生成会覆盖 `USER CODE` 标记区之外的定制** | ⚠️ **2026-09-29 两次重生成实测确认**:首次覆盖 4 处、第二次覆盖 5 处(P0 `Src/iwdg.c` 自动启动、P1 `HardFault_Handler` 打回空死循环、P1 `FreeRTOSConfig.h` 两个加固宏被删、P2 `freertos.c` 中文注释)。✅ **已建立自动检出**:`scripts/check_cubemx_survival.py`(18 项清单 + 哈希基线,已挂 pre-commit)——第二次实测**准确检出全部 5 处**。⚠️ 关键发现:CubeMX **换了一代中断模板**,"生成区外的自造标记区"同样不被保留(详见设计文档 §10.1)。**今后每次重生成后跑一次该脚本即可** | 本次实施记录 |
@@ -3,6 +3,13 @@
> 本规范参照 `docs/测试说明/CH395F_Test_Guide.md` 结构编写,覆盖 `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c`
> 全部公开/私有函数,并向上延伸到 FTL(`nand_ftl.c`)与 FatFS diskio。
> 详细芯片行为见 `docs/芯片手册/GD5F2GQ5UExxG.md`,已知陷阱见 `docs/问题记录/GD5F2GQ5UE_Trap_Records.md`。
>
> **2026-10-03 错误通道硬化**(E-10/E-42/E-53/E-54/E-56/E-57,见 `问题记录/GD5F2GQ5UE_Trap_Records.md` Trap 06):
> `wait_busy` 区分"超时"(`GD5F_BUSY_TIMEOUT`)与"真故障"(`GD5F_ERROR`)、`bbt_scan` 失败**放弃持久化**、
> init 全链路检查返回值 + **回读 B0h 校验 ECC 使能**、`read` 的 raw 契约文档化、公开 API 统一 `int32_t`。
> 套件回归 `=== GD5F2GQ5UE Tests: 16/16 PASSED (failed=0) ===`。
> ⚠ **编排器注意**:`TC-GD5F-704` 会**预期**打出 `[ERR][NAND] RX DMA buffer not DMA-reachable`(回落轮询,用例 PASS)——
> 用 `run_all_suites.py` 跑本套件时 `--forbidden` 须去掉 `[ERR]`(脚本已按套件覆盖,见其 `SUITES` 注释)。
## 1. 概述
+11 -4
View File
@@ -84,17 +84,24 @@ MDK-ARM\build.bat
- `sd2506_test_run()` 由测试调度器调用,先 init 并记录原始时间,最后用
`sd2506_set_time` 把板子时间**恢复**为测试前的值(不污染真实时钟)。
- 输出形如:
- 输出形如(**只读模式**,默认):
```
[RTC_TEST] === SD2506 Phase 1: basic read/write (public API) ===
[RTC_TEST] [PASS] sd2506_init() ret=0 (expect 0)
...
[RTC_TEST] === Phase 1 basic: 6/6 PASSED (failed=0) ===
[RTC_TEST] === Phase 1 basic: 3/3 PASSED (failed=0) ===
[RTC_TEST] === SD2506 Phase 2: full API coverage ===
...
[RTC_TEST] === Phase 2 API: 9/9 PASSED (failed=0) ===
[RTC_TEST] === Phase 2 API: 6/6 PASSED (failed=0) ===
[RTC_TEST] === RTC Tests: 9/9 PASSED (failed=0) ===
```
- 通过标准:所有 Phase 的 `failed=0`,且总量 `X/Y PASSED` 中 Y 与 PASS 数相等。
- **各 Phase 汇总行只反映本阶段**(进入各 Phase 前统计自清零);末尾
`=== RTC Tests: N/M PASSED (failed=0) ===` 是**套件级**汇总,也是
`run_all_suites.py --require-suite-summary "RTC Tests"` 的判定标记
(见 `test_common.h` / `测试开发标准流程.md` §3.2)。
- **破坏性模式**(`ENABLE_DESTRUCTIVE_TESTS`)额外跑 TC-RTC-002 / 014 / 015,
计数变为 Phase 1 = 6/6、Phase 2 = 11/11、套件级 = 17/17。
- 通过标准:所有 Phase 与套件级的 `failed=0`,且每行 `X/Y PASSED` 中 X = Y。
## 6. 失败排查
+83 -40
View File
@@ -23,11 +23,11 @@
|---|---|---|---|
| **步骤 1** | 环境预检(P0-A,每次上台架必跑) | 常设 | §2 |
| **步骤 2** | 产品 `@test` 闭环 + 自检结果判定(P0-B) | ✅ 已闭环 | §3 |
| **步骤 3** | IWDG 故障注入 V2/V4 + 长跑 V5(P1-A / N-03) | ⬜ 待执行 | §4 |
| **步骤 4** | 堆低水位告警实测(P1-B / S11) | ⬜ 待执行 | §5 |
| **步骤 5** | GD5F 错误通道回归(P2-A / S9) | ⬜ 待执行 | §6 |
| **步骤 6** | 网络稳定性与基线重锁(P2-B / P1-8 + N-2/N-3) | ⬜ 待执行 | §7 |
| **步骤 7** | 工程卫生(P3 / S5 + S4 + 待核实项) | ⬜ 待执行 | §8 |
| **步骤 3** | IWDG 故障注入 V2/V4 + 长跑 V5(P1-A / N-03) | ✅ 已闭环 | §4 |
| **步骤 4** | 堆低水位告警实测(P1-B / S11) | ✅ 已闭环 | §5 |
| **步骤 5** | GD5F 错误通道回归(P2-A / S9) | ✅ 已闭环 | §6 |
| **步骤 6** | 网络稳定性与基线重锁(P2-B / P1-8 + N-2/N-3) | ◐ 部分闭环(7.1/7.2 ✅;7.3 待 PC peer) | §7 |
| **步骤 7** | 工程卫生(P3 / S5 + S4 + 待核实项) | ✅ 已闭环 | §8 |
> **每步出口**:`@build` 0/0 → 一轮 `@test` PASS → ST-LINK 全片回读 `No difference found` →
> 上游登记销项 + 文末变更记录留痕。
@@ -46,7 +46,7 @@
| A2 | 产品构建 0/0 | `& "MDK-ARM\build.bat"` | 末行 `BUILD_OK: 0 errors, 0 warnings` |
| A3 | 构建产物为**产品态** | 检查 `test/board/test_config.h` L30-33 | 四个 `TEST_SUITE_*` **全部注释** |
| A4 | `.map` 非测试态 | 看 `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map` 的 `ER_IROM1` | 产品态基线随已接受的修复增长:262,100(初版)→ 262,836 B(10-02 round 1~6)→ 264,284 B(round 7~9)→ **264,936 B(当前,含 A2/A3/A4 补码)**;明显阶跃 ≈3.7KB 及以上(10-02 实测产品 262,836 → 套件 266,540)= 停在套件态 → 重跑 A2 |
| A5 | 全量静态门禁 | `pre-commit run --all-files` | **16/16 Passed** |
| A5 | 全量静态门禁 | `pre-commit run --all-files` | **17/17 Passed**(2026-10-03 实测;含新增 S5 `test-cppcheck`) |
| A6 | ST-LINK 探针可见 | `& "MDK-ARM\reset.bat" list`(CLI 经 `DTU_STLINK_CLI` / PATH 解析) | 列出探针 SN `0B250D12A218303030303032` |
| A7 | 串口端口正确 | 检查 `.opencode/serial_config.json` 的 `portName` | 端口单一配置源 = 此文件(当前 COM6,2026-09-21 台架重插后由 COM7 变更);变更后先杀旧 daemon 再 `open` |
| A8 | 串口助手 daemon 可用 | `& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py status` | 返回 JSON,`open: true/false` 可读 |
@@ -97,7 +97,11 @@
---
## 4. 步骤 3:IWDG 故障注入与长跑(P1-A,N-03 收尾) · ⬜ 待执行
## 4. 步骤 3:IWDG 故障注入与长跑(P1-A,N-03 收尾) · ✅ 已闭环
> **2026-10-03 台架结论**(实施细节与证据见 `子系统设计/看门狗设计说明.md` 变更记录):
> V2/V4/V5 三用例全部通过——V2(任务饿死)声明 stall 后 ≤2.5s `reset cause: IWDG`;V4(关中断死循环)每 ~4.9s 稳定 IWDG 复位(IWDG 独立于中断);V5(30 min 真实负载)**120/120 次 FTP 传输全 PASS、零 IWDG 复位**。
> **遗留观察**:V5 期间 44 次 stall 声明 100% 与 `e02_probe` 探针的 time_sync 30s 掉线+监听重建重合;隔离实验(仅 FTP 120s)**0 次 stall** → 探针场景专属,非一般负载误杀(已登记 `待处理内容.md`)。
> 来源:`子系统设计/看门狗设计说明.md` §10 步骤 7~10;`待处理内容.md` N-03。
> 判定状态机与 IWDG 配置(PR/RLR 按设计值写入)已落地并经真机验证(2026-10-02,`930215f`,
@@ -149,7 +153,9 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L
---
## 5. 步骤 4:堆低水位告警实测(P1-B,S11) · ⬜ 待执行
## 5. 步骤 4:堆低水位告警实测(P1-B,S11) · ✅ 已闭环
> **2026-10-03 台架结论**:告警代码(`default_task.c` 的 `HEAP_LOW_WATER_B=3072` 门闩 + `[HEAP]` 告警 + syslog 自检)已落地;复用 §4.2 V5 窗口 30 min 长跑,**全程未触发**(`min_ever=4728 B` > 3072 B)→ 记录"当前负载下 min_ever 仍高于阈值"为基线,已更新 `内存管理/RAM使用分析与优化方案.md`。
**现状证据**:2026-10-03 上板复测 `free=8,688 / min_ever=3,712~4,728 B`
(两次开机,运行间波动 ≈1KB;初始 `configTOTAL_HEAP_SIZE` 48KB)——
@@ -178,7 +184,11 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L
---
## 6. 步骤 5:GD5F 错误通道统一(P2-A,S9 = 上游 C2) · ⬜ 待执行
## 6. 步骤 5:GD5F 错误通道统一(P2-A,S9 = 上游 C2) · ✅ 已闭环
> **2026-10-03 台架结论**:四要素(分类/透传/分级/留痕)+ 返回类型统一 `int32_t` 已落地(E-10/E-42/E-53/E-54/E-56/E-57 全部销项,见 `错误码传播审计.md`)。
> 套件回归:`test/run_all_suites.py --suites gd5f` → **`=== GD5F2GQ5UE Tests: 16/16 PASSED (failed=0) ===`**(只读模式,4 个破坏性用例按设计跳过);编排器 `[GD5F] -> PASS`。
> **顺带修复(编排器缺陷,非本次代码引入)**:`run_all_suites.py` 原对所有套件统一 `--forbidden "HardFault;assert;[ERR]"`,而 GD5F 的 `TC-GD5F-704`(CCM 非 DMA 可达缓冲)会**预期**打出 `[ERR][NAND] RX DMA buffer not DMA-reachable ... fallback poll`(用例 PASS)→ GD5F 套件**永远被判 FAIL**(自 `2055de0` 起)。按 `verify_log.py` 的说明改为**按套件覆盖 `--forbidden`**(GD5F = `HardFault;assert`),修复后 PASS。
> **来源**:`错误码传播审计.md` 第二档 #1(E-10/E-42/E-53/E-54/E-56/E-57);
> `测试与架构改进建议.md` §2.3 / §4.2 四要素骨架。
@@ -224,9 +234,9 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L
---
## 7. 步骤 6:网络稳定性与基线重锁(P2-B) · ⬜ 待执行
## 7. 步骤 6:网络稳定性与基线重锁(P2-B) · ◐ 部分闭环(7.1/7.2 已闭环;7.3 待 PC peer)
### 7.1 P1-8 网络重建失败恢复(A2;实施细节见 `网络/Net层阻塞模型架构设计.md`,状态见 §10)
### 7.1 P1-8 网络重建失败恢复(A2;实施细节见 `网络/Net层阻塞模型架构设计.md`,状态见 §10) · ✅ 正常路径已闭环
**判据**:拔插网线/改 IP 触发重建后——
- 失败路径**写 syslog 自检**(不再零日志);
@@ -235,31 +245,50 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L
**验证手段**:`test/host/e02_probe.py`(网络探针)+ UI 网口设置页改 IP。
### 7.2 N-2:49s 全系统日志静默复现与定位
**2026-10-03 台架结论**:
- **正常路径 ✅**:V5 长跑(30 min,FTP 512 KB 上/下传)**120/120 PASS、零 IWDG 复位**,
网络重建后的 FTP/time_sync 服务据代次号重建监听、功能正常(证据 `/tmp/v5_soak_serial_20261003.log`)。
- **负向路径(拔线/芯片级 `net_init` 失败)**受**台架拓扑**限制(CH395↔交换机,
拔交换机侧线缆链路仍 UP;芯片级失败需 SPI 故障注入)→ 已登记 `待处理内容.md` **N-16**
(① PHY 拔线负向 ② A2 `net_init` 失败分支 ③ A4 短写压测),需专项窗口。
### 7.2 N-2:49s 全系统日志静默复现与定位 · ✅ 判据②已满足 / 判据①未复现
**现状证据**(登记簿 N-2):2026-09-22 台架实测,串口日志 **49s 无行**
(testTask/netTask/[STACK] 行全缺),期间 net 栈照常处理外部连接 → **日志管线停滞**,
队列满后各任务日志行被丢;系统功能不受损。
**目标**:复现条件定位(NAND 写/GC 长尾 vs UART TX 卡滞)+ 队列满的丢弃计数/告警
(**现静默丢行,排障不可见**)。
**判据**:
- 复现 49s 静默并定位到具体卡点(NAND GC / UART TX);
- 实现**队列满丢弃计数**(至少一个 `DBG_ERROR` 或计数器经 UI/日志可见)。
**判据与核实(2026-10-03)**:
- **判据②(队列满丢弃计数/告警)→ ✅ 代码已满足**:日志管线**已有**显式丢弃可观测性——
`App/log/console_log.c` 的 `s_con_drop` 计数 + 排空末尾 `[ERR] CONSOLE dropped %lu lines`
提示 + `dbg_console_dropped()` 访问器;`App/log/sys_log_mcu.c` 的队列满/preboot 满分别
打 `DBG_ERROR("queue full, drop: …")` / `("preboot full, drop: …")`。另 UART TX 卡滞
已由**僵尸通道自愈**覆盖(`con_uart_reinit()` + `[WARN] CONSOLE tx fault: UART reinit`,
`s_con_tx_fault` 计数)。**"现静默丢行、排障不可见"已不成立**。
- **判据①(复现 49s 并定位卡点)→ 未复现**:对 V5 30 min 长跑日志(6031 条带时间戳行)
做间隔分析,**最大静默 41s**(发生在**开机/烧录边界**,非运行期),**运行期无 49s 静默**。
运行期 >10s 的静默共 24 处,**全部**为 ~28s,且**均落在 `e02_probe` 的 time_sync 30s
挂起客户端窗口内**——与 **N-17** 同源(探针场景专属,非通用负载)。
- **结论**:49s 静默在当前固件 + V5 负载下**不复现**;可观测性缺口已由既有代码补齐。
N-2 降级为**低风险观察项**(保留登记,若再现则借 `[ERR] CONSOLE dropped` 计数定位)。
**执行步骤**:长跑(§4.2 V5)期间周期性打点 + 对照 `CH395F` 无关的 SD2506 侧日志判别卡点。
### 7.3 N-3:L5 队列往返基线重验
### 7.3 N-3:L5 队列往返基线重验 · ⬜ 待 PC peer
**现状**:L5 基线(1223/988µs)属**分片时代**;L3 对该路径逐位不变 → 机制上无影响,
但严格重锁需跑一轮 Phase 13(**需 PC peer**)。
**判据**:Phase 13 窗口跑一轮,基线数值与阈值余量(>8×)对照,重锁数值。
**2026-10-03 台架**:本窗口**无 PC peer**,Phase 13 无法执行 → 保留待办(低风险:
阈值余量 >8×,机制上 L3 对该路径逐位不变)。
---
## 8. 步骤 7:工程卫生(P3) · ⬜ 待执行
## 8. 步骤 7:工程卫生(P3) · ✅ 已闭环
> S5 / S4 落地(离机可做),待核实项已实机核实并修复。本节留复跑命令与判据。
### 8.1 S5:`test/` 代码宽松静态检查
@@ -281,6 +310,11 @@ cppcheck --enable=warning --error-exitcode=1 test/ 2>&1 | tee test_cppcheck_base
**判据**:`test-cppcheck` 在 CI PASS;新增测试代码越界/未初始化被自动拦截。
> **注意**:若基线告警过多,**保持现状亦可接受**——此时应把该决策写明确("理由:存量 N 条")。
**结论(✅ 已闭环,2026-10-03)**:已加 `test-cppcheck` 钩子(`.pre-commit-config.yaml`
`id: test-cppcheck`,**仅开 `warning`、排除模拟器桩/第三方头**)。基线可控,
`pre-commit run test-cppcheck --all-files` **PASS**。新增 `test/board/` 代码的
未初始化/越界/空指针问题现由钩子自动拦截。
### 8.2 S4:用例追溯脚本
**现状**:文档声称 91 个 TC、代码有 222 个 `TEST_CHECK`(断言点数 > 用例数是正常的),
@@ -293,30 +327,37 @@ cppcheck --enable=warning --error-exitcode=1 test/ 2>&1 | tee test_cppcheck_base
**判据**:一键列出双向差异清单;差异收敛后再考虑升为门禁。
### 8.3 待核实项:`TEST_SUITE_SELECTED` 未含 `TEST_SUITE_RTC`
**结论(✅ 已闭环,2026-10-03)**:`scripts/check_test_traceability.py` 已落地并跑通,
**默认仅告警(exit 0)、不阻断提交**。当前输出 **30 处差异**(26 处"文档有、代码无" +
4 处"代码有、文档无":`TC-GD5F-1100/1101/1102`、`TC-STO-06`)。差异清单已收敛为
可追踪项,后续按需补登记;**暂不升门禁**(与 §8.2 目标一致)。
**现状证据**(`test/board/test_config.h`):
- L45 互斥检查**已包含** `TEST_SUITE_RTC`(4 个套件互斥正确);
- **L49-51 的 `TEST_SUITE_SELECTED` 只含前三个**(CH395F / STORAGE / GD5F),**未含 RTC**。
### 8.3 待核实项:`TEST_SUITE_SELECTED` 未含 `TEST_SUITE_RTC` · ✅ 已核实(非缺陷)
**疑似影响**:`TEST_SUITE_SELECTED` 被用于 `defaultTask` 是否空闲、是否创建 `testTask` 等分支——
若 RTC 套件构建下 `TEST_SUITE_SELECTED` 未定义,**testTask 可能不被创建 → RTC 套件不执行**。
但 `run_all_suites.py --check-build` 实测 RTC 套件**能编译通过**(那只是编译,不是运行)。
**原疑点**(`test/board/test_config.h`):派生标记 `TEST_SUITE_SELECTED` 只含
CH395F / STORAGE / GD5F,未含 RTC;怀疑 RTC 构建下 `testTask` 不被创建 → 套件不执行。
**判据**:有实机时,选 `TEST_SUITE_RTC` 构建并烧录,观察:
- 串口是否出现 `=== RTC Tests: N/M PASSED ===` 汇总行;
- 若**无汇总行** → 证实缺陷,`TEST_SUITE_SELECTED` 需补 `|| defined(TEST_SUITE_RTC)`;
- 若**有汇总行** → 说明另有分支覆盖,属"有意为之",在 `test_config.h` 注释说明。
**2026-10-03 实机核实结论**:
- **`TEST_SUITE_SELECTED` 是死代码**:`grep -rn TEST_SUITE_SELECTED` 全仓**仅本文件有定义、
无任何使用点**。`Src/freertos.c` **无条件创建 `testTask`**;`test/board/test_config.c`
的 `start_test_task()` **直接按 `TEST_SUITE_RTC` 等宏分支**(不经 `TEST_SUITE_SELECTED`)。
→ "testTask 不被创建 → RTC 不执行"的推断**不成立**。
- **RTC 套件确实执行**:选 `TEST_SUITE_RTC` 构建烧录后,串口出现完整 RTC 输出
(`SD2506 test suite started` … `Phase 1 basic: 3/3 PASSED` … `Phase 2 API: 6/6 PASSED`)。
- **真正的缺口 = 缺套件级汇总行**:原 `sd2506_test_run()` 只打 Phase 级汇总,**没有**
`=== RTC Tests: N/M PASSED (failed=0) ===` → `run_all_suites.py --require-suite-summary "RTC Tests"`
判 **FAIL**(设备实际 12/12 全过)。属**判据缺口**,非设备缺陷。
**执行步骤**:
```bash
# 1) 改 test_config.h:取消注释 TEST_SUITE_RTC(其余保持注释)
& "MDK-ARM\build.bat"
# 2) 烧录(注意 reset.bat flash 后必须 -Rst -Run)
& "MDK-ARM\reset.bat" flash MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex
# 3) 抓串口日志,grep 汇总行
# 4) 还原 test_config.h 为产品态 + 重建产品固件(避免 .map 污染)
```
**修复(2026-10-03)**:
1. `test/board/sd2506_test_task.c`:各 Phase 进入前**自清零**,末尾输出套件级
`=== RTC Tests: N/M PASSED (failed=0) ===`(复用 `SD2506_TEST_REPORT` 宏,格式与
`test_common.h` 统一)。顺带修正 Phase 级汇总行**累计**问题——原 Phase 2 显示的是
含 Phase 1 的累计值(9/9),现各阶段独立:Phase 1 = 3/3、Phase 2 = 6/6、套件级 = 9/9。
2. `test/board/test_config.h`:`TEST_SUITE_SELECTED` **补齐 RTC** 并注释其"当前无引用",
防止将来真有分支复用时重演"漏 RTC"。
**复跑命令**:`& '.venv\Scripts\python.exe' test/run_all_suites.py --suites RTC`
**判据**:`[RTC] -> PASS`、`VERDICT: PASS`,日志含 `=== RTC Tests: 9/9 PASSED (failed=0) ===`。
---
@@ -410,6 +451,8 @@ cppcheck --enable=warning --error-exitcode=1 test/ 2>&1 | tee test_cppcheck_base
| 日期 | 变更 |
|---|---|
| 2026-10-03 | **步骤 5/6/7 闭环**:§6 GD5F 错误通道回归 PASS(`=== GD5F2GQ5UE Tests: 16/16 PASSED (failed=0) ===`;E-10/42/53/54/56/57 全部销项;顺带发现并修复编排器判据缺口——GD5F 的 TC-GD5F-704 故意触发 `[ERR]` 回落轮询,`run_all_suites.py` 改为**按套件**覆盖 `--forbidden`,与 `verify_log.py` 说明一致)→ §1/§6 改「✅ 已闭环」。§7 网络稳定性:**7.1 正常路径 ✅**(V5 120/120 FTP、零复位;负向路径转 `N-16`,台架拓扑受限);**7.2 N-2 ✅判据②/未复现判据①**(丢弃可观测性代码已有:`console_log.c` `s_con_drop`+`[ERR] CONSOLE dropped`、`sys_log_mcu.c` 队列满 `DBG_ERROR`、TX 僵尸通道自愈;V5 30 min 日志间隔分析最大静默 41s 且在开机边界、运行期 24 处 >10s 静默**全为 ~28s 且均在 `e02_probe` 窗口内**=N-17 同源)→ N-2 降级低风险观察项;**7.3 N-3 ⬜ 待 PC peer** → §1/§7 改「◐ 部分闭环」。§8 工程卫生:S5 `test-cppcheck` 钩子 PASS(仅 warning,排除模拟器桩);S4 `scripts/check_test_traceability.py` 落地并跑通(**30 处差异**,默认 warn-only);**待核实项 `TEST_SUITE_SELECTED` 实机核实 = 死代码**(全仓无使用点;`start_test_task()` 直接按宏分支,RTC 套件实际正常执行;真缺口 = 缺 `=== RTC Tests: N/M PASSED ===` 汇总行,已补 `test/board/sd2506_test_task.c` 并修正 Phase 级汇总累计问题;`test/board/test_config.h` 补齐 RTC 并注释"当前无引用")→ §1/§8 改「✅ 已闭环」 |
| 2026-10-03 | **步骤 3/4 闭环**:V2/V4/V5 台架验收通过(V2 任务饿死声明 stall 后 ≤2.5s `reset cause: IWDG`;V4 关中断死循环每 ~4.9s 稳定 IWDG 复位;V5 30 min / **120×FTP 全 PASS、零 IWDG 复位**)→ §1 总览与 §4 状态改「✅ 已闭环」,实施细节与证据移入 `子系统设计/看门狗设计说明.md` 变更记录。S11 堆告警代码落地、V5 全程未触发(`min_ever=4728 B` > 3072 B)→ §5 改「✅ 已闭环」,基线入 `内存管理/RAM使用分析与优化方案.md`。新登记 `待处理内容.md` **N-17**(V5 期间 44 次 stall 与 `e02_probe` 的 time_sync 30s 掉线+监听重建重合;隔离实验「仅 FTP 120s = 0 stall」证实探针场景专属) |
| 2026-10-03 | **重构为纯执行流程**:确立单一组织逻辑——本文 = **按顺序执行**的上板测试流程(步骤 1~7 + 状态标记),参考内容(§9 执行注意 / §10 已完成事项去向 / §11 引用闭环)后置。已完成事项(S7、A1~A4)的实施细节与真机证据**移出本文**,§10 只留「事项 → 去向」指针;§3 只留复跑命令与判据;§1「事项总览」改「执行流程总览」(步骤/状态列) |
| 2026-10-03 | **§3.2 去"待上板"框架**:A1~A4 均已落地并(A1 全项 / A2~A4 正常路径)经真机验证,删除"待上板事项(代码状态 2026-10-02 核对)"表述,改写为**客观事实记录**(列名改为「实施内容(已落地)」+「验证判据」);§1 总览 P0-B 状态由「待上板」改「✅ 已闭环」;抬头与 `docs/README.md` 索引同步为「真机验证状态」。残留事实(A2/A3/A4 故障分支 + A4 推流压测需专项窗口、N-16 拔线负向测试)仍如实保留 |
| 2026-10-02 | **更名**:`实机测试规划.md` → `实机测试实施指南.md`(与文档性质对齐——常设执行判据,非规划;引用面改造后仅剩 `docs/README.md` 索引 1 处,同步更新) |
@@ -196,3 +196,38 @@ if (ok) {
一次实验即可排除/坐实整个故障域,远胜在上层猜机制
- 探针自身也可能有 bug:错误码要记录"是哪次调用返回的 + 枚举语义"——
本例 fr=6 在日志里挂了几十个 boot 没人查过枚举表
## Trap 06 — 轮询路径"伪完成":HAL 失败被伪装成操作完成(已修复,2026-10-03)
**发现时间**:2026-10-03(错误码传播审计 E-10/E-42/E-53/E-54/E-56/E-57 复核 + 台架套件回归)
**现象/机制**:
`gd5f_wait_busy` 内 3 次 HAL 调用(`HAL_SPI_Transmit` / `HAL_SPI_Receive`)返回值**全部丢弃**,
失败时局部 `status` 保持初值 `0` → `(status & GD5F_STATUS_OIP) == 0` 立即成立 →
**"操作完成"被伪造出来**。SPI 故障下后续命令在未就绪的芯片上执行,
最终表现为"静默报成功但数据没落盘"。
**同族缺口**:
- `gd5f_bbt_scan` 丢弃 `gd5f_page_read` / `gd5f_read_from_cache` 返回值,且 `spare[8]`
跨轮不清零 → 读失败时用**陈旧内容**判坏块;整片扫描全失败仍返回 `GD5F_OK`,
`bbt_rebuild` 照常 `gd5f_bbt_save()` → 可能把**错表**持久化覆盖正确副本(数据安全优先项)。
- init 全链路(扫坏块 / 使能 ECC / 解块保护 / 定位保留池)大量忽略返回值 → 返回 `GD5F_OK`
但 ECC 未真正打开 → 存储栈在**无 ECC 保护**下运行(数据完整性底线静默降级)。
- 公开 API 返回类型 `int` / `int32_t` 混用(码值均为小负数,无功能影响)。
**修复(2026-10-03)**:
- `gd5f_wait_busy` 检查三次 HAL 调用,失败即返 `GD5F_ERROR`(与 `GD5F_BUSY_TIMEOUT` 区分)。
- `gd5f_bbt_scan` 返回错误码、每轮 `spare` 清零、读失败中止;`bbt_rebuild` 扫描失败
**放弃持久化**(`gd5f_bbt_load()` 回滚 RAM 后直接返回,不 `bbt_save`)。
- init 全链路检查返回值;init 后**回读 B0h 校验 ECC 使能(bit4)**,未使能即报错。
- `gd5f2gq5ue_read` 的 raw 契约在头文件显式文档化("0 = SPI 传输完成,正确性由 ECC 层保证")。
- 公开 API 统一 `int32_t`;`nand_ftl.c` 2 处未检查的 `gd5f_read_from_cache` 补检查。
**验证**:台架套件回归 `=== GD5F2GQ5UE Tests: 16/16 PASSED (failed=0) ===`(只读模式)。
**经验教训**:
- "看起来被检查了"(调用点写了 `ret = wait_busy(...)`)**不等于**真检查——
**函数内部**不区分"超时"与"HAL 失败"时,伪完成会优先于超时返回成功;
- 坏块表这类**持久化状态**,扫描失败时必须"放弃持久化"而非"照常保存"——
保护既有好副本比刷新更重要;
- 使能类配置(ECC)必须**回读校验**,不能只信写命令的返回。
+7 -6
View File
@@ -46,6 +46,7 @@
> 它是本次审计中唯一"并发条件下会说谎"的通道,且 pending 二期不覆盖它,故独立小修先行。
>
> **2026-10-03 变更**:**E-34 / E-04 已修复并通过真机验证**(CH395F 字节命令有限超时 + `remo_send` 返回值校验;见 §5 第一档)。
> **2026-10-03 变更②**:**GD5F 错误通道统一(E-10 / E-42 / E-53 / E-54 / E-56 / E-57)已修复并通过台架套件回归**(`=== GD5F2GQ5UE Tests: 16/16 PASSED (failed=0) ===`,只读模式;见 §5 第二档 #1)。
> 真机全闭环 `@test` PASS 7/7;执行判据见 `测试说明/实机测试实施指南.md` §3 / §10。
---
@@ -217,7 +218,7 @@ E-57(`int` / `int32_t` 混用)。
- **机制**:扫坏块、使能 ECC、解除块保护、定位保留池等步骤大量调用点忽略返回值。
- **后果**:init 返回 `GD5F_OK` 但实际配置未生效(例如 ECC 未真正打开),
存储栈在**无 ECC 保护**下运行 → 数据完整性底线静默降级。
- **当前状态(复核)**:**仍然存在**。`gd5f_wait_busy` 自身有 `int` 返回值
- **当前状态**:✅ **已修复(2026-10-03)**——init 全链路检查返回值(见 §5 第二档 #1)。修复前复核:**当时仍然存在**。`gd5f_wait_busy` 自身有 `int` 返回值
(`gd5f2gq5ue.c:147`,超时返 `GD5F_BUSY_TIMEOUT`),但 init 与 BBT rebuild 里
至少 5 处(`:548` / `:552` / `:643` / `:647` / `:1000`)直接丢弃返回值;
`gd5f_bbt_scan()` 内部的 `gd5f_page_read` / `gd5f_read_from_cache`(`:505-506`)同样未检查。
@@ -236,7 +237,7 @@ E-57(`int` / `int32_t` 混用)。
- **后果**:SPI 读失败时局部 8 字节 `spare[8]` 保持**上一轮的陈旧内容**,
按 `spare[0] != 0xFF` 判定 → 把好块判成坏块(保守,可接受)或反之。
更严重的是**整片扫描全失败时也返回成功**,上层以为坏块表可用。
- **当前状态(复核)**:**仍然存在**。调用点 `:548`(BBT rebuild)与 `:639`(init)
- **当前状态**:✅ **已修复(2026-10-03)**——`bbt_scan` 检查读返回值 + 每轮 `spare` 清零 + 读失败中止,`bbt_rebuild` 扫描失败**放弃持久化**(见 §5 第二档 #1)。修复前复核:**当时仍然存在**。调用点 `:548`(BBT rebuild)与 `:639`(init)
都直接忽略返回,rebuild 路径随后照常 `gd5f_bbt_save()` → 可能把**错表**持久化覆盖正确副本。
- **建议**:扫描中任一读失败即中止并返回错误;`bbt_scan` 增加 `spare[8]` 每轮清零;
`gd5f2gq5ue_bbt_rebuild` 在扫描失败时必须**放弃持久化**(保留既有好副本)。
@@ -253,7 +254,7 @@ E-57(`int` / `int32_t` 混用)。
**"操作完成"被伪造出来**。
- **后果**:SPI 故障 → 状态字节读到垃圾 → 误判操作完成 → 后续命令在未就绪的芯片上执行,
最终表现为"静默报成功但数据没落盘"。
- **当前状态(复核)**:**仍然存在**。`wait_busy` 的返回值形式上是"看起来被检查了"
- **当前状态**:✅ **已修复(2026-10-03)**——`gd5f_wait_busy` 内检查三次 HAL 调用,失败即返回 `GD5F_ERROR`(与 `GD5F_BUSY_TIMEOUT` 区分;见 §5 第二档 #1)。修复前复核:**当时仍然存在**。`wait_busy` 的返回值形式上是"看起来被检查了"
(很多调用点确实写了 `ret = gd5f_wait_busy(...)`),但**函数内部不区分"超时"与"HAL 失败"**,
且 HAL 失败造成的伪完成会**优先于**超时返回 `GD5F_OK`。
- **建议**:`gd5f_wait_busy` 内检查三次 HAL 调用,失败即返回独立错误码(`GD5F_ERROR`);
@@ -269,7 +270,7 @@ E-57(`int` / `int32_t` 混用)。
- **机制**:`gd5f_read_from_cache` 的返回值语义是"**SPI 传输完成**",不是"**数据正确**"。
ECC 不可纠正页(ECCS 非零)在 SPI 层是正常完成的,因此函数返回 `0`(成功)。
- **后果**:raw API 使用者会拿到**带有未纠正位翻转**的数据并被告知成功。
- **当前状态(复核)**:**仍然存在,缺口范围本轮已精确确认。**
- **当前状态**:✅ **已修复(2026-10-03)**——按建议 ② 在 `gd5f2gq5ue.h` 的 `read` 声明处**显式文档化 raw 契约**("0 = SPI 传输完成,正确性由 ECC 层保证";见 §5 第二档 #1)。修复前复核:**当时仍然存在,缺口范围本轮已精确确认。**
生产主路径是 **dhara glue 层**(`nand_ftl.c`),它自己有 ECC 检查保护,所以是安全的;
缺口只存在于**直接调用公开 raw API 的场景**。真正的消费者目前只有 `test/` 测试套件与诊断路径。
- **建议**:两条路选一条——① `gd5f2gq5ue_read` 增加 ECCS 回读并在不可纠正时返回错误码;
@@ -441,7 +442,7 @@ E-57(`int` / `int32_t` 混用)。
| **E-15** | `App/util/ringbuf.c` | ringbuf 实现存在但**全项目零调用者**(本轮 `grep` 确认除自身外无引用) | **仍然存在** | 修复后启用,或直接删除。**保留一个零调用者的工具模块是负债**,建议本轮就定去留 |
| **E-23** | `test/` | 测试套件缺代次检查(accept 失败分支);TC704 并发度偏高 | **仍然存在** | 加代次断言;TC704 降并发或在测试构建抑制服务任务 |
| **E-30** | `simulator/sim/net_socket_sim.c` | sim 后端 errno 语义与 MCU 不对齐(成功即清 errno,MCU 不清) | **仍然存在**(语义差异在 §6 表中仍成立) | 对齐语义;并在缺陷记录中标明"sim 复现盲区" |
| **E-57** | `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h:119/140/189/222-253` | 公开 API 返回类型 `int` / `int32_t` 混用(码值均为小负数,无功能影响) | **仍然存在** | 统一为 `int32_t`,随 GD5F 错误通道改造一同进行 |
| **E-57** | `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h:119/140/189/222-253` | 公开 API 返回类型 `int` / `int32_t` 混用(码值均为小负数,无功能影响) | ✅ **已修复(2026-10-03)**:公开 API 全部统一为 `int32_t` | 见 §5 第二档 #1 |
| **E-60** | `Drivers/BSP/SD2506/sd2506.c/h` | `SD2506_CHARGE_5K` 宏位与手册 §4.8 不符的风险 | **已订正**(2026-09-21,`41e111b`):宏位已改为 `bit1`,18H 回读校验在位 | 无需改动,仅作为"手册总表 vs 专用表不一致"的案例保留 |
| **E-61** | `Drivers/BSP/SD2506/sd2506.c:482` | `sd2506_clear_int` 注释称"ARST=1 时自动清除 INTAF",实际 ARST 在 **CTR3**(11H)而代码从未写过 CTR3 | **仍然存在(纯注释错误)** | 修注释:明确"ARST 未启用,INTAF 靠读 CTR1 清除" |
| **E-62** | `Lib/FatFs/ffconf.h` | `FF_USE_LFN=0` → 严格 8.3 短名;自愈逻辑若把 `FR_INVALID_NAME` 归入损坏触发集 → 不可逆重格式化(`.fschk` 无限重格式化事故根因) | **铁律已固化**(`AGENTS.md`);`ff.c` `create_short` 全部拒绝点已加 DEBUG 打印 | 无需改动,回归监督项(与 §3.2-E-31 后续同族) |
@@ -487,7 +488,7 @@ E-57(`int` / `int32_t` 混用)。
| # | 项 | 内容 |
|---|---|---|
| 1 | **GD5F 错误通道统一**(E-10 / E-42 / E-53 / E-54 / E-56 / E-57) | `gd5f_wait_busy` 内检查三次 HAL 调用并区分"超时"与"真故障";`bbt_scan` 检查读返回值 + **扫描失败时放弃持久化**(保护好副本);init 全链路检查返回值 + 重试 1~2 次;init 后回读 B0h 校验 ECC 使能(bit4);`gd5f2gq5ue_read` 的 raw 契约文档化或加 ECCS 检查;返回类型统一 `int32_t`。**注意 RAM 预算**:新增错误分支不得引入新的大缓冲 |
| 1 | ~~**GD5F 错误通道统一**(E-10 / E-42 / E-53 / E-54 / E-56 / E-57)~~ | **已修复并通过台架套件回归(2026-10-03)**:`gd5f_wait_busy` 内检查三次 HAL 调用并区分"超时"(`GD5F_BUSY_TIMEOUT`,可重试)与"真故障"(`GD5F_ERROR`);`gd5f_bbt_scan` 检查读返回值 + 每轮 `spare` 清零 + 读失败中止,`bbt_rebuild` 扫描失败时**放弃持久化**(保留好副本);init 全链路检查返回值;init 后**回读 B0h 校验 ECC 使能(bit4)**;`gd5f2gq5ue_read` 的 raw 契约文档化(E-56);返回类型统一 `int32_t`(E-57);`nand_ftl.c` 2 处未检查的 `gd5f_read_from_cache` 补检查。套件回归 `=== GD5F2GQ5UE Tests: 16/16 PASSED (failed=0) ===` ✅ |
| 2 | **N-4 二期:ACCEPT + SEND 阻塞路径 pending 化** | 沿用一期(RECV)机制扩展;同期消化 **E-25**(`net_select` 语义)、**E-24 / E-27 / E-28**(跨上下文裸指针收口为状态查询 API)。基线重锁 + 探针族扩充 + 看门狗接 selftest 留痕 |
| 3 | **E-16** | 在 `cfg_file.h` 显式定义段名 / 键名最大长度,注册 API 拒绝超长名字(把余量变成编译期可验证);`snprintf` 返回值改检查式 |
| 4 | **E-26** | UDP 路径定案:并入队列 / 移除 / 文档化"单任务独占"。倾向 **移除**(无用户) |
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""check_test_traceability.py - S4 用例追溯校验(默认仅告警,不阻断提交)。
背景(docs/审查记录/测试与架构改进建议.md §3.5 / 实机测试实施指南.md §8.2):
文档声称 91 个 TC、代码有 222 个 TEST_CHECK(断言点数 > 用例数正常),
但**双向是否一一对应无人校验**——某个 TC 可能"文档写了、代码没实现",
或"代码实现了、文档没登记"。纯靠人肉比对。
本脚本做什么:
提取两侧的 TC 编号集合,输出双向差异:
① 代码侧:`test/board/*.c`(含 `*_test_task.c` 与 `net_latency_test.c`)
里出现的 `TC-<PFX>-<NNN>`(注释、DBG_INFO 串皆计入);
② 文档侧:`docs/测试说明/*_Test_Guide.md` 里出现的 `TC-<PFX>-<NNN>`。
[DOC-ONLY] 文档有、代码无 -> 未实现(或代码未标注)
[CODE-ONLY] 代码有、文档无 -> 未登记(或文档漏写)
作用域**只取 `*_Test_Guide.md`**:TC-CFG / TC-CLK / TC-WD 属主机侧单元
测试文档(`配置文件单元测试说明.md` 等),不参与板级追溯,天然被排除。
退出码:
0 = 无差异;或有差异但**默认仅告警**(与 doc-links 演进路径一致)
1 = 有差异且显式 `--strict`
2 = 环境错误(目录/文件缺失)
用法(仓库根目录):
& '.venv\\Scripts\\python.exe' scripts/check_test_traceability.py
& '.venv\\Scripts\\python.exe' scripts/check_test_traceability.py --strict
& '.venv\\Scripts\\python.exe' scripts/check_test_traceability.py --verbose
设计依据:docs/审查记录/测试与架构改进建议.md §3.5;docs/测试说明/实机测试实施指南.md §8.2
创建日期:2026-10-03
"""
import argparse
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
CODE_DIR = REPO_ROOT / "test" / "board"
DOC_DIR = REPO_ROOT / "docs" / "测试说明"
# TC 编号:TC-<PFX>-<NNN>。<PFX> 允许字母数字(NET/GD5F/STO/RTC…)
TC_RE = re.compile(r"TC-[A-Za-z0-9]+-\d+")
# 占位符前缀(文档模板里的 `TC-XXX-…`)不参与比对
PLACEHOLDER_PREFIXES = {"XXX"}
def _prefix(tc: str) -> str:
return tc.rsplit("-", 1)[0]
def collect(paths, label):
"""返回 (ids_set, per_prefix_counts)。逐文件扫描,缺文件不致命。"""
ids = set()
for p in paths:
try:
text = p.read_text(encoding="utf-8", errors="replace")
except OSError as e: # pragma: no cover - 环境错误
print(f"[ERR] 无法读取 {p}: {e}", file=sys.stderr)
return None, None
for m in TC_RE.findall(text):
if _prefix(m) in PLACEHOLDER_PREFIXES:
continue
ids.add(m)
counts = {}
for i in sorted(ids):
counts[_prefix(i)] = counts.get(_prefix(i), 0) + 1
return ids, counts
def main():
ap = argparse.ArgumentParser(description="S4 用例追溯校验(TC 编号双向比对)")
ap.add_argument("--strict", action="store_true",
help="有差异时返回退出码 1(默认仅告警,返回 0)")
ap.add_argument("--verbose", action="store_true",
help="额外打印各前缀的 TC 计数")
ap.add_argument("--code-dir", default=str(CODE_DIR))
ap.add_argument("--doc-dir", default=str(DOC_DIR))
args = ap.parse_args()
code_dir = Path(args.code_dir)
doc_dir = Path(args.doc_dir)
code_files = sorted(code_dir.glob("*.c")) if code_dir.is_dir() else []
doc_files = sorted(doc_dir.glob("*_Test_Guide.md")) if doc_dir.is_dir() else []
if not code_files:
print(f"[ERR] 未找到代码侧文件:{code_dir}/*.c", file=sys.stderr)
return 2
if not doc_files:
print(f"[ERR] 未找到文档侧文件:{doc_dir}/*_Test_Guide.md", file=sys.stderr)
return 2
code_ids, code_counts = collect(code_files, "code")
doc_ids, doc_counts = collect(doc_files, "doc")
if code_ids is None or doc_ids is None:
return 2
doc_only = sorted(doc_ids - code_ids)
code_only = sorted(code_ids - doc_ids)
print("=== S4 用例追溯校验(TC 编号双向比对)===")
print(f"代码侧:{len(code_files)} 个文件,{len(code_ids)} 个 TC")
print(f"文档侧:{len(doc_files)} 个文件,{len(doc_ids)} 个 TC")
print(f"交集:{len(code_ids & doc_ids)}")
if args.verbose:
print("--- 各前缀 TC 计数(代码 / 文档)---")
for pfx in sorted(set(code_counts) | set(doc_counts)):
print(f" {pfx}: code={code_counts.get(pfx, 0)} doc={doc_counts.get(pfx, 0)}")
print()
print(f"[DOC-ONLY] 文档有、代码无(未实现/代码未标注):{len(doc_only)}")
for i in doc_only:
print(f" {i}")
print(f"[CODE-ONLY] 代码有、文档无(未登记/文档漏写):{len(code_only)}")
for i in code_only:
print(f" {i}")
if not doc_only and not code_only:
print("\n[OK] 双向一一对应,无差异")
return 0
print(f"\n[WARN] 共 {len(doc_only) + len(code_only)} 处差异"
f"(默认仅告警,不阻断提交;收敛后再考虑升为门禁)")
return 1 if args.strict else 0
if __name__ == "__main__":
sys.exit(main())
+11 -7
View File
@@ -233,8 +233,6 @@ uint8_t lat_probe_armed(void)
void lat_probe_arm(const char *seg, uint32_t max_n, uint32_t spike_th_us)
{
uint32_t i;
if (max_n > (uint32_t)LAT_MAX_SAMPLES) {
max_n = (uint32_t)LAT_MAX_SAMPLES;
}
@@ -245,13 +243,19 @@ void lat_probe_arm(const char *seg, uint32_t max_n, uint32_t spike_th_us)
s_probe_mx = 0U;
s_probe_spike_th = spike_th_us;
s_probe_armed = 0U; /* 最后置位:arm 完成前 netTask 不写入 */
for (i = 0U; i < sizeof(s_probe_seg) - 1U; i++) {
s_probe_seg[i] = seg[i];
if (s_probe_seg[i] == '\0') {
break;
/* 有界拷贝:先取源串实际长度(全部调用点均为字符串字面量),再夹到目标容量
* 以内。原实现"按目标容量逐字节读源 + 遇 '\0' break"虽对字面量安全,但
* cppcheck 无法证明源串提前终止 → 报 arrayIndexOutOfBounds(S5,2026-10-03);
* 改为 strlen+memcpy 后既有界又可证。 */
{
size_t n = strlen(seg);
if (n > sizeof(s_probe_seg) - 1U) {
n = sizeof(s_probe_seg) - 1U;
}
memcpy(s_probe_seg, seg, n);
s_probe_seg[n] = '\0';
}
s_probe_seg[i] = '\0';
s_probe_armed = 1U;
}
+25 -1
View File
@@ -223,14 +223,31 @@ void sd2506_test_run(void)
}
#endif
memset(&g_sd2506_test_stats, 0, sizeof(g_sd2506_test_stats));
/* 套件级累计:每个 Phase 进入前自清零,阶段结束后把本阶段结果累加进来。
* 目的有二:
* 1) 各 Phase 的 `=== Phase N ... ===` 汇总行只反映"本阶段"的断言数,
* 不再出现"Phase 2 显示的是含 Phase 1 的累计值"这种易误读输出;
* 2) 末尾输出套件级 `=== RTC Tests: N/M PASSED (failed=0) ===` 汇总行——
* 它是 run_all_suites.py --require-suite-summary 的判定标记
* (见 test_common.h / docs/测试说明/测试开发标准流程.md §3.2)。 */
uint16_t suite_total = 0U;
uint16_t suite_passed = 0U;
uint16_t suite_failed = 0U;
#ifdef ENABLE_RTC_BASIC_TESTS
memset(&g_sd2506_test_stats, 0, sizeof(g_sd2506_test_stats));
sd2506_phase_basic();
suite_total += g_sd2506_test_stats.total;
suite_passed += g_sd2506_test_stats.passed;
suite_failed += g_sd2506_test_stats.failed;
#endif
#ifdef ENABLE_RTC_API_TESTS
memset(&g_sd2506_test_stats, 0, sizeof(g_sd2506_test_stats));
sd2506_phase_api();
suite_total += g_sd2506_test_stats.total;
suite_passed += g_sd2506_test_stats.passed;
suite_failed += g_sd2506_test_stats.failed;
#endif
#ifdef ENABLE_DESTRUCTIVE_TESTS
@@ -241,6 +258,13 @@ void sd2506_test_run(void)
}
#endif
/* 套件级汇总行:名称固定 "RTC Tests",与 run_all_suites.py 的
* --require-suite-summary 约定一致。复用公共报告宏,保证格式统一。 */
g_sd2506_test_stats.total = suite_total;
g_sd2506_test_stats.passed = suite_passed;
g_sd2506_test_stats.failed = suite_failed;
SD2506_TEST_REPORT("RTC Tests");
DBG_INFO("SD2506 test suite done");
for (;;) {
osDelay(1000);
+8 -1
View File
@@ -46,7 +46,14 @@ extern "C" {
#error "测试套件互斥:一次构建只能启用一个 TEST_SUITE_*"
#endif
#if defined(TEST_SUITE_CH395F) || defined(TEST_SUITE_STORAGE) || defined(TEST_SUITE_GD5F)
/* 派生标记:当前**仅作语义标记,全仓无任何代码引用**(2026-10-03 实机核实:
* grep 全仓仅本文件有定义,无使用点)。testTask 的创建与调度见 Src/freertos.c
* (无条件创建 testTask)+ test/board/test_config.c 的 start_test_task(),后者**直接
* 按 TEST_SUITE_RTC 等宏分支**,与本标记无关——故"RTC 构建下 testTask 不被创建"
* 的旧疑点不成立(实测 RTC 套件正常执行)。
* 4 个套件(含 RTC)在此补齐:若将来真引入"测试构建下 defaultTask 空闲"等分支而复用
* 本标记,可避免重演"漏 RTC"的隐患。详见 docs/测试说明/实机测试实施指南.md §8.3。 */
#if defined(TEST_SUITE_CH395F) || defined(TEST_SUITE_STORAGE) || defined(TEST_SUITE_GD5F) || defined(TEST_SUITE_RTC)
#define TEST_SUITE_SELECTED
#endif
+14 -10
View File
@@ -57,18 +57,21 @@ BUILD_BAT = REPO_ROOT / "MDK-ARM" / "build.bat"
DOWNLOAD_BAT = REPO_ROOT / "MDK-ARM" / "download.bat"
RUN_BOOT = REPO_ROOT / "test" / "host" / "run_boot_test.py"
# 套件 -> (宏名, verify_log 需要的汇总行名称, 该套件的 Required 标记)
# 汇总行名称与套件 `XXX_TEST_REPORT("...")` 里的字符串一致(见各模块 Test_Guide)
# 套件 -> (宏名, verify_log 需要的汇总行名称, 该套件的 Required 标记, --forbidden 覆盖)
# 汇总行名称与套件 `XXX_TEST_REPORT("...")` 里的字符串一致(见各模块 Test_Guide)。
# forbidden 覆盖:套件含"预期 [ERR]"的守卫负路径时须覆盖(空 = 用默认 `HardFault;assert;[ERR]`)。
# GD5F:TC-GD5F-704 故意用非 DMA 可达缓冲触发 `[ERR][NAND] RX DMA buffer not DMA-reachable`
# 回落轮询(用例 PASS)——按 verify_log.py 的说明去掉 [ERR],保留 HardFault/assert。
SUITES = {
"CH395F": ("TEST_SUITE_CH395F", None, ""),
"STORAGE": ("TEST_SUITE_STORAGE", "Storage Tests", "=== Storage Tests:"),
"GD5F": ("TEST_SUITE_GD5F", "GD5F2GQ5UE Tests", "=== GD5F2GQ5UE Tests:"),
"RTC": ("TEST_SUITE_RTC", "RTC Tests", "=== RTC Tests:"),
"CH395F": ("TEST_SUITE_CH395F", None, "", ""),
"STORAGE": ("TEST_SUITE_STORAGE", "Storage Tests", "=== Storage Tests:", ""),
"GD5F": ("TEST_SUITE_GD5F", "GD5F2GQ5UE Tests", "=== GD5F2GQ5UE Tests:", "HardFault;assert"),
"RTC": ("TEST_SUITE_RTC", "RTC Tests", "=== RTC Tests:", ""),
}
# 套件名 -> 汇总行里的名称可能因模块而异,故上面的 summary 值仅为默认;实际以
# `--required` 给出的前缀做 Required 匹配,`--require-suite-summary` 做严格解析。
_ALL_MACROS = [m for m, _, _ in SUITES.values()]
_ALL_MACROS = [m for m, _, _, _ in SUITES.values()]
def _rewrite_config(enable_macro):
@@ -172,7 +175,7 @@ def main(argv=None):
baseline = CONFIG.read_bytes()
for suite in picked:
macro, summary_name, required = SUITES[suite]
macro, summary_name, required, forbid = SUITES[suite]
print("")
print("################ {} ################".format(suite))
_rewrite_config(macro)
@@ -209,8 +212,9 @@ def main(argv=None):
cmd += ["--required", required]
if summary_name:
cmd += ["--require-suite-summary", summary_name]
# 套件构建预期可能出现 [ERR](守卫负路径),放宽 Forbidden 至硬错误
cmd += ["--forbidden", "HardFault;assert;[ERR]"]
# 套件构建预期可能出现 [ERR](守卫负路径)——由套件自身的 forbid 覆盖决定
# (空 = 默认 HardFault;assert;[ERR];GD5F = HardFault;assert,见 SUITES 注释)
cmd += ["--forbidden", forbid or "HardFault;assert;[ERR]"]
rc, out = _run(cmd, timeout=args.flash_timeout + 120)
print("\n".join(out.strip().splitlines()[-8:]))
results[suite] = "PASS" if rc == 0 else "FAIL"