Files
STM32F4-Base/App/task/adc_task.c
T
wandering1 593c5d0c84 refactor(编号): 采集链分层 L1/L1.5/L2/L2.5/L3/L4/L5 → 语义名(批次 1)
用户裁定(2026-10-28):L1/L1.5/L2/L2.5/L3 这套层号「谁能看懂」,改用语义名。
映射(同一批定稿,含 L4/L5):
  L1   → 语义取数与有效性
  L1.5 → 标定折算
  L2   → 全周傅氏相量
  L2.5 → 测频与遥测快照
  L3   → 保护判据
  L4   → 出口与记录
  L5   → 动作留痕

实测规模:384 站点 / 62 文件(App/algo、App/task、test/host 六个套件、docs/算法、
产品需求、架构总览、启动顺序、ADC 采样节拍、IEC104 上行、采集短板评测、内存管理、
AGENTS.md、README.md、docs/README.md、.pre-commit-config.yaml 与 scripts 里的注释)。

改法(不是全局替换,逐模式有上下文规则):
- 先吃同位语再落裸词:L1 有效性校验 → 语义取数与有效性校验、L2 相量层 → 全周傅氏
  相量层、L3 保护 → 保护判据、L2.5 遥测快照 → 测频与遥测快照,避免「保护判据保护判据」
  这类重复(第一版就是这么坏掉的,已整体回滚重做)。
- .c/.h 只改注释:先把字符串字面量切出去(引号数为奇数的跨行注释按整行处理),
  C 字符串里的 L2-in ring 之类原样保留——AGENTS 铁律「字符串字面量必须纯 ASCII」。
- 层内条目号(L1-3 / L2-3 这类)**本批不动**,属批次 2;另外三族(UI 菜单层级
  L0~L3、链路延迟分段 L1~L8、CubeMX 防丢失三层)也不动,属批次 3。
- 顺手修的措辞:范围写法 L1~L3 → 「分层(语义取数与有效性 → 保护判据)」、
  CI 段「不含 L1/L2/L3」→「不含采集链分层」、目录地图里同一处并列表述。

验证:@build 0 错 0 警(Code=236456,与改前逐字节同尺寸——纯注释改动);
内存布局配图已按新脚本重生成(自检通过);doc-links --all --sections-strict 通过
(195 文档);mojibake / bom-guard / coding-standard / 新编号门禁自查 全 rc=0;
git diff --check 干净。
2026-10-28 13:04:02 +08:00

631 lines
40 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 采集消费 + 全周傅氏相量计算——TPAFE5160 快照 + STM32 ADC1 DMA 乒乓帧消费
* (经 语义取数与有效性层 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。
* **语义取数与有效性层**:取数改走 App/algo/adc_input.c——
* 本任务每拍调 adc_input_poll()(TPAFE 快照 + STM32 ADC1 9 通道扫描),
* 经 adc_input_raw() **按语义**读通道(STM32 ADC1 启动与扫描 / 通道语义映射),不再直接触碰驱动缓冲。
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
* 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 王建锋 注明"预留桩(未启用)",避免误认为已实现
* 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 王建锋 STM32 ADC1 启动与扫描 / 通道语义映射:取数改走 App/algo/adc_input.c(语义映射 +
* STM32 ADC1 9 通道扫描);首帧日志补全 8 路电气量 + 温度,
* 心跳日志增 adc1_rc / adc1_err 观测 STM32 侧健康度。
* v2.4 2026-10-10 王建锋 全周傅氏相量:接入相量层——每拍把 9 路语义值压入采样环
* (sample_ring)并重算全部相量实例(phasor_run);
* 攒满一周波后**首拍即落一次相量日志**(开机证据,不等
* 10 s 心跳),心跳日志增 ring / skip 计数与各实例相量
* (Re/Im/有效值,×0.1 定点打印,因 MicroLIB 的 snprintf
* 无 %f)。
* ⚠ 通道标定(一次系数 + 零漂)未实现(缺标准源)→ 暂以**原始码值**
* 充作工程量(系数 = 1),故相量读数只证明**链路通**,
* 不可作幅值精度判据(同 STM32 ADC1 启动与扫描 的替代口径)。
* 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 王建锋 采样有效性校验:每拍调 adc_health_check()
* (App/algo/adc_health.c,纯逻辑判定:静默/丢帧/
* 单调性/错误计数增量)——严重事件与故障电平上升沿
* 打 DBG_ERROR + 每类每次开机一次 SYSLOG_SELFTEST;
* 恢复打 DBG_INFO;心跳日志增 tgap(TPAFE 侧丢帧,
* 与 adc1_gap 对称)。判定不在本文件(算法与
* I/O 分离),本文件只做"消费计数 → 告警"的编排
* v3.0 2026-10-21 王建锋 修复"任务初始化入口从未被调用":adc_task_func 进入
* 循环之前先调 adc_task_init()。原实现下判定层状态靠
* .bss 清零 + 首拍建基线兜住,功能没错,但函数头写的
* "限定条件:adc_task_init() 已调用"是假的,会误导后来
* 者;该入口同时不再打日志——它落在本任务静态最坏链上,
* 每条 DBG_* 约 216 B 栈,而这一行毫无诊断价值(本任务
* 存活由首帧日志与 TASK_WD_ADC 心跳证明)
* v3.1 2026-10-25 DTU 项目组 标定折算折算 + 采样通道自检 贴轨观测(「数据可信」标定折算落地,道A):
* frame[s] 由 (float)code 改 adc_cal_apply()——工程量 =
* (code+b)×k,现口径 = ADC 引脚电压 V(eng=0,量程口径冻结的
* ROM 系数,docs/硬件平台/ADC通道映射说明.md §7.1);同拍
* 经 adc_health_rail_watch() 逐通道贴轨观察(采样通道自检 残余,
* 门限 adc_cal_rail_ths,码值域与标定正交),锁存边沿 →
* DBG_ERROR + 每开机一次 SYSLOG;心跳增 cal: 行(mV 定点
* + valid/clipped 位图 + rail 锁存),台架注入口径见该行判读
* v3.2 2026-10-25 DTU 项目组 测频与遥测快照发布(「数据可信」这一项,道D,M5):新模块
* App/algo/meas.c——本任务为唯一发布者,新帧拍内
* freq_run() 之后按 200 ms **tick 经过**分频调
* meas_tick()(全仓调用点 = 1,评审判据);品质注入源 =
* adc_cal_flags() 削顶位图,频率 = 同拍 freq 读数;心跳新增
* meas: 行(seq/v/ua·un·ia_mv/f/fv),读数比对类判据待台架实测
* v3.3 2026-10-27 DTU 项目组 已知电压注入验证台架读出口:心跳新增 adc: 行——连续输出 9 路
* **原始码值**(与 first: 行同序同格式)。动因 = 注入验证
* 原本只能靠"改电压 + 复位 + 抓一次 first:",8 个通道 × 3 个
* 注入点要复位二十余次;改为每 10 s 一行后可全程免复位。
* 不并进 cal: 行是因为 9 个码值会把该行推到 168 字符、
* 撞 DBG_CON_LINE_MAX(192) 的静默截断边界(见该行注释)。
* 无新增跨任务状态、无新增外设访问,栈 +36 B(局部码值数组)
*/
#include "adc_task.h"
#include "cmsis_os.h"
#include "task_handles.h" /* adcTaskHandle(原为本文件局部 extern) */
#include "adc_input.h" /* 语义取数与有效性层:adc_input_poll()/adc_input_raw()(按语义取数) */
#include "sample_ring.h" /* 全周傅氏相量输入:sample_ring_push_frame()(采样环缓冲) */
#include "phasor.h" /* 全周傅氏相量层:phasor_run()/phasor_get()(全周傅氏) */
#include "freq.h" /* 全周傅氏相量的派生量:freq_run()/freq_get()(测频,B8) */
#include "adc_health.h" /* 采样有效性校验(静默/丢帧/单调性/错误增量/贴轨) */
#include "adc_cal.h" /* 标定折算:通道标定折算(「数据可信」标定折算落地;flags 供 cal 心跳行) */
#include "meas.h" /* 全周傅氏相量发布侧:遥测快照 meas_tick()(「数据可信」遥测快照发布,道D) */
#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 增量)。
* 原始码值缓冲由 语义取数与有效性层 App/algo/adc_input.c 持有(消费侧按语义取数,不写死下标) */
static uint32_t s_frames_consumed;
static uint32_t s_frames_idle;
/* 因通道尚未首次成功而**整拍跳过入环**的次数(不向相量窗注入 0 点) */
static uint32_t s_frames_skip;
/* 采样有效性校验 告警编排状态:
* s_health_prev - 上次电平故障位(判上升/下降沿,避免每拍重复告警)
* s_health_logged- 已写 SYSLOG_SELFTEST 的位(每类每次开机只留痕一次,
* 防持续故障刷屏把 32KB selftest.log 写满) */
static uint32_t s_health_prev;
static uint32_t s_health_logged;
/* 采样通道自检 贴轨编排状态(锁存本体在 adc_health 内,这里只留编排层视角):
* s_rail_edge_latched - 本开机以来出现过锁存边沿的通道位图(心跳 cal: 行展示)
* s_rail_syslogged - SYSLOG 留痕只写一次的闸门(init 重置) */
static uint32_t s_rail_edge_latched;
static uint8_t s_rail_syslogged;
/* 遥测快照发布闸门(「数据可信」这一项):上次发布拍时基。分频用 **tick 经过**
* 对比 MEAS_PERIOD_MS,非拍计数——idle/skip 拍会把拍计数口径拉得超过名义
* 200 ms(与 v2.6 修"重复采样污染 DFT 窗"同族误差,勿"优化"成拍计数) */
static uint32_t s_meas_ms;
/* 消费者就绪门:由 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)。逐实例数据驱动输出,
* 实例表加行即自动覆盖。输入自 v3.1 起为 标定折算折算后的工程量
* (现口径 = ADC 引脚电压 V,见 cal: 心跳行与文件头 v3.1 记录);
* 二次值语义仅在 adc_cal_flags() 出 VALID 后成立。
*/
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,过频 / 低频 测量侧台架可见性):同样 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());
}
}
/*
* 函数功能:采样有效性校验 判定结果的告警编排(边沿 → 日志 + 自检留痕)
* 入口参数: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;
s_rail_edge_latched = 0u;
s_rail_syslogged = 0u;
s_meas_ms = 0u;
adc_health_init(); /* 内含 采样通道自检 贴轨观察器复位(rail_init) */
(void)meas_init(); /* 遥测快照双区/序号复位(幂等;发布状态属本任务,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) 采集一拍:语义取数与有效性层 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) 全周傅氏相量输入 + 相量重算:把本拍 9 路语义值转浮点帧压入采样环,再重算全部
* 相量实例(每 1 ms 滑动窗刷新:每拍以"当前时刻为终止的前一周波"整窗重算)。
*
* ⚠ **仅在本拍确有新帧(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;
uint32_t rail_edge = 0u;
for (s = 0u; s < (uint8_t)ADC_SEM_COUNT; s++) {
int32_t code = 0;
int32_t lo_th = 0;
int32_t hi_th = 0;
if (adc_input_raw((adc_semantic_t)s, &code) != 0) {
all_ok = 0u;
break;
}
/* 标定折算折算(「数据可信」标定折算落地,§4.1 模型):工程量 = (code + b) × k。
* 现口径 = **ADC 引脚电压 V**(eng=0,量程口径冻结的 ROM 系数,见
* docs/硬件平台/ADC通道映射说明.md §7.1);端子变比确认与两点标定写入标定
* 系数(adc_cal_flags() 出 VALID)后才按二次值消费。TEMP 透传码值
* 不折算(决策 5)。*/
frame[s] = adc_cal_apply((adc_semantic_t)s, code);
/* 采样通道自检 码值越界(贴轨)观察:在**码值域**判定,与标定正交——
* 连续一周波贴轨带且此前见过释放沿才锁存(空载恒 0 不误报)。
* TEMP 无「轨」语义,adc_cal_rail_ths() 返 -1 自然跳过。*/
if (adc_cal_rail_ths((adc_semantic_t)s, &lo_th, &hi_th) == 0) {
if (adc_health_rail_watch(s, code, lo_th, hi_th) != 0u) {
rail_edge |= (1uL << s);
}
}
}
if (all_ok != 0u) {
(void)sample_ring_push_frame(frame, (uint8_t)ADC_SEM_COUNT);
phasor_run();
/* 全周傅氏相量的派生量测频(B8):同拍紧随 phasor_run——本拍使用区已
* 切换,freq 读的就是本拍相量(1 ms 同上下文,无竞态) */
freq_run();
/* 测频与遥测快照发布(「数据可信」这一项,道D,M5):200 ms tick 经过分频。
* 唯一发布者 = 本任务(全仓 meas_tick 调用点数 = 1 为评审判据,
* 「数据可信」§8 风险表"快照双写"行);只在**压环成功的真新帧拍**发布
* ——idle/skip 拍压环未推进,发布 = 重复旧窗。品质注入唯一源 =
* adc_cal_flags()(§4.2 三态表;纯读,每 200 ms 才跑一遍)。 */
if ((uint32_t)(now - s_meas_ms) >= MEAS_PERIOD_MS) {
uint32_t cm = 0u;
uint8_t m;
s_meas_ms = now;
for (m = 0u; m < (uint8_t)ADC_SEM_COUNT; m++) {
if ((adc_cal_flags((adc_semantic_t)m) & ADC_CAL_F_CLIPPED) != 0u) {
cm |= (1uL << m);
}
}
meas_tick(now, freq_valid(), freq_last(), cm);
}
} else {
s_frames_skip++;
}
/* 贴轨锁存边沿留痕:帧循环后统一处理(skip 拍也已在循环内逐通道
* 观察过,不因取数半途失败而漏报已观察的通道)。SYSLOG 每开机只
* 留痕一次——贴轨是持续工况,逐拍写会把 selftest.log 刷满 */
if (rail_edge != 0u) {
s_rail_edge_latched |= rail_edge;
if (dbg_console_writer_ready() != 0u) {
DBG_ERROR("rail: edge=0x%lx sticky=0x%lx (code-domain, 20-run+release)",
(unsigned long)rail_edge,
(unsigned long)adc_health_rail_sticky());
}
if (s_rail_syslogged == 0u) {
s_rail_syslogged = 1u;
(void)syslog_post(SYSLOG_SELFTEST, "adc code rail fault");
}
}
}
/* 2b) 采样有效性校验(每拍,常数次整数运算):把驱动侧观测计数交给
* 纯逻辑判定层,本任务只做告警编排。⚠ 计数一律取**本拍 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() **按语义**取数(通道语义映射),
* 用于验证"语义 → 物理通道"映射与两片转换器取数契约 */
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)后立即落一次,作为 全周傅氏相量链路的开机证据。
* 不依赖 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 错误计数 + 全周傅氏相量。
* 写者就绪前抑制(同 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 侧同类丢帧计数(采样有效性校验,与 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");
/* 全 9 路**原始码值**(「数据可信」已知电压注入验证的唯一连续读出口):
* 与 first: 行同序同格式(UA/UB/UC/UN/IA/IB/IC/IN/T),台架
* "改电压 → 读一行"即可对表,不必为每个注入点反复复位去抓只在开机
* 出现一次的 first: 行。两处的通道顺序是同一份约定,改一处必须同步另一处。
* ⚠ 为何单独一行、不并进下面的 cal: 行:cal: 已 75 字符,9 个码值最坏
* 再加 93 字符,逼近 DBG_CON_LINE_MAX(192) 的截断边界——截断**不报错**,
* 只会静默少打几路,是最难发现的台架假数据来源;分行后各自留足余量。
* ⚠ 取数失败(-2 该来源无有效数据)时该槽保持 0,故"9 路全 0"读作
* "没数据",不读作"九路都是 0 V"——判据是 frames 心跳是否仍在涨。 */
{
int32_t rc[ADC_SEM_COUNT];
uint8_t j;
for (j = 0u; j < (uint8_t)ADC_SEM_COUNT; j++) {
rc[j] = 0;
(void)adc_input_raw((adc_semantic_t)j, &rc[j]);
}
DBG_INFO("adc: UA=%ld UB=%ld UC=%ld UN=%ld IA=%ld IB=%ld IC=%ld IN=%ld T=%ld",
(long)rc[ADC_SEM_UA], (long)rc[ADC_SEM_UB],
(long)rc[ADC_SEM_UC], (long)rc[ADC_SEM_UN],
(long)rc[ADC_SEM_IA], (long)rc[ADC_SEM_IB],
(long)rc[ADC_SEM_IC], (long)rc[ADC_SEM_IN],
(long)rc[ADC_SEM_TEMP]);
}
/* 标定折算/采样通道自检 台架观测面(×1000 mV 定点,MicroLIB 无 %f):
* ua/un/ia_mv = 三路 TPAFE 引脚电压毫伏(未标定域,**不是**
* 二次值;两点标定注入验证的读数落点,偏差判据 ≤2 %)
* vl = VALID 位图(默认构建应恒 0x00——未标定即未标定,
* 未标定即闭锁的判据在 protect 侧同源消费这个位)
* cl = CLIPPED 位图(V0.1 应恒 0xE6 = UB/UC/IB/IC/IN)
* rail/edge = 贴轨锁存/历史边沿位图(注入饱和电平后应起)*/
{
int32_t c = 0;
long ua_mv = 0L, un_mv = 0L, ia_mv = 0L;
uint32_t vl = 0u, cl = 0u;
uint8_t i;
if (adc_input_raw(ADC_SEM_UA, &c) == 0) {
ua_mv = (long)(adc_cal_apply(ADC_SEM_UA, c) * 1000.0f);
}
if (adc_input_raw(ADC_SEM_UN, &c) == 0) {
un_mv = (long)(adc_cal_apply(ADC_SEM_UN, c) * 1000.0f);
}
if (adc_input_raw(ADC_SEM_IA, &c) == 0) {
ia_mv = (long)(adc_cal_apply(ADC_SEM_IA, c) * 1000.0f);
}
for (i = 0u; i < 8u; i++) {
const uint32_t f = adc_cal_flags((adc_semantic_t)i);
if ((f & ADC_CAL_F_VALID) != 0u) {
vl |= (1uL << i);
}
if ((f & ADC_CAL_F_CLIPPED) != 0u) {
cl |= (1uL << i);
}
}
DBG_INFO("cal: ua_mv=%ld un_mv=%ld ia_mv=%ld vl=0x%02lx cl=0x%02lx rail=0x%03lx edge=0x%03lx",
ua_mv, un_mv, ia_mv,
(unsigned long)vl, (unsigned long)cl,
(unsigned long)adc_health_rail_sticky(),
(unsigned long)s_rail_edge_latched);
}
/* M5 遥测快照台架观测面(定点打印,MicroLIB 无 %f):
* seq = 发布帧数(10 s 心跳间隔内应 +500±,不涨 = 发布闸门停)
* v = 整帧有效;ua/un/ia = 基波有效值毫伏(未标定域 = 引脚电压;
* 本行是 200 ms 快照轨,cal: 行是当拍瞬时码折算——同量级不同
* 相位,勿互为判据)
* f/fv = 频率读数与新鲜度(x0.01)
* 读数与注入源比对类判据 = **待台架实测**(实测采样类跳过口径) */
{
meas_snap_t ms;
(void)meas_get(&ms); /* 未就绪(-2) = 清零帧,以下打印全 0 */
DBG_INFO("meas: seq=%lu v=%u ua_mv=%ld un_mv=%ld ia_mv=%ld f=%ld fv=%u",
(unsigned long)ms.seq, (unsigned)ms.valid,
(long)(ms.yc[ADC_SEM_UA] * 1000.0f),
(long)(ms.yc[ADC_SEM_UN] * 1000.0f),
(long)(ms.yc[ADC_SEM_IA] * 1000.0f),
(long)(ms.freq_hz * 100.0f),
(unsigned)ms.freq_valid);
}
}
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);
}
}