adcTask 事件驱动消费(P1-5 方案 a)+ 补 F4 ADC 上电自校准 + 修正 2 ms 忙等过期表述
P1-5 方案 a(事件驱动消费,Task #32): - TIM6 1 kHz ISR 除触发 CONVST 外再调 adc_tick_irq_notify(),置位 ADC_WAKE_FLAG;adcTask 循环尾改 osThreadFlagsWait(osWaitForever), 删除 ADC_TASK_BEAT_MS 定时轮询 - adcTask 优先级 Normal(24) -> AboveNormal(32)(Src/freertos.c USER CODE 区) - 补消费者就绪门(规划未预见):TIM6 在 app_main_init 就启动,比调度器 早约 100 ms,未就绪时 ISR 不得 osThreadFlagsSet - 实测 6 次上板:idle 由 1~649 降到 0/20000 拍,frames 1000.0 Hz, seq-frames=7 正是就绪窗口;UI 侧 uip/uiw/uis 无退化 补 STM32F4 ADC 上电自校准(此前项目从未做过): - 核查证实裁剪版 HAL 无 HAL_ADCEx_Calibration_Start、裁剪版 CMSIS 头无 ADC_CR2_CAL -> 按 RM0090 13.3.6 在 Src/adc.c 的 USER CODE 区手写 adc1_calibrate(),MX_ADC1_Init() 末尾调用 - 新增 cubemx-survival 审计项 adc1-calibration-call / -impl(清单 21->23) - 实测校准前后同一拍 STM32 侧 6 路同向偏移 +10~+63 LSB,TPAFE 侧不变 修正两处过期表述(实测反证): - 原文 9 通道全超时最坏 = 18 ms 连占 有误:stm32_scan() 在首个超时处即 Stop+return,单拍上界 = 一次超时约 2~3 ms - adcm 约 2.1 ms 的峰值归因由 ADC 采集改判为每 10 s 一次的心跳日志; 6 次对照 adcp = 122 us(无超时)/ 2209 us(超时 1 次) - 同步修正 adc_task.c 两处过期注释;新发现超时拍 STM32 侧静默沿用 上一拍值的正确性代价,硬化排序记入 4.4 / 10.4(未实施) 验证:@build 0 错 0 警(Code 256728 -> 256868);@test 6/6 PASS; stack_report 7 任务全 PASS(adcTask 832 B / 余 59%);18 钩子全绿
This commit is contained in:
@@ -36,10 +36,10 @@
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| ER_IROM1 | 272,436 B (266.05 KB) | 1,048,576 B (1024 KB) | 26.0% |
|
||||
| RW_IRAM1 | 91,328 B (89.19 KB) | 131,072 B (128 KB) | 69.7% |
|
||||
| ER_IROM1 | 273,028 B (266.63 KB) | 1,048,576 B (1024 KB) | 26.0% |
|
||||
| RW_IRAM1 | 91,336 B (89.20 KB) | 131,072 B (128 KB) | 69.7% |
|
||||
| RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% |
|
||||
| **Flash (ROM 合计)** | **273,236 B (266.8 KB)** | 1,048,576 B (1024 KB) | **26.1%** |
|
||||
| **Flash (ROM 合计)** | **273,836 B (267.4 KB)** | 1,048,576 B (1024 KB) | **26.1%** |
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
各区的容量与常量来源:
|
||||
|
||||
+92
-29
@@ -4,9 +4,11 @@
|
||||
> (`Src/main.c` 回调 + `app_main.c` 启动 + `adc_task.c` 退化为纯消费者),`@build` 0 错 0 警;
|
||||
> 台架 `@test` **PASS**(`frame_seq` 每 10 s 精确 +10000 = 1000.0 Hz、`frst_err=0`、回读一致;
|
||||
> 实测见 §7.1)。
|
||||
> **事件驱动消费改造方案 a 已规划入档(Task #32 P1-5,2026-10-10,待实施)**:用户决策
|
||||
> "ISR 只发唤醒 + 计算留任务 + 提优先级"(方案 a),拒绝"ISR 内计算";并附下一版硬件
|
||||
> DMA 双缓冲建议(§10.5)。详见 §10;消费端当前仍为 1 ms 轮询,待该方案排期落地。
|
||||
> **事件驱动消费改造方案 a 已落地(Task #32 P1-5,2026-10-10)**:用户决策
|
||||
> "ISR 只发唤醒 + 计算留任务 + 提优先级"(方案 a),拒绝"ISR 内计算"。消费端已从
|
||||
> 1 ms `osDelayUntil` 轮询改为 **TIM6 ISR 置位 `ADC_WAKE_FLAG` 唤醒**,`adcTask`
|
||||
> 优先级 `Normal` → **`AboveNormal`**。台架实测 **`idle = 0`**(20000 拍全零;改前 1~649)、
|
||||
> `frames` 严格 1000.0 Hz、UI 侧 `uip/uiw/uis` 无退化。详见 §10。
|
||||
> **上位需求**:`docs/算法/继电保护功能复现规划.md` §3.2 L1-1(周期 CONVST 重触发 1000 Hz,
|
||||
> 与保护节拍同源)。**驱动契约**:`Drivers/BSP/TPAFE5160/README.md`。
|
||||
> **背景**:B1(2026-10-10)已把 `adc_task` 由空桩改为真实消费者,但采样节拍是
|
||||
@@ -170,7 +172,8 @@ t=1000 µs 下一个 TIM6 更新事件
|
||||
跑 `python scripts/check_mem_numbers.py --write` 刷新快照。
|
||||
7. **消费端与生产端同频**:TIM6(84 MHz 派生)与 FreeRTOS tick TIM7(同源)都恰好 1 kHz,
|
||||
相位固定 → 消费端每拍恰好取到 1 帧。**一旦进入等周波调频(采样率≠1 kHz),
|
||||
1 ms 轮询会与生产端失配 → 消费端须改为"帧就绪事件驱动"**(见 §8)。
|
||||
固定周期轮询会与生产端失配 → 消费端须改为"节拍事件驱动"**(见 §8)。
|
||||
✅ **该前置已满足(2026-10-10,§10)**:消费唤醒改由 TIM6 ISR 置位,与采样率天然同步。
|
||||
|
||||
---
|
||||
|
||||
@@ -200,11 +203,15 @@ t=1000 µs 下一个 TIM6 更新事件
|
||||
| 6 | 开机时间戳正常 | ✅ 日志带墙钟时间戳,`net init: OK` 在 +530 ms |
|
||||
| 7 | 无 HardFault / `[ERR]` / 看门狗复位 | ✅ 零 `[ERR]`/HardFault;`reset cause:` 仅 1 次(调试器软复位) |
|
||||
|
||||
**关于 `idle`(不是缺陷,是轮询消费的固有抖动)**:生产端(TIM6)与消费端(`adcTask` 1 ms 轮询)
|
||||
**关于 `idle`(改前:轮询消费的固有抖动)**:生产端(TIM6)与消费端(`adcTask` 1 ms 轮询)
|
||||
同为 1000 Hz(TIM6 与 FreeRTOS tick 源 TIM7 参数一致,均 84 MHz/84/1000),但二者无显式同步 →
|
||||
偶发 ±1 帧的相位抖动:每 10 s 约 5 次"两次轮询落在同一生产间隔"(`idle` +5)配 5 次"一次轮询跨两个
|
||||
生产间隔"(`frames` 少 5,即 9995/10000 ≈ 99.95% 取数)。要彻底消除需把消费端改为**帧就绪事件驱动**
|
||||
(在 BUSY EXTI 读帧完成时通知 `adcTask`)——与 §8 等周波调频的消费端改造是同一件事,留待该阶段一并做。
|
||||
生产间隔"(`frames` 少 5,即 9995/10000 ≈ 99.95% 取数)。
|
||||
|
||||
✅ **现状(§10 已落地,2026-10-10)**:消费端改由 **TIM6 ISR 置位 `ADC_WAKE_FLAG`** 唤醒。
|
||||
落点与当初设想的"BUSY EXTI 读帧完成时通知"不同——选 TIM6 是为了让唤醒与**采样节拍**严格同源,
|
||||
且不必把内核 API 引入 BUSY EXTI(它与 TIM6 同抢占优先级,见 §5);效果一致:实测 **`idle = 0`**
|
||||
(6 次上板、每次 20000 拍全零)→ 判据 5 由"基本为 0"收紧为**恒 0**。
|
||||
|
||||
---
|
||||
|
||||
@@ -216,7 +223,8 @@ t=1000 µs 下一个 TIM6 更新事件
|
||||
- 本配置已为此预留:**1 µs 分辨率** + **ARR 预装载使能** → 运行期
|
||||
`__HAL_TIM_SET_AUTORELOAD(&htim6, 周期µs - 1)` 即可无毛刺改频。
|
||||
- 需要配套:① 测频源(过零/DFT 相位);② 把当前采样率同步给保护算法(相量/定值);
|
||||
③ **消费端改事件驱动**(§6.7)——否则 1 ms 轮询在非 1 kHz 采样率下会丢帧。
|
||||
③ **消费端改事件驱动**(§6.7)——否则固定周期轮询在非 1 kHz 采样率下会丢帧。
|
||||
✅ **已落地(2026-10-10,§10)**:消费唤醒与 TIM6 同源 → 调频时只改 ARR,消费节拍自动跟随。
|
||||
|
||||
---
|
||||
|
||||
@@ -228,22 +236,28 @@ t=1000 µs 下一个 TIM6 更新事件
|
||||
| 2026-10-10 | **实现(方案 A)** | CubeMX 加 TIM6(PSC=83 / ARR=999 / ARR 预装载 / NVIC 抢占优先级 6);`Src/main.c` 回调加 TIM6 分支触发 CONVST;`app_main.c` 用 `HAL_TIM_Base_Start_IT(&htim6)` 启动节拍;`adc_task.c` 退化为纯消费者。`@build` 0 错 0 警;台架 `@test` 验收待做 |
|
||||
| 2026-10-10 | **台架 `@test` 验收 PASS** | 真机闭环:`seq` 10005→20005→30005(每 10 s +10000 = 1000.0 Hz)、`frst_err=0`、`idle` 每 10 s +5(0.05%,轮询抖动)、回读 `No difference found`、零 `[ERR]`/HardFault、`reset cause` 仅 1 次。证据 `boot_log_data/20261010_prod_adc_tim6_boot_log.txt` |
|
||||
| 2026-10-10 | **事件驱动消费方案 a 规划入档(Task #32 P1-5)** | 记录用户决策与改造清单(TIM6 ISR 置位 `ADC_WAKE_FLAG` 唤醒 `adcTask` + `adcTask` 提至 `osPriorityAboveNormal`),并附下一版硬件 DMA 双缓冲建议(§10.5)。代码待实施,消费端仍为 1 ms 轮询 |
|
||||
| 2026-10-10 | **事件驱动消费方案 a 已实施(Task #32 P1-5)** | 四处改动落地:`Src/main.c` TIM6 分支调 `adc_tick_irq_notify()`;`adc_task.c` 循环尾改 `osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever)` 并删 `ADC_TASK_BEAT_MS`/`next`;`Src/freertos.c` 优先级 `Normal`→**`AboveNormal`**;`App/task/adc_task.h` 加 `ADC_WAKE_FLAG` 与 ISR 入口。实施时补**就绪门**(§10.6,规划未预见:TIM6 比调度器早起 ≈100 ms)。`@build` 0 错 0 警(Code 256368→256728);`@test` PASS:**`idle = 0`/19999 拍**(改前 1~649)、`frames` 1000.0 Hz、`seq−frames = 7` 正是就绪窗口、UI 侧 `uip/uiw/uis` 无退化 |
|
||||
| 2026-10-10 | **§10.1/§10.3/§10.4 复核修正(无功能改动)** | ① 2 ms 忙等的单拍上界由"9 通道 ×2 = 18 ms"更正为**一次超时 ≈2~3 ms**(`stm32_scan()` 在首个超时处即 `Stop`+`return`);② 6 次上板对照 `adcp` = **122 µs**(超时 0 次的 5 轮)/ **2209 µs**(超时 1 次的那轮)→ 2 ms 不是常态开销;③ `adcm` 2.1 ms 峰值**归因改判**为每 10 s 一次的心跳日志(`idle=0`/20000 反证无每拍 2 ms);④ 新增超时的**正确性**代价(超时拍 STM32 侧静默沿用上一拍值,`skip` 抓不到)与硬化排序 |
|
||||
| 2026-10-10 | **补 ADC1 上电自校准(§10.4)** | 发现 F4 必需的自校准在本项目**完全缺失**(CubeMX 不生成 + 裁剪 HAL 无 `HAL_ADCEx_Calibration_Start` + 裁剪 CMSIS 头无 `ADC_CR2_CAL`)→ 在 `Src/adc.c` 的 `USER CODE` 区实现 `adc1_calibrate()`(RM0090 §13.3.6 时序),`MX_ADC1_Init()` 末尾调用。新增审计项 `adc1-calibration-call`/`-impl`。`@build` 0 错 0 警(Code 256728→256868,+140 B);`@test` PASS,`adcp` 仍 122 µs、`adc1_err=0`;实测 STM32 侧 6 路同向偏移 +10~+63 LSB、TPAFE 侧不变 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 事件驱动消费改造(Task #32 P1-5,2026-10-10 规划:用户决策方案 a,待实施)
|
||||
## 10. 事件驱动消费改造(Task #32 P1-5,2026-10-10:用户决策方案 a,**已落地**)
|
||||
|
||||
> 上位背景:Task #32 暴露"UI 任务看门狗停摆"候选占用者之一是 `adcTask` 的 1 ms 轮询——
|
||||
> 原 `osDelayUntil(1ms)` 把消费唤醒**钉在 FreeRTOS tick(1 ms)边界**,带来 0~1 ms 随机
|
||||
> 相位抖动 + 累积漂移风险,且 `adcTask` 与 `uiTask` 同为可被长时抢占的对象。用户决策:
|
||||
> **方案 a(ISR 只发唤醒 + 计算仍在任务 + 提优先级)**,并拒绝"ISR 内计算"方案。
|
||||
> 本方案**尚未实施**:消费端当前仍为 1 ms 轮询,改造清单见 §10.2,待排期落地。
|
||||
> **落地状态(2026-10-10)**:四处改动全部实施(清单见 §10.2),`@build` 0 错 0 警、
|
||||
> 台架 `@test` PASS,实测见 §10.3;实施时补了一条规划未预见的**就绪门**(§10.6)。
|
||||
|
||||
### 10.1 决策:为什么选 a,不选"ISR 内计算"
|
||||
|
||||
- **拒绝"ISR 内计算"**:把相量/压环搬进 TIM6 ISR 是反模式——
|
||||
① `adc_input_poll()` 内含 `HAL_ADC_PollForConversion` 的 **2 ms 忙等**(`ADC_INPUT_POLL_TIMEOUT_MS=2U`),
|
||||
绝不可入 ISR(会冻结系统,最坏 9 通道 ×2 ms = 18 ms 全系统中断关闭);
|
||||
绝不可入 ISR(TIM6 抢占优先级 6,自旋期间低优先级中断与全部任务都被压住;单拍最坏 =
|
||||
**一次**超时 ≈2~3 ms——`stm32_scan()` 在首个超时处即 `HAL_ADC_Stop()`+`return`
|
||||
(`App/algo/adc_input.c:79-84`),不会累加成 9 次);
|
||||
② 相量/采样环是任务级共享数据,ISR 内改写需额外临界区,破坏 `tpafe5160_snapshot()` 的
|
||||
"仅任务上下文"铁律;③ `HAL_TIM_PeriodElapsedCallback` 是 CubeMX 生成区回调,承载 L2 计算
|
||||
违反"生成区不改"约定,重生成即丢。
|
||||
@@ -251,14 +265,14 @@ t=1000 µs 下一个 TIM6 更新事件
|
||||
并把 `adcTask` 优先级由 `osPriorityNormal` 提到 `osPriorityAboveNormal`,使唤醒后**立即抢占**
|
||||
其余 Normal 任务,固定 µs 级延迟。计算量、数据共享、生成区约定全部不变。
|
||||
|
||||
### 10.2 拟改造内容(待实施)
|
||||
### 10.2 改造内容(已实施)
|
||||
|
||||
| 文件 | 拟改动 |
|
||||
| 文件 | 实际改动 |
|
||||
|---|---|
|
||||
| `Src/main.c` | `HAL_TIM_PeriodElapsedCallback` 的 TIM6 分支:在 `tpafe5160_start_conv_irq()` 后加 `(void)osThreadFlagsSet(adcTaskHandle, ADC_WAKE_FLAG);` |
|
||||
| `App/task/adc_task.c` | 主循环尾 `osDelayUntil(1ms)` 改为 `(void)osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever);`;删除 `ADC_TASK_BEAT_MS` 与 `next` 变量 |
|
||||
| `Src/freertos.c` | `adcTask_attributes.priority`:`osPriorityNormal` → `osPriorityAboveNormal`(=32,高于其余 Normal=24;`configMAX_PRIORITIES=56` 有裕量) |
|
||||
| `App/task/adc_task.h` | 新增 `#define ADC_WAKE_FLAG 0x0001U` 与 `extern osThreadId_t adcTaskHandle;` |
|
||||
| `Src/main.c` | `HAL_TIM_PeriodElapsedCallback` 的 TIM6 分支(`USER CODE` 区,L2):`tpafe5160_start_conv_irq()` 后加 **`adc_tick_irq_notify();`**——**不是**直接 `osThreadFlagsSet`(理由见 §10.6);并在 `USER CODE BEGIN Includes` 加 `#include "adc_task.h"` |
|
||||
| `App/task/adc_task.c` | 主循环尾 `osDelayUntil(next)` → **`(void)osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever);`**;删除 `ADC_TASK_BEAT_MS` 与 `next`(P0-2 的重同步随之不再需要);新增 `adc_tick_irq_notify()` 与就绪门 `s_consumer_ready` |
|
||||
| `Src/freertos.c` | `adcTask_attributes.priority`:`osPriorityNormal` → **`osPriorityAboveNormal`**(=32 > 其余 Normal=24;`configMAX_PRIORITIES=56` 有裕量) |
|
||||
| `App/task/adc_task.h` | 新增 `#define ADC_WAKE_FLAG 0x0001U` 与 `void adc_tick_irq_notify(void);`。**未**把 `extern osThreadId_t adcTaskHandle;` 放进头文件——句柄只在 `adc_task.c` 内引用,`main.c` 不碰任何内核符号(与 `task_wd_tick_probe()` 同构;规划里的这一条按此收敛) |
|
||||
|
||||
- **ISR 安全性(设计校验)**:TIM6 抢占优先级 = 6 ≥ `configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY`(=5) →
|
||||
`osThreadFlagsSet` 在 ISR 内走 `xTaskNotifyFromISR` + `portYIELD_FROM_ISR`(CMSIS-RTOS V2 的
|
||||
@@ -267,23 +281,50 @@ t=1000 µs 下一个 TIM6 更新事件
|
||||
优先级调整不影响掩码。
|
||||
- **验证前置**:实施后须 `@build` 0 错 0 警 + 台架 `@test` 复核 `idle` 是否降至 ≈0(§7.1)。
|
||||
|
||||
### 10.3 预期收益与语义
|
||||
### 10.3 收益与语义(含落地实测)
|
||||
|
||||
1. **唤醒延迟固定 µs 级**:原 `osDelayUntil(1ms)` 把唤醒量化到 tick 边界 → 0~1 ms 随机相位抖动 +
|
||||
一旦超拍即"永久落后/空转"风险(即 P0-2 的成因);事件驱动走 TIM6 硬件节拍,唤醒 = 生产时刻 + ISR/调度延迟(亚 µs~数 µs),**无 tick 量化、无累积漂移**。
|
||||
2. **idle 趋近 0**:原轮询与生产的 ±1 帧相位抖动(§7.1 每 10 s +5)消失——唤醒与生产同源锁定。
|
||||
待台架复核(§7.1)。
|
||||
3. **电平型标志的健壮性**:若单拍偶超 1 ms,TIM6 多发脉冲合并为一次唤醒,`adc_input_poll()` 返回
|
||||
一旦超拍即"永久落后/空转"风险(即 P0-2 的成因);事件驱动走 TIM6 硬件节拍,唤醒 = 生产时刻 +
|
||||
ISR/调度延迟(亚 µs~数 µs),**无 tick 量化、无累积漂移**。
|
||||
2. **idle 归零(实测已达成)**:原轮询与生产的 ±1 帧相位抖动消失——唤醒与生产同源锁定。
|
||||
3. **粘滞标志的健壮性**:若单拍偶超 1 ms,TIM6 多发脉冲合并为一次唤醒,`adc_input_poll()` 返回
|
||||
`r==0`(无新帧)记 `idle`,**不重复处理同帧**,与 P0 修复(仅 `r==1` 压环)口径一致,无 DFT 窗污染。
|
||||
|
||||
### 10.4 残留风险与后续硬化(P1-5,规划中)
|
||||
**落地实测(2026-10-10,`@test` PASS,开机 29 s,台架无客户端)**:
|
||||
|
||||
- **§4.4 的 2 ms 忙等仍在 L1 层**:`adc_input_poll()` 内的 `HAL_ADC_PollForConversion`
|
||||
(`ADC_INPUT_POLL_TIMEOUT_MS=2U`)是任务级忙等。改造后 `adcTask` 若提至 AboveNormal,若 STM32 ADC
|
||||
偶发超时,该 2 ms 忙等会在**高优先级**下阻塞其余 Normal 任务(比现状更敏感)。**硬化方向(待做)**:
|
||||
超时即**整拍跳过**(不轮询 9 通道),或在 `adc_input.c` 改用非阻塞查询 + 标志,彻底消除忙等占用。
|
||||
- **启动 fail-safe(设计)**:TIM6 未启动前 `adcTask` 永久阻塞于 `osThreadFlagsWait` → 无心跳 → 看门狗
|
||||
`TASK_WD_ADC` 触发复位。属安全 fail-safe,非缺陷(确保"节拍源缺失"不被静默放过)。
|
||||
| 指标 | 改前(1 ms 轮询 + Normal) | **改后(事件驱动 + AboveNormal)** | 判读 |
|
||||
|---|---|---|---|
|
||||
| `idle` 无新帧拍数 | 649/20001;P0-1 后仍 **1/20000** | **0 / 19999**(两次心跳全零) | 相位抖动**彻底消失**,唤醒与生产同源 |
|
||||
| `frames` 速率 | 1000.0/s | **1000.0/s**(t=21155→29227 ms 恰 +8072 拍) | 节拍未丢、未重复消费 |
|
||||
| `ring` | == `frames` | == `frames`(19999) | P0-1 口径保持 |
|
||||
| `adcm` 单拍最坏 | ≈2.1 ms | **2.12~2.25 ms** | ⚠ 归因已复核:峰值来自**每 10 s 一次的心跳日志**(`DBG_INFO` + `log_phasors`),不是采集——同批 `adcp` 仅 122 µs,且 `idle=0` 反证没有"每拍 2 ms"(否则粘滞标志会合并唤醒、`idle` 必 >0) |
|
||||
| `uip` / `uiw` / `uis` | 61.3 / 11.4 ms / 1 µs | **61.3 / 11.4 ms / 1 µs** | 提优先级**未**伤到 UI(LVGL 地板不变) |
|
||||
| `defd` 控制台排空 | 46 ms | 46 ms | 未被 AboveNormal 挤动 |
|
||||
| `seq` − `frames` | — | 20006 − 19999 = **7** | 正是"TIM6 已在跑、消费者尚未就绪"的 ≈100 ms 窗口(§10.6),与设计一致 |
|
||||
|
||||
### 10.4 残留风险与后续硬化(落地后仍开放)
|
||||
|
||||
- **Task #32 §4.4 的 2 ms 忙等仍在 L1 层,且如今跑在 AboveNormal 上**:`adc_input_poll()` 内的
|
||||
`HAL_ADC_PollForConversion`(`ADC_INPUT_POLL_TIMEOUT_MS=2U`)是任务级忙等(`HAL_GetTick()`
|
||||
1 ms 粒度、判据 `> Timeout` → 实际 **2~3 ms**)。提优先级后,STM32 ADC 一旦偶发超时,这 2~3 ms
|
||||
会在**高于 net/ftp/default/timeSync** 的优先级上压住它们——比改前更敏感。
|
||||
**6 次上板对照(2026-10-10)**:超时 1 次的那轮 `adcp = 2209 µs`;超时 0 次的 5 轮
|
||||
`adcp = 122 µs`(= 9 通道 × 7.4 µs + TPAFE 快照)→ **2 ms 只在超时拍出现,不是常态开销**。
|
||||
⚠ **超时的第二个代价是正确性,不只是耗时**:超时拍 `s_stm32_valid` 仍是上次成功留下的 1,
|
||||
而 `adc_input_poll()` 的返回值只由 TPAFE 决定 → 该拍照样 `r == 1`、照样压环,STM32 侧几路
|
||||
(UB/UC/IB/IC/IN/温度)**静默沿用上一拍的值**(`adc_input.c:209-212`),`s_frames_skip`
|
||||
只统计"从未成功过"、抓不到它。
|
||||
**硬化方向(待做,按性价比排序)**:① 超时即置 `s_stm32_valid = 0` → 本拍 STM32 侧判为无有效
|
||||
数据,走既有 `skip` 整帧跳过路径(一行改动、无时序风险,先把"静默陈旧帧"这条正确性问题关掉);
|
||||
② EOC 改 EXTI 或 DMA + 非阻塞查询,彻底消除忙等;③ DMA 双缓冲属 V0.2 硬件项(§10.5)。
|
||||
- **已做的一项(2026-10-10):补 F4 ADC 上电自校准**。CubeMX **不生成**该步骤,且本仓库裁剪版
|
||||
HAL **没有** `HAL_ADCEx_Calibration_Start` → 由 `Src/adc.c` 的 `adc1_calibrate()` 实现
|
||||
(`USER CODE` 区 = L2,审计项 `adc1-calibration-call` / `adc1-calibration-impl` 兜底)。
|
||||
实测校准前后同一拍 STM32 侧 6 路**同向**偏移 +10~+63 LSB、TPAFE 侧一字不变 → 此前 ADC 确实
|
||||
带失调在跑。它**不消** 2 ms 忙等,但把"未校准"这条可疑路径从 §7.2 的候选根因里划掉。
|
||||
- **启动 fail-safe(设计如此,已按此实现)**:TIM6 未启动 / 停摆 → `adcTask` 永久阻塞于
|
||||
`osThreadFlagsWait(osWaitForever)` → `TASK_WD_ADC` 缺席 → IWDG 复位。属安全 fail-safe,
|
||||
非缺陷(确保"节拍源缺失"不被静默放过)。
|
||||
|
||||
### 10.5 下一版硬件建议:DMA + 双缓冲(方案 b,用户要求记录)
|
||||
|
||||
@@ -300,3 +341,25 @@ t=1000 µs 下一个 TIM6 更新事件
|
||||
AboveNormal 算力给保护算法;与 §8 等周波调频天然契合(ARR 改频 + DMA 自动续传)。
|
||||
- **验收前置**:先以逻辑分析仪确认 TPAFE 接口类型与 `DRDY` 时序,**再**定型下一版硬件 DMA 方案,
|
||||
避免返工。
|
||||
|
||||
### 10.6 实施时补的一条:消费者就绪门(规划未预见,必须保留)
|
||||
|
||||
规划写的是"ISR 里 `osThreadFlagsSet(adcTaskHandle, ...)`"。实施时发现**时序上不能直接这么写**:
|
||||
|
||||
- TIM6 在 `app_main_init()`(`Src/main.c:140`)里就 `HAL_TIM_Base_Start_IT(&htim6)`,而
|
||||
`osKernelInitialize()` / `MX_FREERTOS_Init()` / `osKernelStart()` 在 `:144/145/148`
|
||||
→ **"节拍源已在跑"与"消费者已存在"之间有约 100 ms 窗口**,其间 `adcTaskHandle` 还是 NULL、
|
||||
调度器也还没起。
|
||||
- `osThreadFlagsSet()` 对 NULL 句柄本身安全(`cmsis_os2.c` 的入参检查返回 `osErrorParameter`),
|
||||
但"句柄已赋值、调度器未启动"这段(建任务 → `osKernelStart`)仍会让 ISR 版内核 API 在调度器外跑。
|
||||
本仓库对这类坑有实测记录:`Src/main.c` 的 **TIM7 分支注释**(2026-10-02,port.c 优先级断言
|
||||
把系统卡死在 ISR 内)。
|
||||
- **处置**:ISR 只调 `adc_tick_irq_notify()`,其内含就绪门 `s_consumer_ready`——由 `adcTask`
|
||||
自己在进入等待前置 1(单写者 = 任务,ISR 只读)。把时序约束变成**显式代码**,而不是依赖
|
||||
FreeRTOS 的内部实现细节。
|
||||
- **实测印证**:`seq − frames = 20006 − 19999 = 7`——正是这 ≈100 ms 窗口里 TIM6 发出的 7 个脉冲
|
||||
被丢弃(当时无消费者),之后一帧不差。
|
||||
|
||||
**教训(与 `docs/问题记录/UI任务看门狗停摆分析.md` §9.3.1 的 I-2/I-3 同类)**:把"定时轮询"换成
|
||||
"事件驱动"时,必须单独核对**节拍源启动时刻 vs 消费者创建时刻**,否则第一次上电就会踩到
|
||||
"ISR 对还不存在的消费者动手"。
|
||||
|
||||
@@ -130,9 +130,10 @@ STM32(U17)为 LQFP144 脚号。
|
||||
|---|---|---|
|
||||
| **通道归属(硬件缺陷)** | 8 路电气量分属两片转换器:UA/UN/IA → TPAFE,UB/UC/IB/IC/IN → STM32 ADC | **V0.1 硬件缺陷**(非有意分配,理由见 §1)→ V0.1 上禁止跨转换器做相位/功率/功率因数/矢量类运算;**V0.2 改版要求 8 路电气量全部走 TPAFE**(登记簿 N-18) |
|
||||
| STM32 ADC 外设 | **已生成(2026-10-09,CubeMX 6.17 + FW_F4)**:`Src/adc.c`/`Inc/adc.h` 已入库并挂进 `MDK-ARM/*.uvprojx`;`Inc/stm32f4xx_hal_conf.h` 的 `HAL_ADC_MODULE_ENABLED` 已生效;HAL `stm32f4xx_hal_adc.c/_ex.c` + `stm32f4xx_ll_adc.h` 已补入仓库(HAL 原本是裁剪子集);`Src/main.c` 里 `MX_ADC1_Init()` 排在 `MX_IWDG_Init()` 之后。生成配置核对无误:9 通道 / Rank 1~9 / 全部 144 cycles / `ScanConvMode=ENABLE` / `ContinuousConvMode=DISABLE` / `ExternalTrigConv=ADC_SOFTWARE_START` + `EDGE_NONE` / `EOCSelection=ADC_EOC_SINGLE_CONV` / `DataAlign=RIGHT` / 12 位;`HAL_ADC_MspInit()` 已把 PA0~PA7+PC4 配成 `GPIO_MODE_ANALOG` | 硬件通路已打通(`hadc1` 可用);**消费者已落地**——`App/algo/adc_input.c` 的 `stm32_scan()`(L1-3,2026-10-10):`HAL_ADC_Start` + 逐通道 `PollForConversion`/`GetValue`,9 通道一轮 ≈ 67 µs |
|
||||
| ADC1 上电自校准 | **已补(2026-10-10)**:`Src/adc.c` 的 `USER CODE BEGIN ADC1_Init 2` 调用 `adc1_calibrate()`(实现同文件 `USER CODE BEGIN 1`,按 RM0090 §13.3.6:清 ADON→等生效→置 ADON→置 CAL→等 CAL 由硬件清零)。⚠ **CubeMX 不生成这一步**,且本仓库裁剪版 HAL **没有** `HAL_ADCEx_Calibration_Start`、裁剪版 CMSIS 头**没有** `ADC_CR2_CAL`(bit3,代码里按 RM 自建 `ADC1_CAL_BIT`)→ 属 L2 手工定制,审计项 `adc1-calibration-call` / `adc1-calibration-impl` 兜底 | 实测有效:校准前后同一拍 STM32 侧 6 路**同向**变化 `UB 898→961`、`UC 978→1038`、`IB 906→922`、`IC 1061→1071`、`IN 1017→1055`、`T 790→846`(+10~+63 LSB ≈ 0.2~1.5 % FS);TPAFE 侧 `UA/UN/IA` 一字不变 → 变化确来自 ADC1 校准。缺这一步不报错,只是**读数带失调** |
|
||||
| ADC 时钟 | CubeMX 的 F4 ADC 面板**有** Clock Prescaler 一项(此前"面板没有该项、固定 DIV2"的判断有误),已选 **PCLK2/4 → ADCCLK = 21 MHz** ✓。面板里 **DIV2 是灰的、选不到**:DB 里 `FadcConstraint Max=36000000`,APB2 = 84 MHz → DIV2 = 42 MHz 超限,该候选值被 `Action="DISABLE"` 禁掉,超规值进不来 | **不设额外防护(2026-10-09 决定)**:约束已由 CubeMX 把关;`.ioc` 虽不保存 `ADC1.ClockPrescaler` 键,重生成也只会回落到约束允许的值。按 144+12 cycles 算 ≈ 7.4 µs/通道、9 通道一轮 ≈ 67 µs |
|
||||
| `adc_task` | **已实现(B1,2026-10-10;L1 层 v2.3,2026-10-10)**:`App/task/adc_task.c` 每 1 ms 调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 原子快照(8 通道)+ STM32 ADC1 顺序扫描,**经 `adc_input_raw()` 按语义取数**(不再直接索引驱动缓冲);任务在 `Src/freertos.c` 的 `USER CODE` 区创建为 `adcTask`(512 words / Normal) | 数据路径已通(台架 `@test`:`frame_seq` 严格 1000 Hz 递增;`adc1_rc=0` / `adc1_err=0`) |
|
||||
| TPAFE 采样触发 | **周期 CONVST 已实现(2026-10-10)**:`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 **TIM6 更新中断(1 kHz)** 内调 `tpafe5160_start_conv_irq()`;`app_main_init()` 用 `HAL_TIM_Base_Start_IT(&htim6)` 启动节拍(在 `tpafe5160_init()` 之后) | 连续波形(1 kHz);详见 `子系统设计/ADC采样节拍设计说明.md` |
|
||||
| `adc_task` | **已实现(B1,2026-10-10;L1 层 v2.3;P1-5 事件驱动消费,2026-10-10)**:`App/task/adc_task.c` 每拍调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 原子快照(8 通道)+ STM32 ADC1 顺序扫描,**经 `adc_input_raw()` 按语义取数**(不再直接索引驱动缓冲);**消费唤醒由 TIM6 ISR 置位 `ADC_WAKE_FLAG`**(不再 1 ms 轮询);任务在 `Src/freertos.c` 的 `USER CODE` 区创建为 `adcTask`(512 words / **AboveNormal**) | 数据路径已通(台架 `@test`:`frames` 严格 1000 Hz 递增、**`idle=0`**、`adc1_rc=0` / `adc1_err=0`) |
|
||||
| TPAFE 采样触发 | **周期 CONVST 已实现(2026-10-10)**:`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 **TIM6 更新中断(1 kHz)** 内调 `tpafe5160_start_conv_irq()`;`app_main_init()` 用 `HAL_TIM_Base_Start_IT(&htim6)` 启动节拍(在 `tpafe5160_init()` 之后)。同一 TIM6 分支还调 `adc_tick_irq_notify()` **唤醒消费者**(P1-5 方案 a,2026-10-10;内含"消费者就绪"门) | 连续波形(1 kHz);详见 `子系统设计/ADC采样节拍设计说明.md` §10 |
|
||||
| 前端偏置 | 每路 1k 串联 + 1k 到 `*_GND` + 滤波 C,**无中点偏置** | STM32 侧只能测 **0~3.3 V 单极性**,交流量负半周被削顶;主板侧只做监视可接受,若要算 RMS/功率需硬件补偏置或明确算法口径 |
|
||||
| TPAFE 量程 | 默认 ±5 V(RANGE 接 GND),16 位二进制补码,LSB ≈ 152.59 µV | 用 `tpafe5160_to_voltage()` 换算 |
|
||||
| 取数契约(**2026-10-10 已修**,E-44 / E-45 / E-58 闭环) | 旧的 `get_buf` / `data_ready` / `clear_ready` **已删除**(删除前确认全仓库零调用者):`get_buf` 只给指针、任务再拷 → 被 BUSY EXTI 抢占就是**两帧混合**;`s_ready` 电平标志被 `start_conv_irq` 触发前清 0 → **重触发即丢未消费帧**;`read_all` 从不看 FRSTDATA(宏有定义、代码零引用)→ 芯片死亡时返回 OK + 常量垃圾。现口径:**`tpafe5160_snapshot(dst, count)`**(临界区原子拷贝,返回 1/0/-1,**仅任务上下文**)+ **`tpafe5160_frame_seq()`**(帧序号只增不清)+ **`tpafe5160_frstdata_err()`**(无跳变帧计数);`tpafe5160_init()` 增**首帧自检**——一帧读下来 FRSTDATA 无跳变(芯片不驱动并行总线;板上 R24 4.7k 上拉会把它钉死为高)→ 返回 `-3` + `[ERR]` | 消费者**只能**用 `snapshot()` 取数(`AGENTS.md` 已立为铁律);真机实测开机 `[ADC] Init OK` 现只在自检帧通过后才打印 → 判据在本板成立、非空转 |
|
||||
|
||||
+166
-93
@@ -4,13 +4,14 @@
|
||||
> → 连续 8 次(800ms)缺席 → 停喂 IWDG → 复位。
|
||||
> **背景**:L2 相量层落地(commit `3762c03`)后**新出现**的间歇性回归:L2 版 3/10 次运行失败,
|
||||
> 改前基线(`git stash` 重建)0/6。
|
||||
> **状态**:**分析完成,P0/P1/P2 处置已落地并上板验证**(2026-10-10)。P0(adc 压环条件 +
|
||||
> 截止期重同步)修复后累计 22 次启动 0/22 复现;P1-4a 已解耦 UI 心跳与 netTask 往返;P2 常开
|
||||
> 探针已上线。详见 §8「处置落地」。
|
||||
> **遗留 → §9**:P1-4a 只解耦了**心跳**,**刷新仍被 `ui_net_sink_poll()` 拖**(用户可观测的
|
||||
> "刷新不稳")。§9 已**结案**:A 门控 + 兜底**已落地并 10/10 上板验证**;"把推流搬出 uiTask"
|
||||
> 的 B 方案**已评估并决定不做**。⚠ 实测修正了当初的归因——刷新最坏周期(`uip` ≈61 ms)是
|
||||
> **LVGL 帧周期地板,与网络无关**。详见 §9.3.2 / §9.4。
|
||||
> **状态**:**分析完成,全部处置已落地并上板验证**(2026-10-10)。P0(adc 压环条件 + 截止期
|
||||
> 重同步)、P1-4a(UI 心跳自产)、P2(常开 `[PROF]` 探针)、§9 方案 A(推流就绪门控 + 兜底)、
|
||||
> **P1-5(ADC 消费改 TIM6 ISR 事件驱动 + `adcTask` 提至 `AboveNormal`)** 全部落地;停摆累计
|
||||
> **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` / 抽样窗 / 长耗时白名单)、
|
||||
@@ -30,9 +31,11 @@
|
||||
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 的心跳就晚一拍。**
|
||||
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 会**一起**掉线——它们没有。
|
||||
@@ -95,6 +98,9 @@
|
||||
### 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 日志反证(有力)
|
||||
|
||||
@@ -122,7 +128,7 @@
|
||||
| netTask | `osPriorityNormal` | 24 | ✅ |
|
||||
| ftpTask | `osPriorityNormal` | 24 | ✅ |
|
||||
| timeSyncTask | `osPriorityNormal` | 24 | ✅ |
|
||||
| adcTask | `osPriorityNormal` | 24 | ✅ |
|
||||
| adcTask | `osPriorityNormal`(**改前**;P1-5 后为 `osPriorityAboveNormal`) | 24 → **32** | ✅ |
|
||||
| **uiTask** | **`osPriorityBelowNormal`** | **16** | ✅ |
|
||||
| testTask | `osPriorityBelowNormal` | 16 | ✖(产品构建不创建) |
|
||||
|
||||
@@ -130,9 +136,16 @@
|
||||
uiTask 就**完全拿不到 CPU**;`configUSE_TIME_SLICING=1` 只保护**同优先级**之间的公平,
|
||||
对它无效。
|
||||
|
||||
> **P1-5 落地后的变化(2026-10-10)**:`adcTask` 已提到 **`AboveNormal`(32)** → 本表"uiTask
|
||||
> 垫底"的格局更极端。之所以可接受:① 改造后 `adcTask` 由 TIM6 **精确 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 掉"**)
|
||||
|
||||
`App/task/ui_task.c:117` 主循环:
|
||||
**改前**(`ui_task.c` v1.2,本次失效的现场)主循环:
|
||||
|
||||
```c
|
||||
while (1) {
|
||||
@@ -143,8 +156,8 @@ while (1) {
|
||||
}
|
||||
```
|
||||
|
||||
`Drivers/BSP/UI/ui_net_sink.c:457` 的 `ui_net_sink_poll()` 在**无远端客户端时**(台架常态)
|
||||
每轮都走 `net_accept()`;而 `Drivers/BSP/NET/net_socket.c:1222` 的 `net_accept()`:
|
||||
**改前** `ui_net_sink_poll()` 在**无远端客户端时**(台架常态)每轮都走 `net_accept()`;
|
||||
而 `net_accept()`(`Drivers/BSP/NET/net_socket.c`)的固定模式是:
|
||||
|
||||
```c
|
||||
if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { ... } /* 队列深 8,满则永久阻塞 */
|
||||
@@ -153,12 +166,17 @@ msg.result = net_wait_result("acc", sockfd);
|
||||
|
||||
`net_wait_result()`(`net_socket.c:851`)末句是 **`xTaskNotifyWait(..., portMAX_DELAY)`——无限期阻塞**。
|
||||
|
||||
→ **UI 的心跳周期 = LVGL 渲染 + 一次 `netTask` 往返 + 10 ms。** `netTask` 被延迟多久,
|
||||
→ **改前 UI 的心跳周期 = LVGL 渲染 + 一次 `netTask` 往返 + 10 ms。** `netTask` 被延迟多久,
|
||||
UI 的心跳就晚多久。**这是 UI 独有的依赖,其它任务没有**——正好解释"只有 UI 位缺失"。
|
||||
|
||||
### 4.3 adcTask 的截止期一旦错过就**不可恢复**
|
||||
**现状(两处已改,此依赖已断开)**:① **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)。
|
||||
|
||||
`App/task/adc_task.c:243`:
|
||||
### 4.3 adcTask 的截止期一旦错过就**不可恢复**(**已修 = P0-2**)
|
||||
|
||||
**改前**(`App/task/adc_task.c`):
|
||||
|
||||
```c
|
||||
next += ADC_TASK_BEAT_MS; /* 固定 +1ms,不随实际进度修正 */
|
||||
@@ -171,19 +189,58 @@ next += ADC_TASK_BEAT_MS; /* 固定 +1ms,不随实际进度修正 */
|
||||
(单次 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)`,唤醒由
|
||||
TIM6 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 优先级。
|
||||
9 通道全超时最坏 = **18 ms 连占**。这是现成的饥饿源。
|
||||
这是现成的饥饿源。
|
||||
|
||||
> **单拍上界修正(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 次。
|
||||
|
||||
**现状(P1-5 后:忙等未改,但跑它的任务优先级抬高了 → 敏感度上升)**:
|
||||
`ADC_INPUT_POLL_TIMEOUT_MS = 2U` 与忙等**仍未改**——P1-5 只做"事件驱动 + 提优先级",
|
||||
超时硬化列为后续(见
|
||||
[`../子系统设计/ADC采样节拍设计说明.md`](../子系统设计/ADC采样节拍设计说明.md) §10.4)。
|
||||
三点须一起看:
|
||||
|
||||
- 这条饥饿源如今**基本不被触发**,且**只在超时拍出现**: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 换来的新暴露,
|
||||
记在账上**;
|
||||
- ⚠ **超时的第二个代价是正确性,不只是耗时**:超时拍 `s_stm32_valid` 仍是上次成功留下的 1,
|
||||
而 `adc_input_poll()` 的返回值只由 TPAFE 决定 → 该拍照样 `r == 1`、照样压环,STM32 侧几路
|
||||
(UB/UC/IB/IC/IN/温度)**静默沿用上一拍的值**(`adc_input.c:209-212`);`s_frames_skip`
|
||||
只统计"从未成功过",抓不到它。→ 硬化第一步应是**超时即置 `s_stm32_valid = 0`**(走既有
|
||||
`skip` 整帧跳过路径,一行改动),而不是先去消忙等。
|
||||
|
||||
---
|
||||
|
||||
## 5. 顺带发现的一个**独立**缺陷:无新帧仍然压环
|
||||
## 5. 顺带发现的一个**独立**缺陷:无新帧仍然压环(**已修 = P0-1**)
|
||||
|
||||
`App/task/adc_task.c:140-176`:`r == 0`(TPAFE 无新帧)时**仍然**执行
|
||||
**改前**(`App/task/adc_task.c`):`r == 0`(TPAFE 无新帧)时**仍然**执行
|
||||
`sample_ring_push_frame()` + `phasor_run()`——只有 `r == -1` 才不压。
|
||||
|
||||
日志精确印证(`ring = frames + idle`):
|
||||
@@ -200,15 +257,23 @@ next += ADC_TASK_BEAT_MS; /* 固定 +1ms,不随实际进度修正 */
|
||||
这与本次停摆**无关**(不增加多少 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)的必需任务。
|
||||
- **只有 UI(改前)**:UI 是唯一低优先级(§4.1)**且**心跳依赖别的任务(§4.2)的必需任务。
|
||||
其它五个任务只要 100 ms 内被调度到一次就能喂上位,而它们全在 Normal 级,彼此时间片轮转保障。
|
||||
- **间歇**:需要一次"某个 Normal 任务连续占用 ≥800 ms"的巧合。候选(见 §7):
|
||||
**现状**:低优先级这一条**至今未变**(§4.1 的表仍成立,uiTask 仍是掩码里唯一的
|
||||
BelowNormal),变的是后半条——心跳已自产(P1-4a)、往返已门控(§9-A)。
|
||||
- **间歇**:需要一次"某个 Normal 任务连续占用 ≥800 ms"的巧合。**当时的**候选(见 §7):
|
||||
`netTask` 回执延迟、`defaultTask` 的长段(syslog → NAND/FTL 写、配置保存)、
|
||||
adcTask 的超时突发。
|
||||
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。**这正是"负载上去以后最先倒下的那块短板"。**
|
||||
@@ -222,13 +287,22 @@ next += ADC_TASK_BEAT_MS; /* 固定 +1ms,不随实际进度修正 */
|
||||
(a) `netTask` 回执延迟(§4.2),还是
|
||||
(b) `defaultTask` 长段(syslog NAND 写 / 配置保存),还是
|
||||
(c) adcTask 超时突发(§4.4)。
|
||||
**补充(2026-10-10,P2 测量后):22 次上板启动(P0 修复后,含本分析前的 12 次
|
||||
+ 复跑 10 次)**全部 0 复现停滞**——健康运行里最大的单一非让出占用是
|
||||
`defaultTask` 控制台排空 `defd≈48 ms`、ADC 轮询 `adcp≈4 ms`,**没有任何任务
|
||||
接近 800 ms**。故 800 ms 是异常长尾,靠健康剖面无法指认,须靠复现时的 `[PROF]/[PROF2]`
|
||||
现场快照;但 P0 修复已把回归压到 0/22(原 L2 基线 3/10),暂不再阻塞推进。
|
||||
2. **`adc1_err=288` 的自身根因未查。** 2 ms 超时对 7.4 µs 的转换而言异常巨大,
|
||||
指向 ADC 被反复 `Start/Stop` 或 EOC/OVR 状态异常;需专项测量,不要与本条混谈。
|
||||
**补充(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——说明本台架上电复位本就走软件复位路径
|
||||
@@ -236,75 +310,70 @@ next += ADC_TASK_BEAT_MS; /* 固定 +1ms,不随实际进度修正 */
|
||||
证据仍是 `[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` 的物理含义**:`idle=649/20001` 意味着任务轮询略快于帧产生(相位漂移),
|
||||
反映 `osDelayUntil`(1 ms 内核 tick)与 TIM6(1 kHz 硬件)之间的**时钟源差异**,
|
||||
本身不是故障,但会让 §5 的缺陷持续发生。
|
||||
5. **`idle` 的物理含义**:任务轮询与帧产生之间的相位漂移(`osDelayUntil` 的 1 ms 内核 tick
|
||||
vs TIM6 的 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. 处置建议(分层,**待确认后执行**)
|
||||
## 8. 处置:决策与落地结果(2026-10-10)
|
||||
|
||||
### P0 — 缺陷修复(低风险、可独立验证)
|
||||
1. **§5 压环条件**:`adc_task_func` 改为**仅在 `r == 1` 时**压帧 + `phasor_run()`,
|
||||
消除重复采样污染相量窗。
|
||||
2. **§4.3 截止期重同步**:`if (osDelayUntil(next) != osOK) { next = osKernelGetTickCount(); }`
|
||||
—— 让"落后"变成**一次性重同步**,而不是永久空转。
|
||||
> 本节原本是"待确认后执行"的分层建议;处置已按 **P2 先测 → 按数据定 P1 → P0 顺路一起修**
|
||||
> 的顺序执行完毕,故改写为**决策 + 结果**记录。当初的取舍理由仍是活约束,保留在状态列里。
|
||||
|
||||
### P1 — 结构性(真正消除饥饿,需择一或组合)
|
||||
3. **把 uiTask 提到 `osPriorityNormal`**(一行改动)。代价:UI 与其它任务平等竞争,
|
||||
LVGL 单轮 10 ms 量级可能给 netTask 增加抖动;收益:**结构上不可能再被无限期饿死**。
|
||||
4. **解开"心跳依赖别的任务"**(§4.2):`ui_task_func` 主循环把 `task_wd_beat(TASK_WD_UI)`
|
||||
提到 `ui_net_sink_poll()` **之前**;或给 `net_accept()` 一条**非阻塞**查询路径
|
||||
(`NET_MSG_DONTWAIT` 语义)供 UI 推流使用。**这条即使不动优先级也应做**——
|
||||
否则"UI 心跳"实际测的是"netTask 响应性"。
|
||||
5. **给 adcTask 的单拍耗时设硬上界**(§4.4):ADC 扫描改 **DMA / 中断驱动**(彻底去掉忙等),
|
||||
或至少把单通道超时压到不会吃掉整拍的量级,并在超时时**整拍跳过**而非继续自旋。
|
||||
| 项 | 内容 | 状态 |
|
||||
|---|---|---|
|
||||
| **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 / 超时整拍跳过)**仍未办**,且因优先级抬高而更敏感(§4.4 现状) |
|
||||
| **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 直发现场快照 |
|
||||
|
||||
### P2 — 观测(先能看见,再谈修)
|
||||
6. 启用现成的 `TEST_LATENCY_PROBE`(`App/task/ui_task.c:109` 的 `ui_loop` 段)→
|
||||
直接得到 UI 循环周期分布,一次性区分"饿死 vs 被拖"。
|
||||
7. 新增两个埋点:`adcTask` **单拍耗时**最坏值、`ui_net_sink_poll()` **往返耗时**最坏值。
|
||||
8. 停摆取证再加一项:把 `s_miss_hist` 与**各任务在本窗口的调用计数**一起落 `wdev.log`,
|
||||
直接指认占用者(现证据只到位图)。
|
||||
### 关键测量结论(当时决定 P1 取舍的依据)
|
||||
|
||||
### 建议执行顺序
|
||||
**P2-6/7 先测(一次 @test 即可拿到区分性数据)→ 按数据定 P1 的方案 → P0 顺路一起修 → 复跑 10 次确认 0/10。**
|
||||
不建议直接上 P1-3(提优先级)了事:那会**掩盖** §4.2 与 §4.4 两个真实缺陷。
|
||||
- **改前**健康运行 `uiw ≈ uis ≈ 50 ms` → UI 单轮耗时几乎等于推流往返,证实 §4.2:
|
||||
心跳实际测的是"netTask 响应性"而非"UI 活着"。
|
||||
- **改前** `uisn = 0`(推流段无"仍在段内"积压)→ "永久卡在往返"这一机理被排除。
|
||||
- 最大单一非让出占用一直是 `defaultTask` 控制台排空(46~48 ms),**没有任何任务接近 800 ms**
|
||||
→ 800 ms 属异常长尾(§7.1)。
|
||||
|
||||
### 处置落地(2026-10-10,按"先测量后修"顺序执行完毕)
|
||||
### 验证与遗留
|
||||
|
||||
- **P2(测量)= 已落地。** 新增常开探针模块 `App/util/task_prof.c/.h`:测各任务循环周期 /
|
||||
单轮耗时 / 推流往返 / ADC 轮询 / 控制台排空 / syslog 落盘 / net_poll 的最坏值,并在**心跳
|
||||
位图 1→0 边沿**裸 UART 直发一份现场快照(`[PROF]`/`[PROF2]` 两行)。接线覆盖 ui / adc /
|
||||
default / net 四个任务,已在 uvprojx 登记。
|
||||
- **P2 数据结论(决定 P1 的依据):**
|
||||
- 健康运行 `uiw≈uis≈50 ms` → **UI 单轮耗时几乎等于推流往返耗时**,证实 §4.2:心跳被
|
||||
`netTask` 往返主导,实际测的是"netTask 响应性"而非"UI 活着"。
|
||||
- 健康运行里 `uisn=0`(推流段无"仍在段内"积压)→ UI **未被卡在往返**,阻塞机理排除。
|
||||
- 最大单一非让出占用 `defd≈48ms`(控制台排空)/ `adcp≈4ms`(ADC 轮询),无任务接近 800ms。
|
||||
- **P1(结构性)= 已落地 P1-4a。** `App/task/ui_task.c`:把 `task_wd_beat(TASK_WD_UI)`
|
||||
**提到 `ui_net_sink_poll()` 之前**(v1.3)。心跳从此测"UI 自己被调度到",不再被 netTask
|
||||
往返拖垮。**未上 P1-3(提优先级)**:doc 明确警告它会掩盖 §4.2/§4.4。**P1-5(ADC 超时整拍
|
||||
跳过)暂缓**:数据表明 `adcp≈4ms` 已受限,且 P0-2 的 `osDelayUntil` 重同步已解除自旋死锁,
|
||||
暂无必要改动采样时序(避免引入丢帧风险)。**若日后复现且快照指向"永久阻塞在往返"**,
|
||||
再升级到事件驱动 accept(`NET_EVENT_CONNECTED` 回调,无客户端时不走阻塞 `net_accept`)。
|
||||
- **P0(缺陷修复)= 已落地并验证。** `App/task/adc_task.c`:
|
||||
- P0-1(§5):仅 `r==1` 时压环 + 重算。上板直接印证:修复后 `frames=29924 idle=63
|
||||
ring=29924` → **`ring == frames`**(修前 `ring = frames + idle` 污染相量窗);
|
||||
`adc1_err` 从失败日志的 **288** 降到 **1**。
|
||||
- P0-2(§4.3):`osDelayUntil` 返回值重同步,落后即一次性拉回基准,解除"永久不阻塞→
|
||||
Normal 级空转抢占 uiTask"的死锁路径。
|
||||
- **验证:** P0 修复后累计 22 次上板启动 **0/22** 复现停滞(原 L2 基线 3/10);
|
||||
P0+P1 后再复跑 10 次确认(见 `prof_batch4`)。构建 0 错误 0 警告
|
||||
(`Code=256276 RO=16160 RW=800 ZI=139760`)。
|
||||
- **遗留(不在本次处置范围):** §7.2 `adc1_err` 自身根因、§7.4 `[STACK]` 周期漂移、§4.4 的
|
||||
ADC 忙等未彻底改 DMA(仅以 P0-2 + 暂缓 P1-5 管控)。如复现重现,优先回头查 §7.2。
|
||||
- **验证**: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 无丢样,以上数据可信 |
|
||||
|
||||
---
|
||||
|
||||
@@ -326,10 +395,10 @@ next += ADC_TASK_BEAT_MS; /* 固定 +1ms,不随实际进度修正 */
|
||||
|
||||
| poll 内调用 | 触发频率 | 实际代价 | 上界 |
|
||||
|---|---|---|---|
|
||||
| `osMessageQueuePut(..., osWaitForever)` | 每次 RPC | 队列深 **8**,与 FTP / timeSync 共享(登记簿 **N-10**) | **无**(满则永久阻塞) |
|
||||
| `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** | 一次往返 + `net_accept_locked()` 内再跑一遍 `net_poll()`(整轮 SPI 中断读) | 实测最坏 **≈50 ms**(§8 `uis≈uiw≈50ms`) |
|
||||
| `net_recv(NET_MSG_DONTWAIT)` | 有客户端时每轮 | 同上(`net_recv_locked()` 亦先 `net_poll()`) | 同上 |
|
||||
| `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
|
||||
@@ -341,9 +410,9 @@ next += ADC_TASK_BEAT_MS; /* 固定 +1ms,不随实际进度修正 */
|
||||
|
||||
| # | 判据 | 现状 |
|
||||
|---|---|---|
|
||||
| **J1** | uiTask 主循环里 **0 次** net 往返 → 刷新周期只由 LVGL + 调度决定 | ❌ 每轮 1~2 次 |
|
||||
| **J1** | uiTask 主循环里 **0 次** net 往返 → 刷新周期只由 LVGL + 调度决定 | 🟡 **无客户端时已 0 次**(A 门控,`uis` = 1 µs);有客户端时仍有——刻意保留(§9.4) |
|
||||
| **J2** | 心跳自产,不依赖任何其他任务 | ✅ P1-4a |
|
||||
| **J3** | 推流的阻塞只影响推流自己(故障域隔离)+ 有观测 | ❌ 与刷新同域 |
|
||||
| **J3** | 推流的阻塞只影响推流自己(故障域隔离)+ 有观测 | ⚠ **刻意接受**:仍与刷新同域(B 已否决);观测 = `[PROF]` 的 `uis`/`uisn`(§9.4) |
|
||||
|
||||
### 9.3 方案 A:门控——让"没事的时候"poll 真的不做事
|
||||
|
||||
@@ -374,9 +443,9 @@ int net_can_read(int sockfd); /* 1 = 有数据可读或已 EOF;0 = 无 */
|
||||
① **晚一轮发现**(≤1 ms:netTask 自己每 1 ms 就 `net_poll()` 一次,`readable`/`state` 早已刷新);
|
||||
② **扑空**(状态刚变,RPC 返回 `WOULDBLOCK`,现有分支已处理)。
|
||||
|
||||
**收益**:台架常态(无客户端)poll 从"一次往返(最坏 50 ms)"降到**两次 RAM 读(< 2 µs)**;
|
||||
顺带销项 **N-10**(100 msg/s 队列压力,与 FTP 抢深度 8 的队列)。
|
||||
⚠ **A 不改变"有客户端时"的行为**——那正是 B 的靶子。
|
||||
**收益(实测见 §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 实现时新增的两条,设计阶段未预见)
|
||||
|
||||
@@ -391,7 +460,7 @@ int net_can_read(int sockfd); /* 1 = 有数据可读或已 EOF;0 = 无 */
|
||||
|
||||
#### 9.3.2 A 落地实测(2026-10-10,`@test` PASS,33 个 `[PROF]` 样本 / 开机 29 s / 台架无客户端)
|
||||
|
||||
| 字段 | 改前(§8 基线) | **改后实测** | 判读 |
|
||||
| 字段 | 改前(§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**——网络已不在刷新路径上 |
|
||||
@@ -471,3 +540,7 @@ A(门控 + 兜底)→ @build + @test + e2e → 看 [PROF]:uis 掉到 µs
|
||||
| 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 | **§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 忙等与"超时沿用旧值"**仍未处理** |
|
||||
|
||||
Reference in New Issue
Block a user