Files
STM32F4-Base/docs/测试说明/单元测试/ADC输入层单元测试说明.md
wandering1 f754d59218 docs(测试说明): 按用途分六个子目录并同步全库引用
分层:流程与规范/ 单元测试/ 板级测试/ 专项验证/ 测试工具/ 测试数据/,
顶层只留新增的目录导航 README.md(归层判据 + 硬约束 + 本次处置记录)。

- 搬迁 28 个路径:测试数据三个目录 58 个文件为纯改名(R100,内容逐字节不变)
- 全库引用同步 124 个文件:AGENTS / docs 索引 / skills / 门禁注释 / scripts
- 门禁面:scripts/check_test_traceability.py 的 glob 改 rglob
  (分层后不递归扫描=追溯门禁直接瞎掉)
- scripts/rename_ref.py:收尾的 ℹ 提示在 GBK 代码页抛 UnicodeEncodeError,
  让进程以退出码 1 收场而改写其实已落盘(RC 与真实结果相反);改为把
  stdout/stderr reconfigure 成 UTF-8,与 check_doc_links.py 等同法
- docs/文档规范.md §1.1 补「子目录文件过多时再分一层」;§3.6 补 rename_ref
  搬整目录的两个实测坑:① 会改写目录内部文件的内容 ② 内部文件的 ../ 出链
  不补层级(本次 23 个被误改的证据文件已按 HEAD 原始字节逐字节还原)
- docs/README.md §6 改为与六个子目录一一对应,并补登原先漏登记的
  「启动日志原子性复验流程」一行
- .cubemx_survival_baseline.json:App/util/task_watchdog.c 的注释路径也随分层更新,
  该文件是存活审计的 watchdog-module 漂移锚点,按脚本自己的提示跑 --write 重建基线
  (实测重建后仅 watchdog-module 一条哈希变化)
- 铁律 4 例外(本次在任务树里改了单写者文件):AGENTS.md 与
  .pre-commit-config.yaml 只被 rename_ref 改写路径字面量(后者全在注释行),
  任务表块与钩子声明一字未动

验证:check_doc_links.py --sections-strict 覆盖全部 88 个改动 .md 全绿;
check_test_traceability.py 输出与主树逐行一致;196 个文本文件无 BOM、无新增
CR、无 U+FFFD、无行尾空白;陈旧引用面由主树 25 处降到 6 处(无新增断链)。
2026-10-30 07:16:07 +08:00

269 lines
18 KiB
Markdown
Raw Permalink 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 输入层单元测试说明(主机侧)
> **模块**:ADC Input Layer(语义取数与预处理,`App/algo/adc_input.c`)
> **测试类型**:**主机侧纯逻辑单元测试**——在 PC 上离线验证语义通道映射、
> STM32 ADC1 的 DMA 乒乓帧消费(含半区选择)、TPAFE 快照消费与取数守卫,零硬件、零烧录、
> 毫秒级、不占 MCU 的 RAM。
> **状态**:✅ 已落地(**103** 用例,`/W4` 0 告警,已挂 pre-commit 钩子)
> **日期**:2026-10-10
> **关联文档**:
> [`../../../AGENTS.md`](../../../AGENTS.md)(平台约束 · CubeMX 生成区铁律)、
> [`测试开发标准流程.md`](../流程与规范/测试开发标准流程.md)(测试分层与门禁总览)、
> [`配置文件单元测试说明.md`](配置文件单元测试说明.md)(同层"强符号桩"范本)、
> [`相量层单元测试说明.md`](相量层单元测试说明.md)(同批落地的 全周傅氏相量姊妹篇)、
> [`../../硬件平台/ADC通道映射说明.md`](../../硬件平台/ADC通道映射说明.md)(**语义映射权威口径**)、
> [`../../算法/继电保护功能复现规划.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/`)**无法每改必跑地验证本层的核心判据**:
- **通道映射**:台架只在 2026-10-27 一次性验过映射(万用表注入已知电压 + 逐路短接 AGND,8 路各自塌到
本路,实测记录 → `docs/硬件平台/ADC通道映射说明.md` §8.1),**不占 CI、不能每改必跑**;而映射表一旦
写错(本项目 V0.1 有 13/17 路被置换),读到的是**另一只脚**或**恒为陈旧值**,且**不会有任何报错**
——本层最危险的失效模式,必须由主机侧逐 Rank 钉死。
- **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`](../../../test/host/common/unit_test_harness.py),
本模块 `run_unit_test.py` 只**声明** `TestBuild`(源文件 / 桩目录 / 汇总行标记)。
### 1.4 运行方式
```powershell
# 仓库根目录(统一用 .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` 变动 |
| **门禁层级** | **主机侧纯逻辑单测**(介于静态门禁与板端测试套件之间):主机侧、秒级、纯逻辑 |
| **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 完成回调推进(消费动作不改它) |
**当前结果**:**103/103 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 路电气量分属两片转换器且**无共同采样触发**(登记簿条目「双转换器无共同采样触发」),本层只做**逐通道独立监视**。
"跨转换器相量合成"(线 / 负序 / 零序)不得在本层或 全周傅氏相量上做——属「线/负序/零序合成」且 V0.1 受限
(见 `docs/算法/继电保护功能复现规划.md` §1.6)。