diff --git a/.cubemx_survival_baseline.json b/.cubemx_survival_baseline.json index 5eb906b3..2b155633 100644 --- a/.cubemx_survival_baseline.json +++ b/.cubemx_survival_baseline.json @@ -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", diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e44c9827..d8d4b8e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/App/task/default_task.c b/App/task/default_task.c index 61771fb0..705e993a 100644 --- a/App/task/default_task.c +++ b/App/task/default_task.c @@ -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; diff --git a/App/util/task_watchdog.c b/App/util/task_watchdog.c index d41d3818..00192585 100644 --- a/App/util/task_watchdog.c +++ b/App/util/task_watchdog.c @@ -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 diff --git a/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c b/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c index f4eadb0c..6eaab887 100644 --- a/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c +++ b/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c @@ -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; } diff --git a/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h b/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h index 8bb87c37..b3749346 100644 --- a/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h +++ b/Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h @@ -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 可用块数(总块数减去保留块池) diff --git a/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c b/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c index 2a6c059f..a6e69d62 100644 --- a/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c +++ b/Drivers/BSP/GD5F2GQ5UE/nand_ftl.c @@ -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; } diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index c162a4bb..ae7db3cd 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| 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%** | 各区的容量与常量来源: diff --git a/docs/子系统设计/看门狗设计说明.md b/docs/子系统设计/看门狗设计说明.md index 6fb16ea6..27ed59b7 100644 --- a/docs/子系统设计/看门狗设计说明.md +++ b/docs/子系统设计/看门狗设计说明.md @@ -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 包含关系一致 | diff --git a/docs/审查记录/下一步任务规划.md b/docs/审查记录/下一步任务规划.md index 65f65413..b3541aec 100644 --- a/docs/审查记录/下一步任务规划.md +++ b/docs/审查记录/下一步任务规划.md @@ -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 | 一档/二档 | diff --git a/docs/审查记录/测试与架构改进建议.md b/docs/审查记录/测试与架构改进建议.md index b1930405..f5d24d38 100644 --- a/docs/审查记录/测试与架构改进建议.md +++ b/docs/审查记录/测试与架构改进建议.md @@ -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 与上游任务的关系 diff --git a/docs/待处理内容.md b/docs/待处理内容.md index a95f8016..785c4da4 100644 --- a/docs/待处理内容.md +++ b/docs/待处理内容.md @@ -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)。**今后每次重生成后跑一次该脚本即可** | 本次实施记录 | diff --git a/docs/测试说明/GD5F2GQ5UE_Test_Guide.md b/docs/测试说明/GD5F2GQ5UE_Test_Guide.md index 1b269954..c55c3d8f 100644 --- a/docs/测试说明/GD5F2GQ5UE_Test_Guide.md +++ b/docs/测试说明/GD5F2GQ5UE_Test_Guide.md @@ -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. 概述 diff --git a/docs/测试说明/SD2506_Test_Guide.md b/docs/测试说明/SD2506_Test_Guide.md index 13f2d8f6..32d529b6 100644 --- a/docs/测试说明/SD2506_Test_Guide.md +++ b/docs/测试说明/SD2506_Test_Guide.md @@ -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. 失败排查 diff --git a/docs/测试说明/实机测试实施指南.md b/docs/测试说明/实机测试实施指南.md index 630eb4be..410e651d 100644 --- a/docs/测试说明/实机测试实施指南.md +++ b/docs/测试说明/实机测试实施指南.md @@ -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 处,同步更新) | diff --git a/docs/问题记录/GD5F2GQ5UE_Trap_Records.md b/docs/问题记录/GD5F2GQ5UE_Trap_Records.md index f8798548..23703305 100644 --- a/docs/问题记录/GD5F2GQ5UE_Trap_Records.md +++ b/docs/问题记录/GD5F2GQ5UE_Trap_Records.md @@ -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)必须**回读校验**,不能只信写命令的返回。 diff --git a/docs/问题记录/错误码传播审计.md b/docs/问题记录/错误码传播审计.md index 06232806..d7805999 100644 --- a/docs/问题记录/错误码传播审计.md +++ b/docs/问题记录/错误码传播审计.md @@ -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 路径定案:并入队列 / 移除 / 文档化"单任务独占"。倾向 **移除**(无用户) | diff --git a/scripts/check_test_traceability.py b/scripts/check_test_traceability.py new file mode 100644 index 00000000..5a9f2292 --- /dev/null +++ b/scripts/check_test_traceability.py @@ -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--`(注释、DBG_INFO 串皆计入); + ② 文档侧:`docs/测试说明/*_Test_Guide.md` 里出现的 `TC--`。 + + [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--。 允许字母数字(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()) diff --git a/test/board/net_latency_test.c b/test/board/net_latency_test.c index fcad0ead..f79efd77 100644 --- a/test/board/net_latency_test.c +++ b/test/board/net_latency_test.c @@ -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; } diff --git a/test/board/sd2506_test_task.c b/test/board/sd2506_test_task.c index d2941c38..b0617020 100644 --- a/test/board/sd2506_test_task.c +++ b/test/board/sd2506_test_task.c @@ -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); diff --git a/test/board/test_config.h b/test/board/test_config.h index a1a609e1..bfcfc2d0 100644 --- a/test/board/test_config.h +++ b/test/board/test_config.h @@ -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 diff --git a/test/run_all_suites.py b/test/run_all_suites.py index 83591efb..ab778c81 100644 --- a/test/run_all_suites.py +++ b/test/run_all_suites.py @@ -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"