@@ -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) ** |