/* * 模块名称: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(局部码值数组) * v3.4 2026-10-29 DTU 项目组 故障录波数据面接线(task-2):新增 2c——每拍把 8 路 * 语义原码压进 wave_ring 并 tick 推进;EVT_SEALED 那一拍现取 * 折算/有效性/墙钟事实组 wave_seal_cal_t 并 latch_meta 恰好 * 一次(握手次序照 App/algo/wave_ring.h 头注唯一合法次序); * 心跳末尾加 §9 唯一周期行 wave:(字段逐字,取数 * wave_ring_get_stat);新增 2d——bench 定周期自抓,只在 * DTU_BENCH 且 WAVE_BENCH_AUTOCAP 非 0 的双层编译门内存在, * 默认关闭 + 5 分钟时长上限 + 启动一次性告警(§10 红线); * adc_task_init 内接 wave_ring_init(init 禁日志约束不破) */ #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" /* 常开耗时探针:单拍耗时 */ #include "wave_ring.h" /* 故障录波环(task-2 数据面接线):push/tick/latch_meta/get_stat */ #include "sys_clock.h" /* 封存交棒的 wall 字段:sys_clock_get() epoch 秒(UINT32_MAX=未对时,写 0) */ #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 /* bench 定周期自抓(§10 红线「默认关闭 + 有时长上限 + 启动打一次告警」的落点): * WAVE_BENCH_AUTOCAP 默认 0——产品档连外层 DTU_BENCH 都没有,双层编译门保证 * 产品镜像里 2d 段整段不存在;台架确实要定周期刷段时才本地改 1 重编译, * 用完立刻回 0(每 2 s 一段 = 每 5 分钟一轮 NAND 写,长挂白烧写次数,dhara * 坏块记账还是持久代价)。宏形态照 App/algo/adc_cal.c 的 adc_cal_prot_blocked(): * 联合条件放 #if,未定义宏恒为假,不把 cppcheck 引到旁路上。 * 周期/上限取自 §10 的算式:2 s/段、300 s 自动停。 */ #define WAVE_BENCH_AUTOCAP 0 #define WAVE_BENCH_PERIOD_MS 2000u #define WAVE_BENCH_MAX_MS 300000u /* 统计:已消费帧数 / 无新帧次数(首拍必为无新帧,之后应稳定为 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; /* 故障录波环接线状态(task-2;状态机与计数全在 App/algo/wave_ring.c,这里只搬运): * s_wave_codes - 本拍 8 路语义原码(文件级 static,不上单拍的栈——本任务静态 * 最坏链余量按 1024 B ISR 底线核) * s_wave_cal - EVT_SEALED 拍的交棒结构(同理不放局部:整块 ~160 B) */ static int16_t s_wave_codes[WAVE_RING_CH]; static wave_seal_cal_t s_wave_cal; #if defined(DTU_BENCH) && (WAVE_BENCH_AUTOCAP != 0) /* bench 自抓编排:on = 已过写者就绪门开始跑;off = 到时自动停(init 重置) */ static uint8_t s_wave_bench_on; static uint8_t s_wave_bench_off; static uint32_t s_wave_bench_t0; static uint32_t s_wave_bench_next; #endif /* 消费者就绪门:由 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 无日志) */ wave_ring_init(); /* 录波环纯清 RAM(无内核依赖;本函数在开闸前跑,init 禁日志不破) */ #if defined(DTU_BENCH) && (WAVE_BENCH_AUTOCAP != 0) s_wave_bench_on = 0u; s_wave_bench_off = 0u; s_wave_bench_t0 = 0u; s_wave_bench_next = 0u; #endif 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); } /* 2c) 故障录波环泵送(task-2 数据面接线;状态机与计数全在 App/algo/wave_ring.c, * 本段只做搬运)。每拍取 8 路语义**原码**压环 + tick 推进;触发那拍的样本 * 落在段内点 WAVE_RING_PRE-1(点序 lead 钉死)。只存原码不存折算工程量—— * §6.3 裁定:样本域 = 原始码,折算系数进段头,回放/PC 侧统一换算。 * 某路取不到(开机头几拍)保持上值(与 adc: 心跳行同一兜底口径)。 * tick 返 EVT_SEALED 的那拍必须**同拍** latch meta(头注唯一合法次序), * 漏了 = 2 s 后环自弃计 err,defaultTask 根本不会见到这一段。 */ { uint8_t w; int32_t wev; for (w = 0u; w < WAVE_RING_CH; w++) { int32_t wc = 0; if (adc_input_raw((adc_semantic_t)w, &wc) == 0) { s_wave_codes[w] = (int16_t)wc; } } (void)wave_ring_push(s_wave_codes, WAVE_RING_CH); wev = wave_ring_tick(now, s_frames_consumed); if (wev == WAVE_RING_EVT_SEALED) { uint8_t c; /* 封存瞬间事实(交棒结构 s_wave_cal 为文件级 static,不上栈): * eng / VALID 位图 / CLIPPED 位图 / 墙钟 / 逐路 conv+bits+avail+k+b。 * eng 口径 = adc_cal.h 注③ 的 8 路量纲标签(判据源 * App/app/ui_data_feed.c feed_eng(),与 prot_blocked 的三路口径**不是** * 一件事):8 路电气量全 VALID 才是二次值;V0.1 无 CT 变比,一次值不可达。 * wall 未对时写 0(sys_clock_get 失败 = UINT32_MAX,不许拿上电毫秒冒充)。 * k 取 adc_cal_k_q16() int32 原值;b 的 float 回乘在本域无损 * (|b| 限幅 8192 << 2^24,float32 精确表示整数;禁用浮点只针对 k)。 */ s_wave_cal.flags_seq = 0u; s_wave_cal.clip_mask = 0u; for (c = 0u; c < WAVE_RING_CH; c++) { const uint32_t f = adc_cal_flags((adc_semantic_t)c); const adc_channel_desc_t *d = adc_input_desc((adc_semantic_t)c); if ((f & ADC_CAL_F_VALID) != 0u) { s_wave_cal.flags_seq |= (uint8_t)(1u << c); } if ((f & ADC_CAL_F_CLIPPED) != 0u) { s_wave_cal.clip_mask |= (uint16_t)(1u << c); } s_wave_cal.ch[c].conv = (d != NULL) ? (uint8_t)d->source : (uint8_t)ADC_SRC_STM32; s_wave_cal.ch[c].bits = (s_wave_cal.ch[c].conv == (uint8_t)ADC_SRC_TPAFE) ? 16u : 12u; s_wave_cal.ch[c].avail = (uint8_t)f; s_wave_cal.ch[c].k_q16 = adc_cal_k_q16((adc_semantic_t)c); s_wave_cal.ch[c].b_codes = (int32_t)adc_cal_b_codes((adc_semantic_t)c); } s_wave_cal.eng = (s_wave_cal.flags_seq == 0xFFu) ? (uint8_t)WAVE_ENG_SEC : (uint8_t)WAVE_ENG_PIN; { uint32_t wall = sys_clock_get(); s_wave_cal.wall = (wall == UINT32_MAX) ? 0u : wall; } /* 只在 EVT_SEALED 拍调一次;返错不补救——环 2 s 超时自弃兜底, * 这里补第二行日志就违反 §9「不在已留痕处重复锚点」 */ (void)wave_ring_latch_meta(&s_wave_cal); } } /* 2d) bench 定周期自抓(§10 红线落点:默认关闭 + 时长上限 + 启动一次性告警)。 * 编译门 = DTU_BENCH 且 WAVE_BENCH_AUTOCAP 非 0,产品镜像无本段。 * 节拍:每 WAVE_BENCH_PERIOD_MS 投一次 bench 请求(环侧冷却/去重自管, * 被丢的计入 ovf——这是 bench 的预期观测面,不是故障)。 * 计时起点等写者就绪门(v2.1 理由:就绪前直发会与 defaultTask 存储 * 日志字节级交错);到时自动停 + 一次性 STOP 行,之后不再投任何请求。 * ⚠ 这两条 WARN/STOP 是一次性生命周期日志,不是周期行,§9 一条线约束不破 */ #if defined(DTU_BENCH) && (WAVE_BENCH_AUTOCAP != 0) if (s_wave_bench_off == 0u) { if (s_wave_bench_on == 0u) { if (dbg_console_writer_ready() != 0u) { s_wave_bench_on = 1u; s_wave_bench_t0 = now; s_wave_bench_next = now + WAVE_BENCH_PERIOD_MS; /* [WARN] 进正文(本仓无 WARN 级;用 ERR 级会带 [ERR] 误触 /serial-boot-test 禁用标记) */ DBG_INFO("[WARN] wave bench autocap ON: 1 seg / %u ms, auto-stop %u s (BENCH BUILD ONLY)", (unsigned int)WAVE_BENCH_PERIOD_MS, (unsigned int)(WAVE_BENCH_MAX_MS / 1000u)); } } else if ((uint32_t)(now - s_wave_bench_t0) >= WAVE_BENCH_MAX_MS) { s_wave_bench_off = 1u; DBG_INFO("wave bench autocap STOP after %u s", (unsigned int)(WAVE_BENCH_MAX_MS / 1000u)); } else if ((uint32_t)(now - s_wave_bench_next) >= WAVE_BENCH_PERIOD_MS) { s_wave_bench_next = now + WAVE_BENCH_PERIOD_MS; (void)wave_ring_request((uint8_t)WAVE_TRIG_BENCH, 0u); } else { /* 本拍未到周期点:无事可做(比较用无符号差值,回绕安全) */ } } #endif /* 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 心跳 → 台架 /serial-boot-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); } /* 录波环周期行(docs/子系统设计/故障录波设计说明.md §9 逐字模板, * 字段名与顺序**不许动**——PC/台架脚本按行名 grep)。全链路只加这一条 * 周期行(每条 DBG ≈90 B Flash + 216 B 栈,§9 明令不加第二条): * seg = 累计成功 ACK 的段数(wave_ring_stat.seg_total,不是轮转号) * armed= 1 表示此刻有段在飞(FREEZE/SEALED;IDLE/COOL 为 0—— * COOL 只挡新触发不录新段,画进 armed 会让台架误读) * src = 触发源 M 手动 / P 保护 / T bench 定时,- = 本 boot 未触发过 * err = 落盘失败 + SEALED 超时自弃(环内 err_cnt 收口,落盘道不数 * 第二份账);ovf = ACK 前重复触发与冷却期丢弃数; * drop = 冻结窗口丢弃的录波点数(单位=点) * 判读:bench 自抓一轮后 seg_total 应增长;err 涨而 seg 不涨 = * 落盘道在丢段,查 defaultTask 侧 [WAVE] 失败行 */ { wave_ring_stat_t wst; (void)wave_ring_get_stat(&wst); { const char *sc; if (wst.trig_src == (uint8_t)WAVE_TRIG_MANUAL) { sc = "M"; } else if (wst.trig_src == (uint8_t)WAVE_TRIG_PROTECT) { sc = "P"; } else if (wst.trig_src == (uint8_t)WAVE_TRIG_BENCH) { sc = "T"; } else { sc = "-"; } DBG_INFO("wave: seg=%lu armed=%u src=%s pts=%u err=%lu ovf=%lu drop=%lu", (unsigned long)wst.seg_total, (unsigned)((wst.state == (uint8_t)WAVE_RING_ST_FREEZE) || (wst.state == (uint8_t)WAVE_RING_ST_SEALED)) ? 1u : 0u, sc, (unsigned int)WAVE_RING_PTS, (unsigned long)wst.err_cnt, (unsigned long)wst.ovf_cnt, (unsigned long)wst.drop_cnt); } } } 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); } }