@@ -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` L 181/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` 同步重指向模块文档 |