Files
STM32F4-Base/App/algo/adc_health.h
T
wandering1 9f826dbb4d fix(protect): 采样陈旧时独立闭锁保护(判定层失明的兜底门)
问题:L5-4 闭锁读的 adc_health_flags() 只有 adcTask 每拍跑
adc_health_check() 才可能置位。TIM3 一停,adcTask 就永久阻塞在
osThreadFlagsWait(刻意的 fail-safe),check() 不再执行 → stall 位
永远是 0,而 protectTask 仍每 20 ms 拿冻在最后一帧的相量继续判据。
窗口最坏约 800 ms(停喂阈值)+ 2498 ms(IWDG 标称)≈ 3.3 s。

方案:保护侧建一条与消费者存活完全解耦的兜底新鲜度门。
- adc_health.c/h 新增 adc_health_age_stale():无模块状态的纯判据,
  参照系 adc_health_age_t 由调用方自存。故意不让 protectTask 去调
  adc_health_check()——那是「每拍一次、单写者」的契约,两个上下文
  同写会互相刷新差值基线,把现有判定层一起弄坏。
- 新增 ADC_HF_STALE = bit8(与判定层 bit0..bit7 位段错开)。本位
  不由判定层写、不进 sticky、不进编排层 logged 位图。
- protect_task.c 用自己的时基直接读两个**中断侧递增**的计数器
  (tpafe5160_frame_seq / adc_input_stm32_seq),并入掩码。两条车道
  各自一份 primed:共用会让先观测的那条把标志置 1,另一条首拍拿
  adv_ms = 0 算年龄 → 开机即误锁保护。
- 门限 PROT_STALE_AGE_MS = 100 + 20 = 120 ms,买的是时序阶梯:
  判定层活着时它先报 0x1/0x2(带日志与自检留痕),死了只出现
  0x100。闭锁最迟 140 ms,仍远小于最小动作延时(默认 1000 ms)。

实测:
- @build 0 错 0 警:Code 213424 → 213520(+96 B);ZI 131480 →
  131504(+24 B = 两份 age 结构);RW-data 1184 不变
- stack_report:protectTask 静态最坏链 872 B 不变(余 1176 B / 57%)、
  adcTask 1320 B 不变——新判据无日志,不进最坏链
- 主机单测 45/45(新增 TC-ADCHEALTH-012..017 共 14 条断言:首拍只建
  基线 / 超拍不判老 / 门限含等号 / 跨 tick 回绕 / 倒退按分工不判老 /
  双观察者互不污染 / 空参数不判老)
- 台架注入(一次性停 TIM3 300 ms 探针,验完已撤;探针固件与最终
  固件 Program Size 逐字节相同):
  [+5462ms] [ERR][PROT] sampling FAULT -> protection blocked (hf=0x00000100)
  [+5757ms] [PROT] sampling recovered -> protection resumed
  同次开机 [ADC_TASK] health: 出现 0 次;MISS 快照缺的正是 bit6
  (0x1F7 - 0x1B7 = 0x40,实证 adcTask 当时确实阻塞,不是"在跑但没报");
  reset cause: 仅 1 次(300 ms 被 800 ms 容忍吸收,闭锁不靠复位)
  证据 docs/测试说明/boot_log_data/20261021_probe_stale_gate_boot_log.txt
- 字节卫生:BOM 无 / CR 0 / U+FFFD 0;新增行无非法字面量

gate: only mem-budget/mem-numbers pending lead ledger backfill
pre-commit run --all-files 失败仅这两条(台账受管快照与本树 .map 的
Flash/RAM 水位漂移,即本次 +96 B Code、+24 B ZI)。按并行分支约定
本分支不跑 --write,台账由 Lead 合并后统一回填。

文档同步:设计说明新增 §15(原相关文档改 §16),§7 位表加 bit8 行、
§8 责任矩阵与「已知窗口」段改写为已闭合、§10 盲区第 3 条、§11 用例
表、§12 判读手册、§14 改前清单全部跟上;位图配图补 bit8 列,并修掉
生成器把列数硬编码成 8 列导致新列被备注框盖住的问题。
2026-10-22 00:43:32 +08:00

167 lines
9.1 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 采样有效性校验(L1 采集与预处理,规划项 L1-6)
* 模块功能:对两片转换器的"节拍是否还在喂数据"做**判定层**(观测计数由
* `adc_input` / `tpafe5160` 驱动提供,本模块只做判定与告警位):
* ① 静默(stall):帧序号连续 >= ADC_HEALTH_STALL_MS 无前进;
* ② 丢帧(gap):帧序号一次前进 >1(消费者超拍,中间帧被覆盖);
* ③ 单调性破坏:帧序号倒退(计数器被改写 / 驱动 bug,锁存);
* ④ 错误计数增量:FRSTDATA 跳变 / HAL ADC·DMA 错误 / STM32 停摆计数。
* ⚠ 本层**纯逻辑**:不碰寄存器、不取数、不打日志——输入是调用方
* 采集的计数快照,输出是故障位;日志与自检上报由消费者
* (`App/task/adc_task.c`)按位执行 → 主机侧可单测(L0.5,A-4)。
* 适用平台:STM32F407ZGTx + TPAFE5160
* 作者:王建锋
* 创建日期:2026-10-11
* 修改记录:
* v1.0 2026-10-11 王建锋 创建:L1-6 采样有效性校验(静默/丢帧/单调性/错误增量)
* v1.1 2026-10-21 王建锋 新增 adc_health_age_stale()(无模块状态的帧年龄
* 纯判据,参照系由调用方自存)与 ADC_HF_STALE 位:
* 判定层的 stall/backward 位只在 adcTask 每拍跑
* check() 时才可能置起,节拍源停摆会把本任务卡在
* osThreadFlagsWait 上 → 那些位永远是 0,而相量冻在
* 最后一帧仍被 protectTask 用于判据。该纯判据让保护侧
* 能用**自己的时基 + 中断侧计数器**独立判"数据还新不新"
*/
#ifndef ADC_HEALTH_H
#define ADC_HEALTH_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* 静默判定门限(ms):帧序号连续无前进达到该时长即判静默。
* 名义节拍 1 kHz → 正常每拍 +1;100 拍无前进不可能是抖动,只可能是
* 节拍源/转换器/EXTI 真停(消费者超拍只会造成 gap=d>1,不会 d=0)。 */
#define ADC_HEALTH_STALL_MS 100U
/* 电平故障位(adc_health_flags(),当前活跃;stall 可自愈清除,backward 锁存)
* ——供后续 L5-4「自检失败闭锁」直接消费 */
#define ADC_HF_TPAFE_STALL (1uL << 0) /* TPAFE 帧序号静默 */
#define ADC_HF_STM32_STALL (1uL << 1) /* STM32 DMA 完成计数静默 */
#define ADC_HF_SEQ_BACKWARD (1uL << 2) /* 帧序号倒退(锁存,不自愈) */
/* 锁存故障位(adc_health_sticky(),出现过即置位、直到 init 才清;
* 与电平位共用高位编号,gap 类"常态性超拍"不入锁存) */
#define ADC_HF_FRSTDATA (1uL << 3) /* FRSTDATA 跳变计数有增量(锁存) */
#define ADC_HF_STM32_ERR (1uL << 4) /* STM32 停摆计数有增量(锁存) */
#define ADC_HF_HW_ERR (1uL << 5) /* HAL ADC/DMA 错误回调有增量(锁存) */
/* 边沿事件位(adc_health_check() 出参 events,仅本次调用有效)。
* ⚠ FRSTDATA/STM32_ERR/HW_ERR 三个事件位与同名锁存位**编号一致**——
* 消费者可直接用 events 掩码对 sticky/logged 位图做"每类留痕一次"的编排 */
#define ADC_HE_TPAFE_GAP (1uL << 6) /* 本次 TPAFE 序号前进 >1 */
#define ADC_HE_STM32_GAP (1uL << 7) /* 本次 STM32 gap 计数有增量 */
#define ADC_HE_STM32_ERR (1uL << 4) /* 本次 STM32 停摆计数有增量 */
#define ADC_HE_FRSTDATA (1uL << 3) /* 本次 FRSTDATA 计数有增量 */
#define ADC_HE_HW_ERR (1uL << 5) /* 本次 HAL 错误计数有增量 */
/* 兜底新鲜度位(bit8,与判定层 bit0..bit7 位段错开)
* ⚠ 本位**不由本模块置位**:由 protectTask 调 adc_health_age_stale() 独立观测后
* OR 进闭锁位图。因此它不进 sticky、不进 adc_task 编排层的 logged 位图、
* 也不由本模块打日志——留痕走 protect 侧既有的闭锁边沿日志与 SOE。
* 位段纪律见 docs/算法/采样有效性校验设计说明.md §14 */
#define ADC_HF_STALE (1uL << 8) /* 帧年龄超限(电平,序号恢复前进即清) */
/* 计数快照(由调用方采集,本模块不触碰任何驱动/外设) */
typedef struct {
uint32_t tpafe_seq; /* tpafe5160_frame_seq() */
uint32_t tpafe_frst_err; /* tpafe5160_frstdata_err()(驱动为 uint16,此处放宽) */
uint32_t stm32_seq; /* adc_input_stm32_seq() */
uint32_t stm32_errs; /* adc_input_stm32_errs() */
uint32_t stm32_gaps; /* adc_input_stm32_gaps() */
uint32_t stm32_hw_errs; /* adc_input_stm32_hw_errs() */
uint32_t now_ms; /* osKernelGetTickCount()(tick 回绕由差值法吸收) */
} adc_health_in_t;
/* 判定结果快照 */
typedef struct {
uint32_t flags; /* 电平故障位 ADC_HF_TPAFE/STM32_STALL/SEQ_BACKWARD */
uint32_t events; /* 本次边沿事件位 ADC_HE_xxx */
uint32_t sticky; /* 锁存故障位(含电平位的历史并集) */
uint32_t tpafe_gaps; /* TPAFE 累计丢帧点数(序号前进 >1 的超出部分之和) */
uint32_t tpafe_stall_ms; /* TPAFE 当前静默时长(0=正常) */
} adc_health_status_t;
/* 帧年龄参照系(adc_health_age_stale() 用)。状态**归调用方所有**,所以多个观察者
* 各判各的、互不污染——这是它与本模块其余接口的关键区别(check() 用的是模块级
* 单写者状态,只能有一个每拍调用者) */
typedef struct {
uint32_t seq; /* 上次看到的序号 */
uint32_t adv_ms; /* 最后一次看到序号前进的时刻(与调用方时基同源) */
uint8_t primed; /* 0 = 尚未建立基线(首拍只记录、不判定) */
} adc_health_age_t;
/*
* 函数功能:采样有效性校验初始化(幂等,内核启动前可调)
* 入口参数:无
* 返 回 值:无
* 限定条件:无
* 函数说明:清全部状态;首次 adc_health_check() 只建基线、不判定。
*/
void adc_health_init(void);
/*
* 函数功能:按计数快照做一次有效性判定(每拍调用,开销为常数次整数运算)
* 入口参数:in - 计数快照(NULL 则本次跳过,out 不动)
* out - 判定结果(NULL 则仅更新内部状态)
* 返 回 值:无
* 限定条件:仅任务上下文(与 adc_input_poll() 同节奏调用)
* 函数说明:首次调用建立基线(不产生事件/故障)。序号差值按
* `(int32_t)(now - prev)` 口径计算,tick/计数回绕均安全。
*/
void adc_health_check(const adc_health_in_t *in, adc_health_status_t *out);
/*
* 函数功能:帧年龄判定(无模块状态,供与消费者存活解耦的兜底门使用)
* 入口参数:a_p_age - 调用方自存的参照系
* a_cur_seq - 本次读到的序号(须是**中断侧**递增的计数器,如
* tpafe5160_frame_seq() / adc_input_stm32_seq())
* a_now_ms - 本次时基(与 a_p_age->adv_ms 取同一个任务的 tick)
* a_age_ms - 判老门限(ms)
* 返 回 值:1 - 已陈旧(调用方应据此闭锁);0 - 新鲜,或仍在建立基线
* 限定条件:任意任务上下文;零内核依赖、零共享状态 → 主机侧免桩可直接单测
* 函数说明:差值口径与 adc_health_check() 一致((int32_t) 差值,计数与 tick
* 回绕均安全)。首次调用只建基线并返回 0;序号有任何前进即返回 0
* (一次前进多帧属消费者超拍,不是故障);倒退不在此判老,单调性
* 破坏归 ADC_HF_SEQ_BACKWARD 管。参数错返回 0——宁可不锁,不可误锁。
*/
uint8_t adc_health_age_stale(adc_health_age_t *a_p_age, uint32_t a_cur_seq,
uint32_t a_now_ms, uint32_t a_age_ms);
/*
* 函数功能:读当前电平故障位
* 入口参数:无
* 返 回 值:ADC_HF_TPAFE_STALL / ADC_HF_STM32_STALL / ADC_HF_SEQ_BACKWARD 组合
* 限定条件:无
* 函数说明:供 L5-4 闭锁判定与 UI;与 check() 出参 flags 同源。
*/
uint32_t adc_health_flags(void);
/*
* 函数功能:读锁存故障位(出现过即置位,直到 init 才清)
* 入口参数:无
* 返 回 值:电平位 + ADC_HF_FRSTDATA/STM32_ERR/HW_ERR 的并集
* 限定条件:无
* 函数说明:stall 自愈后 sticky 仍保留该次故障痕迹("曾经停摆过"必须留痕)。
*/
uint32_t adc_health_sticky(void);
/*
* 函数功能:读 TPAFE 累计丢帧点数
* 入口参数:无
* 返 回 值:序号前进 >1 的超出部分累计(与 STM32 侧 adc_input_stm32_gaps() 对称)
* 限定条件:无
* 函数说明:消费者超拍(单拍耗时 > 1 ms)所致;非零本身不是闭锁条件,
* 但持续增长说明相量窗出现时间空洞,须排查耗时。
*/
uint32_t adc_health_tpafe_gaps(void);
#ifdef __cplusplus
}
#endif
#endif /* ADC_HEALTH_H */