feat(采集标定): 卡2 adc_cal 折算模块 + 卡4 系数落盘 + L5-2 码值贴轨检出

卡 2(App/algo/adc_cal.*,新增;adc_task.c:335 折算点收口):
- 模型:工程量 =(code+b)×k(「数据可信」§4.1);k 以 int32 Q16(µV/码×65536)
  落盘、b 以 int32 码值落盘;ROM 默认 = 卡 1 定死的引脚侧 LSB,Q16 下整除无
  舍入(TPAFE 10,000,000=152.587890625 µV/码;STM32 52,800,000=805.6640625)
- adc_task 每拍 frame[s] 改 adc_cal_apply()(现单位 = ADC 引脚电压 V,eng=0,
  二次值语义等 VALID);TEMP 透传码值(决策 5);不缓存 k_f 副本——逐点由
  RAM 现值派生(无回调设计下杜绝陈旧系数;18k flops/s 可忽略)
- 三态 flags(§4.2):VALID = 文件位图 ∧ k 在合法带;CLIPPED 按来源运行期
  派生(V0.2/N-18 后自动恒 0);消费者判可用性只查 adc_cal_flags()
- 合法带哨兵:k∉ROM×[0.5,2.0] 或 |b|>8192 → 折算回退 ROM + 每通道每开机
  一条 [ERR] + VALID 压制(位图说谎也兜住)——同时挡坏文件与坏标定
- 两点标定公式 adc_cal_two_point() 算术核齐备(int64、±舍入界、斜率/零点
  守卫 -1/-2 分档);台架两点标定(卡 12)按用户指示未跑,留台架注入口径:
  心跳新增 cal: 行(ua/un/ia_mv 定点 + vl/cl/rail 位图,未标定 vl 应恒 0x00、
  cl 应恒 0xE6,注入饱和电平 rail 位应起)

卡 4([adc_cal] k_ua..k_in+valid / [adc_cal_ofs] b_ua..b_in,§4.4 布局):
- 两段注册进 cfg 引擎(data_size 33/32,不挂回调——RAM 副本即生效值);
  adc_cal_init() 落 app_main_init 内核前(注册必须先于 defaultTask 的
  cfg_file_load;跨任务读走 4 字节对齐字,µs 级混窗为「数据可信」§4.4 明载
  设计);文件缺失/损坏 = 引擎回退工厂默认 = ROM 表本体(默认即理论系数)
- adc_cal_set():整定写入口(属主上下文),越带拒绝 RAM 不动
- 单测走真实 cfg_file 引擎集成:文件值生效 / 越界 k 哨兵兜住且不报 VALID /
  超长损坏文件回退默认且 flags 无 VALID(卡 4 DoD 原句)/ 段缺失默认

L5-2(adc_health v1.2,越界门限按 FS 百分比、码值域与标定正交):
- adc_health_rail_watch():连续 20 拍(=一周波)贴同侧轨带(TPAFE±98%半量程、
  STM32 顶 98%/底 2%,门限由 adc_cal_rail_ths 供)且此前见过中带释放沿才
  锁存——空载恒 0 / 开机恒 0 合法工况不误报(短板评测 §6.2 判据①);
  adc_task 编排:边沿 DBG_ERROR + SYSLOG 每开机一次 + 心跳 rail/edge 位图
- 健康用例 60/60(+13):释放沿/清零/交替/双侧/init 复位/越界通道守卫

测试与注册:新套件 test/host/adc_cal(89/89,8 通道×≥3 码点期望值由 LSB
常数独立推导、Q16 无舍入、set 边界含端点、两点还原合成 k/b 往返);
编号形态返工一轮:TC-ADCCAL-00A~00D 改 014~017、注释 FS-1 字样改写、
id_registry 族 pattern 转义修正、族游标登至 017;coding-standard 返工:
公开实现函数补齐函数头注释块(CODE-RULE-09)
uvprojx 仅增 adc_cal.c 一条挂载(diff 已核零抖动);SIM_EXEMPT/id_registry
(TC-HOST-ADCCAL 族)/adc-cal-unit-test 钩子行按 new-source-file 与 B 层口径
补齐;覆盖率 34/34 套件 89.6%,adc_cal.c 99.2%(baseline --update 属共享
台账文件,留 lead done 后统一回填)

gate:@build 0 错 0 警(Code=215440 RO=19780 RW=1220 ZI=131980);
RAM 增量 +144 B(RW+16/ZI+128:贴轨 9×6B + 两段 RAM 副本,IRAM1 57.7%→
约 57.8%);Flash +1336B/+920B RO。adcTask 栈 worst=1344B margin 1216B
(47%) PASS(stack_report 实测)。staged 门禁除 mem-numbers/mem-budget 外
全绿(该两条 = 台账漂移,回填 --write 落 docs/内存管理/** 单写者区,按
Lead 既定例外归 lead done 后处理):
gate: only mem-budget/mem-numbers pending lead ledger backfill
BOM/CR/U+FFFD/字面量 ASCII 已逐文件自查(本次未触碰任何带 BOM 文件)
This commit is contained in:
2026-10-25 04:21:12 +08:00
parent 65c5e7d310
commit 3528644e9d
15 changed files with 1545 additions and 24 deletions
+15
View File
@@ -341,6 +341,21 @@ repos:
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
# 7h2) 通道标定层(L1.5,「数据可信」卡 2/卡 4)主机侧单元测试(2026-10-25 加,道A)
# 背景:标定折算是"码值→工程量"的唯一插入点,Q16 系数/零点两段落盘 +
# 合法带哨兵回退 + 两点标定公式——板端无标准源、坏文件注入路径在真机
# 上难以复现,只有离线端能逐系数钉死。钩子用 MSVC 编译真实源文件
# (#include 纳入;桩:dbg_log / cfg_file_register / adc_input_desc 强符号)。
# files 含被测源与头、系数口径联动的 adc_health/cfg_file/adc_task 消费方,
# 以及 harness 本身。
- id: adc-cal-unit-test
name: 通道标定层单测(主机侧)
entry: python test/host/adc_cal/run_unit_test.py
language: system
files: ^(App/algo/adc_cal\.(c|h)|App/algo/adc_health\.(c|h)|App/cfg/cfg_file\.(c|h)|App/task/adc_task\.c|test/host/adc_cal/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 7i) 保护定值模型(P1 / L5-1 载体)主机侧单元测试(2026-10-11 加)
# 定值是保护动作的"输入宪法":范围校验、整表装载 all-or-nothing、
# 校验和封存-自检-闭锁(参考 tagPRS dwTagSum/dwTagSumN 模式)必须在
+39 -12
View File
@@ -10,16 +10,19 @@
| 文件 | 层 | 作用(一句话) | 主要对外入口 | 覆盖规划项 |
|---|---|---|---|---|
| `adc_input.h` / `.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` / `.c` | L1 | 采样有效性判定:吃计数快照,产出静默 / 丢帧 / 单调性 / 错误增量故障位(判定与告警编排分离) | `adc_health_check()` `adc_health_flags()` | L1-6 |
| `adc_health.h` / `.c` | L1 | 采样有效性判定:吃计数快照,产出静默 / 丢帧 / 单调性 / 错误增量故障位(判定与告警编排分离);L5-2 残余的码值贴轨观察器同层 | `adc_health_check()` `adc_health_flags()` `adc_health_rail_watch()` | L1-6 / L5-2(残余) |
| `adc_cal.h` / `.c` | L1.5 | 通道标定折算(「数据可信」卡 2/卡 4):工程量 =(code+b)×k,Q16 系数/零点两段落盘 + 三态 flags + 合法带哨兵 + 两点公式 | `adc_cal_apply()` `adc_cal_flags()` `adc_cal_set()` | L1-5(M4 半边) |
| `sample_ring.h` / `.c` | L2 输入 | 按语义通道存最近 2 周波(40 点),供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | L2 输入(为 L2-6 供数) |
| `phasor.h` / `.c` | L2 | 全周傅氏:一周波 20 点 DFT → 基波 / 谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | L2-1 / L2-2 / L2-3 / L2-6 / L2-7 |
| `freq.h` / `.c` | L2 派生量 | 频率测量:相邻两拍基波相量共轭乘积取角 + 自适应窗滑动平均 + 假值钳制 + 幅值门(UA→IA 源回退) | `freq_run()` `freq_get()` `freq_valid()` | L3-5 测量侧(测频范围 → `docs/算法/继电保护功能复现规划.md` §4.2) |
| `prot_set.h` / `.c` | L3 输入 | 保护定值模型:Q10 定点 RAM 表 + 范围校验 + 校验和封存 / 自检 / 闭锁 + 持久化 blob 纯逻辑 | `prot_set_load_raw()` `prot_set_get_f()` `prot_set_selfcheck()` | L5-1 载体 |
| `prot_elm.h` / `.c` | L3 | 保护元件框架:通用"越限 + 返回系数滞回 + 定时限"核 + 数据驱动实例表(过流三段 / 过负荷 / 过压 / 欠压 / 零序过压 / 过频 / 低频) | `prot_elm_run()` `prot_elm_eval()` `prot_elm_active_mask()` | L3-1 / L3-3 / L3-4 / L3-5 / L3-6(软件侧) |
依赖(自下而上、无环):`adc_input.h`(纯枚举 + 声明)← `sample_ring` ← `phasor` ← `freq`;
依赖(自下而上、无环):`adc_input.h`(纯枚举 + 声明)← `adc_cal`(另依赖 `cfg_file.h` 接口,
纯头文件级)← `sample_ring` ← `phasor` ← `freq`;
`prot_set` **独立**;`prot_elm` ← `prot_set` + `phasor` + `freq`。
`adc_health` 只吃调用方喂进来的计数快照,不反向依赖 L1 实现。
`adc_health` 只吃调用方喂进来的计数 / 码值,不反向依赖 L1 实现(L5-2 贴轨门限由调用方
从 `adc_cal_rail_ths()` 取,本层仍零算法依赖)。
**除 `adc_input.c` 外全部零平台依赖**(只用 `stdint.h` / `math.h` / `stddef.h` 与本目录头)
→ 可被主机单测 `#include` 原样纳入、免桩跑。`adc_input.c` 另依赖 `adc.h`(`hadc1`)与
@@ -27,16 +30,20 @@
## 2. 一拍的数据流(一眼版)
`adc_input_poll()` → `adc_input_raw()` ×9 → `sample_ring_push_frame()` → `phasor_run()`
→ `freq_run()` → 消费者(`adcTask` 心跳与 L1 健康判定;`protectTask` 每 20 ms 取相量 / 频率跑
`adc_input_poll()` → `adc_input_raw()` ×9 → `adc_cal_apply()` ×9(L1.5 折算,现口径 =
ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(L5-2,码值域)→ `sample_ring_push_frame()`
→ `phasor_run()` → `freq_run()` → 消费者(`adcTask` 心跳与 L1 健康判定;`protectTask` 每 20 ms 取相量 / 频率跑
`prot_elm_run()`)。节拍 = TIM3 1 kHz 更新事件,两片转换器同源触发。
完整数据流图与初始化落点 → `docs/算法/采集与算法层设计说明.md` §2。
## 3. 逐文件要点(只留"改代码时必须知道"的;机制与理由见设计文档)
> **改本目录前先记四条红线**:① 取数只经 `adc_input_raw()`,不写死下标;② **禁止跨转换器**做
> 相位 / 功率 / 矢量运算(N-18);③ 标定未落地,现以原始码值充当工程量,**幅值读数不可作精度判据**;
> ④ 本层不建任务、不等事件、不写日志,`*_init()` 才可在内核前调。
> **改本目录前先记五条红线**:① 取数只经 `adc_input_raw()`,不写死下标;② **禁止跨转换器**做
> 相位 / 功率 / 矢量运算(N-18);③ 帧值已是 L1.5 工程量,但 **未标定(`adc_cal_flags()` 无
> VALID)时单位只是 ADC 引脚电压**,不是端子二次值——下游消费精度判据见「数据可信」§4.2 三态
> 表;④ 工程量 → 帧的唯一折算点是 `adc_cal_apply()`,禁止旁路再乘系数;⑤ 本层不建任务、
> 不等事件、不写日志(`adc_cal` 的 `[ERR]` 哨兵留痕是唯一例外,且每通道每开机一次),
> `*_init()` 才可在内核前调。
> 全文 → `docs/算法/采集与算法层设计说明.md` §10。
### 3.1 `adc_input.c` / `adc_input.h` — L1 语义取数
@@ -81,11 +88,28 @@
- 源切换即废相位基准重建(UA / IA 无共同相位基准,N-18)。
- 判据 **J1~J6** 与实测口径 → `docs/算法/采集与算法层设计说明.md` §7.2(与 `test/host/freq/freq_test.c` 文件头同源)。
### 3.8 `adc_cal.c` / `.h` — L1.5 通道标定折算(「数据可信」卡 2 / 卡 4)
- 折算模型:工程量 = (code + b) × k;**k 落盘用 int32 Q16(µV/码 × 65536)**、b 用
int32 码值(负零点靠 `CFG_VAL_INT`)——单位与定点格式的选型依据见
`docs/任务规划/数据可信.md` §4.1,勿改回 V/码 直存(Q16 只剩 20 个刻度)。
- **ROM 默认 = 卡 1 定死的引脚侧 LSB**(TPAFE 152.59 / STM32 805.66 µV/码,
`docs/硬件平台/ADC通道映射说明.md` §7.1);表下标 = 语义枚举(UA0…IN7),
V0.2 改版与本目录 `adc_input` 映射表同步改。
- 三态可用性 `adc_cal_flags()`:VALID(已按变比标定,二次值成立——卡 0b/12 前恒 0)
/ CLIPPED(运行期按来源派生,V0.2 后自动清零)。**消费者判「能不能信」只许查它**,
`adc_cal_apply()` 不返回可用性(「数据可信」§8 风险表)。
- 合法带哨兵:k ∉ ROM×[0.5,2.0] 或 |b| > 8192 → 折算回退 ROM + 每通道每开机一条
`[ERR]` + VALID 被压制(**即便文件位图说已标定**);这层兜底同时挡坏文件与坏标定。
- `adc_cal_set()` / `adc_cal_two_point()`:整定与两点公式的算术核已齐备(单测还原合成
k/b);**台架两点标定(卡 12)待实测**,本层不含注入。
- cfg 两段 `[adc_cal]` / `[adc_cal_ofs]` **不挂回调**(RAM 副本即生效值,引擎 load/poll
原地改写,逐点现读);注册在 `app_main_init()` 内核前完成,必须先于 `cfg_file_load`。
## 4. 构建与测试
### 4.1 挂载与模拟器边界
- 七个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `<IncludePath>` 内。
- 本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路):七个文件全部登记在
- 八个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `<IncludePath>` 内。
- 本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路):八个文件全部登记在
`scripts/check_registration.py` 的 `SIM_EXEMPT`(附理由),`registration-check` 钩子反向校验(N-23)。
### 4.2 主机侧单测(L0.5)——生产源文件以 `#include` 原样纳入
@@ -98,14 +122,17 @@
| 定值模型 | `& '.venv\Scripts\python.exe' test/host/prot_set/run_unit_test.py` | `prot-set-unit-test` |
| 元件框架 | `& '.venv\Scripts\python.exe' test/host/prot_elm/run_unit_test.py` | `prot-elm-unit-test` |
| 频率测量 | `& '.venv\Scripts\python.exe' test/host/freq/run_unit_test.py` | `freq-unit-test` |
| 通道标定(含真实 cfg_file 引擎集成) | `& '.venv\Scripts\python.exe' test/host/adc_cal/run_unit_test.py` | `adc-cal-unit-test` |
改本目录任一 `.c/.h`,提交时对应钩子自动跑。**用例清单与判据数字的属主**是各
`test/host/<模块>/` 与 `docs/测试说明/` 下的单测说明,本文不复制(三处抄数字必然打架)。
### 4.3 板端
`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍 / 心跳(`phasor(...)`、`freq(...)`、
`ring` / `skip` / `adc1_*` / `tgap`);健康判据 = **零 `[ERR]`**(健康告警全以 `[ERR]` 开头,
误报会被闭环直接抓住)。→ `docs/算法/采集与算法层设计说明.md` §12。
`cal: ua_mv=… vl=0x00 cl=0xE6 rail=…`、`ring` / `skip` / `adc1_*` / `tgap`);
健康判据 = **零 `[ERR]`**(健康告警与标定哨兵全以 `[ERR]` 开头,误报会被闭环直接抓住;
`vl` 在未标定默认构建必须为 0x00,卡 5 闭锁落地后 `cal:` 行就是台架期望的直接证据)。
→ `docs/算法/采集与算法层设计说明.md` §12。
## 5. 新增一个算法模块的固定动作
+374
View File
@@ -0,0 +1,374 @@
/*
* 模块名称:通道标定与量纲折算(L1.5)——实现
* 模块功能:见 adc_cal.h。折算模型(工程量 = (code + b) × k × 1e-6,k 为 Q16
* µV/码)与两段 cfg 布局([adc_cal]:k_* + valid 位图;[adc_cal_ofs]:
* b_*)照 docs/任务规划/数据可信.md §4.1 / §4.4 落地;系数口径
* (ROM 默认 = 卡 1 定死的引脚侧 LSB)见 docs/硬件平台/ADC通道映射说明.md
* §7.1。
* 并发契约:cfg RAM 副本属主 = 配置引擎(defaultTask 的 load/poll 原地
* 改写),本层逐点折算现读——int32 对齐字在 Cortex-M4 上读写原子,
* poll 重载「复位默认 + 逐键解析」存在 µs 级窗口,撞上则该拍该通道
* 为默认值/半新值(相量 20 点窗摊薄,且触发面 = 用户改文件后 4 s
* 沉降一次性事件)——「数据可信」§4.4 明载此设计(不挂回调)。
* 合法带哨兵(防损坏文件/错误标定把假电压送进下游):k ∉ ROM×[0.5,2.0]
* 或 |b| > ADC_CAL_B_MAX → 该通道折算回退 ROM + b=0,flags 不报
* VALID,每通道每次开机首见沿打一条 [ERR](adc_cal_init 重置留痕)。
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
* 作者:DTU 项目组
* 创建日期:2026-10-25(道A 卡 2/卡 4)
* 修改记录:
* v1.0 2026-10-25 DTU 项目组 创建:Q16 系数/零点两段落盘 + 折算 + 三态 flags
* + 合法带哨兵回退 + 两点标定公式(台架注入待跑)
*/
/* 头文件包含区 */
#include <stddef.h>
#include <string.h>
#include "adc_cal.h"
#include "cfg_file.h"
#define DBG_TAG "[CAL]"
#include "dbg_log.h"
/* 常量定义区 */
/* 段名(≤ CFG_FILE_MAX_NAME_LEN)与键名(k_ua…b_in,均 4/5 字符)
* ——「数据可信」§4.4 布局;键的书写顺序按该表,存储下标按语义枚举 */
#define ADC_CAL_SEC_K "adc_cal"
#define ADC_CAL_SEC_B "adc_cal_ofs"
/* Q16 → V 的合并比例因子:1e-6 / 65536(float 常量,精确到 1 ulp 以内即可:
* 系数本身 Q16 舍入误差 < 0.001 %,见「数据可信」§4.1) */
#define ADC_CAL_K_SCALE (1.52587890625e-11f)
/* 静态变量定义区 */
/* [adc_cal] 段 RAM 副本:k 按语义下标 0..7,valid 位图 bit=语义下标。
* data_size 取 offsetof(valid)+1 = 33(尾随对齐填充不属于引擎视图,
* 引擎「复位默认」按 data_size memcpy,必须覆盖 valid,见单测 TC-ADCCAL-001) */
typedef struct {
int32_t k[8];
uint8_t valid;
} adc_cal_kcfg_t;
/* [adc_cal_ofs] 段 RAM 副本:b 按语义下标 0..7(int32 有符号,卡 3 的
* CFG_VAL_INT 是负零点偏移能干净落盘的前提) */
typedef struct {
int32_t b[8];
} adc_cal_bcfg_t;
static adc_cal_kcfg_t s_kcfg; /* 生效系数副本(引擎原地更新) */
static adc_cal_bcfg_t s_bcfg; /* 生效零点副本 */
/* 工厂默认(ROM 理论系数,卡 1 定死,「数据可信」§4.4):**下标 = 语义枚举**
* (UA0 / UB1 / UC2 / UN3 / IA4 / IB5 / IC6 / IN7)——UA/UN/IA 走 TPAFE
* (16 位 ±5 V 档),UB/UC/IB/IC/IN 走 STM32 ADC1;逐路对过
* docs/硬件平台/ADC通道映射说明.md §3(IN=ADC11/STM32,勿再按分组顺序误写)。
* ROM 表按**当前 V0.1 来源排布**显式书写(不查 desc 运行期派生),因为它是
* 卡 1 定稿的存档事实;V0.2(N-18)改版时本表与 adc_input 映射表同步改,
* 上层语义与 API 不动。CLIPPED 位则**运行期按来源派生**(见 adc_cal_flags) */
static const adc_cal_kcfg_t s_kcfg_rom = {
/* idx0 UA / idx1 UB / idx2 UC / idx3 UN */
{ ADC_CAL_K_Q16_TPAFE, ADC_CAL_K_Q16_STM32, ADC_CAL_K_Q16_STM32,
ADC_CAL_K_Q16_TPAFE,
/* idx4 IA / idx5 IB / idx6 IC / idx7 IN */
ADC_CAL_K_Q16_TPAFE, ADC_CAL_K_Q16_STM32, ADC_CAL_K_Q16_STM32,
ADC_CAL_K_Q16_STM32 },
0u /* 出厂未标定:VALID 全 0 */
};
static const adc_cal_bcfg_t s_bcfg_rom = { { 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L } };
/* 合法带哨兵留痕位(bit=语义下标;每通道每次开机只打一条 [ERR],
* adc_cal_init 清零 = 新一轮观测窗) */
static uint8_t s_err_latched;
/* 键表:offset 用 offsetof 取真值(k[0]@0 / k[1]@4 … valid@32) */
static const cfg_key_def_t s_k_keys_tbl[9] = {
{ "k_ua", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[0]), 4u },
{ "k_un", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[3]), 4u },
{ "k_ia", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[4]), 4u },
{ "k_ub", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[1]), 4u },
{ "k_uc", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[2]), 4u },
{ "k_ib", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[5]), 4u },
{ "k_ic", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[6]), 4u },
{ "k_in", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[7]), 4u },
{ "valid", CFG_VAL_UINT, offsetof(adc_cal_kcfg_t, valid), 1u },
};
static const cfg_key_def_t s_b_keys_tbl[8] = {
{ "b_ua", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[0]), 4u },
{ "b_un", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[3]), 4u },
{ "b_ia", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[4]), 4u },
{ "b_ub", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[1]), 4u },
{ "b_uc", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[2]), 4u },
{ "b_ib", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[5]), 4u },
{ "b_ic", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[6]), 4u },
{ "b_in", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[7]), 4u },
};
/* 函数定义区 */
/*
* 函数功能:判定语义通道是否 STM32 源(经 adc_input 权威映射表,不另立第二份表)
* 入口参数:sem - 语义通道 unsigned
* 返 回 值:1 - STM32;0 - TPAFE(含 desc 取不到时的保守分支) int
* 限定条件:无
*/
static int s_rom_k_stm32(unsigned sem) {
const adc_channel_desc_t *d = adc_input_desc((adc_semantic_t)sem);
return ((d != NULL) && (d->source == ADC_SRC_STM32)) ? 1 : 0;
}
/*
* 函数功能:取语义通道的 ROM 默认系数(合法带/回退参照)
* 入口参数:sem - 语义下标(0..7) unsigned
* 返 回 值:Q16 系数 int32_t
* 限定条件:sem < 8(调用方已先行拦截 TEMP/越界)
* 函数说明:直接读出厂表本体——保证「哨兵合法带基准」与「引擎工厂默认」
* 永远同一份数(两者若靠重复表达式各算各的,改一处漏一处就是暗雷)
*/
static int32_t s_rom_k(unsigned sem) {
return s_kcfg_rom.k[sem];
}
/*
* 函数功能:k/b 合法带检查(ROM×[0.5,2.0] / |b| ≤ ADC_CAL_B_MAX)
* 入口参数:sem - 语义下标(0..7) unsigned
* k - 待检系数 Q16 int32_t
* b - 待检零点 码值 int32_t
* 返 回 值:1 - 合法;0 - 越带 int
* 限定条件:sem < 8
*/
static int s_kband_ok(unsigned sem, int32_t k, int32_t b) {
const int32_t rom = s_rom_k(sem);
if (k < (rom >> 1) || k > (rom << 1)) {
return 0;
}
if (b > ADC_CAL_B_MAX || b < -ADC_CAL_B_MAX) {
return 0;
}
return 1;
}
/*
* 函数功能:越带留痕(每通道每次开机首见沿一条 [ERR],init 重置)
* 入口参数:sem - 语义下标 0..7;k/b - 现场值
* 返 回 值:无
* 限定条件:仅 adc_cal_apply/set 内部
*/
static void s_note_bad(unsigned sem, int32_t k, int32_t b) {
const uint8_t bit = (uint8_t)(1u << sem);
if ((s_err_latched & bit) == 0u) {
s_err_latched |= bit;
DBG_ERROR("ch=%u k_q16=%ld b=%ld out of ROM band [0.5x,2x]/|b|<=%ld -> ROM fallback",
(unsigned)sem, (long)k, (long)b, (long)ADC_CAL_B_MAX);
}
}
/*
* 函数功能:读通道「当前生效且合法」的 k/b(越带就地回退 ROM)
* 入口参数:sem - 语义下标 0..7;p_k/p_b - 输出
* 返 回 值:1 - 用了合法值;0 - 已回退 ROM
* 限定条件:sem < 8
*/
static int s_eff_kb(unsigned sem, int32_t *p_k, int32_t *p_b) {
const int32_t k = s_kcfg.k[sem];
const int32_t b = s_bcfg.b[sem];
if (s_kband_ok(sem, k, b) != 0) {
*p_k = k;
*p_b = b;
return 1;
}
s_note_bad(sem, k, b);
*p_k = s_rom_k(sem);
*p_b = 0L;
return 0;
}
/*
* 函数功能:标定层初始化——RAM 副本置 ROM 默认 + 注册两段进 cfg 引擎(幂等)
* 入口参数:无
* 返 回 值:无
* 限定条件:内核启动前调用(app_main_init);注册必须先于 cfg_file_load
* 函数说明:完整契约见 adc_cal_init() 同名声明(adc_cal.h);注册失败(段名重复/
* 段数超限)打 [ERR] 后继续——ROM 默认系数照常生效,仅文件整定失效
*/
void adc_cal_init(void) {
cfg_section_reg_t reg;
(void)memcpy(&s_kcfg, &s_kcfg_rom, sizeof(s_kcfg));
(void)memcpy(&s_bcfg, &s_bcfg_rom, sizeof(s_bcfg));
s_err_latched = 0u;
(void)memset(&reg, 0, sizeof(reg));
reg.name = ADC_CAL_SEC_K;
reg.p_data = &s_kcfg;
reg.p_defaults = &s_kcfg_rom;
reg.data_size = (offsetof(adc_cal_kcfg_t, valid) + 1u); /* 33:覆盖到 valid */
reg.p_keys = s_k_keys_tbl;
reg.key_count = 9u;
reg.p_cb = NULL; /* 不挂回调:RAM 副本即生效值(「数据可信」§4.4) */
if (cfg_file_register(&reg) != 0) {
DBG_ERROR("register '%s' FAIL (dup/full?) - calibration file values inactive",
ADC_CAL_SEC_K);
}
(void)memset(&reg, 0, sizeof(reg));
reg.name = ADC_CAL_SEC_B;
reg.p_data = &s_bcfg;
reg.p_defaults = &s_bcfg_rom;
reg.data_size = sizeof(s_bcfg); /* 32 */
reg.p_keys = s_b_keys_tbl;
reg.key_count = 8u;
reg.p_cb = NULL;
if (cfg_file_register(&reg) != 0) {
DBG_ERROR("register '%s' FAIL (dup/full?) - zero-offset file values inactive",
ADC_CAL_SEC_B);
}
}
/*
* 函数功能:码值 → 工程量折算(「数据可信」§4.1 模型)
* 入口参数:sem - 语义通道;code - 原始码值 adc_semantic_t / int32_t
* 返 回 值:工程量浮点(未标定 = ADC 引脚电压 V;TEMP/越界 = 原样透传码值) float
* 限定条件:无(任意上下文,纯读)
* 函数说明:见 adc_cal.h 同名声明;k/b 经 s_eff_kb 合法带哨兵(越带回退 ROM)
*/
float adc_cal_apply(adc_semantic_t sem, int32_t code) {
int32_t k;
int32_t b;
if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) {
return (float)code; /* TEMP/越界:不纳入线性标定(决策 5),原样透传 */
}
(void)s_eff_kb((unsigned)sem, &k, &b);
return ((float)(code + b)) * ((float)k) * ADC_CAL_K_SCALE;
}
/*
* 函数功能:读通道可用性标志(消费者判「能不能信」的唯一入口)
* 入口参数:sem - 语义通道 adc_semantic_t
* 返 回 值:ADC_CAL_F_VALID / ADC_CAL_F_CLIPPED 组合;TEMP/越界恒 0 uint32_t
* 限定条件:无
* 函数说明:VALID 需文件位图与 k 合法带**同时**成立(哨兵压制说谎位图,见文件头)
*/
uint32_t adc_cal_flags(adc_semantic_t sem) {
uint32_t f = 0u;
if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) {
return 0u;
}
/* VALID = 文件位图置位 且 k 合法(哨兵不落共享状态,随重载自愈) */
if (((s_kcfg.valid & (uint8_t)(1u << (unsigned)sem)) != 0u) &&
(s_kband_ok((unsigned)sem, s_kcfg.k[sem], s_bcfg.b[sem]) != 0)) {
f |= ADC_CAL_F_VALID;
}
/* CLIPPED 由**来源**派生(R5:STM32 侧单极性无偏置 → 负半周削顶)——
* V0.2 改版(N-18,8 路全接 TPAFE)后本位自动恒 0,无需改码,
* 对应「数据可信」§10「页面与 104 绑定零改动」的要求 */
if (s_rom_k_stm32((unsigned)sem) != 0) {
f |= ADC_CAL_F_CLIPPED;
}
return f;
}
/*
* 函数功能:读通道当前生效系数(µV/码,回退后口径)
* 入口参数:sem - 语义通道 adc_semantic_t
* 返 回 值:k[µV/码];TEMP/越界返 0 float
* 限定条件:无
* 函数说明:Q16 现值 ÷ 65536 浮点还原;勿据其反推二次值(可用性看 flags)
*/
float adc_cal_k_uv(adc_semantic_t sem) {
int32_t k;
int32_t b;
if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) {
return 0.0f;
}
(void)s_eff_kb((unsigned)sem, &k, &b);
return ((float)k) / 65536.0f;
}
/*
* 函数功能:整定入口——写通道系数与零点,并置位 VALID(「数据可信」卡 4)
* 入口参数:sem - 语义通道;k_q16 - 系数(µV/码×65536);b_codes - 零点(码值)
* 返 回 值:0 - 成功;-1 - 参数非法/越出合法带(RAM 不动,[ERR] 留痕) int
* 限定条件:属主上下文(defaultTask 整定/标定链);落盘由属主经 cfg_file_save
* 函数说明:见 adc_cal.h 同名声明
*/
int adc_cal_set(adc_semantic_t sem, int32_t k_q16, int32_t b_codes) {
if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) {
return -1;
}
if (s_kband_ok((unsigned)sem, k_q16, b_codes) == 0) {
DBG_ERROR("set ch=%u rejected: k_q16=%ld b=%ld out of ROM band [0.5x,2x]/|b|<=%ld",
(unsigned)sem, (long)k_q16, (long)b_codes, (long)ADC_CAL_B_MAX);
return -1;
}
s_kcfg.k[sem] = k_q16;
s_bcfg.b[sem] = b_codes;
s_kcfg.valid |= (uint8_t)(1u << (unsigned)sem);
return 0;
}
/*
* 函数功能:两点标定公式(卡 12 的算术核,纯函数无状态)
* 入口参数:c_lo/c_hi、v_lo_uv/v_hi_uv、p_k_q16/p_b 见 adc_cal.h 同名声明
* 返 回 值:0 成功 / -1 参数或斜率非法 / -2 零点越 ±B_MAX int
* 限定条件:无(int64 中间量,主机可测;台架注入待跑)
*/
int adc_cal_two_point(int32_t c_lo, int32_t c_hi, int32_t v_lo_uv, int32_t v_hi_uv,
int32_t *p_k_q16, int32_t *p_b) {
long long dc;
long long dv;
long long kq;
long long bb;
if ((p_k_q16 == NULL) || (p_b == NULL)) {
return -1;
}
dc = (long long)c_hi - (long long)c_lo;
dv = (long long)v_hi_uv - (long long)v_lo_uv;
if ((dc <= 0LL) || (dv <= 0LL)) {
return -1; /* 斜率必须为正:码与电压同向,反向 = 映射/接线错 */
}
kq = ((dv << 16) + (dc >> 1)) / dc; /* (v_hi−v_lo)×65536/dc,四舍五入(dc>0) */
if ((kq <= 0LL) || (kq > 100000000LL)) {
return -1; /* 绝对合理域上界 1526 µV/code:越界 = 单位/量级错 */
}
bb = (((long long)v_lo_uv) << 16) / kq - (long long)c_lo;
if ((bb > (long long)ADC_CAL_B_MAX) || (bb < -(long long)ADC_CAL_B_MAX)) {
return -2; /* 零点异常 → 先查前端,别用系数凑(§8 风险表) */
}
*p_k_q16 = (int32_t)kq;
*p_b = (int32_t)bb;
return 0;
}
/*
* 函数功能:通道码值域贴轨带门限(L5-2 判定的门限来源,按 FS 百分比)
* 入口参数:sem - 语义通道;p_lo_th/p_hi_th - 输出下/上轨带界 int32_t *
* 返 回 值:0 - 已给出门限;-1 - TEMP/越界/参数空(不做贴轨监视) int
* 限定条件:无
* 函数说明:状态机在 adc_health_rail_watch();本函数只供门限,无状态
*/
int adc_cal_rail_ths(adc_semantic_t sem, int32_t *p_lo_th, int32_t *p_hi_th) {
if ((p_lo_th == NULL) || (p_hi_th == NULL) ||
((unsigned)sem >= (unsigned)ADC_SEM_TEMP)) {
return -1; /* TEMP(热敏非线性,不判「轨」)与越界不做监视 */
}
if (s_rom_k_stm32((unsigned)sem) != 0) {
*p_hi_th = (int32_t)((4095L * 98L) / 100L); /* 4013 = 98 % FS */
*p_lo_th = (int32_t)((4095L * 2L) / 100L); /* 81 = 触底带(削顶签名) */
} else {
*p_hi_th = 32112L; /* +98 % 半量程 */
*p_lo_th = -32112L; /* -98 % 半量程 */
}
return 0;
}
+144
View File
@@ -0,0 +1,144 @@
/*
* 模块名称:通道标定与量纲折算(L1.5,「数据可信」卡 2/卡 4)
* 模块功能:把 adc_input 的原始码值折算成工程量——模型与系数口径的唯一落点:
* 工程量 = (code + b) × k_f,k_f = k[µV/码] × 1e-6
* k 以 int32 **Q16**(µV/码 × 65536)落盘、b 以 int32 码值落盘,
* RAM 副本注册进 cfg_file 引擎([adc_cal] / [adc_cal_ofs] 两段,
* 布局见 docs/任务规划/数据可信.md §4.4);**不挂回调**——RAM 副本
* 即生效值,逐点折算现取现用(1 kHz × 9 通道 = 9k 次/秒浮点乘,可忽略)。
* 三态可用性:ADC_CAL_F_VALID(已按变比标定 → 工程量为二次值)与
* ADC_CAL_F_CLIPPED(V0.1 硬件削顶)由 adc_cal_flags() 单点查询,
* 104 / UI / 保护闭锁(卡 5)共用(docs/任务规划/数据可信.md §4.2)。
* ⚠ 输出单位口径 = **ADC 引脚电压 V**(eng=0):ROM 默认系数按卡 1
* 定死的引脚侧 LSB(TPAFE 152.59 µV/码、STM32 805.66 µV/码,见
* docs/硬件平台/ADC通道映射说明.md §7.1);端子变比(卡 0b)与两点
* 标定(卡 12)完成前不得把输出当二次值消费。TEMP 不参与线性标定
* (决策 5),adc_cal_apply() 原样透传码值。
* 本层**纯算法 + 纯数据**:不碰寄存器、不等事件;adc_cal_init() 内核前
* 可调(注册进引擎必须先于 cfg_file_load,N-19 契约);apply/flags
* 任意上下文可读(cfg RAM 副本 4 字节对齐字读写在 Cortex-M4 上原子,
* 与 defaultTask 的 load/poll 复位存在 µs 级混窗——设计接受,见 .c 头)。
* 适用平台:STM32F407ZGTx(Cortex-M4F);主机侧 L0.5 单测 test/host/adc_cal/
* 作者:DTU 项目组
* 创建日期:2026-10-25(道A 卡 2/卡 4)
* 修改记录:
* v1.0 2026-10-25 DTU 项目组 创建:Q16 系数/零点 + 折算/flags/集
*/
#ifndef ADC_CAL_H
#define ADC_CAL_H
#include <stdint.h>
#include "adc_input.h" /* adc_semantic_t(语义通道编号 = 系数表下标口径) */
#ifdef __cplusplus
extern "C" {
#endif
/* 通道可用性标志(adc_cal_flags() 返回值位) */
#define ADC_CAL_F_VALID (0x01u) /* k 已按变比标定 → 工程量为二次值(卡 12 后置位) */
#define ADC_CAL_F_CLIPPED (0x02u) /* 硬件削顶(V0.1 STM32 侧单极性无偏置)→ 交流量不可信 */
/*
* ROM 默认系数(int32 Q16 = µV/码 × 65536,卡 1 定死的引脚侧口径):
* TPAFE ±5 V 档:10 V 跨度 / 65536 码 = 152.587890625 µV/码 → ×65536 = 10,000,000(整)
* STM32 0~3.3 V:3.3 V / 4096 码 = 805.6640625 µV/码 → ×65536 = 52,800,000(整)
* 两个默认值在 Q16 下**无舍入**(系数定点舍入项从误差预算里抹掉,
* 选型依据 → docs/任务规划/数据可信.md §4.1)
*/
#define ADC_CAL_K_Q16_TPAFE (10000000L)
#define ADC_CAL_K_Q16_STM32 (52800000L)
/* 系数合法带(「数据可信」§8 风险表):k 必须落在板级 ROM 系数的 ×[0.5, 2.0]
* 区间内;带宽用 ROM 值移位实现(>>1 / <<1),不设独立宏防两处真相 */
/* 零点偏移幅值上限(码值):超此即判文件损坏/异常标定,回退 ROM 并留痕 */
#define ADC_CAL_B_MAX (8192L)
/*
* 函数功能:标定层初始化——RAM 副本置 ROM 默认 + 注册两段进 cfg 引擎(幂等)
* 入口参数:无
* 返 回 值:无
* 限定条件:内核启动前调用(app_main_init);注册必须先于 cfg_file_load
* (「所有受管段先注册、后 load」契约,见 Drivers/BSP/NETCFG/net_cfg.c 的
* net_cfg_load 注释)
* 函数说明:段属主为 defaultTask(配置引擎),本模块只提供 RAM 副本与默认值;
* 文件值由引擎 load/poll 原地写入 RAM 副本,本层逐点折算现读
*/
void adc_cal_init(void);
/*
* 函数功能:码值 → 工程量折算(「数据可信」§4.1 模型)
* 入口参数:sem - 语义通道(TEMP 透传不折算) adc_semantic_t
* code - 原始码值(TPAFE 16 位补码 / STM32 0..4095) int32_t
* 返 回 值:工程量浮点(未标定时单位 = ADC 引脚电压 V;VALID 后置位 = 二次值) float
* 限定条件:无(任意上下文,纯读)
* 函数说明:eng = (code + b) × k[µV/码] × 1e-6,由 Q16 现值派生,无缓存副本
* (避免无回调下的陈旧系数);k/b 越合法带 → 该通道回退 ROM 默认 +
* 首见沿 [ERR] 留痕(adc_cal_init 重置留痕位),flags 同时不报 VALID
*/
float adc_cal_apply(adc_semantic_t sem, int32_t code);
/*
* 函数功能:读通道可用性标志(消费者判「能不能信」的唯一入口)
* 入口参数:sem - 语义通道 adc_semantic_t
* 返 回 值:ADC_CAL_F_VALID / ADC_CAL_F_CLIPPED 组合;TEMP 恒 0;sem 越界恒 0 uint32_t
* 限定条件:无
* 函数说明:VALID = 文件位图置位 **且** 该通道 k 在合法带内(哨兵不落状态,
* 天然跟随 load/poll 重载);CLIPPED = V0.1 静态表(UB/UC/IB/IC/IN)
*/
uint32_t adc_cal_flags(adc_semantic_t sem);
/*
* 函数功能:读通道当前生效系数(µV/码,浮点)
* 入口参数:sem - 语义通道 adc_semantic_t
* 返 回 值:k[µV/码](回退后口径);TEMP / 越界返 0 float
* 限定条件:无
* 函数说明:L5-2 门限换算、日志与整定界面回显用;勿据其反推二次值(看 flags)
*/
float adc_cal_k_uv(adc_semantic_t sem);
/*
* 函数功能:整定入口——写通道系数与零点,并置位 VALID(「数据可信」卡 4)
* 入口参数:sem - 语义通道(电气 8 路) adc_semantic_t
* k_q16 - 系数,µV/码 × 65536 内 int32_t
* b_codes - 零点偏移,码值 内 int32_t
* 返 回 值:0 - 成功;-1 - 参数非法/越出合法带(RAM 不动,[ERR] 留痕) int
* 限定条件:**属主上下文**(defaultTask 的标定流程/整定链),勿在 adcTask 调用
* 函数说明:只写 RAM 副本;落盘由属主经 cfg_file_save 完成(卡 10 前整定链
* 未接,台架两点标定(卡 12)是首个消费者)
*/
int adc_cal_set(adc_semantic_t sem, int32_t k_q16, int32_t b_codes);
/*
* 函数功能:两点标定公式(「数据可信」卡 12 的算术核,纯函数无状态)
* 入口参数:c_lo/c_hi - 两点码值(c_hi > c_lo) int32_t
* v_lo_uv/v_hi_uv - 两点实测电压(µV,v_hi > v_lo,端子/引脚域均可) int32_t
* p_k_q16 / p_b - 输出:系数(Q16)与零点偏移(码值) int32_t *
* 返 回 值:0 - 成功;-1 - 参数/斜率非法;-2 - 零点越 ±ADC_CAL_B_MAX(先查前端,
* 不得用系数凑错斜率,「数据可信」§8 风险表) int
* 限定条件:无(纯整数运算,主机侧可直接单测)
* 函数说明:k = (v_hi−v_lo)×65536/(c_hi−c_lo)(四舍五入),b = v_lo×65536/k − c_lo;
* 实测注入未做(台架待跑),本函数即「公式齐备」的交付物
*/
int adc_cal_two_point(int32_t c_lo, int32_t c_hi, int32_t v_lo_uv, int32_t v_hi_uv,
int32_t *p_k_q16, int32_t *p_b);
/*
* 函数功能:通道码值域「贴轨带」门限(L5-2 越界检出的门限来源,按 FS 百分比)
* 入口参数:sem - 语义通道 adc_semantic_t
* p_lo_th - 下轨带上界(code ≤ 此值判触底) int32_t *
* p_hi_th - 上轨带下界(code ≥ 此值判触顶) int32_t *
* 返 回 值:0 - 已给出门限;-1 - TEMP/越界(不做贴轨监视) int
* 限定条件:无
* 函数说明:TPAFE 双极性 ±98% FS(±32112);STM32 上轨 98% FS(4013)、
* 下轨 2% FS(81);判定状态机在 adc_health_rail_watch()(L5-2)
*/
int adc_cal_rail_ths(adc_semantic_t sem, int32_t *p_lo_th, int32_t *p_hi_th);
#ifdef __cplusplus
}
#endif
#endif /* ADC_CAL_H */
+89 -1
View File
@@ -1,7 +1,8 @@
/*
* 模块名称:ADC 采样有效性校验(L1 采集与预处理,规划项 L1-6)
* 模块功能:见 adc_health.h 文件头。本文件为**纯逻辑实现**:输入计数快照、
* 输出故障位/事件位,零平台依赖(仅 stdint.h)→ 主机侧免桩单测。
* 输出故障位/事件位,零平台依赖(仅 stdint.h/string.h 标准 C)→
* 主机侧免桩单测。
* 适用平台:STM32F407ZGTx + TPAFE5160
* 作者:王建锋
* 创建日期:2026-10-11
@@ -10,10 +11,15 @@
* v1.1 2026-10-21 王建锋 新增 adc_health_age_stale():无模块状态的帧年龄
* 纯判据(参照系由调用方自存),供 protectTask 的
* 兜底新鲜度门使用,见 adc_health.h 同名声明
* v1.2 2026-10-25 DTU 项目组 新增 L5-2 残余:码值越界(贴轨)观察器
* (rail_watch/rail_sticky/rail_init),判据与释放
* 沿理由见 adc_health.h L5-2 段注释
*/
#include "adc_health.h"
#include <string.h> /* memset(L5-2 贴轨观察器数组复位) */
/* ===================== 内部状态 ===================== */
/* 基线是否已建立:首次 check() 只记基线、不判定(开机首拍无参照系) */
@@ -36,6 +42,17 @@ static uint32_t s_flags; /* 电平位(stall 可自愈;backward 锁存在
static uint32_t s_sticky; /* 锁存位(含电平位历史并集) */
static uint32_t s_tpafe_gaps;
/* L5-2 贴轨观察器(每通道独立小状态机,单写者 = adcTask) */
typedef struct {
uint16_t run_hi; /* 连续贴上轨计数(饱和 0xFFFF) */
uint16_t run_lo; /* 连续贴下轨计数 */
uint8_t seen_mid; /* 见过中带样本(释放沿)——锁存的必要前提 */
uint8_t latched; /* 已报过边沿(不重复报) */
} adc_rail_t;
static adc_rail_t s_rail[ADC_RAIL_CH_NUM];
static uint32_t s_rail_sticky;
/* ===================== 对外接口 ===================== */
/*
@@ -46,6 +63,7 @@ static uint32_t s_tpafe_gaps;
* 函数说明:清全部内部状态;判定从下一次 check() 的首拍基线重新开始。
*/
void adc_health_init(void) {
adc_health_rail_init();
s_primed = 0u;
s_tpafe_seq = 0u;
s_tpafe_frst = 0u;
@@ -238,3 +256,73 @@ uint32_t adc_health_sticky(void) {
uint32_t adc_health_tpafe_gaps(void) {
return s_tpafe_gaps;
}
/* ===================== L5-2 贴轨观察器实现 ===================== */
/*
* 函数功能:贴轨观察器复位(幂等;adc_health_init() 已内含)
* 入口参数:无
* 返 回 值:无
* 限定条件:与 check() 同为单写者任务上下文
*/
void adc_health_rail_init(void) {
(void)memset(s_rail, 0, sizeof(s_rail));
s_rail_sticky = 0u;
}
/*
* 函数功能:单通道单拍贴轨观察(判据与释放沿前提见 adc_health.h 同名声明)
* 入口参数:ch / code / lo_th / hi_th - 见 adc_health.h
* 返 回 值:1 - 本拍新锁存(边沿);0 - 无新事件(含 ch 越界忽略) uint32_t
* 限定条件:每拍每通道至多一次;纯算术、零平台依赖
*/
uint32_t adc_health_rail_watch(uint8_t ch, int32_t code, int32_t lo_th, int32_t hi_th) {
adc_rail_t *p;
if (ch >= (uint8_t)ADC_RAIL_CH_NUM) {
return 0u;
}
p = &s_rail[ch];
if (code >= hi_th) { /* 贴上轨带:清对侧计数 */
p->run_lo = 0u;
if (p->run_hi < 0xFFFFu) {
p->run_hi++;
}
if ((p->run_hi >= (uint16_t)ADC_RAIL_N_RUN) && (p->seen_mid != 0u)
&& (p->latched == 0u)) {
p->latched = 1u;
s_rail_sticky |= (1uL << ch);
return 1u;
}
return 0u;
}
if (code <= lo_th) { /* 贴下轨带(对称) */
p->run_hi = 0u;
if (p->run_lo < 0xFFFFu) {
p->run_lo++;
}
if ((p->run_lo >= (uint16_t)ADC_RAIL_N_RUN) && (p->seen_mid != 0u)
&& (p->latched == 0u)) {
p->latched = 1u;
s_rail_sticky |= (1uL << ch);
return 1u;
}
return 0u;
}
/* 中带:记释放沿(锁存的必要前提),并清两侧连续计数 */
p->seen_mid = 1u;
p->run_hi = 0u;
p->run_lo = 0u;
return 0u;
}
/*
* 函数功能:读贴轨锁存位图
* 入口参数:无
* 返 回 值:bit ch = 曾检出贴轨(低 9 位有效) uint32_t
* 限定条件:无
*/
uint32_t adc_health_rail_sticky(void) {
return s_rail_sticky;
}
+51 -1
View File
@@ -20,7 +20,13 @@
* check() 时才可能置起,节拍源停摆会把本任务卡在
* osThreadFlagsWait 上 → 那些位永远是 0,而相量冻在
* 最后一帧仍被 protectTask 用于判据。该纯判据让保护侧
* 能用**自己的时基 + 中断侧计数器**独立判"数据还新不新"
* 能用**自己的时基 + 中断侧计数器**独立判"数据还新不新" * v1.2 2026-10-25 DTU 项目组 新增 L5-2 残余「码值越界(贴轨)检出」:
* adc_health_rail_watch()/rail_sticky()/rail_init()
* ——连续 N 拍贴满量程带且该通道见过释放沿才锁存
* (空载恒 0 / 开机恒 0 是合法工况,不误报);
* 判据动因 docs/问题记录/采集与数据处理短板评测.md
* §6.2,门限来源 adc_cal_rail_ths()(按 FS 百分比)
*/
#ifndef ADC_HEALTH_H
@@ -159,6 +165,50 @@ uint32_t adc_health_sticky(void);
*/
uint32_t adc_health_tpafe_gaps(void);
/* ===================== L5-2 残余:码值越界(贴轨)检出,2026-10-25 ===================== */
/* 贴轨锁存的连续拍数:20 拍 = 1 kHz 节拍下一个 50 Hz 周波。削顶/触轨波形的
* 贴带段持续 ≥ 半周波;噪声毛刺不可能连续 20 拍钉在同侧轨带内(短板评测 §6.2
* 判据①「连续 N 点贴满量程」的定标)。 */
#define ADC_RAIL_N_RUN 20u
/* 贴轨监视的通道数上限(9 = 8 电气量 + 温度;温度是否监视由调用方经
* adc_cal_rail_ths() 返回 -1 决定,本层不内嵌语义知识) */
#define ADC_RAIL_CH_NUM 9u
/*
* 函数功能:贴轨观察器复位(adc_health_init() 已内含,单独暴露供编排层复用)
* 入口参数:无
* 返 回 值:无
* 限定条件:与 check() 同——仅任务上下文(单写者)
*/
void adc_health_rail_init(void);
/*
* 函数功能:单通道单拍贴轨观察(L5-2 码值越界检出,纯算术)
* 入口参数:ch - 通道号(0..ADC_RAIL_CH_NUM-1,越界忽略) uint8_t
* code - 本拍原始码值(**码值域**,与标定无关) int32_t
* lo_th - 下轨带上界(code ≤ 此值算贴底) int32_t
* hi_th - 上轨带下界(code ≥ 此值算贴顶) int32_t
* 返 回 值:1 - 本拍**新锁存**该通道(边沿,供编排层落日志);0 - 无新事件 uint32_t
* 限定条件:每拍每通道至多一次(与采样帧同节奏,单写者)
* 函数说明:锁存条件 = 连续 ADC_RAIL_N_RUN 拍贴**同一侧**轨带,且该通道此前
* 出现过中带样本(「释放沿」)。释放沿要求的理由:空载信号恒 0、
* 开机未采到帧恒 0 都是**合法工况**(短板评测 §6.2:空载与零值都是
* 合法工况)——只有「动过又钉死在轨上」才是削顶/越界签名。锁存后
* 不再重复报边沿,直到 rail_init/health_init 重置。
*/
uint32_t adc_health_rail_watch(uint8_t ch, int32_t code, int32_t lo_th, int32_t hi_th);
/*
* 函数功能:读贴轨锁存位图
* 入口参数:无
* 返 回 值:bit ch = 该通道曾检出贴轨(1 kHz × 9 路 → 低 9 位有效) uint32_t
* 限定条件:无
* 函数说明:供心跳日志 / 后续品质位映射(卡 8/9 的 qds 消费者)只读。
*/
uint32_t adc_health_rail_sticky(void);
#ifdef __cplusplus
}
#endif
+11
View File
@@ -23,6 +23,7 @@
#include "gd5f2gq5ue.h"
#include "tpafe5160.h"
#include "adc_input.h" /* L1 采集层:adc_input_init()(语义映射 + STM32 ADC1 扫描) */
#include "adc_cal.h" /* L1.5 标定层:adc_cal_init()(ROM 系数 + cfg 两段注册,道A 卡2/4) */
#include "phasor.h" /* L2 相量层:phasor_init()(系数表 + 实例 + 双区交换) */
#include "freq.h" /* L2 派生量:freq_init()(相位解缠绕测频,B8) */
#include "prot_set.h" /* L3 定值模型:prot_set_init()(B5,内核前封存) */
@@ -250,6 +251,16 @@ int app_main_init(void) {
(void)freq_init();
DBG_INFO("freq layer init: OK");
/* 4c-3) 通道标定层(L1.5,「数据可信」卡 2/卡 4)初始化:RAM 系数副本置
* ROM 默认(卡 1 定死的引脚侧 LSB,docs/硬件平台/ADC通道映射说明.md §7.1)
* + 向配置引擎注册 [adc_cal] / [adc_cal_ofs] 两段。注册必须先于
* defaultTask 的 cfg_file_load(「先注册后 load」契约)→ 只能落内核前;
* 折算消费在 adcTask 每拍(adc_cal_apply),标定文件值随 load/poll 原地
* 生效,无回调。 */
adc_cal_init();
DBG_INFO("adc cal init: OK (k_q16 tpafe=%ld stm32=%ld)",
(long)ADC_CAL_K_Q16_TPAFE, (long)ADC_CAL_K_Q16_STM32);
/* 4d) 保护定值 + 元件层初始化(B5):出厂默认定值封存校验和 + 元件状态
* 复位并解析相量索引(须在 phasor_init 之后)。内核前完成 → iec104
* 定值读/protectTask 首拍前定值必已装载。判据消费在 protectTask。 */
+99 -10
View File
@@ -84,6 +84,14 @@
* 者;该入口同时不再打日志——它落在本任务静态最坏链上,
* 每条 DBG_* 约 216 B 栈,而这一行毫无诊断价值(本任务
* 存活由首帧日志与 TASK_WD_ADC 心跳证明)
* v3.1 2026-10-25 DTU 项目组 L1.5 折算 + L5-2 贴轨观测(「数据可信」卡 2,道A):
* frame[s] 由 (float)code 改 adc_cal_apply()——工程量 =
* (code+b)×k,现口径 = ADC 引脚电压 V(eng=0,卡 1 冻结的
* ROM 系数,docs/硬件平台/ADC通道映射说明.md §7.1);同拍
* 经 adc_health_rail_watch() 逐通道贴轨观察(L5-2 残余,
* 门限 adc_cal_rail_ths,码值域与标定正交),锁存边沿 →
* DBG_ERROR + 每开机一次 SYSLOG;心跳增 cal: 行(mV 定点
* + valid/clipped 位图 + rail 锁存),台架注入口径见该行判读
*/
#include "adc_task.h"
@@ -93,7 +101,8 @@
#include "sample_ring.h" /* L2 输入:sample_ring_push_frame()(采样环缓冲) */
#include "phasor.h" /* L2 相量层:phasor_run()/phasor_get()(全周傅氏) */
#include "freq.h" /* L2 派生量:freq_run()/freq_get()(测频,B8) */
#include "adc_health.h" /* L1-6:采样有效性校验(静默/丢帧/单调性/错误增量) */
#include "adc_health.h" /* L1-6:采样有效性校验(静默/丢帧/单调性/错误增量/贴轨) */
#include "adc_cal.h" /* L1.5:通道标定折算(「数据可信」卡 2;flags 供 cal 心跳行) */
#include "tpafe5160.h"
#include "task_watchdog.h"
#include "console_log.h"
@@ -122,6 +131,12 @@ static uint32_t s_frames_skip;
static uint32_t s_health_prev;
static uint32_t s_health_logged;
/* L5-2 贴轨编排状态(锁存本体在 adc_health 内,这里只留编排层视角):
* s_rail_edge_latched - 本开机以来出现过锁存边沿的通道位图(心跳 cal: 行展示)
* s_rail_syslogged - SYSLOG 留痕只写一次的闸门(init 重置) */
static uint32_t s_rail_edge_latched;
static uint8_t s_rail_syslogged;
/* 消费者就绪门:由 adcTask 自己在进入等待前置 1;单写者 = 本任务,ISR 只读。
* ⚠ 为什么必须有这道门:TIM3 在 app_main_init()(Src/main.c:140)里就
@@ -159,7 +174,9 @@ void adc_tick_irq_notify(void) {
* 限定条件:仅任务上下文;调用方须保证 dbg_console_writer_ready() 为真
* 函数说明:MicroLIB 的 snprintf **不支持 %f** → 以 ×0.1 定点整数打印
* (打印值 = 实际值 × 10,分辨率 0.1)。逐实例数据驱动输出,
* 实例表加行即自动覆盖。⚠ L1-5 标定未实现 → 读数单位是**原始码值**。
* 实例表加行即自动覆盖。输入自 v3.1 起为 L1.5 折算后的工程量
* (现口径 = ADC 引脚电压 V,见 cal: 心跳行与文件头 v3.1 记录);
* 二次值语义仅在 adc_cal_flags() 出 VALID 后成立。
*/
static void log_phasors(const char *tag) {
uint8_t k;
@@ -246,7 +263,9 @@ int adc_task_init(void) {
s_frames_consumed = 0u;
s_frames_idle = 0u;
s_frames_skip = 0u;
adc_health_init();
s_rail_edge_latched = 0u;
s_rail_syslogged = 0u;
adc_health_init(); /* 内含 L5-2 贴轨观察器复位(rail_init) */
return 0;
}
@@ -319,20 +338,35 @@ void adc_task_func(void *arg) {
* 任一通道读不到(尚未首次成功)→ 本拍**整帧跳过**:不向窗内注入 0 点
* (单个 0 点会显著污染 20 点窗),并计入 skip 观测。 */
if (r == 1) {
float frame[ADC_SEM_COUNT];
uint8_t s;
uint8_t all_ok = 1u;
float frame[ADC_SEM_COUNT];
uint8_t s;
uint8_t all_ok = 1u;
uint32_t rail_edge = 0u;
for (s = 0u; s < (uint8_t)ADC_SEM_COUNT; s++) {
int32_t code = 0;
int32_t code = 0;
int32_t lo_th = 0;
int32_t hi_th = 0;
if (adc_input_raw((adc_semantic_t)s, &code) != 0) {
all_ok = 0u;
break;
}
/* ⚠ L1-5 通道标定尚未实现(缺标准源)→ 暂以**原始码值**充作工程量
* (系数 = 1)。标定落地后此处改为 工程量 = (code + b) × k(定点 2^10)。 */
frame[s] = (float)code;
/* L1.5 折算(「数据可信」卡 2,§4.1 模型):工程量 = (code + b) × k。
* 现口径 = **ADC 引脚电压 V**(eng=0,卡 1 冻结的 ROM 系数,见
* docs/硬件平台/ADC通道映射说明.md §7.1);卡 0b/卡 12 写入标定
* 系数(adc_cal_flags() 出 VALID)后才按二次值消费。TEMP 透传码值
* 不折算(决策 5)。*/
frame[s] = adc_cal_apply((adc_semantic_t)s, code);
/* L5-2 码值越界(贴轨)观察:在**码值域**判定,与标定正交——
* 连续一周波贴轨带且此前见过释放沿才锁存(空载恒 0 不误报)。
* TEMP 无「轨」语义,adc_cal_rail_ths() 返 -1 自然跳过。*/
if (adc_cal_rail_ths((adc_semantic_t)s, &lo_th, &hi_th) == 0) {
if (adc_health_rail_watch(s, code, lo_th, hi_th) != 0u) {
rail_edge |= (1uL << s);
}
}
}
if (all_ok != 0u) {
@@ -344,6 +378,22 @@ void adc_task_func(void *arg) {
} else {
s_frames_skip++;
}
/* 贴轨锁存边沿留痕:帧循环后统一处理(skip 拍也已在循环内逐通道
* 观察过,不因取数半途失败而漏报已观察的通道)。SYSLOG 每开机只
* 留痕一次——贴轨是持续工况,逐拍写会把 selftest.log 刷满 */
if (rail_edge != 0u) {
s_rail_edge_latched |= rail_edge;
if (dbg_console_writer_ready() != 0u) {
DBG_ERROR("rail: edge=0x%lx sticky=0x%lx (code-domain, 20-run+release)",
(unsigned long)rail_edge,
(unsigned long)adc_health_rail_sticky());
}
if (s_rail_syslogged == 0u) {
s_rail_syslogged = 1u;
(void)syslog_post(SYSLOG_SELFTEST, "adc code rail fault");
}
}
}
/* 2b) L1-6 采样有效性校验(每拍,常数次整数运算):把驱动侧观测计数交给
@@ -436,6 +486,45 @@ void adc_task_func(void *arg) {
(unsigned long)adc_health_tpafe_gaps());
log_phasors("hb");
/* L1.5/L5-2 台架观测面(×1000 mV 定点,MicroLIB 无 %f):
* ua/un/ia_mv = 三路 TPAFE 引脚电压毫伏(未标定域,**不是**
* 二次值;卡 12 注入验证的读数落点,偏差判据 ≤2 %)
* vl = VALID 位图(默认构建应恒 0x00——未标定即未标定,
* 卡 5 的闭锁判据在 protect 侧同源消费这个位)
* cl = CLIPPED 位图(V0.1 应恒 0xE6 = UB/UC/IB/IC/IN)
* rail/edge = 贴轨锁存/历史边沿位图(注入饱和电平后应起)*/
{
int32_t c = 0;
long ua_mv = 0L, un_mv = 0L, ia_mv = 0L;
uint32_t vl = 0u, cl = 0u;
uint8_t i;
if (adc_input_raw(ADC_SEM_UA, &c) == 0) {
ua_mv = (long)(adc_cal_apply(ADC_SEM_UA, c) * 1000.0f);
}
if (adc_input_raw(ADC_SEM_UN, &c) == 0) {
un_mv = (long)(adc_cal_apply(ADC_SEM_UN, c) * 1000.0f);
}
if (adc_input_raw(ADC_SEM_IA, &c) == 0) {
ia_mv = (long)(adc_cal_apply(ADC_SEM_IA, c) * 1000.0f);
}
for (i = 0u; i < 8u; i++) {
const uint32_t f = adc_cal_flags((adc_semantic_t)i);
if ((f & ADC_CAL_F_VALID) != 0u) {
vl |= (1uL << i);
}
if ((f & ADC_CAL_F_CLIPPED) != 0u) {
cl |= (1uL << i);
}
}
DBG_INFO("cal: ua_mv=%ld un_mv=%ld ia_mv=%ld vl=0x%02lx cl=0x%02lx rail=0x%03lx edge=0x%03lx",
ua_mv, un_mv, ia_mv,
(unsigned long)vl, (unsigned long)cl,
(unsigned long)adc_health_rail_sticky(),
(unsigned long)s_rail_edge_latched);
}
}
report_at = now + ADC_TASK_REPORT_MS;
}
+5
View File
@@ -1007,6 +1007,11 @@
<FileType>1</FileType>
<FilePath>..\App\algo\adc_health.c</FilePath>
</File>
<File>
<FileName>adc_cal.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\algo\adc_cal.c</FilePath>
</File>
<File>
<FileName>prot_set.c</FileName>
<FileType>1</FileType>
+1
View File
@@ -94,6 +94,7 @@ SIM_EXEMPT = [
("App/log/soe_rec.c", "SOE 记录编解码纯逻辑(B9),L0.5 主机单测覆盖,模拟器无消费路径"),
("App/log/soe_store.c", "SOE 落盘 MCU 胶水(队列+FatFs 单写者,B9),模拟器无消费路径"),
("App/algo/sample_ring.c", "L2 环形缓冲纯算法,模拟器无消费路径"),
("App/algo/adc_cal.c", "L1.5 标定折算纯算法 + cfg 段注册(消费者 = MCU 侧 adcTask 每拍;模拟器不跑采样任务),模拟器无消费路径"),
("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"),
("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"),
("App/log/console_log.c", "MCU 控制台输出,模拟器无消费路径"),
+22
View File
@@ -1686,6 +1686,28 @@
"owner_note": "B 层口径(用户裁定 2026-10-21):这些号实测在代码目录外零消费者(唯一外部引用是 id_aliases.json 自己),故属主登记处 = 单测代码本身,不逐号补文档;class 仍为 TC → 跨模块同号由校验 4 抓",
"cursor_note": "游标按实测最大号收录(2026-10-21):越界即视为未登记的冒号牌"
},
{
"name": "TC-HOST-ADCCAL",
"class": "TC",
"title": "主机侧单测用例号:L1.5 通道标定折算(test/host/adc_cal,道A 卡2/4)",
"pattern": "\\bTC-ADCCAL-[0-9]{1,4}[a-z]?\\b",
"digit_width": [
2,
4
],
"owner_docs_glob": [
"test/host/adc_cal/*"
],
"scope": "global",
"frozen": false,
"cursor": "TC-ADCCAL-017",
"cursor_owner_only": true,
"check_dangling": true,
"allow_orphans": true,
"subitem_ok": true,
"owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCHEALTH 先例;2026-10-25 道A 卡2 建族):属主登记处 = 单测代码目录,不逐号补文档",
"cursor_note": "游标按建族时实测最大号收录(2026-10-25):越界即视为未登记的冒号牌"
},
{
"name": "TC-HOST-IEC104",
"class": "TC",
+510
View File
@@ -0,0 +1,510 @@
/*
* 模块名称:通道标定层(L1.5,adc_cal)主机侧单元测试
* 被测对象:真实源文件 App/algo/adc_cal.c + App/cfg/cfg_file.c(均 #include
* 原样纳入——卡 4 的落盘契约必须用真引擎测,不造轮子替身)。
* 平台依赖桩:dbg_log.h(stub/ 计数宏)、cfg_store_*(本文件内存
* 虚拟盘,同 cfg_file 套件先例)、syslog_post(计数)、
* adc_input_desc()(按 ADC通道映射说明.md 权威表给 V0.1 来源映射)。
*
* 为什么必须主机侧(数据可信 §2.6):板端无标准源,「读数 = 系数 × 码值」
* 这一根本判据在台架上不可验;Q16 舍入、系数翻倍口径、负零点落盘、合法带
* 哨兵漏放,只有离线端能用独立推导的期望值 + 坏文件注入一次性钉死。
*
* 覆盖点(TC-ADCCAL-xxx;属主登记处 = 本目录,族 TC-HOST-ADCCAL):
* TC-ADCCAL-001 init 注册两段:名称/键数/data_size/无回调/默认表指向
* TC-ADCCAL-002 UA(TPAFE)折算:0 / 8192 / 32767 / -32768 对 10V/65536 独立推导
* TC-ADCCAL-003 UB(STM32)折算:0 / 2048 / 4095 对 3.3V/4096
* TC-ADCCAL-004 其余 6 路各 1 点(合计 8 通道 × ≥3 码值点,卡 2 DoD)
* TC-ADCCAL-005 TEMP 透传 + flags/k_uv 恒 0;sem 越界守卫
* TC-ADCCAL-006 默认 flags 三态:VALID 全 0;CLIPPED = UB/UC/IB/IC/IN(0xE6)
* TC-ADCCAL-007 k_uv 精确值:152.587890625 / 805.6640625(Q16 无舍入验证)
* TC-ADCCAL-008 set():合法带边界含端点 / 越界拒绝 RAM 不动 / TEMP 拒绝
* TC-ADCCAL-009 合法带哨兵回退:坏 k/坏 b → ROM 值 + 每通道一次 [ERR] +
* flags 不报 VALID(即便位图置位)+ init 重置观测窗
* TC-ADCCAL-014 零点 b 生效(set 路径):(code+b)×k 逐项可算
* TC-ADCCAL-015 两点标定公式:合成数据还原 k/b + 写回往返 + 参数/斜率/
* 零点越界(-2)守卫(卡 12 的算术核,台架注入待跑)
* TC-ADCCAL-016 rail_ths 门限:TPAFE ±32112 / STM32 4013·81 / TEMP 不监视
* TC-ADCCAL-017 init 幂等:重复调用 RAM 复位默认 + 重新注册
* —— 以下走真实 cfg_file 引擎(卡 4 DoD)——
* TC-ADCCAL-010 文件值生效:k_ua/b_ua 经 load 改变折算结果,VALID 置位
* TC-ADCCAL-011 越界文件值:k=999 / k=-999 被引擎收下后由哨兵兜住:
* 折算 = ROM、VALID 不报(位图置位也不报)、[ERR] 留痕
* TC-ADCCAL-012 段损坏(超长文件)→ 回退 ROM 且 flags 无 VALID(卡 4 DoD)
* TC-ADCCAL-013 段缺失(文件无本段)→ ROM 默认、VALID 全 0
*
* 通过:全部 TC PASS(main 返回 0)。
* 创建日期:2026-10-25(道A 卡 2/卡 4)
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/* 先纳入桩头(DBG_* → 计数),再纳入生产源码 */
#include "dbg_log.h"
/* ===================== 内存虚拟文件系统(cfg_store_* 强符号) ===================== */
/* 与 cfg_file 套件同款的最小实现;生产代码经 cfg_file_store.h 做全部文件/堆操作 */
#include "cfg_file_store.h"
#define VFS_CAP 8192u
static uint8_t s_vfs[VFS_CAP];
static size_t s_vfs_len = 0u;
static uint32_t s_vfs_mtime = 100u;
static int s_vfs_exists = 0;
void *cfg_store_alloc(size_t size) { return malloc(size); }
void cfg_store_free(void *p_buf) { free(p_buf); }
uint32_t cfg_store_heap_free(void) { return 0u; }
int cfg_store_read(const char *p_path, uint8_t *p_buf, size_t cap, size_t *p_nread) {
size_t n;
(void)p_path;
if (!s_vfs_exists) {
return -1;
}
n = s_vfs_len;
if (n > cap) {
n = cap;
}
(void)memcpy(p_buf, s_vfs, n);
*p_nread = n;
return 0;
}
int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len) {
(void)p_path;
if (len > VFS_CAP) {
return -1;
}
(void)memcpy(s_vfs, p_buf, len);
s_vfs_len = len;
s_vfs_exists = 1;
s_vfs_mtime++;
return 0;
}
int cfg_store_stat(const char *p_path, cfg_file_stat_t *p_st) {
(void)p_path;
if (!s_vfs_exists) {
return -1;
}
p_st->size = (uint32_t)s_vfs_len;
p_st->mtime = s_vfs_mtime;
return 0;
}
int cfg_store_remove(const char *p_path) {
(void)p_path;
s_vfs_exists = 0;
s_vfs_len = 0u;
return 0;
}
const char *cfg_store_config_path(void) { return "/config.ini"; }
const char *cfg_store_tmp_path(void) { return "/config.tmp"; }
/* ---- sys_log 桩:只计数自检留痕 ---- */
#include "sys_log.h"
static int s_syslog_count = 0;
void syslog_post(syslog_id_t id, const char *desc) {
(void)id;
(void)desc;
s_syslog_count++;
}
/* ---- adc_input_desc 桩:V0.1 权威映射(docs/硬件平台/ADC通道映射说明.md §3/§5) ---- */
#include "adc_input.h"
static const adc_channel_desc_t s_desc_stub[ADC_SEM_COUNT] = {
{ ADC_SRC_TPAFE, 6u }, /* UA = buf[6] */
{ ADC_SRC_STM32, 14u }, /* UB = CH14(PC4) */
{ ADC_SRC_STM32, 3u }, /* UC = CH3 */
{ ADC_SRC_TPAFE, 5u }, /* UN = buf[5] */
{ ADC_SRC_TPAFE, 2u }, /* IA = buf[2] */
{ ADC_SRC_STM32, 6u }, /* IB = CH6 */
{ ADC_SRC_STM32, 1u }, /* IC = CH1 */
{ ADC_SRC_STM32, 2u }, /* IN = CH2 */
{ ADC_SRC_STM32, 5u }, /* TEMP = CH5 */
};
const adc_channel_desc_t *adc_input_desc(adc_semantic_t ch) {
if ((int)ch < 0 || (unsigned)ch >= (unsigned)ADC_SEM_COUNT) {
return NULL;
}
return &s_desc_stub[(unsigned)ch];
}
/* ---- dbg_log 桩观测计数定义(stub/dbg_log.h 声明为 extern) ---- */
int g_caltest_err_count = 0;
int g_caltest_info_count = 0;
int g_caltest_debug_count = 0;
/* ===================== 测试框架(极简,同 cfg_file 套件) ===================== */
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)
/* ===================== 被测代码 ===================== */
/* 真实引擎 + 真实标定层(顺序:cfg_file.c 先,其 DBG_TAG 宏被 adc_cal.c 重定义
* 仅产生主机侧 C4005 告警——MCU 端两者本就在不同 TU,无此现象) */
#include "cfg_file.c"
#include "adc_cal.c"
/* ===================== 独立推导的期望值 ===================== */
/* 期望一律用 **double 直接除**(10/65536、3.3/4096),不引用被测的 Q16 常量,
* 满足卡 2 DoD「期望值由 LSB 常数独立算得(不是抄实现)」;容差只需盖住
* float32 中间积舍入(相对 ~1e-7)与打印粒度。 */
static double exp_tpafe(int32_t code) { return (double)code * (10.0 / 65536.0); }
static double exp_stm32(int32_t code) { return (double)code * (3.3 / 4096.0); }
static double exp_q16(int32_t code, int32_t b, int32_t k_q16) {
return ((double)code + (double)b) * ((double)k_q16 / 65536.0) / 1.0e6;
}
static int near_d(double got, double want) {
double d = got - want;
if (d < 0.0) { d = -d; }
return (d <= 1.0e-4 + (want < 0.0 ? -want : want) * 1.0e-5) ? 1 : 0;
}
static long my_lround(double x) { return (long)((x >= 0.0) ? (x + 0.5) : (x - 0.5)); }
/* ===================== 辅助 ===================== */
/* 复位「引擎注册表 + 虚拟盘 + 全部观测计数」= 刚上电(白盒代价同 cfg_file 套件) */
static void reset_all(void) {
memset(s_vfs, 0, sizeof(s_vfs));
s_vfs_len = 0u;
s_vfs_mtime = 100u;
s_vfs_exists = 0;
memset((void *)s_sections, 0, sizeof(s_sections));
memset(&s_base, 0, sizeof(s_base));
memset(&s_last, 0, sizeof(s_last));
s_stable = 0u;
s_base_valid = 0u;
g_caltest_err_count = 0;
g_caltest_info_count = 0;
g_caltest_debug_count = 0;
s_syslog_count = 0;
}
static void vfs_put(const char *text) {
size_t n = strlen(text);
if (n > VFS_CAP) { n = VFS_CAP; }
(void)memcpy(s_vfs, text, n);
s_vfs_len = n;
s_vfs_exists = 1;
s_vfs_mtime++;
}
static const char *vfs_get(void) { return (const char *)s_vfs; }
/* ===================== 用例(白盒组) ===================== */
/* TC-ADCCAL-001:init 注册两段(名称/键数/data_size/无回调) */
static void tc_001_init_registers(void) {
printf("[TC-ADCCAL-001] init registers [adc_cal]/[adc_cal_ofs]\n");
reset_all();
adc_cal_init();
CHECK(s_sections[0].used == 1u, "section slot0 used");
CHECK(strcmp(s_sections[0].name, "adc_cal") == 0, "sec0 name = adc_cal");
CHECK(s_sections[0].key_count == 9u, "sec0 keys = 8k + valid");
CHECK(s_sections[0].data_size == 33u, "sec0 data_size = 33 (covers valid byte)");
CHECK(s_sections[0].p_cb == NULL, "sec0 no callback (RAM copy = live values)");
CHECK(s_sections[1].used == 1u && strcmp(s_sections[1].name, "adc_cal_ofs") == 0,
"sec1 name = adc_cal_ofs");
CHECK(s_sections[1].key_count == 8u, "sec1 keys = 8b");
CHECK(s_sections[1].data_size == 32u, "sec1 data_size = 32");
CHECK(s_sections[2].used == 0u, "only two sections registered");
}
/* TC-ADCCAL-002/003/004:8 通道 × ≥3 码值点折算,期望独立推导(卡 2 DoD) */
static void tc_002_004_apply(void) {
printf("[TC-ADCCAL-002] UA (TPAFE) apply\n");
reset_all();
adc_cal_init();
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 0), exp_tpafe(0)), "UA code 0 = 0V");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 8192), exp_tpafe(8192)), "UA 8192 = 1.25V");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 32767), exp_tpafe(32767)), "UA full scale minus 1 (bipolar top code)");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, -32768), exp_tpafe(-32768)), "UA -FS (bipolar)");
printf("[TC-ADCCAL-003] UB (STM32) apply\n");
CHECK(near_d(adc_cal_apply(ADC_SEM_UB, 0), exp_stm32(0)), "UB code 0 = 0V");
CHECK(near_d(adc_cal_apply(ADC_SEM_UB, 2048), exp_stm32(2048)), "UB mid = 1.65V");
CHECK(near_d(adc_cal_apply(ADC_SEM_UB, 4095), exp_stm32(4095)), "UB +FS 3.2992V");
printf("[TC-ADCCAL-004] other six channels one mid point each\n");
CHECK(near_d(adc_cal_apply(ADC_SEM_UC, 123), exp_stm32(123)), "UC 123");
CHECK(near_d(adc_cal_apply(ADC_SEM_UN, -10000), exp_tpafe(-10000)), "UN -10000");
CHECK(near_d(adc_cal_apply(ADC_SEM_IA, 11452), exp_tpafe(11452)), "IA 11452 (bench first-frame code)");
CHECK(near_d(adc_cal_apply(ADC_SEM_IB, 4000), exp_stm32(4000)), "IB 4000");
CHECK(near_d(adc_cal_apply(ADC_SEM_IC, 1), exp_stm32(1)), "IC 1");
CHECK(near_d(adc_cal_apply(ADC_SEM_IN, 4094), exp_stm32(4094)), "IN 4094");
}
/* TC-ADCCAL-005:TEMP 透传与守卫 */
static void tc_005_temp(void) {
printf("[TC-ADCCAL-005] TEMP passthrough + guards\n");
reset_all();
adc_cal_init();
CHECK(adc_cal_apply(ADC_SEM_TEMP, 3000) == 3000.0f, "TEMP raw passthrough");
CHECK(adc_cal_flags(ADC_SEM_TEMP) == 0u, "TEMP flags = 0 (both)");
CHECK(adc_cal_k_uv(ADC_SEM_TEMP) == 0.0f, "TEMP k_uv = 0");
CHECK(adc_cal_flags((adc_semantic_t)ADC_SEM_COUNT) == 0u, "flags sem OOB = 0");
CHECK(adc_cal_set((adc_semantic_t)ADC_SEM_COUNT, 10000000L, 0L) == -1, "set sem OOB rejected");
CHECK(adc_cal_apply(ADC_SEM_UA, 0) == 0.0f, "UA still applies after guards");
}
/* TC-ADCCAL-006:默认三态位图 */
static void tc_006_default_flags(void) {
uint32_t vl = 0u, cl = 0u;
uint8_t i;
printf("[TC-ADCCAL-006] default flags: no VALID, CLIPPED=0xE6\n");
reset_all();
adc_cal_init();
for (i = 0u; i < 8u; i++) {
const uint32_t f = adc_cal_flags((adc_semantic_t)i);
if ((f & ADC_CAL_F_VALID) != 0u) { vl |= (1uL << i); }
if ((f & ADC_CAL_F_CLIPPED) != 0u) { cl |= (1uL << i); }
}
CHECK(vl == 0u, "VALID map = 0x00 (uncalibrated default build)");
/* bit1 UB, bit2 UC, bit5 IB, bit6 IC, bit7 IN */
CHECK(cl == ((1uL << 1) | (1uL << 2) | (1uL << 5) | (1uL << 6) | (1uL << 7)),
"CLIPPED map = UB/UC/IB/IC/IN (0xE6)");
}
/* TC-ADCCAL-007:k_uv 精确值(Q16 无舍入口径) */
static void tc_007_k_uv(void) {
printf("[TC-ADCCAL-007] k_uv exact (Q16 no-rounding defaults)\n");
reset_all();
adc_cal_init();
CHECK(adc_cal_k_uv(ADC_SEM_UA) > 152.5877f && adc_cal_k_uv(ADC_SEM_UA) < 152.5881f,
"UA k_uv = 152.587890625");
CHECK(adc_cal_k_uv(ADC_SEM_UB) > 805.6639f && adc_cal_k_uv(ADC_SEM_UB) < 805.6643f,
"UB k_uv = 805.6640625");
}
/* TC-ADCCAL-008:set() 合法带边界与拒绝 */
static void tc_008_set(void) {
float before;
printf("[TC-ADCCAL-008] set band edges inclusive / rejects leave RAM\n");
reset_all();
adc_cal_init();
CHECK(adc_cal_set(ADC_SEM_UA, 10500000L, -12L) == 0, "set in-band ok");
CHECK((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) != 0u, "VALID set after accepted");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 1000), exp_q16(1000, -12, 10500000)),
"apply uses accepted k/b");
CHECK(adc_cal_set(ADC_SEM_UA, 5000000L, 0L) == 0, "ROM/2 boundary accepted");
CHECK(adc_cal_set(ADC_SEM_UA, 20000000L, 0L) == 0, "ROM*2 boundary accepted");
before = adc_cal_apply(ADC_SEM_UA, 100);
CHECK(adc_cal_set(ADC_SEM_UA, 4999999L, 0L) == -1, "below ROM/2 rejected");
CHECK(adc_cal_set(ADC_SEM_UA, 20000001L, 0L) == -1, "above ROM*2 rejected");
CHECK(adc_cal_set(ADC_SEM_UA, 10000000L, 8193L) == -1, "b +8193 rejected");
CHECK(adc_cal_set(ADC_SEM_UA, 10000000L, -8193L) == -1, "b -8193 rejected");
CHECK(adc_cal_set(ADC_SEM_TEMP, 10000000L, 0L) == -1, "TEMP rejected");
CHECK(adc_cal_apply(ADC_SEM_UA, 100) == before, "RAM untouched after all rejects");
}
/* TC-ADCCAL-009:合法带哨兵(白盒直写 RAM 模拟引擎收到坏值) */
static void tc_009_sanitize(void) {
double want;
printf("[TC-ADCCAL-009] band sentinel: ROM fallback + one-shot ERR + no VALID\n");
reset_all();
adc_cal_init();
s_kcfg.k[0] = 30000000L; /* > ROM*2:模拟坏文件/坏标定已写入 RAM */
s_kcfg.valid |= 0x01u; /* 位图也说「已标定」——更要用哨兵兜住 */
want = exp_tpafe(1000);
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 1000), want), "out-of-band k falls back to ROM");
CHECK((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) == 0u, "VALID suppressed while k out of band");
CHECK(g_caltest_err_count == 1, "first bad observation logs exactly one ERR");
(void)adc_cal_apply(ADC_SEM_UA, 2000);
(void)adc_cal_apply(ADC_SEM_UA, 3000);
CHECK(g_caltest_err_count == 1, "repeat observations do not spam ERR");
/* b 越界同哨兵(k 合法、b 坏) */
s_bcfg.b[1] = 99999L;
CHECK(near_d(adc_cal_apply(ADC_SEM_UB, 1000), exp_stm32(1000)), "bad b -> ROM + b=0");
CHECK(g_caltest_err_count == 2, "bad b noted on its own channel");
/* init 重置留痕位 = 新观测窗 */
adc_cal_init();
CHECK(adc_cal_k_uv(ADC_SEM_UA) > 152.5f && s_kcfg.valid == 0u, "init restores ROM + clear valid");
/* stub 计数器是测试侧累计量,init 不清它——用增量断言 */
{
const int e0 = g_caltest_err_count;
s_kcfg.k[0] = 30000000L;
(void)adc_cal_apply(ADC_SEM_UA, 1);
CHECK(g_caltest_err_count == e0 + 1, "ERR window restarts after init (delta 1)");
}
}
/* TC-ADCCAL-014:b 生效(经 set 正规路径) */
static void tc_00a_b_effect(void) {
printf("[TC-ADCCAL-014] zero offset b via set path\n");
reset_all();
adc_cal_init();
CHECK(adc_cal_set(ADC_SEM_IA, 10000000L, -77L) == 0, "set IA b=-77");
CHECK(near_d(adc_cal_apply(ADC_SEM_IA, 1077), exp_q16(1077, -77, 10000000)),
"IA apply = (code-77)*ROMk");
}
/* TC-ADCCAL-015:两点标定公式 + 写回往返 */
static void tc_00b_two_point(void) {
int32_t kq = 0, bb = 0;
const int32_t k0 = 10000000L;
const int32_t b0 = -30L;
int32_t c1 = -250, c2 = 32000;
int32_t v1, v2;
int rc;
printf("[TC-ADCCAL-015] two-point formula recovers synthetic k/b\n");
/* 合成测量:v = (c + b0) * k0/65536 µV(lround 模拟量化读数) */
v1 = (int32_t)my_lround(((double)c1 + (double)b0) * ((double)k0 / 65536.0));
v2 = (int32_t)my_lround(((double)c2 + (double)b0) * ((double)k0 / 65536.0));
rc = adc_cal_two_point(c1, c2, v1, v2, &kq, &bb);
CHECK(rc == 0, "two_point ok");
CHECK(kq > (k0 - 5) && kq < (k0 + 5), "k recovered within +-5 q16");
CHECK(bb > (b0 - 3) && bb < (b0 + 3), "b recovered within +-3 codes");
/* 写回往返:set 后 apply(c2) 应还原 v2(µV 级) */
reset_all();
adc_cal_init();
CHECK(adc_cal_set(ADC_SEM_UA, kq, bb) == 0, "set recovered coeffs");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, c2), (double)v2 / 1.0e6), "roundtrip v(c2) = measured");
printf("[TC-ADCCAL-015] two_point guards\n");
CHECK(adc_cal_two_point(100, 100, 0L, 100000L, &kq, &bb) == -1, "dc=0 rejected");
CHECK(adc_cal_two_point(100, 200, 50000L, 50000L, &kq, &bb) == -1, "dv=0 rejected");
CHECK(adc_cal_two_point(100, 200, 100000L, 50000L, &kq, &bb) == -1, "inverted slope rejected");
CHECK(adc_cal_two_point(100, 200, 0L, 1000L, NULL, &bb) == -1, "NULL k rejected");
CHECK(adc_cal_two_point(100, 200, 0L, 1000L, &kq, NULL) == -1, "NULL b rejected");
/* 真零点 +9000 码 > B_MAX:斜率对但 b 越界 -> -2(先查前端,不用系数凑) */
v1 = (int32_t)my_lround((100.0 + 9000.0) * ((double)k0 / 65536.0));
v2 = (int32_t)my_lround((2000.0 + 9000.0) * ((double)k0 / 65536.0));
CHECK(adc_cal_two_point(100, 2000, v1, v2, &kq, &bb) == -2, "b beyond +-8192 -> -2");
}
/* TC-ADCCAL-016:贴轨门限表 */
static void tc_00c_rail_ths(void) {
int32_t lo = 0, hi = 0;
printf("[TC-ADCCAL-016] rail thresholds by source\n");
CHECK(adc_cal_rail_ths(ADC_SEM_UA, &lo, &hi) == 0 && hi == 32112L && lo == -32112L,
"TPAFE band = +-98% half-FS");
CHECK(adc_cal_rail_ths(ADC_SEM_UB, &lo, &hi) == 0 && hi == 4013L && lo == 81L,
"STM32 band = 98%/2% of 4095");
CHECK(adc_cal_rail_ths(ADC_SEM_TEMP, &lo, &hi) == -1, "TEMP not rail-watched");
CHECK(adc_cal_rail_ths(ADC_SEM_UA, NULL, &hi) == -1, "NULL guard");
}
/* TC-ADCCAL-017:init 幂等 */
static void tc_00d_idempotent(void) {
printf("[TC-ADCCAL-017] re-init restores defaults\n");
reset_all();
adc_cal_init();
(void)adc_cal_set(ADC_SEM_UA, 12345678L, 5L);
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 1000), exp_q16(1000, 5, 12345678)), "pre-check tuned");
adc_cal_init(); /* 幂等重入:RAM 复位 + 再注册 */
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 1000), exp_tpafe(1000)), "re-init back to ROM");
CHECK(adc_cal_flags(ADC_SEM_UA) == 0u, "re-init clears VALID too");
}
/* ===================== 用例(引擎集成组,卡 4 DoD) ===================== */
/* TC-ADCCAL-010:文件值经 load 生效 + VALID */
static void tc_010_file_apply(void) {
printf("[TC-ADCCAL-010] file values applied via cfg_file_load\n");
reset_all();
adc_cal_init();
vfs_put("[adc_cal]\r\nk_ua=12000000\r\nvalid=1\r\n"
"[adc_cal_ofs]\r\nb_ua=-40\r\n");
CHECK(cfg_file_load() == 0, "load ok");
CHECK(adc_cal_k_uv(ADC_SEM_UA) > 183.105f && adc_cal_k_uv(ADC_SEM_UA) < 183.106f,
"UA k = 12000000 q16 = 183.10546875 uV");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 6557), exp_q16(6557, -40, 12000000)),
"apply uses file k and negative file b");
CHECK((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) != 0u, "VALID from file bitmap");
/* 未列键保留默认 */
CHECK(adc_cal_k_uv(ADC_SEM_UB) > 805.663f && adc_cal_k_uv(ADC_SEM_UB) < 805.665f,
"UB untouched keeps ROM");
}
/* TC-ADCCAL-011:越界文件值由哨兵兜住(含位图说谎) */
static void tc_011_file_bad(void) {
printf("[TC-ADCCAL-011] out-of-band file k -> sentinel ROM fallback, VALID denied\n");
reset_all();
adc_cal_init();
vfs_put("[adc_cal]\r\nk_ua=999\r\nk_ub=-999\r\nvalid=7\r\n");
CHECK(cfg_file_load() == 0, "load ok (values structurally valid INT)");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 500), exp_tpafe(500)), "UA sanitized to ROM");
CHECK(near_d(adc_cal_apply(ADC_SEM_UB, 500), exp_stm32(500)), "UB sanitized to ROM");
CHECK(adc_cal_flags(ADC_SEM_UA) == 0u, "UA VALID denied despite bitmap bit (k out of band)");
CHECK(adc_cal_flags(ADC_SEM_UB) == ADC_CAL_F_CLIPPED, "UB keeps only CLIPPED");
CHECK(g_caltest_err_count >= 2, "both sanitizations logged");
}
/* TC-ADCCAL-012:段损坏(超长文件)→ 回退默认且 flags 无 VALID(卡 4 DoD 原句) */
static void tc_012_file_corrupt(void) {
char big[CFG_FILE_MAX_LEN + 64];
size_t i;
printf("[TC-ADCCAL-012] oversized corrupt file -> ROM defaults + no VALID\n");
reset_all();
adc_cal_init();
/* 先把有效值 + valid 位图写入 RAM(模拟曾标定过),再用超长文件触发引擎复位 */
(void)adc_cal_set(ADC_SEM_UA, 11000000L, 3L);
for (i = 0u; i < (sizeof(big) - 1u); i++) {
big[i] = 'A';
}
big[sizeof(big) - 1u] = '\0';
vfs_put(big);
CHECK(cfg_file_load() == 0, "load survives corruption");
CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 4096), exp_tpafe(4096)), "corrupt -> ROM coefficients");
CHECK(adc_cal_flags(ADC_SEM_UA) == 0u, "corrupt -> no VALID anywhere (DoD wording)");
CHECK(s_syslog_count >= 1, "corruption left self-test trail");
}
/* TC-ADCCAL-013:段缺失 → 引擎默认(=ROM),VALID 全 0 */
static void tc_013_section_missing(void) {
printf("[TC-ADCCAL-013] missing sections -> factory ROM, vl=0\n");
reset_all();
adc_cal_init();
vfs_put("; only a comment, no managed sections\n");
CHECK(cfg_file_load() == 0, "load ok with unrelated file");
CHECK(near_d(adc_cal_apply(ADC_SEM_IN, 2048), exp_stm32(2048)), "IN still ROM");
CHECK(adc_cal_flags(ADC_SEM_IA) == 0u, "no VALID with missing sections");
}
/* ===================== main ===================== */
int main(void) {
printf("=== ADC Cal Layer Tests ===\n");
tc_001_init_registers();
tc_002_004_apply();
tc_005_temp();
tc_006_default_flags();
tc_007_k_uv();
tc_008_set();
tc_009_sanitize();
tc_00a_b_effect();
tc_00b_two_point();
tc_00c_rail_ths();
tc_00d_idempotent();
tc_010_file_apply();
tc_011_file_bad();
tc_012_file_corrupt();
tc_013_section_missing();
printf("\n=== ADC Cal Layer Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""通道标定层(L1.5,adc_cal)主机侧单元测试运行器(L0.5,纯标准库)。
做什么:
用本机 MSVC 编译 test/host/adc_cal/adc_cal_test.c(它以 #include 纳入
**生产源文件** App/algo/adc_cal.c 与 App/cfg/cfg_file.c),在 PC 上离线跑
Q16 系数折算 / 三态 flags / 合法带哨兵回退 / 两点标定公式 / 两段 cfg
落盘契约(含段损坏回退),失败即非 0 退出。零硬件、零烧录、毫秒级。
工具链探测 / 编译运行全部委托 test/host/common/unit_test_harness.py。
为什么要主机侧单测(而不是只用板端套件):
标定折算的判据是"读数 = 系数 × 码值"——板端**无标准源**(数据可信 §2.6),
系数错、Q16 舍入错、哨兵漏放、负零点落盘错,在台架上全都看不出来;
主机侧用独立推导的期望值 + 坏文件注入把它们一次性钉死。
用法(仓库根目录):
python test/host/adc_cal/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误。
设计依据:docs/任务规划/数据可信.md 卡 2 / 卡 4(道A);
创建日期:2026-10-25
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
# 顺序敏感:stub 先于生产目录(覆盖 dbg_log.h)。cfg_file.h / cfg_file_store.h /
# adc_input.h / sys_log.h 用真实头;adc_input_desc 与 syslog_post 由测试单元给
# 强符号桩(后者供 cfg_file.c 的错误留痕路径)。
BUILD = TestBuild(
name="ADC Cal Layer Tests",
src=os.path.join(HERE, "adc_cal_test.c"),
include_dirs=[
os.path.join(HERE, "stub"),
os.path.join(ROOT, "App", "algo"),
os.path.join(ROOT, "App", "cfg"),
os.path.join(ROOT, "App", "log"),
HERE,
],
summary_marker="=== ADC Cal Layer Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+21
View File
@@ -0,0 +1,21 @@
/*
* 主机侧单测桩:dbg_log.h
* 模块功能:为 adc_cal.c / cfg_file.c 提供主机可编译的日志宏(映射为计数)。
* ⚠ 仅供 test/host/adc_cal/ 使用,不参与 MCU 构建。同 cfg_file 套件先例。
* 创建日期:2026-10-25
*/
#ifndef DBG_LOG_H
#define DBG_LOG_H
#include <stdio.h>
extern int g_caltest_err_count;
extern int g_caltest_info_count;
extern int g_caltest_debug_count;
#define DBG_INFO(fmt, ...) do { g_caltest_info_count++; } while (0)
#define DBG_DEBUG(fmt, ...) do { g_caltest_debug_count++; } while (0)
#define DBG_ERROR(fmt, ...) do { g_caltest_err_count++; } while (0)
#endif /* DBG_LOG_H */
+114
View File
@@ -31,6 +31,14 @@
* TC-ADCHEALTH-015 帧年龄纯判据:tick 与序号跨 0xFFFFFFFF 不误判、停滞仍判老
* TC-ADCHEALTH-016 帧年龄纯判据:序号倒退按「有变化」处理,不判老(判据分工)
* TC-ADCHEALTH-017 帧年龄纯判据:两个 age 对象互不干扰;空指针不判老
* —— L5-2 残余「码值越界(贴轨)检出」(2026-10-25 道A,卡 2 分支加入)——
* TC-ADCHEALTH-018 释放沿后连续 20 拍贴上轨 -> 锁存 + 边沿只报一次
* TC-ADCHEALTH-019 无释放沿(开机/空载恒 0)永不锁存(合法工况不误报)
* TC-ADCHEALTH-020 中带样本清零连续计数(19+19 不锁);触轨含等号边界
* TC-ADCHEALTH-021 上下轨交替互清对侧计数,永不锁存
* TC-ADCHEALTH-022 下轨带对称锁存(STM32 削顶 = 贴底签名)
* TC-ADCHEALTH-023 rail_init 清 sticky 与释放沿记录;health_init 内含复位;
* 通道越界忽略不污染
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
@@ -474,6 +482,105 @@ static void tc_age_two_observers(void) {
"TC-ADCHEALTH-017 NULL age never reports stale (no false lock)");
}
/* ===================== L5-2 贴轨观察器(2026-10-25 道A) ===================== */
/* 便捷喂数:连发 n 拍同值,返回最后一次 watch 的边沿结果 */
static uint32_t feed(uint8_t ch, int32_t code, int n) {
uint32_t edge = 0u;
int i;
for (i = 0; i < n; i++) {
edge = adc_health_rail_watch(ch, code, -32112, 32112);
}
return edge;
}
/* TC-ADCHEALTH-018:释放沿后连续 20 拍贴上轨 -> 锁存,边沿只报一次 */
static void tc_rail_latch_after_release(void) {
uint32_t edge;
adc_health_rail_init();
(void)feed(0, 0, 1);
edge = feed(0, 32500, 19);
CHECK(edge == 0u && adc_health_rail_sticky() == 0u,
"018: 19 hi beats after release -> no latch");
edge = feed(0, 32600, 1);
CHECK(edge == 1u, "018: 20th consecutive hi beat -> edge");
CHECK(adc_health_rail_sticky() == (1uL << 0), "018: sticky bit0");
edge = feed(0, 32700, 5);
CHECK(edge == 0u, "018: no repeat edge after latched");
}
/* TC-ADCHEALTH-019:无释放沿(恒 0)永不锁存——空载/开机合法工况 */
static void tc_rail_static_zero_ok(void) {
adc_health_rail_init();
(void)feed(3, 0, 30);
CHECK(adc_health_rail_sticky() == 0u,
"019: static in-band zero (no release) never latches");
}
/* TC-ADCHEALTH-020:计数被中带清零;再连续 20 才锁存(含触轨带含等号边界) */
static void tc_rail_reset_by_mid(void) {
uint32_t edge;
adc_health_rail_init();
(void)feed(1, 0, 1);
(void)feed(1, 32113, 19);
(void)feed(1, 0, 1);
edge = feed(1, 32112, 19);
CHECK(edge == 0u && adc_health_rail_sticky() == 0u,
"020: mid sample resets run counter (19+19 never hits 20)");
edge = feed(1, 32112, 1);
CHECK(edge == 1u && adc_health_rail_sticky() == (1uL << 1),
"020: fresh 20-run latches; boundary code == hi_th counts as rail");
}
/* TC-ADCHEALTH-021:上下轨交替互不清锁存但各自清零对侧计数 */
static void tc_rail_alt_no_latch(void) {
int i;
adc_health_rail_init();
(void)feed(2, 0, 1);
for (i = 0; i < 60; i++) {
(void)adc_health_rail_watch(2, ((i & 1) != 0) ? 32500 : -32500, -32112, 32112);
}
CHECK(adc_health_rail_sticky() == 0u,
"021: alternating rails clear each other, never latch");
}
/* TC-ADCHEALTH-022:下轨对称路径(STM32 削顶 = 贴底签名,同样先要释放沿) */
static void tc_rail_low_side(void) {
uint32_t edge = 0u;
int i;
adc_health_rail_init();
(void)adc_health_rail_watch(5, 2000, 81, 4013);
for (i = 0; i < 20; i++) {
edge |= adc_health_rail_watch(5, 0, 81, 4013);
}
CHECK(edge == 1u && adc_health_rail_sticky() == (1uL << 5),
"022: low rail band latches symmetrically (ch5)");
}
/* TC-ADCHEALTH-023:rail_init 全清(含释放沿记录);health_init 内含;越界通道忽略 */
static void tc_rail_reset_and_oob(void) {
adc_health_rail_init();
(void)feed(0, 0, 1);
(void)feed(0, 32500, 20);
CHECK(adc_health_rail_sticky() != 0u, "023: pre-check latch happens");
adc_health_rail_init();
CHECK(adc_health_rail_sticky() == 0u, "023: rail_init clears sticky");
(void)feed(0, 32500, 25);
CHECK(adc_health_rail_sticky() == 0u, "023: release history cleared too");
(void)feed(0, 0, 1);
(void)feed(0, 32500, 20);
CHECK(adc_health_rail_sticky() != 0u, "023: pre-check relatch for health_init test");
adc_health_init();
CHECK(adc_health_rail_sticky() == 0u, "023: adc_health_init() also clears rail");
CHECK(adc_health_rail_watch(9, 32767, -32112, 32112) == 0u,
"023: ch OOB ignored, no sticky pollution");
}
int main(void) {
printf("=== ADC Health Tests ===\n");
@@ -498,6 +605,13 @@ int main(void) {
tc_tick_wrap();
tc_null_guard();
tc_reinit();
printf("-- L5-2 rail watch --\n");
tc_rail_latch_after_release();
tc_rail_static_zero_ok();
tc_rail_reset_by_mid();
tc_rail_alt_no_latch();
tc_rail_low_side();
tc_rail_reset_and_oob();
printf("\n=== ADC Health Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);