Files
STM32F4-Base/App/task/adc_task.c
T
wandering1 60a650c175 fix(adc_task): 接上从未被调用的任务初始化入口(含判定层参照系)
问题:adc_task_func 函数头写「限定条件:adc_task_init() 已调用」,但
adc_task_init() 全库无调用点;同类空壳 net_task_init()(体内只有一条
DBG_INFO,函数头却写「创建 TCP Server 监听 Socket」)同样无人调用。

判定层为什么今天还没坏:状态全在 .bss(复位即 0),且 adc_health 的
首拍只建基线、不判定,所以漏调 init 只是「少清一次」。但假契约会让
下一个接手者照它推理,故必须修在成灾之前。

改动:
- adc_task_func 进入循环之前(开闸 s_consumer_ready 之前)自己调用
  adc_task_init(),不依赖外部接线;init 与首拍之间无抢占窗口
- adc_task_init() 去掉 DBG_INFO 并在函数头标注禁止加日志:它落在
  adcTask 静态最坏调用链上,每条 DBG_* 约 216 B 栈,而这条启动行
  无诊断价值(存活由首帧日志与 TASK_WD_ADC 心跳证明)
- 删除 net_task.c/.h 里的空壳 net_task_init()(声明 + 定义 + 误述)
- docs/算法/采样有效性校验设计说明.md §14 与变更记录同步改写:
  原来登记的「init 不在产品路径」事实已修复,不留过时结论

实测(任务工作树 adc-health,构建 + 静态分析):
- @build 0 错 0 警;Code 213356 → 213424(+68 B)
- RW-data 1184 / ZI-data 131480 逐字节不变 ⇒ RAM 零增量
- stack_report:adcTask 静态最坏链 1320 B → 1320 B 不变,
  余 1240 B / 48% 仍 PASS(netTask 1912 B、protectTask 872 B 不变)
- ADC 有效性单测 31/31 通过(钩子按 adc_task.c 自动触发)
- 字节卫生自查:BOM 无 / CR 0 / U+FFFD 0,字符串字面量纯 ASCII
  (中文只在注释里),新增行无非 ASCII 字面量

gate: only mem-budget/mem-numbers pending lead ledger backfill
pre-commit run --all-files = 33 Passed / 2 Failed,失败仅这两条,
均为台账受管区与本树 .map 的 Flash 差异(233,120 → 233,188,即本次
+68 B);RW_IRAM1 75,240 与 RW_CCM 57,376 与台账完全一致。按并行
分支约定本分支不跑 --write,台账由 Lead 合并后统一回填。

另记一处门禁缺口(非本次引入):ui-golden 的黄金参考 simulator/
tests/ref/ 被 .gitignore 排除、从未入库,新工作树里必然失败;本树
用 check_content_pages.py --update 生成 11 页基线后 ui-golden Passed
(即本次改动对 UI 像素中性)。是否让基线入库待裁。
2026-10-22 00:43:31 +08:00

458 lines
28 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:ADC Sampling Task
* 模块功能:ADC 采集消费 + L2 相量计算——TPAFE5160 快照 + STM32 ADC1 DMA 乒乓帧消费
* (经 L1 层 App/algo/adc_input.c 按语义取数)→ 压入采样环
* (App/algo/sample_ring.c)→ 全周傅氏相量重算(App/algo/phasor.c)
* + 帧统计(节拍由 TIM3 硬件定时器提供)
* 状态说明:**B1 已落地(2026-10-10)**——本任务从空桩转为真实消费者:
* 用 tpafe5160_snapshot() 原子取数(AGENTS 铁律),统计帧数/无新帧次数,
* 并纳入看门狗 required_mask(TASK_WD_ADC)。
* **采样节拍**:由 **TIM3 更新中断(1 kHz)** 硬件触发——
* ISR 内调 tpafe5160_start_conv_irq()(Src/main.c 的
* HAL_TIM_PeriodElapsedCallback);本任务**不再自行触发**,退化为**纯消费者**。
* **消费唤醒**:由 1 ms `osDelayUntil`
* 轮询改为 **TIM3 ISR 置位 `ADC_WAKE_FLAG` 的事件驱动**——唤醒与生产同源锁定,
* 消除 0~1 ms 的 tick 量化相位抖动与"超拍永久落后 → 空转"风险;本任务优先级
* 同步由 Normal 提到 **AboveNormal**(唤醒即抢占)。详见
* docs/子系统设计/ADC采样节拍设计说明.md §6。
* **L1 采集层**:取数改走 App/algo/adc_input.c——
* 本任务每拍调 adc_input_poll()(TPAFE 快照 + STM32 ADC1 9 通道扫描),
* 经 adc_input_raw() **按语义**读通道(L1-3/L1-4),不再直接触碰驱动缓冲。
* V0.1 上只有 TPAFE 三路(UA/UN/IA)测量有效;本任务只取数,不做保护运算。
* 接入前必读:取数只用 adc_input_raw() / tpafe5160_snapshot()(临界区原子快照,
* 仅任务上下文;旧的 get_buf/data_ready/clear_ready 已删除);接消费者前的
* 强制门禁见根 AGENTS.md「接 ADC / RS-485 消费者前的强制门禁」+ 建任务前后
* 各跑 scripts/stack_report.py 做栈预算。
* 适用平台:STM32F407ZGTx + TPAFE5160
* 作者:王建锋
* 创建日期:2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
* v1.1 2026-10-05 王建锋 P3-11:注明"预留桩(未启用)",避免误认为已实现
* v2.0 2026-10-10 王建锋 B1:实现真实消费者(1ms 节拍 + 原子快照 + 帧统计 +
* 看门狗心跳),由 Src/freertos.c 创建为 adcTask
* v2.1 2026-10-10 王建锋 日志门控:首帧/周期统计仅在 dbg_console_writer_ready()
* 为真后落日志。写者未注册时控制台走无锁直发,与
* defaultTask 的存储初始化日志字节级交错(台架实测);
* 就绪前挂起上报,就绪后补报最近一帧。
* v2.2 2026-10-10 王建锋 采样节拍改由 TIM3 更新中断(1 kHz)硬件触发;本任务
* 删除自行触发,退化为纯消费者(见文件头"采样节拍")。
* v2.3 2026-10-10 王建锋 L1-3/L1-4:取数改走 App/algo/adc_input.c(语义映射 +
* STM32 ADC1 9 通道扫描);首帧日志补全 8 路电气量 + 温度,
* 心跳日志增 adc1_rc / adc1_err 观测 STM32 侧健康度。
* v2.4 2026-10-10 王建锋 L2:接入相量层——每拍把 9 路语义值压入采样环
* (sample_ring)并重算全部相量实例(phasor_run);
* 攒满一周波后**首拍即落一次相量日志**(开机证据,不等
* 10 s 心跳),心跳日志增 ring / skip 计数与各实例相量
* (Re/Im/有效值,×0.1 定点打印,因 MicroLIB 的 snprintf
* 无 %f)。
* ⚠ L1-5 通道标定未实现(缺标准源)→ 暂以**原始码值**
* 充作工程量(系数 = 1),故相量读数只证明**链路通**,
* 不可作幅值精度判据(同 L1-3 的替代口径)。
* v2.5 2026-10-10 王建锋 常开耗时测量埋点:单拍加探针
* (task_prof ADC_BEAT),用于判定本任务是否超拍
* (名义节拍 1 ms = 168000 周期)而挤占低优先级任务
* v2.6 2026-10-10 王建锋 两处正确性修复:
* 无新帧仍压环 → 改为**仅 r == 1 时**压环 + 重算
* (消除重复采样污染 20 点 DFT 窗的系统性误差);
* osDelayUntil 返回值被丢弃 → 落后时一次性重同步
* (原写法会永久落后 → 不再阻塞 → 空转抢占 uiTask)
* v2.7 2026-10-10 王建锋 事件驱动消费改造:
* 消费唤醒 osDelayUntil(1ms) → osThreadFlagsWait(ADC_WAKE_FLAG,
* osWaitForever),标志由 TIM3 ISR 经 adc_tick_irq_notify() 置位;
* 删除 ADC_TASK_BEAT_MS 与 next(上一轮加的重同步钩子因此不再需要);
* 新增"消费者就绪"门 s_consumer_ready——TIM3 在 app_main_init()
* 就启动,**远早于** MX_FREERTOS_Init()/osKernelStart(),
* ISR 不得在此之前对内核动手(同 Src/main.c 里 TIM7 的
* 2026-10-02 实测坑)。优先级 Normal → AboveNormal(见 freertos.c)
* v2.8 2026-10-10 王建锋 节拍源换 TIM3(F4 上 ADC1 收不到 TIM6/TIM7 的
* TRGO,换源判据见 docs/子系统设计/ADC采样节拍设计说明.md
* §3.2);STM32 侧改 DMA 双缓冲
* 消费,adc_input_poll() 不再忙等;心跳日志增
* adc1_seq / adc1_hw(adc1_err 语义改为"本拍无新帧")
* v2.9 2026-10-11 王建锋 L1-6 采样有效性校验:每拍调 adc_health_check()
* (App/algo/adc_health.c,纯逻辑判定:静默/丢帧/
* 单调性/错误计数增量)——严重事件与故障电平上升沿
* 打 DBG_ERROR + 每类每次开机一次 SYSLOG_SELFTEST;
* 恢复打 DBG_INFO;心跳日志增 tgap(TPAFE 侧丢帧,
* 与 adc1_gap 对称)。判定不在本文件(A-4:算法与
* I/O 分离),本文件只做"消费计数 → 告警"的编排
* v3.0 2026-10-21 王建锋 修复"任务初始化入口从未被调用":adc_task_func 进入
* 循环之前先调 adc_task_init()。原实现下判定层状态靠
* .bss 清零 + 首拍建基线兜住,功能没错,但函数头写的
* "限定条件:adc_task_init() 已调用"是假的,会误导后来
* 者;该入口同时不再打日志——它落在本任务静态最坏链上,
* 每条 DBG_* 约 216 B 栈,而这一行毫无诊断价值(本任务
* 存活由首帧日志与 TASK_WD_ADC 心跳证明)
*/
#include "adc_task.h"
#include "cmsis_os.h"
#include "adc_input.h" /* L1 采集层:adc_input_poll()/adc_input_raw()(按语义取数) */
#include "sample_ring.h" /* L2 输入:sample_ring_push_frame()(采样环缓冲) */
#include "phasor.h" /* L2 相量层:phasor_run()/phasor_get()(全周傅氏) */
#include "freq.h" /* L2 派生量:freq_run()/freq_get()(测频,B8) */
#include "adc_health.h" /* L1-6:采样有效性校验(静默/丢帧/单调性/错误增量) */
#include "tpafe5160.h"
#include "task_watchdog.h"
#include "console_log.h"
#include "sys_log.h" /* SYSLOG_SELFTEST:采样故障留痕(仅故障才写) */
#include "task_prof.h" /* 常开耗时探针:单拍耗时 */
#define DBG_TAG "[ADC_TASK]"
#include "dbg_log.h"
/* 心跳日志周期(ms):周期性上报帧统计,作为采集链路存活的观测面。
* (原 ADC_TASK_BEAT_MS = 1U 的消费轮询周期已随事件驱动改造删除——
* 消费节拍改由 TIM3 ISR 置位 ADC_WAKE_FLAG 提供,与生产同源,不再自定周期) */
#define ADC_TASK_REPORT_MS 10000U
/* 统计:已消费帧数 / 无新帧次数(首拍必为无新帧,之后应稳定为 0 增量)。
* 原始码值缓冲由 L1 层 App/algo/adc_input.c 持有(消费侧按语义取数,不写死下标) */
static uint32_t s_frames_consumed;
static uint32_t s_frames_idle;
/* 因通道尚未首次成功而**整拍跳过入环**的次数(不向相量窗注入 0 点) */
static uint32_t s_frames_skip;
/* L1-6 告警编排状态:
* s_health_prev - 上次电平故障位(判上升/下降沿,避免每拍重复告警)
* s_health_logged- 已写 SYSLOG_SELFTEST 的位(每类每次开机只留痕一次,
* 防持续故障刷屏把 32KB selftest.log 写满) */
static uint32_t s_health_prev;
static uint32_t s_health_logged;
/* adcTask 的内核句柄,定义在 Src/freertos.c(引用方式同 App/task/default_task.c) */
extern osThreadId_t adcTaskHandle;
/* 消费者就绪门:由 adcTask 自己在进入等待前置 1;单写者 = 本任务,ISR 只读。
* ⚠ 为什么必须有这道门:TIM3 在 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;
/*
* 函数功能:TIM3 采样节拍的 ISR 入口——置位唤醒 adcTask 消费一拍
* 入口参数:无
* 返 回 值:无
* 限定条件:仅 ISR 上下文。TIM3 抢占优先级 = 6 ≥ configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
* (=5) → osThreadFlagsSet 走 xTaskNotifyFromISR + portYIELD_FROM_ISR,合法且立即切换
* 函数说明:ISR **只发唤醒**,全部计算留在任务里——"ISR 内计算"是反模式:取数与相量
* 计算都读写任务级共享数据,且本回调属 CubeMX 生成区,重生成即丢。线程标志粘滞 → 单拍偶超 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" 周期心跳)
* 返 回 值:无
* 限定条件:仅任务上下文;调用方须保证 dbg_console_writer_ready() 为真
* 函数说明:MicroLIB 的 snprintf **不支持 %f** → 以 ×0.1 定点整数打印
* (打印值 = 实际值 × 10,分辨率 0.1)。逐实例数据驱动输出,
* 实例表加行即自动覆盖。⚠ L1-5 标定未实现 → 读数单位是**原始码值**。
*/
static void log_phasors(const char *tag) {
uint8_t k;
for (k = 0u; k < phasor_inst_count(); k++) {
const phasor_inst_t *d = phasor_inst(k);
phasor_t p;
if ((d != NULL) && (phasor_get(k, &p) == 0)) {
DBG_INFO("phasor(%s) %s: Re=%ld Im=%ld rms=%ld (x0.1)",
tag, d->name,
(long)(p.fRe * 10.0f),
(long)(p.fIm * 10.0f),
(long)(p.fAmSQ * 10.0f));
}
}
/* 测频行(B8,L3-5 测量侧台架可见性):同样 x0.01 定点(分辨率 0.01 Hz)。
* valid=0 = 未启动收敛或幅值门失效(浮空输入 rms≈0),此时 f 为保持值
* (从未收敛则 0)——判读:正常带波形应 valid=1 且 f≈5000。 */
{
float f = 0.0f;
(void)freq_get(&f);
DBG_INFO("freq(%s): f=%ld valid=%u (x0.01)",
tag, (long)(f * 100.0f), (unsigned)freq_valid());
}
}
/*
* 函数功能:L1-6 判定结果的告警编排(边沿 → 日志 + 自检留痕)
* 入口参数:hs - adc_health_check() 判定结果快照
* 返 回 值:无
* 限定条件:仅任务上下文;日志仍受写者就绪门控(SYSLOG 不受——内核前有
* s_preboot 缓冲,故障痕迹不因开机窗口丢失)
* 函数说明:策略分级(判定本身不在本文件,见 App/algo/adc_health.c)——
* ① 严重事件(FRSTDATA / HAL 错误 / STM32 停摆计数增量):这三类
* 计数增量恒应为 0,出现即故障 → 每次 DBG_ERROR;
* ② 电平故障上升沿(静默/序号倒退)→ DBG_ERROR;下降沿(静默自愈)
* → DBG_INFO("恢复"必须可见,否则只看到一次错误无从判断是否还在);
* ③ SYSLOG_SELFTEST 每类每次开机**只留痕一次**(持续故障刷屏会把
* 32KB selftest.log 写满,也淹没其它模块的故障记录);
* ④ gap(丢帧)**不告警**:消费者超拍是已知现象(心跳日志
* adc1_gap / tgap 即观测面),告警会淹没真故障。
*/
static void adc_health_report(const adc_health_status_t *hs) {
uint32_t serious = hs->events & (ADC_HE_FRSTDATA | ADC_HE_STM32_ERR | ADC_HE_HW_ERR);
uint32_t rise = hs->flags & ~s_health_prev;
uint32_t fall = s_health_prev & ~hs->flags;
if ((serious != 0u) && (dbg_console_writer_ready() != 0u)) {
DBG_ERROR("sampling fault: events=0x%lx sticky=0x%lx",
(unsigned long)hs->events, (unsigned long)hs->sticky);
}
if ((rise != 0u) && (dbg_console_writer_ready() != 0u)) {
DBG_ERROR("health: flags=0x%lx stall_ms=%lu tgap=%lu",
(unsigned long)hs->flags,
(unsigned long)hs->tpafe_stall_ms,
(unsigned long)hs->tpafe_gaps);
}
if (((serious | rise) & ~s_health_logged) != 0u) {
(void)syslog_post(SYSLOG_SELFTEST, "adc sampling fault");
s_health_logged |= (serious | rise);
}
if ((fall != 0u) && (dbg_console_writer_ready() != 0u)) {
DBG_INFO("health: recovered flags=0x%lx sticky=0x%lx",
(unsigned long)hs->flags, (unsigned long)hs->sticky);
}
s_health_prev = hs->flags;
}
/*
* 函数功能:ADC 任务初始化
* 入口参数:无
* 返 回 值:0 - 成功,-1 - 失败
* 限定条件:tpafe5160_init() 已在 app_main_init()(内核启动前)完成
* 函数说明:驱动初始化不在本任务(见限定条件);此处清帧统计 + 清判定层参照系
* (adc_health_init())。由 adc_task_func 进入循环之前调用一次;幂等,
* 可重复调用(运行期要重建判定参照系,走的也是这里)。
* ⚠ 本函数内禁止加日志:它在 adcTask 的静态最坏调用链上,每条 DBG_*
* 约 216 B 栈,而本任务栈余量按 1024 B ISR 底线核算。
*/
int adc_task_init(void) {
s_frames_consumed = 0u;
s_frames_idle = 0u;
s_frames_skip = 0u;
adc_health_init();
return 0;
}
/*
* 函数功能:ADC 任务主函数
* 入口参数:arg - FreeRTOS 传入参数(未使用)
* 返 回 值:无
* 限定条件:内核已启动。本函数自己在进入循环前调 adc_task_init()——不依赖外部
* 调用(历史上该入口全库无调用点,判定层状态只靠 .bss 清零兜住)
* 函数说明:每拍采集一次(adc_input_poll():TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 乒乓缓冲)
* 并统计。采样节拍由 TIM3 硬件定时器提供(ISR 内触发 CONVST),本任务不再触发;
* **本任务的唤醒同样由该 TIM3 ISR 置位 ADC_WAKE_FLAG 驱动**(事件驱动,
* 不再自定 1 ms 周期)。稳态下 consumed 每拍 +1、idle 不再增长;idle 持续增长 =
* EXTI 未在读帧或 TIM3 未启动(异常),由周期心跳日志暴露。
* ⚠ 若 TIM3 停摆,本任务会**永久阻塞**在 osThreadFlagsWait → TASK_WD_ADC 缺席 →
* IWDG 复位。这是刻意的 fail-safe("节拍源缺失"不被静默放过),不是缺陷。
*/
void adc_task_func(void *arg) {
uint32_t report_at = osKernelGetTickCount() + ADC_TASK_REPORT_MS;
uint8_t first_logged = 0u;
uint8_t first_phasor_logged = 0u;
(void)arg;
/* 任务自有状态清零(含判定层参照系)。放在开闸之前:此刻 ISR 还不会唤醒
* 本任务,init 与首拍之间不存在"用半初始化状态做判定"的窗口 */
(void)adc_task_init();
/* 开闸:此刻任务已存在、调度器必已运行 → ISR 的唤醒从此生效。
* 放在进入循环之前:置位与等待之间不会漏唤醒(线程标志粘滞) */
s_consumer_ready = 1u;
for (;;) {
int r;
uint32_t now = osKernelGetTickCount(); /* 本拍时基:健康判定 + 心跳共用一份 */
/* 探针:单拍起点。名义节拍 1 ms = 168000 周期;若实测**持续**超过,
* 本任务(osPriorityAboveNormal)会持续抢占 uiTask
* (osPriorityBelowNormal),是"UI 心跳缺席"的候选占用者之一 */
task_prof_enter(TASK_PROF_ADC_BEAT);
/* 1) 采集一拍:L1 层 adc_input_poll() = TPAFE 原子快照 + 消费一帧 STM32 ADC1
* DMA 双缓冲(仅任务上下文;TPAFE 侧 1=有新帧 0=无新帧 -1=参数错)。
* 采样节拍由 TIM3 更新事件提供(ISR 内触发 CONVST + TRGO 触发 ADC1),
* 本任务既不触发也不等待转换。
* 探针:本段单独计时。本段**零忙等**(转换由 TIM3 TRGO 触发、DMA 搬运,
* 不再有逐通道轮询超时),只剩 TPAFE 快照临界区 + 拷 9 个半字 → us 量级。
* ⚠ adcm(整拍)的 ~2.1 ms 峰值来自每 10 s 一次的心跳日志,不是本段,勿混读 */
task_prof_enter(TASK_PROF_ADC_POLL);
r = adc_input_poll();
task_prof_exit(TASK_PROF_ADC_POLL);
if (r == 1) {
s_frames_consumed++;
} else if (r == 0) {
s_frames_idle++;
} else {
/* r == -1:参数错。调用点 count 为常量,不可达;不计入 idle 以区分异常 */
}
/* 2) L2 输入 + 相量重算:把本拍 9 路语义值转浮点帧压入采样环,再重算全部
* 相量实例(L2-6 滑动窗:每拍以"当前时刻为终止的前一周波"整窗重算)。
*
* ⚠ **仅在本拍确有新帧(r == 1)时**才压环。
* 原实现在 r == 0(TPAFE 无新帧)时**仍然**压环 + phasor_run(),只有 r == -1
* 才跳过 —— 于是把**上一拍的重复采样值**塞进 20 点 DFT 窗,等效采样间隔被拉长。
* 量级参考(台架曾测得 idle 占 3.2%):窗长从 20.0 ms 漂到约 20.6 ms → 对 50 Hz
* 相当于约 3% 频率失配 → 幅值/相位**系统性**误差(不是随机噪声)。
* 回归判据 = 恒等式 `ring == frames`(只有真新帧才入环)。
*
* 任一通道读不到(尚未首次成功)→ 本拍**整帧跳过**:不向窗内注入 0 点
* (单个 0 点会显著污染 20 点窗),并计入 skip 观测。 */
if (r == 1) {
float frame[ADC_SEM_COUNT];
uint8_t s;
uint8_t all_ok = 1u;
for (s = 0u; s < (uint8_t)ADC_SEM_COUNT; s++) {
int32_t code = 0;
if (adc_input_raw((adc_semantic_t)s, &code) != 0) {
all_ok = 0u;
break;
}
/* ⚠ L1-5 通道标定尚未实现(缺标准源)→ 暂以**原始码值**充作工程量
* (系数 = 1)。标定落地后此处改为 工程量 = (code + b) × k(定点 2^10)。 */
frame[s] = (float)code;
}
if (all_ok != 0u) {
(void)sample_ring_push_frame(frame, (uint8_t)ADC_SEM_COUNT);
phasor_run();
/* L2 派生量测频(B8):同拍紧随 phasor_run——本拍使用区已
* 切换,freq 读的就是本拍相量(1 ms 同上下文,无竞态) */
freq_run();
} else {
s_frames_skip++;
}
}
/* 2b) L1-6 采样有效性校验(每拍,常数次整数运算):把驱动侧观测计数交给
* 纯逻辑判定层,本任务只做告警编排。⚠ 计数一律取**本拍 poll 之后**
* 的值——与刚消费的帧同一时点,避免"下一拍的增量被提前看到"。 */
{
adc_health_in_t h_in;
adc_health_status_t h_st;
h_in.tpafe_seq = tpafe5160_frame_seq();
h_in.tpafe_frst_err = (uint32_t)tpafe5160_frstdata_err();
h_in.stm32_seq = adc_input_stm32_seq();
h_in.stm32_errs = adc_input_stm32_errs();
h_in.stm32_gaps = adc_input_stm32_gaps();
h_in.stm32_hw_errs = adc_input_stm32_hw_errs();
h_in.now_ms = now;
adc_health_check(&h_in, &h_st);
adc_health_report(&h_st);
}
/* 3) 首帧码值上报:**写者就绪后**才落日志。写者未注册时 con_append 走
* 无锁直发路径,与 defaultTask 的存储初始化日志会字节级交错
* (2026-10-10 台架实测);此时挂起上报,就绪后补报最近一帧。
* 8 路电气量 + 温度全部经 adc_input_raw() **按语义**取数(L1-4),
* 用于验证"语义 → 物理通道"映射与两片转换器取数契约 */
if ((first_logged == 0u) && (s_frames_consumed != 0u) &&
(dbg_console_writer_ready() != 0u)) {
int32_t ua = 0, ub = 0, uc = 0, un = 0;
int32_t ia = 0, ib = 0, ic = 0, in = 0, temp = 0;
(void)adc_input_raw(ADC_SEM_UA, &ua);
(void)adc_input_raw(ADC_SEM_UB, &ub);
(void)adc_input_raw(ADC_SEM_UC, &uc);
(void)adc_input_raw(ADC_SEM_UN, &un);
(void)adc_input_raw(ADC_SEM_IA, &ia);
(void)adc_input_raw(ADC_SEM_IB, &ib);
(void)adc_input_raw(ADC_SEM_IC, &ic);
(void)adc_input_raw(ADC_SEM_IN, &in);
(void)adc_input_raw(ADC_SEM_TEMP, &temp);
first_logged = 1u;
DBG_INFO("first: UA=%ld UB=%ld UC=%ld UN=%ld IA=%ld IB=%ld IC=%ld IN=%ld T=%ld",
(long)ua, (long)ub, (long)uc, (long)un,
(long)ia, (long)ib, (long)ic, (long)in, (long)temp);
}
/* 4) 首拍相量上报:攒满一周波(20 ms)后立即落一次,作为 L2 链路的开机证据。
* 不依赖 10 s 心跳 → 台架 @test 的 settle 窗口内即可见(否则会被静默判据
* 截断,见 docs/测试说明/自动化测试流程.md 的 settle 口径)。 */
if ((first_phasor_logged == 0u) && (dbg_console_writer_ready() != 0u)) {
phasor_t p;
if (phasor_get(0u, &p) == 0) {
first_phasor_logged = 1u;
log_phasors("first");
}
}
/* 5) 看门狗心跳(本任务已纳入 required_mask) */
task_wd_beat(TASK_WD_ADC);
/* 6) 周期心跳日志:帧统计 + 驱动侧 FRSTDATA 错误计数 + L2 相量。
* 写者就绪前抑制(同 3) 的交错理由);到点无论是否落日志都推进下个上报点 */
{
if ((int32_t)(now - report_at) >= 0) {
if (dbg_console_writer_ready() != 0u) {
/* STM32 侧观测量(DMA 版),四类各有独立成因:
* adc1_rc = 最近一拍取帧状态(0 OK / -2 本拍无新帧)
* adc1_seq = DMA 整帧完成计数(正常时 ≈ seq,恒定小偏移)
* adc1_gap = 丢帧:seq 一次前进 >1 → 本任务超拍(单拍 > 1 ms)
* adc1_err = 停摆:连续 >=2 拍无新帧 → 节拍源/DMA 真停
* adc1_hw = HAL ADC/DMA 错误回调
* 判读:adc1_seq 停增而 seq 仍涨 → STM32 侧停摆;
* adc1_gap 增长 → 本任务耗时超拍(看 adcm/adcn);
* tgap = TPAFE 侧同类丢帧计数(L1-6,与 adc1_gap 对称) */
DBG_INFO("frames=%lu idle=%lu skip=%lu seq=%lu frst_err=%u ring=%lu"
" adc1_rc=%d adc1_err=%lu adc1_seq=%lu adc1_gap=%lu adc1_hw=%lu"
" tgap=%lu",
(unsigned long)s_frames_consumed,
(unsigned long)s_frames_idle,
(unsigned long)s_frames_skip,
(unsigned long)tpafe5160_frame_seq(),
(unsigned)tpafe5160_frstdata_err(),
(unsigned long)sample_ring_frames(),
adc_input_stm32_rc(),
(unsigned long)adc_input_stm32_errs(),
(unsigned long)adc_input_stm32_seq(),
(unsigned long)adc_input_stm32_gaps(),
(unsigned long)adc_input_stm32_hw_errs(),
(unsigned long)adc_health_tpafe_gaps());
log_phasors("hb");
}
report_at = now + ADC_TASK_REPORT_MS;
}
}
/* 7) 等下一次节拍唤醒(事件驱动)
* 原实现是 osDelayUntil(next += 1ms):把消费唤醒**钉在 FreeRTOS tick(1 ms)边界**
* → 与 TIM3 生产之间有 0~1 ms 随机相位抖动(日志里的 idle 正是这么来的),
* 且一旦超拍,next 就永久落后 → 任务不再阻塞 → 空转抢占低优先级 uiTask
* (空转抢占低优先级任务正是这么发生的)。现在唤醒与生产**同源**:TIM3 ISR 触发 CONVST 后置位
* ADC_WAKE_FLAG,本任务被唤醒的延迟 = ISR + 调度(µs 级;本任务已提至
* AboveNormal → 立即抢占),无 tick 量化、无累积漂移,也就不再需要重同步钩子。
* 标志是命令位(等待即消费)→ osFlagsWaitAny,不加 osFlagsNoClear;
* osWaitForever 无超时 = 刻意的 fail-safe(TIM3 停摆 → 无心跳 → IWDG 复位)。 */
task_prof_exit(TASK_PROF_ADC_BEAT); /* 探针:单拍耗时结算(不含等待) */
(void)osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever);
}
}