From 9231a3c22c761a084d6eaf9c4ab430895bf69bb2 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sat, 10 Oct 2026 22:41:19 +0800 Subject: [PATCH] =?UTF-8?q?adcTask=20=E4=BA=8B=E4=BB=B6=E9=A9=B1=E5=8A=A8?= =?UTF-8?q?=E6=B6=88=E8=B4=B9=EF=BC=88P1-5=20=E6=96=B9=E6=A1=88=20a?= =?UTF-8?q?=EF=BC=89+=20=E8=A1=A5=20F4=20ADC=20=E4=B8=8A=E7=94=B5=E8=87=AA?= =?UTF-8?q?=E6=A0=A1=E5=87=86=20+=20=E4=BF=AE=E6=AD=A3=202=20ms=20?= =?UTF-8?q?=E5=BF=99=E7=AD=89=E8=BF=87=E6=9C=9F=E8=A1=A8=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 钩子全绿 --- .cubemx_survival_baseline.json | 4 +- AGENTS.md | 11 +- App/algo/README.md | 7 +- App/task/adc_task.c | 106 +++++++--- App/task/adc_task.h | 14 ++ Inc/adc.h | 4 + README.md | 2 +- Src/adc.c | 68 ++++++- Src/freertos.c | 11 +- Src/main.c | 10 +- docs/内存管理/RAM使用分析与优化方案.md | 6 +- docs/子系统设计/ADC采样节拍设计说明.md | 121 +++++++++--- docs/硬件平台/ADC通道映射说明.md | 5 +- docs/问题记录/UI任务看门狗停摆分析.md | 259 ++++++++++++++++--------- scripts/check_cubemx_survival.py | 22 +++ scripts/gen_task_table.py | 2 +- 16 files changed, 480 insertions(+), 172 deletions(-) diff --git a/.cubemx_survival_baseline.json b/.cubemx_survival_baseline.json index bea7223d..d5bb76cf 100644 --- a/.cubemx_survival_baseline.json +++ b/.cubemx_survival_baseline.json @@ -19,7 +19,9 @@ "ioc-keepusercode": "b5bea41b6c623f7c", "ioc-iwdg-present": "d3a6e667048c95db", "ioc-task-stacks": "3262f737d92dcbee", - "adc1-hal-enabled": "1a8ed6cff34f7ba6" + "adc1-hal-enabled": "1a8ed6cff34f7ba6", + "adc1-calibration-call": "8d5f4b40fb218913", + "adc1-calibration-impl": "d33bc4323e243248" }, "_note": "CubeMX 存活审计基线;改动用 --write 重建" } diff --git a/AGENTS.md b/AGENTS.md index 46a93876..ec86753e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v ## 平台约束 - `Inc/`/`Src/` 中 CubeMX 生成文件带 `USER CODE BEGIN/END` 标记:自定义代码**只写标记区之间**;`Drivers/BSP/` 为纯手工代码 -- **CubeMX 重生成会覆盖标记区之外的一切**(`.ioc` 的 `KeepUserCode=true` **只**保护 `USER CODE` 区;2026-09-29 两次重生成各覆盖 4~5 处定制,其中 P1 两处**不报编译错**、只在真出故障时暴露)。防止定制丢失按三层优先级:**L1 写进 `.ioc`**(凡 CubeMX 界面能表达的配置,如任务栈/优先级/队列/事件组/堆大小/IWDG 参数——权限最高,重生成只会照写)→ **L2 搬进 `USER CODE` 区**(**只信 CubeMX 当前模板真实生成的区名**,优先文件头 `USER CODE BEGIN 0` 这类从未变过的标准位)→ **L3 审计兜底**。**每次重生成后立刻跑 `python scripts/check_cubemx_survival.py`**(已挂 pre-commit `cubemx-survival` 钩子;改动 `.ioc`/`Src/`/`Inc/`/`task_watchdog`/`.uvprojx` 触发)——清单 21 项 + 哈希基线漂移检测,直接列出"被覆盖了哪几处"。**2026-10-09 第三次重生成(启用 ADC1)补充**:`Inc/FreeRTOSConfig.h` 的自造 `FREE_RTOS_CONFIG_HARDENING` 区**第二次**被抹 → 已改走 **L1**:`configCHECK_FOR_STACK_OVERFLOW=2` / `configUSE_MALLOC_FAILED_HOOK=1` 在 GUI 的 **FreeRTOS → Config parameters → Hook function related definitions** 里设(写进 `.ioc` 由模板生成,永不再丢;教训:**别凭印象断言"界面里没有该项"**,先翻标签页或查 `db/mcu/IP/*_Modes.xml`)。⚠️ 启用后 CubeMX 会在 `Src/freertos.c` 生成 `USER CODE BEGIN 4 / 5` 区(默认内容是**空桩**)——**钩子实现必须整体写进这两个区**(原先在 `USER CODE Application` 区的实现要搬走,否则同名函数两处定义 = **编译错**;配套 helper `hook_ptr_ok()` 须在钩子之前 → 放 `USER CODE BEGIN PD` 区)。审计项 `stack-overflow-hook-impl` / `malloc-failed-hook-impl` / `stack-overflow-hook-helper` 分别校验 4 / 5 / PD 三个区。⚠️ **不要依赖"自造标记区"**:2026-09-29 实测 CubeMX **换了一代中断函数模板**(旧 `HardFault_Handler`/`HardFault_Report` 自造区 → 新函数体内 `HardFault_IRQn 0`/`W1_HardFault_IRQn 0`),旧区**连同内容一起消失**(看起来仍像"遵守了规则")。**生成区内无标记位可用的改动**分两种处理(2026-10-09 更新):① 要**去掉/替换生成区里的一句调用** → 用宏改名升级为 **L2**:例 `Src/iwdg.c` 的 `USER CODE BEGIN 0` 里放空实现 `iwdg_deferred_start()` + `#define HAL_IWDG_Init(a_h) iwdg_deferred_start(a_h)`,生成那句 `HAL_IWDG_Init(&hiwdg)` 自动变空操作(`.map` 证实 `Removing ... HAL_IWDG_Init, 94 bytes`),生成区保持原样、**重生成零 diff**,不必再手工删。⚠ **IWDG 启动后无法停止**(唯一手段是复位),所以不能"反初始化",只能在启动前拦截。② 要**替换生成实体的定义**(4 个 Fault Handler 本体——本代模板整函数重写)→ 同一招升级为 **L2**(2026-10-09 已做):我们的 `__asm void X_Handler` 定义放 `USER CODE BEGIN 0`,**必须写在 `#define X X_Gen` 之前**(写在之后会被自己的宏改名),生成区恢复 CubeMX 原样(`HardFault_IRQn 0` / `W1_HardFault_IRQn 0` 标记区照留),不再用锚点注释。`.map` 复核口径:`startup_*.o(RESET) refers to stm32f4xx_it.o(.emb_text) for HardFault_Handler` + 4 个 `*_Gen` 各 `Removing ... (2 bytes)`。**L3 清单已清空:重生成应当零 diff**;若仍有 diff,跑审计脚本定位(`protected` 项的断言只能用代码符号,注释会被剥掉)。方法论详见 `docs/子系统设计/看门狗设计说明.md` §10.1/§10.2 +- **CubeMX 重生成会覆盖标记区之外的一切**(`.ioc` 的 `KeepUserCode=true` **只**保护 `USER CODE` 区;2026-09-29 两次重生成各覆盖 4~5 处定制,其中 P1 两处**不报编译错**、只在真出故障时暴露)。防止定制丢失按三层优先级:**L1 写进 `.ioc`**(凡 CubeMX 界面能表达的配置,如任务栈/优先级/队列/事件组/堆大小/IWDG 参数——权限最高,重生成只会照写)→ **L2 搬进 `USER CODE` 区**(**只信 CubeMX 当前模板真实生成的区名**,优先文件头 `USER CODE BEGIN 0` 这类从未变过的标准位)→ **L3 审计兜底**。**每次重生成后立刻跑 `python scripts/check_cubemx_survival.py`**(已挂 pre-commit `cubemx-survival` 钩子;改动 `.ioc`/`Src/`/`Inc/`/`task_watchdog`/`.uvprojx` 触发)——清单 23 项 + 哈希基线漂移检测,直接列出"被覆盖了哪几处"。**2026-10-09 第三次重生成(启用 ADC1)补充**:`Inc/FreeRTOSConfig.h` 的自造 `FREE_RTOS_CONFIG_HARDENING` 区**第二次**被抹 → 已改走 **L1**:`configCHECK_FOR_STACK_OVERFLOW=2` / `configUSE_MALLOC_FAILED_HOOK=1` 在 GUI 的 **FreeRTOS → Config parameters → Hook function related definitions** 里设(写进 `.ioc` 由模板生成,永不再丢;教训:**别凭印象断言"界面里没有该项"**,先翻标签页或查 `db/mcu/IP/*_Modes.xml`)。⚠️ 启用后 CubeMX 会在 `Src/freertos.c` 生成 `USER CODE BEGIN 4 / 5` 区(默认内容是**空桩**)——**钩子实现必须整体写进这两个区**(原先在 `USER CODE Application` 区的实现要搬走,否则同名函数两处定义 = **编译错**;配套 helper `hook_ptr_ok()` 须在钩子之前 → 放 `USER CODE BEGIN PD` 区)。审计项 `stack-overflow-hook-impl` / `malloc-failed-hook-impl` / `stack-overflow-hook-helper` 分别校验 4 / 5 / PD 三个区。⚠️ **不要依赖"自造标记区"**:2026-09-29 实测 CubeMX **换了一代中断函数模板**(旧 `HardFault_Handler`/`HardFault_Report` 自造区 → 新函数体内 `HardFault_IRQn 0`/`W1_HardFault_IRQn 0`),旧区**连同内容一起消失**(看起来仍像"遵守了规则")。**生成区内无标记位可用的改动**分两种处理(2026-10-09 更新):① 要**去掉/替换生成区里的一句调用** → 用宏改名升级为 **L2**:例 `Src/iwdg.c` 的 `USER CODE BEGIN 0` 里放空实现 `iwdg_deferred_start()` + `#define HAL_IWDG_Init(a_h) iwdg_deferred_start(a_h)`,生成那句 `HAL_IWDG_Init(&hiwdg)` 自动变空操作(`.map` 证实 `Removing ... HAL_IWDG_Init, 94 bytes`),生成区保持原样、**重生成零 diff**,不必再手工删。⚠ **IWDG 启动后无法停止**(唯一手段是复位),所以不能"反初始化",只能在启动前拦截。② 要**替换生成实体的定义**(4 个 Fault Handler 本体——本代模板整函数重写)→ 同一招升级为 **L2**(2026-10-09 已做):我们的 `__asm void X_Handler` 定义放 `USER CODE BEGIN 0`,**必须写在 `#define X X_Gen` 之前**(写在之后会被自己的宏改名),生成区恢复 CubeMX 原样(`HardFault_IRQn 0` / `W1_HardFault_IRQn 0` 标记区照留),不再用锚点注释。`.map` 复核口径:`startup_*.o(RESET) refers to stm32f4xx_it.o(.emb_text) for HardFault_Handler` + 4 个 `*_Gen` 各 `Removing ... (2 bytes)`。**L3 清单已清空:重生成应当零 diff**;若仍有 diff,跑审计脚本定位(`protected` 项的断言只能用代码符号,注释会被剥掉)。方法论详见 `docs/子系统设计/看门狗设计说明.md` §10.1/§10.2 - **4 个 Cortex-M4 Fault 全部采集现场**(2026-09-29):`HardFault` / `MemManage` / `BusFault` / `UsageFault` 均为手工 `__asm` Handler(取 MSP/PSP 帧指针)→ `B` 到 per-fault C 入口 `fault_entry_{hard,mem,bus,usage}`(各自带故障名)→ `hard_fault_report(tag, frame)` 打 `[ERR] PC/LR/xPSR/R0-R3/CFSR/HFSR[/BFAR][/MMFAR]` 后 `NVIC_SystemReset()`。**后三者默认禁用**(复位后 `SHCSR` 只开 HardFault),不使能就会被**升级成 HardFault**、日志看不出根因 → `fault_handler_enable()`(`Src/stm32f4xx_it.c` 的 `USER CODE BEGIN 1` 区)在 `app_main_init()` **第一行**调用。**刻意不启用** `CCR.UNALIGN_TRP` / `CCR.DIV_0_TRP`(会把普通 bug 升级成死机)。⚠️ **ARMCC `__asm` 取不到 C 的数据符号**(`ADR r1, xxx_tag` 报 `A1516E: Bad symbol`,去 static 也无效)且无法传字符串参数 → 故障名必须由 C 侧 per-fault 入口带上,不要在 `__asm` 里取 C 变量 - **`SHCSR` 的 Fault 使能位无法在 CubeMX 配置**(2026-10-01 查模板源码证实):CubeMX 对 System Exception **只生成优先级(且仅当优先级 ≠ 0),从不生成使能代码**——`tpl_main_c.ftl` 的 `MX_NVIC_Init()` 用 `[#if vector.systemHandler=="false"]` 挡住 `HAL_NVIC_EnableIRQ`,而 `NVIC-STM32F417_Modes.xml` 里 `MemoryManagement/BusFault/UsageFault_IRQn` 都标 `Y`(=systemHandler);**全部模板中 `SHCSR` 只出现 1 次**且是 MPU 清理的 `&= ~MEMFAULTENA`(禁用),无任何 `|=` 置位;MPU 路径也堵死(`.ioc` 无 MPU,且 `HAL_MPU_Enable` 只置 `MEMFAULTENA` 一位)。⚠️ **`.ioc` 的 `NVIC.MemoryManagement_IRQn=true` 不代表已使能**(第 1 段只是"向量在 NVIC 表里可见")——别误以为勾了 Enabled 就等于开了故障。结论:只能手工写(现放 `USER CODE BEGIN 1` 区,属 L2,重生成不丢)+ 审计项 `fault-handler-enable` 兜底 - **`MDK-ARM/*.uvprojx` 每次 `@build` 后被 Keil UV4 自动改写,改动一律不入库**(2026-09-29 实测):UV4 构建时会把工程 XML 归一化成它本机风格的旧式写法(`` → ``、XML 声明加 `standalone="no"`),并把 `PackID` 降到**本机装的 DFP 版本**(本机实测 `3.1.1` → `3.0.0`,属**降级**)。这些差异是环境相关的,提交后会把别人的 Pack 版本也一起改掉。**判断方法**:`git diff MDK-ARM/STM32F407-Demo.uvprojx` 里若只见 XML 格式/`PackID` 差异而**无源文件挂载增删** → `git checkout -- MDK-ARM/STM32F407-Demo.uvprojx` 丢弃。只有真的新增/删除源文件(`grep 文件名` 计数变化)才提交 @@ -54,7 +54,8 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v - **FatFs 打开模式陷阱(2026-09-20 台架实测,`Lib/FatFs` 移植版)**:`FA_OPEN_APPEND`(0x30) = `FA_OPEN_ALWAYS|FA_SEEKEND`,**不含 `FA_WRITE`(0x02)**——不带写位打开的文件 `f_write` 恒返回 `FR_DENIED`(7)(/log 写失败真根因);追加写必须用 `FA_WRITE|FA_OPEN_APPEND`。该移植的 `f_write` 簇分配失败时返回 `FR_OK`+短写(无错误码),判失败必须校验写回字节数。标志位为旧式编号(`FA_READ`=0x01 / `FA_WRITE`=0x02 / `FA_CREATE_NEW`=0x04 / `FA_CREATE_ALWAYS`=0x08 / `FA_OPEN_ALWAYS`=0x10 / `FA_OPEN_APPEND`=0x30 / `FA_SEEKEND`=0x20(ff.c 内部定义)),与上游 FatFs R0.14 不同,勿照抄上游代码示例 - **FatFs 文件名约束(2026-09-20 台架实证,`.fschk` 无限重格式化事故根因)**:本移植 `Lib/FatFs/ffconf.h` 为 `FF_USE_LFN=0`,**只支持严格 8.3 短名**——主名≤8 字符、扩展≤3 字符、**不得以点开头**(`.fschk` 会被 `ff.c create_short` 把点后当扩展域解析超限 → `f_open` 恒返 `FR_INVALID_NAME`(6),纯 RAM 名字判定、不碰盘)。ff.c `create_short` 全部拒绝点已加 DEBUG 打印:`[ERR][FATFS] SFN reject (原因) seg='名字片段' bad_ch=坏字符`,非法名字调用看开机日志即可定位。自愈类逻辑**严禁**把名字/参数类错误码(`FR_INVALID_NAME` 等)归入卷损坏触发集(整卷重格式化不可逆) - **ADC 取数只用 `tpafe5160_snapshot()`(原子快照)**(2026-10-10 修 E-44/E-45/E-58):旧的 `get_buf()` / `data_ready()` / `clear_ready()` 已从驱动**删除**——`get_buf` 只给缓冲区指针、任务再逐通道拷,两步之间被 BUSY EXTI 抢占并跨过一帧周期就拷回**两帧混合**数据(8 通道失去同一时刻性,且无错误指示);`s_ready` 电平标志有三个写者,`start_conv_irq` 触发前清 0 = **重触发本身就会抹掉一帧未消费数据**。现口径:帧序号只增不清(`tpafe5160_frame_seq()`)、取数=临界区内拷贝(`tpafe5160_snapshot(dst, count)` 返回 1/0/-1,**仅任务上下文**)、FRSTDATA 全程无跳变(芯片不驱动并行总线)由 `tpafe5160_frstdata_err()` 计数且 `tpafe5160_init()` 首帧自检直接返回 `-3` + `[ERR]`。**消费者侧(L1 采集层)**:`App/algo/adc_input.c` 提供 `adc_input_poll()`(= TPAFE 快照 + STM32 ADC1 9 通道顺序扫描)与 `adc_input_raw()`(**按语义** `adc_semantic_t` 取数,不写死下标);映射表自检在 `adc_input_init()`(查每个 STM32 语义通道确实在扫描 rank 里,防"映射到从不扫描的通道 → 恒读陈旧值") -- **接 ADC / RS-485 消费者前的强制门禁(E-59,2026-10-10 固化)**:`adc_task` / `rs485_task` 从空桩转为真实消费者**之前**必须确认——① 驱动契约已修:TPAFE **E-44 / E-45 / E-58 已修 ✅**、RS485 **E-36 / E-37 已修 ✅** 但 **E-50(单缓冲无同步 → 包撕裂)未修 ❌**、**E-49(TX 超时拉 DE)未修 ❌**;② 栈预算:建任务前后各跑一次 `scripts/stack_report.py`。**`adc_task` 已过闸(B1,2026-10-10)**:消费者本体 + `adcTask`(512 words / Normal,`USER CODE` 区手工创建,同 timeSyncTask)+ 栈预算(静态 **560 B** / 余 **72%**,2026-10-10 实测)+ IWDG 心跳(`TASK_WD_ADC`)均已落地;**`rs485_task` 仍被挡(E-50 / E-49 未修)**。另按 N-18:V0.1 上两片转换器之间**禁止**做相位/功率/功率因数类运算 +- **STM32F4 ADC 上电必须自校准(2026-10-10 补,此前项目从未做过)**:CubeMX **不生成**这一步,且本仓库裁剪版 HAL **没有** `HAL_ADCEx_Calibration_Start`、裁剪版 CMSIS 头 `stm32f407xx.h` **没有** `ADC_CR2_CAL`(bit3)→ 只能按 RM0090 §13.3.6 手写:`Src/adc.c` 的 `adc1_calibrate()`(`USER CODE BEGIN 1` 区,自建 `ADC1_CAL_BIT`)+ `MX_ADC1_Init()` 末尾 `USER CODE BEGIN ADC1_Init 2` 调用;时序 = 清 ADON→等生效→置 ADON→置 CAL→等 CAL 由硬件清零。缺它**不报任何错**,只是读数带失调(实测校准让 ADC1 六路同向偏移 +10~+63 LSB,TPAFE 侧不变)。审计项 `adc1-calibration-call` / `adc1-calibration-impl` 兜底。⚠ 顺带一条编译坑:**块注释里别写 `JEXT*/EXT*` 这类串**——其中的 `*/` 会提前闭合注释,报 `#7: unrecognized token` +- **接 ADC / RS-485 消费者前的强制门禁(E-59,2026-10-10 固化)**:`adc_task` / `rs485_task` 从空桩转为真实消费者**之前**必须确认——① 驱动契约已修:TPAFE **E-44 / E-45 / E-58 已修 ✅**、RS485 **E-36 / E-37 已修 ✅** 但 **E-50(单缓冲无同步 → 包撕裂)未修 ❌**、**E-49(TX 超时拉 DE)未修 ❌**;② 栈预算:建任务前后各跑一次 `scripts/stack_report.py`。**`adc_task` 已过闸(B1,2026-10-10)**:消费者本体 + `adcTask`(512 words / **AboveNormal**——P1-5 方案 a 提优先级,`USER CODE` 区手工创建,同 timeSyncTask)+ 栈预算(静态 **560 B** / 余 **72%**,B1 实测;P1-5 后复测 **832 B** / 余 **59%**,见下)+ IWDG 心跳(`TASK_WD_ADC`)均已落地;**`rs485_task` 仍被挡(E-50 / E-49 未修)**。另按 N-18:V0.1 上两片转换器之间**禁止**做相位/功率/功率因数类运算 - **`DBG_*` 每条日志约 90 B Flash + 216 B 栈**(2026-10-02 S10 实测;2026-10-10 修正栈量):`dbg_log.h` 的 `DBG_INFO/ERROR/DEBUG` 在**每个调用点内联**行缓冲 `char __con_line[DBG_LINE_BUF_MAX]`(默认 = `console_log.h` 的 `DBG_CON_LINE_MAX` **192 B**)+ 时间戳缓冲 **24 B**(已并入同一数组,原为独立 32 B)+ 完整 `snprintf` 调用序列 → 单条约 **90 B Flash**(字符串常量另计)。给新模块接入日志时按"条数 × 90 B"估算 Flash 代价(实测 `sd2506_init` 加 6 条 = +576 B;`app_main` 加 3 条 = +180 B);**深调用链模块另需核算栈**——每条日志在栈上多占一份行缓冲、会进 `stack_report.py` 的最坏链:实测 defaultTask 那条链上 9 个帧各含一份 → **约 1.5 KB 纯粹是日志行缓冲**(2026-10-10 把该链 5312 → 2776 B 的主要杠杆之一)。故**日志不是免费的**——已覆盖的步骤不要再加"进入前/完成后"重复锚点,只在真正黑洞(原先静默返回)处补 - **深调用链模块收紧日志行缓冲:`#define DBG_LINE_BUF_MAX 128U`,写在 `#include "dbg_log.h"` 之前**(2026-10-10 加)。已收紧:`nand_ftl.c`/`gd5f2gq5ue.c`/`cfg_file.c`/`net_cfg.c`/`net_socket.c`/`ch395f.c`/`time_sync.c`/`default_task.c`。⚠ **取值必须 ≥ 该模块最长日志行**(时间戳 20 + 标签 + 正文),否则该行被截断——各文件注释已写出实测行长。改前/改后各跑一次 `stack_report.py` @@ -67,10 +68,10 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v | testTask | 2048 | BelowNormal | | uiTask | 4096 | BelowNormal | | timeSyncTask | 512 | Normal | -| adcTask | 512 | Normal | +| adcTask | 512 | AboveNormal | -(实际值以 `Src/freertos.c` 为准;testTask 仅在启用测试套件时创建。改动栈/缓冲前先核对此表与实测。2026-09-21 上调:defaultTask 512→1024(扛 mount/mkfs/FAT探针/cfg/syslog 深层 FatFS I/O,旧值经 `configCHECK_FOR_STACK_OVERFLOW=2` 钩子实测溢出)、testTask 1024→2048(存储套件 deep call chain + 局部缓冲溢出)。FreeRTOS 堆已迁 CCM,任务栈合计已不占 SRAM;2026-09-21 可观测性落地后实测:ftpTask 峰值曾达 3480B/4096B(余量 15%,低于 1024B 底线)→ 已按 `docs/内存管理/RAM使用分析与优化方案.md` §1.4 把 lftpd 9 处大局部缓冲静态化(BSS +4.6KB),复测峰值 **1184B/4096B(余量 71%)**;2026-10-10 新增 adcTask 后 **7 任务全 PASS**(adcTask 静态 560 B / 余 72%)) +(实际值以 `Src/freertos.c` 为准;testTask 仅在启用测试套件时创建。改动栈/缓冲前先核对此表与实测。2026-09-21 上调:defaultTask 512→1024(扛 mount/mkfs/FAT探针/cfg/syslog 深层 FatFS I/O,旧值经 `configCHECK_FOR_STACK_OVERFLOW=2` 钩子实测溢出)、testTask 1024→2048(存储套件 deep call chain + 局部缓冲溢出)。FreeRTOS 堆已迁 CCM,任务栈合计已不占 SRAM;2026-09-21 可观测性落地后实测:ftpTask 峰值曾达 3480B/4096B(余量 15%,低于 1024B 底线)→ 已按 `docs/内存管理/RAM使用分析与优化方案.md` §1.4 把 lftpd 9 处大局部缓冲静态化(BSS +4.6KB),复测峰值 **1184B/4096B(余量 71%)**;2026-10-10 新增 adcTask 后 **7 任务全 PASS**(adcTask 静态 560 B / 余 72%);同日 P1-5 后复测 adcTask **832 B / 余 59%**(新增 `osThreadFlagsWait` 调用链,仍高于 1024 B 底线)) ## 架构要点 @@ -80,7 +81,7 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v - **就绪屏障**(`App/app/sys_events.h` Event Flags,取代魔法延时):`SYS_EVT_NET_READY`(netTask 置位)/ `STORAGE_READY`(defaultTask:卷挂载 + 网络配置加载完成)/ `FTP_READY` / `TIMESYNC_READY`;消费者用 `osEventFlagsWait` 阻塞等待 - netTask:`net_stack_bring_up`(配置已由 defaultTask 在屏障前加载,本任务只读 RAM 副本 → `net_init` + PHY 强制 100M 全双工 → 递增网络代次号)→ 置 NET_READY → 循环 `net_poll` + `net_process_messages`;消费 `SYS_EVT_NET_RECONFIG` → 立即重建网络(新 IP 免重启生效,各 TCP 服务据代次号重建监听) - defaultTask:`f_mount` forced(无卷自动 `f_mkfs` FAT32;挂载后卷健康探针:小写两击确认,FAT 区陈旧/擦除时自动重格式化自愈,`/config.ini` 回落工厂默认——2026-09-20 /log 写失败修复)→ 网络配置加载(`net_cfg_load`;旧档迁移已于 2026-10-10 删除,见「模块约定」网口配置条)→ STORAGE_READY → LED 闪烁;同时是 syslog(`/log/run.log`·`selftest.log`,定长 64B 记录)的**唯一写者**与**配置引擎属主**(`cfg_file` 静态态单任务收敛于此:UI 保存请求执行 `net_cfg_save_run` + 2s `cfg_file_poll`,经 `SYS_EVT_CFG_SAVE` 事件唤醒;netTask/uiTask 不得直接触碰配置引擎,见 P1-3) -- adcTask(**B1,2026-10-10;L1 层 v2.3,2026-10-10**):**纯消费者**——每 1 ms 调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 临界区原子快照(8 通道)+ **STM32 ADC1 9 通道软件触发顺序扫描**(L1-3)→ 帧统计 + 每 10 s 心跳日志;取数一律经 `adc_input_raw()` **按语义**(L1-4,不写死下标);只取数、不做保护运算(V0.1 仅 TPAFE 三路 `UA`/`UN`/`IA` 测量有效,见 N-18)。**采样节拍(1 kHz)由 TIM6 更新中断硬件触发**(`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 TIM6 分支内调 `tpafe5160_start_conv_irq()`,2026-10-10);本任务 1 ms 轮询仅作消费节奏。手写于 `Src/freertos.c` 的 `USER CODE` 区(同 timeSyncTask,512 words / Normal),纳入 IWDG `required_mask`(`TASK_WD_ADC`)。详见 `docs/子系统设计/ADC采样节拍设计说明.md`、`App/algo/README.md` +- adcTask(**B1,2026-10-10;L1 层 v2.3,2026-10-10;P1-5 事件驱动消费,2026-10-10**):**纯消费者**——每拍调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 临界区原子快照(8 通道)+ **STM32 ADC1 9 通道软件触发顺序扫描**(L1-3)→ 帧统计 + 每 10 s 心跳日志;取数一律经 `adc_input_raw()` **按语义**(L1-4,不写死下标);只取数、不做保护运算(V0.1 仅 TPAFE 三路 `UA`/`UN`/`IA` 测量有效,见 N-18)。**采样节拍(1 kHz)由 TIM6 更新中断硬件触发**(`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 TIM6 分支内调 `tpafe5160_start_conv_irq()`,2026-10-10);**消费唤醒与该节拍同源**——同一 TIM6 分支再调 `adc_tick_irq_notify()` 置位 `ADC_WAKE_FLAG`,任务用 `osThreadFlagsWait(..., osWaitForever)` 等唤醒(不再 1 ms 轮询;实测 `idle` 由 1~649 降到 **0**)。⚠ ISR 侧**只能**走 `adc_tick_irq_notify()`——它内含"消费者就绪"门(TIM6 在 `app_main_init` 就启动,比调度器早 ≈100 ms)。手写于 `Src/freertos.c` 的 `USER CODE` 区(同 timeSyncTask,512 words / **AboveNormal**),纳入 IWDG `required_mask`(`TASK_WD_ADC`)。详见 `docs/子系统设计/ADC采样节拍设计说明.md` §10、`App/algo/README.md` - **采集与算法层(L1/L2)落在 `App/algo/`**(2026-10-10 建):现有 `adc_input.c/h` = L1(语义通道映射 + STM32 ADC1 扫描);L2 相量层待建。新算法模块一律进这里,勿塞进 `App/task/`(任务只做消费/编排);见 `App/algo/README.md` - 全景 `docs/架构设计/项目架构总览.md`(六层分层 / 启动时序 / 存储·网络·UI 栈 / 目录组织) diff --git a/App/algo/README.md b/App/algo/README.md index dbe41c0b..9d8e5c90 100644 --- a/App/algo/README.md +++ b/App/algo/README.md @@ -19,13 +19,14 @@ `sample_ring.c` / `phasor.c` **零平台依赖**(只用 `math.h` / `stddef.h` 与本目录头); `adc_input.c` 另依赖 `adc.h`(`hadc1`)与 `tpafe5160.h`(快照),主机侧以桩头替身。 -## 2. 数据流(一拍 = 1 ms) +## 2. 数据流(一拍 = TIM6 1 kHz 节拍) ```mermaid flowchart TD - TIM6["TIM6 更新中断 1 kHz
Src/main.c → tpafe5160_start_conv_irq()"] --> TPAFE["TPAFE5160 转换
BUSY EXTI 收帧"] + TIM6["TIM6 更新中断 1 kHz
Src/main.c → tpafe5160_start_conv_irq()
+ adc_tick_irq_notify()"] --> TPAFE["TPAFE5160 转换
BUSY EXTI 收帧"] + TIM6 -. "ADC_WAKE_FLAG
osThreadFlagsWait" .-> TASK TPAFE --> POLL - TASK["adcTask(1 ms osDelayUntil)
App/task/adc_task.c"] --> POLL["adc_input_poll()
TPAFE 快照 + STM32 ADC1 9 通道扫描"] + TASK["adcTask(事件驱动唤醒,非定时轮询)
App/task/adc_task.c"] --> POLL["adc_input_poll()
TPAFE 快照 + STM32 ADC1 9 通道扫描"] POLL --> RAW["adc_input_raw(ch) × 9
按语义取原始码值"] RAW --> RING["sample_ring_push_frame()
整帧入环(40 点窗)"] RING --> RUN["phasor_run()
逐实例 20 点重算 + 双区切换"] diff --git a/App/task/adc_task.c b/App/task/adc_task.c index 39791600..03211c12 100644 --- a/App/task/adc_task.c +++ b/App/task/adc_task.c @@ -9,9 +9,12 @@ * 并纳入看门狗 required_mask(TASK_WD_ADC)。 * **采样节拍(v2.2,2026-10-10)**:由 **TIM6 更新中断(1 kHz)** 硬件触发—— * ISR 内调 tpafe5160_start_conv_irq()(Src/main.c 的 - * HAL_TIM_PeriodElapsedCallback);本任务**不再自行触发**,退化为**纯消费者**, - * 1ms 轮询(osDelayUntil)仅作消费节奏。详见 - * docs/子系统设计/ADC采样节拍设计说明.md。 + * HAL_TIM_PeriodElapsedCallback);本任务**不再自行触发**,退化为**纯消费者**。 + * **消费唤醒(v2.7,2026-10-10,Task #32 P1-5 方案 a)**:由 1 ms `osDelayUntil` + * 轮询改为 **TIM6 ISR 置位 `ADC_WAKE_FLAG` 的事件驱动**——唤醒与生产同源锁定, + * 消除 0~1 ms 的 tick 量化相位抖动与"超拍永久落后 → 空转"风险;本任务优先级 + * 同步由 Normal 提到 **AboveNormal**(唤醒即抢占)。详见 + * docs/子系统设计/ADC采样节拍设计说明.md §10。 * **L1 采集层(v2.3,2026-10-10)**:取数改走 App/algo/adc_input.c—— * 本任务每拍调 adc_input_poll()(TPAFE 快照 + STM32 ADC1 9 通道扫描), * 经 adc_input_raw() **按语义**读通道(L1-3/L1-4),不再直接触碰驱动缓冲。 @@ -54,6 +57,14 @@ * (消除重复采样污染 20 点 DFT 窗的系统性误差); * §4.3 osDelayUntil 返回值被丢弃 → 落后时一次性重同步 * (原写法会永久落后 → 不再阻塞 → 空转抢占 uiTask) + * v2.7 2026-10-10 王建锋 Task #32 P1-5 方案 a(事件驱动消费): + * 消费唤醒 osDelayUntil(1ms) → osThreadFlagsWait(ADC_WAKE_FLAG, + * osWaitForever),标志由 TIM6 ISR 经 adc_tick_irq_notify() 置位; + * 删除 ADC_TASK_BEAT_MS 与 next(P0-2 的重同步因此不再需要); + * 新增"消费者就绪"门 s_consumer_ready——TIM6 在 app_main_init() + * 就启动,**远早于** MX_FREERTOS_Init()/osKernelStart(), + * ISR 不得在此之前对内核动手(同 Src/main.c 里 TIM7 的 + * 2026-10-02 实测坑)。优先级 Normal → AboveNormal(见 freertos.c) */ #include "adc_task.h" @@ -69,9 +80,9 @@ #define DBG_TAG "[ADC_TASK]" #include "dbg_log.h" -/* 消费轮询周期(ms)。采样节拍由 TIM6(1 kHz)提供,本周期仅作消费节奏 */ -#define ADC_TASK_BEAT_MS 1U -/* 心跳日志周期(ms):周期性上报帧统计,作为采集链路存活的观测面 */ +/* 心跳日志周期(ms):周期性上报帧统计,作为采集链路存活的观测面。 + * (原 ADC_TASK_BEAT_MS = 1U 的消费轮询周期已随 v2.7 事件驱动改造删除—— + * 消费节拍改由 TIM6 ISR 置位 ADC_WAKE_FLAG 提供,与生产同源,不再自定周期) */ #define ADC_TASK_REPORT_MS 10000U /* 统计:已消费帧数 / 无新帧次数(首拍必为无新帧,之后应稳定为 0 增量)。 @@ -81,6 +92,38 @@ static uint32_t s_frames_idle; /* 因通道尚未首次成功而**整拍跳过入环**的次数(不向相量窗注入 0 点) */ static uint32_t s_frames_skip; +/* adcTask 的内核句柄,定义在 Src/freertos.c(引用方式同 App/task/default_task.c) */ +extern osThreadId_t adcTaskHandle; + +/* 消费者就绪门:由 adcTask 自己在进入等待前置 1;单写者 = 本任务,ISR 只读。 + * ⚠ 为什么必须有这道门:TIM6 在 app_main_init()(Src/main.c:140)里就 + * HAL_TIM_Base_Start_IT 启动,**远早于** osKernelInitialize()/MX_FREERTOS_Init()/ + * osKernelStart()(Src/main.c:144/145/148)→ "节拍源已在跑"与"消费者已存在"之间有 + * 约 100 ms 窗口,其间 adcTaskHandle 还是 NULL、调度器也还没起。 + * osThreadFlagsSet() 对 NULL 句柄本身会安全返回 osErrorParameter(cmsis_os2.c 的入参 + * 检查),但"句柄已赋值、调度器未启动"这段(freertos.c 建任务 → osKernelStart)仍会让 + * ISR 版内核 API 在调度器外跑。本仓库对这类坑有实测记录:Src/main.c 的 TIM7 分支注释 + * (2026-10-02,port.c 优先级断言把系统卡死在 ISR 内)。就绪门把这条时序变成**显式约束**, + * 而不是依赖 FreeRTOS 内部实现细节。 */ +static volatile uint8_t s_consumer_ready; + +/* + * 函数功能:TIM6 采样节拍的 ISR 入口——置位唤醒 adcTask 消费一拍 + * 入口参数:无 + * 返 回 值:无 + * 限定条件:仅 ISR 上下文。TIM6 抢占优先级 = 6 ≥ configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY + * (=5) → osThreadFlagsSet 走 xTaskNotifyFromISR + portYIELD_FROM_ISR,合法且立即切换 + * 函数说明:Task #32 P1-5 方案 a——ISR **只发唤醒**,全部计算留在任务里(拒绝"ISR 内计算": + * adc_input_poll() 内含 2 ms 忙等,绝不可入 ISR)。线程标志粘滞 → 单拍偶超 1 ms 时 + * 多个脉冲合并成一次唤醒,下一轮 adc_input_poll() 返回 r==0 记 idle,不会重复处理同一帧 + */ +void adc_tick_irq_notify(void) { + if (s_consumer_ready == 0u) { + return; + } + (void)osThreadFlagsSet(adcTaskHandle, ADC_WAKE_FLAG); +} + /* * 函数功能:打印全部相量实例(×0.1 定点) * 入口参数:tag - 前缀标记("first" 首次产出 / "hb" 周期心跳) @@ -127,32 +170,40 @@ int adc_task_init(void) { * 入口参数:arg - FreeRTOS 传入参数(未使用) * 返 回 值:无 * 限定条件:adc_task_init() 已调用;内核已启动 - * 函数说明:每 1ms 采集一拍(adc_input_poll():TPAFE 原子快照 + STM32 ADC1 9 通道 - * 顺序扫描)并统计。采样节拍由 TIM6 硬件定时器提供(ISR 内触发 CONVST), - * 本任务不再触发。稳态下 consumed 每拍 +1、idle 不再增长;idle 持续增长 = + * 函数说明:每拍采集一次(adc_input_poll():TPAFE 原子快照 + STM32 ADC1 9 通道顺序扫描) + * 并统计。采样节拍由 TIM6 硬件定时器提供(ISR 内触发 CONVST),本任务不再触发; + * **本任务的唤醒同样由该 TIM6 ISR 置位 ADC_WAKE_FLAG 驱动**(v2.7 事件驱动, + * 不再自定 1 ms 周期)。稳态下 consumed 每拍 +1、idle 不再增长;idle 持续增长 = * EXTI 未在读帧或 TIM6 未启动(异常),由周期心跳日志暴露。 + * ⚠ 若 TIM6 停摆,本任务会**永久阻塞**在 osThreadFlagsWait → TASK_WD_ADC 缺席 → + * IWDG 复位。这是刻意的 fail-safe("节拍源缺失"不被静默放过),不是缺陷。 */ void adc_task_func(void *arg) { - uint32_t next = osKernelGetTickCount(); - uint32_t report_at = next + ADC_TASK_REPORT_MS; + uint32_t report_at = osKernelGetTickCount() + ADC_TASK_REPORT_MS; uint8_t first_logged = 0u; uint8_t first_phasor_logged = 0u; (void)arg; + /* 开闸:此刻任务已存在、调度器必已运行 → ISR 的唤醒从此生效。 + * 放在进入循环之前:置位与等待之间不会漏唤醒(线程标志粘滞) */ + s_consumer_ready = 1u; + for (;;) { int r; - /* P2 探针:单拍起点。名义节拍 1 ms = 168000 周期;若实测远超, - * 本任务(osPriorityNormal)会持续抢占 uiTask(osPriorityBelowNormal), - * 是"UI 心跳缺席"的候选占用者之一 */ + /* P2 探针:单拍起点。名义节拍 1 ms = 168000 周期;若实测**持续**超过, + * 本任务(v2.7 起为 osPriorityAboveNormal)会持续抢占 uiTask + * (osPriorityBelowNormal),是"UI 心跳缺席"的候选占用者之一 */ task_prof_enter(TASK_PROF_ADC_BEAT); /* 1) 采集一拍:L1 层 adc_input_poll() = TPAFE 原子快照 + STM32 ADC1 顺序扫描 * (仅任务上下文;TPAFE 侧 1=有新帧 0=无新帧 -1=参数错)。 * 采样节拍由 TIM6 更新中断提供(ISR 内触发 CONVST),本任务不触发。 - * P2 探针:本段单独计时——ADC 超时是 2ms 不让出的忙等(HAL_ADC_ - * PollForConversion),是单拍耗时的主要来源,必须与压环/相量分开看 */ + * P2 探针:本段单独计时。实测常态 adcp = 122 us(9 通道 x 7.4 us + TPAFE 快照); + * 只有 STM32 侧超时才跳到 ~2.2 ms——HAL_ADC_PollForConversion 是 2 ms 不让出的忙等, + * 而 stm32_scan() 在**首个**超时处即 Stop+return,故单拍上界是**一次**超时(不是 9 次)。 + * ⚠ adcm(整拍)的 ~2.1 ms 峰值来自每 10 s 一次的心跳日志,不是本段,勿混读 */ task_prof_enter(TASK_PROF_ADC_POLL); r = adc_input_poll(); task_prof_exit(TASK_PROF_ADC_POLL); @@ -266,17 +317,16 @@ void adc_task_func(void *arg) { } } - /* 7) 等下一个节拍(固定周期;落后则立即补齐,不累积漂移) - * ⚠ P0 修复(2026-10-10,Task #32 §4.3):osDelayUntil 在**目标时刻已过** - * 时返回 osErrorParameter 并立即返回(cmsis_os2.c 的 "No delay or already - * expired" 分支)。若像原实现那样只写 `(void)` 丢弃返回值,一次超拍会让 - * next 相对当前时刻**永久落后** → 本任务从此不再阻塞、以 Normal 优先级 - * 空转,直到某个"快拍"把差额追平(连续慢拍则锁死,全程抢占低优先级的 - * uiTask)。返回值是现成的重同步钩子:落后就一次性把基准拉回当前时刻。 */ - task_prof_exit(TASK_PROF_ADC_BEAT); /* P2 探针:单拍耗时结算(不含睡眠) */ - next += ADC_TASK_BEAT_MS; - if (osDelayUntil(next) != osOK) { - next = osKernelGetTickCount(); - } + /* 7) 等下一次节拍唤醒(v2.7 事件驱动,Task #32 P1-5 方案 a) + * 原实现是 osDelayUntil(next += 1ms):把消费唤醒**钉在 FreeRTOS tick(1 ms)边界** + * → 与 TIM6 生产之间有 0~1 ms 随机相位抖动(日志里的 idle 正是这么来的), + * 且一旦超拍,next 就永久落后 → 任务不再阻塞 → 空转抢占低优先级 uiTask + * (P0-2 修的正是它)。现在唤醒与生产**同源**:TIM6 ISR 触发 CONVST 后置位 + * ADC_WAKE_FLAG,本任务被唤醒的延迟 = ISR + 调度(µs 级;本任务已提至 + * AboveNormal → 立即抢占),无 tick 量化、无累积漂移,也就不再需要重同步钩子。 + * 标志是命令位(等待即消费)→ osFlagsWaitAny,不加 osFlagsNoClear; + * osWaitForever 无超时 = 刻意的 fail-safe(TIM6 停摆 → 无心跳 → IWDG 复位)。 */ + task_prof_exit(TASK_PROF_ADC_BEAT); /* P2 探针:单拍耗时结算(不含等待) */ + (void)osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever); } } diff --git a/App/task/adc_task.h b/App/task/adc_task.h index 4159d9ad..087769f9 100644 --- a/App/task/adc_task.h +++ b/App/task/adc_task.h @@ -6,6 +6,9 @@ * 创建日期:2026-07-19 * 修改记录: * v1.0 2026-07-19 王建锋 创建初始版本 + * v1.1 2026-10-10 王建锋 Task #32 P1-5 方案 a:新增 ADC_WAKE_FLAG 与 + * adc_tick_irq_notify()——消费节拍由 osDelayUntil(1ms) + * 轮询改为 TIM6 ISR 事件驱动唤醒 */ #ifndef ADC_TASK_H @@ -18,6 +21,17 @@ extern "C" { int adc_task_init(void); void adc_task_func(void *arg); +/* Task #32 P1-5(方案 a,2026-10-10):事件驱动消费。 + * TIM6 1 kHz 更新中断触发 CONVST 后,用本标志唤醒 adcTask 消费一拍。 + * 线程标志在此是**命令位**(等待即消费),不是状态位 → 用 osFlagsWaitAny, + * 不加 osFlagsNoClear(AGENTS「就绪屏障位必须 osFlagsNoClear」只约束状态位) */ +#define ADC_WAKE_FLAG 0x0001U + +/* ISR 入口:由 Src/main.c 的 HAL_TIM_PeriodElapsedCallback(TIM6 分支)调用。 + * 内部自带"消费者就绪"门与句柄判空 → 调度器启动前调用也安全(理由见 .c 注释)。 + * 收在 adc_task.c 而非 main.c 内联:main.c 不碰任何内核句柄,与 task_wd_tick_probe() 同构 */ +void adc_tick_irq_notify(void); + #ifdef __cplusplus } #endif diff --git a/Inc/adc.h b/Inc/adc.h index 68631ad8..c0a0a036 100644 --- a/Inc/adc.h +++ b/Inc/adc.h @@ -42,6 +42,10 @@ void MX_ADC1_Init(void); /* USER CODE BEGIN Prototypes */ +/* ADC1 上电自校准(RM0090 §13.3.6;本仓库 HAL 无 HAL_ADCEx_Calibration_Start, + * 实现见 Src/adc.c 的 USER CODE BEGIN 1)。返回 0 = 完成,-1 = 等待超时 */ +int adc1_calibrate(void); + /* USER CODE END Prototypes */ #ifdef __cplusplus diff --git a/README.md b/README.md index 18837c12..89dca52f 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ main.c | testTask | 2048 words | BelowNormal | 硬件测试入口 | | uiTask | 4096 words | BelowNormal | LVGL UI 界面 | | timeSyncTask | 512 words | Normal | 墙钟时间同步 | -| adcTask | 512 words | Normal | TPAFE5160 采样(1ms 节拍 + 原子快照) | +| adcTask | 512 words | AboveNormal | TPAFE5160 采样(TIM6 1kHz 事件驱动 + 原子快照) | > 上表与 `Src/freertos.c` 的 `osThreadAttr_t` 一致(2026-09-25 校准);改动栈大小前先读 diff --git a/Src/adc.c b/Src/adc.c index cc5cf0d1..59bd5e39 100644 --- a/Src/adc.c +++ b/Src/adc.c @@ -141,7 +141,15 @@ void MX_ADC1_Init(void) Error_Handler(); } /* USER CODE BEGIN ADC1_Init 2 */ - + /* STM32F4 ADC 上电**自校准**:RM0090 §13.3.6 / AN4976 要求的必需步骤,CubeMX 不生成, + * 且本仓库的 HAL 是裁剪子集、**没有** HAL_ADCEx_Calibration_Start()(已核实 + * stm32f4xx_hal_adc_ex.c/.h 均无该函数)→ 用本文件 USER CODE BEGIN 1 里的等价实现。 + * 必须在首次转换之前完成,否则 ADC 存在失调误差(未校准的偏移可达数十 LSB)。 + * 放 USER CODE 区 = L2,重生成不丢;由 cubemx-survival 审计项 adc1-calibration 兜底。 */ + if (adc1_calibrate() != 0) + { + Error_Handler(); + } /* USER CODE END ADC1_Init 2 */ } @@ -222,6 +230,64 @@ void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle) } /* USER CODE BEGIN 1 */ +/* CR2.CAL(bit 3)。⚠ 本仓库的 CMSIS 设备头 stm32f407xx.h 是裁剪版,**未定义** + * ADC_CR2_CAL(已核实:CR2 只有 ADON/CONT/DMA/DDS/EOCS/ALIGN/JEXTSEL/JEXTEN/ + * JSWSTART/EXTSEL/EXTEN/SWSTART,没有 CAL) + * → 按 RM0090 在此补齐,命名避开 CMSIS 前缀以免将来重定义冲突。 */ +#define ADC1_CAL_BIT (0x1UL << 3U) +/* 断电/校准等待上限(ms)。CAL 实测在数十 µs 内清零,10 ms 是 100 倍以上余量 */ +#define ADC1_CAL_TIMEOUT_MS (10U) +/* ADON 0→1 后的稳定等待(µs),同 HAL 的 ADC_STAB_DELAY_US 口径 */ +#define ADC1_STAB_DELAY_US (20U) +/* + * 函数功能:ADC1 上电自校准(等价于 HAL_ADCEx_Calibration_Start,本仓库 HAL 无此函数) + * 入口参数:无(操作 ADC1 寄存器) + * 返 回 值:0 - 校准完成;-1 - 等待超时(ADON 未清零 / CAL 未清零) + * 限定条件:MX_ADC1_Init() 之后、**首次转换之前**;内核启动前可调用 + * (只用 HAL_GetTick 与寄存器,不触碰任何 CMSIS/FreeRTOS 对象) + * 函数说明:RM0090 §13.3.6 时序——① ADC 必须处于关闭态(清 ADON 并等其生效); + * ② 置 ADON 上电,等 Tstab;③ 置 CAL 启动自校准;④ 完成后**硬件自动**清 CAL, + * 等其清零即完成。校准值存在 ADC 内部偏置寄存器,此后所有转换都带上校正。 + * 缺这一步的后果不是"读不到"而是"读得有偏"——正是 §7.2 那类 + * adc1_err/异常值的候选根因之一,见 docs/硬件平台/ADC通道映射说明.md。 + */ +int adc1_calibrate(void) +{ + uint32_t tickstart; + + /* 1) 前置条件:ADC 必须关闭 */ + CLEAR_BIT(ADC1->CR2, ADC_CR2_ADON); + tickstart = HAL_GetTick(); + while ((ADC1->CR2 & ADC_CR2_ADON) != 0U) + { + if ((HAL_GetTick() - tickstart) > ADC1_CAL_TIMEOUT_MS) + { + return -1; + } + } + + /* 2) 上电(ADON 0→1),等待稳定后再启动校准 */ + SET_BIT(ADC1->CR2, ADC_CR2_ADON); + { + __IO uint32_t wait_loop_index = (ADC1_STAB_DELAY_US * (SystemCoreClock / 1000000U)); + while (wait_loop_index != 0U) + { + wait_loop_index--; + } + } + + /* 3) 启动自校准,等硬件把 CAL 清零 */ + SET_BIT(ADC1->CR2, ADC1_CAL_BIT); + tickstart = HAL_GetTick(); + while ((ADC1->CR2 & ADC1_CAL_BIT) != 0U) + { + if ((HAL_GetTick() - tickstart) > ADC1_CAL_TIMEOUT_MS) + { + return -1; + } + } + return 0; +} /* USER CODE END 1 */ diff --git a/Src/freertos.c b/Src/freertos.c index 5877bdf0..b0d0d0d1 100644 --- a/Src/freertos.c +++ b/Src/freertos.c @@ -125,13 +125,18 @@ const osThreadAttr_t timeSyncTask_attributes = { .stack_size = 512 * 4, .priority = (osPriority_t) osPriorityNormal, }; -/* B1(2026-10-10):ADC 采集消费者(1ms 采样节拍 + 原子快照 + 帧统计)。 - * 手写于此而非 .ioc(同 timeSyncTask:不在 Tasks01 里 → 重生成会丢,靠审计 WARN 提示)。 */ +/* B1(2026-10-10):ADC 采集消费者(TIM6 1 kHz 采样节拍 + 原子快照 + 帧统计)。 + * 手写于此而非 .ioc(同 timeSyncTask:不在 Tasks01 里 → 重生成会丢,靠审计 WARN 提示)。 + * ⚠ 优先级 AboveNormal(Task #32 P1-5 方案 a,2026-10-10):消费改由 TIM6 ISR 事件驱动后 + * 提优先级,使"被唤醒 → 立即抢占其余 Normal 任务",唤醒延迟固定为 µs 级。 + * 代价:本任务单拍最坏实测 adcm≈2.1 ms 会在 AboveNormal 上挤占 net/ftp/default/timeSync, + * 其中 ADC 的 2 ms 忙等(App/algo/adc_input.c)尤其敏感 → 硬化方向见 + * docs/子系统设计/ADC采样节拍设计说明.md §10.4。 */ osThreadId_t adcTaskHandle; const osThreadAttr_t adcTask_attributes = { .name = "adcTask", .stack_size = 512 * 4, - .priority = (osPriority_t) osPriorityNormal, + .priority = (osPriority_t) osPriorityAboveNormal, }; /* USER CODE END Variables */ /* Definitions for defaultTask */ diff --git a/Src/main.c b/Src/main.c index 3881ad93..feb9b3f3 100644 --- a/Src/main.c +++ b/Src/main.c @@ -42,6 +42,7 @@ #include "app_main.h" #include "sys_clock.h" #include "task_watchdog.h" /* task_wd_tick_probe():round 8b 时间源存活性取证 */ +#include "adc_task.h" /* adc_tick_irq_notify():TIM6 节拍 → 唤醒 adcTask(P1-5 方案 a) */ /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ @@ -246,13 +247,18 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) task_wd_tick_probe(); } - /* 1 kHz 采样节拍:TIM6 更新中断内脉冲 CONVST(仅两次 GPIO 写,亚 µs)。 + /* 1 kHz 采样节拍:TIM6 更新中断内脉冲 CONVST(仅两次 GPIO 写,亚 µs), + * 再唤醒 adcTask 消费这一拍(Task #32 P1-5 方案 a:ISR 只发唤醒,计算全留在任务)。 * TIM6 与 BUSY EXTI(EXTI9_5_IRQn) 同为抢占优先级 6 → 互不抢占, * 读 RD 时序不会被下一次 CONVST 脉冲打断。详见 - * docs/子系统设计/ADC采样节拍设计说明.md。放 USER CODE 区(L2)。 */ + * docs/子系统设计/ADC采样节拍设计说明.md §10。放 USER CODE 区(L2)。 + * ⚠ 唤醒必须走 adc_tick_irq_notify(),**不要**在这里直接 osThreadFlagsSet: + * TIM6 在 app_main_init() 就启动,远早于建任务与 osKernelStart, + * "消费者就绪"门在 adc_task.c 里(同本文件 TIM7 分支 2026-10-02 的实测坑)。 */ if (htim->Instance == TIM6) { tpafe5160_start_conv_irq(); + adc_tick_irq_notify(); } /* USER CODE END Callback 1 */ } diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 51e46d18..3ef59067 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| 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%** | 各区的容量与常量来源: diff --git a/docs/子系统设计/ADC采样节拍设计说明.md b/docs/子系统设计/ADC采样节拍设计说明.md index 4be21fe8..d34ffeb8 100644 --- a/docs/子系统设计/ADC采样节拍设计说明.md +++ b/docs/子系统设计/ADC采样节拍设计说明.md @@ -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 对还不存在的消费者动手"。 diff --git a/docs/硬件平台/ADC通道映射说明.md b/docs/硬件平台/ADC通道映射说明.md index 166b385b..39c7eff3 100644 --- a/docs/硬件平台/ADC通道映射说明.md +++ b/docs/硬件平台/ADC通道映射说明.md @@ -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` 现只在自检帧通过后才打印 → 判据在本板成立、非空转 | diff --git a/docs/问题记录/UI任务看门狗停摆分析.md b/docs/问题记录/UI任务看门狗停摆分析.md index f8d25d63..fd46cd97 100644 --- a/docs/问题记录/UI任务看门狗停摆分析.md +++ b/docs/问题记录/UI任务看门狗停摆分析.md @@ -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 忙等与"超时沿用旧值"**仍未处理** | diff --git a/scripts/check_cubemx_survival.py b/scripts/check_cubemx_survival.py index e9a8dd7b..3db178a2 100644 --- a/scripts/check_cubemx_survival.py +++ b/scripts/check_cubemx_survival.py @@ -285,6 +285,28 @@ CHECKS = [ # 注:ADC 时钟分频**不设防护**(2026-10-09 决定)。CubeMX 面板里 DIV2 已被约束禁用 # (FadcConstraint Max=36000000;APB2=84 MHz → DIV2=42 MHz 超限,选不到), # .ioc 虽不保存 ADC1.ClockPrescaler 键,重生成也只会回落到约束允许的值。 + # ADC1 上电自校准(2026-10-10 补):RM0090 §13.3.6 的**必需**步骤,CubeMX **不生成**, + # 且本仓库裁剪版 HAL **没有** HAL_ADCEx_Calibration_Start → 只能手写(L2), + # 故必须在此兜底。丢了它不报编译错,只让 ADC 带失调误差(难从日志看出)。 + dict( + id="adc1-calibration-call", + kind="protected", + path="Src/adc.c", + region="ADC1_Init 2", + must_contain=["adc1_calibrate("], + hint="MX_ADC1_Init() 末尾必须调用 adc1_calibrate()(F4 ADC 上电自校准,CubeMX 不生成)。" + "丢了它 = ADC 未校准 → 存在失调误差,且不会报任何编译/运行错误。", + ), + dict( + id="adc1-calibration-impl", + kind="protected", + path="Src/adc.c", + region="1", + must_contain=["int adc1_calibrate(void)", "ADC1_CAL_BIT", "ADC_CR2_ADON"], + hint="自校准实现(清 ADON→等生效→置 ADON→置 CAL→等 CAL 由硬件清零)须在 " + "`USER CODE BEGIN 1` 区。本仓库 HAL 无 HAL_ADCEx_Calibration_Start、" + "CMSIS 头无 ADC_CR2_CAL,两者都是裁剪造成的,勿以为『标准 HAL 里有』。", + ), ] diff --git a/scripts/gen_task_table.py b/scripts/gen_task_table.py index 280de591..1221a8a8 100644 --- a/scripts/gen_task_table.py +++ b/scripts/gen_task_table.py @@ -75,7 +75,7 @@ ROLE = { "testTask": "硬件测试入口", "uiTask": "LVGL UI 界面", "timeSyncTask": "墙钟时间同步", - "adcTask": "TPAFE5160 采样(1ms 节拍 + 原子快照)", + "adcTask": "TPAFE5160 采样(TIM6 1kHz 事件驱动 + 原子快照)", }