diff --git a/App/task/ui_task.c b/App/task/ui_task.c index 6079be99..6e6d1d60 100644 --- a/App/task/ui_task.c +++ b/App/task/ui_task.c @@ -9,8 +9,8 @@ * v1.2 2026-10-10 王建锋 Task #32 的 P2 测量埋点:主循环加常开探针 * (循环周期 / 单轮工作耗时 / ui_net_sink_poll 往返), * 用于把"心跳缺席"拆成 饿死 / 阻塞在往返 / 自身拖长 - * 三种互斥机理(见 docs/问题记录/UI任务看门狗停摆分析.md) - * v1.3 2026-10-10 王建锋 Task #32 的 P1-4a(解耦心跳,§4.2):把 task_wd_beat + * 三种互斥机理(判读见 docs/子系统设计/看门狗设计说明.md §11.4) + * v1.3 2026-10-10 王建锋 Task #32 的 P1-4a(解耦心跳,看门狗设计说明 §11.3):把 task_wd_beat * (TASK_WD_UI) 提到 ui_net_sink_poll() **之前**。原写法心跳 * 在推流往返之后 → 心跳周期 = 渲染 + 一次 netTask 往返 + 10ms, * 实测 uiw≈uis≈50ms(心跳被 netTask 往返主导),心跳实际测的是 @@ -150,7 +150,7 @@ void ui_task_func(void *argument) { tm_loop_valid = 1U; #endif ui_port_step(10); - /* 心跳**先于**推流往返发出(P1-4a,Task #32 §4.2):UI 心跳测的是 + /* 心跳**先于**推流往返发出(P1-4a,看门狗设计说明 §11.3):UI 心跳测的是 * "UI 自身被调度到了",而非"netTask 回执快慢"。原写法心跳在 sink 之后, * 致实测 uiw≈uis≈50ms(心跳被 netTask 往返主导)。提到往返之前后, * 即使往返偶发变慢,心跳也不被其拖垮。 diff --git a/App/util/task_prof.c b/App/util/task_prof.c index 9cbded57..ef91e6cb 100644 --- a/App/util/task_prof.c +++ b/App/util/task_prof.c @@ -9,7 +9,7 @@ * 修改记录: * v1.0 2026-10-10 王建锋 创建(Task #32 的 P2 测量埋点) * - * 设计依据:docs/问题记录/UI任务看门狗停摆分析.md §8(P2 测量项) + * 设计依据:docs/子系统设计/看门狗设计说明.md §11.4(探针字段语义与实测基线) */ #include "task_prof.h" diff --git a/App/util/task_prof.h b/App/util/task_prof.h index bfbac61d..3104cfde 100644 --- a/App/util/task_prof.h +++ b/App/util/task_prof.h @@ -11,7 +11,7 @@ * 修改记录: * v1.0 2026-10-10 王建锋 创建(Task #32 的 P2 测量埋点) * - * 设计背景(docs/问题记录/UI任务看门狗停摆分析.md §8 P2): + * 设计背景(docs/子系统设计/看门狗设计说明.md §11.4): * uiTask 偶发心跳缺席导致 IWDG 复位,但"占用者"无法从既有日志判定。 * 同一现象至少对应三种互斥机理,观测上必须能区分: * ① 被调度饿死 → 循环周期长,而段内耗时短; @@ -36,7 +36,7 @@ * NET_POLL netTask net_poll()(enter/exit)——CH395F 驱动轮询 * * 后四个探针用于**指认占用者**:UI 心跳缺席时,谁的最坏耗时在同一窗口抬起来, - * 谁就是挤占 CPU 的 Normal 任务(见 docs/问题记录/UI任务看门狗停摆分析.md §7.1)。 + * 谁就是挤占 CPU 的 Normal 任务(见 docs/子系统设计/看门狗设计说明.md §11.2 第 4 步)。 */ #ifndef TASK_PROF_H diff --git a/Drivers/BSP/UI/ui_net_sink.c b/Drivers/BSP/UI/ui_net_sink.c index cc3aa4ef..74d22bcb 100644 --- a/Drivers/BSP/UI/ui_net_sink.c +++ b/Drivers/BSP/UI/ui_net_sink.c @@ -478,7 +478,7 @@ int ui_net_sink_start(uint16_t port) { * (accept / recv / 3204 B 的 LCDMEM 应答),仍是"入队 + 等 netTask 回执", * 最坏 ≈4 s。宿主 = uiTask 主循环;"搬出 uiTask"已完整评估并**决定不做** * (实测刷新最坏周期 = LVGL 帧周期地板,与网络无关),理由与残留暴露 - * 见 docs/问题记录/UI任务看门狗停摆分析.md §9.4 + * 见 docs/UI设计/RemoDispBus协议与接收设计说明.md §3 */ void ui_net_sink_poll(void) { int n; diff --git a/Drivers/BSP/UI/ui_net_sink.h b/Drivers/BSP/UI/ui_net_sink.h index a225ec2e..d5447b37 100644 --- a/Drivers/BSP/UI/ui_net_sink.h +++ b/Drivers/BSP/UI/ui_net_sink.h @@ -53,7 +53,7 @@ int ui_net_sink_start(uint16_t port); * xTaskNotifyWait(portMAX_DELAY),均无上界;LCDMEM 应答 3204 B 最坏 ≈4 s) * → 会拖住宿主(uiTask)的刷新;"搬出 uiTask"已完整评估并**决定不做** * (实测刷新最坏周期 = LVGL 帧周期地板,与网络无关),见 - * docs/问题记录/UI任务看门狗停摆分析.md §9.4 + * docs/UI设计/RemoDispBus协议与接收设计说明.md §3 */ void ui_net_sink_poll(void); diff --git a/docs/README.md b/docs/README.md index 00b85e2f..5011e020 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 处置建议 | --- diff --git a/docs/UI设计/RemoDispBus协议与接收设计说明.md b/docs/UI设计/RemoDispBus协议与接收设计说明.md index 7901c412..cfab465a 100644 --- a/docs/UI设计/RemoDispBus协议与接收设计说明.md +++ b/docs/UI设计/RemoDispBus协议与接收设计说明.md @@ -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。 --- diff --git a/docs/子系统设计/ADC采样节拍设计说明.md b/docs/子系统设计/ADC采样节拍设计说明.md index 9d22f9f3..56031d2f 100644 --- a/docs/子系统设计/ADC采样节拍设计说明.md +++ b/docs/子系统设计/ADC采样节拍设计说明.md @@ -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 对还不存在的消费者动手"。 diff --git a/docs/子系统设计/看门狗设计说明.md b/docs/子系统设计/看门狗设计说明.md index 9ab0721a..85738eb4 100644 --- a/docs/子系统设计/看门狗设计说明.md +++ b/docs/子系统设计/看门狗设计说明.md @@ -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 | diff --git a/docs/网络/BSD_Socket_API_使用指南.md b/docs/网络/BSD_Socket_API_使用指南.md index 0311fde7..8b132ba2 100644 --- a/docs/网络/BSD_Socket_API_使用指南.md +++ b/docs/网络/BSD_Socket_API_使用指南.md @@ -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 保存请求) | diff --git a/docs/问题记录/UI任务看门狗停摆分析.md b/docs/问题记录/UI任务看门狗停摆分析.md deleted file mode 100644 index 2ee62e6d..00000000 --- a/docs/问题记录/UI任务看门狗停摆分析.md +++ /dev/null @@ -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)** | diff --git a/docs/问题记录/项目审查与优化建议报告.md b/docs/问题记录/项目审查与优化建议报告.md index 085e57e9..1452644b 100644 --- a/docs/问题记录/项目审查与优化建议报告.md +++ b/docs/问题记录/项目审查与优化建议报告.md @@ -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 行 |