L1-6 采样有效性校验:adc_health 判定层 + 告警编排 + 31 用例单测 + 台架闭环

新增 App/algo/adc_health.c/h(纯逻辑判定层,零平台依赖免桩单测):
- 静默:两侧帧序号连续 >=100ms 无前进(电平位可自愈,flags() 为 L5-4 闭锁预留)
- 丢帧:TPAFE 序号一次前进 >1 计 tgap(与 STM32 侧 adc1_gap 对称,边沿事件)
- 单调性:序号倒退锁存位;tick/计数差值法 (int32_t)(a-b) 回绕安全
- 错误增量:FRSTDATA / HAL 错误 / STM32 停摆计数增量,事件位与锁存位编号对齐,
  消费者按位图做"每类每次开机留痕一次"编排

adc_task.c v2.9 告警编排(判定与编排分离):严重事件/电平上升沿 DBG_ERROR +
SYSLOG_SELFTEST(每类每次开机一次防刷屏),下降沿 DBG_INFO 恢复;gap 不告警
(消费者超拍是已知现象,告警会淹没真故障);心跳行增 tgap 观测字段。

主机单测 test/host/adc_health/ 31 用例全绿(100ms 门限两侧 / 边沿语义 /
倒退锁存 / tick 回绕 / NULL 守卫 / init 幂等),登记 adc-health-unit-test 钩子。

台架 @test PASS:零 [ERR](健康不误报)、tgap=1->2 与 adc1_gap 同步可见、
frames 1000.0 Hz;证据 boot_log_data/20261011_prod_adc_health_boot_log.txt。

抬栈:adc_health_report->syslog_post 控制台回显尾使 adcTask 静态最坏链
832->1080 B(512 words 余 968 B < 1024 B ISR 底线)-> 512->640 words,
复测余 1480 B / 57%,堆 min_ever 7152->6640 B(-512 B 符合预算)。
AGENTS/README 任务表、RAM 方案 §2.3、规划文档 §3.1 L1-6 已同步置完成。
This commit is contained in:
2026-10-11 01:28:20 +08:00
parent 6c8f82ec54
commit 13a4e61c27
15 changed files with 1035 additions and 27 deletions
+14
View File
@@ -290,6 +290,20 @@ repos:
files: ^(App/sync/time_sync\.(c|h)|App/util/task_watchdog\.h|test/host/time_sync/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 7h) ADC 采样有效性校验(L1-6)主机侧单元测试(2026-10-11 加)
# 背景:adc_health.c 是"静默/丢帧/序号倒退/错误计数增量"的判定层——这些
# 故障在健康台架上**不可复现**(复现 = 拔芯片/改驱动),板端闭环只能证明
# "健康时不误报",证不了"故障时必报";100ms 门限、tick 回绕、事件边沿
# 语义等边界只有合成计数序列能精确构造。本钩子用 MSVC 离线编译真实源文件
# (#include 纳入,纯逻辑零桩)把 11 组判据一次性钉死。
# files 含被测源与其头、adc_task.c(告警编排的消费方),以及 harness 本身。
- id: adc-health-unit-test
name: ADC 有效性校验单测(主机侧)
entry: python test/host/adc_health/run_unit_test.py
language: system
files: ^(App/algo/adc_health\.(c|h)|App/task/adc_task\.c|test/host/adc_health/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 8) 测试套件开关门禁(2026-10-01 加)
# 背景:test_config.h 的 TEST_SUITE_* 决定固件里编入哪个测试套件,
# 全部注释 = 产品构建。但此前**无任何门禁检查**——实测已提交的 HEAD 里
+4 -4
View File
@@ -69,10 +69,10 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
| testTask | 2048 | BelowNormal |
| uiTask | 4096 | BelowNormal |
| timeSyncTask | 512 | Normal |
| adcTask | 512 | AboveNormal |
| adcTask | 640 | AboveNormal |
<!-- TASK_TABLE:END -->
(实际值以 `Src/freertos.c` 为准;testTask 仅在启用测试套件时创建。改动栈/缓冲前先核对此表与实测。2026-09-21 上调:defaultTask 512→1024(扛 mount/mkfs/FAT探针/cfg/syslog 深层 FatFS I/O,旧值经 `configCHECK_FOR_STACK_OVERFLOW=2` 钩子实测溢出)、testTask 1024→2048(存储套件 deep call chain + 局部缓冲溢出)。FreeRTOS 堆已迁 CCM,任务栈合计已不占 SRAM;2026-09-21 可观测性落地后实测:ftpTask 峰值曾达 3480B/4096B(余量 15%,低于 1024B 底线)→ 已按 `docs/内存管理/RAM使用分析与优化方案.md` §1.4 把 lftpd 9 处大局部缓冲静态化(BSS +4.6KB),复测峰值 **1184B/4096B(余量 71%)**;2026-10-10 新增 adcTask 后 **7 任务全 PASS**(adcTask 静态 560 B / 余 72%);同日 P1-5 后复测 adcTask **832 B / 余 59%**(新增 `osThreadFlagsWait` 调用链,仍高于 1024 B 底线))
(实际值以 `Src/freertos.c` 为准;testTask 仅在启用测试套件时创建。改动栈/缓冲前先核对此表与实测。2026-09-21 上调:defaultTask 512→1024(扛 mount/mkfs/FAT探针/cfg/syslog 深层 FatFS I/O,旧值经 `configCHECK_FOR_STACK_OVERFLOW=2` 钩子实测溢出)、testTask 1024→2048(存储套件 deep call chain + 局部缓冲溢出)。FreeRTOS 堆已迁 CCM,任务栈合计已不占 SRAM;2026-09-21 可观测性落地后实测:ftpTask 峰值曾达 3480B/4096B(余量 15%,低于 1024B 底线)→ 已按 `docs/内存管理/RAM使用分析与优化方案.md` §1.4 把 lftpd 9 处大局部缓冲静态化(BSS +4.6KB),复测峰值 **1184B/4096B(余量 71%)**;2026-10-10 新增 adcTask 后 **7 任务全 PASS**(adcTask 静态 560 B / 余 72%);同日 P1-5 后复测 adcTask **832 B / 余 59%**(新增 `osThreadFlagsWait` 调用链,仍高于 1024 B 底线);2026-10-11 L2 相量 + L1-6 有效性校验上链后静态最坏链 **1080 B**(`adc_health_report`→`syslog_post` 控制台回显尾)→ **adcTask 抬栈 512→640 words**,复测余 **1480 B / 57%**,堆 min_ever 7152→6640 B)
## 架构要点
@@ -82,8 +82,8 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
- **就绪屏障**(`App/app/sys_events.h` Event Flags,取代魔法延时):`SYS_EVT_NET_READY`(netTask 置位)/ `STORAGE_READY`(defaultTask:卷挂载 + 网络配置加载完成)/ `FTP_READY` / `TIMESYNC_READY`;消费者用 `osEventFlagsWait` 阻塞等待
- netTask:`net_stack_bring_up`(配置已由 defaultTask 在屏障前加载,本任务只读 RAM 副本 → `net_init` + PHY 强制 100M 全双工 → 递增网络代次号)→ 置 NET_READY → 循环 `net_poll` + `net_process_messages`;消费 `SYS_EVT_NET_RECONFIG` → 立即重建网络(新 IP 免重启生效,各 TCP 服务据代次号重建监听)
- defaultTask:`f_mount` forced(无卷自动 `f_mkfs` FAT32;挂载后卷健康探针:小写两击确认,FAT 区陈旧/擦除时自动重格式化自愈,`/config.ini` 回落工厂默认——2026-09-20 /log 写失败修复)→ 网络配置加载(`net_cfg_load`;旧档迁移已于 2026-10-10 删除,见「模块约定」网口配置条)→ STORAGE_READY → LED 闪烁;同时是 syslog(`/log/run.log`·`selftest.log`,定长 64B 记录)的**唯一写者**与**配置引擎属主**(`cfg_file` 静态态单任务收敛于此:UI 保存请求执行 `net_cfg_save_run` + 2s `cfg_file_poll`,经 `SYS_EVT_CFG_SAVE` 事件唤醒;netTask/uiTask 不得直接触碰配置引擎,见 P1-3)
- adcTask(**B1,2026-10-10;L1 层 v2.3;P1-5 事件驱动消费;v2.8 节拍源 TIM3 + STM32 侧 DMA,2026-10-10**):**纯消费者**——每拍调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 临界区原子快照(8 通道)+ **消费一帧 STM32 ADC1 DMA 双缓冲**(L1-3)→ 帧统计 + 每 10 s 心跳日志;取数一律经 `adc_input_raw()` **按语义**(L1-4,不写死下标);只取数、不做保护运算(V0.1 仅 TPAFE 三路 `UA`/`UN`/`IA` 测量有效,见 N-18)。**采样节拍(1 kHz)由 TIM3 更新事件硬件提供**(`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 TIM3 分支内调 `tpafe5160_start_conv_irq()`;同一更新事件经 **TRGO** 触发 ADC1 一轮 9 通道转换 → 两片转换器同源对齐);**消费唤醒与该节拍同源**——同一分支再调 `adc_tick_irq_notify()` 置位 `ADC_WAKE_FLAG`,任务用 `osThreadFlagsWait(..., osWaitForever)` 等唤醒(不再 1 ms 轮询;实测 `idle` 由 1~649 降到 **0**)。⚠ ISR 侧**只能**走 `adc_tick_irq_notify()`——它内含"消费者就绪"门(TIM3 在 `app_main_init` 就启动,比调度器早 ≈100 ms)。手写于 `Src/freertos.c` 的 `USER CODE` 区(同 timeSyncTask,512 words / **AboveNormal**),纳入 IWDG `required_mask`(`TASK_WD_ADC`)。详见 `docs/子系统设计/ADC采样节拍设计说明.md` §10、`App/algo/README.md`
- **采集与算法层(L1/L2)落在 `App/algo/`**(2026-10-10 建):现有 `adc_input.c/h` = L1(语义通道映射 + STM32 ADC1 扫描);L2 相量层待建。新算法模块一律进这里,勿塞进 `App/task/`(任务只做消费/编排);见 `App/algo/README.md`
- adcTask(**B1,2026-10-10;L1 层 v2.3;P1-5 事件驱动消费;v2.8 节拍源 TIM3 + STM32 侧 DMA,2026-10-10**):**纯消费者**——每拍调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 临界区原子快照(8 通道)+ **消费一帧 STM32 ADC1 DMA 双缓冲**(L1-3)→ 帧统计 + 每 10 s 心跳日志;取数一律经 `adc_input_raw()` **按语义**(L1-4,不写死下标);只取数、不做保护运算(V0.1 仅 TPAFE 三路 `UA`/`UN`/`IA` 测量有效,见 N-18)。**采样节拍(1 kHz)由 TIM3 更新事件硬件提供**(`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 TIM3 分支内调 `tpafe5160_start_conv_irq()`;同一更新事件经 **TRGO** 触发 ADC1 一轮 9 通道转换 → 两片转换器同源对齐);**消费唤醒与该节拍同源**——同一分支再调 `adc_tick_irq_notify()` 置位 `ADC_WAKE_FLAG`,任务用 `osThreadFlagsWait(..., osWaitForever)` 等唤醒(不再 1 ms 轮询;实测 `idle` 由 1~649 降到 **0**)。⚠ ISR 侧**只能**走 `adc_tick_irq_notify()`——它内含"消费者就绪"门(TIM3 在 `app_main_init` 就启动,比调度器早 ≈100 ms)。手写于 `Src/freertos.c` 的 `USER CODE` 区(同 timeSyncTask,**640 words** / **AboveNormal**——2026-10-11 L2 相量 + L1-6 有效性校验上链后静态最坏链 1080 B,512 words 余量跌破 1024 B 底线故抬栈),纳入 IWDG `required_mask`(`TASK_WD_ADC`)。详见 `docs/子系统设计/ADC采样节拍设计说明.md` §10、`App/algo/README.md`
- **采集与算法层(L1/L2)落在 `App/algo/`**(2026-10-10 建):现有 `adc_input.c/h` = L1(语义通道映射 + STM32 ADC1 DMA 消费)、`adc_health.c/h` = L1-6(采样有效性判定:静默/丢帧/单调性/错误增量,纯逻辑免桩单测)、`sample_ring.c/h` + `phasor.c/h` = L2(40 点采样环 + 全周傅氏 5 实例 + 双区交换,2026-10-11 台架闭环)。新算法模块一律进这里,勿塞进 `App/task/`(任务只做消费/编排);见 `App/algo/README.md`
- 全景 `docs/架构设计/项目架构总览.md`(六层分层 / 启动时序 / 存储·网络·UI 栈 / 目录组织)
## 硬件引脚
+36 -4
View File
@@ -12,11 +12,13 @@
| 文件 | 层 | 作用(一句话) | 主要对外入口 | 覆盖规划项 |
|---|---|---|---|---|
| `adc_input.h` / `adc_input.c` | L1 | **统一语义取数层**:把两片转换器的原始码值按语义(`UA…IN`/温度)暴露,并消费 STM32 ADC1 的 DMA 乒乓双缓冲 | `adc_input_init()` `adc_input_poll()` `adc_input_raw()` | L1-2 / L1-3 / L1-4 |
| `adc_health.h` / `adc_health.c` | L1 | **采样有效性判定层**:吃"计数快照"(帧序号/错误计数),产出静默/丢帧/单调性/错误增量故障位——判定与告警编排分离 | `adc_health_init()` `adc_health_check()` `adc_health_flags()` | L1-6 |
| `sample_ring.h` / `sample_ring.c` | L2 输入 | **采样环缓冲**:按语义通道保存最近 2 个周波(40 点)采样值,供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | L2 输入(为 L2-6 滑动窗供数) |
| `phasor.h` / `phasor.c` | L2 | **全周傅氏相量计算**:一周波 20 点单频 DFT → 基波/谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | L2-1 / L2-2 / L2-3 / L2-6 / L2-7 |
依赖关系(自下而上,无环):`adc_input.h`(纯枚举 + 声明)← `sample_ring` ← `phasor`。
`sample_ring.c` / `phasor.c` **零平台依赖**(只用 `math.h` / `stddef.h` 与本目录头);
依赖关系(自下而上,无环):`adc_input.h`(纯枚举 + 声明)← `sample_ring` ← `phasor`;
`adc_health` **独立**(仅 `stdint.h`,不依赖本目录其它头)。
`sample_ring.c` / `phasor.c` / `adc_health.c` **零平台依赖**(只用 `math.h` / `stddef.h` / `stdint.h` 与本目录头);
`adc_input.c` 另依赖 `adc.h`(`hadc1`)与 `tpafe5160.h`(快照),主机侧以桩头替身。
## 2. 数据流(一拍 = TIM3 1 kHz 节拍)
@@ -119,6 +121,33 @@ flowchart TD
- 每拍 20 点**全部重算**(暴力法,无累积误差;迭代加减首尾项会累积)。相量刷新周期 = 1 ms。
- 相位(L2-4)**未常驻**结构体,需要时自行 `atan2f(fIm, fRe)`。
### 3.4 `adc_health.c` / `adc_health.h` — L1-6 采样有效性判定
**解决的问题**:驱动侧只有"观测计数"(`tpafe5160_frame_seq/_frstdata_err`、
`adc_input_stm32_seq/_errs/_gaps/_hw_errs`),**没有判定**——芯片静默、消费者超拍丢帧、
计数器被改写这些故障此前只能靠人读心跳日志发现。本层把判定收进纯逻辑,
告警编排留在消费者(`adc_task.c` 的 `adc_health_report()`)→ 判定可主机免桩单测。
| API | 干什么 | 注意 |
|---|---|---|
| `adc_health_init()` | 清全部状态(幂等) | 首拍 `check()` 只建基线、不判定 |
| `adc_health_check(&in, &out)` | 吃计数快照做一次判定(每拍调,常数次整数运算) | 差值法 `(int32_t)(now-prev)`,tick/计数回绕安全 |
| `adc_health_flags()` | 电平故障位:`TPAFE_STALL` / `STM32_STALL`(≥100 ms 无前进,可自愈)/ `SEQ_BACKWARD`(锁存) | 为 L5-4 闭锁预留 |
| `adc_health_sticky()` | 锁存位(含 `FRSTDATA` / `STM32_ERR` / `HW_ERR` 增量痕迹) | 自愈后仍留痕 |
| `adc_health_tpafe_gaps()` | TPAFE 累计丢帧点数(序号前进 >1 的超出部分) | 与 STM32 侧 `adc_input_stm32_gaps()` 对称 |
**要点**
- **三类语义严格分开**:电平位(stall,可自愈)/ 锁存位(出现过必留痕)/ 边沿事件位
(仅当拍有效)。`ADC_HE_FRSTDATA/STM32_ERR/HW_ERR` 与同名 `ADC_HF_*` **编号对齐**,
消费者直接用事件掩码对"已留痕位图"做每类一次 `SYSLOG_SELFTEST` 的编排。
- **gap 不告警**:消费者超拍(心跳日志 ~2 ms)是已知现象,`adc1_gap`/`tgap` 每 10 s 心跳
可见;若按事件告警会把真故障淹掉(台架实测开机 `adc1_gap≈24`、稳态每 10 s +2)。
- **静默 ≠ 丢帧**:d=0 才是静默候选;d>1 只说明消费者没跟上,生产者还活着——两者
处置完全不同,判据不许混。
- 告警策略(在 `adc_task.c`,不在本层):严重事件/电平上升沿 `DBG_ERROR` +
`SYSLOG_SELFTEST`(每类每次开机一次,防刷屏写满 32KB selftest.log);下降沿 `DBG_INFO`。
## 4. 关键约定
- **语义映射唯一权威口径**:`docs/硬件平台/ADC通道映射说明.md`。取数一律经 `adc_input_raw()`,
@@ -138,6 +167,7 @@ flowchart TD
| `sample_ring.c` `s_ring[9][40]` float | **1440 B** |
| `phasor.c` `s_zone[2][5]` `phasor_t` | 160 B |
| `adc_input.c` `s_stm32_raw[16]` + `s_tpafe_raw[8]` | 48 B |
| `adc_health.c` 内部状态(9 × uint32 + uint8) | ≈ 40 B |
| 运行期栈:`phasor_run()` 的 `win[20]` | 80 B/帧 |
改容量(`SAMPLE_RING_CYCLES` / 实例数 / `ADC_SEM_COUNT`)前后各跑一次
@@ -145,7 +175,7 @@ flowchart TD
## 6. 构建与测试
- **挂载**:三个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`,`../App/algo` 已在 `<IncludePath>` 内。
- **挂载**:四个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`,`../App/algo` 已在 `<IncludePath>` 内。
本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路)。
- **主机侧单测(L0.5)**——生产源文件以 `#include` 原样纳入,无需改产品代码:
@@ -153,10 +183,12 @@ flowchart TD
|---|---|---|
| 相量层(57 用例:系数表逐项复核 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 双区交换) | `& '.venv\Scripts\python.exe' test/host/phasor/run_unit_test.py` | `phasor-unit-test` |
| ADC 输入层(逐 Rank 注入互异码值钉死映射 + HAL 启动失败/超时分支) | `& '.venv\Scripts\python.exe' test/host/adc_input/run_unit_test.py` | `adc-input-unit-test` |
| 有效性校验(31 用例:100 ms 静默门限两侧 / 丢帧边沿 / 倒退锁存 / 错误增量 / tick 回绕 / NULL 守卫 / init 幂等) | `& '.venv\Scripts\python.exe' test/host/adc_health/run_unit_test.py` | `adc-health-unit-test` |
改本目录任一 `.c/.h`,提交时对应钩子自动跑;详见 `docs/测试说明/相量层单元测试说明.md`。
- **板端**:`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍/心跳日志(`phasor(...)` 行、
`ring` / `skip` 计数、`adc1_rc` / `adc1_err`)。
`ring` / `skip` 计数、`adc1_rc` / `adc1_err`、`tgap`);健康判据 = 零 `[ERR]`
(`adc_health_report` 的告警全部以 `[ERR]` 开头 → 误报会被闭环直接抓住)。
## 7. 新增一个算法模块的固定动作
+200
View File
@@ -0,0 +1,200 @@
/*
* 模块名称:ADC 采样有效性校验(L1 采集与预处理,规划项 L1-6)
* 模块功能:见 adc_health.h 文件头。本文件为**纯逻辑实现**:输入计数快照、
* 输出故障位/事件位,零平台依赖(仅 stdint.h)→ 主机侧免桩单测。
* 适用平台:STM32F407ZGTx + TPAFE5160
* 作者:王建锋
* 创建日期:2026-10-11
* 修改记录:
* v1.0 2026-10-11 王建锋 创建:L1-6 采样有效性校验
*/
#include "adc_health.h"
/* ===================== 内部状态 ===================== */
/* 基线是否已建立:首次 check() 只记基线、不判定(开机首拍无参照系) */
static uint8_t s_primed;
/* 上次看到的计数(差值法判"有无前进",回绕安全) */
static uint32_t s_tpafe_seq;
static uint32_t s_tpafe_frst;
static uint32_t s_stm32_seq;
static uint32_t s_stm32_errs;
static uint32_t s_stm32_gaps;
static uint32_t s_stm32_hw;
/* 两侧"最后一次看到序号前进"的时刻(判静默) */
static uint32_t s_tpafe_adv_ms;
static uint32_t s_stm32_adv_ms;
/* 故障位 */
static uint32_t s_flags; /* 电平位(stall 可自愈;backward 锁存在此) */
static uint32_t s_sticky; /* 锁存位(含电平位历史并集) */
static uint32_t s_tpafe_gaps;
/* ===================== 对外接口 ===================== */
/*
* 函数功能:采样有效性校验初始化(幂等)
* 入口参数:无
* 返 回 值:无
* 限定条件:无(内核启动前可调)
* 函数说明:清全部内部状态;判定从下一次 check() 的首拍基线重新开始。
*/
void adc_health_init(void) {
s_primed = 0u;
s_tpafe_seq = 0u;
s_tpafe_frst = 0u;
s_stm32_seq = 0u;
s_stm32_errs = 0u;
s_stm32_gaps = 0u;
s_stm32_hw = 0u;
s_tpafe_adv_ms = 0u;
s_stm32_adv_ms = 0u;
s_flags = 0u;
s_sticky = 0u;
s_tpafe_gaps = 0u;
}
/*
* 函数功能:按计数快照做一次有效性判定
* 入口参数:in - 计数快照;NULL 则本次跳过(out 不动)
* out - 判定结果;NULL 则仅更新内部状态
* 返 回 值:无
* 限定条件:仅任务上下文(与 adc_input_poll() 同节奏,每拍一次)
* 函数说明:判定顺序 = 建基线 → 序号差值(倒退/丢帧/前进)→ 静默时长 →
* 错误计数增量。events 仅本次有效;flags/sticky 为持续状态。
*/
void adc_health_check(const adc_health_in_t *in, adc_health_status_t *out) {
uint32_t events = 0u;
if (in == 0) {
return;
}
/* 1) 首拍基线:只记录,不判定(开机时序号已非 0,无"上一拍"参照) */
if (s_primed == 0u) {
s_primed = 1u;
s_tpafe_seq = in->tpafe_seq;
s_tpafe_frst = in->tpafe_frst_err;
s_stm32_seq = in->stm32_seq;
s_stm32_errs = in->stm32_errs;
s_stm32_gaps = in->stm32_gaps;
s_stm32_hw = in->stm32_hw_errs;
s_tpafe_adv_ms = in->now_ms;
s_stm32_adv_ms = in->now_ms;
} else {
uint32_t d; /* 序号差值(两侧共用,cppcheck:作用域收在本块) */
/* 2) TPAFE 序号差值:倒退 = 单调性破坏(锁存);>1 = 丢帧(事件 + 计数) */
d = in->tpafe_seq - s_tpafe_seq;
if ((int32_t)d < 0) {
s_flags |= ADC_HF_SEQ_BACKWARD;
s_sticky |= ADC_HF_SEQ_BACKWARD;
s_tpafe_seq = in->tpafe_seq; /* 重新对齐,避免持续误判 */
s_tpafe_adv_ms = in->now_ms;
} else {
if (d > 1u) {
s_tpafe_gaps += (d - 1u);
events |= ADC_HE_TPAFE_GAP;
}
if (d >= 1u) {
s_tpafe_seq = in->tpafe_seq;
s_tpafe_adv_ms = in->now_ms;
}
}
/* 3) STM32 序号差值:同一口径(gap 计数在 adc_input 侧另有细账,
* 这里只取"前进/倒退/静默"三态,避免两处判据打架) */
d = in->stm32_seq - s_stm32_seq;
if ((int32_t)d < 0) {
s_flags |= ADC_HF_SEQ_BACKWARD;
s_sticky |= ADC_HF_SEQ_BACKWARD;
s_stm32_seq = in->stm32_seq;
s_stm32_adv_ms = in->now_ms;
} else if (d >= 1u) {
s_stm32_seq = in->stm32_seq;
s_stm32_adv_ms = in->now_ms;
}
/* 4) 静默判定(电平位,可自愈):距最后一次前进 >= 门限 */
if ((uint32_t)(int32_t)(in->now_ms - s_tpafe_adv_ms) >= ADC_HEALTH_STALL_MS) {
s_flags |= ADC_HF_TPAFE_STALL;
s_sticky |= ADC_HF_TPAFE_STALL;
} else {
s_flags &= ~ADC_HF_TPAFE_STALL;
}
if ((uint32_t)(int32_t)(in->now_ms - s_stm32_adv_ms) >= ADC_HEALTH_STALL_MS) {
s_flags |= ADC_HF_STM32_STALL;
s_sticky |= ADC_HF_STM32_STALL;
} else {
s_flags &= ~ADC_HF_STM32_STALL;
}
/* 5) 错误计数增量(事件 + 锁存):三类计数正常应恒为 0 增量 */
if (in->tpafe_frst_err != s_tpafe_frst) {
events |= ADC_HE_FRSTDATA;
s_sticky |= ADC_HF_FRSTDATA;
s_tpafe_frst = in->tpafe_frst_err;
}
if (in->stm32_errs != s_stm32_errs) {
events |= ADC_HE_STM32_ERR;
s_sticky |= ADC_HF_STM32_ERR;
s_stm32_errs = in->stm32_errs;
}
if (in->stm32_gaps != s_stm32_gaps) {
events |= ADC_HE_STM32_GAP;
s_stm32_gaps = in->stm32_gaps;
}
if (in->stm32_hw_errs != s_stm32_hw) {
events |= ADC_HE_HW_ERR;
s_sticky |= ADC_HF_HW_ERR;
s_stm32_hw = in->stm32_hw_errs;
}
}
if (out != 0) {
out->flags = s_flags;
out->events = events;
out->sticky = s_sticky;
out->tpafe_gaps = s_tpafe_gaps;
out->tpafe_stall_ms =
((s_flags & ADC_HF_TPAFE_STALL) != 0u)
? (uint32_t)(int32_t)(in->now_ms - s_tpafe_adv_ms)
: 0u;
}
}
/*
* 函数功能:读当前电平故障位
* 入口参数:无
* 返 回 值:见 adc_health.h ADC_HF_TPAFE_STALL/STM32_STALL/SEQ_BACKWARD
* 限定条件:无
* 函数说明:与 check() 出参 flags 同源。
*/
uint32_t adc_health_flags(void) {
return s_flags;
}
/*
* 函数功能:读锁存故障位
* 入口参数:无
* 返 回 值:电平位 + FRSTDATA/STM32_ERR/HW_ERR 的并集
* 限定条件:无
* 函数说明:出现过即留痕,直到 adc_health_init() 才清。
*/
uint32_t adc_health_sticky(void) {
return s_sticky;
}
/*
* 函数功能:读 TPAFE 累计丢帧点数
* 入口参数:无
* 返 回 值:序号前进 >1 的超出部分累计
* 限定条件:无
* 函数说明:与 STM32 侧 adc_input_stm32_gaps() 对称的 TPAFE 侧细账。
*/
uint32_t adc_health_tpafe_gaps(void) {
return s_tpafe_gaps;
}
+126
View File
@@ -0,0 +1,126 @@
/*
* 模块名称: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 采样有效性校验(静默/丢帧/单调性/错误增量)
*/
#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 错误计数有增量 */
/* 计数快照(由调用方采集,本模块不触碰任何驱动/外设) */
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_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);
/*
* 函数功能:读当前电平故障位
* 入口参数:无
* 返 回 值: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 */
+85 -6
View File
@@ -70,6 +70,13 @@
* 两片转换器由同一硬件事件驱动);STM32 侧改 DMA 双缓冲
* 消费,adc_input_poll() 不再忙等;心跳日志增
* adc1_seq / adc1_hw(adc1_err 语义改为"本拍无新帧")
* v2.9 2026-10-11 王建锋 L1-6 采样有效性校验:每拍调 adc_health_check()
* (App/algo/adc_health.c,纯逻辑判定:静默/丢帧/
* 单调性/错误计数增量)——严重事件与故障电平上升沿
* 打 DBG_ERROR + 每类每次开机一次 SYSLOG_SELFTEST;
* 恢复打 DBG_INFO;心跳日志增 tgap(TPAFE 侧丢帧,
* 与 adc1_gap 对称)。判定不在本文件(A-4:算法与
* I/O 分离),本文件只做"消费计数 → 告警"的编排
*/
#include "adc_task.h"
@@ -77,9 +84,11 @@
#include "adc_input.h" /* L1 采集层:adc_input_poll()/adc_input_raw()(按语义取数) */
#include "sample_ring.h" /* L2 输入:sample_ring_push_frame()(采样环缓冲) */
#include "phasor.h" /* L2 相量层:phasor_run()/phasor_get()(全周傅氏) */
#include "adc_health.h" /* L1-6:采样有效性校验(静默/丢帧/单调性/错误增量) */
#include "tpafe5160.h"
#include "task_watchdog.h"
#include "console_log.h"
#include "sys_log.h" /* SYSLOG_SELFTEST:采样故障留痕(仅故障才写) */
#include "task_prof.h" /* P2 常开探针:单拍耗时(Task #32) */
#define DBG_TAG "[ADC_TASK]"
@@ -97,6 +106,13 @@ static uint32_t s_frames_idle;
/* 因通道尚未首次成功而**整拍跳过入环**的次数(不向相量窗注入 0 点) */
static uint32_t s_frames_skip;
/* L1-6 告警编排状态:
* s_health_prev - 上次电平故障位(判上升/下降沿,避免每拍重复告警)
* s_health_logged- 已写 SYSLOG_SELFTEST 的位(每类每次开机只留痕一次,
* 防持续故障刷屏把 32KB selftest.log 写满) */
static uint32_t s_health_prev;
static uint32_t s_health_logged;
/* adcTask 的内核句柄,定义在 Src/freertos.c(引用方式同 App/task/default_task.c) */
extern osThreadId_t adcTaskHandle;
@@ -155,6 +171,48 @@ static void log_phasors(const char *tag) {
}
}
/*
* 函数功能:L1-6 判定结果的告警编排(边沿 → 日志 + 自检留痕)
* 入口参数:hs - adc_health_check() 判定结果快照
* 返 回 值:无
* 限定条件:仅任务上下文;日志仍受写者就绪门控(SYSLOG 不受——内核前有
* s_preboot 缓冲,故障痕迹不因开机窗口丢失)
* 函数说明:策略分级(判定本身不在本文件,见 App/algo/adc_health.c)——
* ① 严重事件(FRSTDATA / HAL 错误 / STM32 停摆计数增量):这三类
* 计数增量恒应为 0,出现即故障 → 每次 DBG_ERROR;
* ② 电平故障上升沿(静默/序号倒退)→ DBG_ERROR;下降沿(静默自愈)
* → DBG_INFO("恢复"必须可见,否则只看到一次错误无从判断是否还在);
* ③ SYSLOG_SELFTEST 每类每次开机**只留痕一次**(持续故障刷屏会把
* 32KB selftest.log 写满,也淹没其它模块的故障记录);
* ④ gap(丢帧)**不告警**:消费者超拍是已知现象(心跳日志
* adc1_gap / tgap 即观测面),告警会淹没真故障。
*/
static void adc_health_report(const adc_health_status_t *hs) {
uint32_t serious = hs->events & (ADC_HE_FRSTDATA | ADC_HE_STM32_ERR | ADC_HE_HW_ERR);
uint32_t rise = hs->flags & ~s_health_prev;
uint32_t fall = s_health_prev & ~hs->flags;
if ((serious != 0u) && (dbg_console_writer_ready() != 0u)) {
DBG_ERROR("sampling fault: events=0x%lx sticky=0x%lx",
(unsigned long)hs->events, (unsigned long)hs->sticky);
}
if ((rise != 0u) && (dbg_console_writer_ready() != 0u)) {
DBG_ERROR("health: flags=0x%lx stall_ms=%lu tgap=%lu",
(unsigned long)hs->flags,
(unsigned long)hs->tpafe_stall_ms,
(unsigned long)hs->tpafe_gaps);
}
if (((serious | rise) & ~s_health_logged) != 0u) {
(void)syslog_post(SYSLOG_SELFTEST, "adc sampling fault");
s_health_logged |= (serious | rise);
}
if ((fall != 0u) && (dbg_console_writer_ready() != 0u)) {
DBG_INFO("health: recovered flags=0x%lx sticky=0x%lx",
(unsigned long)hs->flags, (unsigned long)hs->sticky);
}
s_health_prev = hs->flags;
}
/*
* 函数功能:ADC 任务初始化
* 入口参数:无
@@ -166,6 +224,7 @@ int adc_task_init(void) {
s_frames_consumed = 0u;
s_frames_idle = 0u;
s_frames_skip = 0u;
adc_health_init();
DBG_INFO("adc_task_init");
return 0;
}
@@ -195,7 +254,8 @@ void adc_task_func(void *arg) {
s_consumer_ready = 1u;
for (;;) {
int r;
int r;
uint32_t now = osKernelGetTickCount(); /* 本拍时基:健康判定 + 心跳共用一份 */
/* P2 探针:单拍起点。名义节拍 1 ms = 168000 周期;若实测**持续**超过,
* 本任务(v2.7 起为 osPriorityAboveNormal)会持续抢占 uiTask
@@ -259,6 +319,24 @@ void adc_task_func(void *arg) {
}
}
/* 2b) L1-6 采样有效性校验(每拍,常数次整数运算):把驱动侧观测计数交给
* 纯逻辑判定层,本任务只做告警编排。⚠ 计数一律取**本拍 poll 之后**
* 的值——与刚消费的帧同一时点,避免"下一拍的增量被提前看到"。 */
{
adc_health_in_t h_in;
adc_health_status_t h_st;
h_in.tpafe_seq = tpafe5160_frame_seq();
h_in.tpafe_frst_err = (uint32_t)tpafe5160_frstdata_err();
h_in.stm32_seq = adc_input_stm32_seq();
h_in.stm32_errs = adc_input_stm32_errs();
h_in.stm32_gaps = adc_input_stm32_gaps();
h_in.stm32_hw_errs = adc_input_stm32_hw_errs();
h_in.now_ms = now;
adc_health_check(&h_in, &h_st);
adc_health_report(&h_st);
}
/* 3) 首帧码值上报:**写者就绪后**才落日志。写者未注册时 con_append 走
* 无锁直发路径,与 defaultTask 的存储初始化日志会字节级交错
* (2026-10-10 台架实测);此时挂起上报,就绪后补报最近一帧。
@@ -303,8 +381,6 @@ void adc_task_func(void *arg) {
/* 6) 周期心跳日志:帧统计 + 驱动侧 FRSTDATA 错误计数 + L2 相量。
* 写者就绪前抑制(同 3) 的交错理由);到点无论是否落日志都推进下个上报点 */
{
uint32_t now = osKernelGetTickCount();
if ((int32_t)(now - report_at) >= 0) {
if (dbg_console_writer_ready() != 0u) {
/* STM32 侧观测量(v2.0 DMA 版),四类各有独立成因:
@@ -314,9 +390,11 @@ void adc_task_func(void *arg) {
* adc1_err = 停摆:连续 >=2 拍无新帧 → 节拍源/DMA 真停
* adc1_hw = HAL ADC/DMA 错误回调
* 判读:adc1_seq 停增而 seq 仍涨 → STM32 侧停摆;
* adc1_gap 增长 → 本任务耗时超拍(看 adcm/adcn) */
* adc1_gap 增长 → 本任务耗时超拍(看 adcm/adcn);
* tgap = TPAFE 侧同类丢帧计数(L1-6,与 adc1_gap 对称) */
DBG_INFO("frames=%lu idle=%lu skip=%lu seq=%lu frst_err=%u ring=%lu"
" adc1_rc=%d adc1_err=%lu adc1_seq=%lu adc1_gap=%lu adc1_hw=%lu",
" adc1_rc=%d adc1_err=%lu adc1_seq=%lu adc1_gap=%lu adc1_hw=%lu"
" tgap=%lu",
(unsigned long)s_frames_consumed,
(unsigned long)s_frames_idle,
(unsigned long)s_frames_skip,
@@ -327,7 +405,8 @@ void adc_task_func(void *arg) {
(unsigned long)adc_input_stm32_errs(),
(unsigned long)adc_input_stm32_seq(),
(unsigned long)adc_input_stm32_gaps(),
(unsigned long)adc_input_stm32_hw_errs());
(unsigned long)adc_input_stm32_hw_errs(),
(unsigned long)adc_health_tpafe_gaps());
log_phasors("hb");
}
+5
View File
@@ -957,6 +957,11 @@
<FileType>1</FileType>
<FilePath>..\App\algo\phasor.c</FilePath>
</File>
<File>
<FileName>adc_health.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\algo\adc_health.c</FilePath>
</File>
</Files>
</Group>
<Group>
+1 -1
View File
@@ -75,7 +75,7 @@ main.c
| testTask | 2048 words | BelowNormal | 硬件测试入口 |
| uiTask | 4096 words | BelowNormal | LVGL UI 界面 |
| timeSyncTask | 512 words | Normal | 墙钟时间同步 |
| adcTask | 512 words | AboveNormal | TPAFE5160 采样(TIM3 1kHz 事件驱动 + 原子快照 + STM32 ADC DMA) |
| adcTask | 640 words | AboveNormal | TPAFE5160 采样(TIM3 1kHz 事件驱动 + 原子快照 + STM32 ADC DMA) |
<!-- TASK_TABLE:END -->
> 上表与 `Src/freertos.c` 的 `osThreadAttr_t` 一致(2026-09-25 校准);改动栈大小前先读
+7 -5
View File
@@ -113,10 +113,12 @@ static int hook_ptr_ok(const void *p)
* 靠审计 WARN 提示),建监听 → net_bind → CH395F → 日志尾。2026-10-10 整改后
* 静态最坏链 1024 B(整改前 1736 B)、实测峰值 616 B;⚠ 余量 1024 B **正好**
* 等于 ISR 底线 → 本任务链上再加任何调用(或新日志行)都必须先抬栈。
* adcTask 512 words:本区下方手工定义(同上,不在 `.ioc` 里)。B1(2026-10-10)
* ADC 采集消费者:链上仅 tpafe5160_start_conv_irq + tpafe5160_snapshot(临界区
* 原子拷贝)+ task_wd_beat + 周期 DBG_INFO(每 10s 一条)。栈值以
* `scripts/stack_report.py` 实测最坏链为准,改动前后各跑一次。
* adcTask 640 words:本区下方手工定义(同上,不在 `.ioc` 里)。B1(2026-10-10)
* ADC 采集消费者;L2 相量(2026-10-11)+ L1-6 有效性校验(同日)后链上为
* adc_input_poll + sample_ring/phasor + adc_health_report(DBG_ERROR +
* syslog_post 回显尾)。静态最坏链 1080 B,512 words 时余 968 B < 1024 B
* ISR 底线 → 抬至 640 words(2.5 KB,堆余量 min_ever 7152→≈6640 B,可承受)。
* 栈值以 `scripts/stack_report.py` 实测最坏链为准,改动前后各跑一次。
*/
/* 存储测试变量(g_storage_stats / s_perf_buf)已迁至 test/board/storage_test_task.c */
osThreadId_t timeSyncTaskHandle;
@@ -135,7 +137,7 @@ const osThreadAttr_t timeSyncTask_attributes = {
osThreadId_t adcTaskHandle;
const osThreadAttr_t adcTask_attributes = {
.name = "adcTask",
.stack_size = 512 * 4,
.stack_size = 640 * 4,
.priority = (osPriority_t) osPriorityAboveNormal,
};
/* USER CODE END Variables */
@@ -36,10 +36,10 @@
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 273,860 B (267.44 KB) | 1,048,576 B (1024 KB) | 26.1% |
| RW_IRAM1 | 91,488 B (89.34 KB) | 131,072 B (128 KB) | 69.8% |
| ER_IROM1 | 274,728 B (268.29 KB) | 1,048,576 B (1024 KB) | 26.2% |
| RW_IRAM1 | 91,544 B (89.40 KB) | 131,072 B (128 KB) | 69.8% |
| RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% |
| **Flash (ROM 合计)** | **274,684 B (268.2 KB)** | 1,048,576 B (1024 KB) | **26.2%** |
| **Flash (ROM 合计)** | **275,608 B (269.1 KB)** | 1,048,576 B (1024 KB) | **26.3%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
@@ -95,6 +95,9 @@
> 同日**增列 `adcTask`**(B1:ADC 采集消费者,512 words,静态最坏链 504 B / 余 75%)。
> **2026-10-11 L2 相量层接入后复测**:adcTask 静态 **832 B** / 台架实测峰值 **928 B / 2048(余 54%)**,
> 增量为压环 + `phasor_run()`(`win[20]` 80 B)+ `log_phasors` 日志链;仍高于 1024 B 底线,7 任务全 PASS。
> **同日 L1-6 有效性校验后抬栈 512→640 words**:`adc_health_report`→`syslog_post` 控制台回显尾把静态
> 最坏链推到 **1080 B**(512 words 时余 968 B < 1024 B ISR 底线)→ 抬至 2.5 KB,余 **1480 B / 57%**;
> 堆 `min_ever` 7152 → **6640 B**(-512 B,符合预算)。
> 此前 2026-10-04 的 V5 30 min 重采曾把 defaultTask 推到 **3664B/4096(余量 432B ≈ 10%,登记 S-1)**;
> 2026-10-10 的栈整改(**未抬栈**,见 §4 阶段 0 的"2026-10-10 复测")把它压回 **2152B/4096**,S-1 已销项。
@@ -106,7 +109,7 @@
| testTask | `2048*4` | 8KB | 测试套件(产品构建不创建;存储套件深链需要) | 未创建 / 静态 496 B |
| defaultTask | `1024*4` | 4KB | FatFS 挂载/探针 + 配置引擎 + syslog + 栈水位探针 | 2152 B(余 47%)/ 静态 2776 B |
| timeSyncTask | `512*4` | 2KB | TCP 时间同步 | 616 B(余 69%)/ 静态 **1024 B**(= ISR 底线,无再加深空间) |
| adcTask | `512*4` | 2KB | TPAFE5160 采样消费 + L2 相量(TIM3 1 kHz 节拍 + 快照 + 压环 + 全周傅氏,B1→L2 2026-10-11) | **928 / 2048**(余 1120 B / 54%,台架 `@test` 实测 2026-10-11);静态 832 B |
| adcTask | `640*4` | 2.5KB | TPAFE5160 采样消费 + L2 相量 + L1-6 有效性校验(TIM3 1 kHz 节拍,B1→L2→L1-6 2026-10-11) | **952 / 2560**(余 1608 B / 62%,台架 `@test` 实测 2026-10-11);静态 1080 B |
| idle(内核) | `configMINIMAL_STACK_SIZE=128` | 0.5KB | 空闲 | 未观测(未取句柄) |
| timer(内核) | `configTIMER_TASK_STACK_DEPTH=256` | 1KB | 软定时器 | 未观测 |
@@ -0,0 +1,116 @@
[+0ms] [----/--/-- --:--:--][APP] boot: syslog init OK
[+0ms] [----/--/-- --:--:--][APP] fw build: Oct 11 2026 00:38:20
[+10ms] [----/--/-- --:--:--][APP] reset cause: software (NVIC_SystemReset) (CSR=0x14000000)
[+21ms] PREVRUN seg=2 seq=1914766133 t=1240757
[+21ms] TICKRUN uw=1240801 n=1240801 duw=44
[+21ms] [----/--/-- --:--:--][RTC] sd2506_init: OK
[+31ms] [----/--/-- --:--:--][APP] SD2506 init: OK (ret=0)
[+31ms] [2026-10-10 17:13:03][CLK] sys_clock_init: RTC initial time 2026-10-10 17:13:03
[+42ms] [2026-10-10 17:13:03][CLK] sys_clock_init: wall clock base = unix 1791652383, base_tick=137
[+52ms] [2026-10-10 17:13:03][APP] RS485 init: OK (UART4, DE=PA15)
[+64ms] [2026-10-10 17:13:03][NAND] Resetting NAND...
[+94ms] [2026-10-10 17:13:03][NAND] Reading NAND ID...
[+94ms] [2026-10-10 17:13:03][NAND] NAND ID: MID=0xC8, DID=0x52
[+94ms] [2026-10-10 17:13:03][NAND] Scanning bad blocks...
[+223ms] [2026-10-10 17:13:03][NAND] BBT scan: 0 bad blocks found
[+234ms] [2026-10-10 17:13:03][NAND] Enabling ECC...
[+234ms] [2026-10-10 17:13:03][NAND] Unlocking block protection...
[+244ms] [2026-10-10 17:13:03][NAND] Locating BBT pool...
[+244ms] [2026-10-10 17:13:03][NAND] Loading persisted BBT...
[+255ms] [2026-10-10 17:13:03][NAND] NAND init OK
[+255ms] [2026-10-10 17:13:03][APP] NAND init: OK
[+255ms] [2026-10-10 17:13:03][ADC] Init OK
[+266ms] [2026-10-10 17:13:03][APP] ADC init: OK
[+266ms] [2026-10-10 17:13:03][APP] ADC input layer init: OK
[+266ms] [2026-10-10 17:13:03][APP] phasor layer init: OK (inst=5)
[+276ms] [2026-10-10 17:13:03][APP] SD2506 ID: 2512153425482405
[+287ms] [2026-10-10 17:13:03][APP] SD2506 time: 2026-10-10 17:13:04 (week 2)
[+287ms] [2026-10-10 17:13:03][APP] SD2506 temp: 28 C
[+297ms] [2026-10-10 17:13:03][APP] SD2506 battery: 3.11 V
[+297ms] [2026-10-10 17:13:03][APP] App init OK
[+308ms] [2026-10-10 17:13:03][NAND_FTL] Initializing NAND FTL...
[+339ms] [2026-10-10 17:13:03][NAND_FTL] Map resumed: 14094/97504 sectors used
[+339ms] [2026-10-10 17:13:03][NAND_FTL] Map head=117088 tail=99894 epoch=1
[+349ms] [2026-10-10 17:13:03][NAND_FTL] NAND FTL ready (capacity: 97504 pages = 190 MB)
[+421ms] [2026-10-10 17:13:03] [FS] FATFS mounted
[+473ms] [2026-10-10 17:13:03][SYSLOG] runlog boot entry: power on
[+473ms] [2026-10-10 17:13:03][CFG] register: section 'network' (4 keys)
[+484ms] [2026-10-10 17:13:03][CFG] load: OK (523B, heap free 14728B)
[+494ms] [2026-10-10 17:13:03][NET_CFG] load: OK ip=192.168.1.100
[+546ms] [2026-10-10 17:13:03][ADC_TASK] first: UA=11429 UB=1026 UC=817 UN=11453 IA=11452 IB=976 IC=1017 IN=865 T=940
[+556ms] [2026-10-10 17:13:03][ADC_TASK] phasor(first) C01-IA-F1: Re=0 Im=0 rms=0 (x0.1)
[+566ms] [2026-10-10 17:13:03][ADC_TASK] phasor(first) C02-UA-F1: Re=3 Im=0 rms=3 (x0.1)
[+566ms] [2026-10-10 17:13:03][ADC_TASK] phasor(first) C03-UN-F1: Re=0 Im=0 rms=1 (x0.1)
[+577ms] [2026-10-10 17:13:03][ADC_TASK] phasor(first) C04-IA-F2: Re=3 Im=1 rms=3 (x0.1)
[+587ms] [2026-10-10 17:13:03][ADC_TASK] phasor(first) C05-IA-F5: Re=0 Im=-2 rms=2 (x0.1)
[+598ms] [2026-10-10 17:13:03][NET_TASK] === Network Init ===
[+598ms] [2026-10-10 17:13:03][NET_TASK] net init: OK
[+598ms] [2026-10-10 17:13:03][NET_TASK] MAC: 3C:AB:72:53:2B:12
[+608ms] [2026-10-10 17:13:03][NET_TASK] IP_INF: IP=192.168.1.100 GW=192.168.1.1 MASK=255.255.255.0
[+608ms] MISS start snap=0x00000067 req=0x00000077 t=717
[+619ms] [2026-10-10 17:13:03][WD] [WD] armed: timeout ~2498ms (worst ~1701ms), miss_tol=800ms
[+629ms] [2026-10-10 17:13:03][UI_TASK] UI task: started, wait net ready
[+2541ms] [2026-10-10 17:13:05][NET_TASK] PHY: 100M FULL
[+2551ms] [2026-10-10 17:13:05][NET_TASK] netTask: started (single-socket mode)
[+2593ms] [2026-10-10 17:13:05] [FTP] waiting for connection...
[+2624ms] [2026-10-10 17:13:05][TIME_SYNC] time_sync: TCP listen on :8888 (sock=1)
[+2634ms] [2026-10-10 17:13:05][UI_TASK] UI task: net ready, init port
[+2645ms] [2026-10-10 17:13:05][UI_TASK] UI task: ui_port_init enter
[+2645ms] [2026-10-10 17:13:05][UI_PORT] theme: mono dark_bg=true applied
[+2655ms] [2026-10-10 17:13:05][UI_TASK] UI port init: OK
[+2655ms] [2026-10-10 17:13:05][UI_TASK] UI task: ui_menu_init enter
[+2666ms] [2026-10-10 17:13:05][UI_MENU] menu build start: total=39408 free=36572 big=36572
[+2676ms] [2026-10-10 17:13:05][UI_MENU_VIEW] view init: root frame w=66 h=107
[+2686ms] [2026-10-10 17:13:05][UI_MENU] menu build done: total=39292 free=35708 big=35696 used=3584
[+2686ms] [2026-10-10 17:13:05][UI_TASK] UI menu init: OK
[+2697ms] [2026-10-10 17:13:05][UI_STANDBY] standby: ready (idle=60000ms)
[+2697ms] [2026-10-10 17:13:05][UI_TASK] UI standby: ready (idle=60s)
[+2749ms] [2026-10-10 17:13:05][UI_TASK] UI net sink: started (port 7003)
[+2749ms] MISS start snap=0x00000067 req=0x00000077 t=2857
[+2760ms] [PROF] t=2861 uic=0 uip=0 uipn=0 uil=0 uiw=0 uis=0 uisn=0 adc=2455 adcm=1983 adcn=0 gl=0
[+2770ms] [PROF2] t=2861 defd=11954 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0
[+4922ms] [PROF] t=5025 uic=213 uip=19574 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=4618 adcm=1983 adcn=0 gl=0
[+4933ms] [PROF2] t=5025 defd=11954 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0
[+5335ms] [2026-10-10 17:13:08] [STACK] default peak_used=2152/4096 free_min=1944 (47%)
[+5346ms] [2026-10-10 17:13:08] [STACK] net peak_used=972/3840 free_min=2868 (74%)
[+5356ms] [2026-10-10 17:13:08] [STACK] ftp peak_used=740/4096 free_min=3356 (81%)
[+5356ms] [2026-10-10 17:13:08] [STACK] ui peak_used=4052/16384 free_min=12332 (75%)
[+5366ms] [2026-10-10 17:13:08] [STACK] timesync peak_used=684/2048 free_min=1364 (66%)
[+5377ms] [2026-10-10 17:13:08] [STACK] adc peak_used=952/2560 free_min=1608 (62%)
[+5377ms] [2026-10-10 17:13:08] [STACK] heap free=14576 min_ever=6640
[+6932ms] [PROF] t=7038 uic=408 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=6631 adcm=1983 adcn=0 gl=0
[+6943ms] [PROF2] t=7038 defd=11954 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0
[+8948ms] [PROF] t=9054 uic=608 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=8647 adcm=1983 adcn=0 gl=0
[+8958ms] [PROF2] t=9054 defd=11954 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0
[+10304ms] [2026-10-10 17:13:13][ADC_TASK] frames=10002 idle=0 skip=23 seq=10006 frst_err=0 ring=9979 adc1_rc=0 adc1_err=0 adc1_seq=10004 adc1_gap=23 adc1_hw=0 tgap=1
[+10315ms] [2026-10-10 17:13:13][ADC_TASK] phasor(hb) C01-IA-F1: Re=2 Im=0 rms=2 (x0.1)
[+10325ms] [2026-10-10 17:13:13][ADC_TASK] phasor(hb) C02-UA-F1: Re=1 Im=0 rms=1 (x0.1)
[+10335ms] [2026-10-10 17:13:13][ADC_TASK] phasor(hb) C03-UN-F1: Re=0 Im=0 rms=0 (x0.1)
[+10346ms] [2026-10-10 17:13:13][ADC_TASK] phasor(hb) C04-IA-F2: Re=0 Im=-1 rms=1 (x0.1)
[+10346ms] [2026-10-10 17:13:13][ADC_TASK] phasor(hb) C05-IA-F5: Re=-1 Im=0 rms=1 (x0.1)
[+10956ms] [PROF] t=11057 uic=802 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=10650 adcm=1983 adcn=0 gl=0
[+10966ms] [PROF2] t=11057 defd=47667 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0
[+12977ms] [PROF] t=13074 uic=1002 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=12667 adcm=1983 adcn=0 gl=0
[+12977ms] [PROF2] t=13074 defd=47667 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0
[+14986ms] [PROF] t=15091 uic=1202 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=14684 adcm=1983 adcn=0 gl=0
[+14996ms] [PROF2] t=15091 defd=47667 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0
[+17009ms] [PROF] t=17108 uic=1402 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=16701 adcm=1983 adcn=0 gl=0
[+17019ms] [PROF2] t=17108 defd=47667 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0
[+19025ms] [PROF] t=19125 uic=1602 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=18718 adcm=1983 adcn=0 gl=0
[+19036ms] [PROF2] t=19125 defd=47667 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0
[+20305ms] [2026-10-10 17:13:23][ADC_TASK] frames=20002 idle=0 skip=25 seq=20006 frst_err=0 ring=19977 adc1_rc=0 adc1_err=0 adc1_seq=20004 adc1_gap=25 adc1_hw=0 tgap=2
[+20316ms] [2026-10-10 17:13:23][ADC_TASK] phasor(hb) C01-IA-F1: Re=0 Im=0 rms=0 (x0.1)
[+20326ms] [2026-10-10 17:13:23][ADC_TASK] phasor(hb) C02-UA-F1: Re=-2 Im=-2 rms=3 (x0.1)
[+20337ms] [2026-10-10 17:13:23][ADC_TASK] phasor(hb) C03-UN-F1: Re=0 Im=0 rms=1 (x0.1)
[+20337ms] [2026-10-10 17:13:23][ADC_TASK] phasor(hb) C04-IA-F2: Re=-1 Im=-1 rms=1 (x0.1)
[+20348ms] [2026-10-10 17:13:23][ADC_TASK] phasor(hb) C05-IA-F5: Re=2 Im=1 rms=2 (x0.1)
[+21059ms] [PROF] t=21158 uic=1799 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=20751 adcm=1983 adcn=0 gl=0
[+21069ms] [PROF2] t=21158 defd=47964 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0
[+23075ms] [PROF] t=23175 uic=1999 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=22768 adcm=1983 adcn=0 gl=0
[+23085ms] [PROF2] t=23175 defd=47964 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0
[+25094ms] [PROF] t=25192 uic=2199 uip=58732 uipn=9991 uil=10003 uiw=10337 uis=1 uisn=0 adc=24785 adcm=1983 adcn=0 gl=0
[+25094ms] [PROF2] t=25192 defd=47964 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0
[+27102ms] [PROF] t=27209 uic=2399 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=26802 adcm=1983 adcn=0 gl=0
[+27112ms] [PROF2] t=27209 defd=47964 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0
[+29122ms] [PROF] t=29226 uic=2599 uip=58732 uipn=9995 uil=9999 uiw=10337 uis=1 uisn=0 adc=28819 adcm=1983 adcn=0 gl=0
[+29132ms] [PROF2] t=29226 defd=47964 defs=7 netp=0 netn=0 adcp=10 adcq=0 gl=0
@@ -27,6 +27,8 @@
|---|---|---|---|
| 2026-10-10 | `prod_adc_tim6` / 开机 + 30 s 空载 | `adc` 528/2048(74%);`default` 2164/4096(47%);heap free=15088 min_ever=7152 | `20261010_prod_adc_tim6_boot_log.txt` |
| 2026-10-10 | `prod_adc_l1` / 开机 + 30 s 空载 | `adc` **584/2048(71%)**(+56 B,L1 层 adc_input_poll/raw 上链);`default` 2164/4096(47%);heap free=15088 min_ever=7152 | `20261010_prod_adc_l1_boot_log.txt` |
| 2026-10-11 | `prod_phasor` / 开机 + 30 s 空载(L2 台架闭环) | `adc` **928/2048(54%)**(+344 B,压环 + `phasor_run` + `log_phasors` 上链);`default` 2152/4096(47%);heap free=15088 min_ever=7152 | `20261011_prod_phasor_boot_log.txt` |
| 2026-10-11 | `prod_adc_health` / 开机 + 30 s 空载(L1-6 + 抬栈 640 words) | `adc` **952/2560(62%)**(静态最坏链 1080 B,`adc_health_report`→`syslog_post` 回显尾);`default` 2152/4096(47%);heap free=14576 **min_ever=6640**(抬栈 -512 B,符合预算) | `20261011_prod_adc_health_boot_log.txt` |
## 4. 关联
@@ -67,7 +67,7 @@
| **B0 产品定义** | 采集是否持续 / 采样率与通道 / 转发目标 / 保护元件清单——决定 B1 与保护层的**规模** | ✅ 已确认(2026-10-10,见 §1.5) |
| **B1 ADC 采集链路** | 周期 CONVST + `adc_task` 消费 `tpafe5160_snapshot()` + 建任务 + 栈预算 | ✅ 已实现并 `@test` 实机闭环(2026-10-10;`adcTask` 512 words,台架 1000 Hz) |
| **N-18 硬件缺陷** | V0.1 跨转换器无同一时刻性 → 限定 V0.1 可实现的保护范围 | ⚠️ 硬件改版项(V0.2) |
| **栈预算** | 保护任务栈需入 `Src/freertos.c` 并经 `scripts/stack_report.py` 核算 | 🔶 `adcTask` 已核算(**L2 接入后复测 2026-10-11:静态 832 B / 台架实测峰值 928/2048 / 余 54%**);`protectTask` 为后续 |
| **栈预算** | 保护任务栈需入 `Src/freertos.c` 并经 `scripts/stack_report.py` 核算 | 🔶 `adcTask` 已核算(**L1-6 后:栈抬至 640 words,静态 1080 B / 台架实测峰值 952/2560 / 余 57%**);`protectTask` 为后续 |
### 1.5 B0 产品定义(2026-10-10 确认)
@@ -243,7 +243,7 @@
| L1-3 | STM32 ADC1 启动与扫描 | 9 通道、144 cycles、**TIM3 TRGO 硬件触发 + DMA** | 全库出现 `HAL_ADC_Start_DMA`;9 通道码值随注入变化 | ✅ 2026-10-10(v2.0):`App/algo/adc_input.c` 的 `stm32_take()`——ADC1 由 TIM3 更新事件经 TRGO 触发一轮 9 通道,`HAL_ADC_Start_DMA(&hadc1, buf, 18)` + DMA2_Stream4 循环模式把码值自动搬进**乒乓双缓冲**,任务侧只消费刚完成的半区(**零忙等**,实测单拍 `adcp` 122 µs → **8 µs**);台架 `@test` PASS——`adc1_seq` 每拍 +1、`adc1_err=0`、`adc1_hw=0`。⚠ 判据"随注入变化"以"逐通道独立读数 + 零停摆 + 跨次开机读数不同"替代(台架无信号源,同 §7.1 无示波器口径);**引脚级注入验证(`ADC通道映射说明.md` §8 步骤 2)仍为待做项** |
| L1-4 | 通道语义映射 | 见 `docs/硬件平台/ADC通道映射说明.md`(**按语义取数,不写死下标**) | `UA→buf[6]`、`UN→buf[5]`、`IA→buf[2]`;STM32 侧 `UB→ADC_CHANNEL_14` | ✅ 2026-10-10:`App/algo/adc_input.c` 的 `k_adc_map[]` 语义表(`UA/UN/IA`→TPAFE `buf[6]/[5]/[2]`,`UB/UC/IB/IC/IN/TEMP`→ADC1 `CH14/3/6/1/2/5`)+ `adc_input_raw()` 按语义取数;开机**映射自检**(`adc_input_init()` 校验 STM32 映射通道必落在扫描序列内 → 挡住"映射到不扫描的通道"静默错)通过(`ADC input layer init: OK`)。台架首帧日志 8 路 + 温度全部读出:`UA=11429 UN=11454 IA=11452`(与 TIM3 轮次一致)、`UB=1018 UC=1044 IB=943 IC=1069 IN=1052 T=850`(互异、非饱和;**两次开机读数不同** → 确为实时转换而非常量) |
| L1-5 | 通道标定(一次系数 + 零漂) | 每通道 `k`、`b`;系数定点 `2^10` | 注入标准源,读数误差 ≤ 目标精度 | ⬜ |
| L1-6 | 采样有效性校验 | FRSTDATA 跳变、帧序号单调、丢帧计数 | 芯片静默/丢帧被检出并置告警 | ⬜ |
| L1-6 | 采样有效性校验 | FRSTDATA 跳变、帧序号单调、丢帧计数 | 芯片静默/丢帧被检出并置告警 | ✅ 2026-10-11:新增 `App/algo/adc_health.c/h`(纯逻辑判定层:静默 100 ms 门限 / 丢帧边沿 / 序号倒退锁存 / FRSTDATA·HAL 错误·STM32 停摆计数增量)+ `adc_task.c` 告警编排(严重事件与故障上升沿 `DBG_ERROR` + 每类每次开机一次 `SYSLOG_SELFTEST`,恢复 `DBG_INFO`;gap 不告警只计数,心跳增 `tgap`);主机单测 `test/host/adc_health/` **31 用例全绿**(`adc-health-unit-test` 钩子);台架 `@test` PASS 零 `[ERR]`(健康不误报,`boot_log_data/20261011_prod_adc_health_boot_log.txt`)。⚠ 抬栈:判定链 `adc_health_report`→`syslog_post` 回显尾使 adcTask 静态最坏链 832→**1080 B** → 栈 512→**640 words**(余 1480 B / 57%) |
| L1-7 | 等周波采样(软件测频 + 调频闭环) | 20 点/周波;测频范围建议 **45~55 Hz**(参考 40.5~60.5) | 频率偏移 ±2 Hz 时幅值误差 ≤ 1% | ⬜ |
| L1-8 | 零序通道接入(外部专用 / 自产切换) | 外部 `UN`/`IN`;自产 `3I0 = Ia+Ib+Ic`(V0.2 起) | 零序通道可切换来源,值正确 | ⬜ |
| L1-9 | 二次谐波/五次谐波提取入口 | k=2(涌流)、k=5(选线) | 相量层可产出对应谐波相量 | ✅ 2026-10-11:`phasor.c` 实例表 `C04-IA-F2`(k=2)/ `C05-IA-F5`(k=5)随每拍重算产出,台架心跳日志 `phasor(hb) C04/C05` 行可见(`boot_log_data/20261011_prod_phasor_boot_log.txt`);谐波**抑制/提取正确性**已由主机单测覆盖(`test/host/phasor/`) |
@@ -463,7 +463,7 @@
| 阶段 | 名称 | 内容 | 前置 | 里程碑 |
| --- | --- | --- | --- | --- |
| **P0** | 采样与相量底座 | B1(周期 CONVST + `adcTask` 消费 + 建任务 + 栈预算)+ L1 + L2 | B0 + B1 ✅ | 每 1 ms 产出正确的基波相量 —— **✅ 2026-10-11 台架闭环**(L1-5 标定 / L1-6 有效性 / L1-7 等周波为 P0 遗留项,精度复核依赖标准源) |
| **P0** | 采样与相量底座 | B1(周期 CONVST + `adcTask` 消费 + 建任务 + 栈预算)+ L1 + L2 | B0 + B1 ✅ | 每 1 ms 产出正确的基波相量 —— **✅ 2026-10-11 台架闭环**(L1-6 同日落地 ✅;L1-5 标定 / L1-7 等周波为 P0 遗留项,精度复核依赖标准源) |
| **P1** | 基础保护元件 | **V0.1 有效子集**:过流(A 相,`IA`)、过负荷(A 相)+ 定值模型;B/C 相过流**建接口不使能** | P0 | 标准源注入 A 相可正确动作 |
| **P2** | 电压/频率类保护 | 过压/欠压(`UA`)、过频/低频、零序过压(`UN`) | P0 | 电压/频率越限正确动作 |
| **P3** | 异常检测与闭锁 | L5 自检闭锁、采样通道自检;三相类(涌流/PT·CT 断线/母线接地)**建接口不使能** | P1/P2 | 自检失败闭锁;未使能元件不误动 |
@@ -602,3 +602,4 @@ B0 产品定义 ✅(2026-10-10 已确认,见 §1.5)
| 2026-10-10 | **L2 相量层落地 + L2-2 实例数口径对齐** | 新增 `App/algo/sample_ring.c/h`(L2 输入:按语义通道保存 2 周波 = 40 点环缓冲,整帧写后统一推进写指针)与 `App/algo/phasor.c/h`(L2 核心:全周傅氏单频 DFT,N = 20;系数表**离线预乘 √2/N** → `fAmSQ` 直接为**有效值**;计算区/使用区**双区原子切换**;实例表 `k_inst[]` 数据驱动);主机侧单测 `test/host/phasor/` **57 用例全绿**(`phasor-unit-test` 钩子)。**§3.2 L2-2 行口径对齐**:参考项目为 **4 个实例**,本项目按 V0.1 有效通道展开为 **5 个**(`C01-IA-F1`/`C02-UA-F1`/`C03-UN-F1`/`C04-IA-F2`/`C05-IA-F5`),已加注说明 4 → 5 的来历;§4.3 相量区 RAM 行同步为 5 实例。⚠ §3.2 的 L2 各点状态**仍标 ⬜**——实现与主机单测已完成,但**台架 `@test` 闭环未做**,且 L2-3 的完成判据(标准源读数 = 设定有效值)在无信号源台架上不可达,故不置 ✅(口径见 §3.2 表下注) |
| 2026-10-10 | **L1-1/L1-3 节拍源与搬运方式升级(v2.8)** | 节拍源 **TIM6 → TIM3**(更正硬事实:F4 上 ADC1 的外部触发清单里没有 TIM6/TIM7 TRGO,只接 DAC),TIM3 更新事件同时触发 TPAFE CONVST 与(经 TRGO)ADC1 一轮 9 通道;L1-3 由"软件触发 + 逐通道轮询"改为 **DMA2_Stream4 循环搬运 + 乒乓双缓冲**,`stm32_scan()` → `stm32_take()`,单拍 `adcp` **122 µs → 8 µs**。§1.4 采样软件行、§3.2 L1-1/L1-3/L1-4 同步。完整设计与实测见 `docs/子系统设计/ADC采样节拍设计说明.md` §11 |
| 2026-10-11 | **L2 台架闭环:P0 收尾** | `@build` 0 错 0 警 → `@test` 闭环把 `phasor(first)`/`phasor(hb)` 列为 Required 并校验顺序 `App init OK>phasor(first)>phasor(hb)`(⚠ `net init: OK` **不在**该序内——adcTask 与 netTask 并行,`phasor(first)` ≈+500 ms 早于 `net init: OK` ≈+574 ms 属正常)→ **PASS**:5 实例相量开机即产出、心跳 5 实例持续刷新、`frames` 每 10 s +10000(1000.0 Hz)、`idle=0`、`frst_err=0`、`adc1_err=0`、`adc1_hw=0`、`fw build` 身份戳匹配、回读一致、锚点 ×1、零 `[ERR]`/HardFault;证据 `boot_log_data/20261011_prod_phasor_boot_log.txt`。栈复测:adcTask 静态 832 B / 实测峰值 **928/2048(余 54%)**,7 任务全 PASS(RAM 方案 §2.3 已同步)。§3.1 L1-9 置 ✅(`C04-IA-F2`/`C05-IA-F5` 台架产出);§3.2 L2-1~L2-4/L2-6/L2-7 置 ✅(精度判据保留"待标准源"注),**L2-5 更正为"V0.1 不适用、随 V0.2"**(跨转换器合成被 N-18 禁止、零序走外部专用通道——原表下注"L2-1~L2-7 实现完成"对 L2-5 系误记);§7.1 P0 里程碑置 ✅(遗留 L1-5/L1-6/L1-7)。下一步 = **P1**(A 相过流/过负荷 + 定值模型)与 **L1-6 采样有效性校验** |
| 2026-10-11 | **L1-6 采样有效性校验落地** | 新增 `App/algo/adc_health.c/h`(**纯逻辑判定层**,零平台依赖免桩单测):① 静默——两侧帧序号连续 ≥100 ms 无前进(电平位,可自愈;`adc_health_flags()` 为 L5-4 闭锁预留);② 丢帧——TPAFE 序号一次前进 >1 计 `tgap`(与 STM32 侧 `adc1_gap` 对称,边沿事件不告警——消费者超拍是已知现象,告警会淹没真故障);③ 单调性——序号倒退锁存位;④ 错误增量——FRSTDATA / HAL 错误回调 / STM32 停摆计数增量(事件位与锁存位**编号对齐**,消费者按位图"每类每次开机留痕一次")。`adc_task.c` 只做告警编排:严重事件/上升沿 `DBG_ERROR` + `SYSLOG_SELFTEST`("adc sampling fault"),下降沿 `DBG_INFO`,心跳行增 `tgap`。主机单测 `test/host/adc_health/` **31 用例全绿**(100 ms 门限两侧、tick 回绕、边沿语义、NULL 守卫、init 幂等;`adc-health-unit-test` 钩子登记)。台架 `@test` PASS:零 `[ERR]`(健康不误报)、`tgap=1→2` 与 `adc1_gap` 同步可见、`frames` 1000.0 Hz;证据 `boot_log_data/20261011_prod_adc_health_boot_log.txt`。⚠ **抬栈**:`adc_health_report`→`syslog_post` 控制台回显尾使 adcTask 静态最坏链 832→**1080 B**(512 words 余 968 B < 1024 B 底线)→ **512→640 words**,复测余 **1480 B / 57%**、堆 min_ever 7152→6640 B(AGENTS 任务表 / RAM 方案 §2.3 已同步)。§3.1 L1-6 置 ✅ |
+374
View File
@@ -0,0 +1,374 @@
/*
* 模块名称:ADC 采样有效性校验(L1-6)主机侧单元测试
* 模块功能:在 PC 上对 App/algo/adc_health.c 做离线验证。
*
* 被测对象:真实源文件(以 #include 原样纳入,保证测的是生产代码而非副本):
* ../../../App/algo/adc_health.c
* 本层为**纯逻辑**(仅 stdint.h),**无需任何桩**。
*
* 为什么要主机侧单测(而不是只用板端闭环):
* 1. 静默/倒退/错误增量在健康台架上**不可复现**(复现 = 拔芯片/改驱动),
* 板端只能证明"健康时不误报",证不了"故障时必报";
* 2. 边界(恰好 100ms 门限 / tick 回绕 / 序号倒退后再前进 / 事件只发一拍)
* 只有合成计数序列能精确构造;
* 3. 不占 MCU 的 RAM,毫秒级完成。
*
* 覆盖点(对应 docs/算法/继电保护功能复现规划.md §3.1 L1-6):
* TC-AH-001 init + 首拍只建基线(不误报)
* TC-AH-002 正常 1 kHz 推进 2000 拍:零故障零事件(健康不误报)
* TC-AH-003 TPAFE 静默:99ms 不报 / 100ms 报 / 恢复清除电平位、锁存位留痕
* TC-AH-004 STM32 静默:TPAFE 正常时只报 STM32 侧
* TC-AH-005 TPAFE 丢帧:d=3 → tgap+=2、事件仅当拍(边沿语义)
* TC-AH-006 STM32 gap 计数增量 → 事件(不入锁存:超拍非故障)
* TC-AH-007 序号倒退 → SEQ_BACKWARD 锁存(前进后仍留痕)
* TC-AH-008 FRSTDATA / STM32_ERR / HW_ERR 增量 → 事件位与锁存位编号一致
* TC-AH-009 tick 回绕:跨 0xFFFFFFFF 的正常推进不误报静默;回绕后冻结仍报
* TC-AH-010 NULL 守卫:in=NULL 跳过;out=NULL 只更新内部状态
* TC-AH-011 init 幂等:故障后 re-init 全清
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-11
*/
#include <stdint.h>
#include <stdio.h>
/* ===================== 断言 ===================== */
static int s_total = 0, s_passed = 0, s_failed = 0;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
/* ===================== 被测代码 ===================== */
/* 原样纳入生产源文件(纯逻辑,无需桩) */
#include "adc_health.c"
/* ===================== 工具 ===================== */
/* 构造一份"健康"计数快照模板:两侧序号同拍号 seq,错误计数全 0 */
static adc_health_in_t mk_in(uint32_t seq, uint32_t now_ms) {
adc_health_in_t in;
in.tpafe_seq = seq;
in.tpafe_frst_err = 0u;
in.stm32_seq = seq;
in.stm32_errs = 0u;
in.stm32_gaps = 0u;
in.stm32_hw_errs = 0u;
in.now_ms = now_ms;
return in;
}
/* ===================== TC ===================== */
/* TC-AH-001:init + 首拍只建基线 */
static void tc_prime(void) {
adc_health_in_t in;
adc_health_status_t st;
adc_health_init();
in = mk_in(12345u, 1000u); /* 开机时序号已非 0 */
adc_health_check(&in, &st);
CHECK((st.flags == 0u) && (st.events == 0u) && (st.sticky == 0u) &&
(st.tpafe_gaps == 0u),
"TC-AH-001 first check only primes: no flags/events/sticky");
/* 第二拍序号未动但仅 1ms(< 门限)→ 仍不报 */
in.now_ms = 1001u;
adc_health_check(&in, &st);
CHECK(st.flags == 0u, "TC-AH-001 1ms without advance is not a stall");
}
/* TC-AH-002:正常 1 kHz 推进 2000 拍零误报 */
static void tc_healthy(void) {
adc_health_status_t st;
adc_health_in_t in;
uint32_t seq = 0u, now = 0u;
int i, ok = 1;
adc_health_init();
for (i = 0; i < 2000; i++) {
in = mk_in(seq, now);
adc_health_check(&in, &st);
if ((st.flags != 0u) || (st.events != 0u) || (st.sticky != 0u)) {
ok = 0;
}
seq++;
now++;
}
CHECK(ok, "TC-AH-002 healthy 1kHz sweep 2000 beats: zero flags/events");
}
/* TC-AH-003:TPAFE 静默 99/100ms 门限 + 恢复留痕 */
static void tc_tpafe_stall(void) {
adc_health_status_t st;
adc_health_in_t in;
uint32_t seq = 500u, now = 500u;
int i;
adc_health_init();
/* 先跑 10 拍健康节拍建立"前进中"基线 */
for (i = 0; i < 10; i++) {
in = mk_in(seq, now);
adc_health_check(&in, &st);
seq++; now++;
}
/* 仅 TPAFE 冻结(STM32 继续走):前 100 拍(elapsed 0..99ms)不报。
* 注意第 1 拍会消费掉 healthy 循环末尾"未消费的那次 +1",
* 故静默计时从 now=510 起算,第 i 拍 elapsed = i。 */
for (i = 0; i < 100; i++) {
in = mk_in(seq, now);
in.stm32_seq = seq + (uint32_t)i + 1u; /* STM32 侧照常前进 */
adc_health_check(&in, &st);
now++;
}
CHECK((st.flags & ADC_HF_TPAFE_STALL) == 0u,
"TC-AH-003 no TPAFE stall before 100ms");
/* 第 100ms:报 */
in = mk_in(seq, now);
in.stm32_seq = seq + 100u;
adc_health_check(&in, &st);
CHECK((st.flags & ADC_HF_TPAFE_STALL) != 0u,
"TC-AH-003 TPAFE stall at 100ms");
CHECK((st.sticky & ADC_HF_TPAFE_STALL) != 0u,
"TC-AH-003 stall latched into sticky");
CHECK(st.tpafe_stall_ms >= ADC_HEALTH_STALL_MS,
"TC-AH-003 stall_ms reported >= threshold");
/* 恢复:TPAFE 序号重新前进 → 电平位清除、sticky 留痕 */
in = mk_in(seq + 1u, now + 1u);
in.stm32_seq = seq + 101u;
adc_health_check(&in, &st);
CHECK((st.flags & ADC_HF_TPAFE_STALL) == 0u,
"TC-AH-003 stall level clears on recovery");
CHECK((adc_health_sticky() & ADC_HF_TPAFE_STALL) != 0u,
"TC-AH-003 sticky keeps the fault trace after recovery");
}
/* TC-AH-004:STM32 静默(TPAFE 正常) */
static void tc_stm32_stall(void) {
adc_health_status_t st;
adc_health_in_t in;
uint32_t seq = 100u, now = 100u;
int i;
adc_health_init();
for (i = 0; i < 10; i++) {
in = mk_in(seq, now);
adc_health_check(&in, &st);
seq++; now++;
}
/* STM32 冻结(TPAFE 继续走)。⚠ 第 1 拍消费 healthy 循环末尾的"未消费 +1"
* 并把静默计时起点重置为该拍 now → 第 i 拍 elapsed = i,需 101 拍才到 100ms */
for (i = 0; i < 101; i++) {
in = mk_in(seq + (uint32_t)i + 1u, now + (uint32_t)i + 1u);
in.stm32_seq = seq; /* 冻结在旧值 */
adc_health_check(&in, &st);
}
CHECK((st.flags & ADC_HF_STM32_STALL) != 0u,
"TC-AH-004 STM32 stall at 100ms");
CHECK((st.flags & ADC_HF_TPAFE_STALL) == 0u,
"TC-AH-004 healthy TPAFE not falsely stalled");
}
/* TC-AH-005:TPAFE 丢帧计数 + 边沿语义 */
static void tc_tpafe_gap(void) {
adc_health_status_t st;
adc_health_in_t in;
adc_health_init();
in = mk_in(10u, 10u);
adc_health_check(&in, &st); /* 基线 */
in.tpafe_seq = 13u; in.stm32_seq = 11u; in.now_ms = 11u; /* TPAFE d=3 */
adc_health_check(&in, &st);
CHECK((st.events & ADC_HE_TPAFE_GAP) != 0u,
"TC-AH-005 gap event on d>1");
CHECK(st.tpafe_gaps == 2u, "TC-AH-005 d=3 accumulates 2 lost points");
in.tpafe_seq = 14u; in.stm32_seq = 12u; in.now_ms = 12u; /* 恢复正常 d=1 */
adc_health_check(&in, &st);
CHECK(st.events == 0u, "TC-AH-005 gap event is edge-only (next beat clean)");
CHECK(st.tpafe_gaps == 2u, "TC-AH-005 gap counter is cumulative");
}
/* TC-AH-006:STM32 gap 计数增量 → 事件、不入锁存 */
static void tc_stm32_gap(void) {
adc_health_status_t st;
adc_health_in_t in;
adc_health_init();
in = mk_in(1u, 1u);
adc_health_check(&in, &st);
in.stm32_gaps = 1u; in.tpafe_seq = 2u; in.stm32_seq = 2u; in.now_ms = 2u;
adc_health_check(&in, &st);
CHECK((st.events & ADC_HE_STM32_GAP) != 0u,
"TC-AH-006 STM32 gap increment raises event");
CHECK((st.sticky & (ADC_HF_FRSTDATA | ADC_HF_STM32_ERR |
ADC_HF_HW_ERR)) == 0u,
"TC-AH-006 gap does NOT latch sticky (overrun is not a fault)");
}
/* TC-AH-007:序号倒退 → 锁存 */
static void tc_backward(void) {
adc_health_status_t st;
adc_health_in_t in;
uint32_t s, t;
int i;
adc_health_init();
in = mk_in(1000u, 100u);
adc_health_check(&in, &st);
in.tpafe_seq = 995u; in.stm32_seq = 995u; in.now_ms = 101u; /* 倒退 */
adc_health_check(&in, &st);
CHECK((st.flags & ADC_HF_SEQ_BACKWARD) != 0u,
"TC-AH-007 backward seq raises latched flag");
CHECK((st.sticky & ADC_HF_SEQ_BACKWARD) != 0u,
"TC-AH-007 backward latched into sticky");
/* 恢复正常推进 50 拍:SEQ_BACKWARD 不清(锁存语义) */
s = 996u; t = 102u;
for (i = 0; i < 50; i++) {
in = mk_in(s, t);
adc_health_check(&in, &st);
s++; t++;
}
CHECK((adc_health_flags() & ADC_HF_SEQ_BACKWARD) != 0u,
"TC-AH-007 SEQ_BACKWARD persists after normal advance");
}
/* TC-AH-008:三类错误计数增量 → 事件位与锁存位编号一致 */
static void tc_err_counters(void) {
adc_health_status_t st;
adc_health_in_t in;
adc_health_init();
in = mk_in(1u, 1u);
adc_health_check(&in, &st);
in.tpafe_seq = 2u; in.stm32_seq = 2u; in.now_ms = 2u;
in.tpafe_frst_err = 1u;
adc_health_check(&in, &st);
CHECK(((st.events & ADC_HE_FRSTDATA) != 0u) &&
((st.sticky & ADC_HF_FRSTDATA) != 0u),
"TC-AH-008 FRSTDATA increment: event + sticky (same bit number)");
in.tpafe_seq = 3u; in.stm32_seq = 3u; in.now_ms = 3u;
in.stm32_errs = 1u;
adc_health_check(&in, &st);
CHECK(((st.events & ADC_HE_STM32_ERR) != 0u) &&
((st.sticky & ADC_HF_STM32_ERR) != 0u),
"TC-AH-008 STM32 err increment: event + sticky (same bit number)");
in.tpafe_seq = 4u; in.stm32_seq = 4u; in.now_ms = 4u;
in.stm32_hw_errs = 1u;
adc_health_check(&in, &st);
CHECK(((st.events & ADC_HE_HW_ERR) != 0u) &&
((st.sticky & ADC_HF_HW_ERR) != 0u),
"TC-AH-008 HAL err increment: event + sticky (same bit number)");
/* 计数不再变化 → 事件不再重复触发 */
in.tpafe_seq = 5u; in.stm32_seq = 5u; in.now_ms = 5u;
adc_health_check(&in, &st);
CHECK(st.events == 0u, "TC-AH-008 steady error counters raise no repeat events");
}
/* TC-AH-009:tick 回绕安全 */
static void tc_tick_wrap(void) {
adc_health_status_t st;
adc_health_in_t in;
uint32_t seq = 70000u, now = 0xFFFFFF00u;
int i, ok = 1;
adc_health_init();
/* 跨 0xFFFFFFFF 正常推进 512 拍:不得误报静默 */
for (i = 0; i < 512; i++) {
in = mk_in(seq, now);
adc_health_check(&in, &st);
if (st.flags != 0u) {
ok = 0;
}
seq++; now++; /* uint32 自然回绕 */
}
CHECK(ok, "TC-AH-009 tick wrap sweep: no false stall");
/* 回绕后两侧同时冻结。⚠ 第 1 拍消费 sweep 末尾的"未消费 +1"并重置计时起点
* → 第 i 拍 elapsed = i,需 101 拍才到 100ms */
for (i = 0; i < 101; i++) {
in = mk_in(seq, now);
adc_health_check(&in, &st);
now++;
}
CHECK(((st.flags & ADC_HF_TPAFE_STALL) != 0u) &&
((st.flags & ADC_HF_STM32_STALL) != 0u),
"TC-AH-009 stall still detected after tick wrap");
}
/* TC-AH-010:NULL 守卫 */
static void tc_null_guard(void) {
adc_health_status_t st;
adc_health_in_t in;
int i;
adc_health_init();
in = mk_in(1u, 1u);
adc_health_check(&in, &st);
st.flags = 0xA5A5A5A5u;
adc_health_check(NULL, &st);
CHECK(st.flags == 0xA5A5A5A5u, "TC-AH-010 in=NULL leaves out untouched");
/* out=NULL:内部状态照常更新(两侧冻结 101 拍 → 静默位应出现) */
for (i = 0; i < 101; i++) {
in = mk_in(1u, (uint32_t)(2 + i));
adc_health_check(&in, NULL);
}
CHECK((adc_health_flags() & ADC_HF_TPAFE_STALL) != 0u,
"TC-AH-010 out=NULL still updates internal state");
}
/* TC-AH-011:init 幂等 / 故障后全清 */
static void tc_reinit(void) {
adc_health_status_t st;
adc_health_in_t in;
int i;
adc_health_init();
in = mk_in(1u, 1u);
adc_health_check(&in, &st);
for (i = 0; i < 150; i++) { /* 制造双侧静默 */
in = mk_in(1u, (uint32_t)(2 + i));
adc_health_check(&in, &st);
}
CHECK(st.sticky != 0u, "TC-AH-011 pre: sticky non-zero before re-init");
adc_health_init();
CHECK((adc_health_flags() == 0u) && (adc_health_sticky() == 0u) &&
(adc_health_tpafe_gaps() == 0u),
"TC-AH-011 re-init clears flags/sticky/gaps");
in = mk_in(9u, 9u); /* 序号跳变也无妨:首拍只建基线 */
adc_health_check(&in, &st);
CHECK((st.flags == 0u) && (st.events == 0u),
"TC-AH-011 re-prime after re-init is clean");
}
int main(void) {
printf("=== ADC Health Tests ===\n");
printf("-- prime / healthy --\n");
tc_prime();
tc_healthy();
printf("-- stall (TPAFE / STM32) --\n");
tc_tpafe_stall();
tc_stm32_stall();
printf("-- gap / backward / err counters --\n");
tc_tpafe_gap();
tc_stm32_gap();
tc_backward();
tc_err_counters();
printf("-- tick wrap / guards / re-init --\n");
tc_tick_wrap();
tc_null_guard();
tc_reinit();
printf("\n=== ADC Health Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""ADC 采样有效性校验(L1-6)的主机侧单元测试运行器(L0.5,纯标准库)。
做什么:
用本机 MSVC 编译 test/host/adc_health/adc_health_test.c(它以 #include 纳入
**生产源文件** App/algo/adc_health.c),在 PC 上离线跑完整的判定逻辑验证:
首拍建基线 / 健康不误报 / 静默 100ms 门限与恢复留痕 / 丢帧边沿语义 /
序号倒退锁存 / 三类错误计数增量 / tick 回绕 / NULL 守卫 / init 幂等,
失败即非 0 退出。零硬件、零烧录、毫秒级。
工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness
`test/host/common/unit_test_harness.py`(本文件只做声明)。
为什么要主机侧单测(而不是只用板端闭环):
静默/倒退/错误增量在健康台架上不可复现(复现 = 拔芯片/改驱动)——板端只能
证明"健康时不误报",证不了"故障时必报";而 100ms 门限、tick 回绕、事件
边沿语义这些边界只有合成计数序列能精确构造。
本层为**纯逻辑**:无寄存器、无 RTOS、无文件/外设依赖,故**不需要任何桩目录**。
用法(仓库根目录):
python test/host/adc_health/run_unit_test.py
或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/adc_health/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
设计依据:docs/算法/继电保护功能复现规划.md §3.1(L1-6);
docs/测试说明/测试开发标准流程.md §4.2(L0.5 主机侧单测)。
创建日期:2026-10-11
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
# harness 位于 test/host/common/
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
# 无桩目录:被测代码为纯逻辑(adc_health.c 仅依赖 stdint.h 与自身头)。
BUILD = TestBuild(
name="ADC Health Tests",
src=os.path.join(HERE, "adc_health_test.c"),
include_dirs=[
os.path.join(ROOT, "App", "algo"),
HERE,
],
summary_marker="=== ADC Health Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))