From 13a4e61c2724b6c4f18aa0f2c9f7d9e007aa2f68 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sun, 11 Oct 2026 01:28:20 +0800 Subject: [PATCH] =?UTF-8?q?L1-6=20=E9=87=87=E6=A0=B7=E6=9C=89=E6=95=88?= =?UTF-8?q?=E6=80=A7=E6=A0=A1=E9=AA=8C=EF=BC=9Aadc=5Fhealth=20=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E5=B1=82=20+=20=E5=91=8A=E8=AD=A6=E7=BC=96=E6=8E=92?= =?UTF-8?q?=20+=2031=20=E7=94=A8=E4=BE=8B=E5=8D=95=E6=B5=8B=20+=20?= =?UTF-8?q?=E5=8F=B0=E6=9E=B6=E9=97=AD=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 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 已同步置完成。 --- .pre-commit-config.yaml | 14 + AGENTS.md | 8 +- App/algo/README.md | 40 +- App/algo/adc_health.c | 200 ++++++++++ App/algo/adc_health.h | 126 ++++++ App/task/adc_task.c | 91 ++++- MDK-ARM/STM32F407-Demo.uvprojx | 5 + README.md | 2 +- Src/freertos.c | 12 +- docs/内存管理/RAM使用分析与优化方案.md | 11 +- .../20261011_prod_adc_health_boot_log.txt | 116 ++++++ docs/测试说明/boot_log_data/README.md | 2 + docs/算法/继电保护功能复现规划.md | 7 +- test/host/adc_health/adc_health_test.c | 374 ++++++++++++++++++ test/host/adc_health/run_unit_test.py | 54 +++ 15 files changed, 1035 insertions(+), 27 deletions(-) create mode 100644 App/algo/adc_health.c create mode 100644 App/algo/adc_health.h create mode 100644 docs/测试说明/boot_log_data/20261011_prod_adc_health_boot_log.txt create mode 100644 test/host/adc_health/adc_health_test.c create mode 100644 test/host/adc_health/run_unit_test.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e1396897..21307e08 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 里 diff --git a/AGENTS.md b/AGENTS.md index 18be78ab..b198f776 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 | -(实际值以 `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 栈 / 目录组织) ## 硬件引脚 diff --git a/App/algo/README.md b/App/algo/README.md index 4158632a..e3a463fe 100644 --- a/App/algo/README.md +++ b/App/algo/README.md @@ -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` 已在 `` 内。 +- **挂载**:四个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`,`../App/algo` 已在 `` 内。 本层**不进** `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. 新增一个算法模块的固定动作 diff --git a/App/algo/adc_health.c b/App/algo/adc_health.c new file mode 100644 index 00000000..77cccc38 --- /dev/null +++ b/App/algo/adc_health.c @@ -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; +} diff --git a/App/algo/adc_health.h b/App/algo/adc_health.h new file mode 100644 index 00000000..2e24df6f --- /dev/null +++ b/App/algo/adc_health.h @@ -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 + +#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 */ diff --git a/App/task/adc_task.c b/App/task/adc_task.c index 535a6d18..c6211766 100644 --- a/App/task/adc_task.c +++ b/App/task/adc_task.c @@ -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"); } diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index 4bef565e..15c97bd0 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -957,6 +957,11 @@ 1 ..\App\algo\phasor.c + + adc_health.c + 1 + ..\App\algo\adc_health.c + diff --git a/README.md b/README.md index 4c094eca..1bd71484 100644 --- a/README.md +++ b/README.md @@ -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) | > 上表与 `Src/freertos.c` 的 `osThreadAttr_t` 一致(2026-09-25 校准);改动栈大小前先读 diff --git a/Src/freertos.c b/Src/freertos.c index b0d0d0d1..37f0f4ca 100644 --- a/Src/freertos.c +++ b/Src/freertos.c @@ -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 */ diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 450eb256..97d0d1c8 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| 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%** | 各区的容量与常量来源: @@ -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 | 软定时器 | 未观测 | diff --git a/docs/测试说明/boot_log_data/20261011_prod_adc_health_boot_log.txt b/docs/测试说明/boot_log_data/20261011_prod_adc_health_boot_log.txt new file mode 100644 index 00000000..cba05672 --- /dev/null +++ b/docs/测试说明/boot_log_data/20261011_prod_adc_health_boot_log.txt @@ -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 diff --git a/docs/测试说明/boot_log_data/README.md b/docs/测试说明/boot_log_data/README.md index 874be6c0..5c0c0cd6 100644 --- a/docs/测试说明/boot_log_data/README.md +++ b/docs/测试说明/boot_log_data/README.md @@ -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. 关联 diff --git a/docs/算法/继电保护功能复现规划.md b/docs/算法/继电保护功能复现规划.md index 19ad4f61..23925208 100644 --- a/docs/算法/继电保护功能复现规划.md +++ b/docs/算法/继电保护功能复现规划.md @@ -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 置 ✅ | diff --git a/test/host/adc_health/adc_health_test.c b/test/host/adc_health/adc_health_test.c new file mode 100644 index 00000000..724d9f90 --- /dev/null +++ b/test/host/adc_health/adc_health_test.c @@ -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 +#include + +/* ===================== 断言 ===================== */ +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; +} diff --git a/test/host/adc_health/run_unit_test.py b/test/host/adc_health/run_unit_test.py new file mode 100644 index 00000000..d418e88c --- /dev/null +++ b/test/host/adc_health/run_unit_test.py @@ -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))