5a9988b557
清掉的(读者无从解析、且所指文档已不存在) - 已删任务文档的编号:Task #32、其内部 §5 / §4.3、P1-5、P0-2、P1-4a、 "方案 a"、"Lead 批准 2026-10-12" - 正文里当代词用的文件版本号:"(v2.7 起为 AboveNormal)""已随 v2.0 事件 驱动改造删除""(v2.0:DMA 消费)""见文件头 v1.3 说明"等 → 改写成陈述现状 - 测试与脚本同类:adc_input 桩头"v2.0:DMA 模型"、stub/adc.h 的 v2.0、 check_cubemx_survival.py 段头的"2026-10-10 v2.8"、run_all_suites.py 的"方案 A" 刻意保留 - 可检索登记簿锚点:P-AUDIT 真条目(P0-1/P1-8/P2-1/P2-9/P3-11)、S6/S11、 E-31/E-44、N-18/N-23/N-32、B1/B5/B7/B8、L1-x~L5-x 功能号 - 文件头修改记录块内的 vN.M(规范 V1.0 §2.1/§2.2 要求保留该块;块内编号 在本文件内自洽可读) 顺带纠正注释与代码已经不一致的过时事实 - 仍把 TIM6 写成现行节拍源:Src/main.c 的 include 注释与回调注释、 Src/freertos.c 的 adcTask 块 → 改 TIM3;adc_input.h 修订条目里"ADC1 只认 TIM2/TIM3/TIM8 的 TRGO"不准确 → 换成指向判据(ADC采样节拍设计说明 §3.2) - App/algo/phasor.c 双区注释称"唯一消费者是 adcTask、为后续保护任务预留", 实际 protectTask 已跨上下文读(prot_elm_run -> phasor_get)→ 改为"读者跨 adcTask 心跳与 protectTask 判据两个上下文,勿改加锁、勿取消双区",并同步 修正 docs/算法/采集与算法层设计说明.md 同一句 - adc_tick_irq_notify() 的"拒绝 ISR 内计算"理由仍写"内含 2 ms 忙等"(DMA 化 后已无忙等)→ 换成三条真实理由:任务级共享数据 / 计算量 / 生成区回调 - 裸 §5(指代不明)与"规划 §1.6 / 规划 §4"昵称式 → 写成完整路径紧邻式 字节卫生(Edit 会静默剥 BOM) - App/task/ui_task.c 与 Drivers/BSP/UI/ui_net_sink.c 被剥掉 BOM 两次,均按 3 字节 EF BB BF 前缀补回并复验;全部 20 个文件 CR=0、U+FFFD=0、无行尾空白 门禁 - @build 0 错误 0 警告,且 Program Size 与清理前逐项相同 (Code=213356 RO=18580 RW=1184 ZI=131480)→ 纯注释改动、中文未被误重编码 - ui-golden 首跑红:新工作树缺被 gitignore 的黄金参考 simulator/tests/ref/ (不入库、只在主工作树生成过);从主工作树复制该基线后复跑通过 → 注释改动 零像素影响 - 全套 pre-commit 在这 20 个文件上 Passed(coding-standard / cppcheck / 6 个主机单测 / doc-links / task-table / mem-numbers / cubemx-survival / registration-check / mem-budget / font-charset / ui-golden / id-check) - id-check 可判定比例 82.71% -> 82.88%(删掉的是"形似 P-AUDIT 但查无定义"的 假锚点),悬空引用 863 -> 854
443 lines
26 KiB
C
443 lines
26 KiB
C
/*
|
||
* 模块名称: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 分离),本文件只做"消费计数 → 告警"的编排
|
||
*/
|
||
|
||
#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()(内核启动前)完成
|
||
* 函数说明:驱动初始化不在本任务(见限定条件);此处仅清帧统计。
|
||
*/
|
||
int adc_task_init(void) {
|
||
s_frames_consumed = 0u;
|
||
s_frames_idle = 0u;
|
||
s_frames_skip = 0u;
|
||
adc_health_init();
|
||
DBG_INFO("adc_task_init");
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:ADC 任务主函数
|
||
* 入口参数:arg - FreeRTOS 传入参数(未使用)
|
||
* 返 回 值:无
|
||
* 限定条件:adc_task_init() 已调用;内核已启动
|
||
* 函数说明:每拍采集一次(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 的唤醒从此生效。
|
||
* 放在进入循环之前:置位与等待之间不会漏唤醒(线程标志粘滞) */
|
||
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);
|
||
}
|
||
}
|