Files
STM32F4-Base/App/algo/adc_input.c
T
wandering1 0e4cc03c2d docs(ADC): 更正注入验证口径——已知电压注入 2026-10-27 台架已闭环
数据可信.md §2.6 原写「ADC 从未采过已知电压」,那是 2026-10-23 挂账
时的现状,与 2026-10-27 的台架实测矛盾——按现状重写:实测 × 判据表 +
④ 段(端子 → AINn 子板变比)缺口 + M4 承诺边界(只到 ADC 引脚电压、
eng = 0);§2.6 节号与「承诺边界」口径保留,代码头注释的引用不断。

同族陈旧现在时一并更正:继电保护规划「引脚级注入验证仍为待做项」、
ADC通道映射 §7.1「不填任何实测数值」、ADC输入层单测说明「板端无信
号源、无法验证读到的是哪只脚」、adc_input.c 与 test/host/adc_input
两份注释「注入验证做不了」→ 统一改成「台架已一次性验过(§8.1),
主机单测与开机自检仍必要」。首帧悬案等历史叙述删除,只留结论。

顺带:ADC 输入层单测计数 101 → 103(runner 报 103/103 与 CHECK 调用
点 103 处双源互证),test/host/README.md 同数同步。

验证:@build 0 错 0 警(Code=236456 RO=28124 RW=1876 ZI=137084);
test/host/adc_input 103/103 rc=0;check_doc_links --all 0 处失效;
该批文件里的变更记录节按同日裁定一并清理。
2026-10-28 22:00:33 +08:00

404 lines
18 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 输入层(语义取数与预处理)
* 模块功能:语义通道映射(通道语义映射)+ STM32 ADC1 采样消费(STM32 ADC1 启动与扫描,TIM3 TRGO 触发 +
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
* DMA 双缓冲)+ TPAFE 快照消费。
* 消费侧经 adc_input_raw() 按语义取数,不写死下标。
* 适用平台:STM32F407ZGTx + TPAFE5160
* 作者:王建锋
* 创建日期:2026-10-10
* 修改记录:
* v1.0 2026-10-10 王建锋 创建:STM32 ADC1 启动与扫描(STM32 ADC1 扫描)+ 通道语义映射(语义映射)
* v2.0 2026-10-10 王建锋 STM32 侧由"软件触发 + 逐通道轮询"改为
* "TIM3 TRGO 硬件触发 + DMA 双缓冲消费":本层不再触发
* 也不等待转换(消除最坏 2~3 ms 忙等);新增三个 HAL
* 回调(ISR 内只计数)与 seq / hw_errs 观测量
* v2.1 2026-10-21 王建锋 丢帧计数口径由「次数」改为「帧点数」(累加每次前进
* 的超出部分),与判定层 TPAFE 侧
* adc_health_tpafe_gaps() 同单位。⚠ 换版前固件里心跳
* 字段 adc1_gap 是次数口径,不可跨版本直接对比
*/
#include "adc_input.h"
#include <stddef.h>
#include "adc.h" /* hadc1:MX_ADC1_Init() 已配 9 通道 / 144 cycles / 软件触发 */
#include "tpafe5160.h" /* tpafe5160_snapshot():临界区原子快照(AGENTS 铁律) */
/* STM32 ADC1 通道号上限(F4 上 ADC_CHANNEL_0..15 的宏值即通道号 0..15) */
#define ADC_INPUT_STM32_CH_MAX 16U
/* DMA 搬运缓冲长度 = 2 x 9 = 18:**双缓冲(乒乓)**。
* ADC1 配 ExternalTrigConv=TIM3_TRGO + DMAContinuousRequests=ENABLE + DMA CIRCULAR,
* 每拍(TIM3 TRGO,1 kHz)自动搬 9 个 → 每 2 拍绕一圈:半传输中断 = 前半 9 个就绪,
* 传输完成中断 = 后半 9 个就绪。消费侧永远读"刚完成的那一半",而 DMA 正在写另一半
* → 无撕裂、无需临界区。代价:STM32 侧数据固定滞后 1 拍(1 ms),9 路同一延迟,
* 对 20 点 DFT 窗是纯传输延迟、不引入通道间偏差。 */
#define ADC_INPUT_STM32_DMA_LEN (2U * ADC_INPUT_STM32_CH_NUM)
/* 连续"无新帧"达到该拍数才计一次停摆故障(单拍是相位伪影,见 stm32_take 注释) */
#define ADC_INPUT_STALL_RUN_MAX 2U
/* ===== 语义映射表(通道语义映射)=====
* 权威口径:docs/硬件平台/ADC通道映射说明.md(2026-10-09 硬件确认的语义分配)。
* index 含义:TPAFE 源 = 驱动 buf 下标 0..7;STM32 源 = ADC1 通道号 0..15。
* ⚠ 消费侧一律经本表按语义取数,**不得写死下标**(V0.1 有 13/17 路被置换,
* 网名编号与通道号不等价);V0.2 改版(消除双转换器无共同采样触发)只改本表内容。 */
static const adc_channel_desc_t k_adc_map[ADC_SEM_COUNT] = {
[ADC_SEM_UA] = { ADC_SRC_TPAFE, 6U }, /* TPAFE buf[6](ch7) */
[ADC_SEM_UB] = { ADC_SRC_STM32, (uint8_t)ADC_CHANNEL_14 }, /* PC4 */
[ADC_SEM_UC] = { ADC_SRC_STM32, (uint8_t)ADC_CHANNEL_3 }, /* PA3 */
[ADC_SEM_UN] = { ADC_SRC_TPAFE, 5U }, /* TPAFE buf[5](ch6) */
[ADC_SEM_IA] = { ADC_SRC_TPAFE, 2U }, /* TPAFE buf[2](ch3) */
[ADC_SEM_IB] = { ADC_SRC_STM32, (uint8_t)ADC_CHANNEL_6 }, /* PA6 */
[ADC_SEM_IC] = { ADC_SRC_STM32, (uint8_t)ADC_CHANNEL_1 }, /* PA1 */
[ADC_SEM_IN] = { ADC_SRC_STM32, (uint8_t)ADC_CHANNEL_2 }, /* PA2 */
[ADC_SEM_TEMP] = { ADC_SRC_STM32, (uint8_t)ADC_CHANNEL_5 }, /* PA5(V_ZL2) */
};
/* STM32 ADC1 顺序扫描顺序(Rank 1..9),须与 MX_ADC1_Init() 的 Rank 配置严格一致:
* Rank1..8 = CH0..CH7(PA0..PA7),Rank9 = CH14(PC4)。 */
static const uint8_t k_stm32_rank_ch[ADC_INPUT_STM32_CH_NUM] = {
(uint8_t)ADC_CHANNEL_0, (uint8_t)ADC_CHANNEL_1, (uint8_t)ADC_CHANNEL_2,
(uint8_t)ADC_CHANNEL_3, (uint8_t)ADC_CHANNEL_4, (uint8_t)ADC_CHANNEL_5,
(uint8_t)ADC_CHANNEL_6, (uint8_t)ADC_CHANNEL_7, (uint8_t)ADC_CHANNEL_14,
};
/* STM32 ADC1 最新一帧原始码值,按 ADC1 通道号索引(0..15) */
static uint16_t s_stm32_raw[ADC_INPUT_STM32_CH_MAX];
/* DMA 目的缓冲(乒乓两半区,按 Rank 序 0..8 / 9..17)。
* ⚠ **必须落在 SRAM(0x20000000 段)**:ADC1 挂的是 **DMA2**,而 F4 的 DMA2 不可达
* CCM(0x10000000)。实测 .map 里本文件 .bss 在 RW_IRAM1 → 普通 static 即可;
* 日后加大缓冲或改链接段时**必须用 .map 复核地址**——放错不报编译错、不报运行错,
* 只是数据永远搬不回来。 */
static uint16_t s_stm32_dma[ADC_INPUT_STM32_DMA_LEN];
static volatile uint32_t s_dma_seq; /* DMA 半帧/整帧完成次数(ISR 递增,只增不清) */
static uint32_t s_dma_seq_seen; /* 消费侧上次看到的完成计数 */
static volatile uint32_t s_stm32_hw_errs; /* HAL ADC/DMA 错误回调累计(ISR 写) */
static uint32_t s_stm32_gaps; /* 丢帧点数:Σ(前进量-1),与 tgap 同单位 */
static uint8_t s_stall_run; /* 连续"无新帧"拍数(相位伪影只会是 1 拍)*/
/* TPAFE5160 最新一帧原始码值(16 位二进制补码),按驱动 buf 下标索引 */
static int16_t s_tpafe_raw[ADC_INPUT_TPAFE_CH_NUM];
static uint8_t s_tpafe_valid; /* TPAFE 侧是否已有有效帧 */
static uint8_t s_stm32_valid; /* STM32 侧是否已有有效扫描 */
static int s_stm32_rc; /* STM32 最近一次扫描状态码 */
static uint32_t s_stm32_errs; /* STM32 扫描累计失败次数 */
/*
* 函数功能:消费一帧 STM32 ADC1 采样(STM32 ADC1 启动与扫描,DMA 双缓冲)
* 入口参数:无
* 返 回 值:0 - 取到新一帧;-2 - 本拍无新帧(TIM3 未启动 / ADC 停摆 / DMA 出错)
* 限定条件:仅任务上下文调用;adc_input_init() 已挂好 DMA
* 函数说明:本函数**既不触发转换也不等待转换**——转换由 TIM3 TRGO 硬件触发、由 DMA
* 自动搬运。每拍只做"看完成计数是否前进 + 拷 9 个半字",故**零忙等**:
* 单拍只剩"读一个计数 + 拷 9 个半字"→ µs 量级。
* 读的是"上一拍刚完成的那一半"乒乓缓冲(DMA 此刻正在写另一半)→ 无撕裂。
* ⚠ 计数未前进 = 本拍 STM32 侧**无有效数据**,必须清 s_stm32_valid,绝不
* 沿用上一拍旧值(半新半旧的混合帧会污染相量窗)→ adc_input_raw()
* 返 -2 → adc_task 走既有"整帧跳过"路径;下一拍自动恢复。
*/
static int stm32_take(void) {
uint32_t seq = s_dma_seq; /* 单次 32 位读:与 ISR 自增不产生撕裂 */
const uint16_t *src;
uint32_t d;
uint8_t half;
uint8_t i;
if (seq == s_dma_seq_seen) {
s_stm32_valid = 0u;
s_stm32_rc = ADC_INPUT_RC_STALL;
/* 单拍"无新帧"**多半是消费者相位伪影,不是故障**:本层在 TIM3 触发瞬间被唤醒,
* 而该拍的 DMA 完成落在 ~67 us 之后;若上一拍被日志/抢占拖过这 67 us,本拍就会
* 看到"计数没前进"(那个完成已被上一拍消费掉)。这种伪影**只会连续 1 拍**——
* DMA 若真停,第 2 拍仍不会前进。故只有连续 >= ADC_INPUT_STALL_RUN_MAX 拍
* 无新帧才计入故障计数(实测开机日志突发期约 21~26 次伪影、稳态每 10 s 心跳 1 次,
* 全部为单拍;见 docs/子系统设计/ADC采样节拍设计说明.md §8) */
if (s_stall_run < 255u) {
s_stall_run++;
}
if (s_stall_run >= ADC_INPUT_STALL_RUN_MAX) {
s_stm32_errs++;
}
return ADC_INPUT_RC_STALL;
}
d = seq - s_dma_seq_seen;
if (d > 1u) {
/* 计数一次前进 >1 = 中间有帧没被读走(乒乓缓冲已被覆盖)→ **真丢帧**,
* 与"无新帧"分开计数:前者是消费者超拍(本任务单拍耗时 > 1 ms),
* 后者多半是相位伪影。按**丢掉的帧数**累计(d-1),与判定层 TPAFE 侧的
* adc_health_tpafe_gaps() 同单位——否则心跳里 adc1_gap 与 tgap 一个是次
* 一个是点,跨字段对比会得出错误结论 */
s_stm32_gaps += (d - 1u);
}
s_stall_run = 0u;
s_dma_seq_seen = seq;
/* 半区下标由 seq 奇偶推出,**不再另读 ISR 变量**:DMA 从 NDTR=18 起、每拍搬 9 个,
* 故第 1/3/5... 次完成的是前半区(下标 0)、第 2/4/6... 次是后半区(下标 1)。
* 只读 seq 一个量 → 消费侧与 ISR 之间不存在"读到一半被换半区"的窗口。 */
half = (uint8_t)(1u - (seq & 1u));
src = &s_stm32_dma[((uint32_t)half * ADC_INPUT_STM32_CH_NUM)];
for (i = 0u; i < ADC_INPUT_STM32_CH_NUM; i++) {
s_stm32_raw[k_stm32_rank_ch[i]] = src[i];
}
s_stm32_valid = 1u;
s_stm32_rc = ADC_INPUT_RC_OK;
return ADC_INPUT_RC_OK;
}
/*
* 函数功能:语义映射表自洽性自检
* 入口参数:无
* 返 回 值:0 - 自洽;-1 - 存在不自洽条目
* 限定条件:无
* 函数说明:逐条校验映射表与扫描配置是否一致——① TPAFE 源下标须落在
* buf[0..7];② STM32 源的通道号必须出现在 k_stm32_rank_ch[] 中,
* 否则该通道永远不会被扫描(读到的恒为陈旧值)。这是"通道宏写错 →
* 读到另一只脚/读不到"这一警告的代码级兜底(警告口径与逐通道验证方法:
* docs/硬件平台/ADC通道映射说明.md §5、§8。台架注入验证只在台架做一次,
* 本自检每次开机都跑,两者不互相替代)。
*/
static int map_check(void) {
uint8_t s;
int rc = 0;
for (s = 0u; s < (uint8_t)ADC_SEM_COUNT; s++) {
if (k_adc_map[s].source == ADC_SRC_TPAFE) {
if (k_adc_map[s].index >= ADC_INPUT_TPAFE_CH_NUM) {
rc = -1;
}
} else {
uint8_t i;
uint8_t found = 0u;
for (i = 0u; i < ADC_INPUT_STM32_CH_NUM; i++) {
if (k_stm32_rank_ch[i] == k_adc_map[s].index) {
found = 1u;
}
}
if (found == 0u) {
rc = -1;
}
}
}
return rc;
}
/*
* 函数功能:ADC 输入层初始化
* 入口参数:无
* 返 回 值:0 - 成功;-1 - 语义映射表自检失败;-2 - DMA 挂载失败
* 限定条件:内核启动前调用(无 RTOS 依赖);STM32 ADC1 外设已由 MX_ADC1_Init 配好
* 函数说明:清空原始码值缓冲与统计 + 映射表自洽性自检 + **挂载一次 ADC1 DMA**
* (此后转换由 TIM3 TRGO 硬件触发、DMA 自动搬运,本层不再触发转换)。
* 须在启动采样节拍(app_main.c 步骤 6)之前调用。
*/
int adc_input_init(void) {
uint8_t i;
for (i = 0u; i < ADC_INPUT_STM32_CH_MAX; i++) {
s_stm32_raw[i] = 0u;
}
for (i = 0u; i < ADC_INPUT_STM32_DMA_LEN; i++) {
s_stm32_dma[i] = 0u;
}
for (i = 0u; i < ADC_INPUT_TPAFE_CH_NUM; i++) {
s_tpafe_raw[i] = 0;
}
s_tpafe_valid = 0u;
s_stm32_valid = 0u;
s_stm32_rc = ADC_INPUT_RC_NOT_RUN;
s_stm32_errs = 0u;
s_dma_seq = 0u;
s_dma_seq_seen = 0u;
s_stm32_hw_errs = 0u;
s_stm32_gaps = 0u;
s_stall_run = 0u;
if (map_check() != 0) {
return -1;
}
/* 挂**一次** DMA,之后不再重启:ADC 由 TIM3 TRGO 每拍触发一轮 9 通道转换,
* DMA 在 CIRCULAR 模式下自动搬运、硬件自动重载 NDTR → 每 2 拍绕一圈(乒乓)。
* 内核启动前调用安全:只碰寄存器/HAL,不创建任何 CMSIS 对象(AGENTS 铁律)。
* 此刻 TIM3 尚未启动(app_main.c 步骤 6)→ 不会发生任何转换或搬运。 */
if (HAL_ADC_Start_DMA(&hadc1, (uint32_t *)s_stm32_dma,
ADC_INPUT_STM32_DMA_LEN) != HAL_OK) {
s_stm32_rc = ADC_INPUT_RC_START;
s_stm32_errs++;
return -2;
}
return 0;
}
/*
* 函数功能:采集一拍(TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 采样)
* 入口参数:无
* 返 回 值:TPAFE 快照结果(1=有新帧 0=无新帧 -1=参数错)
* 限定条件:仅**任务上下文**调用(TPAFE 快照走临界区)
* 函数说明:先取 TPAFE 一帧,再消费 STM32 侧最近一帧 DMA 双缓冲;两者相互独立,
* STM32 侧无新帧不影响 TPAFE 快照与返回值语义(其状态另由
* adc_input_stm32_rc() / adc_input_stm32_seq() 暴露)。
* ⚠ STM32 侧数据比 TPAFE 侧**滞后 1 拍**(乒乓双缓冲的固有延迟,
* 9 路一致);因双转换器无共同采样触发,V0.1 本就禁止跨转换器做矢量运算。
*/
int adc_input_poll(void) {
int r = tpafe5160_snapshot(s_tpafe_raw, (uint8_t)ADC_INPUT_TPAFE_CH_NUM);
if (r == 1) {
s_tpafe_valid = 1u;
}
(void)stm32_take();
return r;
}
/*
* 函数功能:取语义通道的物理描述
* 入口参数:ch - 语义通道
* 返 回 值:指向静态描述表的指针;ch 越界返回 NULL
* 限定条件:无
* 函数说明:返回的是静态存储地址,调用方不得修改。
*/
const adc_channel_desc_t *adc_input_desc(adc_semantic_t ch) {
if ((unsigned int)ch >= (unsigned int)ADC_SEM_COUNT) {
return NULL;
}
return &k_adc_map[ch];
}
/*
* 函数功能:读取语义通道的原始码值(未标定)
* 入口参数:ch - 语义通道
* out - 输出原始码值(TPAFE 侧为 16 位补码;STM32 侧为 0..4095)
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 该来源当前无有效数据
* 限定条件:无
* 函数说明:消费侧取数的唯一入口(按语义,不写死下标)。
*/
int adc_input_raw(adc_semantic_t ch, int32_t *out) {
if ((out == NULL) || ((unsigned int)ch >= (unsigned int)ADC_SEM_COUNT)) {
return -1;
}
if (k_adc_map[ch].source == ADC_SRC_TPAFE) {
if (s_tpafe_valid == 0u) {
return -2;
}
*out = (int32_t)s_tpafe_raw[k_adc_map[ch].index];
} else {
if (s_stm32_valid == 0u) {
return -2;
}
*out = (int32_t)s_stm32_raw[k_adc_map[ch].index];
}
return 0;
}
/*
* 函数功能:读取 STM32 ADC1 最近一次扫描状态码
* 入口参数:无
* 返 回 值:ADC_INPUT_RC_xxx
* 限定条件:无
* 函数说明:供心跳日志 / 自检观测 STM32 侧健康度。
*/
int adc_input_stm32_rc(void) {
return s_stm32_rc;
}
/*
* 函数功能:读取 STM32 ADC1 停摆故障累计次数
* 入口参数:无
* 返 回 值:DMA 挂载失败次数 + "连续 >=2 拍无新帧"的次数
* 限定条件:无
* 函数说明:单调递增,供心跳日志观测。单拍无新帧(相位伪影)**不计入**,
* 丢帧由 adc_input_stm32_gaps() 单独计数。
*/
uint32_t adc_input_stm32_errs(void) {
return s_stm32_errs;
}
/*
* 函数功能:读取 STM32 ADC1 累计丢帧点数
* 入口参数:无
* 返 回 值:Σ(完成计数每次前进的超出部分),即被乒乓缓冲覆盖掉、没被消费的帧数
* 限定条件:无
* 函数说明:这是"消费者超拍"的直接证据(本任务单拍耗时 > 1 ms,如心跳日志突发期);
* 与 adc_input_stm32_errs()(节拍源停摆)是两类不同的故障,**勿混读**。
* 单位与判定层 TPAFE 侧 adc_health_tpafe_gaps() 一致(都是帧点数),心跳里
* 可直接对比;注意 2026-10-21 之前固件该字段是"次数"口径。
*/
uint32_t adc_input_stm32_gaps(void) {
return s_stm32_gaps;
}
/*
* 函数功能:读取 STM32 ADC1 的 DMA 整帧完成计数
* 入口参数:无
* 返 回 值:DMA 半区完成中断 + 传输完成中断的累计次数(每拍 +1)
* 限定条件:无
* 函数说明:这是 STM32 侧"节拍是否还在喂数据"的直接观测量:与 TPAFE 的
* tpafe5160_frame_seq() 对称。它若停增而 TIM3 仍在跑 → DMA/ADC 侧停摆。
*/
uint32_t adc_input_stm32_seq(void) {
return s_dma_seq;
}
/*
* 函数功能:读取 STM32 ADC1 的 HAL 层错误回调累计次数
* 入口参数:无
* 返 回 值:HAL_ADC_ErrorCallback 触发次数(DMA 传输错误等)
* 限定条件:无
* 函数说明:与 adc_input_stm32_errs()(消费侧"无新帧")分开计数,便于区分
* "节拍源停了"与"DMA 出错"两类故障。
*/
uint32_t adc_input_stm32_hw_errs(void) {
return s_stm32_hw_errs;
}
/* ===== HAL 回调(ISR 上下文)=====
* ⚠ 三个回调都在中断里跑(DMA2_Stream4_IRQn,抢占优先级 6)→ **只允许**写 volatile
* 计数/标志,不得调用任何 CMSIS-RTOS API、不得日志、不得拷贝大缓冲。
* 搬运与消费全在任务侧(stm32_take())。 */
/*
* 函数功能:DMA 传输完成回调 = 后半区(下标 1)9 个采样就绪
* 入口参数:hadc - 触发回调的 ADC 句柄
* 返 回 值:无
* 限定条件:仅 ISR 上下文;覆盖 HAL 的 __weak 实现(本板只有 ADC1)
* 函数说明:见 stm32_take() 的奇偶推半区口径。
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
if (hadc == &hadc1) {
s_dma_seq++;
}
}
/*
* 函数功能:DMA 半传输完成回调 = 前半区(下标 0)9 个采样就绪
* 入口参数:hadc - 触发回调的 ADC 句柄
* 返 回 值:无
* 限定条件:仅 ISR 上下文;覆盖 HAL 的 __weak 实现
* 函数说明:DMA 长度 18 = 2 x 9,故半传输即"一帧就绪",与传输完成交替出现。
*/
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) {
if (hadc == &hadc1) {
s_dma_seq++;
}
}
/*
* 函数功能:ADC/DMA 错误回调
* 入口参数:hadc - 触发回调的 ADC 句柄
* 返 回 值:无
* 限定条件:仅 ISR 上下文;覆盖 HAL 的 __weak 实现
* 函数说明:只计数。错误后 DMA 可能已停 → 消费侧会看到 s_dma_seq 停增(rc=STALL、
* 每拍 skip),并由 IWDG 心跳日志暴露,不在 ISR 里尝试恢复。
*/
void HAL_ADC_ErrorCallback(ADC_HandleTypeDef *hadc) {
if (hadc == &hadc1) {
s_stm32_hw_errs++;
}
}