52 KiB
实机测试规划(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 |
每批出口:
@build0/0 → 一轮@testPASS → 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
已排除(逐条有证据,勿重复怀疑):
- 单次 console drain 过长:round-3 已限 250ms;round-6 再加"故障即止 + 300ms 冷却", 本轮误复位仍复现 → 非 drain 时长问题(但僵尸通道本身未根除)。
- poller 自身长 FTL 窗口:round-5
nand_ftl_wd_feed直喂已覆盖;本轮停喂时刻无 FTL 操作。 - 并发 FTL 写:round-5 卷锁已串行化(
lftpd_io全序列持锁、syslog/cfgtry 语义)。 - 主循环无界阻塞:
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:
vol_lock_wait(0u)try 取卷锁(调用方 = poller 上下文,绝不阻塞);f_open("/log/wdev.log", FA_WRITE | FA_OPEN_APPEND)写证据行 +"\r\n"(8.3 合法名; 文件 > 8KB 时先f_unlink重建,防长跑无界增长);释放锁;- 随后立刻调
s_append(SYSLOG_RUN, "wd stall declared")—— ⚠️ 关键:FTL 单页缓存的语义是"写到不同 LPN 才把前一脏页刷进 NAND", 没有这一步,WDEV 证据会在 1.7s 后的复位中回滚丢失(409 条里被回滚的 07:56 记录即同类教训); - 返回值语义: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 分片节拍"。
顺带确立的两条事实:
- 稳态
0x17是良性的(抽样 100ms vs timesync 心跳 ~200ms 的节拍差),但它是掩码精确匹配 设计的脆弱点:timesync 心跳一旦滑出两个连续窗口就会开始累积 streak。建议 round 8 顺手把 timeSyncTask 的等待也按 ≤50ms 分片喂心跳(低成本消抖)——但它不是本次复位的原因。 /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_IRAM1copy/zero、RW_CCMcopy/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):
seg=8=TASK_WD_SEG_STACK=stack_probe_poll()—— defaultTask 主循环最后进入的是栈水位观测段, 再没走到下一轮的seg=1(poll)。seq为大随机值 → 反证.noinit确实未被零初始化(保留成功)。- 串口在该段被切断在
[STACK]报告的第 2 行([STACK] default …完整、[STACK] net半行)→ defaultTask 冻结在stack_probe_poll()的DBG_INFO突发行内。 - 无
[ERR] STACK OVERFLOW、无 HardFault(复位原因是 IWDGRSTF 而非 SFTRSTF)→ 排除栈溢出与故障。 - 停喂前最后快照
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 必读,可省一天)
-R32不会 halt 内核(-CoreReg才 halt,实测约 30ms/次且可重复)→ 用-R32做 无扰动存活性采样,只在需要 PC 时用-CoreReg。(round 8c 每 0.43s 采一次样, 而"停摆→复位"窗口实测只有 ~150ms —— 采样周期长于窗口才系统性错过,不是采不到; 另外 halt 期间 TIM7 ISR 不会执行,会让n出现"假停顿",须用非 halt 读法甄别。)-R32 <addr> <count>的count单位是「字节」(-R32 0x1000FF00 16得 4 个字)。 按字理解会把.noinit槽静默截断(round 8b/8c 因此只读到 magic+段号)。-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 步已被其推翻)。
- 清台账 —— 不需要再"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 之后的整段。
- 实测:2048 B
- 全量
@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 路径)。现已把"设备侧无此文件"单列为正常路径。
- 退出码 0;标记全绿:
- 回读(已执行):
ST-LINK_CLI -c SWD -CmpFile "MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex" 0x08000000→No difference found✓ - uvprojx 取舍:保留
App\fs\vol_lock.c挂载 +../App/fs包含路径两个真实差异, 丢弃 UV4 XML 格式 /PackID噪声(判据见AGENTS.md)。 - 文档销项(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销项(①.iocUSART1 TX-only 结构根治 / ② dhara 重试自旋残留 / ③ 可选全序列卷锁;N-03 至 P1-A)。
- 提交:单行中文标题 <100 字符(
git commit -F)→ pushv2-on-ui; 预提交含watchdog-unit-test/cubemx-survival/test-suite-switch(确认产品构建)。 - 之后按 §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",而是整个卷在复位后回到旧状态。
取证链(逐步缩小,不猜):
-
f_unlink → sync_fs → disk_ioctl(CTRL_SYNC)全程无[ERR][NAND_FTL](@test也确认[ERR]缺席)→ 不是写盘报错。 -
给
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()是有效的, 写盘侧没问题。 -
决定性证据 —— 复位前后 journal 状态对比:
head tail epoch 复位前( CTRL_SYNC之后)96 115143 1 复位后( resume完成)130752 115068 0 head 倒退 92 页、tail 倒退 75 页 →
dhara_journal_resume()恢复回了旧 checkpoint。 -
直读 NAND 两端的 checkpoint 头页(页 15 与最高块页 15)确认:
CHK-FIRST: Dha epoch=1(块 0 = 最新,因为环已回绕);而 resume 锚定的是 epoch=0 的旧 checkpoint。 -
旁证(最容易踩的坑):
[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 本窗口实测的关键陷阱(必读,照做可少绕一天)
- 写入"复位后消失"的两层原因(本窗口最大坑,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 有效。
- L1 dhara checkpoint 回滚(主因,见 §0.5.5.1):环回绕后
- FTP 路径:lftpd 相对路径基准是根目录(
SIZE selftest.log→ 550),一律用 绝对路径/log/xxx;LIST /log实测列的是根目录内容(lftpd 怪癖)——查状态只用SIZE/RETR。 - 0 字节 STOR 的数据连接存活期仅
1050ms,短于芯片/网络层轮询粒度 → 芯片只见DISCONNECTED (chip tcp=0x00)→ accept 失败(425 或控制连接被重置);本轮实测客户端PASV→STOR间隔达 11 s(数据口 LISTEN 半开)。这是与看门狗误复位不同的另一类 问题(CH395F 数据通道);若修复后仍复现 425,转 §6(P2-B / N-2/N-3)专项。 - 串口采集铁律:
open→clear→ 烧录/复位 →wait-idle --timeout 90 --settle-ms 8000 --max-ms 45000→ 直接 Read/Greptools/serial_tool/state/serial.log(log --grep子命令不可信);daemon 残留 →taskkill /F /PID <旧>后重新open。 - 只用
.venv\Scripts\python.exe(PATH 里的python是 msys64);每次烧录后必须 ST-LINK 全片回读No difference found。 - 误复位会使台账 +1 条
watchdog reset:取证/修复期间台账只会增长, 属预期(清台账留到最后一次修复后的出口闭环)。 - 未提交改动是 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。
判据(可观测、量化):
verify_log.py --require-selftest-pass退出码 0 = PASS / 1 = FAIL;- 判定规则:
selftest.log中出现任一SYSLOG_SELFTEST记录 = 有故障 → FAIL(v1 只记故障,不记通过); - 文件不存在/大小为 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(⏳ 待台架);待处理内容.mdN-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 |
实施顺序(数据安全优先):
- 先建分类函数(
gd5f_wait_busy内检查三次 HAL 调用)——所有下游判断的基础; - 再修最危险的:
gd5f_bbt_scan失败放弃持久化(保护好副本)——数据安全优先; - 补 init 全链路检查 + 重试 1~2 次;
- 回读 B0h 校验 ECC 使能(bit4);
gd5f2gq5ue_read的 raw 契约文档化或加 ECCS 检查;- 全程在
GD5F2GQ5UE_Trap_Records.md留痕,同步GD5F2GQ5UE_Test_Guide.md。 - 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 3 |