实机测试规划改造为执行判据说明:删除一次性台架窗口状态(§0.5取证记录)与规划框架,A1~A4代码状态按代码核对改写,16处外部引用重指向模块文档

This commit is contained in:
2026-10-02 23:46:27 +08:00
parent 554b2e88da
commit 3cc08f5d53
13 changed files with 142 additions and 521 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"assert-not-deadloop": "d4a00718ab2bd071",
"assert-impl": "b7fd1fadb4522edf",
"stack-overflow-hook-impl": "b7fd1fadb4522edf",
"watchdog-module": "c2580a87fa1fb72c",
"watchdog-module": "9c3c1c1f4a01dcd3",
"reset-cause-latch": "52fc71acbb0f1aed",
"iwdg-hal-driver": "4b902875a84554e9",
"hal-iwdg-enabled": "c8c9b916b05464b9",
+2 -1
View File
@@ -124,7 +124,8 @@ int app_main_init(void) {
/* 0b) round 8 取证:上报上一轮 defaultTask 主循环的**最后进段**。
* 轨迹槽在 .noinit(scatter UNINIT_CCM),IWDG/引脚复位不清 SRAM →
* 误复位后开机第一时间可见 `PREVRUN seg=N`,直接指向主循环阻塞点
* (round 7 结论:判定分支未执行 = poller 自身停摆,见 实机测试规划.md §0.5.4)。
* (round 7 观察"判定分支未执行"经 round 8b 更正:真因是 IWDG 超时被静默
* 配成复位默认值,见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)。
* 裸 UART 直发,且一次复位只报一次(内部清 magic)。 */
task_wd_seg_report();
+1 -1
View File
@@ -18,7 +18,7 @@
* 保证喂狗节奏 ≤ ~300ms。根治(USART1 TX-only 灭 ORE 机制)
* 另行推进(.ioc 结构性修改 + CubeMX 重生成,见待处理内容①)。
* 2026-10-02 王建锋 round 8b 加固:发送超时判据改用 DWT CYCCNT(时间源无关)。
* 台架 round 8a 取证(实机测试规划.md §0.5.4.2)定位到:defaultTask
* 台架 round 8a 取证(见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)定位到:defaultTask
* 冻结在 console 突发行内、无 STALL、复位为 IWDGRSTF。唯一自洽机理 =
* HAL_UART_Transmit 的超时判据 `HAL_GetTick() - Tickstart` 依赖
* TIM7 的 uwTick —— TIM7 一旦停摆,超时永不成立 → 写者死等 TXE →
+1 -1
View File
@@ -125,7 +125,7 @@ void syslog_drain(void);
* 返回值:0 - 成功;-1 - 卷占用或写失败(证据可能回滚,**不阻断判定**) int
* 限定条件:poller(defaultTask)上下文调用;卷锁一律 try(绝不阻塞,
* 本上下文 = 看门狗喂狗点)
* 函数说明:round 7 取证通道(设计见 docs/测试说明/实机测试规划.md §0.5.4:
* 函数说明:round 7 取证通道(设计见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录:
* 误复位后 RAM 态随复位消失、stall 行经僵尸串口丢失,故证据必须落盘)。
* 内部在释放卷锁后立刻追加一条 s_append(SYSLOG_RUN, ...) 写另一个
* 文件——FTL 单页缓存语义为"写到不同 LPN 才把前一脏页刷进 NAND",
+1 -1
View File
@@ -15,7 +15,7 @@
* (本任务 = 看门狗喂狗点,绝不阻塞等锁)
* v1.3 2026-10-02 王建锋 新增 sys_log_evidence():把看门狗停喂现场
* 证据落 /log/wdev.log(掉电不丢)+ flush 诱导
* (round 7 纯取证,见 实机测试规划.md §0.5.4)
* (round 7 纯取证,见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)
*/
/* 头文件包含区 */
+2 -1
View File
@@ -168,7 +168,8 @@ void default_task_func(void *argument) {
for (;;) {
/* round 8 取证:进段打点——每步前记录段号到 .noinit 槽(复位不丢)。
* 误复位后开机 `PREVRUN seg=N` 即"上一轮主循环死在哪个调用点"
* (round 7 结论 = poller 自身停摆,见 实机测试规划.md §0.5.4)。 */
* (round 7 初判"poller 自身停摆"经 round 8b 更正:真因是 IWDG 超时被静默
* 配成复位默认值,见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)。 */
task_wd_seg(TASK_WD_SEG_POLL);
/* 心跳抽样与喂狗判定(内部节流至 100ms;全员到位才喂,否则累计停喂) */
task_wd_poll();
+6 -5
View File
@@ -11,7 +11,7 @@
* v1.1 2026-10-02 王建锋 round 7 纯取证埋点:近 8 次抽样位图环 + 最大喂狗
* 间隔统计 + 停喂现场落 /log/wdev.log(掉电不丢);
* 全部在 TASK_WATCHDOG_UNIT_TEST 守卫内,判定状态机
* 与主机侧单测逐字不变(见 实机测试规划.md §0.5.4)
* 与主机侧单测逐字不变(见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)
* v1.2 2026-10-02 王建锋 round 8 取证:复位不丢的"进段打点"(.noinit 槽 +
* PREVRUN 开机上报 + STALL 行附 seg)——round 7 结论为
* "poller 自身停摆",需定位 defaultTask 主循环阻塞点
@@ -19,7 +19,7 @@
* (TIM7 ISR 每 1ms 写 uwTick/FreeRTOS tick/计数)+
* TICKRUN 开机上报(含相对进段时刻的增量 duw/dfr)——
* round 8a 定位到 console 通道后,需钉死是 TIM7(HAL tick)
* 还是 SysTick(FreeRTOS tick) 停摆(实机测试规划.md §0.5.4.3)
* 还是 SysTick(FreeRTOS tick) 停摆(见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)
*
* v1.4 2026-10-02 王建锋 **P0 修复**:wd_platform_start() 补写 IWDG 的
* PR/RLR(= WD_IWDG_PRESCALER / WD_IWDG_RELOAD,即设计值
@@ -108,7 +108,7 @@ static uint32_t s_started; /* 1 = IWDG 已启动 */
static uint32_t s_last_tick; /* 上次抽样时刻(100ms 节流基准) */
/* ==== round 7 纯取证状态(仅 MCU 构建;判定状态机逐字不变)====
* 设计见 docs/测试说明/实机测试规划.md §0.5.4:误复位后 RAM 态随复位消失,
* 设计背景见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录:误复位后 RAM 态随复位消失,
* 故把"停喂现场"快照落盘(/log/wdev.log)供复位后回读定性。 */
#ifndef TASK_WATCHDOG_UNIT_TEST
#define WD_SNAP_HIST_DEPTH 8u
@@ -265,8 +265,9 @@ static void wd_stall_evidence(uint32_t snapshot) {
}
/* ==================== round 8 取证:复位不丢的"进段打点" ====================
* 见 task_watchdog.h 的说明与 实机测试规划.md §0.5.4 round 7 结论(假设 2:
* poller 自身停摆 → 需定位 defaultTask 主循环死在哪个调用点)。
* 见 task_watchdog.h 的说明(round 7 初判"poller 自身停摆"经 round 8b 更正:
* 真因是 IWDG 超时被静默配成复位默认值,见 docs/子系统设计/看门狗设计说明.md
* 2026-10-02 变更记录)→ 需定位 defaultTask 主循环冻结在哪个调用点。
* 槽放 .noinit(scatter UNINIT_CCM):IWDG/引脚复位不改变 SRAM,复位后仍可读。 */
/*
+7 -5
View File
@@ -83,10 +83,11 @@ uint32_t task_wd_is_armed(void); /* 1 = IWDG 已启动 */
/*
* ==================== round 8 取证:复位不丢的"进段打点" ====================
* 背景(实机测试规划.md §0.5.4 round 7 结论):误复位时判定分支(STALL)从未
* 执行 → **喂狗点 task_wd_poll 自身停摆**(假设 2:poller 被阻塞),而非某个被
* 监视任务饿死。RAM 态随复位消失、串口又可能来不及打,故需要一条**复位后仍可读**
* 的"defaultTask 主循环最后走到哪一段"轨迹。
* 背景(2026-10-02 台架 round 7~8 取证,完整链条见
* docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录):误复位时判定分支(STALL)从未
* 执行,当时判为**喂狗点 task_wd_poll 自身停摆**(round 8b 已更正:真因是 IWDG 超时
* 被静默配成复位默认值),而非某个被监视任务饿死。RAM 态随复位消失、串口又可能来不及打,
* 故需要一条**复位后仍可读**的"defaultTask 主循环最后走到哪一段"轨迹。
*
* 实现:轨迹槽放在 .noinit(scatter 的 UNINIT 执行区,见
* MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct 的 UNINIT_CCM)——IWDG/引脚复位
@@ -152,7 +153,8 @@ void task_wd_tick_snapshot(void);
* 限定条件:TIM7 中断上下文调用(`HAL_TIM_PeriodElapsedCallback` 内,每 1ms)
* 函数说明:更新 cur 槽 = [magic, uwTick, 本轮 TIM7 中断计数, 0](纯 SRAM 写,无阻塞)。
* 与 task_wd_seg 的"进段打点"配合,复位后由 task_wd_seg_report() 上报。
* 背景(实机测试规划.md §0.5.4.3 第 2 条):HAL_UART_Transmit 的超时判据
* 背景(2026-10-02 台架 round 8b 取证,见 docs/子系统设计/看门狗设计说明.md
* 2026-10-02 变更记录):HAL_UART_Transmit 的超时判据
* 依赖 uwTick,时间源停摆即死等——必须先把"TIM7 是否停摆"钉死再定向修复。
* ⚠ **本函数内严禁调用任何 FreeRTOS/CMSIS-RTOS API**:TIM7 在 HAL_Init
* 阶段即开始中断(远早于 xPortStartScheduler),此时 ISR 版内核 API 会触发
+2 -2
View File
@@ -87,7 +87,7 @@ int main(void)
/* round 8b 取证:**必须在 HAL_Init() 之前**把上一轮的"时间源存活性"终值
* 快照到 .noinit 的 prev 槽——TIM7 在 HAL_Init 里就 HAL_TIM_Base_Start_IT
* 开始 1ms 中断,而开机上报要到 app_main_init(≈117ms)才跑;不先搬走,
* 上一轮终值会被本轮开机瞬间的 tick 覆盖(实机测试规划.md §0.5.4.3)。 */
* 上一轮终值会被本轮开机瞬间的 tick 覆盖(见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)。 */
task_wd_tick_snapshot();
/* USER CODE END 1 */
@@ -232,7 +232,7 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
/* round 8b 取证:TIM7 每 1ms 把"时间源存活性"写进复位不丢的 .noinit 槽
* (uwTick + 中断计数)。复位后由 task_wd_seg_report() 打 TICKRUN 行,
* 与"进段打点"时刻对比,判定 TIM7(HAL 时基)是否在冻结同刻停摆
* (实机测试规划.md §0.5.4.3)。
* (见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)。
* ⚠ task_wd_tick_probe() 内不得调用任何 FreeRTOS API:TIM7 在 HAL_Init
* 阶段即开始中断,远早于调度器启动,此时 ISR 版内核 API 会触发 port.c 的
* 优先级断言并把系统卡死在本 ISR 内(2026-10-02 台架实测)。
+1 -1
View File
@@ -93,7 +93,7 @@ docs/
| [`测试文档模板规范.md`](测试说明/测试文档模板规范.md) | 测试文档模板与命名规则(TM / TC / DP) |
| [`链路延迟测试说明.md`](测试说明/链路延迟测试说明.md) | 网络链路时延分段定义、判定基线、执行流程 |
| [`模拟器差距清单.md`](测试说明/模拟器差距清单.md) | **PC 模拟器 vs 真机差距登记册**(L3 门禁可信度依据;改动涉及网络/日志/硬件层前必查 §3 决策规则) |
| [`实机测试规划.md`](测试说明/实机测试规划.md) | **上实机后的执行规划**(7 批:环境预检 → `@test`+S7 → IWDG 注入/长跑 → 堆告警 → GD5F 错误通道 → 网络基线 → 工程卫生);每项含判据与精确命令 |
| [`实机测试规划.md`](测试说明/实机测试规划.md) | **上板(真机)测试执行判据与待上板事项**(环境预检 → `@test`+自检判定 → IWDG 注入/长跑 → 堆告警 → GD5F 错误通道 → 网络基线 → 工程卫生 → 实测陷阱);每项含判据与精确命令 |
**主机侧单元测试(L0.5)**
@@ -1113,4 +1113,4 @@ python scripts/check_cubemx_survival.py --write # 重建基线(确认改动
| 2026-09-29 | **3 个 Fault 升级为崩溃采集** | `MemManage` / `BusFault` / `UsageFault` 由空 `while(1)`(死机静默)升级为与 `HardFault` 同一套采集:生成区整函数替换(各带独立锚点 `MEMFAULT-ANCHOR` / `BUSFAULT-ANCHOR` / `USAGEFAULT-ANCHOR`)+ `#define X X_Gen` 改名保留生成实体(链接器各移除 2 字节)。新增 `fault_handler_enable()` 使能 `SHCSR` 后三者(默认只开 HardFault,不开则故障被升级、看不出根因),声明入 `Inc/stm32f4xx_it.h`、调用在 `app_main_init()` 第一行。因 ARMCC `__asm` 取不到 C 数据符号,改为 per-fault C 入口 `fault_entry_{hard,mem,bus,usage}` 带故障名调 `hard_fault_report(tag, frame)`。**刻意不启用** `UNALIGN_TRP` / `DIV_0_TRP`(避免把普通 bug 升级成死机)。审计脚本新增 `fault-handlers-3` / `fault-handler-enable` 两项、`UNPROTECTED_LEGACY` 清掉 3 个 Fault(WARN 2→1)、清单 16→18 项。`@build` 0 错误 0 警告;审计 18 通过 / 1 警告 / 0 失败 |
| 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/`实机测试规划.md` §0.5.4.1 "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-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 断言) |
+3 -3
View File
@@ -19,10 +19,10 @@
|---|---|---|---|---|
| S-1 | defaultTask 静态最坏链余量贴线:2872B/4096(余量 1224B = 29.9% < 30% 判据,差 **4.8B**)——`storage_bring_up → … → dhara GC → SPI` 链,**L3 前即如此**(L3 不触 defaultTask 调用链) | 2026-09-22(L3 产品构建栈报告) | 处置方向二选一:栈 4096→4608(CCM 堆余量 4816B 可覆盖 +512B)或 GC 链缓冲局部削减;与 S8(非阻塞日志)并案评估 | `scripts/stack_report.py`(L3 产品构建档) |
| S-2 | N-2 消化后,台架 20MB 串口日志截头会丢早期 [STACK] 水位行——跨构建的"前后"栈水位对照建议固化 boot_log 快照(`%TEMP%\opencode\boot_log_*.txt`)进 `docs/测试说明/` 存档目录 | 2026-09-22(L3 收尾发现:切片时代水位行已被截头,A/B 只能定性) | 流程项,无代码改动 | 本文件 N-2 佐证 |
| S-3 | **USART1 TX-only 结构根治(灭 ORE 僵尸机制)**——ORE 锁存在外设软复位后毫秒级重锁(PA10 悬空 + RX 使能),是 2026-10-02 台架"背靠背 drain 拉长主循环 → 喂狗节奏饥饿 → IWDG 误复位"的**机制根源**。现仅有缓解(round-3 单次 drain 250ms 预算 + round-6 发送故障后 300ms 冷却),**未根治** | 2026-10-02(台架 gen-288,见 `实机测试规划.md` §0.5.5) | **待办**:改 `.ioc` 令 USART1 为 TX-only(不使能 RX / PA10 不复用为 USART1_RX)→ CubeMX 重生成 → 跑 `scripts/check_cubemx_survival.py`。属结构性改动,需单开一轮并复跑台架闭环 | `子系统设计/控制台单写者日志设计说明.md` §12(含 O4);`App/log/console_log.c` 2026-10-02 修改记录 |
| S-4 | **dhara 重试自旋残留**——`try_recover()` 的 `DHARA_MAX_RETRIES`(8) 自旋在坏块密集时仍可能长时间占用 defaultTask(喂狗点) | 2026-10-02(round 7/8 取证期观察) | 低优先:现有 `vol_lock` + 喂狗节奏缓解;若台架再出"存储侧喂狗饥饿"再排期 | `Lib/dhara/map.c`;`实机测试规划.md` §0.5.5 第 5 步 |
| S-3 | **USART1 TX-only 结构根治(灭 ORE 僵尸机制)**——ORE 锁存在外设软复位后毫秒级重锁(PA10 悬空 + RX 使能),是 2026-10-02 台架"背靠背 drain 拉长主循环 → 喂狗节奏饥饿 → IWDG 误复位"的**机制根源**。现仅有缓解(round-3 单次 drain 250ms 预算 + round-6 发送故障后 300ms 冷却),**未根治** | 2026-10-02(台架 gen-288,见 `控制台单写者日志设计说明.md` §12) | **待办**:改 `.ioc` 令 USART1 为 TX-only(不使能 RX / PA10 不复用为 USART1_RX)→ CubeMX 重生成 → 跑 `scripts/check_cubemx_survival.py`。属结构性改动,需单开一轮并复跑台架闭环 | `子系统设计/控制台单写者日志设计说明.md` §12(含 O4);`App/log/console_log.c` 2026-10-02 修改记录 |
| S-4 | **dhara 重试自旋残留**——`try_recover()` 的 `DHARA_MAX_RETRIES`(8) 自旋在坏块密集时仍可能长时间占用 defaultTask(喂狗点) | 2026-10-02(round 7/8 取证期观察) | 低优先:现有 `vol_lock` + 喂狗节奏缓解;若台架再出"存储侧喂狗饥饿"再排期 | `Lib/dhara/map.c` |
| S-5 | **可选全序列卷锁**——现卷锁仅覆盖 `syslog` / `cfg` / `lftpd` 三条路径(`vol_lock_wait(0)` try 语义),其它潜在访问点(如 UI 读日志)未纳入 | 2026-10-02(卷锁落地时) | 待评估:先确认是否真有第二写者;无则关闭本项 | `App/fs/vol_lock.{c,h}` |
| S-6 | ~~**dhara resume 回滚:复位后所有写入全部丢失**~~ | 2026-10-02(round 9 取证) | ✅ **已修**:`dhara_journal_resume()` 的 epoch 锚点改为比较两个候选 epoch(回绕后锚"低地址=最新")。详见 `实机测试规划.md` §0.5.5.1 与 `架构设计/STM32F4-Base存储架构说明.md` §5.4.1。判定手法已固化为开机日志 `Map head=… tail=… epoch=…` | `Lib/dhara/journal.c` 2026-10-02 本地修复注释 |
| S-6 | ~~**dhara resume 回滚:复位后所有写入全部丢失**~~ | 2026-10-02(round 9 取证) | ✅ **已修**:`dhara_journal_resume()` 的 epoch 锚点改为比较两个候选 epoch(回绕后锚"低地址=最新")。详见 `架构设计/STM32F4-Base存储架构说明.md` §5.4.1。判定手法已固化为开机日志 `Map head=… tail=… epoch=…` | `Lib/dhara/journal.c` 2026-10-02 本地修复注释 |
## 网络(二期排期,随第二档 #1)
+114 -498
View File
@@ -1,451 +1,39 @@
# 实机测试规划(On-Board Test Plan)
> **用途**:把**当前所有"必须真机才能验证/收尾"的事项**集中成一份**可照做**的执行规划——
> 每项给出「现状证据 → 目标 → 判据(可观测的量化指标)→ 执行步骤(含精确命令)→ 风险/回滚」。
> **背景**:2026-10-02 起台架进入**无 MCU 窗口**(离线只做 L0/L0.5 与文档),
> 第 1/2 波离线改造(S8/S1-a/S1-b/S2/S3/S10/S13/S14/S6)已全部落地并推送,
> 遗留的**验证债**(代码已改、判据需真机)即本文的输入。
> 本文档描述项目的**上板(真机)测试执行判据**与**当前待上板事项**:
> 每项给出「现状 → 判据(可观测、量化)→ 执行步骤(含精确命令)→ 风险/回滚」。
> **前置铁律**:本文所有步骤**均在接上 MCU / 串口 / ST-LINK 之后**执行;
> 无实机时**只读本文,不执行任何烧录**。
> **上游**:`错误码传播审计.md`(E-xx)· `审查记录/测试与架构改进建议.md`(S7/S9/S11/S5/S4)·
> `审查记录/下一步任务规划.md`(阶段 A/C/D)· `待处理内容.md`(登记簿 N-03/N-2/S-1)·
> `子系统设计/看门狗设计说明.md`(§10 待台架步骤)。
> **约定**:本文是**执行规划**不是结果记录;每项完成后**先销上游登记**,再在本文件 §9 变更记录留一行。
> `审查记录/下一步任务规划.md`(阶段 A)· `待处理内容.md`(登记簿)·
> `子系统设计/看门狗设计说明.md`(§10 台架步骤)。
> **约定**:本文事项完成后**先销上游登记**,再在文末变更记录留一行;
> 结论与证据沉淀进对应模块文档/问题记录(2026-10-02 台架窗口的完整取证链——
> IWDG PR/RLR 静默错配 / dhara resume 回滚 / ORE 僵尸通道——见 `看门狗设计说明.md` 2026-10-02 变更记录、
> `STM32F4-Base存储架构说明.md` §5.4.1、`控制台单写者日志设计说明.md` §12;修复已随 `930215f` 提交)。
---
## 0. 一句话总览
## 1. 事项总览
台架窗口就绪后,按 **P0 → P1 → P2 → P3** 四批顺序执行:
台架窗口就绪后,按 **P0 → P1 → P2 → P3** 批次顺序执行:
| 批次 | 事项 | 性质 | 预估单次耗时 |
| 批次 | 事项 | 性质 | 章节 |
|---|---|---|---|
| **P0-A** | 环境预检(串口/ST-LINK/`@build`/`.map` 基线) | 前置 | 5 min |
| **P0-B** | 产品 `@test` 闭环 + 自检结果判定(S7 + A1/A2/A3/A4) | 验证债 | 15 min |
| **P1-A** | IWDG 故障注入 V2/V4 + 长跑 V5(N-03) | 可靠性 | 45 min |
| **P1-B** | 堆低水位告警实测(S11) | 可观测性 | 含于长跑 |
| **P2-A** | GD5F 错误通道(S9 = 上游 C2)+ 套件回归 | 结构债 | 60 min |
| **P2-B** | 网络重建恢复(P1-8)+ 存量网络项重锁(N-2/N-3) | 稳定性 | 45 min |
| **P3** | `test/` 静态检查基线(S5)+ 用例追溯(S4)+ 待核实项 | 工程卫生 | 30 min |
| **P0-A** | 环境预检(串口/ST-LINK/`@build`/`.map` 基线) | 前置 | §2 |
| **P0-B** | 产品 `@test` 闭环 + 自检结果判定(S7 + A1~A4) | 待上板 | §3 |
| **P1-A** | IWDG 故障注入 V2/V4 + 长跑 V5(N-03) | 可靠性 | §4 |
| **P1-B** | 堆低水位告警实测(S11) | 可观测性 | §5 |
| **P2-A** | GD5F 错误通道(S9 = 上游 C2)+ 套件回归 | 结构债 | §6 |
| **P2-B** | 网络重建恢复(P1-8)+ 存量网络项重锁(N-2/N-3) | 稳定性 | §7 |
| **P3** | `test/` 静态检查基线(S5)+ 用例追溯(S4)+ 待核实项 | 工程卫生 | §8 |
> **每批出口**:`@build` 0/0 → 一轮 `@test` PASS → ST-LINK 全片回读 `No difference found` →
> 上游登记销项 + 本文件 §9 留痕。
> 上游登记销项 + 文末变更记录留痕。
---
## 0.5 台架执行现状与交接(2026-10-02 台架窗口)
> **读法**:接手的 Agent **先读本块**(现状 + 立即下一步),再按需读 §1 起的批次规划。
> 本块随台架窗口更新;每轮修复的完整证据在 P0-B 出口统一销入设计文档前,**以本块 + 串口日志为准**。
### 0.5.1 一句话现状
**P0-A 通过;P0-B 卡在一处**:`/log/selftest.log` 台账清空(0 字节 STOR)会触发
**IWDG 误复位**(round 3~6 共四轮修复后仍 **5/5 复现**)。台账因此"清不掉",
S7(已落地并提交)拿不到 PASS。
**round 7 取证已完成(§0.5.4.1)**:连续复现 4 次,**判定"假设 2:poller 自身停摆"成立**
——假设 1(accept_timeout 分片节拍)与假设 3(僵尸通道吞证据)均被否。
**round 8a 取证已完成(§0.5.4.2)**:以复位不丢的 `.noinit`"进段打点"定位到
**defaultTask 冻结在 `stack_probe_poll()` 的 `DBG_INFO`→console 通道内**,
机理 = **时间源停摆使 `HAL_UART_Transmit` 超时永不成立 → 死等**。
**立即下一步 = round 8b(§0.5.4.3)**:给 console 通道加与时间源无关的硬上界 + 加 tick 存活性探针
定位停摆源 → 再清台账 → 全量 `@test` → 回读 → 销项 → 提交。
### 0.5.2 已完成(本窗口实测)
| 项 | 状态 | 证据 |
|---|---|---|
| P0-A 环境预检 | **PASS**(A5 待出口前复核) | A2 构建 `BUILD_OK: 0 errors, 0 warnings`;A3 四个 `TEST_SUITE_*` 全注释(产品态);A4 `.map` `ER_IROM1 Size=0x000402B4` = **262,836 B**(含 round 1~6 修复增量,基线 262,100 已上移;266,540 才是套件态);A6/A7/A8 ST-LINK/串口/daemon 可用 |
| **S7 自检判定**(§2.1) | **已落地并提交**(`4a4cd29`) | `verify_log.py --require-selftest-pass` + `--selftest-log`;`run_boot_test.py` 同名参数,未给本地副本时**自动 FTP 抓取** `/log/selftest.log`;语义 = 缺失/0 字节 PASS、任一 64B 记录 FAIL。**但本轮尚未取得 PASS**(台账有 409 条记录) |
| 看门狗误复位修复 round 1~6 | **代码已落地、未提交**(等 P0-B 出口统一提交) | 见 §0.5.7 陷阱与下表 |
**round 1~6 修复集摘要**(全部未提交,**切勿 `git checkout`/stash**):
| 轮 | 修复 | 针对现象 |
|---|---|---|
| 1~2 | 就绪屏障位一律 `osFlagsNoClear` | 屏障位被消费 → 消费者永不就绪 |
| 3 | console 单次 drain 预算 250ms + UART 软复位自愈;lftpd STOR/accept 路径修复 | ORE 僵尸通道(USART1 TX_RX + PA10 悬空) |
| 4 | FTL 长窗口心跳 `nand_ftl_wd_beat`(按任务路由) | 0 字节 STOR 截断 → dhara GC 长尾 → FTP 位缺席 |
| 5 | FTL 直喂 `nand_ftl_wd_feed` + `vol_lock` 卷锁(新增 `App/fs/vol_lock.{h,c}`) | poller 自身长 FTL 窗口;并发 FTL 写竞争 journal |
| 6 | console 发送故障即止 + 300ms 发送冷却 | 背靠连发块超时链拉长主循环 → 喂狗节奏饥饿 |
### 0.5.3 当前阻断点:0 字节 STOR → IWDG 误复位
**复现步骤**(每轮台架都跑):`SIZE /log/selftest.log`(记基线)→ 空 STOR 该文件(ftplib
`storbinary` 传空 `BytesIO`)→ 观察串口与复位原因。
**最近一次时间线**(设备时刻,镜像 = round-6 构建):
```
09:00:23.0 [FTP] CMD STOR /log/selftest.log
09:00:23.0 [NET] sock3 DISCONNECTED, releasing (chip tcp=0x00) ← 芯片侧丢弃数据连接
09:00:23.1 [NET] sock3 auto-relisten ok / relisten retry ok
09:00:24.4 reset cause: IWDG (independent watchdog) (CSR=0x24000000) ← 误复位
09:00:24.x 重启 → 写 1 条 `watchdog reset` 自检记录 → 台账反而 +1
```
**已排除**(逐条有证据,勿重复怀疑):
1. **单次 console drain 过长**:round-3 已限 250ms;round-6 再加"故障即止 + 300ms 冷却",
本轮误复位仍复现 → 非 drain 时长问题(但僵尸通道本身未根除)。
2. **poller 自身长 FTL 窗口**:round-5 `nand_ftl_wd_feed` 直喂已覆盖;本轮停喂时刻**无 FTL 操作**。
3. **并发 FTL 写**:round-5 卷锁已串行化(`lftpd_io` 全序列持锁、`syslog`/`cfg` try 语义)。
4. **主循环无界阻塞**:`default_task.c` 主循环逐段核对(drain ≤ ~110ms(round-6 后)、
事件等待 ≤50ms、卷锁 try、2s 门为 tick 判断非延时),未找到可阻塞 1.7s 的段。
**尚存假设**(round 7 取证定性,三者互斥)——**已于 §0.5.4.1 判定:假设 2 成立,假设 1/3 均否**:
| # | 假设 | 若成立的取证特征 |
|---|---|---|
| 1 | **FTP 位缺席**:0 字节 STOR 落在 `lftpd_inet_accept_timeout(..., 5000)` 等待中;该等待虽每 100ms 分片喂心跳,但分片周期 = 100ms + netTask 消息往返,**抽样窗口恰为 100ms** → 可能整窗缺席 → 800ms 判定停喂 | WDEV 记录 `snap=0x36`(其余位齐) |
| 2 | **poller 自身被阻塞**(判定代码根本没跑到) | **WDEV.LOG 无记录**——前提是先确认证据通道可用(文件已存在且此前有记录),否则与"通道写失败"混淆 |
| 3 | **僵尸通道吞掉证据**:停喂真因是假设 1,但 `[WD] task stall` 行经僵尸串口丢失 → "无 stall 日志"**不能**用来排除 1 | WDEV 记录 `cf>0`(console tx_fault 计数 > 0) |
> ⚠️ **判据缺口**(本轮绕圈的根因):误复位后**无法区分**上述三类——stall 只写串口
> (僵尸时丢失),RAM 态随复位消失。故 round 7 必须把证据落到**掉电不丢**的盘上。
### 0.5.4 立即下一步:round 7 纯取证埋点(不改判定语义)
> 目标:让"停喂真因"在下一次误复位后**仍可读**。判定逻辑一字不改(主机单测不受影响)。
**改动 1 — 新增 `App/log/sys_log_mcu.h`**(MCU 专用头,头文件保护宏 `SYS_LOG_MCU_H`):
声明 `int sys_log_evidence(const char *p_desc);`
**改动 2 — `App/log/sys_log_mcu.c` 增实现 `sys_log_evidence`**:
1. `vol_lock_wait(0u)` **try** 取卷锁(调用方 = poller 上下文,绝不阻塞);
2. `f_open("/log/wdev.log", FA_WRITE | FA_OPEN_APPEND)` 写证据行 + `"\r\n"`(8.3 合法名;
文件 > 8KB 时先 `f_unlink` 重建,防长跑无界增长);释放锁;
3. **随后立刻调 `s_append(SYSLOG_RUN, "wd stall declared")`** ——
⚠️ **关键**:FTL 单页缓存的语义是"写到**不同 LPN** 才把前一脏页刷进 NAND",
没有这一步,WDEV 证据会在 1.7s 后的复位中**回滚丢失**(409 条里被回滚的 07:56 记录即同类教训);
4. 返回值语义:0 成功 / -1 卷占用或写失败(证据可能回滚,**不阻断判定**)。
**改动 3 — `App/util/task_watchdog.c`**(**全部放进 `#ifndef TASK_WATCHDOG_UNIT_TEST`**,
保证 `watchdog-unit-test` 主机单测 39 条断言与判定状态机逐字不变):
| 位置 | 内容 |
|---|---|
| 私有变量区(**须在 `task_wd_poll` 之前**,否则未声明) | 8 深度快照环形缓冲 `s_snap_hist[8]` + 索引;`s_last_feed_tick` / `s_max_feed_gap` / `s_feed_seen` |
| 弱实现 `wd_platform_feed()` 内 | 追加 `wd_feed_track()`(更新最近喂狗时刻 + **最大喂狗间隔**)——直接量化"停喂前节奏是否已变慢" |
| `task_wd_poll()` 取快照处(L180-182 后) | `#ifndef` 包住,写环形缓冲 |
| 停喂分支(L196-203) | 保留原 `DBG_ERROR`,追加 `wd_stall_evidence(snapshot)`:格式化 `STALL snap=0x%08X req=0x%08X h=<8×8hex> mg=<ms> cf=<次>` → `sys_log_evidence(...)` |
| 文件头包含区(守卫内) | `#include "sys_log.h"`(`SYSLOG_RUN`)+ `#include "console_log.h"`(`dbg_console_tx_fault()`) |
**执行与判据**:
```bash
# 1) 构建 + 烧录 + 开机校验(铁律仪式,见 §0.5.7)
& "MDK-ARM\build.bat" # 判据:BUILD_OK: 0 errors, 0 warnings
& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py clear
& "MDK-ARM\reset.bat" flash
& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py wait-idle --timeout 90 --settle-ms 8000 --max-ms 45000
# 2) 复现:记 SIZE 基线 → 空 STOR /log/selftest.log → 等误复位
# 3) 复位后读证据(FTP 绝对路径):
# SIZE /log/wdev.log → 非 0 即已落盘(掉电不丢证明 commit 成功)
# RETR /log/wdev.log → 逐行读 STALL 记录
```
| 观察 | 结论 → 下一步(round 8 定向修复) |
|---|---|
| `snap=0x36` 且 `cf` 任意 | **假设 1**:FTP 任务侧心跳漏窗 → 修 `lftpd_inet_accept_timeout` 分片节拍(心跳周期必须 **< 100ms**,或把该等待改为"状态查询 + 短延迟")|
| `snap` 缺其它位 | 对应任务(ui/net/timesync)停摆,按位定位 |
| **无 WDEV.LOG / 无记录** | **假设 2**:poller 自身被阻塞 → 转查主循环各调用点(下一步加"进段打点"或用 `[STACK]`/tick 埋点定位)|
| `mg`(最大喂狗间隔)在停喂前已 > 100ms | 节奏性饥饿(喂狗周期被拉长)而非硬停摆 |
| `cf>0` | 僵尸通道在该窗口活跃 → 解释"无 stall 日志",同时印证"证据必须落盘"的设计 |
> **注意**:round 7 是**纯取证**,预期仍会误复位一次(每复位一次台账 +1 条
> `watchdog reset`);取证完成后按 round 8 修复,再做 §0.5.5 的出口闭环。
---
### 0.5.4.1 round 7 取证结果(2026-10-02 台架)——**假设 2 成立**
**执行**:§0.5.4 三处改动已落地(`App/log/sys_log_mcu.{h,c}` 新增 `sys_log_evidence`;
`App/util/task_watchdog.c` round 7 埋点,全部在 `TASK_WATCHDOG_UNIT_TEST` 守卫内,判定状态机
逐字不变)→ `@build` 0/0 → 主机单测 **39/39 PASS** → 烧录 → 空 STOR 复现。
串口全量捕获(自建 pyserial 落地,`%TEMP%\opencode\sink.log`,1217 行)**连续复现 4 次 IWDG 误复位**。
**关键读数(4 次复位无一例外)**:
| 读数 | 值 | 含义 |
|---|---|---|
| 稳态快照 | `MISS start snap=0x00000017 req=0x00000037`,间隔 ≈204ms | 缺 **TIMESYNC** 位;与 100ms 抽样交替 → **良性节拍差**(streak 反复 0→1,永不累积到 8)|
| 停喂前最后一次 MISS | `snap=0x17`(复位 #2/#3/#4)/ `snap=0x01`(#1)| **从不是 `0x36`** → **假设 1(FTP 位缺席)被否**:`lftpd_inet_accept_timeout` 的 100ms 分片心跳**有效** |
| `STALL` 行(裸 UART) | **0 条** | 停喂分支从未执行 |
| `[WD] task stall`(环形缓冲) | **0 条** | 同上 |
| `/log/wdev.log` | **从未创建** | 盘上证据通道未产出 |
| `CONSOLE tx fault` / `dropped` | **0 条** | 裸 UART 通道**全程健康**(MISS 行一直打到复位前一瞬)|
| 复位原因 | `IWDG (CSR=0x24000000)`(IWDGRSTF,非 SFTRSTF)| 真看门狗溢出,非软件复位 |
**推理链(§0.5.3 的判据缺口已闭合)**:`MISS` 行证明裸 UART 通道**可用且一直可用**(`cf=0`),
故"无 STALL 行"**不能**归因于僵尸通道吞证据 → 停喂分支确实没跑;而 MISS 行在复位前**戛然而止**
(稳态每 204ms 一条,此后完全静默)→ **`task_wd_poll` 停止被调用** → **poller(defaultTask)自身停摆**。
⇒ **结论:假设 2 成立;假设 1、假设 3 均否。round 8 走"查 poller 阻塞点",不走"修 accept_timeout 分片节拍"。**
**顺带确立的两条事实**:
1. 稳态 `0x17` 是**良性**的(抽样 100ms vs timesync 心跳 ~200ms 的节拍差),但它是**掩码精确匹配**
设计的脆弱点:timesync 心跳一旦滑出两个连续窗口就会开始累积 streak。建议 round 8 顺手把
timeSyncTask 的等待也按 ≤50ms 分片喂心跳(低成本消抖)——**但它不是本次复位的原因**。
2. `/log/wdev.log` 未创建**不能**单独当判据:本轮"无记录"来自**分支未执行**,而非写失败。
### 0.5.4.2 round 8a 取证(复位不丢的"进段打点")——**定位到 console 通道**
**改动**(round 8 定向取证的"进段打点",即 §0.5.4 表内"下一步加'进段打点'"的落地):
| 文件 | 改动 |
|---|---|
| `App/util/task_watchdog.{h,c}` | 新增 `task_wd_seg(seg)` / `task_wd_seg_report()`;轨迹槽 `s_wd_seg_trail[4]`(magic/段号/时刻/序号)置于 **`.noinit`**;`wd_stall_evidence` 的 STALL 行追加 `seg=` |
| `App/task/default_task.c` | 主循环**每步前** `task_wd_seg(TASK_WD_SEG_*)`(poll/evtwait/drain/cfgsave/cfgpoll/led/syslog/stack 共 8 段)|
| `App/app/app_main.c` | `reset cause` 行之后调 `task_wd_seg_report()`(裸 UART 打 `PREVRUN seg=N seq=N t=N`)|
| `MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct` | 新增 `UNINIT_CCM 0x1000FF00 UNINIT 0x100 { *(.noinit) }`(RW_CCM 由 `0x10000` 缩到 `0xFF00`;CCM 尾部 16KB 本就是 FreeRTOS 堆之外的闲置区)|
> **为何用 `.noinit` 而非新任务**:IWDG/引脚复位**不清 SRAM**(VDD 保持),故复位后仍可读;
> 新任务方案在"tick 停摆"时自身也会停,取不到数。**已验证**:镜像 `Region$$Table` 共 4 条
> (`RW_IRAM1` copy/zero、`RW_CCM` copy/zero),**无一条指向 `0x1000FF00`** → 启动完全不碰该槽。
> (`zero_init` 属性必须与 `section` 同给,否则 armcc 视其为"带初值 RW",scatterload 会拷初值抹掉保留内容。)
**执行**:`@build` 0/0 → 单测 39/39 → 烧录(`reset.bat flash`,`Programming Complete`)→ 空 STOR 复现。
**读数(复位后开机第一行)**:
```
[----/--/-- --:--:--][APP] reset cause: IWDG (independent watchdog) (CSR=0x24000000)
PREVRUN seg=8 seq=1814443510 t=38069
```
**结论(round 8a)**:
1. **`seg=8` = `TASK_WD_SEG_STACK` = `stack_probe_poll()`** —— defaultTask 主循环**最后进入的是栈水位观测段**,
再没走到下一轮的 `seg=1`(poll)。`seq` 为大随机值 → 反证 `.noinit` **确实未被零初始化**(保留成功)。
2. 串口在该段**被切断在 `[STACK]` 报告的第 2 行**(`[STACK] default …` 完整、`[STACK] net` 半行)→
defaultTask 冻结在 `stack_probe_poll()` 的 `DBG_INFO` 突发行内。
3. **无 `[ERR] STACK OVERFLOW`、无 HardFault**(复位原因是 IWDGRSTF 而非 SFTRSTF)→ 排除栈溢出与故障。
4. 停喂前最后快照 `snap=0x00000007`(缺 **UI + TIMESYNC**)→ 两个任务**更早**停摆(UI 为 BelowNormal,
与"CPU 被更高优先级上下文抢占"的形态一致)。
**机理(唯一自洽解释)**:`stack_probe_poll()` 的 `DBG_INFO` → `con_append` → `dbg_console_drain`
→ **`HAL_UART_Transmit(..., 100ms)`**。该超时的实现依赖 `HAL_GetTick()`(= `uwTick`,
由 **TIM7** 的 `HAL_TIM_PeriodElapsedCallback` 递增,见 `Src/main.c:216`)。
`con_append` 的"缓冲满等待"(`vTaskDelay(1)` × 200)与 `HAL_UART_Transmit` 的超时**都只靠时间源兜底**——
一旦**时间源停摆**,两者都会**永久阻塞**(HAL 的超时判据永不成立 → 死等 TXE)。
这与"冻结在 console 行内 + 无 STALL + IWDGRSTF + UI/timesync 先饿死"**全部吻合**。
⇒ **round 8b 方向:不是"分片节拍",而是"时间源停摆 + HAL 阻塞调用无硬上界"两件事。**
### 0.5.4.3 round 8b~8f 结论(2026-10-02 台架)——**根因不是"时间源停摆",是 IWDG 超时被静默配成默认值(短 5 倍)**
原计划的两步已全部执行完,但**第 2 步的实测判据推翻了自己的前提**:
| 步骤 | 状态 | 说明 |
|---|---|---|
| ① console 加与时间源无关的硬上界 | ✅ 完成(round 8b) | `App/log/console_log.c`:`con_tx_bounded()` 用 `DWT->CYCCNT` 做 100ms 硬上界、`con_append` 用 200ms 硬上界。**保留**——消除"tick 停摆 → `HAL_UART_Transmit` 死等"这条单点,属净收益 |
| ② tick 存活性探针 + 定位停摆源 | ✅ 完成(round 8b/8c/8e) | 槽与 `TICKRUN uw/n/duw` 上报已落地,**但结论与原假设相反**(见下) |
| ③ timeSyncTask 200ms 等待分片喂心跳 | ✅ 完成 | 见 `task_watchdog.c` 的 `wd_required_mask()` 说明 |
#### 关键更正:`duw` 小 ≠ "同刻停摆"
`duw = uw − 进段时刻`,而 `uw` 是 TIM7 的**最后一次**跳——它必然就发生在复位前一刻
(IWDG 复位即停 TIM7)。所以 `duw=9..41` **只说明** defaultTask 最后一次进段打点就在复位前
9~41ms,**不能推出时间源停摆**。round 8b 据此写下的"假设 2:poller 自身停摆"是**误判**:
实测 `n`(TIM7 中断计数)一路递增到复位那一刻,**时间源从未停摆**,系统全程健康,
不存在"谁屏蔽中断/谁在 ISR 长驻"。
#### 真正的根因
`Src/iwdg.c` 为实现"延后启动"删掉了 `HAL_IWDG_Init()`(`IWDG-DEFER-START-ANCHOR`),
而 `HAL_IWDG_Init()` 是**唯一**会把 `hiwdg.Init.Prescaler/Reload` 写进 IWDG 寄存器的路径。
删掉后 `MX_IWDG_Init()` 只填结构体;`App/util/task_watchdog.c` 里的
`WD_IWDG_PRESCALER` / `WD_IWDG_RELOAD` 只被 `#define` 出来、**从未写进寄存器**。
IWDG 于是停在**硬件复位默认值**(SWD 实测读数 `PR=0`、`RLR=4095`):
| | 分频 | RLR | 超时(LSI 32k~47k) |
|---|---|---|---|
| **设计值**(`WD_IWDG_*`) | /64 | 1249 | **1701~2498 ms** |
| **实机值**(复位默认) | /4 | 4095 | **349~512 ms** |
短约 **5 倍** → `WD_MISS_TOLERANCE`(8×100ms=800ms)**永远等不到**就被 IWDG 复位,
停喂判定分支成为**死代码**(这正是 §0.5.4.1"STALL 从未执行"的真正原因);且任何 >400ms
的心跳缺口都直接误复位——空 STOR 截断 → FTL flush/GC 使 NET/FTP/UI 心跳缺 ~400ms → 复位。
**round 3~8 的"空 STOR 必复位"是同一个根因。**
#### 免烧录的决定性验证
用 `ST-LINK_CLI -W32` 在**一次**调用内把 PR/RLR 现场改成 4/1249
(`KR=0x5555 → PR=4 → RLR=0x4E1 → KR=0xAAAA`;分多次调用会超过 ~400ms 超时、中途被复位),
再跑同一个空 STOR:**由 5/5 复位变为 100% 成功**
(`226 Closing data connection.` + `[FTP] UPLOAD '/log/selftest.log' 0 bytes OK`,
`n` 全程递增、无复位;`MISS start snap=0x27` 仍出现但被 800ms 容忍吸收)。
> ⚠️ 现场改寄存器只是**验证手段**(IWDG 参数随复位丢失);真正的修复是让固件每次启动都写。
#### 修复
`App/util/task_watchdog.c` 的 `wd_platform_start()`(`__weak`,**不在 CubeMX 生成区**,
重生成不丢)补上按设计值配置 PR/RLR:解锁 → PR → 等 `SR.PVU` 清 → RLR → 等 `SR.RVU` 清
→ `0xCCCC`。键值与同步上界提为具名宏(`WD_IWDG_KEY_*` / `WD_IWDG_SYNC_SPIN`)。
主机侧单测以强符号覆盖该钩子,**不受影响**。
#### 取证工具的三个硬知识(后续 round 必读,可省一天)
1. **`-R32` 不会 halt 内核**(`-CoreReg` 才 halt,实测约 30ms/次且可重复)→ 用 `-R32` 做
**无扰动**存活性采样,只在需要 PC 时用 `-CoreReg`。(round 8c 每 0.43s 采一次样,
而"停摆→复位"窗口实测只有 ~150ms —— **采样周期长于窗口**才系统性错过,不是采不到;
另外 halt 期间 TIM7 ISR 不会执行,会让 `n` 出现"假停顿",须用非 halt 读法甄别。)
2. **`-R32 <addr> <count>` 的 `count` 单位是「字节」**(`-R32 0x1000FF00 16` 得 4 个字)。
按字理解会把 `.noinit` 槽**静默截断**(round 8b/8c 因此只读到 magic+段号)。
3. **`-W32 <addr> <data>` 的 `data` 按「十六进制」解析**(写 `1249` 会变成 `0x1249`,
被 12 位 RLR 截断成 `0x249`=585)。要写十进制 1249 得传 `0x4E1`。另:`IWDG->KR` 是
**只写**寄存器(读回恒 0),故 `-W32` 写 KR 必报 `Error!`——那是 ST-LINK 的写-回读
校验失败,**写入其实成功**。
> ⚠️ 仍**不要** `git checkout`/stash;round 1~8f 全部改动未提交。
### 0.5.5 之后:P0-B 出口闭环(判据见本文件各节)
> **状态:已闭环(2026-10-02 20:40)** —— 全量 `@test --require-selftest-pass` 退出码 0、
> 标记全绿、`[OK] selftest: no fault record`;ST-LINK 回读 `No difference found`。
> 闭环途中额外挖出并修复了比原 P0-B 更严重的存储 P0 —— **round 9:dhara resume 回滚**,
> 见 §0.5.5.1(它是"清台账清不掉"的真正根因,原 §0.5.5 第 1 步已被其推翻)。
1. **清台账** —— **不需要再"flush 诱导"**。原第 1 步(空 STOR + flushchk)清不掉,根因
不是 FTL 页缓存,而是 §0.5.5.1 的 resume 回滚:**回绕之后的任何写入在复位后整段丢弃**,
再多 flush 也救不回来。修复后 `DELE /log/selftest.log` 一次即持久。
- 实测:2048 B `STOR /log/probe.bin` 复位后**仍在**;`DELE` 后复位**仍不在**;
台账现为空(`RETR /log/selftest.log` → 450 = 文件不存在)。
- ⚠️ §0.5.7 陷阱 1 的"FTL 单页缓存回滚"表述**已被 §0.5.5.1 修正**:真正的回滚点是
dhara checkpoint。`CTRL_SYNC` 早就把页缓存刷进了 dhara,丢的是 resume 之后的整段。
2. **全量 `@test`**(已执行,PASS):
`& '.venv\Scripts\python.exe' test/host/run_boot_test.py --require-selftest-pass
--device-ip 192.168.1.100`
- 退出码 **0**;标记全绿:`App init OK` / `Network Init` / `net init: OK` 均在,
`FAIL`/`ERROR`/`HardFault`/`[ERR]` 皆缺席;
- `[OK] selftest: no fault record (file missing or empty)` ← **P0-B 正式 PASS**。
- 开机日志另证两处修复生效:`[WD] armed: timeout ~2498ms (worst ~1701ms)`(= 设计值,
修复前被静默配成 ~349~512 ms)、`[NAND_FTL] Map head=368 tail=115400 epoch=1`。
- ⚠️ 配套修了 `test/host/run_boot_test.py` 的 `_fetch_selftest_log()`:原先只
`except OSError`,而 `ftplib.error_temp` 继承自 `Exception`——台账"仅故障才写",
干净设备上文件本就不存在,`RETR` 返回 **450** 会抛未捕获异常让 `@test` 在最后一步崩掉
(本该是 PASS 路径)。现已把"设备侧无此文件"单列为正常路径。
3. **回读**(已执行):`ST-LINK_CLI -c SWD -CmpFile "MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex"
0x08000000` → **`No difference found`** ✓
4. **uvprojx 取舍**:保留 `App\fs\vol_lock.c` 挂载 + `../App/fs` 包含路径两个**真实差异**,
丢弃 UV4 XML 格式 / `PackID` 噪声(判据见 `AGENTS.md`)。
5. **文档销项**(✅ 已于 2026-10-02 出口统一完成):
- ✅ `子系统设计/看门狗设计说明.md`:§4.5 决策 C 必配项(PR/RLR 必须被真正写进寄存器)、
§5.3(`wd_iwdg_init()` 从未实现的事实)、变更记录写入 round 1~8f 修复链;
- ✅ `子系统设计/控制台单写者日志设计说明.md`:新增 **§12 ORE 僵尸通道**(台架 gen-288
实证 + 缓解/根治边界)+ §11 两条 10-02 时间线 + §13 新增 **O4**(TX-only 根治待办);
- ✅ `架构设计/STM32F4-Base存储架构说明.md`:新增 **§5.4.1 resume 的 epoch 锚点**
(round 9 根因与现行实现);
- ✅ `待处理内容.md`:新增 **S-3**(TX-only 根治)/ **S-4**(dhara 重试自旋)/
**S-5**(全序列卷锁)/ **S-6**(✅ ~~round 9 resume 回滚~~ 已修);
- ✅ 本文件:§0.5.5 结论、新增 **§0.5.5.1**(round 9)、§0.5.7 陷阱 1 修订。
6. **提交**:✅ 已完成 —— `930215f` `修复IWDG超时未配置与dhara resume回滚…`(分支 `v2-on-ui`,
33 文件 / +1874 −93,预提交全绿:`watchdog-unit-test` / `cubemx-survival` /
`test-suite-switch` 均确认产品构建)。
7. 之后按 §2.2/§3 起进入 **A1~A4** 与 **P1 批次**;`待处理内容.md` S-3~S-5 为本次遗留。
### 0.5.5.1 round 9(2026-10-02 台架):dhara resume 回滚 —— 复位后所有写入全部丢失
> 这是清台账清不掉的**真正根因**,且与 IWDG(§0.5.4.3)**互相独立**:即使看门狗完全修好,
> 只要这一条不修,日志/配置/FTP 上传在**任何**复位后都会丢。
**现象**:IWDG 修好后,清台账仍然"清了又回来"——`DELE /log/selftest.log` 后 `SIZE` 立刻
报 550(不存在),复位后又变回 26112 B / 408 条。进一步实测:
`STOR /log/probe.bin` 无论 **2048 B 还是 65536 B,复位后都消失**;`run.log` 永远停在 92 条
(每次开机只留下"最后一次"的 `power on`,此前各次的记录全被回滚)。
→ 既不是 DELE 专有,也不是"没跨 checkpoint",而是**整个卷在复位后回到旧状态**。
**取证链**(逐步缩小,不猜):
1. `f_unlink → sync_fs → disk_ioctl(CTRL_SYNC)` 全程无 `[ERR][NAND_FTL]`
(`@test` 也确认 `[ERR]` 缺席)→ **不是写盘报错**。
2. 给 `disk_ioctl` 的 `CTRL_SYNC` 打点(head/tail/tail_sync/dirty/count)实测:
`dirty=1→0`,head 70→80、82→96,**每次都跨过 checkpoint 头页**
(`push_meta` 在 `head ≡ 14 (mod 16)` 写 checkpoint)→ `dhara_map_sync()` **是有效的**,
写盘侧没问题。
3. **决定性证据** —— 复位前后 journal 状态对比:
| | head | tail | epoch |
|---|---|---|---|
| 复位前(`CTRL_SYNC` 之后) | 96 | 115143 | 1 |
| 复位后(`resume` 完成) | **130752** | **115068** | **0** |
head 倒退 92 页、tail 倒退 75 页 → **`dhara_journal_resume()` 恢复回了旧 checkpoint**。
4. 直读 NAND 两端的 checkpoint 头页(页 15 与最高块页 15)确认:
`CHK-FIRST: Dha epoch=1`(块 0 = **最新**,因为环已回绕);而 resume 锚定的是 **epoch=0**
的旧 checkpoint。
5. **旁证(最容易踩的坑)**:`[NAND_FTL] Map resumed: 11773/96064 sectors used` 在
10:05~12:17 的**每一次开机都一字不差**。覆盖写场景 `m->count` 不变,所以这个指标
**看不出回退**;必须看 head/tail/epoch。为此已在开机日志保留一行
`Map head=… tail=… epoch=…`(纯内存字段、零 I/O)。
**根因**(`Lib/dhara/journal.c`,**2026-09-20 本地补丁引入的回归**):
环**回绕之后**,"最高地址"的 checkpoint 属于**上一圈**(旧 epoch),而**最新** checkpoint 落在
**低地址**(块 0 一侧,epoch = 旧 + 1)。2026-09-20 那次补丁为修"自愈卷重格式化每次开机丢失",
把 epoch 锚点改成 `find_last_checkblock_any()`(= 最高地址块)—— **恰好在回绕后取到旧 epoch**;
随后 `find_last_checkblock()` 用这个旧 epoch 过滤 → 选中旧块 → `find_head()` 在旧块里找空页 →
journal 整体回退到回绕之前 → **回绕之后写的一切(日志、配置、FTP 上传)复位即丢**。
**修复**:锚点改为"比较两个候选 epoch 谁更新"(`Lib/dhara/journal.c` 内附详细注释):
| 两候选 epoch 关系 | 含义 | 锚点 |
|---|---|---|
| `eFirst == eLast` | 未回绕 | 相同,任取 |
| `eFirst == eLast + 1 (mod 256)` | **已回绕,低地址更新** | **`eFirst`** |
| 其它 | 低地址是上一圈残留 | `eLast`(保留 2026-09-20 修复的语义) |
**验证**:`RSUM head=96 tail=115142 epoch=1`(回退消失)→ 2048 B STOR 与 DELE 均跨复位存活 →
`Map resumed` 计数从冻结的 11773 变为 **11774**(开机记录真正落盘)→ 全量 `@test` PASS。
---
### 0.5.6 设备 / 仓库 / 证据快照(2026-10-02 17:05)
| 对象 | 状态 |
|---|---|
| 设备 | 运行 **round-6 镜像**(含 console 故障冷却),已过 17:00:26 误复位并重启,开机标记全绿,已进 standby;`[STACK]`/`heap` 健康(`heap free=8688 min_ever=4728`,6 任务 OK) |
| `/log/selftest.log` | **26176 B = 409 条 × 64 B**;最后一条 = `2026-10-02 09:00:24 … watchdog reset`(本轮误复位记录,**已提交落盘**)→ **S7 必 FAIL,须先清台账** |
| `/log/run.log` | 5888 B = 92 条;最后一条 = `07:24:16 reboot`(此后各次开机的 run.log 记录均被复位回滚,印证 FTL 缓存语义) |
| 工作区 | 19 个文件已改 + `App/fs/` 新增(round 1~6 修复集,**未提交**);HEAD = `d1a2eaa`;分支 `v2-on-ui` |
| 本地证据副本 | `C:\Users\67242\AppData\Local\Temp\opencode\`:`selftest_409_20261002_1700.log`(26176B/409 条,含 09:00:24 watchdog reset)、`runlog_20261002_1700.log`、`selftest_log_408_backup_20261002.log`(26112B/408 条旧态) |
### 0.5.7 本窗口实测的关键陷阱(必读,照做可少绕一天)
1. **写入"复位后消失"的两层原因**(本窗口最大坑,2026-10-02 修订):
- **L1 dhara checkpoint 回滚(主因,见 §0.5.5.1)**:环回绕后 `dhara_journal_resume()`
会恢复旧 checkpoint,**之后写的一切整段丢弃**。这是"台账清了又回来""probe.bin 复位就没"
的真正根因,**flush 诱导救不了**(已修)。
- **L2 FTL 单页写回缓存**:任何磁盘改动(截断/新建/追加)在**写到另一个 LPN / 另一个文件
或 `CTRL_SYNC`** 之前只存在于 RAM(`nand_ftl.c` 的 `s_cache_buf/s_cache_dirty`);
硬复位(含 IWDG 误复位)会丢失最后一次未跨页的写(`N-14` 已登记;`CTRL_SYNC` 已实现,
但**没有周期性/掉电前强制 flush**,属残余风险)。
→ 排查写入丢失时**先看 §0.5.5.1 的 head/tail/epoch**(开机日志 `Map head=… tail=… epoch=…`):
head 复位后倒退 = L1(resume 错);head 正常 = L2(缓存未刷)。
日常"清台账/写证据后写另一个文件"的 flush 诱导只对 L2 有效。
2. **FTP 路径**:lftpd 相对路径基准是**根目录**(`SIZE selftest.log` → 550),一律用
绝对路径 `/log/xxx`;`LIST /log` 实测列的是**根目录**内容(lftpd 怪癖)——查状态只用
`SIZE` / `RETR`。
3. **0 字节 STOR 的数据连接**存活期仅 ~10~50ms,短于芯片/网络层轮询粒度 → 芯片只见
`DISCONNECTED (chip tcp=0x00)` → accept 失败(425 或控制连接被重置);本轮实测客户端
`PASV→STOR` 间隔达 **11 s**(数据口 LISTEN 半开)。这是**与看门狗误复位不同的另一类
问题**(CH395F 数据通道);若修复后仍复现 425,转 §6(P2-B / N-2/N-3)专项。
4. **串口采集铁律**:`open` → `clear` → 烧录/复位 → `wait-idle --timeout 90 --settle-ms 8000
--max-ms 45000` → **直接 Read/Grep `tools/serial_tool/state/serial.log`**
(`log --grep` 子命令不可信);daemon 残留 → `taskkill /F /PID <旧>` 后重新 `open`。
5. **只用 `.venv\Scripts\python.exe`**(PATH 里的 `python` 是 msys64);每次烧录后**必须**
ST-LINK 全片回读 `No difference found`。
6. **误复位会使台账 +1 条** `watchdog reset`:取证/修复期间台账只会增长,
属预期(清台账留到最后一次修复后的出口闭环)。
7. **未提交改动是 round 1~6 的全部成果**:禁止 `git checkout` / `git stash` / `git clean`,
提交时机 = P0-B 出口(§0.5.5 第 6 步)。
---
## 1. 环境预检(P0-A,每次上台架前必跑)
## 2. 环境预检(P0-A,每次上台架前必跑)
> **目的**:避免"改动没问题、台架有问题"的误判(历史上多次把接线/权限问题误判为代码缺陷)。
@@ -454,10 +42,10 @@ journal 整体回退到回绕之前 → **回绕之后写的一切(日志、
| A1 | 工作区干净 | `git status --short` | 无输出(或有明确待提交项) |
| 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` | 产品态基线**随已接受的修复增长**:2026-10-02 台架窗口实测 `0x000402B4` = **262,836 B**(初版基线 262,100 B + round 1~6 修复增量);明显阶跃到 ≈**266,540 B** = 停在套件态 → 重跑 A2 |
| 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)**;明显阶跃 ≈3.7KB 及以上(10-02 实测产品 262,836 → 套件 266,540)= 停在套件态 → 重跑 A2 |
| A5 | 全量静态门禁 | `pre-commit run --all-files` | **16/16 Passed** |
| A6 | ST-LINK 探针可见 | `& "MDK-ARM\reset.bat" list`(CLI 经 `DTU_STLINK_CLI` / PATH 解析) | 列出探针 SN `0B250D12A218303030303032` |
| A7 | 串口端口正确 | 检查 `.opencode/serial_config.json` 的 `portName` | 与设备管理器一致(默认 COM7) |
| 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` 可读 |
> ⚠️ **A4 的陷阱**(2026-10-02 实测):`@build` 是**增量**构建,`run_all_suites.py` 跑完
@@ -468,32 +56,26 @@ journal 整体回退到回绕之前 → **回绕之后写的一切(日志、
---
## 2. 产品 `@test` 闭环 + 自检结果判定(P0-B)
## 3. 产品 @test 闭环 + 自检结果判定(P0-B)
### 2.1 S7:`@test` 闭环增加"自检结果"判定
### 3.1 自检结果判定(S7,已落地并投用)
**现状证据**:
- `App/log/sys_log.h` 定义 `SYSLOG_SELFTEST` 记录(定长 64B:19B 时间戳 + 空格 + 10B epoch + 空格 + 28B 描述)。
- `syslog_post(SYSLOG_SELFTEST, ...)` 全库 **18 个调用点**(见 §2.3 清单),
但 `test/host/verify_log.py` **只解析串口日志**,**从不读 `/log/selftest.log`**。
- `sys_clock_init` 在 RTC 故障时返回 -1 并触发"时钟芯片异常"自检——**当前闭环捕获不到**。
**现状**:`@test` 闭环除串口标记外,还判定设备侧自检结论(v1 只记故障、不记通过):
**目标**:`@test` 闭环不仅看串口标记,**还看设备的自检结论**:
自检有 no-go 项 → FAIL;有 warn 项 → 打印但 PASS。
- `verify_log.py --require-selftest-pass`(可带路径参数):退出码 0 = PASS / 1 = FAIL;
判定语义 = `selftest.log` **缺失/0 字节 → 无故障 → PASS**;`filesize/64 > 0`(任一 64B 定长记录)
= 有故障 → FAIL,并打印故障条目(时间戳 + 描述)便于定位;
- `run_boot_test.py --require-selftest-pass`:未给本地副本时**自动经 FTP 抓取** `/log/selftest.log`
(设备侧 450"文件不存在"按正常路径处理——"仅故障才写"语义下干净设备本就没有该文件;
2026-10-02 修复:原实现把 450 当异常抛出,会让本该 PASS 的闭环在最后一步崩掉)。
**判据**(可观测、量化):
1. `verify_log.py --require-selftest-pass` 退出码 0 = PASS / 1 = FAIL;
2. 判定规则:`selftest.log` 中**出现任一 `SYSLOG_SELFTEST` 记录** = 有故障 → FAIL(v1 只记故障,不记通过);
3. 文件不存在/大小为 0 = **无故障 → PASS**(与"有问题才记录"语义一致)。
**执行**:
**执行步骤**:
```bash
# 1) 先确认设备已跑过一轮(selftest.log 已落盘)
# 通过 FTP 下载(推荐,验证 R5 可读性同时取数):
# 客户端连 <设备IP>:21,匿名/配置账号,GET /log/selftest.log
# 或经串口工具触发 FTP 拉取(如已有脚本)。
# 一键闭环(selftest.log 自动 FTP 抓取;无故障记录 = PASS)
& '.venv\Scripts\python.exe' test/host/run_boot_test.py --require-selftest-pass --device-ip 192.168.1.100
# 2) 对本轮串口日志 + selftest.log 联合判定(已落地;`--selftest-log` 可省,脚本自动 FTP 抓取):
# 或手动联合判定(本轮串口日志 + selftest.log 本地副本)
& '.venv\Scripts\python.exe' test/host/verify_log.py \
--log-path <本轮串口日志> \
--require-selftest-pass \
@@ -501,34 +83,21 @@ journal 整体回退到回绕之前 → **回绕之后写的一切(日志、
# 判据:退出码 0;输出含 "[OK] selftest: no fault record"
```
**S7 落地形态(✅ 2026-10-02 已落地并提交 `4a4cd29`;当前因台账未清空拿不到 PASS,见 §0.5)**:
### 3.2 待上板事项 A1~A4(代码状态 2026-10-02 核对)
| 步骤 | 内容 | 备注 |
|---|---|---|
| 1 | `verify_log.py` 增 `--require-selftest-pass`(可带路径参数) | 纯标准库;与既有 `verify()` 同构 |
| 2 | 判定语义:文件缺失/0 字节 = PASS;`filesize/64 > 0` = 有故障 = FAIL | 定长 64B → O(1) 计数 |
| 3 | 打印故障条目(时间戳 + 描述)便于定位 | 复用 `sys_log.h` 拆分逻辑思路 |
| 4 | `run_boot_test.py` 增 `--selftest-log` 透传 + `--require-selftest-pass` | 保持一键闭环 |
| 5 | 文档:`自动化测试流程.md` / `测试开发标准流程.md` 增参数说明 | 门禁决策表同步 |
> 来源:`审查记录/下一步任务规划.md` 阶段 A。各项**代码状态以当日对现网代码的核对为准**:
**风险/回滚**:纯读取判定,零设备风险;若 FTP 取数不便,可降级为"仅扫串口自检标记"(弱化版)。
### 2.2 阶段 A 存量验证债(A1~A4,同轮台架做最划算)
> **来源**:`审查记录/下一步任务规划.md` §2 阶段 A。这些"代码已改、判据需真机"的项,
> 与 S7 共用**同一轮台架窗口**,一轮解决。
| 序 | 事项 | 现状证据 | 判据 | 命令/入口 |
| 序 | 事项 | 代码状态(2026-10-02 核对) | 上板判据 | 命令/入口 |
|---|---|---|---|---|
| **A1** | **P0-1 `cfg_file` 堆分配改造真机验证** | 模拟器用宿主堆,**照不出** `Heap_Size=0x200`(512B)约束下的真实分配行为;P0 修复的**最后一道门** | 串口日志出现 `/config.ini` **`load: OK`**;Forbidden `FAIL`/`[ERR]` 零命中;**保存配置后复位,值持久** | `& '.venv\Scripts\python.exe' test/host/run_boot_test.py` |
| **A2** | **P1-8 网络重建失败恢复** | 一次 PHY 抖动 → 网络永久不可用且**零日志**,现场无法定位 | 拔插网线/改 IP 触发重建;失败路径写 syslog 自检 + 30s 周期重试;**不自驻留死循环**;`@test` PASS | `App/task/net_task.c` reconfig 路径;UI 网口设置页改 IP |
| **A3** | **E-34 CH395F 字节命令超时** | `ch395f.c:181/195/210` 三处 `HAL_MAX_DELAY` → SPI 异常时 netTask **永久挂死** | 改为有限超时(100~500ms)+ 返回值检查;构建 0/0;**正常通信零回归**(FTP 传输正常) | 代码改后 `@build` + `@test` |
| **A4** | **E-04 `remo_send` 返回值** | `ui_net_sink.c:103` 丢弃 `net_send` 返回值;短写 → 客户端 CRC 失败 → **画面卡死黑屏,设备侧日志全正常** | 改返回 `int` 校验 `ret == len`;短写 `DBG_ERROR` + 连续失败强制重连;**UI 推流压力下无黑屏** | UI 远显推流压力回归 |
| **A1** | **P0-1 `cfg_file` 堆分配改造** | **已落地**(分配收进 `cfg_store_alloc/free` 平台后端;模拟器用宿主堆,**照不出** `Heap_Size=0x200`(512B)约束——真机是最后一道门) | 串口日志出现 `/config.ini` **`load: OK`**;Forbidden `FAIL`/`[ERR]` 零命中;**保存配置后复位,值持久** | `& '.venv\Scripts\python.exe' test/host/run_boot_test.py` |
| **A2** | **P1-8 网络重建失败恢复** | **部分落地**:首启链路超时已有 `comms fault` 自检(`net_task.c` L131);重建(reconfig)失败路径仅一条 `DBG_ERROR`,**无自检记录、无周期重试**;首启 `net_init` 失败 = netTask 无限驻留(L206-210) | 补码后:失败路径写 syslog 自检 + 30s 周期重试;**不自驻留死循环**;恢复后各 TCP 服务据代次号重建监听;`@test` PASS | 补码 → `@build` → `@test`;UI 网口设置页改 IP;`test/host/e02_probe.py` |
| **A3** | **E-34 CH395F 字节命令超时** | **未落地**(`ch395f.c` L181/195/210 仍 `HAL_MAX_DELAY` → SPI 异常时 netTask 永久挂死) | 补码后:有限超时(100~500ms)+ 返回值检查;构建 0/0;**正常通信零回归**(FTP 传输正常) | 补码 → `@build` + `@test` |
| **A4** | **E-04 `remo_send` 返回值** | **未落地**(`ui_net_sink.c` L119 仍丢弃 `net_send` 返回值;短写 → 客户端 CRC 失败 → 画面卡死黑屏而设备侧日志全正常) | 补码后:改返回 `int` 校验 `ret == len`;短写 `DBG_ERROR` + 连续失败强制重连;**UI 推流压力下无黑屏** | 补码 → `@build` + UI 远显推流压力回归 |
> **A5(P1-7 MAC 写入顺序)**:**先查手册**(CH395F §9.2.1 INIT 是否锁存 MAC),
> 再决定改或文档化"无需调整"——**属离线可做部分**,不在本文强绑。
### 2.3 自检项清单(S7 判定对象,18 处)
### 3.3 自检项清单(S7 判定对象,18 处)
> 来源:全库 `syslog_post(SYSLOG_SELFTEST, ...)` 调用点。**这是 S7 的判定覆盖面基准**。
@@ -544,12 +113,13 @@ journal 整体回退到回绕之前 → **回绕之后写的一切(日志、
---
## 3. IWDG 故障注入与长跑(P1-A,N-03 收尾)
## 4. IWDG 故障注入与长跑(P1-A,N-03 收尾)
> **来源**:`子系统设计/看门狗设计说明.md` §10 步骤 7~10(⏳ 待台架);
> `待处理内容.md` N-03。**代码已落地编译通过**,缺的是**故障注入 + 长跑**。
> 来源:`子系统设计/看门狗设计说明.md` §10 步骤 7~10;`待处理内容.md` N-03。
> 判定状态机与 IWDG 配置(PR/RLR 按设计值写入)已落地并经真机验证(2026-10-02,`930215f`,
> 见设计说明 2026-10-02 变更记录);缺的是下述**故障注入 + 长跑**验收。
### 3.1 三个验收用例
### 4.1 三个验收用例
| # | 用例 | 注入方法 | 通过判据 |
|---|---|---|---|
@@ -559,7 +129,7 @@ journal 整体回退到回绕之前 → **回绕之后写的一切(日志、
> ⚠️ **V5 是核心验收项**:误杀比不杀更有害。必须在**真实业务负载**下跑足时长。
### 3.2 执行步骤
### 4.2 执行步骤
```bash
# --- V2/V4(故障注入,需临时改码)---
@@ -595,21 +165,23 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L
---
## 4. 堆低水位告警实测(P1-B,S11)
## 5. 堆低水位告警实测(P1-B,S11)
**现状证据**:`上一轮实测 free=8,784 / min_ever=4,848 B`(初始 `configTOTAL_HEAP_SIZE` 48KB),
**现状证据**:上一轮实测 `free=8,784 / min_ever=4,848 B`(初始 `configTOTAL_HEAP_SIZE` 48KB),
**min_ever 已低于 5KB**,余量不到 10%——新增任务/缓冲可能触发分配失败。
现有代码仅有周期性 `[STACK]` 报告(每周期打印 `heap free/min_ever`),**无阈值告警、不写自检记录**。
**目标**:在 `defaultTask` 低频循环(LED 闪烁节拍)里检查 `xPortGetMinimumEverFreeHeapSize()`,
低于阈值(建议 **4096 B**)时 `DBG_ERROR("[HEAP] low water: %u B")` + 写 syslog selftest。
**待补代码**(约 15 行 + 1 个编译期常量,在 `defaultTask` 低频循环(LED 闪烁节拍)内):
检查 `xPortGetMinimumEverFreeHeapSize()`,低于阈值(建议 **4096 B**)时
`DBG_ERROR("[HEAP] low water: %u B")` + 写 syslog selftest。
**判据**:台架长跑(复用 §3.2 V5 窗口)下,若发生低水位 → 串口出现告警行 + `selftest.log` 有记录;
**判据**:台架长跑(复用 §4.2 V5 窗口)下,若发生低水位 → 串口出现告警行 + `selftest.log` 有记录;
若全程未触发 → 记录"当前负载下 min_ever 仍高于阈值"作为基线。
**执行步骤**:
```bash
# 1) 代码改动(约 15 行 + 1 个编译期常量)在 defaultTask 低频循环内
# 2) @build 0/0 后,与 §3.2 V5 长跑合并执行
# 2) @build 0/0 后,与 §4.2 V5 长跑合并执行
# 3) 长跑结束读取 min_ever:
# - 串口 [HEAP] 告警行(若有)
# - 或 FTP 取 /log/selftest.log
@@ -621,7 +193,7 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L
---
## 5. GD5F 错误通道统一(P2-A,S9 = 上游 C2)
## 6. GD5F 错误通道统一(P2-A,S9 = 上游 C2)
> **来源**:`错误码传播审计.md` 第二档 #1(E-10/E-42/E-53/E-54/E-56/E-57);
> `测试与架构改进建议.md` §2.3 / §4.2 四要素骨架。
@@ -667,9 +239,9 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L
---
## 6. 网络稳定性与基线重锁(P2-B)
## 7. 网络稳定性与基线重锁(P2-B)
### 6.1 P1-8 网络重建失败恢复(与 §2.2 A2 同一项,此处给网络专项视角)
### 7.1 P1-8 网络重建失败恢复(与 §3.2 A2 同一项,此处给网络专项视角;代码状态见 A2 行)
**判据**:拔插网线/改 IP 触发重建后——
- 失败路径**写 syslog 自检**(不再零日志);
@@ -678,7 +250,7 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L
**验证手段**:`test/host/e02_probe.py`(网络探针)+ UI 网口设置页改 IP。
### 6.2 N-2:49s 全系统日志静默复现与定位
### 7.2 N-2:49s 全系统日志静默复现与定位
**现状证据**(登记簿 N-2):2026-09-22 台架实测,串口日志 **49s 无行**
(testTask/netTask/[STACK] 行全缺),期间 net 栈照常处理外部连接 → **日志管线停滞**,
@@ -691,9 +263,9 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L
- 复现 49s 静默并定位到具体卡点(NAND GC / UART TX);
- 实现**队列满丢弃计数**(至少一个 `DBG_ERROR` 或计数器经 UI/日志可见)。
**执行步骤**:长跑(§3.2 V5)期间周期性打点 + 对照 `CH395F` 无关的 SD2506 侧日志判别卡点。
**执行步骤**:长跑(§4.2 V5)期间周期性打点 + 对照 `CH395F` 无关的 SD2506 侧日志判别卡点。
### 6.3 N-3:L5 队列往返基线重验
### 7.3 N-3:L5 队列往返基线重验
**现状**:L5 基线(1223/988µs)属**分片时代**;L3 对该路径逐位不变 → 机制上无影响,
但严格重锁需跑一轮 Phase 13(**需 PC peer**)。
@@ -702,9 +274,9 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L
---
## 7. 工程卫生(P3)
## 8. 工程卫生(P3)
### 7.1 S5:`test/` 代码宽松静态检查
### 8.1 S5:`test/` 代码宽松静态检查
**现状证据**:`test/` 完全豁免 pre-commit("自律"要求),而 `test/board/` 下 **6300+ 行 C 代码**
(实测 `.c`+`.h` 合计 6,313 行)**且编入固件**。曾出现 `s_rx_buf` 使用先于声明。
@@ -724,7 +296,7 @@ cppcheck --enable=warning --error-exitcode=1 test/ 2>&1 | tee test_cppcheck_base
**判据**:`test-cppcheck` 在 CI PASS;新增测试代码越界/未初始化被自动拦截。
> **注意**:若基线告警过多,**保持现状亦可接受**——此时应把该决策写明确("理由:存量 N 条")。
### 7.2 S4:用例追溯脚本
### 8.2 S4:用例追溯脚本
**现状**:文档声称 91 个 TC、代码有 222 个 `TEST_CHECK`(断言点数 > 用例数是正常的),
但**双向是否一一对应无人校验**。
@@ -736,7 +308,7 @@ cppcheck --enable=warning --error-exitcode=1 test/ 2>&1 | tee test_cppcheck_base
**判据**:一键列出双向差异清单;差异收敛后再考虑升为门禁。
### 7.3 待核实项:`TEST_SUITE_SELECTED` 未含 `TEST_SUITE_RTC`
### 8.3 待核实项:`TEST_SUITE_SELECTED` 未含 `TEST_SUITE_RTC`
**现状证据**(`test/board/test_config.h`):
- L45 互斥检查**已包含** `TEST_SUITE_RTC`(4 个套件互斥正确);
@@ -763,11 +335,54 @@ cppcheck --enable=warning --error-exitcode=1 test/ 2>&1 | tee test_cppcheck_base
---
## 8. 与既有文档的引用闭环
## 9. 实测陷阱(2026-10-02 台架窗口,照做可少绕一天)
1. **写入"复位后消失"的两层原因**(窗口最大坑,根因均已修复,详见模块文档):
- **L1 dhara checkpoint 回滚**(`架构设计/STM32F4-Base存储架构说明.md` §5.4.1):环回绕后
`dhara_journal_resume()` 恢复旧 checkpoint,**之后写的一切整段丢弃**
(已修:epoch 锚点改比较两候选的新旧)。
- **L2 FTL 单页写回缓存**:任何磁盘改动(截断/新建/追加)在**写到另一个 LPN / 另一个文件
或 `CTRL_SYNC`** 之前只存在于 RAM(`nand_ftl.c` 的 `s_cache_buf/s_cache_dirty`);
硬复位(含 IWDG 误复位)会丢失最后一次未跨页的写(`N-14` 已登记;`CTRL_SYNC` 已实现,
但**没有周期性/掉电前强制 flush**,属残余风险)。
→ 排查写入丢失时**先看开机日志的 head/tail/epoch**(`Map head=… tail=… epoch=…`,
纯内存字段、零 I/O):head 复位后倒退 = L1(resume 错);head 正常 = L2(缓存未刷)。
日常"清台账/写证据后写另一个文件"的 flush 诱导只对 L2 有效。
2. **FTP 路径**:lftpd 相对路径基准是**根目录**(`SIZE selftest.log` → 550),一律用
绝对路径 `/log/xxx`;`LIST /log` 实测列的是**根目录**内容(lftpd 怪癖)——查状态只用
`SIZE` / `RETR`。
3. **0 字节 STOR 的数据连接**存活期仅 ~10~50ms,短于芯片/网络层轮询粒度 → 芯片只见
`DISCONNECTED (chip tcp=0x00)` → accept 失败(425 或控制连接被重置);2026-10-02 实测客户端
`PASV→STOR` 间隔达 **11 s**(数据口 LISTEN 半开)。这是**与看门狗误复位不同的另一类
问题**(CH395F 数据通道);若修复后仍复现 425,转 §7.1 专项。
4. **串口采集铁律**:`open` → `clear` → 烧录/复位 → `wait-idle --timeout 90 --settle-ms 8000
--max-ms 45000` → **直接 Read/Grep `tools/serial_tool/state/serial.log`**
(`log --grep` 子命令不可信);daemon 残留 → `taskkill /F /PID <旧>` 后重新 `open`。
5. **只用 `.venv\Scripts\python.exe`**(PATH 里的 `python` 是 msys64);每次烧录后**必须**
ST-LINK 全片回读 `No difference found`。
6. **误复位会使台账 +1 条** `watchdog reset`:取证/修复期间台账只会增长,
属预期(清台账留到最后一次修复后的出口闭环)。
7. **ST-LINK_CLI 取证三硬知识**(完整用法见 `ST-LINK_CLI_复位使用说明.md`):
- `-R32` **不会 halt 内核**(`-CoreReg` 才 halt,实测约 30ms/次且可重复)→ 用 `-R32` 做
**无扰动**存活性采样,只在需要 PC 时用 `-CoreReg`(halt 期间 TIM7 ISR 不执行,
会让中断计数出现"假停顿",须用非 halt 读法甄别)。
- `-R32 <addr> <count>` 的 `count` 单位是**字节**(`-R32 0x1000FF00 16` 得 4 个字);
按字理解会把读取**静默截断**。
- `-W32 <addr> <data>` 的 `data` 按**十六进制**解析(写 `1249` 会变成 `0x1249`,
被 12 位 RLR 截断成 `0x249`=585);要写十进制 1249 得传 `0x4E1`。另 `IWDG->KR` 是
**只写**寄存器(读回恒 0),`-W32` 写 KR 必报 `Error!`——那是 ST-LINK 的写-回读
校验失败,**写入其实成功**。
- 现场改寄存器(如 IWDG PR/RLR 验证手段)须在**一次调用**内完成整段键序列
(解锁→参数→键锁)——分多次调用会超过看门狗超时、中途被复位;寄存器随复位丢失,
只能用于验证,不是修复。
---
## 10. 与既有文档的引用闭环
| 本文项 | 上游文档 | 下游落地文档 |
|---|---|---|
| S7 自检判定 | `审查记录/测试与架构改进建议.md` §3.7 | `测试说明/自动化测试流程.md`(待补参数) |
| S7 自检判定 | `审查记录/测试与架构改进建议.md` §3.7 | `测试说明/自动化测试流程.md`(参数已落地,`4a4cd29`) |
| V2/V4/V5 | `子系统设计/看门狗设计说明.md` §10 | `待处理内容.md` N-03(销项) |
| S11 堆告警 | `测试与架构改进建议.md` §4.4 | `内存管理/RAM使用分析与优化方案.md`(基线更新) |
| S9 GD5F | `问题记录/错误码传播审计.md` 第二档 #1 | `问题记录/GD5F2GQ5UE_Trap_Records.md`、`测试说明/GD5F2GQ5UE_Test_Guide.md` |
@@ -782,9 +397,10 @@ cppcheck --enable=warning --error-exitcode=1 test/ 2>&1 | tee test_cppcheck_base
---
## 9. 变更记录
## 变更记录
| 日期 | 变更 |
|---|---|
| 2026-10-02 | 初版:无 MCU 窗口建立。汇总 P0-A 环境预检 / P0-B `@test`+S7 / P1-A IWDG V2/V4/V5 / P1-B S11 / P2-A S9 / P2-B P1-8+N-2+N-3 / P3 S5+S4+待核实项,共 **7 批**。每项含现状证据 / 目标 / 判据(量化)/ 执行步骤(精确命令)/ 风险回滚。18 处自检调用点清单 + 模拟器差距清单 §3 决策规则交叉引用 |
| 2026-10-02 | **台架窗口执行交接**:新增 **§0.5**(P0-A 通过;S7 已落地并提交 `4a4cd29`;P0-B 唯一阻断 = 0 字节 STOR → IWDG 误复位(round 3~6 后仍 5/5 复现);round 1~6 修复集摘要;**round 7 纯取证埋点设计 + 判据表**;P0-B 出口闭环(含 FTL flush 诱导式清台账);设备/台账/本地证据快照;**7 条实测陷阱**);§2.1 标注 S7 已落地、命令形态已确认 |
| 2026-10-02 | **文档改造**(同 `本地工具路径环境变量方案.md` 思路):删除一次性台架窗口状态(原 §0.5——round 1~9 取证全过程、快照、未提交警告:结论已沉淀 `看门狗设计说明.md`/`STM32F4-Base存储架构说明.md` §5.4.1/`控制台单写者日志设计说明.md` §12,修复随 `930215f` 提交)与规划框架(预估耗时/阶段措辞),重组为执行判据说明——§1 事项总览 / §2 环境预检 / §3 `@test` 闭环+自检判定+待上板项 / §4 IWDG 注入长跑 / §5 堆告警 / §6 GD5F 错误通道 / §7 网络基线 / §8 工程卫生 / §9 实测陷阱 / §10 引用闭环;A1~A4 代码状态按当日代码核对改写(A1 已落地 / A2 部分 / A3、A4 未补码);A4 基线更新 264,284 B、A7 端口更正 COM6;原 §0.5.x 的 8 处代码注释、`待处理内容.md` 3 行、`看门狗设计说明.md` 变更记录、`docs/README.md` 同步重指向模块文档 |