Files
STM32F4-Base/docs/测试说明/实机测试规划.md
T

52 KiB
Raw Blame History

实机测试规划(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 变更记录留一行。


0. 一句话总览

台架窗口就绪后,按 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

每批出口:@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())

执行与判据:

# 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 38 的"空 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)(= 设计值, 修复前被静默配成 349512 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. 文档销项(P0-B 出口统一做):
    • 子系统设计/看门狗设计说明.md:§4.3/§4.4 白名单、§5.4、§6.1 埋点、§9/§10 状态、 变更记录写入五次误复位事件(07:56 ORE 僵尸 / 16:19 FTL-GC / 16:29 poller 饥饿 / 16:50 STOR 前停喂 / 17:00 本轮)+ round 1~6 修复链;
    • 子系统设计/控制台单写者日志设计说明.md:ORE 僵尸根因 + round-3 缓解 + round-6 冷却, 并登记"TX-only 结构根治"仍待办;
    • 本文件 §9 留一行;待处理内容.md 销项(① .ioc USART1 TX-only 结构根治 / ② dhara 重试自旋残留 / ③ 可选全序列卷锁;N-03 至 P1-A)。
  6. 提交:单行中文标题 <100 字符(git commit -F)→ push v2-on-ui; 预提交含 watchdog-unit-test / cubemx-survival / test-suite-switch(确认产品构建)。
  7. 之后按 §2.2/§3 起进入 A1~A4 与 P1 批次。

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 的数据连接存活期仅 1050ms,短于芯片/网络层轮询粒度 → 芯片只见 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,每次上台架前必跑)

目的:避免"改动没问题、台架有问题"的误判(历史上多次把接线/权限问题误判为代码缺陷)。

# 检查 命令 通过判据
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
A5 全量静态门禁 pre-commit run --all-files 16/16 Passed
A6 ST-LINK 探针可见 "C:\Program Files (x86)\STMicroelectronics\STM32 ST-LINK Utility\ST-LINK Utility\ST-LINK_CLI.exe" -c SWD -List 列出探针 SN 0B250D12A218303030303032
A7 串口端口正确 检查 .opencode/serial_config.json 的 portName 与设备管理器一致(默认 COM7)
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 跑完 套件后 .map 会停在测试套件体积;仅还原 test_config.h 不够,必须再编一次产品固件 (run_all_suites.py 已在 finally 内自动补这一步)。

预检失败处置:任一项 FAIL → 不上台架,先修到全绿(避免"带病测试"浪费台架窗口)。


2. 产品 @test 闭环 + 自检结果判定(P0-B)

2.1 S7:@test 闭环增加"自检结果"判定

现状证据:

  • 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 闭环不仅看串口标记,还看设备的自检结论: 自检有 no-go 项 → FAIL;有 warn 项 → 打印但 PASS。

判据(可观测、量化):

  1. verify_log.py --require-selftest-pass 退出码 0 = PASS / 1 = FAIL;
  2. 判定规则:selftest.log 中出现任一 SYSLOG_SELFTEST 记录 = 有故障 → FAIL(v1 只记故障,不记通过);
  3. 文件不存在/大小为 0 = 无故障 → PASS(与"有问题才记录"语义一致)。

执行步骤:

# 1) 先确认设备已跑过一轮(selftest.log 已落盘)
#    通过 FTP 下载(推荐,验证 R5 可读性同时取数):
#      客户端连 <设备IP>:21,匿名/配置账号,GET /log/selftest.log
#    或经串口工具触发 FTP 拉取(如已有脚本)。

# 2) 对本轮串口日志 + selftest.log 联合判定(已落地;`--selftest-log` 可省,脚本自动 FTP 抓取):
& '.venv\Scripts\python.exe' test/host/verify_log.py \
    --log-path <本轮串口日志> \
    --require-selftest-pass \
    --selftest-log <下载的 selftest.log 本地副本>
#    判据:退出码 0;输出含 "[OK] selftest: no fault record"

S7 落地形态(✅ 2026-10-02 已落地并提交 4a4cd29;当前因台账未清空拿不到 PASS,见 §0.5):

步骤 内容 备注
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 增参数说明 门禁决策表同步

风险/回滚:纯读取判定,零设备风险;若 FTP 取数不便,可降级为"仅扫串口自检标记"(弱化版)。

2.2 阶段 A 存量验证债(A1~A4,同轮台架做最划算)

来源:审查记录/下一步任务规划.md §2 阶段 A。这些"代码已改、判据需真机"的项, 与 S7 共用同一轮台架窗口,一轮解决。

序 事项 现状证据 判据 命令/入口
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 远显推流压力回归

A5(P1-7 MAC 写入顺序):先查手册(CH395F §9.2.1 INIT 是否锁存 MAC), 再决定改或文档化"无需调整"——属离线可做部分,不在本文强绑。

2.3 自检项清单(S7 判定对象,18 处)

来源:全库 syslog_post(SYSLOG_SELFTEST, ...) 调用点。这是 S7 的判定覆盖面基准。

文件 自检描述串
App/app/app_main.c watchdog reset / clock chip fault / NAND fault / ADC fault
App/cfg/cfg_file.c cfg heap alloc fail(×3) / cfg template write fail / cfg load oversized / cfg load read fail / cfg save read fail / cfg save overflow / cfg save write fail / cfg poll oversized
App/task/default_task.c filesystem fault(×2) / storage fault(×2)
App/task/net_task.c comms fault

联动:syslog_post 的两级自愈(L1 文件级重试 / L2 错误去重 s_fault_reported[]) 决定持续故障只留第一条——故 S7 判定"有记录即 FAIL"是充分的(不会被去重掩盖)。


3. IWDG 故障注入与长跑(P1-A,N-03 收尾)

来源:子系统设计/看门狗设计说明.md §10 步骤 7~10(⏳ 待台架); 待处理内容.md N-03。代码已落地编译通过,缺的是故障注入 + 长跑。

3.1 三个验收用例

# 用例 注入方法 通过判据
V2 任务饿死 临时在 netTask 循环插 while(1); 800 ms 后串口 [ERR] WATCHDOG: task stall mask=0x...;≤2.5 s 复位;重启后 [WD] reset cause=watchdog reset
V4 关中断死循环 临时插 taskDISABLE_INTERRUPTS(); while(1); ≤2.5 s 复位(检验 IWDG 独立于中断);cause=watchdog reset
V5 长期不误杀(核心验收项) FTP 大文件传输 + UI 菜单遍历 + 配置保存 + 网络重配 ≥30 min 零复位(重点验证 §4.4 白名单是否完备)

⚠️ V5 是核心验收项:误杀比不杀更有害。必须在真实业务负载下跑足时长。

3.2 执行步骤

# --- V2/V4(故障注入,需临时改码)---
# 1) 基线:跑一轮产品 @test 确认无复位
& '.venv\Scripts\python.exe' test/host/run_boot_test.py

# 2) 注入:按设计说明 §10 在 netTask 循环插 while(1);(或 taskDISABLE_INTERRUPTS(); while(1);)
#    —— 改完 @build,然后烧录
& "MDK-ARM\build.bat"
& "MDK-ARM\reset.bat" flash MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex

# 3) 抓串口日志,确认 [ERR] WATCHDOG task stall + 复位原因
# 4) 回读校验复位后镜像
"C:\Program Files (x86)\STMicroelectronics\STM32 ST-LINK Utility\ST-LINK Utility\ST-LINK_CLI.exe" \
    -c SWD -CmpFile MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex 0x08000000
#    判据:No difference found

# 5) 撤销注入,@build 回产品态,再跑一轮 @test 确认正常

# --- V5(长跑,需真实负载)---
# 1) 用 test/host/ftp_speed_test.py / ftp_up_dl_test.py 持续传输大文件
& '.venv\Scripts\python.exe' test/host/ftp_speed_test.py
# 2) 同时人工/脚本遍历 UI 菜单 + 反复保存配置 + 触发网络重配
# 3) 持续 ≥30 min,全程串口记录
#    判据:零复位(日志无 "reset cause=watchdog reset");业务不中断

风险/回滚:

  • 故障注入用例会复位设备多次——确认设备非在用/非已部署。
  • 长跑涉及 NAND 写入(FTP 上传)消耗 NAND 块——若设备已接近容量上限,改用只读传输。
  • ⚠️ R5 风险(设计说明 §9):NAND 擦写偶发超 800 ms 可能误复位——若 V5 出现偶发复位, 先按设计说明增大容忍次数或把存储操作改分段置位,不要盲目放宽全局超时。

4. 堆低水位告警实测(P1-B,S11)

现状证据:上一轮实测 free=8,784 / min_ever=4,848 B(初始 configTOTAL_HEAP_SIZE 48KB), min_ever 已低于 5KB,余量不到 10%——新增任务/缓冲可能触发分配失败。

目标:在 defaultTask 低频循环(LED 闪烁节拍)里检查 xPortGetMinimumEverFreeHeapSize(), 低于阈值(建议 4096 B)时 DBG_ERROR("[HEAP] low water: %u B") + 写 syslog selftest。

判据:台架长跑(复用 §3.2 V5 窗口)下,若发生低水位 → 串口出现告警行 + selftest.log 有记录; 若全程未触发 → 记录"当前负载下 min_ever 仍高于阈值"作为基线。

执行步骤:

# 1) 代码改动(约 15 行 + 1 个编译期常量)在 defaultTask 低频循环内
# 2) @build 0/0 后,与 §3.2 V5 长跑合并执行
# 3) 长跑结束读取 min_ever:
#    - 串口 [HEAP] 告警行(若有)
#    - 或 FTP 取 /log/selftest.log
# 4) 更新 docs/内存管理/RAM使用分析与优化方案.md 的 min_ever 基线

成本/回滚:约 15 行 + 零额外缓冲;阈值设编译期常量便于按新基线调整。 若长跑中频繁触发但系统正常 → 说明阈值偏紧,据实测值调整(不是缺陷)。


5. GD5F 错误通道统一(P2-A,S9 = 上游 C2)

来源:错误码传播审计.md 第二档 #1(E-10/E-42/E-53/E-54/E-56/E-57); 测试与架构改进建议.md §2.3 / §4.2 四要素骨架。

现状证据:ch395f.c(1330 行) 与 gd5f2gq5ue.c(1496 行) 是同代驱动,但只有 CH395F 形成了"分类 + 传播 + 审计"完整体系;GD5F 错误处理散落各函数,Trap_Records 仅 198 行。

统一错误通道四要素(以 CH395F 为范式):

要素 内容 对应缺陷
① 分类 wait_busy 区分「超时 / 真故障 / 参数错」——不得混为一个返回码 E-53
② 透传 底层错误码逐层上报,不吞不猜(禁 (void)ret) E-10 / E-42
③ 分级 可恢复(重试)/ 不可恢复(上报自检 + 降级)显式判定 E-54 / E-56
④ 留痕 不可恢复项写 syslog selftest;可恢复项计数并在 UI/日志可见 E-57
— 返回类型统一 int32_t E-57

实施顺序(数据安全优先):

  1. 先建分类函数(gd5f_wait_busy 内检查三次 HAL 调用)——所有下游判断的基础;
  2. 再修最危险的:gd5f_bbt_scan 失败放弃持久化(保护好副本)——数据安全优先;
  3. 补 init 全链路检查 + 重试 1~2 次;
  4. 回读 B0h 校验 ECC 使能(bit4);
  5. gd5f2gq5ue_read 的 raw 契约文档化或加 ECCS 检查;
  6. 全程在 GD5F2GQ5UE_Trap_Records.md 留痕,同步 GD5F2GQ5UE_Test_Guide.md。
  7. RAM 预算:新增错误分支不得引入新的大缓冲(先跑 ram_report.py 核算)。

判据:

  • E-10 / E-42 / E-53 / E-54 / E-56 / E-57 全部销项;
  • GD5F 套件回归 PASS:
# 套件回归(需实机)
& '.venv\Scripts\python.exe' test/run_all_suites.py --suite gd5f
#    判据:=== GD5F Tests: N/M PASSED (failed=0) === 且 N>0

# 只编译验证(离机,改动后随手跑)
& '.venv\Scripts\python.exe' test/run_all_suites.py --check-build

⚠️ 纪律(呼应 AGENTS.md 铁律):自愈逻辑严禁把"名字/参数类错误码" (如 FR_INVALID_NAME)归入"卷损坏触发集"——.fschk 无限重格式化事故即此类(2026-09-20)。 ⚠️ 破坏性用例:GD5F 套件默认只跑只读;ENABLE_DESTRUCTIVE_TESTS 跑一次消耗一个 NAND 块 (实测 95248→95200),仅在产线首启或明确需要时打开。


6. 网络稳定性与基线重锁(P2-B)

6.1 P1-8 网络重建失败恢复(与 §2.2 A2 同一项,此处给网络专项视角)

判据:拔插网线/改 IP 触发重建后——

  • 失败路径写 syslog 自检(不再零日志);
  • 30s 周期重试,不自驻留死循环;
  • 恢复后各 TCP 服务(FTP 21 等)据代次号重建监听,功能正常。

验证手段:test/host/e02_probe.py(网络探针)+ UI 网口设置页改 IP。

6.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/日志可见)。

执行步骤:长跑(§3.2 V5)期间周期性打点 + 对照 CH395F 无关的 SD2506 侧日志判别卡点。

6.3 N-3:L5 队列往返基线重验

现状:L5 基线(1223/988µs)属分片时代;L3 对该路径逐位不变 → 机制上无影响, 但严格重锁需跑一轮 Phase 13(需 PC peer)。

判据:Phase 13 窗口跑一轮,基线数值与阈值余量(>8×)对照,重锁数值。


7. 工程卫生(P3)

7.1 S5:test/ 代码宽松静态检查

现状证据:test/ 完全豁免 pre-commit("自律"要求),而 test/board/ 下 6300+ 行 C 代码 (实测 .c+.h 合计 6,313 行)且编入固件。曾出现 s_rx_buf 使用先于声明。

目标:抓 test/board/ 固件测试代码的真实缺陷(未初始化/越界/空指针), 但不引入存量 style 噪音。

执行步骤(先跑基线,离机可做):

# 1) 基线统计(不需实机)
cppcheck --enable=warning --error-exitcode=1 test/ 2>&1 | tee test_cppcheck_baseline.txt

# 2) 若存量可控(建议 <20 条)→ 加 test-cppcheck 钩子,只开 warning
# 3) 若存量较多 → 先建 suppress 清单(明确每条豁免理由),再挂钩子
# 4) 务必排除模拟器桩/第三方头(test/host/watchdog/stub/ 等)

判据:test-cppcheck 在 CI PASS;新增测试代码越界/未初始化被自动拦截。

注意:若基线告警过多,保持现状亦可接受——此时应把该决策写明确("理由:存量 N 条")。

7.2 S4:用例追溯脚本

现状:文档声称 91 个 TC、代码有 222 个 TEST_CHECK(断言点数 > 用例数是正常的), 但双向是否一一对应无人校验。

目标:新增 [计划] scripts/check_test_traceability.py—— 输入 test/board/*_test_task.c 的 TC 注释 + docs/测试说明/*Test_Guide.md 的 TC 编号; 输出 ① 文档有、代码无(未实现)② 代码有、文档无(未登记)。 默认仅告警(warning),不阻断提交(与 doc-links 演进路径一致)。

判据:一键列出双向差异清单;差异收敛后再考虑升为门禁。

7.3 待核实项:TEST_SUITE_SELECTED 未含 TEST_SUITE_RTC

现状证据(test/board/test_config.h):

  • L45 互斥检查已包含 TEST_SUITE_RTC(4 个套件互斥正确);
  • L49-51 的 TEST_SUITE_SELECTED 只含前三个(CH395F / STORAGE / GD5F),未含 RTC。

疑似影响:TEST_SUITE_SELECTED 被用于 defaultTask 是否空闲、是否创建 testTask 等分支—— 若 RTC 套件构建下 TEST_SUITE_SELECTED 未定义,testTask 可能不被创建 → RTC 套件不执行。 但 run_all_suites.py --check-build 实测 RTC 套件能编译通过(那只是编译,不是运行)。

判据:有实机时,选 TEST_SUITE_RTC 构建并烧录,观察:

  • 串口是否出现 === RTC Tests: N/M PASSED === 汇总行;
  • 若无汇总行 → 证实缺陷,TEST_SUITE_SELECTED 需补 || defined(TEST_SUITE_RTC);
  • 若有汇总行 → 说明另有分支覆盖,属"有意为之",在 test_config.h 注释说明。

执行步骤:

# 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 污染)

8. 与既有文档的引用闭环

本文项 上游文档 下游落地文档
S7 自检判定 审查记录/测试与架构改进建议.md §3.7 测试说明/自动化测试流程.md(待补参数)
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
N-2 日志静默 待处理内容.md N-2 问题记录/错误码传播审计.md E-02
P1-8 网络重建 审查记录/下一步任务规划.md 阶段 A 网络/Net层阻塞模型架构设计.md
S5/S4 测试与架构改进建议.md §3.4/§3.5 测试说明/测试开发标准流程.md
模拟器差距 测试说明/模拟器差距清单.md §3 决策规则 本文为"需回真机"项的执行面

模拟器差距清单 §3 的六条"必须回真机"规则决定"哪些改动**必须在本规划内验证—— 改动触及 Drivers/BSP/ 寄存器时序 / H-1H-7 硬件项 / L-2L-4 / 网络重建并发 / 启动段 / 内存预算, 一律在本文件找到对应批次。


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 36 后仍 5/5 复现);round 16 修复集摘要;round 7 纯取证埋点设计 + 判据表;P0-B 出口闭环(含 FTL flush 诱导式清台账);设备/台账/本地证据快照;7 条实测陷阱);§2.1 标注 S7 已落地、命令形态已确认