docs: 删除 Task #32 停摆分析(已结案),可复用结论并入看门狗设计说明 §11

按登记簿"完成即销项"规则删除 docs/问题记录/UI任务看门狗停摆分析.md
(560 行)。结案依据:累计 38 次上板启动 0 复现(原 L2 基线 3/10),
P0/P1-4a/P1-5/P2/方案 A 全部落地,方案 B 已评估决定不做。

并入 docs/子系统设计/看门狗设计说明.md 新增 §11(只留可复用部分):
- §11.1 三条易误读的判读口径:启动期 MISS start 0x67 是良性 episode;
  miss= 是累计不是连续(连续数饱和在 WD_MISS_TOLERANCE=8);
  reset cause 不能区分 IWDG 溢出与台架软件复位
- §11.2 诊断四步:先看掩码缺几位(只缺 1 位 = 定点打击,多位 = 全局
  饥饿)→ 逐项量化占空比(实测 10%~13%,否掉"CPU 饱和"直觉)→
  用计数速率反证空转(ring 恰好 1000/s)→ 用 [PROF] 指认占用者
- §11.3 三条结构性脆弱点(uiTask 唯一 BelowNormal/心跳曾依赖 netTask
  往返/自定截止期不可恢复)+ 两个顺带修掉的正确性缺陷(无新帧仍压环
  ≈3% 频率失配、失败拍静默沿用旧值)
- §11.4 [PROF]/[PROF2] 全字段语义 + v2.8 实测基线(netp=0 是真实值、
  adcm 峰值来自心跳日志、uip 地板 = LVGL 帧周期)与三机理判读口诀
- §11.5 三条教训:门控类优化必须保留低频兜底;门控会顺手消灭暴露路径;
  事件驱动要核对节拍源启动时刻 vs 消费者创建时刻
- §11.6 明确不做的四条及理由(提 uiTask 优先级会掩盖真缺陷、net_* 有界
  等待版会串回执、推流搬出 uiTask、推流塞进 netTask)

UI 推流门控与方案 B 否决理由收进 RemoDispBus 协议说明 §3(该处原本已有
摘要,改为自洽不再回指);就绪门控 API 的约束已在 BSD Socket 指南 §10.1。

引用改指 12 处:ui_task.c、task_prof.c/.h、ui_net_sink.c/.h、docs/README、
RemoDispBus、看门狗设计说明 R7 行、ADC 采样节拍设计说明、BSD Socket 指南
×2、项目审查报告 N-10 ×2。

⚠ ui_task.c / ui_net_sink.c / ui_net_sink.h 是带 BOM 文件,Edit 剥掉了
BOM,已按原样补回 3 字节 EF BB BF(未整文件重写)。

验证:check_doc_links --all 通过(127 个文档,无悬空引用);
@build 0 错 0 警,Code=257660 与删除前一字不差(纯注释改动)。
This commit is contained in:
2026-10-11 00:38:50 +08:00
parent 9bbe117f97
commit 54d6d2d5d5
12 changed files with 110 additions and 579 deletions
+1 -2
View File
@@ -88,7 +88,7 @@ docs/
| [`系统墙钟时间维护说明.md`](子系统设计/系统墙钟时间维护说明.md) | `sys_clock`:RTC 基准、tick 回绕防护、30 天惰性再基准化 |
| [`控制台单写者日志设计说明.md`](子系统设计/控制台单写者日志设计说明.md) | 控制台日志单写者模型(避免多任务交错输出) |
| [`运行与自检记录设计说明.md`](子系统设计/运行与自检记录设计说明.md) | `/log/run.log`·`selftest.log` 定长记录与待机显示 |
| [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | IWDG(CubeMX 配置)+ 任务心跳位图:选型与超时推导、掩码与埋点、取证机制、验证记录;已台架闭环(V2/V4/V5) |
| [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | IWDG(CubeMX 配置)+ 任务心跳位图:选型与超时推导、掩码与埋点、取证机制、验证记录;已台架闭环(V2/V4/V5)。**§11 = 任务级饥饿的诊断方法与 `[PROF]` 实测基线**(Task #32 沉淀:判读口径、量化归因、三条脆弱点、明确不做的四条) |
| [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍**:TIM3 1 kHz 更新事件同时触发 TPAFE CONVST 与(经 TRGO)STM32 ADC1 + DMA 乒乓缓冲;方案选型、CubeMX 配置规格、代码改造清单、时序/优先级分析、验收判据与实测剖面;**已落地并上板闭环** |
| [`网络/BSD_Socket_API_使用指南.md`](网络/BSD_Socket_API_使用指南.md) | **上层视图**:BSD Socket API 公开语义、示例、常见问题 |
| [`网络/BSD_Socket_API_函数调用关系.md`](网络/BSD_Socket_API_函数调用关系.md) | **内部视图**:netTask / 消息队列 / L3 挂起 / 重试对账的调用关系与设计意图 |
@@ -186,7 +186,6 @@ docs/
| [`GD5F2GQ5UE_Trap_Records.md`](问题记录/GD5F2GQ5UE_Trap_Records.md) | GD5F2GQ5UE SPI NAND 陷阱台账 |
| [`SD2506_Trap_Records.md`](问题记录/SD2506_Trap_Records.md) | SD2506 RTC 陷阱台账 |
| [`错误码传播审计.md`](问题记录/错误码传播审计.md) | 错误码传播缺陷审计(9 类模式 × 子系统) |
| [`UI任务看门狗停摆分析.md`](问题记录/UI任务看门狗停摆分析.md) | **Task #32 归因**:uiTask 看门狗停摆(L2 引入的间歇性回归)——否掉"CPU 饱和",定位为**优先级饥饿 + 心跳依赖 netTask 往返**;含量化算式、独立缺陷(无新帧仍压环)与 P0/P1/P2 处置建议 |
---
@@ -250,9 +250,12 @@ if (n > 0) {
> ⚠ **残留暴露(已知,刻意不处理)**:门控只消除"无谓的往返",消除不了"往返本身无上界"——
> 客户端在连且 netTask 拥塞时,3204 B 的 LCDMEM 应答仍可能让宿主任务等最长 ≈4 s。
> "把推流搬出 uiTask(独立任务 + 帧 seqlock)"已完整评估并**决定不做**:实测刷新最坏周期
> (`uip` ≈61 ms)是 **LVGL 帧周期地板**、与网络无关,搬任务不会让刷新变快。
> 否决理由、残留暴露的观测手段(`[PROF]` 的 `uis`/`uisn`)见
> `docs/问题记录/UI任务看门狗停摆分析.md` §9.4。
> (`uip` ≈61 ms)是 **LVGL 帧周期地板**、与网络无关,搬任务不会让刷新变快;成本却含
> **必改 `.ioc`** 的重生成与全套回归,收益只剩极端并发下的观感隔离。若日后它成为实际问题,
> 正确方向是先治 netTask 排队公平性(登记簿 N-4 二期),不是把 UI 搬走。
> 观测手段:`[PROF]` 的 `uis`(往返最坏)与 `uisn`(此刻仍在推流段内多久)——
> **`uisn` 持续非 0 就是这一格真发生的铁证**;字段语义与判读见
> [`../子系统设计/看门狗设计说明.md`](../子系统设计/看门狗设计说明.md) §11.4。
---
@@ -373,7 +373,7 @@ t=1000 µs 下一个 TIM6 更新事件
- **实测印证**:`seq − frames = 20006 − 19999 = 7`——正是这 ≈100 ms 窗口里 TIM6 发出的 7 个脉冲
被丢弃(当时无消费者),之后一帧不差。
**教训(与 `docs/问题记录/UI任务看门狗停摆分析.md` §9.3.1 的 I-2/I-3 同类)**:把"定时轮询"换成
**教训(与 `docs/子系统设计/看门狗设计说明.md` §11.5 的三条"实施时才暴露"同类)**:把"定时轮询"换成
"事件驱动"时,必须单独核对**节拍源启动时刻 vs 消费者创建时刻**,否则第一次上电就会踩到
"ISR 对还不存在的消费者动手"。
+90 -1
View File
@@ -456,7 +456,7 @@ snapshot == required → miss_streak=0、证据门闩复位、喂狗
| R4 | **NAND 真实卡死时 FTL 直接喂狗** | 设备存活但存储功能冻结(与 ORE 自愈同类边界)。若要求"NAND 死也必须复位",需给 `nand_ftl_wd_feed()` 加次数/时限上限 |
| R5 | **栈溢出与断言在复位原因上不可区分**(都是 `SFTRSTF`) | 可接受;串口分别打 `[ERR] STACK OVERFLOW` / `[ERR] ASSERT FAILED`,`selftest.log` 侧另有记录 |
| R6 | **`RCC_CSR` 读后即清** | 已在 `main.c` 统一处理;漏清会让下次软复位读回上次标志 |
| R7 | **UI 推流的网络往返卡死不占心跳位**(产品掩码仍 `0x77`) | 刻意:往返卡死的根因在 netTask,netTask 已在掩码内、由它暴露;为"远端显示卡了"新增一位只会把观感问题升级成整机复位。UI 心跳已由 P1-4a 自产、不受推流影响;推流耗时看 `[PROF]` 的 `uis`/`uisn`。详见 `docs/问题记录/UI任务看门狗停摆分析.md` §9 |
| R7 | **UI 推流的网络往返卡死不占心跳位**(产品掩码仍 `0x77`) | 刻意:往返卡死的根因在 netTask,netTask 已在掩码内、由它暴露;为"远端显示卡了"新增一位只会把观感问题升级成整机复位。UI 心跳已由 P1-4a 自产、不受推流影响;推流耗时看 `[PROF]` 的 `uis`/`uisn`。成因与判读见本文 **§11.3 第 2 条**;门控方案与残留暴露见 `../UI设计/RemoDispBus协议与接收设计说明.md` §3 |
### 9.2 未办项(登记簿同步)
@@ -520,6 +520,94 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建
---
## 11. 任务级饥饿的诊断方法与实测基线(Task #32 沉淀,2026-10-10)
> **来源**:`[ERR][WD] task stall: mask=0x00000067 req=0x00000077`(缺 bit4 = `TASK_WD_UI`)
> 的完整归因与处置。故障本身已结案——**累计 38 次上板启动 0 复现**(原 L2 基线 3/10),
> 原始分析文档按登记簿"完成即销项"规则删除。本节只留**可复用的判读口径、诊断方法、
> 实测基线与教训**;ADC 侧的处置细节在 [`ADC采样节拍设计说明.md`](ADC采样节拍设计说明.md) §10/§11,
> UI 推流门控在 `../UI设计/RemoDispBus协议与接收设计说明.md` §3,就绪门控 API 在
> `../网络/BSD_Socket_API_使用指南.md` §10.1。
### 11.1 现场日志的三条判读口径(都被误读过)
| 现象 | 正确读法 |
|---|---|
| 启动期出现 `MISS start snap=0x67 req=0x77` | **良性 episode**:`[WD] armed` 在 bring-up 完成后武装,而 `required_mask` 静态含 UI 位 → UI 首跳之前必现一次,随后自行恢复。**不是故障判据**,只作"UI 尚未起跳"的旁证 |
| `miss=11` / `miss=12` | `miss=` 是**累计**缺席次数(`s_miss_total`),**不是连续次数**;连续次数 `s_miss_streak` 饱和在 `WD_MISS_TOLERANCE=8`。缺口要用 `t=`(毫秒时基)差算,本例恰为 800 ms |
| `reset cause: software (NVIC_SystemReset)` | **不能**用来区分"IWDG 溢出"与"台架拉 NRST / 烧录工具软件复位"——本台架**健康**启动同样报这一串。停摆的权威证据是 `[ERR][WD] task stall` 行本身 |
### 11.2 诊断四步:先量化,再归因
1. **看掩码缺哪几位。** 只缺**一位** = 定点打击(该任务独有的依赖或优先级处境);
**多位同时缺** = 全局饥饿 / CPU 饱和。本例 `0x67` 只缺 UI,其余五个 Normal 任务心跳
全正常 → **当场否掉"计算量把 CPU 吃满"这个直觉**(用户假设方向对、结论不成立)。
2. **把嫌疑任务的单拍成本逐项算出来**(不要估)。当时算式:ADC1 一轮 9 通道 ≈67 µs、
TPAFE 快照 + 9 路取数 ≈10~20 µs、压环 + 5 实例傅氏 ≈10~15 µs → 单拍 0.10~0.13 ms
= **占空比 10%~13%**,距饱和很远。
3. **用计数速率反证"有没有空转"。** `ring` 在两次心跳间严格 +10000/10 s = **恰好 1000 次/秒**;
若该任务失去阻塞(空转),按 0.11 ms 单拍应为 ≈9000 次/秒 → 反证它每拍都睡满。
4. **用 `[PROF]` 指认最大非让出占用**(§11.4)。实测最大是 `defaultTask` 控制台排空
≈46 ms,**没有任何任务接近 800 ms** → 800 ms 属**异常长尾**,只能靠复现现场的快照抓,
不能靠健康剖面指认;⚠ 别把"健康剖面里没有"当成"不存在"。
### 11.3 三条结构性脆弱点(当时全部从代码证实)
| # | 脆弱点 | 现状 |
|---|---|---|
| 1 | **uiTask 是 `required_mask` 里唯一的 `osPriorityBelowNormal`**(其余五个全 `Normal`)。FreeRTOS 数值越大优先级越高 → 任一 Normal 任务持续就绪,uiTask 就**完全拿不到 CPU**;`configUSE_TIME_SLICING=1` 只保护**同优先级**公平 | **至今未变**(刻意不提优先级,理由见 §11.6)。风险由"心跳自产 + 往返门控 + 采集零忙等"三面压住 |
| 2 | **UI 心跳曾依赖 netTask 往返**:`ui_net_sink_poll()` → `net_accept()` → `osMessageQueuePut(osWaitForever)` + `xTaskNotifyWait(portMAX_DELAY)`,**之后**才 `task_wd_beat(TASK_WD_UI)` → netTask 慢一拍、UI 心跳晚一拍。这正是"**只有 UI 位缺失**"的根因(别的任务没这条依赖) | ✅ 已断两处:心跳提到 poll **之前**(`App/task/ui_task.c`);无客户端时门控让往返 = **0 次**(`uis` 实测 1 µs) |
| 3 | **自定截止期一旦错过就不可恢复**:`next += 1; osDelayUntil(next)` 且**丢弃返回值** → 单拍 >1 ms 后 `next` 相对当前时刻永久落后 → 该任务从此不再阻塞、以 Normal 优先级空转(**连续**慢拍才锁死,单次慢拍会自己追平)。返回值本是现成的重同步钩子 | ✅ 已消:adcTask 改**事件驱动唤醒**(`osThreadFlagsWait`),"落后"这个概念不存在了(`ADC采样节拍设计说明.md` §10) |
⚠ 顺带揪出的两个**独立正确性**缺陷(与停摆无关,但同源发现,别只当性能问题):
- **无新帧仍压环**:`r == 0`(TPAFE 无新帧)时也压帧 → 上一拍的重复值进 20 点 DFT 窗,
等效采样间隔被拉长。当时 `idle` 占 3.2% → 窗长从 20.0 ms 漂到 ≈20.6 ms ≈ **3% 频率失配**
→ 幅值/相位**系统性误差**(不是随机噪声)。现仅 `r == 1` 压帧,上板印证 `ring == frames`。
- **失败拍静默沿用上一拍值**:失败支路不清 `s_stm32_valid` → 照样压环、`skip` 抓不到。
现失败即清标志走整帧跳过(`ADC采样节拍设计说明.md` §10.4;单测 TC-AI-070..078 钉死)。
### 11.4 `[PROF]` / `[PROF2]` 字段语义与当前基线
探针本体见 `App/util/task_prof.h`(DWT `CYCCNT`,168 周期 = 1 µs;每 ID 记 max/count/last,
enter/exit 段还能读出"此刻仍在段内已多久"——**永久阻塞的段永不结算,只有这个字段能证明**)。
| 字段 | 探针 | 含义 | 基线(2026-10-10 v2.8,无客户端,开机 29 s) |
|---|---|---|---|
| `uip` / `uil` | UI_PERIOD | uiTask 循环周期最坏 / 最近值 | **61.3 ms** / 10.0 ms——⚠ 地板是 **LVGL 整帧渲染**(独立实测帧周期 61.6 ms),**与网络无关** |
| `uiw` | UI_WORK | 本轮起点→心跳前(含 LVGL) | **11.4 ms**(改前 ≈50 ms 且 ≈`uis` → 当时单轮几乎全是推流) |
| `uis` / `uisn` | UI_SINK | 推流往返最坏 / 此刻仍在段内 | **1 µs** / 0(改前 ≈50 ms);**`uisn` 持续非 0 = "客户端在连且 netTask 拥塞"这一格真发生的铁证** |
| `adcm` | ADC_BEAT | adcTask 单拍(含心跳日志) | 2.0~2.25 ms——⚠ 峰值来自**每 10 s 一次的心跳日志**,不是采集,勿混读 |
| `adcp` | ADC_POLL | `adc_input_poll()` 单次 | **8 µs**(DMA 前 122 µs;超时拍曾 2.2 ms) |
| `defd` / `defs` | DEF_DRAIN / DEF_SYSLOG | 控制台排空 / syslog 落盘 | **46 ms** / 7 µs——**当前最大的单一非让出占用** |
| `netp` / `netn` | NET_POLL | `net_poll()` 最坏 / 在段内 | **0 µs** / 0——⚠ **0 是真实值**:空闲时 `net_poll()` 开头读 INT# GPIO 为高即 `break`,整条就是一次 GPIO 读,别判成"探针坏了" |
| `gl` | glitch | DWT 丢样 / 计数器重置计数 | 0(非 0 时以上数据不可信) |
**判读口诀**:`uip` 大而 `uiw`/`uis` 小 → **被调度饿死**;`uis` 大 → **阻塞在网络往返**;
`uiw` 大而 `uis` 小 → **自身渲染拖长**。三种机理在日志上必须能分开,否则归因只能靠猜。
### 11.5 三条教训(同类问题会复发)
1. **门控类优化必须保留低频兜底。** `readable` 只由 RECV_OK 中断置位,而 `net_reconcile_tick()`
的三类对账**不含**"芯片 RX 非空但 `readable=0`" → 中断一旦丢失,纯门控会把已到达的数据
**永久藏住**。现每 32 轮无条件 recv 一次。**拿"自愈能力"换性能是隐性负债。**
2. **门控会顺手消灭暴露路径。** 加了 accept 门控后,监听控制块消失时 `net_can_accept()`
恒返 0 → 改前"每轮 accept 撞 BADF"这条**暴露**随之消失 → 端口永久不再接受新客户端而无声。
现补 `net_get_sock(g_listen) == NULL` 判定。**加"跳过"逻辑时要问:原来哪条错误是靠这次调用暴露出来的?**
3. **把"定时轮询"换成"事件驱动"时,必须单独核对节拍源启动时刻 vs 消费者创建时刻**
(本仓 TIM7 分支 2026-10-02 的实测坑同款)——否则第一次上电就踩"ISR 对还不存在的消费者动手"。
### 11.6 明确不做的四条(理由要留着,别被"顺手优化"推翻)
| 备选 | 为什么不做 |
|---|---|
| **提高 uiTask 优先级** | 会**掩盖**"心跳依赖他任务"与"忙等"两个真实缺陷;而且它根本不解决"UI 在等 netTask"。缺陷已修完后,提优先级才成为可讨论项 |
| 给 `net_*` 加"有界等待"版 | 回执通道是"每任务一个 32 位通知值",超时后**迟到的回执会被下一次调用的零等待拾取当成新结果**(`net_wait_result` 注释里 L7 挂死/整段错位的同款根因)。要做对必须引入请求序号,代价高于搬家 |
| 把推流搬出 uiTask(独立 `uiSinkTask`) | 实测 `uis`=1 µs 而 `uip` 仍 60.9 ms = **LVGL 帧周期地板** → 搬走推流**不会让刷新变快**;收益只剩极端并发下的观感隔离,成本含**必改 `.ioc`** 的重生成与全套回归。若日后成问题,正确方向是先治 netTask 排队公平性(登记簿 N-4 二期),不是搬 UI |
| 把推流塞进 netTask | 零往返零新增 RAM,但共享源码要走 backend 端口(双端同改)、netTask 这个唯一执行者更胖、一个 UI 侧 bug 直接变成整机复位 |
---
## 附录 A:参考依据(当前有效)
| 来源 | 引用点 |
@@ -574,3 +662,4 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建
| 2026-10-10 | **正文按当前代码重写(清理立项前的描述)** | 正文原样保留了大量**立项前(2026-09-29)**的状态取证与已废弃的"路径 C 考古",与现状矛盾,故重写为现状设计说明(1144 → 571 行;**历史与逐次决策全部保留在下方变更记录,未删**)。主要更正:① §1.1"现状:无任何硬件看门狗"改为**历史背景**(IWDG 已在 `.ioc`/`hal_conf`/工程三处就位);② **ftpTask 早已纳入监视**——经 `lftpd_task_wd_beat()` 弱钩子(事件循环 / RETR·STOR 分片 / 连接与卷锁等待),旧正文"首期不埋点、由掩码剔除"及 R3/O1 已作废;③ 补齐正文缺失的三块现状实现:**FTL 心跳路由**(`task_wd_bind()` + `nand_ftl_wd_beat()`)、**FTL 直接喂狗**(`nand_ftl_wd_feed()`,含"设备存活但功能冻结"边界)、**round 7/8/8b 取证机制**(`/log/wdev.log`、`.noinit` 槽 + `UNINIT_CCM`、`PREVRUN`/`TICKRUN`/`MISS start`/`STALL`);④ 掩码改为按代码实况列表(产品 `0x37` / `ENABLE_HW_LAYER_TESTS` `0x35` / **testTask 任何构建都不入掩码**)并写明"精确相等"语义;⑤ 复位原因优先级按 `reset_cause_str()` 实际顺序改写(旧表把 POR 排第一,代码是 **IWDG 最优先**);⑥ §4.6 更正为**已集成**:看门狗复位写 `selftest.log`(旧正文"未改");⑦ §8 预算改用 `.map` 实测(`task_watchdog.o` 1098 B 代码 / 44+104 B RAM / `.noinit` 48 B;`iwdg.o` 仅 28 B,`HAL_IWDG_Init` 94 B 被移除);⑧ 附录 B 自相矛盾修正(旧表把"借助 CubeMX"列为否决,实际已选它);⑨ §7 由"验证计划"改为**验证记录**(V2/V4/V5 ✅、U1 39 断言 ✅,V3/V6/V7 标为未单独注入、V8 未做)。连带同步:`Src/iwdg.c`、`Src/freertos.c`、`App/task/default_task.c` 三处注释的章节引用(旧 §4.5/§4.6 → 新 §4.1/§4.4),`scripts/check_cubemx_survival.py` 提示语(§10.1 → §10.2/§10.3),`实机测试实施指南.md`(§10 → §7 ×3)、`看门狗单元测试说明.md`(§10 → §7)、`模拟器差距清单.md` H-1(§4.9 → §4.7,并更正"模拟器不编译本模块、无 IWDG 桩")、`docs/README.md`("实施中/待验证" → 已闭环)。⚠️ **下方历史行里的 §x.y 引用指当时编号**,不再回溯改写 |
| 2026-10-10 | **adcTask 纳入监视(B1):必需掩码 `0x37`→`0x77`** | B1(ADC 采集链路)落地时新增 `adcTask`(`App/task/adc_task.c`:1 ms 采样节拍触发 CONVST + 消费 `tpafe5160_snapshot()`)并纳入 IWDG 监视:`task_wd_id_t` 新增 `TASK_WD_ADC = 6`;`wd_required_mask()` 产品 `0x37`→`0x77`、`ENABLE_HW_LAYER_TESTS` `0x35`→`0x75`(adcTask **不依赖任何就绪屏障** → 任何构建都置位)。adcTask 主循环每 1 ms `task_wd_beat(TASK_WD_ADC)`;**不做 NAND I/O → 不登记 FTL 心跳路由**(无需 `task_wd_bind()`)。主机侧单测 `beat_all()` 与掩码断言(`tc_003`/`tc_005`/`tc_011` 的 `0x37`→`0x77`)同步,39 断言全绿。任务由 `Src/freertos.c` 的 `USER CODE` 区手工创建(同 timeSyncTask,不在 `.ioc` 里),栈 512 words、优先级 Normal,`stack_report.py` 实测最坏链 504 B(余 1544 B / 75%)。见 `docs/算法/继电保护功能复现规划.md` §7 |
| 2026-10-10 | **B1 `@test` 实机闭环:adcTask 掩码真机验证** | 台架闭环(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`)确认 `adcTask` 纳入掩码后**无 adc 相关误复位**:`[STACK] adc peak_used=496/2048 free_min=1552 (75%)`、`frames/seq` 每 10 s 严格 +10000(=1000 Hz)、`idle=0`/`frst_err=0`、全片回读 `No difference found`。启动早期两次 `MISS start snap=0x67 req=0x77` 仅缺 bit4(`timeSync` 未启动,属正常就绪窗;adc 的 bit6 自始置位),掩码随后收敛、全程无复位。同轮另修 `dbg_console_writer_ready()` 门控(adcTask 周期日志在写者注册前抑制,见 `控制台单写者日志设计说明.md`) |
| 2026-10-10 | **新增 §11:任务级饥饿的诊断方法与实测基线** | 承接 Task #32(`task stall: mask=0x67 req=0x77`,缺 `TASK_WD_UI`)结案后的沉淀——原分析文档 `docs/问题记录/UI任务看门狗停摆分析.md` 按"完成即销项"删除,可复用部分并入本文:**§11.1** 三条易误读的判读口径(启动期 `MISS start 0x67` 良性、`miss=` 是累计不是连续、`reset cause` 不能区分 IWDG 与台架);**§11.2** 诊断四步(先看掩码缺几位 → 量化占空比 → 用计数速率反证空转 → 用 `[PROF]` 指认占用者;本例否掉"CPU 饱和",实测 10%~13%);**§11.3** 三条结构性脆弱点(uiTask 唯一 BelowNormal/心跳依赖 netTask 往返/自定截止期不可恢复)与两个顺带修掉的**正确性**缺陷(无新帧仍压环 ≈3% 频率失配、失败拍静默沿用旧值);**§11.4** `[PROF]`/`[PROF2]` 全字段语义 + v2.8 基线(含 `netp=0` 是真实值、`adcm` 峰值来自心跳日志、`uip` 地板 = LVGL 帧周期)与三机理判读口诀;**§11.5** 三条教训(门控须留低频兜底、门控会消灭暴露路径、事件驱动要核对节拍源与消费者创建时刻);**§11.6** 明确不做的四条及理由。§9.1 的 R7 行改指本文 §11.3 |
+2 -2
View File
@@ -886,7 +886,7 @@ int net_can_read(int sockfd); /* 1 = 应发一次 recv;0 = 可跳过本轮
| `net_can_accept` 判据 | `state == ESTABLISHED`,与 `net_accept_locked()` 严格等价(单连接模型:监听 Socket 自身在 CONNECT 后转数据通道) |
| `net_can_read` 判据 | `ESTABLISHED` → 看 `readable`;**非 `ESTABLISHED` 一律返 1**(必须让上层 recv 一次拿到 EOF/错误并复位自身状态,门掉它会把"连接已消失"永久藏住);描述符失效也返 1(让 recv 报 BADF 走既有错误分支) |
| 用途 | **门控**:无活时跳过 `net_accept()`/`net_recv()`——两者内部都是 `osMessageQueuePut(osWaitForever)` + `xTaskNotifyWait(portMAX_DELAY)`,**均无上界**(队列深 8 且与 FTP/timeSync 共享) |
| ⚠ 不可作为唯一判据 | `readable` 只由 RECV_OK 中断置位,而 §3.6 的三类对账**不含**"芯片 RX 非空但 `readable=0`" → 中断丢失时 `net_can_read()` 恒返 0。**调用方必须保留低频兜底 recv**(`ui_net_sink.c` 每 32 轮无条件收一次,见 `docs/问题记录/UI任务看门狗停摆分析.md` §9.3.1) |
| ⚠ 不可作为唯一判据 | `readable` 只由 RECV_OK 中断置位,而 §3.6 的三类对账**不含**"芯片 RX 非空但 `readable=0`" → 中断丢失时 `net_can_read()` 恒返 0。**调用方必须保留低频兜底 recv**(`ui_net_sink.c` 每 32 轮无条件收一次,教训见 `docs/子系统设计/看门狗设计说明.md` §11.5 第 1 条) |
| 竞态 | netTask 是唯一写者,单字读;最坏"晚一轮发现"(netTask 每 1 ms 就 `net_poll()`)或"扑空"(RPC 返回 `NET_ERR_WOULDBLOCK`),均无副作用 |
| 模拟器端 | `net_socket_sim.c` 同名实现**恒返回 1**——Winsock 本就非阻塞、无 RPC 成本,恒 1 使门控在模拟器上逐位等价于改前行为 |
@@ -1412,7 +1412,7 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po
| 日期 | 变更 |
|------|------|
| 2026-10-10 | 新增 **`net_can_accept()` / `net_can_read()` 就绪门控 API**(§10.1):纯 RAM 读控制块、**不走消息队列、不等 netTask 回执**,供实时性敏感任务在发 RPC 前判断"有没有活"(动因 = `ui_net_sink_poll()` 每 10 ms 一次 `net_accept` 把 uiTask 拖成最坏 ≈50 ms;原拟登记 N-19 / C7,当日结案不入登记簿;设计与实测见 `docs/问题记录/UI任务看门狗停摆分析.md` §9);§3.5 补"netTask 运行要求"例外条;⚠ 记录两条使用约束——**不可作为唯一判据**(`readable` 只由 RECV_OK 中断置位,须保留低频兜底 recv)与**模拟器后端恒返 1**(`net_socket_sim.c`,Winsock 无 RPC 成本) |
| 2026-10-10 | 新增 **`net_can_accept()` / `net_can_read()` 就绪门控 API**(§10.1):纯 RAM 读控制块、**不走消息队列、不等 netTask 回执**,供实时性敏感任务在发 RPC 前判断"有没有活"(动因 = `ui_net_sink_poll()` 每 10 ms 一次 `net_accept` 把 uiTask 拖成最坏 ≈50 ms;原拟登记 N-19 / C7,当日结案不入登记簿;设计与实测见 `docs/子系统设计/看门狗设计说明.md` §11(原分析文档已并入该节));§3.5 补"netTask 运行要求"例外条;⚠ 记录两条使用约束——**不可作为唯一判据**(`readable` 只由 RECV_OK 中断置位,须保留低频兜底 recv)与**模拟器后端恒返 1**(`net_socket_sim.c`,Winsock 无 RPC 成本) |
| 2026-09-23 | 同步 L3 pending 化、D10 非阻塞重监听、Trap 22 对账、Trap 23 排渣隔离后的现状:新增 `net_recv_to` / `net_sock_state` / `net_resync_listen` / `net_relisten`(§10.1)及 `net_getsockname`;§3 新增 3.6(对账与自愈四机制)、3.3/3.5 改写(recv 挂起模型、队列语义、netTask 运行要求);§4/§5 示例更新(监听由就绪屏障后服务任务建立、`net_sock_state` 轮询);§10.2 改述 `net_process_messages()` 每轮节奏(重监听重试 → 挂起 recv 完成 → 200ms 对账 → 队列),补 `timeout_ms`/新消息类型说明;新增 §11.2 分片阻塞读(`net_recv_to`);§12.1/12.3/12.4/12.9 更新,新增 §12.10(自愈行为数据与排障判据,含 Trap 21 结案) |
| 2026-09-21 | **修正公开 API 的错误码回传**:`net_msg_t` 按值拷入队列 → netTask 对消息的写入调用方不可见,改用 `xTaskNotify()` 通知值回传具体错误码(负值),公开 API 恢复 errno 后统一返回 -1(此前被覆盖成 `NET_ERR`,使 `net_get_errno() != NET_ERR_WOULDBLOCK` 的"空轮询"判断恒成立 → `ui_net_sink` 连上即 `net_close`);§12.4 补单连接模型断开处理(EOF 时不得 `net_close` 监听 socket);§12.9 补错误码回传约束 |
| 2026-09-15 | 同步配置引擎属主变更(P1-3):`net_init` 调用方说明改为"netTask 只读 RAM 副本"(网口配置由 defaultTask 在就绪屏障前加载);模拟器端启动序列补 `net_cfg_save_run()`(主循环 = 模拟器侧配置引擎属主,执行 UI 保存请求) |
@@ -1,560 +0,0 @@
# UI 任务看门狗停摆分析(Task #32)
> **对象**:`[ERR][WD] task stall: mask=0x00000067 req=0x00000077`(缺 bit4 = `TASK_WD_UI`)
> → 连续 8 次(800ms)缺席 → 停喂 IWDG → 复位。
> **背景**:L2 相量层落地(commit `3762c03`)后**新出现**的间歇性回归:L2 版 3/10 次运行失败,
> 改前基线(`git stash` 重建)0/6。
> **状态**:**分析完成,全部处置已落地并上板验证**(2026-10-10)。P0(adc 压环条件 + 截止期
> 重同步)、P1-4a(UI 心跳自产)、P2(常开 `[PROF]` 探针)、§9 方案 A(推流就绪门控 + 兜底)、
> **P1-5(ADC 消费改定时器 ISR 事件驱动 + `adcTask` 提至 `AboveNormal`;同日 v2.8 进一步把
> 节拍源定为 TIM3 并让 STM32 ADC1 走 TRGO + DMA 乒乓缓冲,§4.4 的 2 ms 忙等已消除)** 全部落地;停摆累计
> **38 次上板启动 0 复现**(P0 后 22 次 + A 后 10 次 + P1-5 后 6 次)。§9 方案 B
> (把推流搬出 uiTask)**已评估并决定不做**。
> **阅读口径**:§2~§7 是**故障当时**的证据与归因,其中已被处置改掉的代码片段就地标注
> **改前 / 现状**;§8 末「当前健康剖面」是收口后的实测基线。⚠ 实测还修正了当初的归因——
> 刷新最坏周期(`uip` ≈61 ms)是 **LVGL 帧周期地板,与网络无关**(§9.3.2)。
> **关联**:
> [`../待处理内容.md`](../待处理内容.md)(**N-17** 心跳缺口登记)、
> [`../子系统设计/看门狗设计说明.md`](../子系统设计/看门狗设计说明.md)(`required_mask` / 抽样窗 / 长耗时白名单)、
> [`../子系统设计/ADC采样节拍设计说明.md`](../子系统设计/ADC采样节拍设计说明.md)(1 kHz 节拍)、
> [`../测试说明/实机测试实施指南.md`](../测试说明/实机测试实施指南.md)(停喂取证机制)、
> [`../../AGENTS.md`](../../AGENTS.md)(看门狗铁律)
---
## 1. 结论速览
**用户假设"计算量太大、CPU 负载满了"——方向对(L2 的 CPU 开销确实是触发因素),但结论不成立。**
实测占空比约 **10%~20%**,距饱和很远(§3 有算式与日志反证)。真正的失效是
**优先级饥饿 + 心跳路径依赖另一个任务**:
1. `uiTask` 是 `required_mask` 六个任务里**唯一的 `osPriorityBelowNormal`**(其余五个全是
`osPriorityNormal`)。FreeRTOS 下**就绪的 Normal 任务永远抢占 BelowNormal 任务**,
时间片轮转只对**同优先级**生效 → uiTask 只能在"五个 Normal 任务同时阻塞"的缝隙里运行。
2. **(改前)** `uiTask` 的心跳**不是自产的**:每轮先调 `ui_net_sink_poll()` → `net_accept()`
→ `osMessageQueuePut` + **`xTaskNotifyWait(portMAX_DELAY)`**,即**绕 `netTask` 走一次往返**,
之后才 `task_wd_beat(TASK_WD_UI)`——**netTask 慢一拍,UI 心跳就晚一拍。**
**现状**:P1-4a 已把心跳提到 poll **之前**(`App/task/ui_task.c:159`),A 门控又让无客户端时
**一次 RPC 都不发**(§9.3)→ 这条依赖已断开,见 §4.2。
**决定性证据**:失败时掩码 `0x67` 只缺 UI 一位,其余五个任务(default / net / ftp / timeSync / adc)
的心跳**全部正常**。若是 CPU 饱和,Normal 级的 netTask/ftpTask/adcTask 会**一起**掉线——它们没有。
---
## 2. 证据:失败日志时间线
唯一留存的失败日志:`.dsh/skills/serial-assistant/scripts/state/serial.log`(2026-10-10 17:36)。
(其余失败运行未归档,`docs/测试说明/boot_log_data/` 只存了通过的两次。)
| 时刻(抓包侧) | 事件 |
|---|---|
| 17:36:12.551 | 内核启动;`reset cause: software (NVIC_SystemReset) (CSR=0x14000000)` |
| 17:36:12.813 | `phasor layer init: OK (inst=5)` |
| 17:36:17.002 | `[FS] FATFS mounted`(距 NAND FTL ready 17:36:12.897 **约 4.1 s**) |
| 17:36:17.196 | `MISS start snap=0x00000067 req=0x00000077 t=4756` |
| 17:36:17.206 | `[WD] armed: timeout ~2498ms (worst ~1701ms), miss_tol=800ms` |
| 17:36:17.206 | `UI task: started, wait net ready` |
| 17:36:17.868 | `[STACK] ui peak_used=488/16384`(UI 尚未渲染) |
| 17:36:18.219 | `UI task: net ready, init port` → 菜单/待机屏初始化 |
| 17:36:18.343 | **`UI net sink: started (port 7003)`** ← **UI 的最后一条日志** |
| 17:36:18.343 | `MISS start snap=0x00000067 req=0x00000077 t=5904` |
| 17:36:19.902 | `[STACK] ui peak_used=4052/16384`(LVGL 开始渲染) |
| 17:36:22.850 | `[ADC_TASK] frames=9957 idle=44 skip=0 seq=10005 frst_err=0 adc1_rc=0 adc1_err=0 ring=10001` |
| 17:36:23.027 | `MISS start snap=0x00000067 req=0x00000077 t=10585` ← **最后一轮 episode 起点** |
| 17:36:23.743 | `STALL snap=0x00000067 req=0x00000077 h=00000067 ×8 mg=330 cf=0 seg=1` |
| 17:36:23.826/.837 | `[ERR][WD] task stall: mask=0x00000067 req=0x00000077 miss=11 / miss=12` |
| 17:36:23.951 | `[STACK] ui peak_used=4052/16384`(与 19.902 相同 → UI 此后**没有推进过**) |
| 17:36:32.855 | `frames=19352 idle=649 skip=0 seq=20005 frst_err=0 adc1_rc=0 adc1_err=288 ring=20001` |
| 17:36:34.981 | 再次启动:`reset cause: software (NVIC_SystemReset)` |
**读法说明**(`App/util/task_watchdog.c:434`):`t=` 是毫秒计数;`miss=` 是**累计**缺席次数
(`s_miss_total`),不是连续次数;连续次数 `s_miss_streak` 饱和在 `WD_MISS_TOLERANCE=8`。
故 `t=10585` 起算、`t≈11385` 达 8 次 → **缺口恰为 800ms**,与 `miss_tol=800ms` 一致。
**关键观察**:
- UI 的最后一条日志是"推流已启动",**之后进入主循环就再没有任何心跳**(`ui peak_used` 冻结在 4052)。
- 同时段 adcTask、defaultTask(`[STACK]`)、net/ftp/timeSync 全部活着 → **非全局饥饿**。
- `ui peak_used=4052/16384` → UI 栈余量 75%,**排除 UI 栈溢出**。
---
## 3. 量化:CPU 到底占了多少(回答"负载满了")
### 3.1 逐项算式
| 项 | 算式 | 结果(@168 MHz,1 ms = 168 000 周期) |
|---|---|---|
| ADC1 顺序扫描 | `ADC_CLOCK_SYNC_PCLK_DIV4`(84 MHz / 4 = 21 MHz);每通道 (144 + 12) 周期 = 156/21 MHz ≈ 7.4 µs × **9 通道** | **≈ 67 µs** |
| TPAFE 原子快照 + 9 路 `adc_input_raw()` | 临界区拷贝 8 值 + 9 次查表 | ≈ 10~20 µs |
| 采样环压帧 + `phasor_run()` | 5 实例 × (20 点窗拷贝 + 20 次取模 + 40 次浮点乘加 + `sqrtf`) ≈ 400~500 周期/实例 | **≈ 10~15 µs** |
| 其中:整数取模 | 窗 100 次(`% 40`,常量 → 魔数乘)+ 计算 100 次(`% n`,**运行期变量 → 走 UDIV/`__aeabi_uidivmod`**) | ≈ 3~7 µs(**占比 0.3%~0.7%**) |
| **adcTask 单拍合计** | 上列之和 | **≈ 0.10~0.13 ms → 占空比 10%~13%** |
**含 ADC 超时的最坏情形**:日志 `adc1_err=288`(10 s 窗口)= 288 次超时 × `ADC_INPUT_POLL_TIMEOUT_MS=2` ms
= **576 ms 额外忙等 / 10 s → +5.8%**。故 adcTask 最坏约 **16%~19%**。
### 3.2 结论
**合计 10%~20%,不是"负载满了"。** 按 1 ms 节拍算,adcTask 每拍仍能睡约 0.87 ms。
**现状印证**:收口后 `[PROF2]` 的 `adcp`(ADC 单次最坏)实测 **0.12~2.5 ms**、`[PROF]` 的
`adcm`(单拍最坏)**≈2.1 ms**;`adc1_err` 从当年的 **288/10 s** 降到 **1**(§7.2)——
上表"+5.8% 忙等"那一项如今基本不出现。
### 3.3 日志反证(有力)
`ring` 计数在两次心跳间严格 **+10000 / 10 s = 恰好 1000 次/秒**
(`10001 → 20001`,`9957 → 19352` 同理)。
- 若 adcTask **失去阻塞**(空转),其循环速率 = 1/单拍耗时。按 0.11 ms 的单拍耗时算,
空转应为 **约 9000 次/秒** → `ring` 会涨 ~90000/10 s。
- 实测恰好 1000 次/秒 → **adcTask 确实每拍都在 `osDelayUntil` 里睡满**,没有空转。
**因此:在失败窗口内,adcTask 不是 CPU 独占者。** 这同时否掉了"计算量把 CPU 吃满"的直接说法。
---
## 4. 三个结构性脆弱点(均已从代码证实)
### 4.1 uiTask 优先级垫底(**这是失效能被"定点打击"的根因**)
`Src/freertos.c` 的任务属性 + `Middlewares/.../CMSIS_RTOS_V2/cmsis_os2.c:444`
(`prio = (UBaseType_t)attr->priority`,FreeRTOS 中**数值越大优先级越高**):
| 任务 | `osThreadAttr_t.priority` | uxPriority | 在 `required_mask` |
|---|---|---|---|
| defaultTask | `osPriorityNormal` | 24 | ✅ |
| netTask | `osPriorityNormal` | 24 | ✅ |
| ftpTask | `osPriorityNormal` | 24 | ✅ |
| timeSyncTask | `osPriorityNormal` | 24 | ✅ |
| adcTask | `osPriorityNormal`(**改前**;P1-5 后为 `osPriorityAboveNormal`) | 24 → **32** | ✅ |
| **uiTask** | **`osPriorityBelowNormal`** | **16** | ✅ |
| testTask | `osPriorityBelowNormal` | 16 | ✖(产品构建不创建) |
→ **uiTask 是必需掩码里唯一的低优先级任务。** 只要有**任何一个** Normal 任务持续就绪,
uiTask 就**完全拿不到 CPU**;`configUSE_TIME_SLICING=1` 只保护**同优先级**之间的公平,
对它无效。
> **P1-5 落地后的变化(2026-10-10)**:`adcTask` 已提到 **`AboveNormal`(32)** → 本表"uiTask
> 垫底"的格局更极端。之所以可接受:① 改造后 `adcTask` 由定时器(现 TIM3)**精确 1 kHz** 唤醒,单拍
> µs~2.25 ms 且**不再有空转窗口**——改前的 `osDelayUntil` 一旦超拍就永久落后 → 不再阻塞 →
> 以 Normal 全程抢占 uiTask,那才是本表真正担心的形态(P0-2 已修,见 §4.3);② 实测 6 次上板
> `uip` / `uiw` / `uis` 与提优先级前**一字不差**(61.3 ms / 11.4 ms / 1 µs)。
> ⚠ 换来的新暴露见 §4.4 现状。
### 4.2 UI 心跳依赖 `netTask` 往返(**为什么"只有 UI 掉"**)
**改前**(`ui_task.c` v1.2,本次失效的现场)主循环:
```c
while (1) {
ui_port_step(10); /* LVGL:lv_tick_inc + lv_timer_handler(同步渲染) */
ui_net_sink_poll(); /* ← 阻塞点 */
task_wd_beat(TASK_WD_UI);/* ← 心跳在这之后才发 */
osDelay(10);
}
```
**改前** `ui_net_sink_poll()` 在**无远端客户端时**(台架常态)每轮都走 `net_accept()`;
而 `net_accept()`(`Drivers/BSP/NET/net_socket.c`)的固定模式是:
```c
if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { ... } /* 队列深 8,满则永久阻塞 */
msg.result = net_wait_result("acc", sockfd); /* 等 netTask 回执 */
```
`net_wait_result()`(`net_socket.c:851`)末句是 **`xTaskNotifyWait(..., portMAX_DELAY)`——无限期阻塞**。
→ **改前 UI 的心跳周期 = LVGL 渲染 + 一次 `netTask` 往返 + 10 ms。** `netTask` 被延迟多久,
UI 的心跳就晚多久。**这是 UI 独有的依赖,其它任务没有**——正好解释"只有 UI 位缺失"。
**现状(两处已改,此依赖已断开)**:① **P1-4a** 把 `task_wd_beat(TASK_WD_UI)` 提到 poll
**之前**(`App/task/ui_task.c:159`,poll 在 `:164`)→ 心跳从此只测"UI 自己被调度到";
② **§9-A 门控**让无客户端时**一次 RPC 都不发**(`uis` 实测 1 µs,§9.3.2)。
有客户端时往返仍在,属刻意保留的残留暴露(§9.4)。
### 4.3 adcTask 的截止期一旦错过就**不可恢复**(**已修 = P0-2**)
**改前**(`App/task/adc_task.c`):
```c
next += ADC_TASK_BEAT_MS; /* 固定 +1ms,不随实际进度修正 */
(void)osDelayUntil(next); /* ← 返回值被丢弃 */
```
`osDelayUntil` 在**目标时刻已过**时返回 `osErrorParameter`(`cmsis_os2.c:898`)。
若单拍耗时 > 1 ms,`next` 相对当前时刻**永久落后** → `osDelayUntil` 立刻返回 →
**该任务从此不再阻塞**,以 Normal 优先级空转,直到某个"快拍"把差额追平。
(单次 2 ms 的慢拍只会多跑一拍就追平,见 §5 的 `ring` 反证;**连续**慢拍才会锁死。)
**返回值是现成的重同步钩子,但被丢弃了。**
**现状(P0-2,`adc_task.c:277-280`)**:
```c
next += ADC_TASK_BEAT_MS;
if (osDelayUntil(next) != osOK) { next = osKernelGetTickCount(); } /* 落后 → 一次性拉回基准 */
```
实测印证:单拍最坏 `adcm ≈ 2.1 ms` **确实会越过 1 ms 节拍**,但 `ring` 仍严格 1000 次/秒
(§3.3 的判据依旧成立)→ 重同步在生效,没有退化成 Normal 级空转。
**再现状(P1-5,2026-10-10)**:这段已**整体删除**——`ADC_TASK_BEAT_MS` 与 `next` 都不在了,
循环尾改为 `(void)osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever)`,唤醒由
定时器(现 TIM3)ISR 提供。**"超拍永久落后 → 空转"这一类缺陷在结构上消失**(不再有自定截止期,也就没有
"落后"这个概念),P0-2 的重同步钩子随之作废。实测 6 次上板 **`idle = 0`**(改前 1~649),
见 §8 与 [`../子系统设计/ADC采样节拍设计说明.md`](../子系统设计/ADC采样节拍设计说明.md) §10。
### 4.4 ADC 超时是"2 ms 纯忙等",不阻塞、不让出
`App/algo/adc_input.c:21` `ADC_INPUT_POLL_TIMEOUT_MS = 2U`,配合
`HAL_ADC_PollForConversion()`(`stm32f4xx_hal_adc.c:882`)里的
`while (!EOC) { if ((HAL_GetTick() - tickstart) > Timeout) return HAL_TIMEOUT; }`
—— **一个通道超时 = 2 ms 不让出 CPU 的自旋**,且发生在 Normal 优先级。
这是现成的饥饿源。
> **单拍上界修正(2026-10-10 复核)**:本段原文写"9 通道全超时最坏 = **18 ms** 连占"**有误**。
> `stm32_scan()` 在**第一个**超时处就 `HAL_ADC_Stop()` + `return`(`App/algo/adc_input.c:79-84`),
> 从不轮询剩余通道 → **单拍最坏 = 一次超时 ≈2~3 ms**(`HAL_GetTick()` 是 1 ms 粒度、判据是
> `> Timeout`),不是 9 次。
**现状(v2.8,2026-10-10:这条饥饿源已消除)**:`ADC_INPUT_POLL_TIMEOUT_MS` 与
`HAL_ADC_PollForConversion` **已从代码中删除**——STM32 ADC1 改由 **TIM3 更新事件经 TRGO
硬件触发 + DMA 循环搬运**,任务侧只消费 DMA 已写好的乒乓半区,单拍 `adcp` 实测
**122 µs → 8 µs**;"超时"这个概念在 L1 层不复存在,因此也不存在 2~3 ms 自旋。
完整设计与实测见 [`../子系统设计/ADC采样节拍设计说明.md`](../子系统设计/ADC采样节拍设计说明.md) §11。
下面两段保留,作为该结论的推导与当时的实测依据(历史):
- 这条饥饿源如今**基本不被触发**,且**只在超时拍出现**:6 次上板对照——超时 1 次的那轮
`adcp = 2209 µs`,超时 0 次的 5 轮 `adcp = 122 µs`(= 9 通道 × 7.4 µs + TPAFE 快照);
`adc1_err` 从 288/10 s 降到 **0~1**;
- ~~但 `adcTask` 现已在 **`AboveNormal`(32)**,一旦某拍真走到 2 ms 忙等,它挤的是
netTask / ftpTask / defaultTask / timeSyncTask(改前是同级竞争)→ **这是 P1-5 换来的新暴露,
记在账上**~~ → **v2.8 已消**:忙等本体没了,这条新暴露随之关闭(2026-10-10 实测
`adcp = 8 µs`、`adc1_err = 0`、`adc1_hw = 0`)。
- ✅ **超时的第二个代价是正确性,不只是耗时(2026-10-10 已修)**:超时拍 `s_stm32_valid` 此前
仍是上次成功留下的 1,而 `adc_input_poll()` 的返回值只由 TPAFE 决定 → 该拍照样 `r == 1`、
照样压环,STM32 侧几路(UB/UC/IB/IC/IN/温度)**静默沿用上一拍的值**(且超时前几个 Rank 已写
本拍新值 → 半新半旧的混合帧);`s_frames_skip` 只统计"从未成功过",抓不到它。
**修复** = `stm32_scan()` 两条失败支路(`HAL_ADC_Start` 失败 / 轮询超时)**都清
`s_stm32_valid`** → 本拍 `adc_input_raw()` 对 STM32 语义通道返 -2 → `adc_task` 走既有
`skip` 整帧跳过路径,下一拍成功自动恢复。主机侧新增 **TC-AI-070..078** 钉死(旧用例
TC-AI-034/039b 都是"init 后立刻失败",`valid` 本就是 0,照不出这条路径),并做**反向验证**:
把支路改回旧行为 → 3 条立即 FAIL(76/79)。**剩余未做 = 消掉 2 ms 忙等本身(DMA)**
→ ✅ **已办(同日 v2.8)**:DMA + TIM3 TRGO + 乒乓双缓冲落地,`stm32_scan()` 换成
`stm32_take()`;"成功过之后失败 → 静默沿用旧值"这条缺陷路径在结构上消失(无超时概念),
改由"本拍无新帧"触发同一条 `skip` 防线,主机用例 **TC-AI-070..078** 原样保留并仍反向验证。
---
## 5. 顺带发现的一个**独立**缺陷:无新帧仍然压环(**已修 = P0-1**)
**改前**(`App/task/adc_task.c`):`r == 0`(TPAFE 无新帧)时**仍然**执行
`sample_ring_push_frame()` + `phasor_run()`——只有 `r == -1` 才不压。
日志精确印证(`ring = frames + idle`):
| 窗口 | frames | idle | ring | 校验 |
|---|---|---|---|---|
| 09:36:21 | 9957 | 44 | 10001 | 9957 + 44 = 10001 ✓ |
| 09:36:31 | 19352 | 649 | 20001 | 19352 + 649 = 20001 ✓ |
**后果**:把**上一拍的重复采样值**塞进 20 点 DFT 窗 → 等效采样间隔被拉长。
本例 `idle` 占 **3.2%** → 窗长从 20.0 ms 漂到约 20.6 ms → 对 50 Hz 信号相当于
**约 3% 的频率失配** → 幅值/相位**系统性误差**(不是随机噪声)。
这与本次停摆**无关**(不增加多少 CPU),但属**L2 的真实正确性缺陷**,应一并修:
**只在 `r == 1` 时压帧**。
**现状(已修,`adc_task.c:180`)**:仅 `r == 1` 才 `sample_ring_push_frame()` + `phasor_run()`。
上板直接印证:`frames=19999 idle=1 ring=19999` → **`ring == frames`**(修前是
`ring = frames + idle`),且 `idle` 从 649/20001 降到 **1/20000**(另见 §7.5)。
---
## 6. 为什么"只有 UI"、"为什么间歇"
- **只有 UI(改前)**:UI 是唯一低优先级(§4.1)**且**心跳依赖别的任务(§4.2)的必需任务。
其它五个任务只要 100 ms 内被调度到一次就能喂上位,而它们全在 Normal 级,彼此时间片轮转保障。
**现状**:低优先级这一条**至今未变**(§4.1 的表仍成立,uiTask 仍是掩码里唯一的
BelowNormal),变的是后半条——心跳已自产(P1-4a)、往返已门控(§9-A)。
- **间歇**:需要一次"某个 Normal 任务连续占用 ≥800 ms"的巧合。**当时的**候选(见 §7):
`netTask` 回执延迟、`defaultTask` 的长段(syslog → NAND/FTL 写、配置保存)、
adcTask 的超时突发。**现状**:探针已把这三条的上界量出来——`defd ≈ 46 ms`(控制台排空)、
`adcm ≈ 2.1 ms`(单拍)、`uis = 1 µs`(推流往返),**没有任何任务接近 800 ms**
(§8「当前健康剖面」)。
- **为什么 L2 才暴露**:L2 让 adcTask 每拍多干 10~15 µs 并**引入了 `adc1_err` 超时**
(L1 阶段台架 `adc1_err=0`,L2 阶段出现 288/10 s)→ Normal 级的可用余量被吃掉,
uiTask 的缝隙被挤到 0。**这正是"负载上去以后最先倒下的那块短板"。**
---
## 7. 尚未证实、需要测量的部分(诚实边界)
1. **800 ms 的具体占用者未锁定。** 本次日志只能证明"UI 被饿/被拖了 800 ms",
不能区分是
(a) `netTask` 回执延迟(§4.2),还是
(b) `defaultTask` 长段(syslog NAND 写 / 配置保存),还是
(c) adcTask 超时突发(§4.4)。
**补充(2026-10-10,P2 测量 + §9-A 收口后):累计 32 次上板启动(P0 后 22 次 +
A 后 10 次)全部 0 复现停滞**——健康剖面里最大的单一非让出占用是 `defaultTask`
控制台排空 `defd ≈ 46 ms`,其次是 ADC 单拍 `adcm ≈ 2.1 ms`(⚠ 复核后归因已修正:该峰值来自
每 10 s 一次的心跳日志,不是采集,见 §4.4)、ADC 单次 `adcp ≈ 0.12~2.5 ms`,
**没有任何任务接近 800 ms**。故 800 ms 属异常长尾,靠健康剖面无法指认,只能靠复现现场的
`[PROF]/[PROF2]` 快照;但 P0 已把回归压到 0(原 L2 基线 3/10),暂不再阻塞推进。
2. **`adc1_err=288` 的自身根因仍未查,但现象已随 P0-1 基本消失。** 2 ms 超时对 7.4 µs 的
转换而言异常巨大,指向 ADC 被反复 `Start/Stop` 或 EOC/OVR 状态异常。
**现状**:失败日志 **288/10 s** → 收口后 **1/20 s**;若再现,优先回头查这条。
**已补一项候选根因(2026-10-10)**:F4 ADC 的**上电自校准**此前在本项目**完全缺失**
(CubeMX 不生成该步骤,且本仓库裁剪版 HAL 没有 `HAL_ADCEx_Calibration_Start`)→ 现由
`Src/adc.c` 的 `adc1_calibrate()` 补上(详见
[`../硬件平台/ADC通道映射说明.md`](../硬件平台/ADC通道映射说明.md) 的"ADC1 上电自校准"行)。
实测校准让 STM32 侧 6 路**同向**偏移 +10~+63 LSB → 说明此前 ADC 确实带失调在跑。
它直接解释的是"读数带偏",未必等于 EOC 超时的根因,但"未校准状态下 ADC 内部行为异常"
是可疑路径,故记为 §7.2 已收敛的一半。
3. **复位归因已澄清为非诊断项。** 失败日志报 `reset cause: software (NVIC_SystemReset)
(CSR=0x14000000)`(即 `SFTRSTF|PINRSTF`);但 2026-10-10 的健康启动**同样**报
`software (NVIC_SystemReset)` 且判定 PASS——说明本台架上电复位本就走软件复位路径
(或烧录工具经软件复位),**不能据此区分"IWDG 溢出"还是"台架拉 NRST"**。停摆的权威
证据仍是 `[ERR][WD] task stall` 行本身,而非复位原因寄存器。
4. **`[STACK]` 周期从 2.0 s 拉到 4.05 s**(17:36:19.902 → 23.951,标称 `STACK_PROBE_PERIOD_MS=2000`)
值得注意,但**不能作为证据**:控制台缓冲满时会**丢行**,该报告也可能只是被丢了。
5. **`idle` 的物理含义**:任务轮询与帧产生之间的相位漂移(`osDelayUntil` 的 1 ms 内核 tick
vs 定时器(现 TIM3)的 1 kHz 硬件节拍,两个时钟源不同),本身不是故障。
**现状**:P0-1 后 `idle` 只计数、不再压环,实测也从 649/20001 降到 **1/20000**——
当年"它会让 §5 的缺陷持续发生"这条耦合已解除。
6. **启动阶段 `MISS start snap=0x67` 是良性的(2026-10-10 新增)。** `[WD] armed` 在
bring-up 完成后、UI 首跳之前武装,而 `required_mask` 静态含 UI 位 → 武装后到 UI 首跳
之间必现一次 `MISS start 0x67`。健康运行(`FATFS mounted≈0.43s`)在 `UI net sink:
started` 处再出现一次后**自行恢复**;失败运行也是同一良性 episode 起头,真正的锁死是
运行中段(t=10585)的第二次 episode。**故该 episode 本身不是故障判据**,只作"UI 尚未
起跳"的旁证。
7. **`netp`(netTask 的 `net_poll()` 最坏)恒报 0 µs——这是真实值,不是探针坏了。**
`net_poll()` 开头先读 INT# GPIO,高电平(无中断待处理)就直接 `break`
(`Drivers/BSP/NET/net_socket.c:431`)→ 空闲时整条 `net_poll()` 只是一次 GPIO 读,
< 1 µs 合理。读 `[PROF2]` 时**不要**把 `netp=0` 误判成"探针没接上"(接线在
`App/task/net_task.c:276-278`,每轮都在测)。
---
## 8. 处置:决策与落地结果(2026-10-10)
> 本节原本是"待确认后执行"的分层建议;处置已按 **P2 先测 → 按数据定 P1 → P0 顺路一起修**
> 的顺序执行完毕,故改写为**决策 + 结果**记录。当初的取舍理由仍是活约束,保留在状态列里。
| 项 | 内容 | 状态 |
|---|---|---|
| **P0-1** | §5 压环条件:仅 `r == 1` 时压帧 + 重算 | ✅ 已落地,上板印证 `ring == frames` |
| **P0-2** | §4.3 截止期重同步:`osDelayUntil` 返回值不再丢弃 | ✅ 已落地;**后被 P1-5 取代**——整段 `osDelayUntil` 已删,"落后"这个概念不存在了(§4.3 再现状) |
| **P1-3** | 把 uiTask 提到 `osPriorityNormal` | ❌ **刻意不做**:它会**掩盖** §4.2/§4.4 两个真实缺陷(当时的判断,现仍成立) |
| **P1-4a** | 心跳提到 `ui_net_sink_poll()` **之前** | ✅ 已落地(`ui_task.c:159`,v1.3) |
| **P1-4b** | 给 `net_accept()` 一条非阻塞查询路径 | ✅ 以更彻底的形式落地 = **§9-A 门控**(`net_can_accept()` / `net_can_read()`) |
| **P1-5** | 原列"adcTask 单拍硬上界(ADC 改 DMA / 超时整拍跳过)"→ 经用户决策改走**方案 a:事件驱动消费**(TIM6 ISR 置位 `ADC_WAKE_FLAG` 唤醒 + `adcTask` 提至 `AboveNormal`) | ✅ **已落地**(2026-10-10;6 次上板 **`idle=0`**、`frames` 严格 1000 Hz、UI 侧探针一字不差;完整记录见 [`../子系统设计/ADC采样节拍设计说明.md`](../子系统设计/ADC采样节拍设计说明.md) §10)。⚠ 原列的"**单拍硬上界**"(DMA / 超时整拍跳过)**已办(同日 v2.8)**——STM32 ADC1 改 TIM3 TRGO 硬件触发 + DMA 乒乓双缓冲,忙等本体删除,
`adcp` 122 µs → **8 µs**;该"新暴露"随之关闭(§4.4 现状)。完整记录见
[`../子系统设计/ADC采样节拍设计说明.md`](../子系统设计/ADC采样节拍设计说明.md) §11 |
| **P2-6** | 启用现成的 `TEST_LATENCY_PROBE`(`ui_task.c` 的 `ui_loop` 段) | ✅ 已被**常开探针 `App/util/task_prof.c/.h`** 取代(`[PROF]`/`[PROF2]`),无需再开测试开关 |
| **P2-7** | 新增 adcTask 单拍 / 推流往返两个埋点 | ✅ 已落地(`TASK_PROF_ADC_BEAT` / `TASK_PROF_ADC_POLL` / `TASK_PROF_UI_SINK`) |
| **P2-8** | 停摆取证带上"各任务在本窗口的调用计数" | ✅ 以更彻底的形式落地 = 心跳位图 **1→0 边沿**裸 UART 直发现场快照 |
### 关键测量结论(当时决定 P1 取舍的依据)
- **改前**健康运行 `uiw ≈ uis ≈ 50 ms` → UI 单轮耗时几乎等于推流往返,证实 §4.2:
心跳实际测的是"netTask 响应性"而非"UI 活着"。
- **改前** `uisn = 0`(推流段无"仍在段内"积压)→ "永久卡在往返"这一机理被排除。
- 最大单一非让出占用一直是 `defaultTask` 控制台排空(46~48 ms),**没有任何任务接近 800 ms**
→ 800 ms 属异常长尾(§7.1)。
### 验证与遗留
- **验证**:P0 后累计 22 次上板 0 复现;§9-A 收口后再 10 次;**P1-5 落地后再 6 次** →
**累计 38 次 0 复现**(原 L2 基线 3/10)。构建 0 错误 0 警告;P1-5 后体积
`Code=256728 RO=16160 RW=808 ZI=139760`(+360 B:ISR 入口 + `osThreadFlagsWait` 路径)。
- **遗留(仍未办)**:§7.2 `adc1_err` 自身根因、§7.4 `[STACK]` 周期漂移、§4.4 的 ADC 忙等
(未改 DMA / 未做超时整拍跳过——P1-5 解掉的是"相位抖动 + 超拍空转"两类,**没有**解忙等)。
停摆若再现,优先回头查 §7.2。
### 当前健康剖面(2026-10-10 **P1-5 后**复测:`@test` 6 连跑,无客户端,开机 29 s)
| 探针 | 现值(最坏) | 含义 |
|---|---|---|
| `uip` / `uil` | **61.3 ms** / 10.0 ms | uiTask 循环周期最坏 / 标称——地板是 LVGL 帧周期(§9.3.2) |
| `uiw` | **11.4 ms** | UI 单轮工作,**只含 LVGL**(改前 ≈50 ms 且 ≈`uis`) |
| `uis` / `uisn` | **1 µs** / 0 | 推流往返(A 门控后;改前 ≈50 ms)/ 从未卡在段内 |
| `adcm` / `adcp` | 2.12~2.25 ms / **122 µs**(超时拍 2.2 ms) | adcTask 单拍 / `adc_input_poll()` 单次(P1-5 后跑在 `AboveNormal`)。⚠ `adcm` 的 2 ms 峰值来自每 10 s 一次的心跳日志,不是采集(§4.4) |
| ADC 消费计数 | `frames` 1000.0/s、**`idle = 0`**、`ring == frames` | P1-5 后唤醒与生产同源 → 相位抖动归零(改前 `idle` 1~649 / 20000 拍) |
| `defd` / `defs` | 46 ms / 7 µs | 控制台排空 / syslog 落盘——**当前最大的单一非让出占用** |
| `netp` / `netn` | 0 µs / 0 | `net_poll()` 空闲即一次 GPIO 读(§7.7) |
| `gl` | 0 | DWT 无丢样,以上数据可信 |
---
## 9. 彻底解耦(2026-10-10 结案:**A 已落地,B 决定不做**)
> **结案状态**:**§9.3 方案 A(门控 + 兜底)已落地并验证**(`net_can_accept()`/`net_can_read()`
> + `ui_net_sink_poll()` 先门控再 RPC;`@build` 0 错 0 警、`@test` 10/10 PASS、e2e 0/25600 位差异);
> **§9.4 方案 B(把推流搬出 uiTask)已评估并决定不做**,理由与残留暴露写在 §9.4。
> **动因**:P1-4a 之后"心跳"不再被推流拖,但**刷新仍被推流拖**——现象从"偶发复位"变成
> "画面周期性卡顿 / 刷新间隔不稳"。**实测结论修正了这个动因**:刷新的最坏周期其实是
> LVGL 帧周期地板,与网络无关(§9.3.2)。
> **登记**:N-19 / C7 **不入登记簿**——当日登记当日结案,结论只沉淀在本节。
### 9.1 先把"poll 运行时间不可预测"量化:3 个**没有上界**的等待点
`ui_net_sink_poll()` 自己不产生耗时——它只是**向 netTask 发 RPC 并原地等回执**
(`net_*` 公开 API 的固定模式:`osMessageQueuePut` → `net_wait_result`,见
`Drivers/BSP/NET/net_socket.c:851`)。
| poll 内调用 | 触发频率 | 实际代价 | 上界 |
|---|---|---|---|
| `osMessageQueuePut(..., osWaitForever)` | 每次 RPC | 队列深 **8**,与 FTP / timeSync 共享(原 N-10,已销项) | **无**(满则永久阻塞) |
| `net_wait_result()` → `xTaskNotifyWait(..., portMAX_DELAY)` | 每次 RPC | 等 netTask 排到本条(它还要跑 `net_poll` + 3 个自愈 tick + FTP 大文件) | **无** |
| `net_accept()` | **改前**:无客户端时每 10 ms 一次(台架常态)= **100 msg/s**;**现状:0 次**(A 门控) | 一次往返 + `net_accept_locked()` 内再跑一遍 `net_poll()`(整轮 SPI 中断读) | 改前实测最坏 **≈50 ms** → 现状 **1 µs**(§9.3.2) |
| `net_recv(NET_MSG_DONTWAIT)` | 有客户端时每轮;**现状**:`readable` 门控 + 每 32 轮兜底(I-2) | 同上(`net_recv_locked()` 亦先 `net_poll()`) | 同上 |
| `net_send()` LCDMEM 应答 **3204 B** | 客户端每请求一帧(peer ≈10 次/s) | `net_send_locked` 按 `NET_SEND_CHUNK_MAX = min(CH395F_SEND_BUF_SIZE_BYTES=2×512, NET_DMA_MAX_PAYLOAD=2KB) = 1024 B` 拆 **4 块**,每块写完等 `SENBUF_FREE`,`NET_SEND_TIMEOUT_MS=1000` | 最坏 **≈4 s**,调用方全程等 |
**结论**:poll 的耗时 = **"netTask 什么时候有空"**,不是"UI 算得慢"。netTask 是全部 socket
操作的**唯一执行者**,这个等待天然不可预测——E-02 实测一次空闲 FTP 登录把远端显示冻结
**17.7 s**(`docs/问题记录/错误码传播审计.md`);帧周期真机 **61.6 ms** vs 模拟器 **6.8 ms**
(`docs/测试说明/链路延迟测试说明.md` L8)同源。**任何"把心跳挪个位置"的改法都改变不了这一点。**
### 9.2 "彻底解决"的三条判据
| # | 判据 | 现状 |
|---|---|---|
| **J1** | uiTask 主循环里 **0 次** net 往返 → 刷新周期只由 LVGL + 调度决定 | 🟡 **无客户端时已 0 次**(A 门控,`uis` = 1 µs);有客户端时仍有——刻意保留(§9.4) |
| **J2** | 心跳自产,不依赖任何其他任务 | ✅ P1-4a |
| **J3** | 推流的阻塞只影响推流自己(故障域隔离)+ 有观测 | ⚠ **刻意接受**:仍与刷新同域(B 已否决);观测 = `[PROF]` 的 `uis`/`uisn`(§9.4) |
### 9.3 方案 A:门控——让"没事的时候"poll 真的不做事
**做法**:发 RPC 之前先用**控制块 RAM 状态**判断"有没有活要干",没有就直接返回。
| 需要的信息 | 现成 RAM 判据(`net_get_sock()` 是公开 API、纯 RAM 读) | 语义依据 |
|---|---|---|
| 有连接可接受 | `p->state == NET_SOCK_STATE_ESTABLISHED` | 与 `net_accept_locked()` 同判据(单连接模型:监听 Socket 自身在 CONNECT 后转数据通道) |
| 有数据可读 | `p->readable != 0` | `net_poll` 在 RECV_OK 置位、`net_recv_locked` 读空清零(`net_select.c` 的 `check_socket_readable` 用的就是它) |
| 对端已关闭 | `p->state == NET_SOCK_STATE_CLOSED` | 需发一次 recv 拿 EOF(0) 并触发 `auto_relisten` |
**落点(关键,别踩坑)**:判据**不能写进共享的 `ui_net_sink.c`**,必须放**平台后端**——
模拟器 `simulator/sim/net_socket_sim.c` 的监听块在 `accept()` 成功**之前**不会是
`ESTABLISHED`,直接门控会让**模拟器永远连不上**。定稿取 **net 层公开 API**(不是 UI backend):
```c
/* Drivers/BSP/NET/net_socket.h(新增,双端各一份实现) */
int net_can_accept(int sockfd); /* 1 = 有连接可接受;0 = 无(纯 RAM 读,无 RPC) */
int net_can_read(int sockfd); /* 1 = 有数据可读或已 EOF;0 = 无 */
```
选 net 层而非 UI backend 的三条理由:① 语义属于 socket,不属于 UI;② 模拟器后端本就是
`net_*` 的镜像实现,加在那里比在 UI backend 里再开一条"绕过 net API"的旁路更收敛;
③ timeSync / FTP 后续同样能用(N-4 pending 二期落地前先给它们减负)。
共享的 `ui_net_sink.c` **只调这两个 API,不直接读控制块**。
**竞态论证**:单字读、netTask 单写者;最坏两种结果都可接受——
① **晚一轮发现**(≤1 ms:netTask 自己每 1 ms 就 `net_poll()` 一次,`readable`/`state` 早已刷新);
② **扑空**(状态刚变,RPC 返回 `WOULDBLOCK`,现有分支已处理)。
**收益(实测见 §9.3.2)**:台架常态(无客户端)poll 从"一次往返(最坏 50 ms)"降到
**两次 RAM 读(实测 1 µs)**;顺带销项 **N-10**(100 msg/s 队列压力,与 FTP 抢深度 8 的队列)。
⚠ **A 不改变"有客户端时"的行为**——该暴露刻意保留,见 §9.4。
#### 9.3.1 落地补充(2026-10-10 实现时新增的两条,设计阶段未预见)
| # | 落地时发现的坑 | 处置 |
|---|---|---|
| **I-2 兜底收帧** | `readable` **只由 RECV_OK 中断置位**(`handle_recv_event`),而 Trap 22 的 `net_reconcile_tick()` 三类对账**不含**"芯片 RX 非空但 `readable=0`" → 该中断一旦丢失,纯门控会把已到达的数据**永久藏住**(改前靠每轮 `net_recv_locked()` 里的 `ch395f_get_recv_len()` 顺带发现) | `ui_net_sink.c` 加 `RM_LIVENESS_DIV=32`:门控判"无数据"时仍**每 32 轮无条件 recv 一次**(10 ms 节拍 → 最坏额外延迟 ≈320 ms;RPC 频率仍从 100/s 降到 ≈3/s)。**教训:门控类优化必须保留低频兜底,否则是拿"自愈能力"换性能** |
| **I-3 监听失效兜底** | 加了 accept 门控后,若监听控制块消失(网络重建 / 异常释放),`net_can_accept()` 恒返 0 → 改前"每轮 accept 撞 BADF"这条**暴露路径随之消失** → 端口永久不再接受新客户端 | 门控返回 0 时补一次 `net_get_sock(g_listen) == NULL` 判定,命中即 `g_listen = -1` 交回既有重建分支 |
**双端语义**:模拟器 `net_socket_sim.c` 的两个同名实现**恒返回 1**——Winsock 本就非阻塞、
没有 RPC 成本,恒 1 使门控在模拟器上**逐位等价于改前行为**(含"每轮都 recv"的自愈特性),
`simulator/tests/check_remo_sink.py` 的协议回归因此仍有效。
#### 9.3.2 A 落地实测(2026-10-10,`@test` PASS,33 个 `[PROF]` 样本 / 开机 29 s / 台架无客户端)
| 字段 | 改前(§8「关键测量结论」) | **改后实测** | 判读 |
|---|---|---|---|
| `uis` 推流往返最坏 | **≈50 ms** | **1 µs**(33 样本 max,`uisn` 全程 0) | 门控生效,**降 5 个数量级**;§9.6 目标 ≤50 µs 达成 |
| `uiw` UI 单轮工作最坏 | ≈50 ms(且 `uiw≈uis` → 单轮几乎全是推流) | **11.4 ms** | 剩下的**只含 LVGL**——网络已不在刷新路径上 |
| `uil` 周期最近值 | 10 ms | 9999 µs | 标称节拍未变 |
| `uip` 周期最坏 | ≈61 ms | **60.9 ms** | ⚠ **未达标**(§9.6 原写 ≤15 ms),但**已与 `uis` 脱钩** → 见下 |
| `gl` 丢样 | 0 | 0 | DWT 数据可信 |
⚠ **`uip` 未达标不是 A 失败,是判据当初归因错了**:`uis` 已到 1 µs 而 `uip` 仍 60.9 ms,
说明 uiTask 循环周期的最坏值由 **LVGL 整帧渲染**决定(`docs/测试说明/链路延迟测试说明.md` L8
实测帧周期真机 **61.6 ms**,与实测 60.877 ms 吻合),推流从来只是**叠加**在这条地板上的
**额外**抖动。**这条地板不是网络问题**——正是这条实测结论让 §9.4 的方案 B 被否决。
**若目标是把刷新周期本身压下来,那是另一个议题(LVGL 帧周期 / 渲染模式),不在本案范围。**
### 9.4 方案 B(把推流搬出 uiTask,独立 `uiSinkTask`)——**已评估,决定不做(2026-10-10)**
**当初的动机**:客户端一连上,几乎每轮都要 `net_send` 3204 B,这个等待由芯片 FIFO / 链路 /
netTask 决定,UI 侧无法设上界(半途截断 = 客户端 CRC 失败 = 黑屏,E-04 已定死不能碰)。
按"uiTask 里一次 net 调用都不留"的思路完整设计过:新任务(256 words / BelowNormal /
不进 `required_mask`)+ 帧 seqlock 取帧 + 栈与堆两条预算路线 + 观测面迁移。
**否决理由(全部来自 §9.3.2 的实测)**:
1. **治不到病**:A 之后 `uis` = 1 µs,而 `uip` 最坏仍是 60.9 ms = **LVGL 整帧渲染地板**
(文档实测帧周期 61.6 ms)。搬走推流**不会让刷新变快**,只是把同一段等待换个任务背。
2. **收益只剩极端并发下的"故障域隔离"**:即"客户端在连 + netTask 被 FTP 挤住"这一格
(E-02 的 17.7 s 是**改前**、FTP 空闲登录场景)。那是**远端显示的观感**问题,
设备本体的复位风险已由 P1-4a 解除。
3. **成本不成比例**:新增任务 + 帧 seqlock + `ui_port`/backend 双端改动 + **必改 `.ioc`**
(栈或堆两条路线都要 CubeMX 重生成 + 存活审计)+ 十余处改动与全套回归;
其中"不动 uiTask 栈"那条路线还会把堆 `min_ever` 压到 S11 告警线(3072 B)以下。
**不做 B 的残留暴露(如实记录)**:客户端在连且 netTask 拥塞时,`ui_net_sink_poll()`
仍可能在 uiTask 里等最长 ≈4 s(LCDMEM 应答 4 块 × `NET_SEND_TIMEOUT_MS`),
表现为**本地画面与远端同时卡住**。观测面已常驻,无需新增埋点:`[PROF]` 的 `uis`
(推流往返最坏)与 `uisn`(此刻仍在推流段内多久)——**`uisn` 持续非 0 就是这一格真发生的铁证**。
若日后它成为实际问题,**正确方向也不是 B**,而是先治 netTask 的排队公平性
(登记簿 N-4 pending 二期),那才是同一条根因的上游。
### 9.5 明确不做的四条(及理由)
| 备选 | 为什么不做 |
|---|---|
| 只调心跳顺序(=现状 P1-4a) | 心跳不再被拖,**刷新仍然被拖**——不治本次的病 |
| 给 `net_*` 加"有界等待"版 | 回执通道是"每任务一个 32 位通知值",超时后**迟到的回执会被下一次调用的零等待拾取当成新结果**(`net_wait_result` 注释里 L7 挂死 / 整段错位的同款根因)。要做对必须引入请求序号,代价高于搬家 |
| 把推流塞进 netTask | 零往返、零新增 RAM,最"彻底";但共享源码 `ui_net_sink.c` 要改成走 backend 的 socket 端口(双端同改)、netTask 这个唯一执行者更胖、且一个 UI 侧 bug 直接变成整机复位。**同样不做**;若 netTask 排队公平性真成问题,从 N-4 二期治上游,而不是把 UI 搬过去 |
| 提高 uiTask 优先级 | §8 已明确警告它会**掩盖** §4.2/§4.4 两个真实缺陷;而且它根本不解决"UI 在等 netTask" |
### 9.6 落地与验收判据
```
A(门控 + 兜底)→ @build + @test + e2e → 看 [PROF]:uis 掉到 µs 级 → 本案结案
(B 已评估否决,见 §9.4)
```
| 判据 | 目标值 | 状态(2026-10-10) |
|---|---|---|
| `uis`(无客户端) | ≤ **50 µs**(门控生效的直接证据;改前 ≈50 ms) | ✅ **A 已达成:实测 1 µs**(§9.3.2) |
| `uiw` 与 `uis` **不再同值** | 改前 `uiw≈uis≈50 ms` | ✅ **A 已达成:11.4 ms vs 1 µs** |
| `uip`(uiTask 循环周期最坏) | ~~≤15 ms~~ → **修订**:`uip` 地板 = LVGL 帧周期 ≈61 ms,**与网络无关,本案不治它**;只要求它**不随网络活动抬升**(`uis` 到 µs 级即为达成) | ⚠ 判据已按实测修订(§9.3.2) |
| 回归 | `@test` 连续 **10 次** 0 复现;`@build` 0 错误 0 警告;模拟器 `ui-verify` 五步 + `simulator/tests/check_remo_sink.py` e2e **0/25600 位差异** | ✅ **A 已达成(2026-10-10)**:`@build` 0 错 0 警;**`@test` 10/10 PASS**(每轮 `uis` ≤ **1 µs**、`uisn` ≡ 0、`task stall` 0 次、`[ERR]` 0 条、`uiw` ≤ 11.36 ms、`uip` ≤ 61.35 ms);e2e `check_remo_sink.py` **0/25600 位差异**;`check_menu_rows.py` PASS;内容页文本断言 50/50(像素比对需本机 `simulator/tests/ref/`,该目录不入库)。⚠ 10 连跑中 `net init: OK` 的 `[+Nms]` 由 ≈589 ms 变到 ≈9.1 s,**是采集零点亮盘/校验/复位耗时推移,非固件变慢**:同一日志内 `reset cause` 在 +8517 ms、`net init: OK` 在 +9141 ms → **固件自身仍 ≈624 ms**;17:31 的旧镜像日志(本次改动之前)已有同形态 |
### 9.7 风险与回退
| 风险 | 缓解 |
|---|---|
| 门控误判(芯片丢中断 → `state`/`readable` 长期不同步) | netTask 已有 `net_reconcile_tick()` 200 ms 对账 + 去抖自愈(Trap 22);门控只是"少发 RPC",**不新增自愈责任**;`readable` 侧另有 **I-2** 每 32 轮无条件 recv 兜底 |
| 监听控制块消失 → 门控恒返 0,端口不再接受新客户端 | **I-3**:门控返 0 时查 `net_get_sock(g_listen)`,消失即 `g_listen = -1` 交回既有重建分支 |
| 回退 | 门控是纯函数级改动,只决定"要不要发 RPC",**单独可回退**(去掉两处 `net_can_*()` 判断即恢复改前行为),无数据结构 / 协议 / 任务变更 |
---
## 变更记录
| 日期 | 变更 |
|---|---|
| 2026-10-10 | 创建。基于唯一留存失败日志(`serial.log` @17:36)+ 代码走查完成归因:否掉"CPU 饱和"(实测占空比 10%~20%,`ring` 速率反证无空转),定位为**优先级饥饿(uiTask 唯一 BelowNormal)+ UI 心跳依赖 netTask 往返**;另发现"无新帧仍压环"的独立正确性缺陷;给出 P0/P1/P2 分层处置与"先测量后修"的推荐顺序 |
| 2026-10-10 | 新增 **§9 彻底解耦方案(待落地)**:量化 `ui_net_sink_poll()` 的 3 个**无上界**等待点(队列 `osWaitForever` / `xTaskNotifyWait(portMAX_DELAY)` / LCDMEM 应答 3204 B 按 1024 B 拆 4 块等 `SENBUF_FREE`,最坏 ≈4 s),指出"poll 耗时 = netTask 什么时候有空";给出 **A 门控**(`net_can_accept()`/`net_can_read()` 纯 RAM 判据,落点必须在平台后端——模拟器监听块 accept 前不是 `ESTABLISHED`)+ 一套"把推流搬出 uiTask"的方案(**同日评估后否决并删除,见下面两轮**);记录四条**明确不做**的备选与理由、验收判据、风险与回退。**代码未改动**,登记 **N-19 / C7** |
| 2026-10-10 | **§9.3 方案 A 已落地**:`net_socket.c/.h` 新增 `net_can_accept()` / `net_can_read()`(纯 RAM 读控制块、零 RPC),`net_socket_sim.c` 同名实现恒返 1(模拟器行为逐位不变),`ui_net_sink_poll()` 改"先门控再 RPC",并补 **I-2 兜底**(每 32 轮无条件 recv,防 RECV_OK 中断丢失把数据永久藏住)与 **I-3 监听失效兜底**(控制块消失 → `g_listen=-1` 重建);`ui_net_sink.h/.c` 注释订正(原"非阻塞轮询 / 不阻塞 LVGL 刷新"表述不准确 → 改为"门控只消除无谓往返,真有活时往返仍无上界")。验证:Keil **0 错 0 警**(Code 256276→256368,+92 B)、模拟器构建 OK、`check_menu_rows.py` PASS、`check_coding_standard.py` PASS、**`@test` PASS**。**实测(§9.3.2):`uis` 50 ms → 1 µs、`uiw` 50 ms → 11.4 ms(只剩 LVGL)、`gl=0`**;`uip` 仍 60.9 ms = LVGL 帧周期地板,§9.6 判据已据此修订 |
| 2026-10-10 | **A 收口 + 方案 B 否决并删除**(用户定案"B 不做")。① **A 收口**:`@test` **10/10 PASS**(每轮 `uis` ≤1 µs、`uisn` ≡ 0、0 `task stall`、0 `[ERR]`)、e2e `check_remo_sink.py` **0/25600 位差异**、内容页文本断言 50/50。② **B 删除**:§9.4 由完整设计(新任务 256 words / BelowNormal / 不进 `required_mask`、帧 seqlock、栈与堆两条预算路线、观测面迁移、12 处改动清单)改写为**否决记录**——实测 `uis`=1 µs 而 `uip` 仍 60.9 ms(LVGL 帧周期地板)证明"搬出 uiTask"**不会让刷新变快**,收益只剩极端并发下的观感隔离,而成本含**必改 `.ioc`** 的 CubeMX 重生成;残留暴露与观测手段(`uis`/`uisn`)如实保留。③ **同步清理**:§9.5/§9.6/§9.7 去掉 B 条目;**N-19 / C7 不入登记簿**(当日结案,`docs/待处理内容.md` 保持无该编号);`子系统设计/看门狗设计说明.md` §9.2 的 N-19 行移除;`RemoDispBus协议与接收设计说明.md` 去掉"规划中改独立任务"段;`ui_net_sink.c/.h` 注释去掉"应搬出 uiTask"的指向 |
| 2026-10-10 | **按项目现状全文校订(无代码改动)**:① 已被处置改掉的代码片段就地标注 **改前 / 现状**——§1 第 2 条、§4.2(`ui_task.c` 心跳顺序,P1-4a 已改)、§4.3(`osDelayUntil` 丢弃返回值,P0-2 已改)、§5(无新帧压环,P0-1 已改)、§6;§4.4 明确"忙等原样保留、P1-5 暂缓"。② §8 由"待确认后执行的 P0/P1/P2 分层建议"改写为**决策 + 结果表**(P1-3 刻意不做 / P1-4b 以 A 门控形式落地 / P1-5 暂缓 / P2-6 被 `task_prof` 取代 / P2-8 以边沿快照形式落地),并新增**当前健康剖面**表。③ §7 更新:0 复现累计 22 → **32 次**、`adc1_err` 288 → 1、`idle` 649/20001 → **1/20000**;新增 §7.7 说明 `netp=0` 是真实值(`net_poll()` 空闲只读一次 INT# GPIO)。④ §9.1/§9.2 的"改前"频率与 J1/J3 状态同步为 A 落地后的现状;删掉失效的行号引用与旧体积数字(256276 → **256368**) |
| 2026-10-10 | **P1-5 已实施(方案 a:事件驱动消费)**:`Src/main.c` TIM6 分支在 `tpafe5160_start_conv_irq()` 后调 `adc_tick_irq_notify()`;`App/task/adc_task.c` 循环尾 `osDelayUntil` → `osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever)` 并删 `ADC_TASK_BEAT_MS`/`next`;`Src/freertos.c` 的 `adcTask` 优先级 `Normal` → **`AboveNormal`**;`App/task/adc_task.h` 新增 `ADC_WAKE_FLAG` 与 ISR 入口。实施时补一条规划未预见的**就绪门** `s_consumer_ready`——TIM6 在 `app_main_init()` 就启动,比建任务与 `osKernelStart()` 早 ≈100 ms,ISR 不能对尚不存在的消费者动手(同本仓 TIM7 分支 2026-10-02 的实测坑)。验证:`@build` 0 错 0 警(Code 256368→**256728**)、**`@test` 6/6 PASS**、**`idle` 由 1~649 降到 0**、`frames` 严格 1000.0 Hz、`seq−frames = 7` 正是就绪窗口、UI 侧 `uip/uiw/uis` 一字不差、0 `task stall`。本文同步:§4.1 补优先级变化与可接受性论证、§4.3 补"再现状"(P0-2 那段已删)、§4.4 改为"忙等未改但敏感度上升"、§8 的 P1-5 行转 ✅ 并拆出仍未办的"单拍硬上界"、健康剖面按 6 连跑复测值更新 |
| 2026-10-10 | **v2.8 消掉 §4.4 的 2 ms 忙等(P1-5 拆出的"单拍硬上界"已办)**:节拍源 **TIM6 → TIM3**(更正一处硬事实:F4 上 ADC1 的外部触发清单里没有 TIM6/TIM7 TRGO,只接 DAC),TIM3 更新事件同时 ① ISR 脉冲 TPAFE CONVST、② 经 **TRGO** 硬件触发 ADC1 一轮 9 通道、③ 同一事件置位 `ADC_WAKE_FLAG`;ADC1 挂 **DMA2_Stream4** 循环模式 + `DMAContinuousRequests=ENABLE`,长度 2×9 **乒乓双缓冲**,半区由完成计数奇偶推出。`App/algo/adc_input.c` 的 `stm32_scan()`(逐通道 `PollForConversion`)→ `stm32_take()`(零忙等);故障观测拆成 `adc1_seq`/`adc1_gap`/`adc1_err`/`adc1_hw` 四类(单拍无新帧 = 消费者相位伪影,不计故障)。实测:`adcp` **122 µs → 8 µs**、`adc1_err=0`、`adc1_hw=0`、`adc1_gap=27`/20002 拍(0.13%,集中在开机日志突发期)、`adc1_seq` 与 `seq` 恒定偏移 2;主机单测 79 → **101**;`@build` 0 错 0 警 Code 256872→**257660**;`@test` PASS;adcTask 静态最坏链 **832 B / 余 59%** 不变。CubeMX 侧三轮重生成的三处坑(ADC1 抢 Stream0 连锁搬走 SPI1_RX/USART1_RX、换流把 NVIC 优先级重置为 0、TIM6 残留)见 §11.2 |
| 2026-10-10 | **§4.4 复核修正(无代码改动)**:① 原文"9 通道全超时最坏 = **18 ms** 连占"**有误**——`stm32_scan()` 在首个超时处即 `HAL_ADC_Stop()`+`return`(`adc_input.c:79-84`),单拍上界 = **一次**超时 ≈2~3 ms;② 6 次上板对照证明 **2 ms 只在超时拍出现**:超时 1 次那轮 `adcp = 2209 µs`,超时 0 次的 5 轮 `adcp = 122 µs`;③ `adcm ≈ 2.1 ms` 的归因由"ADC 采集"**改判**为每 10 s 一次的心跳日志(`idle = 0`/20000 反证不存在"每拍 2 ms");④ 新增超时的**正确性**代价——超时拍 `s_stm32_valid` 仍为 1 → STM32 侧静默沿用上一拍值、`skip` 抓不到 → 硬化第一步应是"超时即置 `s_stm32_valid = 0`"。同步修正 `App/task/adc_task.c` 两处过期注释与 `ADC采样节拍设计说明.md` §10.1/§10.3/§10.4 |
| 2026-10-10 | **补 F4 ADC 上电自校准(§7.2 候选根因之一)**:核查发现本项目**从未**做过 ADC 自校准——CubeMX 不生成该步骤,且仓库裁剪版 HAL 无 `HAL_ADCEx_Calibration_Start`、裁剪版 CMSIS 头无 `ADC_CR2_CAL`。已在 `Src/adc.c` 的 `USER CODE` 区实现 `adc1_calibrate()`(清 ADON→等生效→置 ADON→置 CAL→等 CAL 由硬件清零)并在 `MX_ADC1_Init()` 末尾调用;新增 `cubemx-survival` 审计项 `adc1-calibration-call` / `adc1-calibration-impl`。验证:`@build` 0 错 0 警(Code 256728→**256868**)、`@test` **PASS**(`adcp` 仍 122 µs、`adc1_err=0`、`idle=0`);**校准前后同一拍 STM32 侧 6 路同向偏移** `UB 898→961`/`UC 978→1038`/`IB 906→922`/`IC 1061→1071`/`IN 1017→1055`/`T 790→846`(+10~+63 LSB),TPAFE 侧 `UA/UN/IA` 一字不变 → 变化确来自 ADC1 校准。§4.4 的 2 ms 忙等与"超时沿用旧值"**仍未处理** |
| 2026-10-10 | **修 §4.4 的"超时拍静默沿用旧值"(正确性缺陷)**:`stm32_scan()` 两条失败支路(`HAL_ADC_Start` 失败 / 轮询超时)现都清 `s_stm32_valid` → 失败拍 `adc_input_raw()` 返 -2 → `adc_task` 走既有 `skip` 整帧跳过,不再把上一拍的值当新数据压入 20 点 DFT 窗;下一拍成功自动恢复。主机侧新增 **TC-AI-070..078**(70 → 79 用例)钉死——旧用例 TC-AI-034/039b 都是"init 后立刻失败"(`valid` 本就是 0),照不出"成功过之后再失败"这条路径。**反向验证已做**:把支路改回旧行为 → TC-AI-039b/073/074 立即 FAIL(76/79)。验证:`@build` 0 错 0 警(Code 256868→**256872**)、主机单测 **79/79**、`@test` **PASS**(本轮无超时:`adc1_err=0`、`skip=0`、`idle=0`)。**剩余未做 = 消掉 2 ms 忙等本身(DMA)** |
@@ -320,7 +320,7 @@ D-2/D-3 的共同根因:**没有任何机械检查**保证"文档说的 = 代
|------|------|------|------|
| **N-02** | **FTP 无鉴权**:`cmd_pass` 无条件 `send_simple_response(socket, 230, STATUS_230)` | `lftpd.c:367-371`(`cmd_user :539` 亦不校验) | 最小可用方案:`config.ini` 增 `[ftp] user/pwd_hash`,`cmd_pass` 用**定长比较 + 失败计数锁定**;生产固件默认关闭 FTP 写权限(`FF_FS_READONLY` 式的只读模式) |
| N-09 | `cmd_pasv` 每次新建监听 `lftpd_inet_listen(0)`(随机端口) | `lftpd.c:373` | 在**单连接 socket** 上与 UI `:7003`、对时 `:8888` 争用同一物理 socket;建议限定监听资源池 + 显式"FTP 会话独占"策略,并在文档写明"一次只允许一个 TCP 服务活动" |
| N-10 | uiTask 以 **10 ms** 周期向 netMsgQueue 投递 accept 轮询 → ≈100 msg/s,与 FTP 共享深度 8 的队列 | `ui_net_sink.c` 轮询逻辑 | 队列满时 `osErrorResource` → UI 丢帧/重投。建议:把"服务就绪"改为事件驱动(netTask 在 LISTEN 状态变化时置位),UI 侧改为等待事件;或给队列扩容并在 UI 侧背压。**→ 已销项(2026-10-10)**:未走"事件驱动",改用**门控**——`net_can_accept()`/`net_can_read()` 纯 RAM 读控制块,无活时**一次 RPC 都不发**(100 msg/s → 0);实测 `uis` 50 ms → **1 µs**。方案与实测见 `UI任务看门狗停摆分析.md` §9.3 / §9.3.2 |
| N-10 | uiTask 以 **10 ms** 周期向 netMsgQueue 投递 accept 轮询 → ≈100 msg/s,与 FTP 共享深度 8 的队列 | `ui_net_sink.c` 轮询逻辑 | 队列满时 `osErrorResource` → UI 丢帧/重投。建议:把"服务就绪"改为事件驱动(netTask 在 LISTEN 状态变化时置位),UI 侧改为等待事件;或给队列扩容并在 UI 侧背压。**→ 已销项(2026-10-10)**:未走"事件驱动",改用**门控**——`net_can_accept()`/`net_can_read()` 纯 RAM 读控制块,无活时**一次 RPC 都不发**(100 msg/s → 0);实测 `uis` 50 ms → **1 µs**。方案见 `../UI设计/RemoDispBus协议与接收设计说明.md` §3;判读口径与实测基线见 `../子系统设计/看门狗设计说明.md` §11.4 |
| P2-11 | `net_task.c` 对 `osEventFlagsGet` + Clear 非原子 | 台账 | 用 `osEventFlagsAcquire`/单次 Wait 消 TOCTOU |
| P2-8 | `net_recv` 阻塞超时 30 s | 台账 | 缩短并让上层可取消 |
| — | PHY 强制 100M 全双工,忽略自协商 | AGENTS/architecture | 与某些交换机会联不通;至少加"链路 up 失败 3 次后回退自协商" |
@@ -679,7 +679,7 @@ const ui_page_desc_t ui_page_deadzone = UI_FORM_PAGE("遥测死区", s_items_dea
| F-1/F-2/F-3 | `lv_conf.h` 与 LVGL 9.3 对齐 + 主题/控件/flex 裁剪 | Flash −55 KB |
| N-12/N-13 | `dhara_nand_is_free` 全页校验;`dhara_nand_copy` 处理 `DHARA_NO_PAGE` | 掉电注入测试通过 |
| N-16 | 日志常驻 `FIL` + 批量 flush;UI 记录读缓存升级 | 单条日志 NAND 操作次数下降 ≥50% |
| N-10 | ~~UI accept 改事件驱动(脱离 100 msg/s 轮询)~~ **已落地(2026-10-10,改走"门控"而非事件驱动)** | ✅ 无客户端时队列占用 **100 msg/s → 0**;实测 `uis` 50 ms → **1 µs**(`UI任务看门狗停摆分析.md` §9.3 / §9.3.2) |
| N-10 | ~~UI accept 改事件驱动(脱离 100 msg/s 轮询)~~ **已落地(2026-10-10,改走"门控"而非事件驱动)** | ✅ 无客户端时队列占用 **100 msg/s → 0**;实测 `uis` 50 ms → **1 µs**(`../UI设计/RemoDispBus协议与接收设计说明.md` §3;判读见 `../子系统设计/看门狗设计说明.md` §11.4) |
| D-1 | 芯片手册图片降采样 / 拆 submodule | clone 40 MB → ~8 MB |
| D-3 | `check_doc_links.py` 入门禁 + 修 18 条断链 | 零断链 |
| U1–U4 | UI 令牌化 + 原型库 + 生成器 + golden | 新增一页 ≤30 行 |