修复IWDG超时未配置与dhara resume回滚:看门狗误复位与复位后写入全部丢失,台架全量自检PASS

This commit is contained in:
2026-10-02 20:48:30 +08:00
parent d1a2eaab07
commit 930215f81a
33 changed files with 1875 additions and 94 deletions
@@ -36,10 +36,10 @@
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 262,100 B (255.96 KB) | 1,048,576 B (1024 KB) | 25.0% |
| RW_IRAM1 | 84,864 B (82.88 KB) | 131,072 B (128 KB) | 64.7% |
| RW_CCM | 49,184 B (48.03 KB) | 65,536 B (64 KB) | 75.0% |
| **Flash (ROM 合计)** | **262,800 B (256.6 KB)** | 1,048,576 B (1024 KB) | **25.1%** |
| ER_IROM1 | 264,284 B (258.09 KB) | 1,048,576 B (1024 KB) | 25.2% |
| RW_IRAM1 | 84,952 B (82.96 KB) | 131,072 B (128 KB) | 64.8% |
| RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% |
| **Flash (ROM 合计)** | **265,012 B (258.8 KB)** | 1,048,576 B (1024 KB) | **25.3%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
@@ -344,7 +344,7 @@ void vApplicationMallocFailedHook(void)
依据 RM0090 §3.1.1:F407 的 64KB CCM @0x10000000 仅 CPU 可访问(DMA 不可达、无 cache)。
**进度(2026-09-21)**:第一阶段已落地——scatter 新增 `RW_CCM` 区(`sct:25`,注释 "FreeRTOS heap only")并把 **FreeRTOS 堆整体迁入**(49,184 B,CCM 占用 75.0%)。下表缓冲迁入清单为**后续剩余项**(LVGL 池/UI 缓冲现仍在主 SRAM);`APP_CCM_ENABLE` 门禁宏**尚未实现**(scatter 无开关,见下"风险门禁")。
**进度(2026-09-21)**:第一阶段已落地——scatter 新增 `RW_CCM` 区(`sct:25`,注释 "FreeRTOS heap only")并把 **FreeRTOS 堆整体迁入**(49,184 B,CCM 占用 75.3%)。下表缓冲迁入清单为**后续剩余项**(LVGL 池/UI 缓冲现仍在主 SRAM);`APP_CCM_ENABLE` 门禁宏**尚未实现**(scatter 无开关,见下"风险门禁")。
**迁入 CCM 的缓冲(全部纯 CPU 读写):**
@@ -351,6 +351,16 @@ T_任务循环 < T_监视窗口 < T_IWDG超时 < T_最坏超时(打七折)
> 若后续需要覆盖,可在 `app_main_init` 完成后启动一个"短超时 + 手动多喂"
> 的临时监视(复杂度显著上升,暂不采用)。
> ⚠️ **决策 C 的必配项(2026-10-02 P0 实测补记,曾漏做并致 5 倍超时偏差)**:
> 既然"启动"被延后到 `wd_platform_start()`,**PR/RLR 的写入也必须一并搬过去**。
> 删 `HAL_IWDG_Init()` 的初衷只是阻止它"立即启动",但它同时是**唯一**写这两只
> 寄存器的路径——漏搬的后果是寄存器停在**复位默认值**(`PR=0`→÷4、`RLR=4095`,
> 实测 ≈349~512ms,比设计短约 5 倍),使 §4.3 的"超时 ⊃ 监视窗口"包含关系失效、
> §3.2 的停喂判定分支沦为**死代码**,任何 >400ms 心跳缺口都直接误复位。
> 现已在 `wd_platform_start()` 内补写(见 §5.3 与变更记录)。
> **通用教训:凡"把某个 HAL 初始化函数拆开/延后"的改动,必须逐一核对被拆掉的
> 函数还有哪些副作用没有搬到新位置——HAL 的 `Init()` 往往不只是"赋值 + 调用"。**
### 4.6 决策六:`configASSERT` 改为"先记录后复位"
现状(`Inc/FreeRTOSConfig.h:163`):
@@ -1103,3 +1113,4 @@ python scripts/check_cubemx_survival.py --write # 重建基线(确认改动
| 2026-09-29 | **3 个 Fault 升级为崩溃采集** | `MemManage` / `BusFault` / `UsageFault` 由空 `while(1)`(死机静默)升级为与 `HardFault` 同一套采集:生成区整函数替换(各带独立锚点 `MEMFAULT-ANCHOR` / `BUSFAULT-ANCHOR` / `USAGEFAULT-ANCHOR`)+ `#define X X_Gen` 改名保留生成实体(链接器各移除 2 字节)。新增 `fault_handler_enable()` 使能 `SHCSR` 后三者(默认只开 HardFault,不开则故障被升级、看不出根因),声明入 `Inc/stm32f4xx_it.h`、调用在 `app_main_init()` 第一行。因 ARMCC `__asm` 取不到 C 数据符号,改为 per-fault C 入口 `fault_entry_{hard,mem,bus,usage}` 带故障名调 `hard_fault_report(tag, frame)`。**刻意不启用** `UNALIGN_TRP` / `DIV_0_TRP`(避免把普通 bug 升级成死机)。审计脚本新增 `fault-handlers-3` / `fault-handler-enable` 两项、`UNPROTECTED_LEGACY` 清掉 3 个 Fault(WARN 2→1)、清单 16→18 项。`@build` 0 错误 0 警告;审计 18 通过 / 1 警告 / 0 失败 |
| 2026-10-01 | **查证 `fault_handler_enable()` 的 CubeMX 可行性** | 用户问"能否通过 CubeMX 配置"→ **否**。查 CubeMX 安装目录模板源码得三重证据:① `tpl_main_c.ftl` 的 `MX_NVIC_Init()` 只在 `systemHandler=="false"` 时生成 `HAL_NVIC_EnableIRQ`,而 `NVIC-STM32F417_Modes.xml` 中三个 Fault 都标 `Y`(=systemHandler)→ 永不生成使能代码;② **全部模板中 `SHCSR` 只出现 1 次**且是 MPU 清理的 `&= ~MEMFAULTENA`(禁用),**无任何 `\|=` 置位**;③ `.ioc` 无 MPU(`Mcu.IP` 列表),且 `HAL_MPU_Enable` 只置 `MEMFAULTENA` 一位。实测印证:`HAL_MspInit()` 里三个 Fault 一行代码都没生成。结论:只能手工写,但属 **L2**(`USER CODE BEGIN 1` 区)→ 重生成不丢,有审计项兜底。另新增警示:`.ioc` 的 `NVIC.MemoryManagement_IRQn=true` **不代表已使能**(第 1 段只是"向量可见")。写入 §10.2 与四个 Fault 采集小节 |
| 2026-10-01 | **新增 §7.3 主机侧单元测试(L0.5)** | 硬件访问收敛到薄平台层(`App/util/task_watchdog.h` 末段声明 3 个 `__weak` 钩子:喂狗/启动/取 tick),MCU 端行为零变化(`@build` 0/0)。新建 `test/host/watchdog/`(`watchdog_test.c` 39 用例 + `run_unit_test.py` MSVC 运行器 + `stub/` 三桩头),`#include` 生产源码以保证测的是真代码。挂 pre-commit `watchdog-unit-test` 钩子。新增 `docs/测试说明/看门狗单元测试说明.md`;`App/util/README.md`、`test/host/README.md`、`测试开发标准流程.md`(新增 L0.5 层)同步。顺带改善:`s_last_tick` 由函数局部 static 提为文件 static(可测性 + 模块状态归一)。§5.1 文件表与 §10 步骤表登记;§7.2 S1 优先级降低(位图逻辑已由 U1 覆盖) |
| 2026-10-02 | **P0 根因修复:IWDG 超时被静默配成复位默认值(比设计短 5 倍)** | round 3~8 反复出现的"空 STOR(0 字节截断)→ IWDG 误复位"**根因不是"poller 自身停摆"**。真因:`Src/iwdg.c` 为实现**延后启动**(§4.5 决策 C)删掉了 `HAL_IWDG_Init()`(`IWDG-DEFER-START-ANCHOR`),而 `HAL_IWDG_Init()` 是**唯一**会把 `hiwdg.Init.Prescaler/Reload` 写进 IWDG 寄存器的路径——删掉后 `MX_IWDG_Init()` 只填结构体,`WD_IWDG_PRESCALER`/`WD_IWDG_RELOAD` 沦为**死宏**,寄存器停在**复位默认值** `PR=0`(÷4)/`RLR=4095`。实测(SWD 回读)实际超时 = 4×4096/LSI ≈ **349~512 ms**,而设计是 64×1250/LSI ≈ **1701~2498 ms**。两个后果:① `WD_MISS_TOLERANCE`(8×100ms=800ms)**永远等不到**就被 IWDG 复位 → **停喂判定分支成死代码**(这正是 §7/`实机测试规划.md` §0.5.4.1 "STALL 从未执行"的真正原因,当时据此误判为"poller 停摆");② 任何 >400ms 的心跳缺口都直接误复位(空 STOR 的 FTL flush 使 NET/FTP/UI 心跳缺 ~400ms)。**修复**:`wd_platform_start()`(`__weak`,**不在 CubeMX 生成区**,重生成不丢)补上按设计值配置:解锁(`KR=0x5555`) → `PR` → 等 `SR.PVU` 清 → `RLR` → 等 `SR.RVU` 清 → `KR=0xCCCC`(不等同步会静默丢弃后写的那只,SWD 实测)。`.map` 证实 `Removing stm32f4xx_hal_iwdg.o(i.HAL_IWDG_Init), (94 bytes)`,故这是全固件**唯一**写 PR/RLR 的点。**验证**:SWD 现场改回设计值后,同一空 STOR 由 **5/5 复位 → 100% 成功**;固件烧录后 3 次开机回读均为 `PR=4/RLR=1249`。`@build` 0/0;本地 CI L0+L0.5 全绿(278/278 断言) |
@@ -340,6 +340,34 @@ dhara 通过 7 个回调函数访问物理 NAND:
4. 页面缓存初始化 (s_cache_buf, s_cached_lpn, s_cache_dirty)
```
#### 5.4.1 resume 的 epoch 锚点 —— 回绕后必须锚"最新"而非"最高地址"(2026-10-02 修复)
> ⚠️ **这是"复位后所有写入全部丢失"的曾经真凶**,排查存储丢失时**第一顺位**检查它。
**背景**:dhara 的 checkpoint 头页带 1 字节 `epoch`,journal 每绕满一圈
(`roll_stats()`,即 head 回绕到 0)`epoch++`。resume 时用 epoch 过滤,只认"当前圈"的
checkpoint。
**坑**:环**回绕之后**,"最高地址"的 checkpoint 属于**上一圈**(旧 epoch),而**最新**
checkpoint 落在**低地址**(块 0 一侧,epoch = 旧 + 1)。若把锚点取成
`find_last_checkblock_any()`(= 最高地址块),就会锚到**旧 epoch**,随后的
`find_last_checkblock()` 只能选中旧块 → `find_head()` 在旧块里找空页 → **journal 整体回退到
回绕之前**,回绕之后写的一切(日志、配置、FTP 上传)复位即丢。
(2026-09-20 的本地补丁正是这么改的,用于修"自愈卷重格式化每次开机丢失",属顾此失彼的回归。)
**现行实现**(`Lib/dhara/journal.c`,代码内附详细注释)—— 比较两个候选 epoch:
| `eFirst`(最低地址块)与 `eLast`(最高地址块) | 含义 | 锚点 |
|---|---|---|
| 相等 | 未回绕 | 相同,任取 |
| `eFirst == eLast + 1 (mod 256)` | **已回绕,低地址更新** | **`eFirst`** |
| 其它 | 低地址是上一圈残留 | `eLast` |
**可观测性**:`NAND_FTL` 开机加打一行 `Map head=… tail=… epoch=…`。
**只看 `Map resumed: N/M sectors used` 判断不出回退**(覆盖写不改 `m->count`,
实测该值在回退期间连续 2 小时一字不差);判据是 **head 复位后是否倒退**。
台架实测:回退时 head 96 → **130752**、tail 115143 → 115068;修复后 head=96 / epoch=1。
### 5.5 页面缓存策略
FTL 之上还有一个 **单页写回缓存 (write-back cache)**:
+413 -3
View File
@@ -33,6 +33,415 @@
---
## 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. **文档销项**(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 的数据连接**存活期仅 ~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,每次上台架前必跑)
> **目的**:避免"改动没问题、台架有问题"的误判(历史上多次把接线/权限问题误判为代码缺陷)。
@@ -42,7 +451,7 @@
| 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` | ≈ **262,100 B**(产品固件);若 ≈266,540 = 停在 RTC 套件态 → 重跑 A2 |
| 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) |
@@ -81,7 +490,7 @@
# 客户端连 <设备IP>:21,匿名/配置账号,GET /log/selftest.log
# 或经串口工具触发 FTP 拉取(如已有脚本)。
# 2) 对本轮串口日志 + selftest.log 联合判定(命令形态待 S7 落地后确认):
# 2) 对本轮串口日志 + selftest.log 联合判定(已落地;`--selftest-log` 可省,脚本自动 FTP 抓取):
& '.venv\Scripts\python.exe' test/host/verify_log.py \
--log-path <本轮串口日志> \
--require-selftest-pass \
@@ -89,7 +498,7 @@
# 判据:退出码 0;输出含 "[OK] selftest: no fault record"
```
**S7 落地形态(待实施,本文只给规划)**:
**S7 落地形态(✅ 2026-10-02 已落地并提交 `4a4cd29`;当前因台账未清空拿不到 PASS,见 §0.5)**:
| 步骤 | 内容 | 备注 |
|---|---|---|
@@ -375,3 +784,4 @@ cppcheck --enable=warning --error-exitcode=1 test/ 2>&1 | tee test_cppcheck_base
| 日期 | 变更 |
|---|---|
| 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 已落地、命令形态已确认 |