diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ebe22627..5e8162f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -341,6 +341,32 @@ 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 + # 7h3) 量纲回归(「数据可信」卡 7,道A)主机侧单元测试(2026-10-25 加) + # 背景:台架曾长期缺"不该动作时不动作"的判据,码值/工程量错配曾以 + # 恒假动作的形态混过验收。本钩子用真实链(adc_cal→ring→phasor→ + # prot_elm)+ 显式换算的合成波把七类元件的量纲钉死:折算点被旁路 + # 或量纲再错位,断言立刻翻转。files 覆盖链上全部生产源与两处编排。 + - id: prot-dim-unit-test + name: 量纲回归单测(主机侧) + entry: python test/host/prot_dim/run_unit_test.py + language: system + files: ^(App/algo/(adc_cal|prot_elm|prot_set|phasor|sample_ring|freq)\.(c|h)|App/task/(adc_task|protect_task)\.c|test/host/prot_dim/.*|test/host/common/unit_test_harness\.py)$ + pass_filenames: false + + # 7i) 保护定值模型(P1 / L5-1 载体)主机侧单元测试(2026-10-11 加) # 定值是保护动作的"输入宪法":范围校验、整表装载 all-or-nothing、 # 校验和封存-自检-闭锁(参考 tagPRS dwTagSum/dwTagSumN 模式)必须在 diff --git a/App/algo/README.md b/App/algo/README.md index 5860f348..5801bed6 100644 --- a/App/algo/README.md +++ b/App/algo/README.md @@ -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` 已在 `` 内。 -- 本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路):七个文件全部登记在 +- 八个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `` 内。 +- 本层**不进** `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. 新增一个算法模块的固定动作 diff --git a/App/algo/adc_cal.c b/App/algo/adc_cal.c new file mode 100644 index 00000000..aa430667 --- /dev/null +++ b/App/algo/adc_cal.c @@ -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 +#include + +#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(®, 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(®) != 0) { + DBG_ERROR("register '%s' FAIL (dup/full?) - calibration file values inactive", + ADC_CAL_SEC_K); + } + + (void)memset(®, 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(®) != 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; +} diff --git a/App/algo/adc_cal.h b/App/algo/adc_cal.h new file mode 100644 index 00000000..4b83978c --- /dev/null +++ b/App/algo/adc_cal.h @@ -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 + +#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 */ diff --git a/App/algo/adc_health.c b/App/algo/adc_health.c index a0182bf0..4b888c02 100644 --- a/App/algo/adc_health.c +++ b/App/algo/adc_health.c @@ -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 /* 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; +} diff --git a/App/algo/adc_health.h b/App/algo/adc_health.h index d05dd594..e9aec37d 100644 --- a/App/algo/adc_health.h +++ b/App/algo/adc_health.h @@ -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 diff --git a/App/algo/freq.h b/App/algo/freq.h index fcbcb3ba..34a335dd 100644 --- a/App/algo/freq.h +++ b/App/algo/freq.h @@ -49,9 +49,15 @@ #define FREQ_INST_MIN_HZ 35.0f #define FREQ_INST_MAX_HZ 65.0f -/* 幅值门限(有效值,**原始码值口径**,L1-5 标定未实现):低于该值判 - * "无有效波形"。⚠ 拟定值(远小于正常波形千级码值、远大于浮空噪声), - * 标定落地后须按工程量口径复核(同 prot_set 默认值"拟定"口径)。 */ +/* 幅值门限(有效值,**二次值 V 口径**,「数据可信」卡 6 重定,依据其 §2.3 表第 1 行): + * 低于该值判"无有效波形"。取值 50 V = 0.5×Un(相电压标称 100 V,与 + * App/algo/prot_set.c 头部量纲基准同源):正常带载 UA 在 0.9~1.1×Un, + * 50 V 之下只可能是断保险/PT 空载/浮空噪声(浮空实测 ~1.7 V 引脚域、 + * 二次域更小),不会吃掉正常纹波(纹波谷值远大于 0.5×Un)。 + * ⚠ 过渡期行为(eng=0,卡 12 标定前):帧值还是引脚电压(≤5 V < 50 V), + * 本门**恒失效** → freq_valid()=0 → OF/UF 按"必不启动"填充复位——与卡 5 + * 未标定闭锁同向(判据链本就停走),不构成额外风险;标定后自动恢复本意。 + * 数值沿用原 50.0f 属巧合(原为"远小于千级码值"的拟定值),不是漏改。 */ #define FREQ_AMP_MIN 50.0f /* 相位增量滑窗环容量(拍):96 拍 = 96 ms 上限。实际平均窗长**自适应** diff --git a/App/algo/prot_elm.c b/App/algo/prot_elm.c index 9a15b599..9a0b676c 100644 --- a/App/algo/prot_elm.c +++ b/App/algo/prot_elm.c @@ -189,7 +189,8 @@ void prot_elm_eval(uint32_t now_ms, const float meas[PE_COUNT]) { * 函数功能:适配层——按实例表 src 组装测量数组后调判据核 * 入口参数:now_ms - 单调时基(tick ms) * 返 回 值:无 - * 限定条件:仅保护消费者任务上下文;工程量↔码值换算 = 1(L1-5 标定未实现) + * 限定条件:仅保护消费者任务上下文;测量已是 L1.5 工程量(未标定域由 + * protectTask 卡 5 闭锁挡在门外,本函数不感知标定态) * 函数说明:src=PHASOR 行任一相量未就绪(未满一周波)→ **整拍跳过**, * 不引入陈旧/半新测量(B5 口径)。src=FREQ 行:freq 有有效读数 * (started 且幅值门 OK)→ 填测得频率;否则按方向填"必不启动"值 diff --git a/App/algo/prot_elm.h b/App/algo/prot_elm.h index e0ebc2fa..ed04ee85 100644 --- a/App/algo/prot_elm.h +++ b/App/algo/prot_elm.h @@ -22,7 +22,10 @@ * - 返回**即时**(参考 GRly_TimeRelay 带 10 ms 返回延时——V0.1 无出口硬件, * 抖动只影响日志,P4 接出口时再补返回延时); * - 复压/方向/无压无流闭锁(dwSetFyCtrl 类)随 P2/P3 元件扩展; - * - 工程量↔码值换算系数暂为 1(L1-5 标定未实现,同 phasor 层口径)。 + * - 测量输入 = adc_task 经 adc_cal_apply() 折算后的工程量(L1.5,卡 2 已落地): + * 未标定(VALID=0)时单位只是 ADC 引脚电压——该域下判据必然错配,由 + * protectTask 的未标定闭锁(卡 5)停走整条判据链,元件核本身不掺和 + * 标定语义(保持纯逻辑);标定后单位 = 二次值,定值口径才名副其实。 * * 线程约定:init 内核启动前或保护消费者任务上下文;eval/run 仅保护消费者任务 * 上下文(B5 起 = protectTask);request_reset 任意上下文(跨上下文复归请求, diff --git a/App/algo/prot_set.c b/App/algo/prot_set.c index 71a88759..1392f47e 100644 --- a/App/algo/prot_set.c +++ b/App/algo/prot_set.c @@ -5,6 +5,14 @@ * ⚠ 默认值/范围为拟定值(参考 EEPROM 出厂默认表不在本仓库,规划 §10.3 待补); * 通讯地址逐项对照参考 Comm/Pub/Const_CommSetPara.c g_tCommSetParaTab。 * + * 量纲口径(「数据可信」卡 6 复核,2026-10-25):A/V 项 = **二次值**工程量, + * 标称基准 100 V / 1 A(UA/UB/UC 相电压、3U0、OV/UV 同域——OV 120 = 1.2×Un、 + * UV 20 = 0.2×Un、3U0 15 = 0.15×Un;OC1/2/3 = 10/5/2 ×Ir、OL = 1.0×Ir, + * 三段递升与过负荷低于速断均自洽)。ADC 帧仅在 adc_cal VALID 置位后才处 + * 于此域(未标定 = 引脚电压域,由卡 5 闭锁停走判据,见 protect_task.c); + * min/max 范围覆盖 0~4×标称,无需随本卡改动数值——换算依据与三处下游 + * 常数同源,见 docs/任务规划/数据可信.md §2.3。 + * * 创建日期:2026-10-11 */ #include "prot_set.h" diff --git a/App/app/app_main.c b/App/app/app_main.c index 9515c496..79f56f25 100644 --- a/App/app/app_main.c +++ b/App/app/app_main.c @@ -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。 */ diff --git a/App/log/soe_rec.h b/App/log/soe_rec.h index 430de0c8..faf76dfd 100644 --- a/App/log/soe_rec.h +++ b/App/log/soe_rec.h @@ -69,6 +69,10 @@ typedef enum { SOE_T_PLATE_IN, SOE_T_SAMPLE_BLOCK, SOE_T_SAMPLE_RESUME, + /* 未标定闭锁(「数据可信」卡 5,2026-10-25 道A 追加):UA/UN/IA 的 VALID + * 未全置位 → 判据停走;与采样闭锁分型,边沿各留一条记录 */ + SOE_T_CAL_BLOCK, + SOE_T_CAL_RESUME, SOE_T_COUNT /* 哨兵值,非有效类型 */ } soe_type_t; diff --git a/App/protocol/iec104/iec104_pts.c b/App/protocol/iec104/iec104_pts.c index c3ffd6fd..8269fdbd 100644 --- a/App/protocol/iec104/iec104_pts.c +++ b/App/protocol/iec104/iec104_pts.c @@ -5,6 +5,11 @@ * 遥测死区)。点表口径见 iec104_pts.h 文件头与设计文档 §3。 * 线程约定:init/bind 建服务前;其余仅 104 服务任务上下文(快照非原子)。 * 创建日期:2026-10-12(B4) + * 修改记录: + * 2026-10-25 DTU 项目组 卡 6(「数据可信」):YC 死区由拟定码值域重定为 + * 二次值工程量域(额定 0.5 % 规则),判据依据见 + * k_yc 表头注释;数值随卡 9 迁运行参数,本表将退化为 + * ROM 默认载体。 */ #include "iec104_pts.h" @@ -16,23 +21,31 @@ #include "prot_set.h" /* 板端源:定值 + 闭锁 */ #include "adc_input.h" /* adc_semantic_t(phasor 实例按语义绑定) */ -/* ---------------- YC 表(§1.5 点号 4011+;死区 = 拟定码值,L1-5 后复核) -- */ +/* ---------------- YC 表(§1.5 点号 4011+;死区 = 二次值工程量域) --------- */ typedef struct { uint32_t ioa; int8_t sem; /* 绑定的 phasor 语义通道(-1 = 无 phasor 源,恒无效) */ - float deadband; /* 死区(工程量绝对值) */ + float deadband; /* 死区(二次值工程量绝对值,「数据可信」卡 6 重定) */ } pts_yc_def_t; +/* 死区规则 = 额定二次值的 0.5 %(「数据可信」§2.3 表第 2 行建议规则,卡 6 采纳; + * 额定基准 100 V / 1 A 与 App/algo/prot_set.c 头部同源,勿在此另立口径): + * 电压类 0.5 % × 100 V = 0.5 V 电流类 0.5 % × 1 A = 0.005 A + * 频率点不适用"额定百分比"(额定附近 0.5 Hz 与旧值同量级,维持 0.5 并随 + * 卡 10 运行参数段可调)。IV 品质点(sem=-1)数值仅为将来启用预留同型。 + * ⚠ 过渡期(eng=0 引脚电压域,卡 12 标定前):上送值本身非二次值,死区 + * 按本域判定无意义但也不会漏报——104 侧 IV/数据可信度语义由 M5 快照层 + * (卡 8/9)统一处理,本卡只把"量纲错位"这一处常量债还掉。 */ static const pts_yc_def_t k_yc[] = { - { 0x4011u, (int8_t)ADC_SEM_UA, 10.0f }, /* 1Ua */ - { 0x4012u, -1, 10.0f }, /* 1Ub:V0.1 削顶 → IV */ - { 0x4013u, -1, 10.0f }, /* 1Uc:同上 */ - { 0x4014u, (int8_t)ADC_SEM_UN, 10.0f }, /* 1U0(外部零序专用通道) */ - { 0x4015u, (int8_t)ADC_SEM_IA, 10.0f }, /* 1Ia */ - { 0x4016u, -1, 10.0f }, /* 1Ib */ - { 0x4017u, -1, 10.0f }, /* 1Ic */ - { 0x4018u, -1, 10.0f }, /* 1I0 */ - { 0x401Du, -1, 0.5f }, /* 1f:L1-7 未实现 → IV */ + { 0x4011u, (int8_t)ADC_SEM_UA, 0.5f }, /* 1Ua:0.5 % × 100 V */ + { 0x4012u, -1, 0.5f }, /* 1Ub:V0.1 削顶 → IV */ + { 0x4013u, -1, 0.5f }, /* 1Uc:同上 */ + { 0x4014u, (int8_t)ADC_SEM_UN, 0.5f }, /* 1U0(外部零序专用通道) */ + { 0x4015u, (int8_t)ADC_SEM_IA, 0.005f }, /* 1Ia:0.5 % × 1 A */ + { 0x4016u, -1, 0.005f }, /* 1Ib */ + { 0x4017u, -1, 0.005f }, /* 1Ic */ + { 0x4018u, -1, 0.005f }, /* 1I0 */ + { 0x401Du, -1, 0.5f }, /* 1f:Hz 绝对死区(非额定比)→ IV */ }; #define YC_N ((uint8_t)(sizeof(k_yc) / sizeof(k_yc[0]))) diff --git a/App/task/adc_task.c b/App/task/adc_task.c index 672701bd..7d6ae24f 100644 --- a/App/task/adc_task.c +++ b/App/task/adc_task.c @@ -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; } diff --git a/App/task/protect_task.c b/App/task/protect_task.c index 24191694..6caa4abc 100644 --- a/App/task/protect_task.c +++ b/App/task/protect_task.c @@ -8,7 +8,16 @@ * 可忽略;失配 → prot_set 内部闭锁,eval 自动复位全元件); * ③ prot_elm_run()(phasor rms → 判据步进)+ 动作位图边沿日志; * ④ 全局软压板 PS_GLOBAL_EN(104 YK12 两段式投退,B7):退出 = - * 复位元件 + 停判据(与采样闭锁同构,独立边沿日志)。 + * 复位元件 + 停判据(与采样闭锁同构,独立边沿日志); + * ⑤ 未标定闭锁(「数据可信」卡 5,M4 唯一新增行为变更):UA/UN/IA + * 的 adc_cal VALID 位未全置位 → 工程量还只是 ADC 引脚电压 + * (eng=0),拿去比二次值定值必然错判 → 复位元件 + 本拍不 eval。 + * 默认构建(未标定)恒闭锁是**期望行为**:边沿用 DBG_INFO 而非 + * [ERR](未标定是出厂常态、不是故障;每次开机都走这条沿, + * [ERR] 还会误触 @test 禁用标记);台架旁路只走编译期宏 + * DTU_BENCH(「数据可信」§4.3:不进 cfg、不进 104,本仓任何 + * 提交目标都不定义它)。判定优先级 = 采样可信在前、标定可用 + * 在后(数据都旧了谈不上标定)。 * 定值/元件初始化在 app_main_init(内核前,B5);本任务只消费。 * 线程约定:prot_elm_run / prot_set_selfcheck 仅本任务上下文(prot_elm.h); * 跨上下文复归(104 YK11)经 prot_elm_request_reset 落请求。 @@ -20,6 +29,12 @@ * adc_health 的 stall 位依赖 adcTask 每拍跑 check(), * 节拍源一停本任务就再没有任何人判"数据新不新", * 而相量冻在最后一帧上还会被本任务继续用于判据 + * v1.2 2026-10-25 DTU 项目组 卡 5:新增第⑤路「未标定闭锁」(adc_cal_flags + * VALID 口径,docs/任务规划/数据可信.md §4.3): + * 采样恢复分支内、巡检/压板之前判;边沿 INFO 对 + * + SOE_T_CAL_BLOCK/RESUME(不用 [ERR]:未标定是 + * 常态非故障,且 @test 禁用 [ERR]);DTU_BENCH + * 编译期旁路(本仓目标不定义 = 产品恒强制闭锁) */ #include "protect_task.h" @@ -31,6 +46,7 @@ #include "adc_input.h" /* adc_input_stm32_seq():DMA 完成计数(中断侧写) */ #include "adc_health.h" +#include "adc_cal.h" /* 卡 5:adc_cal_flags() VALID = 未标定闭锁源 */ #include "console_log.h" /* dbg_console_writer_ready():启动锚点行门控(B6) */ #include "prot_elm.h" #include "prot_set.h" @@ -60,6 +76,33 @@ static adc_health_age_t s_age_a1; /* STM32 车道 */ static uint32_t s_prev_active; /* 上拍动作位图(边沿日志用) */ static uint8_t s_prev_hf_lock; /* 上拍采样闭锁态(边沿日志用) */ static uint8_t s_prev_plate_out; /* 上拍全局软压板退出态(边沿日志用,B7) */ +static uint8_t s_prev_cal_lock; /* 上拍未标定闭锁态(边沿日志用,卡 5) */ + +/* + * 函数功能:未标定闭锁判定(「数据可信」卡 5 / §4.3) + * 入口参数:无 + * 返 回 值:1 - 闭锁(判据停走);0 - 放行 uint8_t + * 限定条件:仅本任务上下文(adc_cal_flags 纯读,任意上下文可读) + * 函数说明:判据 = UA/UN/IA 三路 VALID **全**置位——恰好是 V0.1 唯一有效的 + * 三个测量通道(N-18);UB/UC/IB/IC/IN 即使将来标定也另受 CLIPPED + * 品质约束(M5 快照层处理),不掺和进本闭锁源。VALID 随 cfg + * load/poll 与卡 12 标定自动翻转,本函数无自有状态(每拍现查, + * 无陈旧标志位)。台架调试旁路 = 编译期 -DDTU_BENCH(§4.3 决策 1 + * 的例外通道,仅本地台架构建自行定义;产品恒强制)。 + */ +/* 形态说明:用 #ifndef DTU_BENCH 把"产品默认路径"留在主分支(而非 #ifdef + * 双分支)——cppcheck 对未定义宏取真分支,双分支写法会把它引到旁路分支上 + * 报 knownConditionTrueFalse 假信号(实测 2026-10-25)。 */ +static uint8_t s_cal_locked(void) { +#ifndef DTU_BENCH + if (((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) == 0u) || + ((adc_cal_flags(ADC_SEM_UN) & ADC_CAL_F_VALID) == 0u) || + ((adc_cal_flags(ADC_SEM_IA) & ADC_CAL_F_VALID) == 0u)) { + return 1u; + } +#endif + return 0u; +} /* * 函数功能:动作位图变化沿日志 + SOE 投递(逐变化元件) @@ -153,29 +196,48 @@ void protect_task(void *argument) { SOE_ELM_NONE, 0u); s_prev_hf_lock = 0u; } - /* L5-1 巡检:定值区与封存校验和不符 → prot_set 闭锁(下一拍起 - * eval 自动复位全元件)。B7 收口:104 定值写改走 - * prot_set_set_sealed(影子区写+封存后单字交换),selfcheck 另有 - * seqlock 式复核(提交撞见 = 本轮跳过),误锁窗口已消除。 */ - (void)prot_set_selfcheck(); - /* 全局软压板(PS_GLOBAL_EN,104 YK12 两段式投退,B7):退出 = - * 保护停止判据并复位元件状态(重新投入从干净快照起步) */ - if (prot_set_get_ctl(PS_GLOBAL_EN) == 0u) { - if (s_prev_plate_out == 0u) { - DBG_INFO("global plate OUT -> protection disabled"); - (void)soe_store_post((uint8_t)SOE_T_PLATE_OUT, + /* 卡 5:未标定闭锁——夹在「采样可信」与「巡检/压板」之间: + * eng=0 域(引脚电压)拿去比二次值定值必然错判(UV 尤甚), + * 标定 VALID 未全置位前整条判据链停走(「数据可信」§4.3 决策 1) */ + if (s_cal_locked() != 0u) { + if (s_prev_cal_lock == 0u) { + DBG_INFO("uncalibrated -> protection blocked (UA/UN/IA VALID not all set)"); + (void)soe_store_post((uint8_t)SOE_T_CAL_BLOCK, SOE_ELM_NONE, 0u); - s_prev_plate_out = 1u; + s_prev_cal_lock = 1u; } prot_elm_init(); } else { - if (s_prev_plate_out != 0u) { - DBG_INFO("global plate IN -> protection enabled"); - (void)soe_store_post((uint8_t)SOE_T_PLATE_IN, + if (s_prev_cal_lock != 0u) { + DBG_INFO("calibration VALID -> protection unlocked"); + (void)soe_store_post((uint8_t)SOE_T_CAL_RESUME, SOE_ELM_NONE, 1u); - s_prev_plate_out = 0u; + s_prev_cal_lock = 0u; + } + /* L5-1 巡检:定值区与封存校验和不符 → prot_set 闭锁(下一拍起 + * eval 自动复位全元件)。B7 收口:104 定值写改走 + * prot_set_set_sealed(影子区写+封存后单字交换),selfcheck 另有 + * seqlock 式复核(提交撞见 = 本轮跳过),误锁窗口已消除。 */ + (void)prot_set_selfcheck(); + /* 全局软压板(PS_GLOBAL_EN,104 YK12 两段式投退,B7):退出 = + * 保护停止判据并复位元件状态(重新投入从干净快照起步) */ + if (prot_set_get_ctl(PS_GLOBAL_EN) == 0u) { + if (s_prev_plate_out == 0u) { + DBG_INFO("global plate OUT -> protection disabled"); + (void)soe_store_post((uint8_t)SOE_T_PLATE_OUT, + SOE_ELM_NONE, 0u); + s_prev_plate_out = 1u; + } + prot_elm_init(); + } else { + if (s_prev_plate_out != 0u) { + DBG_INFO("global plate IN -> protection enabled"); + (void)soe_store_post((uint8_t)SOE_T_PLATE_IN, + SOE_ELM_NONE, 1u); + s_prev_plate_out = 0u; + } + prot_elm_run(now); } - prot_elm_run(now); } } diff --git a/App/task/ui_task.c b/App/task/ui_task.c index c3350528..d67cb6c0 100644 --- a/App/task/ui_task.c +++ b/App/task/ui_task.c @@ -30,6 +30,8 @@ #include "ui_menu.h" #include "ui_net_sink.h" #include "ui_standby.h" +#include "ui_w_records.h" /* task-6 记录 reader 注入点(UI 页面不再 include sys_log.h) */ +#include "sys_log.h" #include "ui_tokens.h" /* UI 设计令牌:循环周期 UI_TASK_TICK_MS / 待机阈值 UI_STANDBY_IDLE_MS */ #include "sys_clock.h" #include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */ @@ -49,6 +51,41 @@ static void ui_task_time_cb(char *buf, size_t len) { } } +/* + * 函数功能:记录 reader 适配(task-6 注入)——UI 不透明编号 0/1 → syslog_id_t 映射 + * 入口参数:log_id - UI 记录源编号(UI_W_REC_LOG_*) uint8_t + * 返回值:记录条数;未知编号返回 0(页面退化为空态) uint16_t + * 限定条件:uiTask 上下文同步调用(记录页绘制路径,与合并前直调同形) + */ +static uint16_t ui_task_rec_count(uint8_t log_id) { + if (log_id == UI_W_REC_LOG_RUN) { + return (uint16_t)syslog_count(SYSLOG_RUN); + } + if (log_id == UI_W_REC_LOG_SELFTEST) { + return (uint16_t)syslog_count(SYSLOG_SELFTEST); + } + return 0u; +} + +/* + * 函数功能:记录 reader 适配(task-6 注入)——读记录字段直通 sys_log(错误码不改写) + * 入口参数:log_id/idx/ts/ts_len/desc/desc_len - 契约见 ui_w_records.h ui_rec_reader_t + * 返回值:0 成功;-1 编号/序号越界(未知编号同 -1);-2 读失败 int + * 限定条件:同上 + */ +static int ui_task_rec_get(uint8_t log_id, uint32_t idx, char *ts, uint32_t ts_len, + char *desc, uint32_t desc_len) { + if (log_id == UI_W_REC_LOG_RUN) { + return syslog_get_record(SYSLOG_RUN, idx, ts, (size_t)ts_len, desc, (size_t)desc_len); + } + if (log_id == UI_W_REC_LOG_SELFTEST) { + return syslog_get_record(SYSLOG_SELFTEST, idx, ts, (size_t)ts_len, desc, (size_t)desc_len); + } + return -1; +} + +static const ui_rec_reader_t s_rec_reader = { ui_task_rec_count, ui_task_rec_get }; + #define DBG_TAG "[UI_TASK]" #include "dbg_log.h" @@ -103,6 +140,9 @@ void ui_task_func(void *argument) { for (;;) { osDelay(1000); } } DBG_INFO("UI port init: OK"); + /* 记录 reader 注入:必须在 ui_menu_init 之前(记录页在此前不可能被打开, + * 但契约把"启动期先绑定"写死;未绑定窗口组件按空态防御——见 ui_w_records.h) */ + ui_w_records_bind(&s_rec_reader); DBG_INFO("UI task: ui_menu_init enter"); if (ui_menu_init() != 0) { DBG_ERROR("UI menu init: FAIL"); diff --git a/Drivers/BSP/UI/ui_page_action.c b/Drivers/BSP/UI/ui_page_action.c new file mode 100644 index 00000000..6908d098 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_action.c @@ -0,0 +1,199 @@ +/* + * 模块名称:UI 动作确认页(操作页共享实现)实现 + * 模块功能:见 ui_page_action.h 头注释(确认链状态机 + 投递回报 + 表单页自绘) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道B task-5 三组件之一) + */ + +/* 头文件包含区 */ +#include + +#include "ui_page_action.h" + +#include "ui_content_page.h" + +#include "ui_tokens.h" + +/* 私有宏定义区 */ + +#define AC_PROMPT_Y 44 /* 问句/结果行 放置 y(内容区上三分之一) */ +#define AC_STAGE_Y 66 /* 态标行("二次确认" / "ERR")放置 y */ +#define AC_HINT_Y 110 /* 键提示行 放置 y */ + +/* 固定文案(用字均已入字体子集:二/次/确/认/取/消/返/回/越/界 及 ASCII) */ +#define AC_STAGE2_TXT "二次确认" +#define AC_HINT_DO "ENTER=确认 ESC=取消" +#define AC_HINT_BACK "ENTER=返回" +#define AC_ERR_TXT "ERR" + +/* 私有变量定义区 */ + +/* + * 模块单例:内容页框架全局单页(s_open 互斥),同一时刻至多一张操作页, + * 故钩子经"当前实例指针"取上下文即可成立(头注已载此约束;批 2 各跳板 + * 各自 static 实例、先后开页互不冲突) + */ +static ui_page_action_t *s_act; /* 当前实例(NULL = 未在用) */ +static content_page_def_t s_desc; /* 模板描述符(开页时按实例填充) */ + +/* 私有函数声明区 */ + +static void ac_draw_centered(lv_layer_t *a_layer, const char *a_text, + int32_t a_y, const lv_area_t *a_clip); + +/* 函数定义区 */ + +/* + * 函数功能:居中绘制单行文本(框架 cp_draw_centered_text 同法,本文件私有一致拷贝) + * 入口参数:layer - 绘制层 lv_layer_t * + * text - 文本(Flash 字面量) const char * + * y - 放置 y int32_t + * clip - 重绘区 const lv_area_t * + * 返回值:无 + * 限定条件:text 非空;条带(整宽 x0..159,高 UI_TEXT_BAND_H)与 clip 相交才绘制 + */ +static void ac_draw_centered(lv_layer_t *a_layer, const char *a_text, + int32_t a_y, const lv_area_t *a_clip) { + lv_area_t t; + lv_draw_label_dsc_t ld; + + /* 本 LVGL 版 lv_text_get_width:必须传 strlen(0 = 不测量返回 0, + * 传 -1 越串尾死循环——框架同款注释警示) */ + t.x1 = 0; + t.x2 = UI_SCREEN_W - 1; + t.y1 = a_y; + t.y2 = a_y + UI_TEXT_BAND_H; + if (!((t.x1 <= a_clip->x2) && (a_clip->x1 <= t.x2) && + (t.y1 <= a_clip->y2) && (a_clip->y1 <= t.y2))) { + return; /* 不相交(culling) */ + } + + lv_draw_label_dsc_init(&ld); + ld.base.layer = a_layer; + ld.text = a_text; + ld.text_static = 1u; /* Flash 字面量:不拷内存 */ + ld.color = UI_COL_FG; + ld.ofs_x = (UI_SCREEN_W - + (int32_t)lv_text_get_width(a_text, (uint32_t)strlen(a_text), LV_FONT_DEFAULT, 0)) / 2; + ld.ofs_y = 0; + lv_draw_label(a_layer, &ld, &t); +} + +/* + * 函数功能:自绘(框架内置区块之后:问句/态标/键提示 三行居中) + * 入口参数:layer - 绘制层 lv_layer_t * + * clip - 重绘区 const lv_area_t * + * 返回值:无 + * 限定条件:经描述符 draw_cb 挂接,页面打开中(s_act 有效) + */ +static void ac_draw_cb(lv_layer_t *layer, const lv_area_t *clip) { + if ((s_act == NULL) || (s_act->prompt == NULL)) { + return; + } + + /* 行 1:问句(stage 2 换成功结果行) */ + if (s_act->stage == 2u) { + if (s_act->done_text != NULL) { + ac_draw_centered(layer, s_act->done_text, AC_PROMPT_Y, clip); + } + } + else { + ac_draw_centered(layer, s_act->prompt, AC_PROMPT_Y, clip); + } + + /* 行 2:态标(二次确认 / 投递失败 ERR) */ + if (s_act->stage == 1u) { + ac_draw_centered(layer, (s_act->err != 0u) ? AC_ERR_TXT : AC_STAGE2_TXT, + AC_STAGE_Y, clip); + } + + /* 行 3:键提示 */ + ac_draw_centered(layer, (s_act->stage == 2u) ? AC_HINT_BACK : AC_HINT_DO, + AC_HINT_Y, clip); +} + +/* + * 函数功能:按键处理(确认链状态机) + * 入口参数:key - LVGL 键值 lv_key_t + * 返回值:true = 已消费;false = 交回框架(其余键忽略) + * 限定条件:经描述符 key_cb 挂接(key_intercept=0:LEFT 直接关页 = 取消, + * 不进本函数;ESC 恒关页框架保证,同样不进本函数) + * 函数说明:ENTER:stage0 -> stage1(二次确认);stage1 -> 投递 ok_cb: + * 接受且有 done_text -> stage2 回报,接受且无 done_text -> 直接关页, + * 拒收 -> err=1 留在 stage1;stage2 -> 关页返回 + */ +static bool ac_key_cb(lv_key_t key) { + if (key != LV_KEY_ENTER) { + return false; + } + if (s_act == NULL) { + return false; /* 防御 */ + } + + if (s_act->stage == 0u) { + s_act->stage = 1u; + s_act->err = 0u; + ui_content_page_invalidate(); + return true; + } + + if (s_act->stage == 1u) { + bool ok = false; + + if (s_act->ok_cb != NULL) { + ok = s_act->ok_cb(); /* 只投递请求,不等待执行结果 */ + } + if (!ok) { + s_act->err = 1u; + ui_content_page_invalidate(); + return true; /* ERR 显示,再 ENTER 重试 */ + } + if (s_act->done_text == NULL) { + ui_content_page_close(); /* 成功即关页回菜单(无回报行形态) */ + return true; + } + s_act->stage = 2u; + s_act->err = 0u; + ui_content_page_invalidate(); + return true; + } + + /* stage 2:已回报,ENTER 返回 */ + ui_content_page_close(); + return true; +} + +/* + * 函数功能:开操作页(装载实例 + 复位 + 框架开页) + */ +bsp_err_t ui_page_action_open(ui_page_action_t *a_inst, uint8_t a_perm) { + if ((a_inst == NULL) || (a_inst->ok_cb == NULL) || (a_inst->prompt == NULL)) { + return BSP_ERR_PARAM; + } + if (ui_content_page_is_open() != false) { + return BSP_ERR_STATE; /* 框架单页互斥的显式化(失败原因对跳板可判) */ + } + + s_act = a_inst; + s_act->stage = 0u; /* 每次进入固定从一次确认开始 */ + s_act->err = 0u; + + (void)memset(&s_desc, 0, sizeof(s_desc)); + s_desc.id = "action"; + s_desc.title = (a_inst->title != NULL) ? a_inst->title : a_inst->prompt; + s_desc.hide_deco = 1u; /* 表单/操作页形态:无浅 V(用户密钥页先例) */ + s_desc.hide_footer = 1u; /* 无页脚页码 */ + s_desc.selectable = 0u; /* 无行列表 */ + s_desc.key_intercept = 0u; /* LEFT 不拦截:关页 = 取消 */ + s_desc.draw_cb = ac_draw_cb; + s_desc.key_cb = ac_key_cb; + + if (ui_content_page_open(&s_desc, a_perm) == false) { + s_act = NULL; /* 开页失败不残留实例(钩子不会再被调) */ + return BSP_ERR_NO_SPACE; + } + return BSP_OK; +} diff --git a/Drivers/BSP/UI/ui_page_action.h b/Drivers/BSP/UI/ui_page_action.h new file mode 100644 index 00000000..1af4af16 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_action.h @@ -0,0 +1,71 @@ +/* + * 模块名称:UI 动作确认页(操作页共享实现,ui_page_action) + * 模块功能:可复用的"确认 -> 二次确认 -> 执行回报"操作页构建器—— + * 强制复归 / 清除记录 / 手动录波等叶子共用同一页实现, + * 各自以一份实例(标题/问句/结果行/执行回调)开页; + * 执行回调只投递请求不阻塞(跨任务交接由页面跳板实现), + * 成功经 done_text 回报后再 ENTER 返回,done_text=NULL 成功即关页; + * 投递失败显示 ERR,下一次 ENTER 重新走二次确认 + * 适用范围:Drivers/BSP/UI(表单页形态:无浅 V 装饰、无页脚——用户密钥页 + * 跟随实机先例;ESC 恒关页 = 取消,LEFT 亦关页 = 取消) + * 说明:同一时刻仅一张操作页在用(内容页框架本就全局单页),模块以 + * static 单例描述符 + 当前实例指针承载钩子上下文;页面跳板 + * (批 2 各 ui_page_xxx)持 ui_page_action_t 实例并在自己的入口 + * 调 ui_page_action_open() + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道B task-5 三组件之一;规格 = + * 页面扩充详细方案.md §3.4 装置维护操作页 + + * §4.2 ui_page_action bind(title, prompt, ok_cb) + * 二次确认、ok_cb 只投递不阻塞) + */ +#ifndef UI_PAGE_ACTION_H +#define UI_PAGE_ACTION_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "bsp_err.h" /* bsp_err_t(开页结果错误通道——BSP 公开 API 约定) */ + +/* 执行回调:只投递请求(事件位/消息队列),立即返回; + * 返回值:true = 投递被接受(页面转成功回报),false = 拒收(页面显示 ERR) */ +typedef bool (*ui_page_action_ok_fn)(void); + +/* 操作页实例(配置字段 Flash 指针 + 运行态); + * 用法:页面跳板 static 一份、花括号初始化配置,开页经 ui_page_action_open */ +typedef struct { + const char *title; /* Flash:页标题(框架头部居中) */ + const char *prompt; /* Flash:确认问句(如"确认复归所有保护?"—— + 问句用字须已入字体子集) */ + const char *done_text; /* Flash:成功后结果行(如"已请求复归"); + NULL = 成功后直接关页回菜单 */ + ui_page_action_ok_fn ok_cb; /* 必填:执行投递 */ + uint8_t stage; /* 运行态:0 一次确认 / 1 二次确认 / 2 完成回报 */ + uint8_t err; /* 运行态:1 = 上轮投递被拒(显示 ERR) */ +} ui_page_action_t; + +/* + * 函数功能:开操作页(装载实例 + 复位确认态 + 经内容页框架开页) + * 入口参数:inst - 操作页实例(配置字段须已填好;ok_cb 不得为 NULL) + * ui_page_action_t * + * perm - 授予权限(ui_perm_t;由跳板决定传什么权限, + * 组件不拦权限只拦 ok_cb 为空) + * 返回值:BSP_OK = 已开页;BSP_ERR_PARAM = 参数无效; + * BSP_ERR_STATE = 已有页面打开(框架单页互斥); + * BSP_ERR_NO_SPACE = 框架开页失败(池不足等) bsp_err_t + * 限定条件:uiTask 上下文;每次进入从一次确认开始(不跨次保留 stage) + * 函数说明:键语义——ENTER 推进确认链(0->1->执行->2),ESC/LEFT 关页取消; + * stage=2 时 ENTER 关页返回 + */ +bsp_err_t ui_page_action_open(ui_page_action_t *a_inst, uint8_t a_perm); + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_ACTION_H */ diff --git a/Drivers/BSP/UI/ui_page_runlog.c b/Drivers/BSP/UI/ui_page_runlog.c index ff225724..3264ca98 100644 --- a/Drivers/BSP/UI/ui_page_runlog.c +++ b/Drivers/BSP/UI/ui_page_runlog.c @@ -1,8 +1,9 @@ /* * 模块名称:UI Page RunLog (运行记录内容页) * 模块功能:运行记录叶子节点(node 40)的内容页数据——动态列表,每条记录 2 行 - * (偶数行 = 时间戳、奇数行 = 描述),可移选分页;数据源 = sys_log API - * (/log/run.log,上电/重启事件);空日志显示单条伪行"无记录"; + * (偶数行 = 时间戳、奇数行 = 描述),可移选分页;数据经 ui_w_records + * 注入 reader 取自 sys_log(/log/run.log,上电/重启事件;本文件不再直接 + * include sys_log.h——task-6 分层收紧);空日志显示单条伪行"无记录"; * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 @@ -10,6 +11,9 @@ * 修改记录: * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §5.3) * v1.1 2026-10-20 王建锋 改用 ui_w_records 共享组件(W6 U2:合并两页重复实现) + * v1.2 2026-10-25 王建锋 记录 reader 注入(道B task-6):不再 include sys_log.h, + * 数据经组件内注入的 ui_rec_reader_t 取;日志 ID 换 + * 不透明编号 UI_W_REC_LOG_RUN(映射在两端绑定处) */ /* 头文件包含区 */ @@ -20,8 +24,6 @@ #include "ui_tokens.h" -#include "sys_log.h" - /* 静态变量定义区 */ /* 最近读取的记录缓存(偶数行读盘后,奇数行复用;避免同记录重复 I/O) */ @@ -39,7 +41,7 @@ * 实例 s_rec 携带日志 ID 与**本实例自己的**读盘缓存——两页共用组件代码, * 但缓存不得共享,否则切页会命中上一记录号而显示另一页的陈旧行。 */ -static ui_w_records_t s_rec = { SYSLOG_RUN }; +static ui_w_records_t s_rec = { UI_W_REC_LOG_RUN }; static uint16_t pg_count(void) { return ui_w_records_count(&s_rec); diff --git a/Drivers/BSP/UI/ui_page_selftest.c b/Drivers/BSP/UI/ui_page_selftest.c index d18b89c2..a61bf8c8 100644 --- a/Drivers/BSP/UI/ui_page_selftest.c +++ b/Drivers/BSP/UI/ui_page_selftest.c @@ -1,8 +1,9 @@ /* * 模块名称:UI Page SelfTest (自检记录内容页) * 模块功能:自检记录叶子节点(node 39)的内容页数据——动态列表,每条记录 2 行 - * (偶数行 = 时间戳、奇数行 = 故障描述),可移选分页;数据源 = sys_log API - * (/log/selftest.log,开机自检故障,仅故障才写);空日志显示单条伪行"无记录"; + * (偶数行 = 时间戳、奇数行 = 故障描述),可移选分页;数据经 ui_w_records + * 注入 reader 取自 sys_log(/log/selftest.log,仅故障才写;本文件不再直接 + * include sys_log.h——task-6 分层收紧);空日志显示单条伪行"无记录"; * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 @@ -10,6 +11,9 @@ * 修改记录: * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §5.4) * v1.1 2026-10-20 王建锋 改用 ui_w_records 共享组件(W6 U2:合并两页重复实现) + * v1.2 2026-10-25 王建锋 记录 reader 注入(道B task-6):不再 include sys_log.h, + * 数据经组件内注入的 ui_rec_reader_t 取;日志 ID 换 + * 不透明编号 UI_W_REC_LOG_SELFTEST(映射在两端绑定处) */ /* 头文件包含区 */ @@ -20,8 +24,6 @@ #include "ui_tokens.h" -#include "sys_log.h" - /* 静态变量定义区 */ /* 最近读取的记录缓存(偶数行读盘后,奇数行复用;避免同记录重复 I/O) */ @@ -39,7 +41,7 @@ * 实例 s_rec 携带日志 ID 与**本实例自己的**读盘缓存——两页共用组件代码, * 但缓存不得共享,否则切页会命中上一记录号而显示另一页的陈旧行。 */ -static ui_w_records_t s_rec = { SYSLOG_SELFTEST }; +static ui_w_records_t s_rec = { UI_W_REC_LOG_SELFTEST }; static uint16_t pg_count(void) { return ui_w_records_count(&s_rec); diff --git a/Drivers/BSP/UI/ui_w_editor.c b/Drivers/BSP/UI/ui_w_editor.c new file mode 100644 index 00000000..d773db40 --- /dev/null +++ b/Drivers/BSP/UI/ui_w_editor.c @@ -0,0 +1,398 @@ +/* + * 模块名称:UI 数值编辑共享组件(ui_w_editor)实现 + * 模块功能:见 ui_w_editor.h 头注释(逐位步进编辑 + 范围校验 + 光标反显绘制) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道B task-5 三组件之一;编辑交互对齐 + * ui_page_net_cfg 的"反显光标 + 键全消费"模式, + * 差异:字节 -> 十进制数字位、0..255 环绕 -> 0..9 + * 环绕、新增 min/max 提交把关) + */ + +/* 头文件包含区 */ +#include + +#include "ui_w_editor.h" + +#include "ui_tokens.h" + +/* 私有宏定义区 */ + +#define ED_MAX_DECIMALS 3u /* 定点小数位上限(decimals 有效域) */ +#define ED_HINT_GAP 4 /* "越界"提示与值串末尾的水平间隙(px) */ +#define ED_SAFE_POWER 7u /* 参与步进的数字位最大权值指数(10^7;再高 + 环绕步进可致 int32 溢出,该位锁定不动—— + 工程定值值串最长 6 位,正常编辑不触发) */ + +/* 函数定义区 */ + +/* + * 函数功能:10^n 幂(n <= ED_SAFE_POWER+1,调用侧已限定;用循环不用查表越界) + * 入口参数:n - 指数 uint8_t + * 返回值:10^n(n 超界时饱和返回最大值) uint32_t + * 限定条件:无 + */ +static uint32_t ed_pow10(uint8_t a_n) { + uint32_t p = 1u; + uint8_t i; + + for (i = 0u; (i < a_n) && (i < 9u); i++) { + p *= 10u; + } + return p; +} + +/* + * 函数功能:工作值 -> 显示串("500" / "10.24" / "0.001") + * 入口参数:inst - 编辑器实例 const ui_w_editor_t * + * buf - 输出缓冲 char * + * len - 缓冲长度 uint32_t + * 返回值:无 + * 限定条件:buf 非空;值非负(min >= 0 使用前提);decimals <= ED_MAX_DECIMALS + * 函数说明:不用 %*d 变宽(MicroLIB 支持性存疑),小数位 0..3 逐档定式; + * lv_snprintf 而非 snprintf(工程口径,且避开 MSVC C4819 格式串扫描) + */ +void ui_w_editor_fmt_scaled(int32_t a_value, uint8_t a_decimals, char *a_buf, uint32_t a_len) { + uint32_t scale = ed_pow10(a_decimals); + int32_t whole = a_value / (int32_t)scale; + int32_t frac = a_value % (int32_t)scale; + + switch (a_decimals) { + case 1u: + (void)lv_snprintf(a_buf, a_len, "%d.%d", (int)whole, (int)frac); + break; + case 2u: + (void)lv_snprintf(a_buf, a_len, "%d.%02d", (int)whole, (int)frac); + break; + case 3u: + (void)lv_snprintf(a_buf, a_len, "%d.%03d", (int)whole, (int)frac); + break; + default: + (void)lv_snprintf(a_buf, a_len, "%d", (int)whole); + break; + } +} + +/* + * 函数功能:工作值 -> 显示串(实例壳:转调 ui_w_editor_fmt_scaled) + */ +static void ed_format(const ui_w_editor_t *a_inst, char *a_buf, uint32_t a_len) { + ui_w_editor_fmt_scaled(a_inst->cur, a_inst->decimals, a_buf, a_len); +} + +/* + * 函数功能:扫描值串,定位第 inst->digit 个数字字符并给出该位的定点权值 + * 入口参数:inst - 编辑器实例(digit/decimals 有效) const ui_w_editor_t * + * buf - 值串(与 inst->cur 同步,ed_format 产物) const char * + * step_out - 输出:该位 +1 对 cur 的增量(= 10^权值指数) uint32_t * + * 返回值:该数字字符在串中的下标(无数字时 0) uint32_t + * 限定条件:inst->digit < inst->ndigits(调用方经 begin/recount 保证) + * 函数说明:权值指数 = 该位右侧的小数位数 + 更低的整数位数: + * 整数位:dec + (整数位数 - 1 - 位序);小数位:dec - 1 - 小数位序。 + * 例 dec=2 值串 "12.34":'1'->1000 '2'->100 '3'->10 '4'->1 + */ +static uint32_t ed_digit_info(const ui_w_editor_t *a_inst, const char *a_buf, + uint32_t *a_step_out) { + uint32_t total = (uint32_t)strlen(a_buf); + uint32_t dot = total; /* 小数点下标(total = 无小数点) */ + uint32_t i; + uint32_t seen = 0u; /* 已扫过的数字字符数(= 当前数字的位序) */ + uint32_t int_pos = 0u; /* 整数位内位序 */ + uint32_t frac_pos = 0u; /* 小数位内位序 */ + + if (a_step_out != NULL) { + *a_step_out = 1u; + } + + for (i = 0u; i < total; i++) { + if (a_buf[i] == '.') { + dot = i; + break; + } + } + + for (i = 0u; i < total; i++) { + char c = a_buf[i]; + + if ((c < '0') || (c > '9')) { + continue; + } + if (seen == (uint32_t)a_inst->digit) { + uint32_t power; + + if (i < dot) { + uint32_t n_int = 0u; /* 整数位数字个数(求该位权值用) */ + uint32_t j; + + for (j = 0u; j < dot; j++) { + if ((a_buf[j] >= '0') && (a_buf[j] <= '9')) { + n_int++; + } + } + power = (uint32_t)a_inst->decimals + (n_int - 1u) - int_pos; + } + else { + power = ((uint32_t)a_inst->decimals - 1u) - frac_pos; + } + if (a_step_out != NULL) { + *a_step_out = (power > ED_SAFE_POWER) ? 0u : ed_pow10((uint8_t)power); + } + return i; + } + if (i < dot) { + int_pos++; + } + else { + frac_pos++; + } + seen++; + } + return 0u; +} + +/* + * 函数功能:重算 ndigits 并把数字光标夹回有效域(值位数随步进变化时防指空) + * 入口参数:inst - 编辑器实例 ui_w_editor_t * + * 返回值:无 + * 限定条件:inst->cur 已更新 + */ +static void ed_recount(ui_w_editor_t *a_inst) { + char buf[UI_BUF_STRLEN]; + uint32_t i; + uint8_t n = 0u; + + ed_format(a_inst, buf, sizeof(buf)); + for (i = 0u; i < (uint32_t)strlen(buf); i++) { + if ((buf[i] >= '0') && (buf[i] <= '9')) { + n++; + } + } + a_inst->ndigits = n; + if (a_inst->digit >= n) { + a_inst->digit = (n > 0u) ? (uint8_t)(n - 1u) : 0u; + } +} + +/* + * 函数功能:复位实例(清除编辑态与提示) + */ +void ui_w_editor_reset(ui_w_editor_t *a_inst) { + a_inst->editing = 0u; + a_inst->bad = 0u; + a_inst->digit = 0u; + a_inst->ndigits = 0u; +} + +/* + * 函数功能:进入编辑(载入初始值与范围,光标落在个位) + */ +void ui_w_editor_begin(ui_w_editor_t *a_inst, int32_t a_value, + int32_t a_min_v, int32_t a_max_v, uint8_t a_decimals) { + char buf[UI_BUF_STRLEN]; + uint32_t i; + uint8_t int_digits = 0u; + + ui_w_editor_reset(a_inst); + + a_inst->cur = (a_value < 0) ? 0 : a_value; /* 非负前提(防御钳 0) */ + a_inst->min = (a_min_v < 0) ? 0 : a_min_v; + a_inst->max = (a_max_v < a_inst->min) ? a_inst->min : a_max_v; + a_inst->decimals = (a_decimals > ED_MAX_DECIMALS) ? (uint8_t)ED_MAX_DECIMALS : a_decimals; + + if (a_inst->cur > a_inst->max) { + a_inst->cur = a_inst->max; /* 起点必须合法:个位光标才落在实位上 */ + } + if (a_inst->cur < a_inst->min) { + a_inst->cur = a_inst->min; + } + + /* 光标落个位 = 第 (整数位数 - 1) 个数字字符 */ + ed_format(a_inst, buf, sizeof(buf)); + for (i = 0u; i < (uint32_t)strlen(buf); i++) { + if (buf[i] == '.') { + break; + } + if ((buf[i] >= '0') && (buf[i] <= '9')) { + int_digits++; + } + } + a_inst->digit = (int_digits > 0u) ? (uint8_t)(int_digits - 1u) : 0u; + ed_recount(a_inst); + a_inst->editing = 1u; +} + +/* + * 函数功能:查询是否处于编辑态 + */ +bool ui_w_editor_is_editing(const ui_w_editor_t *a_inst) { + return (a_inst != NULL) && (a_inst->editing != 0u); +} + +/* + * 函数功能:编辑态按键处理 + */ +uint8_t ui_w_editor_key(ui_w_editor_t *a_inst, lv_key_t a_key) { + if ((a_inst == NULL) || (a_inst->editing == 0u)) { + return 0u; + } + + a_inst->bad = 0u; /* 任意编辑键先清"越界"提示 */ + + switch (a_key) { + case LV_KEY_UP: + case LV_KEY_DOWN: { + char buf[UI_BUF_STRLEN]; + uint32_t step; + int32_t dcur; + int32_t dnew; + + ed_format(a_inst, buf, sizeof(buf)); + (void)ed_digit_info(a_inst, buf, &step); + if (step == 0u) { + return 1u; /* 超安全权值位(ED_SAFE_POWER 之上):锁定该位 */ + } + dcur = (a_inst->cur / (int32_t)step) % 10; + if (dcur < 0) { + dcur += 10; /* 非负前提下不触发;双保险 */ + } + dnew = (a_key == LV_KEY_UP) ? ((dcur + 1) % 10) : ((dcur + 9) % 10); + a_inst->cur += (dnew - dcur) * (int32_t)step; + if (a_inst->cur < 0) { + a_inst->cur = 0; /* 双保险(0 减 1 走 dnew=9 环绕,本不应为负) */ + } + ed_recount(a_inst); + return 1u; + } + case LV_KEY_LEFT: + if (a_inst->ndigits > 0u) { + a_inst->digit = (uint8_t)((a_inst->digit + a_inst->ndigits - 1u) % a_inst->ndigits); + } + return 1u; + case LV_KEY_RIGHT: + if (a_inst->ndigits > 0u) { + a_inst->digit = (uint8_t)((a_inst->digit + 1u) % a_inst->ndigits); + } + return 1u; + case LV_KEY_ENTER: + if ((a_inst->cur < a_inst->min) || (a_inst->cur > a_inst->max)) { + a_inst->bad = 1u; /* 越界:拒绝提交,留在编辑态显示"越界" */ + return 1u; + } + a_inst->editing = 0u; /* 提交:调用方经 ui_w_editor_value 取回写 RAM 副本 */ + return 1u; + default: + return 0u; /* ESC 到不了这里(框架恒拦截关页 = 取消编辑) */ + } +} + +/* + * 函数功能:取工作值 + */ +int32_t ui_w_editor_value(const ui_w_editor_t *a_inst) { + return (a_inst != NULL) ? a_inst->cur : 0; +} + +/* + * 函数功能:把工作值格式化为显示串(对外壳,供 refresh 值列复用) + */ +void ui_w_editor_format(const ui_w_editor_t *a_inst, char *a_buf, uint32_t a_len) { + if ((a_inst != NULL) && (a_buf != NULL) && (a_len > 0u)) { + ed_format(a_inst, a_buf, a_len); + } +} + +/* + * 函数功能:判断两区域相交(与本目录各绘制文件同实现:static 局部、不跨文件共享) + * 入口参数:a - 区域 1 const lv_area_t * + * b - 区域 2 const lv_area_t * + * 返回值:true = 相交 bool + * 限定条件:无 + */ +static bool ed_areas_intersect(const lv_area_t *a, const lv_area_t *b) { + int32_t x1 = (a->x1 > b->x1) ? a->x1 : b->x1; + int32_t y1 = (a->y1 > b->y1) ? a->y1 : b->y1; + int32_t x2 = (a->x2 < b->x2) ? a->x2 : b->x2; + int32_t y2 = (a->y2 < b->y2) ? a->y2 : b->y2; + + return (x1 <= x2 && y1 <= y2); +} + +/* + * 函数功能:编辑态自绘(数字光标反显盒 + "越界"提示) + */ +void ui_w_editor_draw(ui_w_editor_t *a_inst, lv_layer_t *a_layer, + const lv_area_t *a_clip, int32_t a_row_top, + int32_t a_text_dy, int32_t a_band_h, int32_t a_value_x) { + char buf[UI_BUF_STRLEN]; + uint32_t step; + uint32_t cidx; + int32_t x0; + int32_t w; + lv_area_t box; + + if ((a_inst == NULL) || (a_inst->editing == 0u) || (a_inst->ndigits == 0u)) { + return; + } + + ed_format(a_inst, buf, sizeof(buf)); + cidx = ed_digit_info(a_inst, buf, &step); + + /* 光标反显盒:框住第 digit 个数字字符(左右各外扩 1px,net_cfg 同法) */ + x0 = a_value_x + (int32_t)lv_text_get_width(buf, cidx, LV_FONT_DEFAULT, 0); + w = (int32_t)lv_text_get_width(buf + cidx, 1u, LV_FONT_DEFAULT, 0); + + box.x1 = x0 - 1; + box.x2 = x0 + w; + box.y1 = a_row_top; + box.y2 = a_row_top + a_band_h - 1; + if (ed_areas_intersect(&box, a_clip)) { + lv_draw_rect_dsc_t fd; + lv_draw_label_dsc_t ld; + char seg[2]; + + lv_draw_rect_dsc_init(&fd); + fd.base.layer = a_layer; + fd.bg_color = UI_COL_SEL_BG; + fd.bg_opa = LV_OPA_COVER; + lv_draw_rect(a_layer, &fd, &box); + + /* 只画光标单字符(整串会按盒宽折行溢出邻行——net_cfg 实测坑); + * 编辑行恒为选中行(白带),反显字色取黑 UI_COL_SEL_FG */ + seg[0] = buf[cidx]; + seg[1] = '\0'; + + lv_draw_label_dsc_init(&ld); + ld.base.layer = a_layer; + ld.text = seg; + ld.text_static = 0u; /* 栈缓冲(同帧有效) */ + ld.color = UI_COL_SEL_FG; + ld.ofs_x = 1; /* box.x1 = x0-1,+1 回 x0(与框架行文本原点一致) */ + ld.ofs_y = a_text_dy; + lv_draw_label(a_layer, &ld, &box); + } + + /* "越界"提示:值串右侧(编辑行 = 选中白带行 -> 黑字可见) */ + if (a_inst->bad != 0u) { + lv_area_t hb; + lv_draw_label_dsc_t ld; + + hb.x1 = a_value_x + + (int32_t)lv_text_get_width(buf, (uint32_t)strlen(buf), LV_FONT_DEFAULT, 0) + + ED_HINT_GAP; + hb.x2 = UI_SCREEN_W - 1; + hb.y1 = a_row_top; + hb.y2 = a_row_top + a_band_h - 1; + if (ed_areas_intersect(&hb, a_clip)) { + lv_draw_label_dsc_init(&ld); + ld.base.layer = a_layer; + ld.text = "越界"; + ld.text_static = 1u; + ld.color = UI_COL_SEL_FG; /* 白带内黑字(1bpp:black = 亮墨底反) */ + ld.ofs_x = 0; + ld.ofs_y = a_text_dy; + lv_draw_label(a_layer, &ld, &hb); + } + } +} diff --git a/Drivers/BSP/UI/ui_w_editor.h b/Drivers/BSP/UI/ui_w_editor.h new file mode 100644 index 00000000..91dfb8fb --- /dev/null +++ b/Drivers/BSP/UI/ui_w_editor.h @@ -0,0 +1,151 @@ +/* + * 模块名称:UI 数值编辑共享组件(ui_w_editor) + * 模块功能:把"定点整数值(单位 = 10^decimals)的逐位编辑"收成一份可复用实现—— + * 进/退编辑、按光标位 ±1 步进(上加下减)、左右移数字光标(首末环绕)、 + * ENTER 提交(范围校验:越界拒绝并显示"越界"提示、留在编辑态); + * ESC 不处理(内容页框架恒拦截关页,未提交调整随之丢弃 = 取消); + * 编辑光标反显绘制与 ui_page_net_cfg 的字节光标同一画法; + * 只持渲染与输入抽象,不碰任何数据源——工作值经 ui_w_editor_value() + * 交回调用方(ui_w_setlist)统一写 RAM 副本 + * 适用范围:Drivers/BSP/UI(列表页骨架几何由调用方传入,组件不自带) + * 说明:实例化使用(一个实例同一时刻只编一个值),调用方持有 ui_w_editor_t + * 实例——多个整定页各自独立实例,不跨页串态(同 ui_w_records 的 + * 按实例存态教训);值一律非负(min >= 0 为使用前提,本工程定值全非负) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(并行分道方案 §5.2 道B task-5:定值整定的 + * 数值编辑前置能力,规格 = 页面扩充详细方案.md §3.2.7 + * 编辑交互表 + 页面补齐规划.md §3.2 能力表) + */ +#ifndef UI_W_EDITOR_H +#define UI_W_EDITOR_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "lvgl.h" + +/* 数值编辑器实例(值用定点整数表达:单位 = 10^decimals, + * 如 10.24 A / decimals=2 -> 1024) + * 用法:调用方(页面 / ui_w_setlist)持有实例,进编辑前 begin() 载值与范围, + * 其余字段清零 = 非编辑态 */ +typedef struct { + int32_t min; /* 下限(含;单位 10^decimals) */ + int32_t max; /* 上限(含) */ + int32_t cur; /* 工作值(编辑中实时变;未提交不回写数据源) */ + uint8_t decimals; /* 小数位数 0..3 */ + uint8_t editing; /* 1 = 编辑中 */ + uint8_t bad; /* 1 = 上次 ENTER 越界被拒("越界"提示亮,下一次编辑键清除) */ + uint8_t digit; /* 数字光标:第几个十进制数字字符(0 = 最高位) */ + uint8_t ndigits; /* 当前值串的数字字符数(begin/步进时重算) */ +} ui_w_editor_t; + +/* + * 函数功能:进入编辑(载入初始值与范围,光标落在个位) + * 入口参数:inst - 编辑器实例 ui_w_editor_t * + * value - 初始值(单位 10^decimals) int32_t + * min_v - 下限(含;>= 0) int32_t + * max_v - 上限(含) int32_t + * decimals - 小数位数 0..3 uint8_t + * 返回值:无 + * 限定条件:非编辑态调用;编辑中重复调用 = 先复位再进新值的编辑 + */ +void ui_w_editor_begin(ui_w_editor_t *a_inst, int32_t a_value, + int32_t a_min_v, int32_t a_max_v, uint8_t a_decimals); + +/* + * 函数功能:复位实例(清除编辑态与提示;页面 init_cb 开页时调用) + * 入口参数:inst - 编辑器实例 ui_w_editor_t * + * 返回值:无 + * 限定条件:无 + */ +void ui_w_editor_reset(ui_w_editor_t *a_inst); + +/* + * 函数功能:查询是否处于编辑态 + * 入口参数:inst - 编辑器实例 const ui_w_editor_t * + * 返回值:true = 编辑中 bool + * 限定条件:inst 非空(空按非编辑态处理) + */ +bool ui_w_editor_is_editing(const ui_w_editor_t *a_inst); + +/* + * 函数功能:编辑态按键处理 + * 入口参数:inst - 编辑器实例 ui_w_editor_t * + * key - LVGL 键值 lv_key_t + * 返回值:1 = 已消费(上下步进 / 左右移光标 / ENTER 提交或拒绝); + * 0 = 未消费(非编辑态调用,或方向键与 ENTER 之外的键) + * (键分发标志非错误通道,取 uint8_t 不用 bool——BSP 公开 + * API 约定,见 bsp-err/err-type) + * 限定条件:非编辑态调用本函数恒返回 false(编辑态把守由调用方负责) + * 函数说明:1. 上下:当前光标位数字 +1/-1(0 与 9 环绕、无进位——越界由 ENTER 把关) + * 2. 左右:数字光标首末环绕 + * 3. ENTER:范围校验通过 -> 退出编辑(提交值经 ui_w_editor_value 取回, + * 写回 RAM 副本由调用方负责);越界 -> 拒绝退出、置"越界"提示, + * 下一次任意编辑键先清提示 + * 4. ESC 不到达本函数(内容页框架恒拦截关页 = 取消本次编辑, + * 工作值随 begin 丢弃,不落任何数据源) + */ +uint8_t ui_w_editor_key(ui_w_editor_t *a_inst, lv_key_t a_key); + +/* + * 函数功能:取工作值 + * 入口参数:inst - 编辑器实例 const ui_w_editor_t * + * 返回值:当前工作值(单位 10^decimals;inst 为空返回 0——直返数值形态, + * 无错误通道) int32_t + * 限定条件:编辑中与提交后均可取(提交判定 = 调用方观察 is_editing 变 false) + */ +int32_t ui_w_editor_value(const ui_w_editor_t *a_inst); + +/* + * 函数功能:把当前工作值格式化为显示串("10.24" / "500") + * 入口参数:inst - 编辑器实例 const ui_w_editor_t * + * buf - 输出缓冲(建议 >= UI_BUF_STRLEN) char * + * len - 缓冲长度(含 NUL) uint32_t + * 返回值:无 + * 限定条件:buf 非空;decimals <= 3 + * 函数说明:与内部绘制共用同一格式化口径(刷新值列与光标定位同串一致) + */ +void ui_w_editor_format(const ui_w_editor_t *a_inst, char *a_buf, uint32_t a_len); + +/* + * 函数功能:把任意定点整数值格式化为显示串(共享格式化单一规则源: + * 组件内部 ed_format 与本函数同一实现,值列显示与编辑光标天然同串) + * 入口参数:value - 定点值(单位 10^decimals;非负) int32_t + * decimals - 小数位数 0..3 uint8_t + * buf - 输出缓冲 char * + * len - 缓冲长度(含 NUL) uint32_t + * 返回值:无 + * 限定条件:buf 非空 + */ +void ui_w_editor_fmt_scaled(int32_t a_value, uint8_t a_decimals, char *a_buf, uint32_t a_len); + +/* + * 函数功能:编辑态自绘(数字光标反显盒 + "越界"提示),在框架内置区块之后调用 + * 入口参数:inst - 编辑器实例 ui_w_editor_t * + * layer - 绘制层 lv_layer_t * + * clip - 重绘区 const lv_area_t * + * row_top - 被编辑行行槽顶 y(调用方按列表骨架 + 当前页窗口换算) int32_t + * text_dy - 行文本垂直偏移(= 描述符 text_dy,通常 0) int32_t + * band_h - 选中白带高(反显盒高沿用,= 描述符 band_h) int32_t + * value_x - 值列 x(= 描述符 right_x) int32_t + * 返回值:无 + * 限定条件:编辑中才绘制(非编辑态空操作);被编辑行在当前页窗口内由调用方保证 + * 函数说明:1bpp 极性:反显 = 白底(UI_COL_SEL_BG)盒 + 黑字(UI_COL_SEL_FG) + * 重画光标位单字符(与 ui_page_net_cfg 编辑光标同法,含 culling; + * 只画单字符:整串传入会被 LVGL 按盒宽折行溢出到邻行——net_cfg 实测坑) + */ +void ui_w_editor_draw(ui_w_editor_t *a_inst, lv_layer_t *a_layer, + const lv_area_t *a_clip, int32_t a_row_top, + int32_t a_text_dy, int32_t a_band_h, int32_t a_value_x); + +#ifdef __cplusplus +} +#endif + +#endif /* UI_W_EDITOR_H */ diff --git a/Drivers/BSP/UI/ui_w_records.c b/Drivers/BSP/UI/ui_w_records.c index d31fcbf8..6b5566ac 100644 --- a/Drivers/BSP/UI/ui_w_records.c +++ b/Drivers/BSP/UI/ui_w_records.c @@ -1,12 +1,14 @@ /* * 模块名称:UI 记录页共享组件实现 * 模块功能:见 ui_w_records.h(逻辑照搬合并前的 ui_page_runlog.c / ui_page_selftest.c, - * 两文件差异仅为日志 ID 与符号名,其余逐字相同) + * 两文件差异仅为记录源编号与符号名,其余逐字相同;数据经注入 reader 取) * 适用范围:Drivers/BSP/UI * 作者:王建锋 * 创建日期:2026-10-20 * 修改记录: * v1.0 2026-10-20 王建锋 创建(W6 U2) + * v2.0 2026-10-25 王建锋 reader 注入(道B task-6):syslog_count/syslog_get_record + * 直调改为 s_reader 函数表调用;未注入按空态渲染 */ #include @@ -15,15 +17,29 @@ #include "ui_tokens.h" +/* 静态变量定义区 */ + +/* 注入的记录 reader(模块级单例,理由见头注;NULL = 未注入 → 空态) */ +static const ui_rec_reader_t *s_reader; + /* 函数定义区 */ +/* + * 函数功能:注入记录 reader + */ +void ui_w_records_bind(const ui_rec_reader_t *a_reader) { + s_reader = a_reader; +} + /* * 函数功能:总行数(动态;每条记录 2 行,空日志 = 单条伪行"无记录") * 入口参数:a_inst - 记录页实例 ui_w_records_t * * 返回值:总行数 uint16_t + * 函数说明:未注入 reader 视同 0 条(空态伪行),两端启动期绑定前的开页窗口 + * 也被覆盖(防御不崩) */ uint16_t ui_w_records_count(ui_w_records_t *a_inst) { - uint32_t n = syslog_count(a_inst->id); + uint32_t n = (s_reader != NULL) ? s_reader->count(a_inst->log_id) : 0u; if (n == 0u) { return 1u; /* 空态伪行(页脚"共 1 页",避免"共 0 页") */ @@ -40,7 +56,7 @@ uint16_t ui_w_records_count(ui_w_records_t *a_inst) { * 限定条件:页面打开中(框架绘制/按键上下文,uiTask) */ void ui_w_records_refresh(ui_w_records_t *a_inst, uint16_t a_row, char *a_name, char *a_value) { - uint32_t n = syslog_count(a_inst->id); + uint32_t n = (s_reader != NULL) ? s_reader->count(a_inst->log_id) : 0u; a_value[0] = '\0'; if (n == 0u) { @@ -52,8 +68,9 @@ void ui_w_records_refresh(ui_w_records_t *a_inst, uint16_t a_row, char *a_name, uint32_t rec = a_row / 2u; if (!a_inst->cache_valid || rec != a_inst->last_rec) { - if (syslog_get_record(a_inst->id, rec, a_inst->ts, sizeof(a_inst->ts), - a_inst->desc, sizeof(a_inst->desc)) != 0) { + if ((s_reader->get)(a_inst->log_id, rec, a_inst->ts, sizeof(a_inst->ts), + a_inst->desc, sizeof(a_inst->desc)) != 0) { + a_inst->cache_valid = 0u; a_name[0] = '\0'; /* 读失败(越界/存储异常):空行防御 */ return; } diff --git a/Drivers/BSP/UI/ui_w_records.h b/Drivers/BSP/UI/ui_w_records.h index 80eabc84..fd9c16ae 100644 --- a/Drivers/BSP/UI/ui_w_records.h +++ b/Drivers/BSP/UI/ui_w_records.h @@ -1,14 +1,21 @@ /* * 模块名称:UI 记录页共享组件(运行记录 / 自检记录两页的公共实现) * 模块功能:把"定长记录 -> 每记录 2 行(偶数行时间戳、奇数行描述)+ 同记录读盘缓存 - * + 空态单条伪行"这套逻辑收成一份,供多个日志页按实例复用 - * 适用范围:Drivers/BSP/UI(仅 UI 层,数据源固定为 sys_log API) + * + 空态单条伪行"这套逻辑收成一份,供多个日志页按实例复用; + * 数据经注入式 reader 回调取(UI_数据供给设计说明.md §7.2 契约), + * 本头**不含任何 App 头**——记录属主 sys_log 留在 App 侧,启动时由 + * App/task/ui_task.c(MCU)与 simulator/main.c(PC)各注入一份薄包装 + * 适用范围:Drivers/BSP/UI(仅 UI 层;分层方向由 check_layer_deps 门禁守护) * 说明:缓存必须按实例存放而非文件级静态——两页共用代码但各有独立实例, * 若共享缓存,从 A 页切到 B 页会命中上一记录号而显示另一页的陈旧行(静默错数据)。 * 作者:王建锋 * 创建日期:2026-10-20 * 修改记录: * v1.0 2026-10-20 王建锋 创建(W6 U2:合并 ui_page_runlog / ui_page_selftest 的重复实现) + * v2.0 2026-10-25 王建锋 记录 reader 注入(道B task-6):删 #include "sys_log.h", + * 改 ui_rec_reader_t 函数表 + ui_w_records_bind(); + * log_id 换不透明编号(UI_W_REC_LOG_*);缓冲容量宏本地化 + * (与 sys_log 契约下限同宽,逐字节一致) */ #ifndef UI_W_RECORDS_H @@ -20,34 +27,83 @@ extern "C" { #include -#include "sys_log.h" +/* 记录源不透明编号:UI 侧只认"0 号 / 1 号日志",App 侧适配层映射到 syslog_id_t + * (映射表见 App/task/ui_task.c 与 simulator/main.c 的绑定处;新增记录页 = + * 这里加编号 + 两端适配层各加一条映射) */ +#define UI_W_REC_LOG_RUN 0u /* 0 号:运行记录(MCU /log/run.log) */ +#define UI_W_REC_LOG_SELFTEST 1u /* 1 号:自检记录(MCU /log/selftest.log) */ + +/* 读取缓冲容量(UI 侧定义;必须 >= sys_log 契约下限:时间戳 19+NUL、描述 28+NUL。 + * sys_log 侧 SYSLOG_TS_LEN/SYSLOG_DESC_MAX 变化时这里同步加宽——容量不足时 + * reader 按契约返回负码、页面退化空行,不会读越界) */ +#define UI_W_REC_TS_LEN 20u /* "YYYY-MM-DD HH:MM:SS" 19 + NUL */ +#define UI_W_REC_DESC_LEN 29u /* 描述 28 + NUL */ /* - * 记录页实例:日志源 + 该实例自己的读盘缓存 - * 用法:页面文件里 static ui_w_records_t s_rec = { SYSLOG_XXX }; + * 注入式记录 reader(只读函数表;契约 = UI_数据供给设计说明.md §7.2) + * 实现体在 App / 模拟器侧,uiTask 上下文同步调用——记录页绘制路径上的读盘 + * 时序与合并前直调 syslog_* 完全一致,无新增并发面 + */ +typedef struct { + /* + * 函数功能:取该日志当前记录条数 + * 入口参数:log_id - 不透明记录源编号(UI_W_REC_LOG_*) uint8_t + * 返回值:记录条数(0 = 空态,页面显示伪行"无记录") uint16_t + */ + uint16_t (*count)(uint8_t a_log_id); + /* + * 函数功能:读第 idx 条记录(时间戳 + 描述两字段) + * 入口参数:log_id - 记录源编号 uint8_t + * idx - 记录序号(0 起) uint32_t + * ts - 时间戳缓冲 char * + * ts_len - ts 容量(>= UI_W_REC_TS_LEN) uint32_t + * desc - 描述缓冲 char * + * desc_len - desc 容量(>= UI_W_REC_DESC_LEN)uint32_t + * 返回值:0 = 成功;-1 = 序号/编号越界;-2 = 读失败 + * (与 sys_log 契约同值,适配层直通不改码) int + */ + int (*get)(uint8_t a_log_id, uint32_t a_idx, + char *a_ts, uint32_t a_ts_len, char *a_desc, uint32_t a_desc_len); +} ui_rec_reader_t; + +/* + * 记录页实例:记录源编号 + 该实例自己的读盘缓存 + * 用法:页面文件里 static ui_w_records_t s_rec = { UI_W_REC_LOG_xxx }; * 其余字段自动清零,即"缓存初始无效",与合并前两页的行为一致。 */ typedef struct { - syslog_id_t id; /* 日志源 ID(决定读哪个记录文件) */ - uint8_t cache_valid; /* 缓存有效标志(0 = 无缓存) */ - uint32_t last_rec; /* 已缓存的记录序号 */ - char ts[SYSLOG_TS_LEN + 1u]; /* 时间戳缓存(含 NUL) */ - char desc[SYSLOG_DESC_MAX + 1u];/* 描述缓存(含 NUL) */ + uint8_t log_id; /* 记录源编号(UI_W_REC_LOG_*,透传给 reader) */ + uint8_t cache_valid; /* 缓存有效标志(0 = 无缓存) */ + uint32_t last_rec; /* 已缓存的记录序号 */ + char ts[UI_W_REC_TS_LEN]; /* 时间戳缓存(含 NUL) */ + char desc[UI_W_REC_DESC_LEN];/* 描述缓存(含 NUL) */ } ui_w_records_t; +/* + * 函数功能:注入记录 reader(启动时一次;NULL = 撤销注入,页面回到空态) + * 入口参数:reader - 只读函数表(生命周期由注入方保证,组件只存指针) + * const ui_rec_reader_t * + * 返回值:无 + * 限定条件:uiTask 启动期注入(ui_menu_init 之前,UI_数据供给设计说明.md §7.2); + * 未注入时组件按"空态"渲染(伪行"无记录"),不崩不读野 + * 函数说明:模块级单例——sys_log 读 API 本就是全局的,两端各只有一份实现; + * 两页共享函数表、独享各自的读盘缓存(缓存才是必须按实例存的那半) + */ +void ui_w_records_bind(const ui_rec_reader_t *a_reader); + /* * 函数功能:总行数(动态;每条记录 2 行,空日志 = 单条伪行"无记录") - * 入口参数:inst - 记录页实例 ui_w_records_t * + * 入口参数:a_inst - 记录页实例 ui_w_records_t * * 返回值:总行数 uint16_t */ uint16_t ui_w_records_count(ui_w_records_t *a_inst); /* * 函数功能:逐行取数(偶数行 = 时间戳、奇数行 = 描述;值列恒空) - * 入口参数:inst - 记录页实例 ui_w_records_t * - * row - 全局行下标 uint16_t - * name - 名输出缓冲(框架栈缓冲 UI_BUF_STRLEN=32,含 NUL) char * - * value - 值输出缓冲(同上;本组件恒写空串) char * + * 入口参数:a_inst - 记录页实例 ui_w_records_t * + * a_row - 全局行下标 uint16_t + * a_name - 名输出缓冲(框架栈缓冲 UI_BUF_STRLEN=32,含 NUL) char * + * a_value- 值输出缓冲(同上;本组件恒写空串) char * * 限定条件:页面打开中(框架绘制/按键上下文,uiTask) */ void ui_w_records_refresh(ui_w_records_t *a_inst, uint16_t a_row, char *a_name, char *a_value); diff --git a/Drivers/BSP/UI/ui_w_setlist.c b/Drivers/BSP/UI/ui_w_setlist.c new file mode 100644 index 00000000..27c67d17 --- /dev/null +++ b/Drivers/BSP/UI/ui_w_setlist.c @@ -0,0 +1,326 @@ +/* + * 模块名称:UI 定值表共享组件(ui_w_setlist)实现 + * 模块功能:见 ui_w_setlist.h 头注释(四视图共用列表渲染 + 整定输入抽象, + * 数据经回调注入,数值行走 ui_w_editor,二态行 ENTER 循环切换) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道B task-5 三组件之一) + */ + +/* 头文件包含区 */ +#include + +#include "ui_w_setlist.h" + +#include "ui_tokens.h" + +/* 私有宏定义区 */ + +#define SL_SAVE_NAME "保存" /* 保存行名(字体子集已含 保/存) */ +#define SL_WIN_ROWS UI_LIST_ROWS /* 窗口行数(与框架 rows_per_page 同令牌) */ + +/* 函数定义区 */ + +/* + * 函数功能:判断两区域相交(与本目录各绘制文件同实现:static 局部) + * 入口参数:a - 区域 1 const lv_area_t * + * b - 区域 2 const lv_area_t * + * 返回值:true = 相交 bool + * 限定条件:无 + */ +static bool sl_areas_intersect(const lv_area_t *a, const lv_area_t *b) { + int32_t x1 = (a->x1 > b->x1) ? a->x1 : b->x1; + int32_t y1 = (a->y1 > b->y1) ? a->y1 : b->y1; + int32_t x2 = (a->x2 < b->x2) ? a->x2 : b->x2; + int32_t y2 = (a->y2 < b->y2) ? a->y2 : b->y2; + + return (x1 <= x2 && y1 <= y2); +} + +/* + * 函数功能:整定视图判定(挂了提交回调 = 有保存行、可编辑) + * 入口参数:inst - 定值表实例 const ui_w_setlist_t * + * 返回值:true = 整定视图 bool + * 限定条件:inst 非空由调用链保证 + */ +static bool sl_is_rw(const ui_w_setlist_t *a_inst) { + return (a_inst->commit != NULL); +} + +/* + * 函数功能:二态行串表长度(NULL 结尾) + * 入口参数:opts - 串表 const char *const * + * 返回值:条目数(>=1;空表防御返回 1) uint8_t + * 限定条件:无 + */ +static uint8_t sl_opts_count(const char *const *a_opts) { + uint8_t n = 0u; + + if (a_opts == NULL) { + return 1u; + } + while (a_opts[n] != NULL) { + n++; + } + return (n > 0u) ? n : 1u; +} + +/* + * 函数功能:当前页窗口顶行下标(与框架 cp_draw_rows / cp_draw_footer 同式) + * 入口参数:inst - 定值表实例 const ui_w_setlist_t * + * 返回值:窗口顶行(全局下标) uint16_t + * 限定条件:编辑中行恒在选中行所在页(编辑态 ↑↓ 被编辑器消费,选中不移) + */ +static uint16_t sl_window_first(const ui_w_setlist_t *a_inst) { + uint16_t sel = ui_content_page_selected_row(); + + (void)a_inst; + return (uint16_t)((sel / SL_WIN_ROWS) * SL_WIN_ROWS); +} + +/* + * 函数功能:行槽顶 y(窗口内) + * 入口参数:inst - 定值表实例 const ui_w_setlist_t * + * row - 全局行下标 uint16_t + * 返回值:行槽顶 y int32_t + * 限定条件:row 在当前窗口内(绘制侧由 bad/ed 行可见性决定,越窗行不绘) + */ +static int32_t sl_slot_y(const ui_w_setlist_t *a_inst, uint16_t a_row) { + return UI_LIST_TOP + ((int32_t)a_row - (int32_t)sl_window_first(a_inst)) * UI_ROW_PITCH; +} + +/* + * 函数功能:开页复位(清编辑态) + */ +void ui_w_setlist_reset(ui_w_setlist_t *a_inst) { + ui_w_editor_reset(&a_inst->ed); + a_inst->ed_row = 0u; +} + +/* + * 函数功能:总行数(数据行 + 保存行[整定视图]) + */ +uint16_t ui_w_setlist_count(const ui_w_setlist_t *a_inst) { + if (a_inst->rows == NULL) { + return 0u; + } + return (uint16_t)(a_inst->count + (sl_is_rw(a_inst) ? 1u : 0u)); +} + +/* + * 函数功能:逐行取名/值(框架 refresh 转发) + */ +void ui_w_setlist_refresh(ui_w_setlist_t *a_inst, uint16_t a_row, + char *a_name, char *a_value) { + uint16_t total = ui_w_setlist_count(a_inst); + + a_name[0] = '\0'; + a_value[0] = '\0'; + + if ((a_inst->rows == NULL) || (a_row >= total)) { + return; /* 防御:空表/行越界(动态收缩瞬间) */ + } + + if (a_row < a_inst->count) { + const ui_w_setlist_row_t *r = &a_inst->rows[a_row]; + + (void)lv_snprintf(a_name, UI_BUF_STRLEN, "%s", r->name); + + if (r->kind == UI_W_SETLIST_KIND_ENUM) { + int32_t idx = a_inst->get(r->id); + uint8_t n = sl_opts_count(r->opts); + + if (idx < 0) { + idx = 0; + } + if (idx >= (int32_t)n) { + idx = (int32_t)n - 1; + } + (void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", r->opts[idx]); + return; + } + + if ((a_inst->ed.editing != 0u) && (a_inst->ed_row == a_row)) { + ui_w_editor_format(&a_inst->ed, a_value, UI_BUF_STRLEN); /* 编辑实时串 */ + } + else { + ui_w_editor_fmt_scaled(a_inst->get(r->id), r->decimals, a_value, UI_BUF_STRLEN); + } + return; + } + + /* 保存行(整定视图最后一行;名"保存",值随交接态——net_cfg 保存行先例) */ + (void)lv_snprintf(a_name, UI_BUF_STRLEN, "%s", SL_SAVE_NAME); + { + ui_w_setlist_st_t st = (a_inst->state != NULL) ? a_inst->state() + : UI_W_SETLIST_ST_IDLE; + switch (st) { + case UI_W_SETLIST_ST_WAIT: + (void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", "WAIT"); + break; + case UI_W_SETLIST_ST_OK: + (void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", "OK"); + break; + case UI_W_SETLIST_ST_FAIL: + (void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", "ERR"); + break; + default: + (void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", "ENTER"); + break; + } + } +} + +/* + * 函数功能:按键处理(框架 key_cb 转发) + */ +uint8_t ui_w_setlist_key(ui_w_setlist_t *a_inst, lv_key_t a_key) { + uint16_t sel = ui_content_page_selected_row(); + + if (a_inst->rows == NULL) { + return 0u; + } + + /* 编辑态:键先交编辑器 */ + if (a_inst->ed.editing != 0u) { + if ((sel != a_inst->ed_row) || (sel >= a_inst->count)) { + /* 防御:编辑中选中被外部改动(正常不会——编辑态 ↑↓ 被消费)→ 弃编辑 */ + ui_w_editor_reset(&a_inst->ed); + return 0u; + } + if (ui_w_editor_key(&a_inst->ed, a_key) == 0u) { + return 0u; /* 未消费键(本组件编辑态只接方向/ENTER) */ + } + if (a_inst->ed.editing == 0u) { + /* ENTER 提交成功:值写 RAM 工作副本,落盘统一走保存行整表提交 */ + a_inst->set(a_inst->rows[sel].id, ui_w_editor_value(&a_inst->ed)); + } + ui_content_page_invalidate(); + return 1u; + } + + if (a_key == LV_KEY_ENTER) { + if (ui_content_page_is_editable() == false) { + return 0u; /* 只读权限 ENTER 一律忽略(框架内置也无 ENTER 语义) */ + } + + if (sel < a_inst->count) { + const ui_w_setlist_row_t *r = &a_inst->rows[sel]; + + if (r->kind == UI_W_SETLIST_KIND_NUM) { + ui_w_editor_begin(&a_inst->ed, a_inst->get(r->id), + r->min, r->max, r->decimals); + a_inst->ed_row = sel; + ui_content_page_invalidate(); + return 1u; + } + /* 二态行:循环切换(控制字 告警<->跳闸、软压板 投入<->退出) */ + { + int32_t cur = a_inst->get(r->id); + uint8_t n = sl_opts_count(r->opts); + int32_t nxt = cur + 1; + + if ((cur < 0) || (cur >= (int32_t)n)) { + nxt = 0; + } + a_inst->set(r->id, nxt); + ui_content_page_invalidate(); + return 1u; + } + } + if ((sel == a_inst->count) && sl_is_rw(a_inst)) { + /* 保存行(数据行之后的最后一行):整表提交投递(一次封存一次校验和, + * 显示由 state 回读) */ + a_inst->commit(); + ui_content_page_invalidate(); + return 1u; + } + } + + return 0u; /* 其余键交回框架:↑↓ 移选、LEFT 关页(编辑态才在上面消费) */ +} + +/* + * 函数功能:反显一个名区(保存失败越界行:白底黑字覆盖名文本区) + * 入口参数:inst - 定值表实例 const ui_w_setlist_t * + * layer - 绘制层 lv_layer_t * + * clip - 重绘区 const lv_area_t * + * row - 数据行下标 uint16_t + * 返回值:无 + * 限定条件:行在当前窗口内(调用方已筛);编辑行不重复反显(选中带已反) + */ +static void sl_invert_name(const ui_w_setlist_t *a_inst, lv_layer_t *a_layer, + const lv_area_t *a_clip, uint16_t a_row) { + lv_area_t box; + int32_t top = sl_slot_y(a_inst, a_row); + + box.x1 = UI_SEL_BAND_INSET_L; + box.x2 = UI_LIST_RIGHT_X - 3; /* 名区止于值列前留 3px 缝 */ + box.y1 = top; + box.y2 = top + UI_ROW_BAND_H - 1; + if (!sl_areas_intersect(&box, a_clip)) { + return; + } + { + lv_draw_rect_dsc_t fd; + lv_draw_label_dsc_t ld; + char name[UI_BUF_STRLEN]; + + lv_draw_rect_dsc_init(&fd); + fd.base.layer = a_layer; + fd.bg_color = UI_COL_SEL_BG; + fd.bg_opa = LV_OPA_COVER; + lv_draw_rect(a_layer, &fd, &box); + + (void)lv_snprintf(name, sizeof(name), "%s", a_inst->rows[a_row].name); + lv_draw_label_dsc_init(&ld); + ld.base.layer = a_layer; + ld.text = name; + ld.text_static = 0u; /* 栈缓冲(同帧有效) */ + ld.color = UI_COL_SEL_FG; + ld.ofs_x = UI_LIST_LEFT_X - UI_SEL_BAND_INSET_L; /* ofs 相对 box.x1 */ + ld.ofs_y = 0; + lv_draw_label(a_layer, &ld, &box); + } +} + +/* + * 函数功能:自绘(编辑光标反显 + 越界行反显;框架内置区块之后) + */ +void ui_w_setlist_draw(ui_w_setlist_t *a_inst, lv_layer_t *a_layer, + const lv_area_t *a_clip) { + uint16_t first; + + if (a_inst->rows == NULL) { + return; + } + first = sl_window_first(a_inst); + + /* 1. 保存失败越界行反显(编辑行跳过:选中带已反,避免双重叠绘歧义) */ + if (a_inst->bad != NULL) { + uint16_t total = ui_w_setlist_count(a_inst); + + for (uint16_t r = first; (r < a_inst->count) && (r < total); r++) { + if ((r == a_inst->ed_row) && (a_inst->ed.editing != 0u)) { + continue; + } + if (r == ui_content_page_selected_row()) { + continue; + } + if (a_inst->bad(r)) { + sl_invert_name(a_inst, a_layer, a_clip, r); + } + } + } + + /* 2. 数值编辑光标(含"越界"提示;几何交编辑器按传入行槽绘制) */ + if (a_inst->ed.editing != 0u) { + if (a_inst->ed_row >= first) { + ui_w_editor_draw(&a_inst->ed, a_layer, a_clip, + sl_slot_y(a_inst, a_inst->ed_row), + 0, UI_ROW_BAND_H, UI_LIST_RIGHT_X); + } + } +} diff --git a/Drivers/BSP/UI/ui_w_setlist.h b/Drivers/BSP/UI/ui_w_setlist.h new file mode 100644 index 00000000..858183b1 --- /dev/null +++ b/Drivers/BSP/UI/ui_w_setlist.h @@ -0,0 +1,193 @@ +/* + * 模块名称:UI 定值表共享组件(ui_w_setlist) + * 模块功能:把"参数/定值/控制字/软压板"四类视图的列表渲染与整定输入抽象成 + * 一份实现——行表 + 数据回调注入(取数/写工作副本/提交/状态回读/ + * 越界行查询),同一份组件供多个页面实例复用: + * 查看视图(commit=NULL:无保存行、ENTER 无效)与整定视图 + * (commit 非空:末行固定为"保存"行,ENTER 投递整表提交)只差绑定; + * 数值行走 ui_w_editor 逐位编辑(范围校验提交),二态文字行 + * (控制字"告警/跳闸"、软压板"投入/退出")ENTER 直接切换; + * 保存失败后的越界行经 bad 回调反显高亮; + * 分页跟随内容页框架内置(8 行/页,UI_LIST_ROWS) + * 适用范围:Drivers/BSP/UI(标准列表页骨架几何:UI_LIST_TOP/ROW_PITCH/ + * ROW_BAND_H/LIST_ROWS/RIGHT_X 令牌;页面描述符须 selectable=1、 + * key_intercept=1、rows_per_page=UI_LIST_ROWS、text_dy=0) + * 说明:数据绑定全部经回调注入(批 2 ui_data/prot_set 交接时由页面薄跳板 + * 提供实现),组件不 include 任何 App 头(分层门禁方向); + * 实例化使用——每个页面文件 static 一份实例,钩子包一层 &inst 转调 + * (同 ui_w_records 模式) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道B task-5;规格 = 页面扩充详细方案.md + * §3.2 四视图表/§3.2.6 查看与整定差别/§3.2.7 编辑 + * 交互/§4.2 bind 要点;保存行 ENTER/WAIT/OK/ERR + * 沿用 ui_page_net_cfg 交接显示先例) + */ +#ifndef UI_W_SETLIST_H +#define UI_W_SETLIST_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "ui_content_page.h" /* lvgl(lv_key_t/lv_layer_t)+ 框架类型 */ +#include "ui_w_editor.h" + +/* 保存交接显示态(值列 ENTER/WAIT/OK/ERR,与 net_cfg 保存行同字面) */ +typedef enum { + UI_W_SETLIST_ST_IDLE = 0, /* 空闲:保存行显示 ENTER */ + UI_W_SETLIST_ST_WAIT, /* 请求已投递、属主任务处理中:WAIT */ + UI_W_SETLIST_ST_OK, /* 上轮提交成功:OK */ + UI_W_SETLIST_ST_FAIL /* 上轮提交失败:ERR */ +} ui_w_setlist_st_t; + +/* 行种类 */ +#define UI_W_SETLIST_KIND_NUM 0u /* 数值行:ui_w_editor 逐位编辑(min/max/decimals 有效) */ +#define UI_W_SETLIST_KIND_ENUM 1u /* 二态文字行:ENTER 在 opts 串表内循环切换 */ + +/* + * 视图行描述(Flash const 表,一行一条) + * 值通道统一 int32_t 定点: + * 数值行 = 工程量 * 10^decimals(如 10.24 A -> 1024) + * 二态行 = opts 串表下标(0/1/...) + */ +typedef struct { + const char *name; /* 中文名 <= 7 字(字体子集已预置) */ + uint8_t id; /* 应用侧枚举 ID:组件只透传给回调,不解释语义 */ + uint8_t kind; /* UI_W_SETLIST_KIND_NUM / _ENUM */ + uint8_t decimals; /* 数值行小数位 0..3(二态行忽略) */ + int32_t min; /* 数值行下限(含;>=0;二态行忽略) */ + int32_t max; /* 数值行上限(含;二态行忽略) */ + const char *const *opts; /* 二态行:NUL 结尾 Flash 字符串串表(数值行 NULL) */ +} ui_w_setlist_row_t; + +/* 数据回调组(均 uiTask 上下文、页面绘制/按键链内调用;实现由批 2 页面跳板提供, + * 组件不 include 任何 App 头——数据绑定经此注入) */ + +/* + * 函数功能:读某行当前值(数值行 = 工程量 * 10^decimals;二态行 = 串表下标) + * 入口参数:id - 行表登记的枚举 ID uint8_t + * 返回值:当前值 int32_t + */ +typedef int32_t (*ui_w_setlist_get_fn)(uint8_t a_id); + +/* + * 函数功能:写某行 RAM 工作副本(不写盘;落盘统一经 commit 整表提交) + * 入口参数:id - 行枚举 ID uint8_t + * val - 新值(口径同 get) int32_t + * 返回值:无 + */ +typedef void (*ui_w_setlist_set_fn)(uint8_t a_id, int32_t a_val); + +/* + * 函数功能:整表提交(一次封存一次校验和;实现只投递请求到属主任务,不阻塞) + * 入口参数:无 + * 返回值:无(投递结果经 state 回调回读) + */ +typedef void (*ui_w_setlist_commit_fn)(void); + +/* + * 函数功能:保存交接态回读(保存行值列显示源) + * 入口参数:无 + * 返回值:当前交接态 ui_w_setlist_st_t + */ +typedef ui_w_setlist_st_t (*ui_w_setlist_state_fn)(void); + +/* + * 函数功能:查询某数据行是否为"保存失败越界行"(需反显高亮) + * 入口参数:row - 数据行下标(不含保存行) uint16_t + * 返回值:true = 需反显;false = 正常(提问谓词,非错误通道) bool + */ +typedef bool (*ui_w_setlist_bad_fn)(uint16_t a_row); + +/* + * 定值表实例 + * 用法(页面跳板示例,批 2): + * static ui_w_setlist_t s_sl = { .rows = s_rows, .count = N, .get = get, ... }; + * count_cb -> ui_w_setlist_count(&s_sl) + * refresh -> ui_w_setlist_refresh(&s_sl, row, name, value) + * key_cb -> ui_w_setlist_key(&s_sl, key) + * draw_cb -> ui_w_setlist_draw(&s_sl, layer, clip) + * init_cb -> ui_w_setlist_reset(&s_sl) + */ +typedef struct { + const ui_w_setlist_row_t *rows; /* Flash 行表 */ + uint16_t count; /* 数据行数(不含保存行) */ + ui_w_setlist_get_fn get; /* 必填 */ + ui_w_setlist_set_fn set; /* 必填(整定提交前的 RAM 副本写入口) */ + ui_w_setlist_commit_fn commit; /* NULL = 查看视图(无保存行、禁编辑) */ + ui_w_setlist_state_fn state; /* NULL = 保存行恒显示 ENTER */ + ui_w_setlist_bad_fn bad; /* NULL = 不做越界行反显 */ + ui_w_editor_t ed; /* 数值编辑实例 */ + uint16_t ed_row; /* 编辑中行下标(仅 editing 时有效) */ +} ui_w_setlist_t; + +/* + * 函数功能:开页复位(清编辑态;接页面 init_cb) + * 入口参数:inst - 定值表实例 ui_w_setlist_t * + * 返回值:无 + * 限定条件:无 + */ +void ui_w_setlist_reset(ui_w_setlist_t *a_inst); + +/* + * 函数功能:总行数(接框架 count_cb:数据行 + 保存行[仅整定视图]) + * 入口参数:inst - 定值表实例 const ui_w_setlist_t * + * 返回值:总行数 uint16_t + * 限定条件:rows 非空(空表防御返回 0) + */ +uint16_t ui_w_setlist_count(const ui_w_setlist_t *a_inst); + +/* + * 函数功能:逐行取数(接框架 refresh:名 = 行表、值 = 回调读/编辑器实时串/保存态) + * 入口参数:inst - 定值表实例 ui_w_setlist_t * + * row - 全局行下标 uint16_t + * name - 名输出缓冲(UI_BUF_STRLEN) char * + * value - 值输出缓冲(同上) char * + * 返回值:无 + * 限定条件:页面打开(框架绘制链内调用) + * 函数说明:编辑中行的值列显示编辑器当前串(实时随 ↑↓ 变,同 net_cfg); + * 保存行(最后一行、整定视图)值 = ENTER/WAIT/OK/ERR(state 回调) + */ +void ui_w_setlist_refresh(ui_w_setlist_t *a_inst, uint16_t a_row, + char *a_name, char *a_value); + +/* + * 函数功能:按键处理(接框架 key_cb;配合描述符 key_intercept=1 转发 LEFT) + * 入口参数:inst - 定值表实例 ui_w_setlist_t * + * key - LVGL 键值 lv_key_t + * 返回值:1 = 已消费(框架跳过内置);0 = 交回框架(↑↓ 移选、← 关页等) + * (键分发标志非错误通道,取 uint8_t——BSP 公开 API 约定) + * 限定条件:页面打开(内部经 ui_content_page_selected_row / is_editable 判态) + * 函数说明:1. 数值编辑中:↑↓ 步进 / ←→ 移位 / ENTER 提交(越界拒绝)全由 + * ui_w_editor 消费;提交成功即经 set 回调写工作副本 + * 2. 正常态 ENTER(可编辑权限):数值行进编辑(光标个位); + * 二态行循环切换;保存行投递整表提交(一次封存一次校验和, + * 整表提交口径见 页面补齐规划.md §3.2) + * 3. 只读权限 ENTER 一律不消费;ESC 恒关页(框架,未提交编辑丢弃, + * 已经 ENTER 提交到工作副本的调整保留、不落盘——net_cfg 先例) + */ +uint8_t ui_w_setlist_key(ui_w_setlist_t *a_inst, lv_key_t a_key); + +/* + * 函数功能:自绘(接框架 draw_cb:编辑光标反显 + 保存失败越界行反显) + * 入口参数:inst - 定值表实例 ui_w_setlist_t * + * layer - 绘制层 lv_layer_t * + * clip - 重绘区 const lv_area_t * + * 返回值:无 + * 限定条件:页面打开;框架内置区块(行/带/页脚)已绘完 + * 函数说明:行槽 y 按标准列表骨架 + 当前页窗口换算(与框架 cp_draw_rows 同式: + * first = sel / UI_LIST_ROWS * UI_LIST_ROWS) + */ +void ui_w_setlist_draw(ui_w_setlist_t *a_inst, lv_layer_t *a_layer, + const lv_area_t *a_clip); + +#ifdef __cplusplus +} +#endif + +#endif /* UI_W_SETLIST_H */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index c8a4c4e6..b324cb42 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -1007,6 +1007,11 @@ 1 ..\App\algo\adc_health.c + + adc_cal.c + 1 + ..\App\algo\adc_cal.c + prot_set.c 1 @@ -3511,6 +3516,21 @@ 1 ..\Drivers\BSP\UI\ui_w_records.c + + ui_w_editor.c + 1 + ..\Drivers\BSP\UI\ui_w_editor.c + + + ui_w_setlist.c + 1 + ..\Drivers\BSP\UI\ui_w_setlist.c + + + ui_page_action.c + 1 + ..\Drivers\BSP\UI\ui_page_action.c + ui_style.c 1 diff --git a/docs/UI设计/UI_数据供给设计说明.md b/docs/UI设计/UI_数据供给设计说明.md index 0d5662e2..d2cc9012 100644 --- a/docs/UI设计/UI_数据供给设计说明.md +++ b/docs/UI设计/UI_数据供给设计说明.md @@ -333,6 +333,10 @@ void ui_w_records_bind(const ui_rec_reader_t *reader); /* App 启动时注入 - `log_id` 用 `uint8_t`(**不透明编号**),BSP 不知道 `SYSLOG_RUN` 是什么,只知道"0 号日志"。 - 注入点在 `App/task/ui_task.c`(`ui_menu_init` 之前),实现体是 `sys_log` 的两个薄包装。 +- 【2026-10-25 落地注】模拟器同形镜像:`simulator/main.c` 的 `sim_rec_count/sim_rec_get`, + 无头与窗口两个初始化点都在 `ui_menu_init` 前调 `ui_w_records_bind`;UI 侧编号常量 + `UI_W_REC_LOG_RUN/SELFTEST`(映射只在两端适配层,UI 不感知 `syslog_id_t`)。 + `ui_rec_reader_t` 按本文契约**原样**实现(`int` 错误码 0/-1/-2 未改型)。 ### 7.3 收益:可削减 3 条既有基线 @@ -489,7 +493,7 @@ typedef struct { ## 14. 与既有文档和门禁的关系 -- **门禁**:`scripts/check_layer_deps.py` 的三种合法做法本文全部用上(快照推送=②,写路径与记录 reader=①),**不新增任何 `ALLOW` 或 `BASELINE` 条目**,并计划削减 3 条。 +- **门禁**:`scripts/check_layer_deps.py` 的三种合法做法本文全部用上(快照推送=②,写路径与记录 reader=①),**不新增任何 `ALLOW` 或 `BASELINE` 条目**;计划的 3 条削减**已于 2026-10-25 兑现**(task/UI-rec-reader 落地 §7.2 契约,基线 6 → 3,详见 `docs/门禁说明/layer-deps.md` §1)。 - **页面框架**:`docs/UI设计/内容页架构.md` 的 `refresh` / `count_cb` 钩子**签名不变**,页面只是改为从 `ui_data` 取数。 - **页面清单**:`docs/UI设计/页面补齐规划.md` 的"批 1"第一项即本文 步骤1~步骤3。 - **数据真实性**:`docs/问题记录/采集与数据处理短板评测.md` 的标定(§4.1)与遥测链(§4.2)落地后,只需改 `ui_data_feed` 的取数来源与 `eng` 取值,页面零改动。 diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index fc567af4..4e9b2e93 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ C2 dhara 失败类 +84;RO 另 +208,全是 C2 的失败类名表)→ `ER_IR | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 234,492 B (229.00 KB) | 1,048,576 B (1024 KB) | 22.4% | -| RW_IRAM1 | 75,632 B (73.86 KB) | 131,072 B (128 KB) | 57.7% | +| ER_IROM1 | 237,140 B (231.58 KB) | 1,048,576 B (1024 KB) | 22.6% | +| RW_IRAM1 | 75,784 B (74.01 KB) | 131,072 B (128 KB) | 57.8% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **235,696 B (230.2 KB)** | 1,048,576 B (1024 KB) | **22.5%** | +| **Flash (ROM 合计)** | **238,364 B (232.8 KB)** | 1,048,576 B (1024 KB) | **22.7%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 7d610f97..8b953270 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,18 +15,18 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 234,492 B | 1,048,576 B | 22.4% | 参考 | +| ER_IROM1 (Flash) | 237,140 B | 1,048,576 B | 22.6% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 75,632 B | 131,072 B | 57.7% | PASS | +| RW_IRAM1 | 75,784 B | 131,072 B | 57.8% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| | 网络 | 13,702 B | 12 KB | ⚠ 超 1.4 KB | | 存储 | 7,056 B | 12 KB | OK | -| UI | 30,422 B | 16 KB | ⚠ 超 13.7 KB | +| UI | 30,426 B | 16 KB | ⚠ 超 13.7 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | -| 应用 | 15,275 B | 8 KB | ⚠ 超 6.9 KB | +| 应用 | 15,406 B | 8 KB | ⚠ 超 7.0 KB | | 其它 | 9,208 B | — | 参考 | diff --git a/docs/门禁说明/README.md b/docs/门禁说明/README.md index 7ecdecd2..0a15fb5a 100644 --- a/docs/门禁说明/README.md +++ b/docs/门禁说明/README.md @@ -16,7 +16,7 @@ |---|---|---|---| | **改写型** | 不报错,直接改你的文件 | `trailing-whitespace`、`end-of-file-fixer` | 重新 `git add` 被改的文件再提交(它改的是工作区,不是索引) | | **硬拦截** | 违反即失败,没有存量豁免 | `cppcheck`、`coding-standard`、`doc-links`、`no-mojibake`、`mem-budget`、`ui-golden`、`font-charset`、`lv-conf-sync`、`task-table`、`mem-numbers`、`registration-check`、`cubemx-survival`、`test-suite-switch`、16 个主机单测、官方 5 个、`gitlint` | 只能改代码或改文档;**不要**找豁免开关 | -| **增量拦截** | 规则恒生效,但存量钉在基线/台账里,只许减不许增 | `layer-deps`(6 条)、`bsp-err`(38 条)、`id-check`(`scripts/id_known_backlog.json` 台账) | 报错先分清是**新增**还是**存量搬家**:`--verbose` 看归类,`--prune` 看已还清 | +| **增量拦截** | 规则恒生效,但存量钉在基线/台账里,只许减不许增 | `layer-deps`(3 条,2026-10-25 还清 UI 记录 3 条)、`bsp-err`(38 条)、`id-check`(`scripts/id_known_backlog.json` 台账) | 报错先分清是**新增**还是**存量搬家**:`--verbose` 看归类,`--prune` 看已还清 | ⚠ 增量拦截最容易误会的一点:**绿灯不等于没问题**,只等于"没有比上次更差"。 每个增量拦截门禁的基线数字都写进它自己的文档,读那篇时第一眼看的就是"现在在册多少条"。 diff --git a/docs/门禁说明/layer-deps.md b/docs/门禁说明/layer-deps.md index 5e86d715..f442c329 100644 --- a/docs/门禁说明/layer-deps.md +++ b/docs/门禁说明/layer-deps.md @@ -20,9 +20,12 @@ `App/log/console_log.h`(日志后端,BSP 的 `dbg_log.h` 本来就建在它上面)、 `App/util/sys_clock.h`(时间源,日志时间戳的唯一提供者)、`App/util/sha256.h`(纯算法、无状态)。 -**`BASELINE` 六条(2026-10-22 清点,只减不增)**:`NETCFG/net_cfg.c`、`NETCFG/ftp_cfg.c` -→ `App/cfg/cfg_file.h`(配置引擎属主是 defaultTask);`NET/lftpd/lftpd_io.c` → `App/fs/vol_lock.h`; -`UI/ui_page_runlog.c`、`UI/ui_page_selftest.c`、`UI/ui_w_records.h` → `App/log/sys_log.h`。 +**`BASELINE` 现存三条(2026-10-22 清点六条 → 2026-10-25 道B 还清 UI 记录三条 → 现三条;只减不增)**: +`NETCFG/net_cfg.c`、`NETCFG/ftp_cfg.c` → `App/cfg/cfg_file.h`(配置引擎属主是 defaultTask); +`NET/lftpd/lftpd_io.c` → `App/fs/vol_lock.h`。 +已还清:`UI/ui_page_runlog.c`、`UI/ui_page_selftest.c`、`UI/ui_w_records.h` → `App/log/sys_log.h` +(2026-10-25 task/UI-rec-reader:`ui_w_records` 改注入式 `ui_rec_reader_t`,契约见 +`docs/UI设计/UI_数据供给设计说明.md` §7.2,绑定在 `App/task/ui_task.c` 与 `simulator/main.c`)。 修法是**注入 / 传参**(上层把数据或回调递进 BSP),不是把上层服务搬进 BSP。 ## 2. 为什么要它 @@ -69,8 +72,8 @@ ## 6. 限制与盲区 1. **只查直接 include,查不到传递依赖**。BSP 的 `a.h` 里 include 了 App 头,BSP 的 `b.c` - 只 include `a.h`——`b.c` 这条边不出现,**但 `a.h` 那条会报**。基线里 `ui_w_records.h` 的 - 注释专门写着"(组件头,会外溢到 include 链)",就是提醒这种外溢要人来看。 + 只 include `a.h`——`b.c` 这条边不出现,**但 `a.h` 那条会报**。原基线 `ui_w_records.h` 的 + 注释专门写着"(组件头,会外溢到 include 链)"(该条 2026-10-25 已还清),就是提醒这种外溢要人来看。 2. 只认 `#include "引号"` 形式;`#include ` 这类写法不在管辖内(也违反 include 规范, 由 `coding-standard` 的 CODE-RULE-11 兜)。 3. **按 basename 匹配**:如果 `App/` 与别处存在同名头,会按同名候选放宽(先精确后同名), @@ -80,7 +83,7 @@ `--prune` 也就不会提示你收紧基线。 6. `git` 不可用时退回 `os.walk`(跳过 `.git/.venv/build`)——两条路径的清单可能不同, 在无 git 环境下跑出的"通过"不能当证据。 -7. 绿灯的含义只是"没有新增反向边":6 条存量债务仍然在那里。 +7. 绿灯的含义只是"没有新增反向边":现存 3 条存量债务仍然在那里(2026-10-25 还清 3 条)。 ## 7. 豁免与基线 @@ -97,3 +100,4 @@ | 日期 | 变更 | |---|---| | 2026-10-23 | 首篇逐项文档(在册 6 条基线逐条列出,含每条的属主背景) | +| 2026-10-25 | 道B(task/UI-rec-reader)还清 UI 记录 3 条:`ui_w_records` 注入式 reader(§7.2 契约落地),基线 6 → 3 | diff --git a/scripts/check_layer_deps.py b/scripts/check_layer_deps.py index 2562023e..a3d8eab9 100644 --- a/scripts/check_layer_deps.py +++ b/scripts/check_layer_deps.py @@ -43,15 +43,15 @@ ALLOW = { "App/util/sha256.h", # 纯算法工具:无状态、不依赖任何上层服务 } -# 存量基线(2026-10-22 清点,只减不增)。每条 = (引用方, 被引用的 App 头)。 +# 存量基线(2026-10-22 清点 6 条,只减不增)。每条 = (引用方, 被引用的 App 头)。 # 修法是"注入/传参",不是把上层服务搬进 BSP——详见报告 §0.4 的 S-1 行。 +# 2026-10-25 道B(task/UI-rec-reader)还清 UI 记录 3 条:ui_w_records 改注入式 +# ui_rec_reader_t(契约 UI_数据供给设计说明.md §7.2),runlog/selftest/records.h +# 不再 include sys_log.h,绑定在 App/task/ui_task.c 与 simulator/main.c。 BASELINE = { ("Drivers/BSP/NETCFG/net_cfg.c", "App/cfg/cfg_file.h"), # 配置引擎属主 = defaultTask ("Drivers/BSP/NETCFG/ftp_cfg.c", "App/cfg/cfg_file.h"), # 同上([ftp] 段注册) ("Drivers/BSP/NET/lftpd/lftpd_io.c", "App/fs/vol_lock.h"), # 卷锁在 App/fs - ("Drivers/BSP/UI/ui_page_runlog.c", "App/log/sys_log.h"), # 记录读取应由上层递进来 - ("Drivers/BSP/UI/ui_page_selftest.c", "App/log/sys_log.h"), # 同上 - ("Drivers/BSP/UI/ui_w_records.h", "App/log/sys_log.h"), # 同上(组件头,会外溢到 include 链) } INC_RE = re.compile(r'^[ \t]*#[ \t]*include[ \t]*"([^"]+)"', re.M) diff --git a/scripts/check_registration.py b/scripts/check_registration.py index eccc6b4d..baaf42b9 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -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 控制台输出,模拟器无消费路径"), diff --git a/scripts/id_registry.json b/scripts/id_registry.json index d7b121ea..67e663de 100644 --- a/scripts/id_registry.json +++ b/scripts/id_registry.json @@ -1686,6 +1686,50 @@ "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-PROTDIM", + "class": "TC", + "title": "主机侧单测用例号:量纲回归(test/host/prot_dim,道A 卡7)", + "pattern": "\\bTC-PROTDIM-[0-9]{1,4}[a-z]?\\b", + "digit_width": [ + 2, + 4 + ], + "owner_docs_glob": [ + "test/host/prot_dim/*" + ], + "scope": "global", + "frozen": false, + "cursor": "TC-PROTDIM-010", + "cursor_owner_only": true, + "check_dangling": true, + "allow_orphans": true, + "subitem_ok": true, + "owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCHEALTH/TC-HOST-ADCCAL 先例;2026-10-25 道A 卡7 建族):属主登记处 = 单测代码目录,不逐号补文档", + "cursor_note": "游标按建族时实测最大号收录(2026-10-25):越界即视为未登记的冒号牌" + }, { "name": "TC-HOST-IEC104", "class": "TC", diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 806b3bae..5752ba3a 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -7,4 +7,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-22 213436 18652 1188 131500 232088 22.1 75264 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736 2026-10-23 213676 18860 1188 131500 232536 22.2 75264 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736 2026-10-24 213780 18860 1204 131852 232640 22.2 75632 57.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736 -2026-10-25 214104 20388 1204 131852 234492 22.4 75632 57.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736 +2026-10-25 215744 21396 1224 131984 237140 22.6 75784 57.8 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736 diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index 451126e3..80725360 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,25 +1,25 @@ { - "built_at": "2026-10-25T04:15:33+08:00", - "fingerprint": "bf6ec94a761eacd8e3bcbc590e8780c2293462d384b9b33094e5d0fd6411f295", - "fingerprint_files": 1388, - "fingerprint_source": "git-index", + "built_at": "2026-10-25T05:31:08+08:00", + "fingerprint": "548011fc4a68e087a855c952e73c6d8d85c6f4a4dee28702f29dfa858ff3577c", + "fingerprint_files": 1396, + "fingerprint_missing": 0, "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "e1967cf2eac70b794c920774715c6fe78dc30ac2", + "git_head": "59da40bb4a53db082953164a9d7c58bb342f8c19", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1792872260, + "map_mtime": 1792877445, "pct": { - "ER_IROM1": 22.36, + "ER_IROM1": 22.62, "RW_CCM": 87.89, - "RW_IRAM1": 57.7, + "RW_IRAM1": 57.82, "UNINIT_CCM": 18.75 }, "regions": { "ER_IROM1": { "max": 1048576, - "size": 234492 + "size": 237140 }, "RW_CCM": { "max": 65280, @@ -27,22 +27,21 @@ }, "RW_IRAM1": { "max": 131072, - "size": 75632 + "size": 75784 }, "UNINIT_CCM": { "max": 256, "size": 48 } }, - "schema": 2, + "schema": 1, "scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring", "sizes": { - "code": 214104, - "ro_data": 20388, - "rw_data": 1204, - "zi_data": 131852 + "code": 215744, + "ro_data": 21396, + "rw_data": 1224, + "zi_data": 131984 }, "threshold_pct": 92.0, - "worktree_dirt_at_stamp": 0, "written_by": "scripts/check_size_gate.py --stamp" } diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index 18fd88c5..bc8ada4e 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -1,12 +1,20 @@ { "files": { + "App/algo/adc_cal.c": { + "func_rate": 1.0, + "funcs_covered": 12, + "funcs_valid": 12, + "line_rate": 0.9916, + "lines_covered": 118, + "lines_valid": 119 + }, "App/algo/adc_health.c": { "func_rate": 1.0, - "funcs_covered": 6, - "funcs_valid": 6, + "funcs_covered": 9, + "funcs_valid": 9, "line_rate": 1.0, - "lines_covered": 108, - "lines_valid": 108 + "lines_covered": 143, + "lines_valid": 143 }, "App/algo/adc_input.c": { "func_rate": 1.0, @@ -57,12 +65,12 @@ "lines_valid": 41 }, "App/cfg/cfg_file.c": { - "func_rate": 0.9615, - "funcs_covered": 25, - "funcs_valid": 26, - "line_rate": 0.8773, - "lines_covered": 572, - "lines_valid": 652 + "func_rate": 0.963, + "funcs_covered": 26, + "funcs_valid": 27, + "line_rate": 0.8853, + "lines_covered": 610, + "lines_valid": 689 }, "App/cfg/cfg_file_store_mcu.c": { "func_rate": 1.0, @@ -117,8 +125,8 @@ "funcs_covered": 13, "funcs_valid": 13, "line_rate": 0.9939, - "lines_covered": 324, - "lines_valid": 326 + "lines_covered": 328, + "lines_valid": 330 }, "App/protocol/iec104/iec104_asdu.c": { "func_rate": 1.0, @@ -277,8 +285,8 @@ "funcs_covered": 53, "funcs_valid": 53, "line_rate": 0.9978, - "lines_covered": 887, - "lines_valid": 889 + "lines_covered": 889, + "lines_valid": 891 }, "Drivers/BSP/NETCFG/ftp_cfg.c": { "func_rate": 1.0, @@ -299,11 +307,19 @@ }, "generated_by": "scripts/check_test_coverage.py --update-baseline", "modules": { + "adc_cal": { + "files": { + "App/algo/adc_cal.c": 119, + "App/cfg/cfg_file.c": 689 + }, + "lines_total": 808, + "ok": true + }, "adc_health": { "files": { - "App/algo/adc_health.c": 108 + "App/algo/adc_health.c": 143 }, - "lines_total": 108, + "lines_total": 143, "ok": true }, "adc_input": { @@ -315,9 +331,9 @@ }, "cfg_file": { "files": { - "App/cfg/cfg_file.c": 652 + "App/cfg/cfg_file.c": 689 }, - "lines_total": 652, + "lines_total": 689, "ok": true }, "cfg_file_store_mcu": { @@ -446,9 +462,9 @@ }, "net_socket": { "files": { - "Drivers/BSP/NET/net_socket.c": 889 + "Drivers/BSP/NET/net_socket.c": 891 }, - "lines_total": 889, + "lines_total": 891, "ok": true }, "phasor": { @@ -459,6 +475,18 @@ "lines_total": 96, "ok": true }, + "prot_dim": { + "files": { + "App/algo/adc_cal.c": 119, + "App/algo/freq.c": 130, + "App/algo/phasor.c": 55, + "App/algo/prot_elm.c": 106, + "App/algo/prot_set.c": 179, + "App/algo/sample_ring.c": 41 + }, + "lines_total": 630, + "ok": true + }, "prot_elm": { "files": { "App/algo/freq.c": 130, @@ -517,9 +545,9 @@ }, "sys_log_mcu": { "files": { - "App/log/sys_log_mcu.c": 326 + "App/log/sys_log_mcu.c": 330 }, - "lines_total": 326, + "lines_total": 330, "ok": true }, "task_prof": { @@ -554,6 +582,7 @@ "note": "主机单测覆盖率基线。口径与工具链限制见 docs/测试说明/主机单测覆盖率口径.md。门禁判据:files[].lines_covered 与 funcs_covered 只增不减,下降即拦提交。", "schema": 1, "tested_sources": [ + "App/algo/adc_cal.c", "App/algo/adc_health.c", "App/algo/adc_input.c", "App/algo/freq.c", @@ -594,16 +623,16 @@ ], "totals": { "files_generated_excluded": 5, - "files_tested": 37, - "files_untested": 107, - "func_rate": 0.9231, - "funcs_covered": 396, - "funcs_valid": 429, - "line_rate": 0.8932, - "lines_covered": 5605, - "lines_valid": 6275, - "modules_ok": 33, - "modules_total": 33 + "files_tested": 38, + "files_untested": 115, + "func_rate": 0.9258, + "funcs_covered": 412, + "funcs_valid": 445, + "line_rate": 0.8965, + "lines_covered": 5802, + "lines_valid": 6472, + "modules_ok": 35, + "modules_total": 35 }, "untested_sources": [ { @@ -630,10 +659,18 @@ "code_lines": 281, "path": "App/task/default_task.c" }, + { + "code_lines": 265, + "path": "Drivers/BSP/UI/ui_w_editor.c" + }, { "code_lines": 264, "path": "Drivers/BSP/SD2506/sd2506.c" }, + { + "code_lines": 224, + "path": "App/task/adc_task.c" + }, { "code_lines": 206, "path": "Drivers/BSP/CH395F/ch395f.h" @@ -647,8 +684,8 @@ "path": "Drivers/BSP/UI/ui_standby.c" }, { - "code_lines": 171, - "path": "App/task/adc_task.c" + "code_lines": 204, + "path": "Drivers/BSP/UI/ui_w_setlist.c" }, { "code_lines": 157, @@ -659,7 +696,7 @@ "path": "Drivers/BSP/UI/ui_page_password.c" }, { - "code_lines": 135, + "code_lines": 138, "path": "App/app/app_main.c" }, { @@ -670,6 +707,14 @@ "code_lines": 123, "path": "App/task/net_task.c" }, + { + "code_lines": 117, + "path": "App/task/protect_task.c" + }, + { + "code_lines": 113, + "path": "Drivers/BSP/UI/ui_page_action.c" + }, { "code_lines": 101, "path": "Drivers/BSP/NET/net_types.h" @@ -683,8 +728,8 @@ "path": "Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h" }, { - "code_lines": 90, - "path": "App/task/protect_task.c" + "code_lines": 92, + "path": "App/task/ui_task.c" }, { "code_lines": 83, @@ -706,10 +751,6 @@ "code_lines": 77, "path": "Drivers/BSP/dbg_log.h" }, - { - "code_lines": 71, - "path": "App/task/ui_task.c" - }, { "code_lines": 62, "path": "App/protocol/iec104/iec104_asdu.h" @@ -718,6 +759,10 @@ "code_lines": 56, "path": "App/protocol/iec104/iec104_link.h" }, + { + "code_lines": 55, + "path": "App/algo/adc_health.h" + }, { "code_lines": 54, "path": "Drivers/BSP/TPAFE5160/tpafe5160.h" @@ -728,24 +773,24 @@ }, { "code_lines": 50, - "path": "App/algo/adc_health.h" + "path": "Drivers/BSP/UI/ui_w_setlist.h" + }, + { + "code_lines": 46, + "path": "App/log/soe_rec.h" }, { "code_lines": 44, "path": "App/algo/adc_input.h" }, { - "code_lines": 44, - "path": "App/log/soe_rec.h" + "code_lines": 42, + "path": "App/cfg/cfg_file.h" }, { "code_lines": 42, "path": "Drivers/BSP/UI/ui_content_page.h" }, - { - "code_lines": 41, - "path": "App/cfg/cfg_file.h" - }, { "code_lines": 41, "path": "Drivers/BSP/NET/lftpd/private/lftpd_status.h" @@ -755,12 +800,12 @@ "path": "Drivers/BSP/UI/ui_page_ac.c" }, { - "code_lines": 34, - "path": "Drivers/BSP/NETCFG/net_cfg.h" + "code_lines": 39, + "path": "Drivers/BSP/UI/ui_w_records.c" }, { "code_lines": 34, - "path": "Drivers/BSP/UI/ui_w_records.c" + "path": "Drivers/BSP/NETCFG/net_cfg.h" }, { "code_lines": 33, @@ -782,10 +827,18 @@ "code_lines": 31, "path": "Drivers/BSP/UI/ui_deco.c" }, + { + "code_lines": 31, + "path": "Drivers/BSP/UI/ui_w_editor.h" + }, { "code_lines": 29, "path": "Drivers/BSP/NET/lftpd/lftpd_auth.h" }, + { + "code_lines": 29, + "path": "Drivers/BSP/UI/ui_w_records.h" + }, { "code_lines": 28, "path": "App/algo/prot_elm.h" @@ -794,6 +847,10 @@ "code_lines": 28, "path": "App/protocol/iec104/iec104_pts.h" }, + { + "code_lines": 28, + "path": "Drivers/BSP/app_assert.h" + }, { "code_lines": 27, "path": "App/algo/phasor.h" @@ -826,6 +883,10 @@ "code_lines": 24, "path": "Drivers/BSP/UI/ui_tokens.h" }, + { + "code_lines": 22, + "path": "App/algo/adc_cal.h" + }, { "code_lines": 22, "path": "App/cfg/cfg_file_store.h" @@ -838,6 +899,10 @@ "code_lines": 22, "path": "Drivers/BSP/NETCFG/ftp_cfg.h" }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_action.h" + }, { "code_lines": 22, "path": "Drivers/BSP/UI/ui_page_runlog.c" @@ -854,10 +919,6 @@ "code_lines": 20, "path": "Drivers/BSP/UI/ui_page_dc.c" }, - { - "code_lines": 19, - "path": "Drivers/BSP/UI/ui_w_records.h" - }, { "code_lines": 18, "path": "App/app/sys_events.h" diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index ba62f39b..158ab12f 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -97,6 +97,9 @@ set(UI_SOURCES ${UI_DIR}/ui_page_registry.c ${UI_DIR}/ui_style.c ${UI_DIR}/ui_w_records.c + ${UI_DIR}/ui_w_editor.c + ${UI_DIR}/ui_w_setlist.c + ${UI_DIR}/ui_page_action.c ) # 运行/自检记录引擎(双端共享核心 + 平台存储后端;MCU 端编 sys_log_mcu.c,见 Keil 工程) diff --git a/simulator/main.c b/simulator/main.c index 7cb8f82a..b0fdf790 100644 --- a/simulator/main.c +++ b/simulator/main.c @@ -68,6 +68,7 @@ #include "cfg_file.h" #include "cfg_file_store.h" #include "sys_log.h" +#include "ui_w_records.h" /* task-6 记录 reader 注入点(镜像 App/task/ui_task.c 的绑定) */ /* 屏幕尺寸(与 MCU 端一致) */ #define SIM_SCREEN_W 160 @@ -137,6 +138,39 @@ static void sim_time_cb(char *buf, size_t len) { tmv.tm_hour, tmv.tm_min, tmv.tm_sec); } +/* + * 函数功能:记录 reader 适配(task-6 注入,镜像 ui_task.c)——UI 编号 → syslog_id_t + * 入口参数:log_id - UI 记录源编号(UI_W_REC_LOG_*) uint8_t + * 返回值:记录条数;未知编号返回 0(页面空态) uint16_t + */ +static uint16_t sim_rec_count(uint8_t log_id) { + if (log_id == UI_W_REC_LOG_RUN) { + return (uint16_t)syslog_count(SYSLOG_RUN); + } + if (log_id == UI_W_REC_LOG_SELFTEST) { + return (uint16_t)syslog_count(SYSLOG_SELFTEST); + } + return 0u; +} + +/* + * 函数功能:记录 reader 适配(task-6 注入)——读记录字段直通 sys_log(错误码不改写) + * 入口参数:log_id/idx/ts/ts_len/desc/desc_len - 契约见 ui_w_records.h ui_rec_reader_t + * 返回值:0 成功;-1 编号/序号越界;-2 读失败 int + */ +static int sim_rec_get(uint8_t log_id, uint32_t idx, char *ts, uint32_t ts_len, + char *desc, uint32_t desc_len) { + if (log_id == UI_W_REC_LOG_RUN) { + return syslog_get_record(SYSLOG_RUN, idx, ts, (size_t)ts_len, desc, (size_t)desc_len); + } + if (log_id == UI_W_REC_LOG_SELFTEST) { + return syslog_get_record(SYSLOG_SELFTEST, idx, ts, (size_t)ts_len, desc, (size_t)desc_len); + } + return -1; +} + +static const ui_rec_reader_t s_rec_reader = { sim_rec_count, sim_rec_get }; + /* * 函数功能:无窗口离屏渲染(可注入键序列)并导出末帧像素 * 入口参数:keys - 键序列(u=上 d=下 l=左 r=右 e=确认 s=返回;空 = 不注键) const char * @@ -176,6 +210,9 @@ static int run_headless(const char * keys, const char * path, int standby) { * 使网口设置内容页显示与真机一致的工厂默认值 */ (void)net_cfg_load(); + /* task-6 记录 reader 注入(镜像 ui_task.c:先于 ui_menu_init,两端同形) */ + ui_w_records_bind(&s_rec_reader); + if (ui_menu_init() != 0) { return 1; } @@ -568,6 +605,9 @@ int main(int argc, char **argv) { (void)net_cfg_load(); /* 构建菜单 UI(与 MCU 端共用 ui_menu.c) */ + /* task-6 记录 reader 注入(镜像 ui_task.c:先于 ui_menu_init,两端同形) */ + ui_w_records_bind(&s_rec_reader); + if (ui_menu_init() != 0) { fprintf(stderr, "ui_menu_init failed\n"); SDL_Quit(); diff --git a/test/host/adc_cal/adc_cal_test.c b/test/host/adc_cal/adc_cal_test.c new file mode 100644 index 00000000..aaf19799 --- /dev/null +++ b/test/host/adc_cal/adc_cal_test.c @@ -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 +#include +#include +#include + +/* 先纳入桩头(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; +} diff --git a/test/host/adc_cal/run_unit_test.py b/test/host/adc_cal/run_unit_test.py new file mode 100644 index 00000000..09de40c6 --- /dev/null +++ b/test/host/adc_cal/run_unit_test.py @@ -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)) diff --git a/test/host/adc_cal/stub/dbg_log.h b/test/host/adc_cal/stub/dbg_log.h new file mode 100644 index 00000000..74958e32 --- /dev/null +++ b/test/host/adc_cal/stub/dbg_log.h @@ -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 + +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 */ diff --git a/test/host/adc_health/adc_health_test.c b/test/host/adc_health/adc_health_test.c index 333a6b2c..23da2fe1 100644 --- a/test/host/adc_health/adc_health_test.c +++ b/test/host/adc_health/adc_health_test.c @@ -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); diff --git a/test/host/iec104/iec104_test.c b/test/host/iec104/iec104_test.c index bfc8b97e..ccd001c0 100644 --- a/test/host/iec104/iec104_test.c +++ b/test/host/iec104/iec104_test.c @@ -511,14 +511,15 @@ static void tc_cov_yc(void) handshake(now); s_f_yc[0] = 100.0f; iec104_link_poll(&g_lk, now); /* 快照 = 100 */ - s_f_yc[0] = 105.0f; /* 死区 10 内 */ + s_f_yc[0] = 100.4f; /* 死区 0.5 V 内(卡 6 二次值口径:0.5 %×100 V, + * 旧拟定码值死区 10 已由「数据可信」卡 6 重定) */ iec104_link_poll(&g_lk, now + 100u); CHECK(cap_i_count() == 0, "TC-008 inside the deadband nothing is uploaded"); - s_f_yc[0] = 130.0f; /* 130-105=25 > 10 */ + s_f_yc[0] = 101.25f; /* 101.25-100.4=0.85 > 0.5 */ iec104_link_poll(&g_lk, now + 200u); CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 13u) && (c.cot == IEC104_COT_SPONT) && (c.ioa == 0x4011u) - && (iec104_get_float(c.obj) == 130.0f), "TC-008 crossing the deadband uploads"); + && (iec104_get_float(c.obj) == 101.25f), "TC-008 crossing the deadband uploads"); } static void feed_rd(uint32_t now) diff --git a/test/host/prot_dim/prot_dim_test.c b/test/host/prot_dim/prot_dim_test.c new file mode 100644 index 00000000..3e62309c --- /dev/null +++ b/test/host/prot_dim/prot_dim_test.c @@ -0,0 +1,475 @@ +/* + * 模块名称:量纲回归主机侧单测(「数据可信」卡 7 · 道A) + * 被测对象:真实源文件(#include 原样纳入,全链路 L1.5→L2→L3): + * adc_cal.c + prot_set.c + sample_ring.c + phasor.c + freq.c + + * prot_elm.c + * 平台依赖桩:dbg_log.h(stub/)、cfg_file_register(捕获计数)、 + * adc_input_desc(V0.1 权威来源表,同 adc_cal 套件)。 + * + * 本卡的存在理由(短板评测 + 「数据可信」§2.2):台架此前"通过"是因为没有 + * 一条判据检查**不该动作时不动作**——"码值当工程量"的错配曾让 OC 拿 ~11000 + * 码值比 10 A 定值恒假动作。本套件把这条固化成回归:用例从 **ADC 码值** 出发、 + * **显式写出码值↔二次值换算**,走真实 折算→采样环→全周傅氏→元件判据 链; + * 任何一环把量纲弄丢(比如折算点被旁路),断言立刻翻转。 + * + * ROM 系数覆写(#undef/#define):把"板级理论系数"替换为合成端子侧口径 + * (10 mV/码,K_DIM),模拟卡 12 标定完成后的 ROM/默认状态——合法带随之 + * 锚定合成值,adc_cal_set 走正规路径置 VALID,测试不白盒改引擎内存。 + * + * 覆盖点(族 TC-HOST-PROTDIM,属主登记处 = 本目录): + * TC-PROTDIM-001 标定链基线:0.5 A/100 V/0 V 注入 → 七类元件全静默(含 + * OL:0.5 < 1 A 定值)+ IA 相量有效值 = 0.5 A ±0.5 % + * TC-PROTDIM-002 反错配对照:同一批码值,raw 直灌 → OC1 假动作(复现历史 + * 事故);经 adc_cal_apply → OC1 静默(折算点不可旁路) + * TC-PROTDIM-003 OC1(10 A,dly=0):9.995 不动作 / 10.5 启动即动作 / + * 9.6 滞回保持 / 9.49 释放 + * TC-PROTDIM-004 OC2(5 A,500 ms):6 A 启动,启动后 499 ms 不动作、 + * 501 ms 动作;4.8 保持(ret 4.75)、4.6 释放 + * TC-PROTDIM-005 OC3(2 A,3000 ms):同上时序(2.1 A) + * TC-PROTDIM-006 OL(1 A,10 s):1.05 A 启动、9999/10001 ms 时序、 + * 0.96 保持(ret 0.95)、0.93 释放 + * TC-PROTDIM-007 OV(120 V,10 s):100 V 静默;121 V 启动、到点动作; + * 115 保持(ret 114)、113 释放 + * TC-PROTDIM-008 UV(20 V,10 s,欠量方向):21 V 静默;19.9 V 启动、 + * 到点动作;20.5 保持(ret 21)、21.6 释放 + * TC-PROTDIM-009 3U0(15 V,5 s):14 V 静默;15.1 启动到点; + * 14.4 保持(ret 14.25)、14.1 释放 + * TC-PROTDIM-010 聚合与独立性:OC1+OV 同拍动作位图 = 双位;撤 IA → + * OC1 返回、OV 保持(互不污染) + * + * 通过:全部 TC PASS(main 返回 0)。 + * 创建日期:2026-10-25(道A 卡 7) + */ + +#include +#include +#include +#include + +/* 桩头先于生产源码 */ +#include "dbg_log.h" +int g_dimtest_err_count = 0; +int g_dimtest_info_count = 0; +int g_dimtest_debug_count = 0; + +#include "cfg_file.h" + +static int s_reg_count = 0; + +int cfg_file_register(const cfg_section_reg_t *p_reg) { + (void)p_reg; + s_reg_count++; + return 0; +} + +/* V0.1 权威来源表(docs/硬件平台/ADC通道映射说明.md §3/§5,同 adc_cal 套件桩) */ +#include "adc_input.h" + +static const adc_channel_desc_t s_desc_stub[ADC_SEM_COUNT] = { + { ADC_SRC_TPAFE, 6u }, { ADC_SRC_STM32, 14u }, { ADC_SRC_STM32, 3u }, + { ADC_SRC_TPAFE, 5u }, { ADC_SRC_TPAFE, 2u }, { ADC_SRC_STM32, 6u }, + { ADC_SRC_STM32, 1u }, { ADC_SRC_STM32, 2u }, { ADC_SRC_STM32, 5u }, +}; + +const adc_channel_desc_t *adc_input_desc(adc_semantic_t ch) { + if ((unsigned)ch >= (unsigned)ADC_SEM_COUNT) { + return NULL; + } + return &s_desc_stub[(unsigned)ch]; +} + +/* ---- 测试框架(同 prot_elm 套件) ---- */ +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) + +/* ---- 合成"标定后"板级口径(覆写 adc_cal.h 的 ROM 常量,必须在纳入 .c 之前) ---- + * K_DIM = 655360000 Q16 = 10000 µV/码 = 10 mV/码: + * 电压类 100 V rms → 峰值 141.42 V → 14142 码(TPAFE 半量程内,量级刻意 + * 贴近卡 12 端子变比标定后的真实形态);电流类二次值经变换器落到同一定标。 + * B_IA = -30 码(零点偏移,经 apply 精确对消——验证 b 参与全链而非仅落盘)。 */ +#include "adc_cal.h" +#undef ADC_CAL_K_Q16_TPAFE +#undef ADC_CAL_K_Q16_STM32 +#define ADC_CAL_K_Q16_TPAFE (655360000L) +#define ADC_CAL_K_Q16_STM32 (655360000L) +#define K_DIM ADC_CAL_K_Q16_TPAFE +#define B_IA_DIM (-30L) + +/* ===================== 被测生产源文件 ===================== */ +#include "prot_set.c" +#include "sample_ring.c" +#include "phasor.c" +#include "freq.c" +#include "prot_elm.c" +#include "adc_cal.c" + +/* ===================== 波形/时基(同 prot_elm 套件 run() 级用例口径) ===================== */ +static uint32_t s_t = 0u; + +static long my_lround(double x) { return (long)((x >= 0.0) ? (x + 0.5) : (x - 0.5)); } + +/* 显式换算(卡 7 的核心手法,两处方向都要有): + * code = round(eng[V 或 A] × 65536×1e6 / k_q16) − b (期望 → 码值) + * eng = (code + b) × k_q16 / 65536 / 1e6 = adc_cal_apply (码值 → 工程量) + * 上面两式必须互逆——若实现(adc_cal_apply)与期望推导脱钩,全部 TC 翻转。 */ +static int32_t dim_code(double eng, double b_codes) { + return (int32_t)(my_lround(eng * 65536.0e6 / (double)K_DIM) - b_codes); +} + +/* 一拍:三相量按 50 Hz 正弦(峰值 = rms×√2),其余电气量通道给 0 码值, + * TEMP 透传 0;随后跑真实 L2 链。 */ +static void dim_beat(double ia_rms, double ua_rms, double un_rms) { + float frame[ADC_SEM_COUNT]; + double s50 = sin(2.0 * 3.14159265358979 * 50.0 * (double)s_t / 1000.0); + int32_t ci_ia, ci_ua, ci_un; + uint8_t c; + + ci_ia = dim_code(ia_rms * 1.4142135623730951 * s50, (double)B_IA_DIM); + ci_ua = dim_code(ua_rms * 1.4142135623730951 * s50, 0.0); + ci_un = dim_code(un_rms * 1.4142135623730951 * s50, 0.0); + + frame[ADC_SEM_IA] = adc_cal_apply(ADC_SEM_IA, ci_ia); + frame[ADC_SEM_UA] = adc_cal_apply(ADC_SEM_UA, ci_ua); + frame[ADC_SEM_UN] = adc_cal_apply(ADC_SEM_UN, ci_un); + for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) { + if ((c != (uint8_t)ADC_SEM_IA) && (c != (uint8_t)ADC_SEM_UA) && + (c != (uint8_t)ADC_SEM_UN)) { + frame[c] = adc_cal_apply((adc_semantic_t)c, 0L); + } + } + (void)sample_ring_push_frame(frame, (uint8_t)ADC_SEM_COUNT); + phasor_run(); + freq_run(); + prot_elm_run(s_t); + s_t++; +} + +/* TC 起手:定值/元件全复位 + 标定注入(set 正规路径)+ 中性 45 拍收敛 */ +static void dim_setup(void) { + prot_set_init(); + adc_cal_init(); + (void)adc_cal_set(ADC_SEM_UA, K_DIM, 0L); + (void)adc_cal_set(ADC_SEM_UN, K_DIM, 0L); + (void)adc_cal_set(ADC_SEM_IA, K_DIM, B_IA_DIM); + prot_elm_init(); +} + +static void set_en(prot_set_id_t id, int on) { + (void)prot_set_set(id, PROT_SET_Q10(on ? 1.0 : 0.0)); +} + +/* 只留一个电气量元件家族:默认全 EN=1,逐个关到只剩目标 */ +static void isolate_oc_family(void) { + set_en(PS_OC1_EN, 1); set_en(PS_OC2_EN, 0); set_en(PS_OC3_EN, 0); + set_en(PS_OL_EN, 0); set_en(PS_OV_EN, 0); set_en(PS_UV_EN, 0); + set_en(PS_3U0_EN, 0); +} + +static int pick_of(prot_elm_id_t e) { + prot_elm_state_t st; + prot_elm_state(e, &st); + return (st.picked != 0u) ? 1 : 0; +} + +static int assert_of(prot_elm_id_t e) { + prot_elm_state_t st; + prot_elm_state(e, &st); + return (st.asserted != 0u) ? 1 : 0; +} + +/* 把 IA 拉到 far_below 收敛 45 拍(确保 picked 干净归零、t0 无效) */ +static void settle_oc(double ia_rms, uint32_t beats) { + uint32_t i; + for (i = 0u; i < beats; i++) { dim_beat(ia_rms, 100.0, 0.0); } +} + +/* ===================== TC-PROTDIM-001 标定链基线 ===================== */ +static void tc_001_baseline(void) { + phasor_t p; + double want_rms; + double got; + uint32_t i; + int quiet = 1; + + printf("[TC-PROTDIM-001] calibrated chain baseline: no element moves\n"); + dim_setup(); + for (i = 0u; i < 60u; i++) { dim_beat(0.5, 100.0, 0.0); } + + /* 换算链可验证:C01-IA(idx0)有效值 = 注入 0.5 A ±0.5 % + * (b=-30 码应被 apply 精确对消;若折算丢失,这里会是 ~700 量级码值) */ + want_rms = 0.5; + CHECK(phasor_get(0u, &p) == 0, "001: C01-IA phasor ready"); + got = (double)p.fAmSQ; + CHECK((got > want_rms * 0.995) && (got < want_rms * 1.005), + "001: IA rms = 0.5 A (secondary domain, b cancelled)"); + + for (i = 0u; i < (uint32_t)PE_OC1; i++) { + if ((pick_of((prot_elm_id_t)i) != 0) || (assert_of((prot_elm_id_t)i) != 0)) { + quiet = 0; + } + } + CHECK(quiet, "001: OC1/OC2/OC3/OL/OV/UV/3U0 all silent at 0.5A/100V/0V"); + CHECK(prot_elm_active_mask() == 0u, "001: active mask 0"); +} + +/* ===================== TC-PROTDIM-002 反错配对照 ===================== */ +static void tc_002_mismatch_trap(void) { + float raw[ADC_SEM_COUNT]; + uint32_t i; + + printf("[TC-PROTDIM-002] raw-codes-as-engineering trap / apply guard\n"); + dim_setup(); + isolate_oc_family(); + set_en(PS_OC2_EN, 0); + + /* (a) 复现历史事故:把"码值本身"当工程量灌环(等价于折算点被旁路)。 + * 3.36 A 物理电流的码值 ~672 → 比 OC1(10 A) → 必假动作 */ + for (i = 0u; i < (uint32_t)ADC_SEM_COUNT; i++) { + raw[i] = 0.0f; + } + for (i = 0u; i < 45u; i++) { + double s50 = sin(2.0 * 3.14159265358979 * 50.0 * (double)s_t / 1000.0); + raw[ADC_SEM_IA] = (float)((double)dim_code(3.36 * 1.4142135623730951 * s50, + (double)B_IA_DIM)); + (void)sample_ring_push_frame(raw, (uint8_t)ADC_SEM_COUNT); + phasor_run(); + prot_elm_run(s_t); + s_t++; + } + CHECK((pick_of(PE_OC1) != 0) && (assert_of(PE_OC1) != 0), + "002a: raw code domain falsely trips OC1 (documents the incident)"); + + /* (b) 同一物理量经正规折算链:3.36 A < 10 A → 必须静默。 + * (若生产链有人绕过 adc_cal_apply,本断言与 001 一起翻红) */ + for (i = 0u; i < 45u; i++) { dim_beat(3.36, 100.0, 0.0); } + CHECK((pick_of(PE_OC1) == 0) && (assert_of(PE_OC1) == 0), + "002b: same physical current via apply() stays silent"); +} + +/* ===================== TC-PROTDIM-003 OC1:滞回 + dly0 ===================== */ +static void tc_003_oc1(void) { + printf("[TC-PROTDIM-003] OC1 10A/dly0 hysteresis\n"); + dim_setup(); + isolate_oc_family(); + + settle_oc(3.0, 45u); + settle_oc(9.995, 45u); + CHECK(pick_of(PE_OC1) == 0, "003: 9.995 A below set: no pickup"); + + settle_oc(10.5, 45u); + CHECK((pick_of(PE_OC1) != 0) && (assert_of(PE_OC1) != 0), + "003: 10.5 A -> picked AND asserted (dly=0 same-beat)"); + + settle_oc(9.6, 45u); + CHECK((pick_of(PE_OC1) != 0) && (assert_of(PE_OC1) != 0), + "003: 9.6 A inside hysteresis (ret=9.5): holds"); + + settle_oc(9.49, 45u); + CHECK((pick_of(PE_OC1) == 0) && (assert_of(PE_OC1) == 0), + "003: 9.49 A below return threshold: drops out"); +} + +/* 过量定时限通用核:启动拍受控(先远低收敛),到点前后各验一次 */ +static void timed_over_case(prot_elm_id_t e, prot_set_id_t cur_id, double trip, + double hold, double drop, float dly_ms, + const char *tag) { + uint32_t t_pick = 0u; + uint32_t i; + uint32_t settle; + char nm[96]; + int pre_ok = 1, post_ok = 0, hold_ok = 0, drop_ok = 0, trip_ok = 0; + + (void)cur_id; + (void)tag; + /* 1) 远低收敛;2) 阶跃到 trip,逐拍轮询到 picked(记录启动拍) */ + for (i = 0u; i < 45u; i++) { dim_beat(0.1 * trip, 100.0, 0.0); } + for (i = 0u; i < 400u; i++) { + dim_beat(trip, 100.0, 0.0); + if (pick_of(e) != 0) { t_pick = s_t - 1u; trip_ok = 1; break; } + } + if (trip_ok == 0) { + (void)snprintf(nm, sizeof(nm), "%s: step to trip level picks up", tag); + CHECK(0, nm); + return; + } + /* 3) 到点前 2 ms 不许动作、到点后 2 ms 必须动作(同幅值持续注入) */ + settle = 0u; + for (i = t_pick; i < (t_pick + (uint32_t)dly_ms - 2u); i++) { + dim_beat(trip, 100.0, 0.0); + settle++; + } + (void)settle; + if (assert_of(e) != 0) { pre_ok = 0; } + for (i = 0u; i < 5u; i++) { dim_beat(trip, 100.0, 0.0); } + post_ok = assert_of(e); + + (void)snprintf(nm, sizeof(nm), "%s: no assert before setpoint-2ms", tag); + CHECK(pre_ok != 0 && post_ok != 0, nm); + + /* 4) 返回系数滞回带内保持、跌破释放 */ + for (i = 0u; i < 45u; i++) { dim_beat(hold, 100.0, 0.0); } + hold_ok = (pick_of(e) != 0) && (assert_of(e) != 0); + for (i = 0u; i < 45u; i++) { dim_beat(drop, 100.0, 0.0); } + drop_ok = (pick_of(e) == 0) && (assert_of(e) == 0); + + (void)snprintf(nm, sizeof(nm), "%s: holds inside return band", tag); + CHECK(hold_ok != 0, nm); + (void)snprintf(nm, sizeof(nm), "%s: returns below return threshold", tag); + CHECK(drop_ok != 0, nm); +} + +/* ===================== TC-PROTDIM-004~006 OC2/OC3/OL ===================== */ +static void tc_004_oc2(void) { + printf("[TC-PROTDIM-004] OC2 5A/500ms timed+return\n"); + dim_setup(); + isolate_oc_family(); + set_en(PS_OC2_EN, 1); + timed_over_case(PE_OC2, PS_OC2_CUR, 6.0, 4.8, 4.6, 500.0f, "004:OC2"); +} + +static void tc_005_oc3(void) { + printf("[TC-PROTDIM-005] OC3 2A/3000ms timed+return\n"); + dim_setup(); + isolate_oc_family(); + set_en(PS_OC3_EN, 1); + timed_over_case(PE_OC3, PS_OC3_CUR, 2.1, 1.95, 1.89, 3000.0f, "005:OC3"); +} + +static void tc_006_ol(void) { + printf("[TC-PROTDIM-006] OL 1A/10s timed+return\n"); + dim_setup(); + isolate_oc_family(); + set_en(PS_OL_EN, 1); + timed_over_case(PE_OL, PS_OL_CUR, 1.05, 0.96, 0.93, 10000.0f, "006:OL"); +} + +/* ===================== TC-PROTDIM-007 OV(电压域走 UA) ===================== */ +static void tc_007_ov(void) { + uint32_t t_pick = 0u; + uint32_t i; + int ok; + + printf("[TC-PROTDIM-007] OV 120V/10s (UA secondary domain)\n"); + dim_setup(); + isolate_oc_family(); + set_en(PS_OV_EN, 1); + + for (i = 0u; i < 45u; i++) { dim_beat(0.0, 100.0, 0.0); } + CHECK(pick_of(PE_OV) == 0, "007: nominal 100 V silent"); + for (i = 0u; i < 400u; i++) { + dim_beat(0.0, 121.0, 0.0); + if (pick_of(PE_OV) != 0) { t_pick = s_t - 1u; break; } + } + CHECK(pick_of(PE_OV) != 0, "007: 121 V picks up"); + for (i = t_pick; i < (t_pick + 9998u); i++) { dim_beat(0.0, 121.0, 0.0); } + ok = (assert_of(PE_OV) == 0); + for (i = 0u; i < 5u; i++) { dim_beat(0.0, 121.0, 0.0); } + CHECK((ok != 0) && (assert_of(PE_OV) != 0), + "007: asserts exactly at +10000ms window (not earlier)"); + for (i = 0u; i < 45u; i++) { dim_beat(0.0, 115.0, 0.0); } + CHECK(assert_of(PE_OV) != 0, "007: 115 V holds (ret=114)"); + for (i = 0u; i < 45u; i++) { dim_beat(0.0, 113.0, 0.0); } + CHECK((pick_of(PE_OV) == 0) && (assert_of(PE_OV) == 0), + "007: 113 V returns"); +} + +/* ===================== TC-PROTDIM-008 UV(欠量方向) ===================== */ +static void tc_008_uv(void) { + uint32_t t_pick = 0u; + uint32_t i; + int ok; + + printf("[TC-PROTDIM-008] UV 20V/10s under-direction\n"); + dim_setup(); + isolate_oc_family(); + set_en(PS_UV_EN, 1); + + for (i = 0u; i < 45u; i++) { dim_beat(0.0, 21.0, 0.0); } + CHECK(pick_of(PE_UV) == 0, "008: 21 V above set(20): silent"); + for (i = 0u; i < 400u; i++) { + dim_beat(0.0, 19.9, 0.0); + if (pick_of(PE_UV) != 0) { t_pick = s_t - 1u; break; } + } + CHECK(pick_of(PE_UV) != 0, "008: 19.9 V picks up"); + for (i = t_pick; i < (t_pick + 9998u); i++) { dim_beat(0.0, 19.9, 0.0); } + ok = (assert_of(PE_UV) == 0); + for (i = 0u; i < 5u; i++) { dim_beat(0.0, 19.9, 0.0); } + CHECK((ok != 0) && (assert_of(PE_UV) != 0), "008: asserts at +10000ms"); + for (i = 0u; i < 45u; i++) { dim_beat(0.0, 20.5, 0.0); } + CHECK(assert_of(PE_UV) != 0, "008: 20.5 V holds under return(21)"); + for (i = 0u; i < 45u; i++) { dim_beat(0.0, 21.6, 0.0); } + CHECK((pick_of(PE_UV) == 0) && (assert_of(PE_UV) == 0), "008: 21.6 V returns"); +} + +/* ===================== TC-PROTDIM-009 3U0(UN 通道) ===================== */ +static void tc_009_3u0(void) { + uint32_t t_pick = 0u; + uint32_t i; + int ok; + + printf("[TC-PROTDIM-009] 3U0 15V/5s via UN channel\n"); + dim_setup(); + isolate_oc_family(); + set_en(PS_3U0_EN, 1); + + for (i = 0u; i < 45u; i++) { dim_beat(0.0, 100.0, 14.0); } + CHECK(pick_of(PE_3U0) == 0, "009: 14 V below set(15): silent"); + for (i = 0u; i < 400u; i++) { + dim_beat(0.0, 100.0, 15.1); + if (pick_of(PE_3U0) != 0) { t_pick = s_t - 1u; break; } + } + CHECK(pick_of(PE_3U0) != 0, "009: 15.1 V picks up"); + for (i = t_pick; i < (t_pick + 4998u); i++) { dim_beat(0.0, 100.0, 15.1); } + ok = (assert_of(PE_3U0) == 0); + for (i = 0u; i < 5u; i++) { dim_beat(0.0, 100.0, 15.1); } + CHECK((ok != 0) && (assert_of(PE_3U0) != 0), "009: asserts at +5000ms"); + for (i = 0u; i < 45u; i++) { dim_beat(0.0, 100.0, 14.4); } + CHECK(assert_of(PE_3U0) != 0, "009: 14.4 V holds (ret=14.25)"); + for (i = 0u; i < 45u; i++) { dim_beat(0.0, 100.0, 14.1); } + CHECK((pick_of(PE_3U0) == 0) && (assert_of(PE_3U0) == 0), "009: 14.1 V returns"); +} + +/* ===================== TC-PROTDIM-010 聚合位图与独立性 ===================== */ +static void tc_010_mask(void) { + uint32_t i; + + printf("[TC-PROTDIM-010] dual assert mask independence\n"); + dim_setup(); + isolate_oc_family(); + set_en(PS_OV_EN, 1); + (void)prot_set_set(PS_OV_DLY, PROT_SET_Q10(0.0)); /* 同拍聚合,聚焦位图 */ + + for (i = 0u; i < 45u; i++) { dim_beat(10.5, 121.0, 0.0); } + CHECK((assert_of(PE_OC1) != 0) && (assert_of(PE_OV) != 0), + "010: OC1+OV both asserted"); + CHECK(prot_elm_active_mask() == ((1uL << PE_OC1) | (1uL << PE_OV)), + "010: active mask = bit0|bit4 exactly"); + + for (i = 0u; i < 45u; i++) { dim_beat(3.0, 121.0, 0.0); } + CHECK((assert_of(PE_OC1) == 0) && (assert_of(PE_OV) != 0), + "010: IA withdrawn -> OC1 returns, OV untouched"); + CHECK(prot_elm_active_mask() == (1uL << PE_OV), "010: mask = OV only"); +} + +int main(void) { + printf("=== Prot Dim Regression Tests ===\n"); + adc_cal_init(); /* 先把 RAM 系数置成合成 ROM(各 TC 内还会重跑) */ + tc_001_baseline(); + tc_002_mismatch_trap(); + tc_003_oc1(); + tc_004_oc2(); + tc_005_oc3(); + tc_006_ol(); + tc_007_ov(); + tc_008_uv(); + tc_009_3u0(); + tc_010_mask(); + printf("\n=== Prot Dim Regression Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/prot_dim/run_unit_test.py b/test/host/prot_dim/run_unit_test.py new file mode 100644 index 00000000..266c339a --- /dev/null +++ b/test/host/prot_dim/run_unit_test.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""量纲回归(卡 7,道A)主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + MSVC 编译 test/host/prot_dim/prot_dim_test.c(#include 纳入真实链: + adc_cal + prot_set + sample_ring + phasor + freq + prot_elm),在 PC 上 + 用"码值→二次值显式换算"的全链路合成波驱动七类保护元件,钉死 + 「不该动作时不动作」。工具链细节委托 unit_test_harness.py。 + +为什么要这个套件(数据可信 §2.2 / 卡 7): + 台架此前"通过"是因为没有一条判据检查不该动作时不动作——"码值当工程量" + 的错配曾让 OC 恒假动作。本套件把换算写进用例:折算点被旁路或量纲再错位, + 断言立刻翻转;纯合成数据,零硬件零台架。 + +用法(仓库根目录): + python test/host/prot_dim/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 +设计依据:docs/任务规划/数据可信.md 卡 7(M4 退出判据⑥); +创建日期: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 覆盖);App/algo 供被测链与 adc_input.h; +# App/cfg 供 cfg_file.h(adc_cal.c 的接口头,注册函数打桩)。 +BUILD = TestBuild( + name="Prot Dim Regression Tests", + src=os.path.join(HERE, "prot_dim_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "App", "algo"), + os.path.join(ROOT, "App", "cfg"), + HERE, + ], + summary_marker="=== Prot Dim Regression Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/prot_dim/stub/dbg_log.h b/test/host/prot_dim/stub/dbg_log.h new file mode 100644 index 00000000..f2b57dcb --- /dev/null +++ b/test/host/prot_dim/stub/dbg_log.h @@ -0,0 +1,21 @@ +/* + * 主机侧单测桩:dbg_log.h(prot_dim 量纲回归套件) + * 模块功能:为 adc_cal.c 提供主机可编译的日志宏(映射为计数)。 + * ⚠ 仅供 test/host/prot_dim/ 使用,不参与 MCU 构建。同 adc_cal 套件先例。 + * 创建日期:2026-10-25 + */ + +#ifndef DBG_LOG_H +#define DBG_LOG_H + +#include + +extern int g_dimtest_err_count; +extern int g_dimtest_info_count; +extern int g_dimtest_debug_count; + +#define DBG_INFO(fmt, ...) do { g_dimtest_info_count++; } while (0) +#define DBG_DEBUG(fmt, ...) do { g_dimtest_debug_count++; } while (0) +#define DBG_ERROR(fmt, ...) do { g_dimtest_err_count++; } while (0) + +#endif /* DBG_LOG_H */