Files
STM32F4-Base/App/algo/adc_input.c
T
wandering1 0f7b5a87fb chore(编号): 存量编号全量转语义短语,门禁识别面与语料同批重打
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号)
一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、
采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。

门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏
undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8
reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF;
id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。

test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字,
是本批唯一改变构建产物的文件集合。

为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged
部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、
不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。

实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01%
(基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 /
告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。

基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处
引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使
识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。

⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二
根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有),
今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。

固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0;
App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889
断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿;
check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带
BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。

.cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法:
Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。

同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%,
RW_CCM 57,376 B / 87.9%,阈值 92%。
2026-10-26 12:28:14 +08:00

402 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 输入层(L1 采集与预处理)
* 模块功能:语义通道映射(L1-4)+ STM32 ADC1 采样消费(L1-3,TIM3 TRGO 触发 +
* DMA 双缓冲)+ TPAFE 快照消费。
* 消费侧经 adc_input_raw() 按语义取数,不写死下标。
* 适用平台:STM32F407ZGTx + TPAFE5160
* 作者:王建锋
* 创建日期:2026-10-10
* 修改记录:
* v1.0 2026-10-10 王建锋 创建:L1-3(STM32 ADC1 扫描)+ L1-4(语义映射)
* 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
/* ===== 语义映射表(L1-4)=====
* 权威口径: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 采样(L1-3,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++;
}
}