Files
STM32F4-Base/docs/测试说明/ADC输入层单元测试说明.md
T
wandering1 4c74cfa825 refactor(编号·批次二): 文档侧 194 处保护功能号改成功能名,定义表 7 张首格改语义标识
批次二文档半(道 gate-docs 执行)。先纠三个数的口径账,别再拿任何旧数当准:**234 处是准数** —— 卡片写
52(Lead 用了门禁 Checker 的引用集合,它**刻意排除属主文档内部的定义位**)、道里先报 265(裸正则把
`SDL2-2.32.10` 里的 `L2-2` 当成功能号,误报 10 处)。最终边界口径 = 带左边界 + 跳过反引号代码段。

· 19 只文件 HEAD 共 234 处 → 现 40 处,**40 处 100% 落在变更记录行**(冻结历史,按 Q3 一条没动),
  非变更记录残留 **0**;改写 163 行 / 16 只文档。站点最多:规划文档 110→37、相量层单测说明 25→1、
  采集与数据处理短板评测 21→0、App/algo/README 26→0。
  (`App/algo/README.md` 由上一笔代码提交带上车,路径归属已在该笔正文注明,内容是本批改写)
· **定义表首列由编号改成语义标识**(编号规范 §3.3「表首格就是短语」的完全形态):首列原为编号的表
  共 **7 张**(不是 5 张),删掉与之重复的「语义名」列;表头标签按域词区分——三张 L3 保护**元件**表
  保留 `| 元件 |`,四张 L1/L2 表用 `| 功能名 |`(把「元件」统一洗成功能名会丢掉领域信息)。
  七张表逐张核:表头竖线数 == 分隔行 == 首条数据行 == 列数+1;与 HEAD 逐块对拍**净新增不一致块 0**,
  并且正是这个对拍抓到第一版「三张表数据行已删列、表头没删」的破表。
· 7 处删号留下的残词/重复解释括号人工收口(如「L1-5(通道标定)」→「通道标定(一次系数 + 零漂)」),
  复扫待收口站点 0;措辞逐字取自映射表,无第二种说法。
· tools/arch_diagram 三只生成器 4 处(3 处 docstring + 1 处图形文案)与产物同批:重跑后**只有位图那张
  变 2 个 text 节点的换行位置**,画布 1580x740 与总行数不变,另两张逐字节未变;改前先干跑对照证明三张
  svg 原本与生成器一致(不在 stale 基线里)——`check_generated_fresh --only docs-svg` PASS。

本笔另含 **F-* 半批的文档面**(同一轮 `git add docs tools/arch_diagram AGENTS.md` 一并收进
,非误伤):BSD
函数调用关系里 9 处反引号紧邻指针、属主文档 §7 的停发句与导读行(**12 条定义行原样保留、不删登记族**)、
以及 `gen_net_instability_svg.py` 与它的产物 `网络不稳根因总览.svg`(「唯一读者红线」那条的框宽自适应
526 → 551,右缘 1411 未越界,画布 1560x1468 不变)——该半批的**代码注释面**在下一笔。

⚠ 因 `docs/任务规划/登记簿.md` 与 `docs/任务规划/UI补齐.md` 正被另一张卡(UI 叶数复核)改动,本笔**不含
那两只文件**,批次二的 §9 存量收尾与它们同批提交。
2026-10-27 09:05:24 +08:00

20 KiB
Raw Blame History

ADC 输入层单元测试说明(主机侧)

模块:ADC Input Layer(L1 采集与预处理,App/algo/adc_input.c) 测试类型:主机侧纯逻辑单元测试(L0.5 层)——在 PC 上离线验证语义通道映射、 STM32 ADC1 的 DMA 乒乓帧消费(含半区选择)、TPAFE 快照消费与取数守卫,零硬件、零烧录、 毫秒级、不占 MCU 的 RAM。 状态:✅ 已落地(101 用例,/W4 0 告警,已挂 pre-commit 钩子) 日期:2026-10-10 关联文档: ../../AGENTS.md(平台约束 · CubeMX 生成区铁律)、 测试开发标准流程.md(测试分层与门禁总览)、 配置文件单元测试说明.md(同层"强符号桩"范本)、 相量层单元测试说明.md(同批落地的 L2 姊妹篇)、 ../硬件平台/ADC通道映射说明.md(语义映射权威口径)、 ../算法/继电保护功能复现规划.md(「STM32 ADC1 启动与扫描」/「通道语义映射」设计依据)、 快照语义约定的历史审计(属主文档已于 2026-10-15 删除)


1. 概述

1.1 为什么单独做主机侧单测

adc_input.c 把两片转换器的原始码值按语义统一暴露,其正确性的核心是三者一致:

半边 内容 可测性
纯逻辑(被测) ① 语义通道映射表(「通道语义映射」):UA/UB/UC/UN/IA/IB/IC/IN/TEMP → (来源, 下标);② STM32 ADC1 帧消费(「STM32 ADC1 启动与扫描」,v2.0 DMA 模型):Rank 顺序 ↔ ADC1 通道号 ↔ 语义 + 乒乓半区选择;③ 取数守卫与 poll 契约;④ 状态码与四类故障计数(seq/gap/err/hw) 纯查表 + 状态机,与硬件无关 → 可在 PC 上位穷举
硬件副作用 HAL_ADC_Start_DMA()(挂载)与被测代码自己实现的三个 HAL 弱回调 HAL_ADC_Conv[Half]CpltCallback / HAL_ADC_ErrorCallback;tpafe5160_snapshot()(临界区) 挂载是外部符号 → 主机侧给同名强符号桩;三个回调由被测代码定义 → 测试直接调用它们注入"DMA 完成"

板端套件(test/board/)无法验证本层的核心判据:

  • 通道映射:板端无信号源(见 docs/硬件平台/ADC通道映射说明.md §8),无法注入已知电压去 验证"读到的是哪只脚"。而映射表一旦写错(本项目 V0.1 有 13/17 路被置换),读到的会是 另一只脚或恒为陈旧值,且不会有任何报错——这正是本层最危险的失效模式。
  • HAL 故障路径:HAL_ADC_Start_DMA() 挂载失败、DMA 完成计数不前进(节拍源停摆)、 超拍丢帧、HAL 错误回调在真机上极难稳定复现,且读错乒乓半区这种缺陷在板端只会表现为 "读数偶尔怪"、无法定位。

主机侧的做法:给每个 Rank 注入互不相同的码值(第 i 个 Rank 写成 100+i),把 "Rank 顺序 ↔ ADC1 通道号 ↔ 语义"整条链路逐通道钉死;"DMA 完成"由测试直接调用被测代码 实现的半传输/传输完成回调注入(dma_beat()),故障路径用可注入桩一次覆盖。

⚠ 本单测只覆盖「STM32 ADC1 启动与扫描」与「通道语义映射」。「通道标定(一次系数 + 零漂)」本层不做,故取到的是未标定原始码值; 「周期 CONVST 重触发」与「TPAFE 快照消费」由 adc_task + TPAFE 驱动承担,板端套件覆盖。

1.2 被测对象与桩(不改生产代码)

adc_input.c 的全部平台依赖都是外部符号,故测试单元只需提供同名强符号桩, 生产代码零改动(无任何 #ifdef 测试钩子)。

test/host/adc_input/adc_input_test.c
    ├── #include "adc.h"          → stub/adc.h        (HAL_ADC_* 原型 + hadc1 + ADC_CHANNEL_0..15)
    ├── #include "tpafe5160.h"    → stub/tpafe5160.h  (仅 tpafe5160_snapshot 原型)
    └── #include "adc_input.c"    ← 生产源文件(App/algo/),原样纳入

adc_input.h 用真实头:它只依赖 <stdint.h>,不拖 HAL,故接口契约原汁。 adc.h / tpafe5160.h 必须用桩:真实 Inc/adc.h 经 main.h 拖入整套 HAL; 真实 Drivers/BSP/TPAFE5160/tpafe5160.h 拖入 GPIO 宏——主机侧无法编译。 桩目录必须排在 App/algo 之前(include_dirs 顺序敏感,见 run_unit_test.py)。

平台接口 MCU 端 主机侧桩
hadc1 / hadc_other MX_ADC1_Init() 配置的 ADC1 句柄 两个占位对象:被测代码只比较指针(三个 HAL 回调的 hadc == &hadc1 守卫),从不读字段;hadc_other 专门用来证伪守卫
HAL_ADC_Start_DMA 挂载循环 DMA,长度 2×9(此后每拍由硬件自走) 记录 s_start_dma_calls / s_dma_buf / s_dma_len,返回 s_start_dma_rc(可注入 HAL_ERROR)
HAL_ADC_ConvHalfCpltCallback / HAL_ADC_ConvCpltCallback / HAL_ADC_ErrorCallback 由被测代码实现(覆盖 HAL 弱符号),ISR 上下文 测试用 dma_beat(base) 直接调用:按 seq & 1 决定往哪个半区填 base+i,再触发对应回调——与消费侧的奇偶推导互相证伪
tpafe5160_snapshot 临界区原子快照最新一帧 返回 s_snapshot_rc(1/0/-1);rc==1 时从 s_fake_tpafe[] 拷贝;记录 count 与 dst

桩里 ADC_CHANNEL_0..15 的宏值等于通道号(F4 HAL 即如此),故 (uint8_t)ADC_CHANNEL_14 == 14 与真机一致——这是"映射判据"能成立的前提。

1.3 测试环境

依赖 版本 / 说明
编译器 本机 MSVC(VS2019/2022/2026 任一),由 run_unit_test.py 自动探测
Python 仓库 .venv 或任意 python3(纯标准库)
硬件 无(不需要开发板 / 串口 / ST-LINK / 信号源)

工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托通用 harness ../../test/host/common/unit_test_harness.py, 本模块 run_unit_test.py 只声明 TestBuild(源文件 / 桩目录 / 汇总行标记)。

1.4 运行方式

# 仓库根目录(统一用 .venv)
& '.venv\Scripts\python.exe' test/host/adc_input/run_unit_test.py

退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。

1.5 回归接入

项 说明
触发范围 App/algo/adc_input.c / adc_input.h,或 test/host/adc_input/**、test/host/common/unit_test_harness.py 变动
门禁层级 L0.5 层(介于 L0 静态门禁与 L1 板端套件之间):主机侧、秒级、纯逻辑
pre-commit 钩子 adc-input-unit-test(本地钩子,.pre-commit-config.yaml §7e)
汇总行 === ADC Input Tests: 70/70 PASSED (failed=0) ===
与板端测试的关系 互补:本单测保证映射 / 契约 / 故障路径正确;板端保证 HAL 配置(Rank 顺序、采样时间、引脚) 与真机一致

2. 测试用例

命名 TC-AI-NNN(AI = Adc Input;主机侧单测自定前缀,与板端套件缩写表互不影响)。 全部在 adc_input_test.c 内,主机侧运行。被测源文件被 #include 纳入同一编译单元, 故静态函数(map_check / stm32_take)亦可直接调用(白盒)。

2.1 初始化与复位(adc_input_init)

用例 断言 验证的设计点
TC-ADCIN-001 前置:注入 HAL_ADC_Start 失败 → poll 后失败计数 = 1 统计可见性(前置构造)
TC-ADCIN-002 前置:注入 TPAFE 有效帧 → poll 后 UA 可取数且值正确 取数通路(前置构造)
TC-ADCIN-003 adc_input_init() 返回 0 映射表自检通过
TC-ADCIN-004 init 后 adc_input_stm32_rc() == ADC_INPUT_RC_NOT_RUN 状态码复位
TC-ADCIN-005 init 后 adc_input_stm32_errs() == 0 失败计数归零
TC-ADCIN-006 init 后 TPAFE / STM32 两侧 adc_input_raw() 均返回 -2 有效标志被清(不会取到上一轮陈旧值)

2.2 语义映射表(adc_input_desc)

用例 断言(语义 → 来源 / 下标) 权威口径
TC-ADCIN-010 UA → TPAFE buf[6] docs/硬件平台/ADC通道映射说明.md
TC-ADCIN-011 UB → STM32 CH14 同上
TC-ADCIN-012 UC → STM32 CH3 同上
TC-ADCIN-013 UN → TPAFE buf[5] 同上
TC-ADCIN-014 IA → TPAFE buf[2] 同上
TC-ADCIN-015 IB → STM32 CH6 同上
TC-ADCIN-016 IC → STM32 CH1 同上
TC-ADCIN-017 IN → STM32 CH2 同上
TC-ADCIN-018 TEMP → STM32 CH5(V_ZL2) 同上
TC-ADCIN-019 adc_input_desc(ADC_SEM_COUNT) → NULL 越界防御
TC-ADCIN-01a adc_input_desc((adc_semantic_t)-1) → NULL 无符号比较兜底

2.3 映射表自检(map_check,静态)

用例 断言 验证的设计点
TC-ADCIN-020 map_check() == 0 真实映射表自洽:TPAFE 下标 < 8;每个 STM32 映射通道都出现在扫描 Rank 内

反向路径(表不自洽 → -1)结构上不可达,见 §4.1。

2.4 STM32 ADC1 帧消费(stm32_take,静态;v2.0 DMA 模型)

用例 断言 验证的设计点
TC-ADCIN-021 有新帧 → 返回 0 正常路径
TC-ADCIN-022 adc_input_stm32_rc() == RC_OK 状态码
TC-ADCIN-023 成功不计入失败计数 统计语义
TC-ADCIN-024 HAL_ADC_Start_DMA 只在 init() 挂一次 外设生命周期(每拍不重启)
TC-ADCIN-025 DMA 完成计数 seq = 1 计数只由 HAL 回调推进
TC-ADCIN-026 ⭐ Rank → 通道号映射:UB = CH14 / rank9 = 108 本层核心判据(板端无法验)
TC-ADCIN-027 UC = CH3 / rank4 = 103 同上
TC-ADCIN-028 IB = CH6 / rank7 = 106 同上
TC-ADCIN-029 IC = CH1 / rank2 = 101 同上
TC-ADCIN-02a IN = CH2 / rank3 = 102 同上
TC-ADCIN-02b TEMP = CH5 / rank6 = 105 同上
TC-ADCIN-02c 取帧后 UA(TPAFE)仍为 -2 两来源互相独立
TC-ADCIN-080..085 ⭐ 乒乓双缓冲:往"正在被 DMA 写的另一半区"填 900/800 系干扰值,stm32_take() 仍必须读到刚完成那一半(108 / 208) 撕裂帧防线(读错半区 = 半新半旧混合帧)
TC-ADCIN-086..089 两档超拍:连投 2 帧不消费 → 取到最新帧(408)且 gaps() = 1;再连投 3 帧不消费(一次跨 2 帧)→ gaps() 累计到 3,把单位钉成丢掉的帧点数(2026-10-21 由「次数」改来,与判定层 tgap 同单位);errs() 始终 = 0 丢帧与停摆分开计数(消费者超拍 ≠ 节拍源停);两档并列才分得出「次数 vs 点数」
TC-ADCIN-090..09A HAL_ADC_ErrorCallback(&hadc1) ×2 → hw_errs() = 2;用别的句柄调三个回调 → seq / hw_errs 均不动 三个 HAL 弱回调的 hadc == &hadc1 句柄守卫
TC-ADCIN-030..034 HAL_ADC_Start_DMA 返回 HAL_ERROR → init() 返回 -2、状态码 RC_START、计数 +1、STM32 侧无有效数据 挂载失败(一次性,非每拍)
TC-ADCIN-035..039 无任何 DMA 完成 → RC_STALL、单拍不计故障、seq 不前进、STM32 侧无有效数据 无新帧路径 + 相位伪影不误报
TC-ADCIN-03a..03e 连续两拍无新帧 → errs() = 1;第三拍 → 2;恢复拍成功且 gaps() 仍 0 停摆判据 = 连续 ≥2 拍;跨次单调累计
TC-ADCIN-070..078 ⭐ 停摆拍不得沿用上一拍旧值:先成功一拍让 STM32 侧进入有效态(UB=108),再注入无新帧拍 → adc_input_raw() 必须返 -2;恢复拍重新可取数 陈旧帧(2026-10-10 真机实测发现的缺陷,见下注)

⚠ 为什么 TC-ADCIN-034 照不出这条缺陷:它是"adc_input_init() 后立刻失败",此时 s_stm32_valid 本来就是 0,断言 -2 恒成立。真正的缺陷路径是成功过之后再失败: 本层只有一份 s_stm32_raw[],而有效标志此前只在成功时置 1、失败时从不清零 → 消费侧照样取数、照样压入采样环(adc_input_poll() 返回值只由 TPAFE 决定, s_frames_skip 抓不到),直接污染 20 点 DFT 窗。 修复 = 失败支路清 s_stm32_valid(App/algo/adc_input.c)。 这组用例在轮询版与 DMA 版都保留:触发条件从"轮询超时"变成"本拍无新帧", 同一条缺陷、同一个防线。 反向验证已做(v2.0):把停摆支路改回 valid=1 → TC-ADCIN-039 / 073 / 074 / 074B 四条立即 FAIL(89/93 时点);把半区奇偶取反 → 11 条 FAIL;STALL_RUN_MAX 改 1 → 6 条 FAIL; gap 阈值改 >2 → 1 条 FAIL → 证明这些用例真的咬得住。

2.5 取数与参数守卫(adc_input_raw)

用例 断言 验证的设计点
TC-ADCIN-040 out == NULL → -1 参数防御
TC-ADCIN-041 ch == ADC_SEM_COUNT → -1 越界防御
TC-ADCIN-042 ch == (adc_semantic_t)-1 → -1 负数兜底
TC-ADCIN-043 参数错时不写 *out 无副作用
TC-ADCIN-044 TPAFE 无有效帧 → -2 有效标志语义
TC-ADCIN-045 STM32 无有效扫描 → -2 有效标志语义
TC-ADCIN-049 UA → buf[6] 的值(注入 3006) 按下标取数(非按枚举序)
TC-ADCIN-04a UN → buf[5] 的值(注入 2005) 同上
TC-ADCIN-04b IA → buf[2] 的值(注入 -1234) 16 位补码负值原样保留(不截断 / 不取绝对值)

2.6 采集一拍(adc_input_poll)

用例 断言 验证的设计点
TC-ADCIN-046 快照返回 1 → poll() 返回 1 返回码透传
TC-ADCIN-047 快照被请求 8 通道 与 ADC_INPUT_TPAFE_CH_NUM 一致
TC-ADCIN-048 快照目的缓冲非空 不传 NULL 给驱动
TC-ADCIN-050 快照返回 0(无新帧)→ poll() 返回 0 无新帧语义
TC-ADCIN-051 无新帧时 TPAFE 侧无效 不置有效标志
TC-ADCIN-052 同拍 STM32 扫描照常执行(rc == RC_OK) 两侧独立
TC-ADCIN-053 快照返回 -1(参数错)→ poll() 返回 -1 错误透传
TC-ADCIN-054 参数错时 TPAFE 侧无效 不置有效标志
TC-ADCIN-055 已有有效帧后再"无新帧" → 保持上次有效值 有效标志只置不清
TC-ADCIN-056 STM32 失败不影响 poll() 返回值(仍为 1) 两侧解耦
TC-ADCIN-057 STM32 失败不影响 TPAFE 取数 同上
TC-ADCIN-058 STM32 失败另由 adc_input_stm32_rc() 暴露 故障通道分离

2.7 状态码与失败计数观测

用例 断言
TC-ADCIN-060 init 后未取帧 → RC_NOT_RUN(-3)
TC-ADCIN-061 init 后各计数 = 0
TC-ADCIN-062 一次成功后 rc == OK 且 errs 不变
TC-ADCIN-063 连续两拍无新帧 → errs == 1(单拍伪影不计)
TC-ADCIN-064 rc = 最近一次(RC_STALL)
TC-ADCIN-065 恢复成功后 rc == OK
TC-ADCIN-066 errs 单调不回退(不因成功回落)
TC-ADCIN-067 seq 只由 DMA 完成回调推进(消费动作不改它)

当前结果:101/101 PASSED(failed=0)。


3. 如何扩展用例

  1. 在 adc_input_test.c 加一个 static void tc_xxx(void);
  2. 用例开头调 fakes_reset()(把全部桩复位到"健康"初值 + 复位观测计数);
  3. 用 s_start_rc / s_poll_fail_at / s_poll_vals[] 注入 HAL 行为; 用 s_snapshot_rc / s_fake_tpafe[] 注入 TPAFE 帧;
  4. CHECK(cond, "描述") 断言;在 main() 里注册(并同步更新本文档 §2 与变更记录)。

⚠ fakes_reset() 必须在新用例开头调用:桩计数器(s_poll_calls / s_rank_idx / s_start_calls / s_stop_calls / s_snapshot_calls)跨用例共享,不复位会互相污染。

⚠ 模块静态态(s_stm32_raw / s_tpafe_raw / s_*_valid / s_stm32_rc / s_stm32_errs) 由 adc_input_init() 复位;每个用例开头都应调一次 adc_input_init()(等价"刚上电")。


4. 已知边界与设计取舍

4.1 map_check() 反向路径结构上不可达

map_check() 的失败分支要求"映射表指向 TPAFE 下标 ≥8,或 STM32 通道号不在扫描 Rank 内"。 但 k_adc_map[] 与 k_stm32_rank_ch[] 都是编译期 static const,没有任何运行时输入 能使其不自洽(曾尝试用 #define 重定向表名,会与生产文件内的定义撞成重复定义)。 因此本单测只覆盖正向路径(map_check() == 0);反向路径的等效保障是 adc_input_init() 的返回值——它在开机时自检并留痕(见 app_main.c)。

4.2 本层不做标定(「通道标定(一次系数 + 零漂)」待办)

adc_input_raw() 返回的是未标定原始码值(TPAFE = 16 位补码,STM32 = 0..4095)。 一次系数 / 零漂标定属 「通道标定(一次系数 + 零漂)」,尚未实现,故本单测不对"物理量换算"做任何断言。

4.3 不覆盖 V0.1 的硬件缺陷(双转换器无共同采样触发)

V0.1 上 8 路电气量分属两片转换器且无共同采样触发(登记簿条目「双转换器无共同采样触发」),本层只做逐通道独立监视。 "跨转换器相量合成"(线 / 负序 / 零序)不得在本层或 L2 上做——属「线/负序/零序合成」且 V0.1 受限 (见 docs/算法/继电保护功能复现规划.md §1.6)。


5. 变更记录

日期 变更
2026-10-10 创建;70 用例全绿(/W4 0 告警);覆盖 adc_input.c 全部 8 个函数(6 公开 + 2 静态);挂 pre-commit 钩子 adc-input-unit-test
2026-10-10 新增 TC-AI-070..078(70 → 79 用例):钉死"失败拍不得沿用上一拍旧值"。配套修复 App/algo/adc_input.c 两条失败支路(清 s_stm32_valid)。已做反向验证:改回旧行为 → TC-AI-039b/073/074 立即 FAIL(76/79),证明用例有效
2026-10-10 v2.0 DMA 模型改造(79 → 101 用例):被测代码 stm32_scan()(逐通道 PollForConversion)换成 stm32_take()(消费 DMA 乒乓半区)。桩面同步:删 HAL_ADC_Start/PollForConversion/GetValue/Stop,加 HAL_ADC_Start_DMA 与 hadc_other;"DMA 完成"由测试直接调用被测代码自己实现的三个 HAL 弱回调注入(dma_beat())。新增三组:⭐ TC-AI-080..089 乒乓半区(往正在写的另一半填干扰值仍须读到刚完成那一半)+ 超拍丢帧;TC-AI-090..09A HAL 错误回调与 hadc == &hadc1 句柄守卫;TC-AI-03A..03E / 067 停摆(连续 ≥2 拍)与丢帧(seq 前进 >1)分开计数。TC-AI-070..078 陈旧帧组原样保留(触发条件由"轮询超时"变"本拍无新帧",同一防线)。反向验证四连:半区奇偶取反 → 11 条 FAIL;停摆拍置 valid=1 → 4 条 FAIL;STALL_RUN_MAX 改 1 → 6 条 FAIL;gap 阈值改 >2 → 1 条 FAIL