diff --git a/App/algo/README.md b/App/algo/README.md index 12215a57..38236049 100644 --- a/App/algo/README.md +++ b/App/algo/README.md @@ -11,7 +11,7 @@ |---|---|---|---|---| | `adc_input.h` / `.c` | 语义取数与有效性 | 统一语义取数:两片转换器的码值按语义(`UA…IN` / 温度)暴露,并消费 STM32 ADC1 的 DMA 乒乓缓冲 | `adc_input_init()` `adc_input_poll()` `adc_input_raw()` | TPAFE 快照消费 / STM32 ADC1 启动与扫描 / 通道语义映射 | | `adc_health.h` / `.c` | 语义取数与有效性 | 采样有效性判定:吃计数快照,产出静默 / 丢帧 / 单调性 / 错误增量故障位(判定与告警编排分离);「采样通道自检」残余的码值贴轨观察器同层 | `adc_health_check()` `adc_health_flags()` `adc_health_rail_watch()` | 「采样有效性校验」/「采样通道自检」(残余) | -| `adc_cal.h` / `.c` | 标定折算 | 通道标定折算(「数据可信」标定折算与落盘整定):工程量 =(code+b)×k,Q16 系数/零点两段落盘 + 三态 flags + 合法带哨兵 + 两点公式 | `adc_cal_apply()` `adc_cal_flags()` `adc_cal_set()` | 「通道标定(一次系数 + 零漂)」(M4 半边) | +| `adc_cal.h` / `.c` | 标定折算 | 通道标定折算(「数据可信」标定折算与落盘整定):工程量 =(code+b)×k×r,k(前端斜率)/ b(零点)/ r(端子变比)三维、三段 cfg 落盘 + 三态 flags + 合法带哨兵 + 两点公式(标的是 r) | `adc_cal_apply()` `adc_cal_flags()` `adc_cal_set()` `adc_cal_set_ratio()` `adc_cal_ratio_configured()` | 「通道标定(一次系数 + 零漂)」(M4 半边) | | `meas.h` / `.c` | 全周傅氏相量发布 | 遥测快照(「数据可信」遥测快照发布,M5 地基):200 ms 基波有效值 + 品质快照,双区无锁读,全仓唯一发布者 = adcTask | `meas_tick()` `meas_get()` `meas_seq()` | M5(104 改读快照与频率点绑定、UI 是读者) | | `sample_ring.h` / `.c` | 全周傅氏相量输入 | 按语义通道存最近 2 周波(40 点),供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | 全周傅氏相量输入(为「每 1 ms 滑动窗刷新」供数) | | `phasor.h` / `.c` | 全周傅氏相量 | 全周傅氏:一周波 20 点 DFT → 基波 / 谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | 全周傅氏系数表 / 基波相量实例 / 相量有效值输出 / 每 1 ms 滑动窗刷新 / 双区指针原子交换 | @@ -106,15 +106,18 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」 - **ROM 默认 = 量程口径冻结定死的引脚侧 LSB**(TPAFE 152.59 / STM32 805.66 µV/码, `docs/硬件平台/ADC通道映射说明.md` §7.1);表下标 = 语义枚举(UA0…IN7), V0.2 改版与本目录 `adc_input` 映射表同步改。 -- 三态可用性 `adc_cal_flags()`:VALID(已按变比标定,二次值成立——端子变比确认与两点标定前恒 0) +- 三态可用性 `adc_cal_flags()`:VALID(已两点标定确认:k/b 在带内 **且** 变比 r 已配置) / CLIPPED(运行期按来源派生,V0.2 后自动清零)。**消费者判「能不能信」只许查它**, `adc_cal_apply()` 不返回可用性(docs/算法/采集与算法层设计说明.md §13.3 三态可用性表)。 - 合法带哨兵:k ∉ ROM×[0.5,2.0] 或 |b| > 8192 → 折算回退 ROM + 每通道每开机一条 `[ERR]` + VALID 被压制(**即便文件位图说已标定**);这层兜底同时挡坏文件与坏标定。 -- `adc_cal_set()` / `adc_cal_two_point()`:整定与两点公式的算术核已齐备(单测还原合成 - k/b);**台架两点标定待实测**,本层不含注入。 -- cfg 两段 `[adc_cal]` / `[adc_cal_ofs]` **不挂回调**(RAM 副本即生效值,引擎 load/poll - 原地改写,逐点现读);注册在 `app_main_init()` 内核前完成,必须先于 `cfg_file_load`。 + 变比 r 是另一条独立哨兵:不在 `0` 或 `≥ 1.0` 的合法带里 → 该路**按不折算处理** + (停在引脚电压,不回退),同样不报 VALID。 +- `adc_cal_set()` / `adc_cal_two_point()` / `adc_cal_set_ratio()`:整定、两点公式(标 r) + 与变比写入三个入口齐备(单测还原合成 k/b/r);**台架两点标定待实测**,本层不含注入。 +- cfg 三段 `[adc_cal]` / `[adc_cal_ofs]` / `[adc_cal_sec]` **不挂回调**(RAM 副本即生效值, + 引擎 load/poll 原地改写,逐点现读);注册在 `app_main_init()` 内核前完成, + 必须先于 `cfg_file_load`。 ### 3.9 `meas.c` / `.h` — 遥测快照发布(全周傅氏相量发布侧,「数据可信」波次) - **唯一发布者 = adcTask**:`phasor_run()`/`freq_run()` 同拍、200 ms **tick 经过**分频 @@ -163,11 +166,11 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」 - **只做字节**:文件落点、路径拼法、标定系数、数据本体全部由调用方注入(meta 由 adcTask 填、 数据经 `wave_reader_fn` 回调取、路径由落盘道拼)→ 两份文件零平台依赖,主机单测免桩 `#include` 原样纳入;`wave_store_meta_init()` 只填格式侧静态事实(并把通道表 `sem` 钉成下标、按归属填 - `conv` / `bits`),`avail` / `k_q16` / `b_codes` 一律留 0 等封存瞬间现取现填——不复制第二个事实源。 -- **尺寸等式**(单测钉死,落盘道拿它比对写回字节数):整段 = 32 + 12×通道数 + 2×点数×通道数 + 2; - 当前口径(8 路 320 点)= 头 128 + 数据 5120 + 尾 CRC 2 = **5250 字节**(这条等式是给落盘道比对写回 - 字节数用的):FatFs 该移植在簇分配失败时会返回 FR_OK + 短写且无错误码,判成功必须校验字节数 - (故障录波设计说明「四条已知陷阱」第二条)。 + `conv` / `bits`),`avail` / `k_q16` / `b_codes` / `r_q16` 一律留 0 等封存瞬间现取现填——不复制第二个事实源。 +- **尺寸等式**(单测钉死,落盘道拿它比对写回字节数):整段 = 32 + 16×通道数 + 2×点数×通道数 + 2; + 当前口径(8 路 320 点、格式 v2)= 头 160 + 数据 5120 + 尾 CRC 2 = **5282 字节**(v1 通道表 12 B/路时 + 是 5250 字节;这条等式是给落盘道比对写回字节数用的):FatFs 该移植在簇分配失败时会返回 + FR_OK + 短写且无错误码,判成功必须校验字节数(故障录波设计说明「四条已知陷阱」第二条)。 - **CRC-16/MODBUS 自带无表实现**(反射多项式 0xA001、初值 0xFFFF、异或输出 0,自检锚点字符串 "123456789" = 0x4B37)。设计说明 §6.2 原写「与屏板链路同一张表」经全仓实测**不成立**(这张表不 存在:crc.c 与 crc.h 已于 2026-10-02 删除,屏板侧 `remo_crc()` 是单字节异或)——文档等文档道回改, @@ -178,7 +181,7 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」 写得出、PC 读不了」。反过来**不设**任何品质侧不变式:`avail` 同时带有效与削顶是合法事实 (lead 裁定 2026-10-29),编码器如实透传、解码器不许据此判废。 - **游标分次搬运**(256 B 缓冲够用,整段绝不驻留 RAM;数据 CRC 走增量中间值): - `wave_store_pack_next()` 头那一步要求缓冲容量不小于整头 128,否则返 `WAVE_ST_E_PARAM` 且**不推进 + `wave_store_pack_next()` 头那一步要求缓冲容量不小于整头 160(v1 是 128),否则返 `WAVE_ST_E_PARAM` 且**不推进 游标**;没有数据源却进数据区 → `WAVE_ST_E_STATE` 且**不产空字节**(产空字节等于写出「长度对、 内容全 0」的段 = docs/子系统设计/故障录波设计说明.md §8.1 明令禁止的伪造证据)。 - 段名严格 8.3(该移植 `FF_USE_LFN=0`):`wave_store_seg_path()` 当前布局恒返回 16 个字符(不含结束 diff --git a/App/algo/adc_cal.c b/App/algo/adc_cal.c index 3cc27f3f..15e8edd7 100644 --- a/App/algo/adc_cal.c +++ b/App/algo/adc_cal.c @@ -1,18 +1,25 @@ /* * 模块名称:通道标定与量纲折算(标定折算)——实现 - * 模块功能:见 adc_cal.h。折算模型(工程量 = (code + b) × k × 1e-6,k 为 Q16 - * µV/码)与两段 cfg 布局([adc_cal]:k_* + valid 位图;[adc_cal_ofs]: - * b_*)照 docs/算法/采集与算法层设计说明.md §13.2 / §4.4 落地;系数口径 + * 模块功能:见 adc_cal.h。折算模型(工程量 = (code + b) × k × 1e-6 × r, + * k 为 Q16 µV/码、r 为 Q16 比值)与三段 cfg 布局([adc_cal]:k_* + valid 位图; + * [adc_cal_ofs]:b_*;[adc_cal_sec]:r_*)照 + * docs/算法/采集与算法层设计说明.md §13.2 / §4.4 落地;k 口径 * (ROM 默认 = 量程口径冻结定死的引脚侧 LSB)见 docs/硬件平台/ADC通道映射说明.md - * §7.1。 + * §7.1,r 口径(出厂默认 = 台架实测反算的端子↔引脚变比)见该文档 §7.2。 + * 三维各管一件事:k = 前端斜率(板级事实,ROM 固化)、b = 零点、 + * r = 端子↔引脚变比(现场标定产物)。**不合并**——端子域 k 的 Q16 溢出 + * int32(UA 152587.9 µV/码 → 1.0e10 > 2.147e9),且合并会把硬件分压比 + * 伪装成前端斜率(§7.2 明令禁止「用系数凑硬件」)。 * 并发契约:cfg RAM 副本属主 = 配置引擎(defaultTask 的 load/poll 原地 * 改写),本层逐点折算现读——int32 对齐字在 Cortex-M4 上读写原子, * poll 重载「复位默认 + 逐键解析」存在 µs 级窗口,撞上则该拍该通道 * 为默认值/半新值(相量 20 点窗摊薄,且触发面 = 用户改文件后 4 s * 沉降一次性事件)——docs/算法/采集与算法层设计说明.md §13.8 明载此设计(不挂回调)。 * 合法带哨兵(防损坏文件/错误标定把假电压送进下游):k ∉ ROM×[0.5,2.0] - * 或 |b| > ADC_CAL_B_MAX → 该通道折算回退 ROM + b=0,flags 不报 - * VALID,每通道每次开机首见沿打一条 [ERR](adc_cal_init 重置留痕)。 + * 或 |b| > ADC_CAL_B_MAX → 该通道折算回退 ROM + b=0;r 不在 + * [65536, ADC_CAL_R_MAX] → 该路按 1.0 处理(输出停在引脚电压)。 + * 两种越带都让 flags 不报 VALID,每通道每次开机首见沿打一条 [ERR] + * (adc_cal_init 重置留痕)。 * 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU) * 作者:DTU 项目组 * 创建日期:2026-10-25(道A 标定折算与落盘整定) @@ -30,6 +37,10 @@ * v1.3 2026-10-29 DTU 项目组 故障录波 task-2:新增 adc_cal_k_q16() int32 原值直读口 * (段头 k 字段 LSB 精确,float 反推在 5.28e7 掉尾数, * 口径见 docs/子系统设计/故障录波设计说明.md §6.3) + * v2.0 2026-11-11 DTU 项目组 新增变比维 r([adc_cal_sec] 一段 8 键): + * 折算 (code+b)×k×1e-6×r;+adc_cal_r_uv/r_q16/ + * ratio_configured/set_ratio;two_point 改标 r; + * VALID 追加「r 已配置」条件 */ /* 头文件包含区 */ @@ -45,15 +56,19 @@ /* 常量定义区 */ -/* 段名(≤ CFG_FILE_MAX_NAME_LEN)与键名(k_ua…b_in,均 4/5 字符) +/* 段名(≤ CFG_FILE_MAX_NAME_LEN)与键名(k_ua…b_in…r_in,均 4/5 字符) * ——docs/算法/采集与算法层设计说明.md §13.8 布局;键的书写顺序按该表,存储下标按语义枚举 */ #define ADC_CAL_SEC_K "adc_cal" #define ADC_CAL_SEC_B "adc_cal_ofs" +#define ADC_CAL_SEC_R "adc_cal_sec" /* Q16 → V 的合并比例因子:1e-6 / 65536(float 常量,精确到 1 ulp 以内即可: * 系数本身 Q16 舍入误差 < 0.001 %,见docs/算法/采集与算法层设计说明.md §13.2) */ #define ADC_CAL_K_SCALE (1.52587890625e-11f) +/* Q16 比值 → 无量纲倍率的比例因子:1 / 65536 = 2^-16(二进制精确,无舍入) */ +#define ADC_CAL_R_SCALE (1.52587890625e-05f) + /* 静态变量定义区 */ /* [adc_cal] 段 RAM 副本:k 按语义下标 0..7,valid 位图 bit=语义下标。 @@ -70,8 +85,14 @@ typedef struct { int32_t b[8]; } adc_cal_bcfg_t; +/* [adc_cal_sec] 段 RAM 副本:r 按语义下标 0..7(int32 Q16 比值;0 = 本路不折算) */ +typedef struct { + int32_t r[8]; +} adc_cal_rcfg_t; + static adc_cal_kcfg_t s_kcfg; /* 生效系数副本(引擎原地更新) */ static adc_cal_bcfg_t s_bcfg; /* 生效零点副本 */ +static adc_cal_rcfg_t s_rcfg; /* 生效变比副本 */ /* 工厂默认(ROM 理论系数,量程口径冻结定死,docs/算法/采集与算法层设计说明.md §13.8):**下标 = 语义枚举** * (UA0 / UB1 / UC2 / UN3 / IA4 / IB5 / IC6 / IN7)——UA/UN/IA 走 TPAFE @@ -91,10 +112,27 @@ static const adc_cal_kcfg_t s_kcfg_rom = { }; static const adc_cal_bcfg_t s_bcfg_rom = { { 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L } }; +/* 变比工厂默认:**下标 = 语义枚举**,按族给(UA/UB/UC/UN 电压族、IA/IB/IC/IN 电流族)。 + * 两族单位不同(V/V 与 A/V)——这是它们必须分列而非共用一个数的原因。 + * ⚠ 电流族四路里有三路(IB/IC/IN)在 V0.1 是 STM32 单极性削顶通道,配了变比也测不了 + * 交流量;但**变比照配**——它描述的是端子↔引脚的分压/负担关系,与"这路能不能测" + * 是两件事,等 V0.2 把 8 路全接 TPAFE 后这些值直接继续用。 */ +static const adc_cal_rcfg_t s_rcfg_rom = { + /* idx0 UA / idx1 UB / idx2 UC / idx3 UN —— 电压族 V/V */ + { ADC_CAL_R_Q16_VOLT, ADC_CAL_R_Q16_VOLT, ADC_CAL_R_Q16_VOLT, ADC_CAL_R_Q16_VOLT, + /* idx4 IA / idx5 IB / idx6 IC / idx7 IN —— 电流族 A/V */ + ADC_CAL_R_Q16_CURR, ADC_CAL_R_Q16_CURR, ADC_CAL_R_Q16_CURR, ADC_CAL_R_Q16_CURR } +}; + /* 合法带哨兵留痕位(bit=语义下标;每通道每次开机只打一条 [ERR], * adc_cal_init 清零 = 新一轮观测窗) */ static uint8_t s_err_latched; +/* 变比越带留痕位:**与 s_err_latched 分开**——k/b 越带和「r 配了个不可能的值」 + * 是两件独立的事,共用一位会让先到的那个把后到的永久遮住(同一个通道上 + * 一次开机只留一条痕是刻意的,但两类痕必须各留各的) */ +static uint8_t s_rerr_latched; + /* * 观测计数(三态语义唯一落点在 adc_cal.h,这里只写"怎么自增"):与上面那个留痕位 * 不同,这三个数一个 [ERR] 都不打、也不参与任何判定,因此 **adc_cal_init() 刻意不清** @@ -126,6 +164,18 @@ static const cfg_key_def_t s_b_keys_tbl[8] = { { "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 }, }; +/* 变比键表:键的书写顺序照 §13.8 表(UA/UN/IA 优先——这三路是 V0.1 唯一有效的测量通道), + * 存储下标按语义枚举(同 k 表)。8 键 < CFG_FILE_MAX_KEYS=16,不拆段。 */ +static const cfg_key_def_t s_r_keys_tbl[8] = { + { "r_ua", CFG_VAL_INT, offsetof(adc_cal_rcfg_t, r[0]), 4u }, + { "r_un", CFG_VAL_INT, offsetof(adc_cal_rcfg_t, r[3]), 4u }, + { "r_ia", CFG_VAL_INT, offsetof(adc_cal_rcfg_t, r[4]), 4u }, + { "r_ub", CFG_VAL_INT, offsetof(adc_cal_rcfg_t, r[1]), 4u }, + { "r_uc", CFG_VAL_INT, offsetof(adc_cal_rcfg_t, r[2]), 4u }, + { "r_ib", CFG_VAL_INT, offsetof(adc_cal_rcfg_t, r[5]), 4u }, + { "r_ic", CFG_VAL_INT, offsetof(adc_cal_rcfg_t, r[6]), 4u }, + { "r_in", CFG_VAL_INT, offsetof(adc_cal_rcfg_t, r[7]), 4u }, +}; /* 函数定义区 */ @@ -189,6 +239,19 @@ static void s_note_bad(unsigned sem, int32_t k, int32_t b) { } } +/* + * 函数功能:变比合法带检查(0 = 本路不折算;否则必须 ≥ 1.0) + * 入口参数:r - 待检变比 Q16 int32_t + * 返 回 值:1 - 可折算;0 - 不折算(0 / 负数 / 比值 < 1.0) + * 限定条件:无 + * 函数说明:**上界只在 int64 域查**(ADC_CAL_R_MAX = int32 上限本身, + * int32 里不可能越上界,写成 r > ADC_CAL_R_MAX 是恒假分支、cppcheck 会报 + * knownConditionTrueFalse)。int32 域只守下界 1.0 的理由见 adc_cal.h + */ +static int s_rband_ok(int32_t r) { + return (r >= ADC_CAL_R_MIN) ? 1 : 0; +} + /* * 函数功能:读通道「当前生效且合法」的 k/b(越带就地回退 ROM) * 入口参数:sem - 语义下标 0..7;p_k/p_b - 输出 @@ -211,7 +274,46 @@ static int s_eff_kb(unsigned sem, int32_t *p_k, int32_t *p_b) { } /* - * 函数功能:标定层初始化——RAM 副本置 ROM 默认 + 注册两段进 cfg 引擎(幂等) + * 函数功能:变比越带留痕(每通道每次开机首见沿一条 [ERR],init 重置) + * 入口参数:sem - 语义下标 0..7;r - 现场值(Q16) + * 返 回 值:无 + * 限定条件:仅 s_eff_r 内部 + */ +static void s_note_bad_r(unsigned sem, int32_t r) { + const uint8_t bit = (uint8_t)(1u << sem); + + if ((s_rerr_latched & bit) == 0u) { + s_rerr_latched |= bit; + DBG_ERROR("ch=%u r_q16=%ld below 1.0 ratio -> no scaling (pin volts shown)", + (unsigned)sem, (long)r); + } +} + +/* + * 函数功能:读通道「当前生效」的变比(0 = 本路不折算) + * 入口参数:sem - 语义下标 0..7 + * 返 回 值:Q16 比值;0 = 不折算(配置为 0,或落在合法带外被就地忽略) + * 限定条件:sem < 8 + * 函数说明:**不做 ROM 回退**——这与 s_eff_kb 的关键差别:k 越带必须回退(否则 + * 引脚电压就错了,下游全跟着错),而 r 越带的正确行为是「不折算」, + * 因为折算成什么量纲本来就没有可信答案;r=0 与 r 越带在输出上同义 + * (都停在引脚电压),区别只在于越带会留一条 [ERR] + */ +static int32_t s_eff_r(unsigned sem) { + const int32_t r = s_rcfg.r[sem]; + + if (r == 0L) { + return 0L; + } + if (s_rband_ok(r) != 0) { + return r; + } + s_note_bad_r(sem, r); + return 0L; +} + +/* + * 函数功能:标定层初始化——RAM 副本置 ROM 默认 + 注册三段进 cfg 引擎(幂等) * 入口参数:无 * 返 回 值:无 * 限定条件:内核启动前调用(app_main_init);注册必须先于 cfg_file_load @@ -223,7 +325,9 @@ void adc_cal_init(void) { (void)memcpy(&s_kcfg, &s_kcfg_rom, sizeof(s_kcfg)); (void)memcpy(&s_bcfg, &s_bcfg_rom, sizeof(s_bcfg)); + (void)memcpy(&s_rcfg, &s_rcfg_rom, sizeof(s_rcfg)); s_err_latched = 0u; + s_rerr_latched = 0u; (void)memset(®, 0, sizeof(reg)); reg.name = ADC_CAL_SEC_K; @@ -250,24 +354,49 @@ void adc_cal_init(void) { DBG_ERROR("register '%s' FAIL (dup/full?) - zero-offset file values inactive", ADC_CAL_SEC_B); } + + (void)memset(®, 0, sizeof(reg)); + reg.name = ADC_CAL_SEC_R; + reg.p_data = &s_rcfg; + reg.p_defaults = &s_rcfg_rom; + reg.data_size = sizeof(s_rcfg); /* 32 */ + reg.p_keys = s_r_keys_tbl; + reg.key_count = 8u; + reg.p_cb = NULL; + if (cfg_file_register(®) != 0) { + DBG_ERROR("register '%s' FAIL (dup/full?) - ratio file values inactive, ROM ratio used", + ADC_CAL_SEC_R); + } } /* * 函数功能:码值 → 工程量折算(docs/算法/采集与算法层设计说明.md §13.2 模型) * 入口参数:sem - 语义通道;code - 原始码值 adc_semantic_t / int32_t - * 返 回 值:工程量浮点(未标定 = ADC 引脚电压 V;TEMP/越界 = 原样透传码值) float + * 返 回 值:工程量浮点(r=0 = ADC 引脚电压 V;TEMP/越界 = 原样透传码值) float * 限定条件:无(任意上下文,纯读) - * 函数说明:见 adc_cal.h 同名声明;k/b 经 s_eff_kb 合法带哨兵(越带回退 ROM) + * 函数说明:见 adc_cal.h 同名声明;k/b 经 s_eff_kb 合法带哨兵(越带回退 ROM), + * r 经 s_eff_r(越带/0 → 不折算,即乘 1.0)。两处哨兵都不落共享状态。 + * ⚠ 乘序刻意是 `(码值 × k)` 先算成引脚电压、再乘 r:k 的 Q16 上界 + * (5.28e7)乘 code(±32768)后落在 float 的 2^24 精度区内,而若先把 + * k×r 并成一个大系数(60 万级 Q16 → 4e10)再乘 code,float 只剩 7 位 + * 有效数字,末位抖动会打进 3 位小数的屏面读数 */ float adc_cal_apply(adc_semantic_t sem, int32_t code) { int32_t k; int32_t b; + int32_t r; + float v; 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; + v = ((float)(code + b)) * ((float)k) * ADC_CAL_K_SCALE; + r = s_eff_r((unsigned)sem); + if (r == 0L) { + return v; + } + return v * ((float)r) * ADC_CAL_R_SCALE; } /* @@ -275,7 +404,10 @@ 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 uint32_t * 限定条件:无 - * 函数说明:VALID 需文件位图与 k 合法带**同时**成立(哨兵压制说谎位图,见文件头) + * 函数说明:VALID 需文件位图、k/b 合法带、变比已配置**三者**同时成立 + * (哨兵压制说谎位图,见文件头)。三者里任一不成立就整条不报 VALID—— + * 宁可让现场看到「未标定」,也不能让一个「引脚电压乘以一个猜的变比」 + * 冒充二次值去解锁保护判据 */ uint32_t adc_cal_flags(adc_semantic_t sem) { uint32_t f = 0u; @@ -283,9 +415,10 @@ uint32_t adc_cal_flags(adc_semantic_t sem) { if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) { return 0u; } - /* VALID = 文件位图置位 且 k 合法(哨兵不落共享状态,随重载自愈) */ + /* VALID = 文件位图置位 且 k/b 合法 且 r 已配置(三哨兵都不落共享状态,随重载自愈) */ if (((s_kcfg.valid & (uint8_t)(1u << (unsigned)sem)) != 0u) && - (s_kband_ok((unsigned)sem, s_kcfg.k[sem], s_bcfg.b[sem]) != 0)) { + (s_kband_ok((unsigned)sem, s_kcfg.k[sem], s_bcfg.b[sem]) != 0) && + (s_rband_ok(s_rcfg.r[sem]) != 0)) { f |= ADC_CAL_F_VALID; } /* CLIPPED 由**来源**派生(R5:STM32 侧单极性无偏置 → 负半周削顶)—— @@ -376,6 +509,53 @@ float adc_cal_b_codes(adc_semantic_t sem) { return ((float)b); } +/* + * 函数功能:读该路变比(浮点,V/V 或 A/V) + * 入口参数:sem - 语义通道 adc_semantic_t + * 返 回 值:r 比值;0 = 本路不折算;TEMP/越界返 0 float + * 限定条件:无(任意上下文,纯读) + * 函数说明:与折算同视图(越带即按 0 报)。Q16 还原用 2^-16 精确乘, + * 不写除以 65536.0f——除法在 ARMCC 软浮点路径上多一次舍入且更慢, + * 这里的值域完全用不到"精确除法"那点好处 + */ +float adc_cal_ratio(adc_semantic_t sem) { + if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) { + return 0.0f; + } + return ((float)s_eff_r((unsigned)sem)) * ADC_CAL_R_SCALE; +} + +/* + * 函数功能:读该路变比的 int32 Q16 原值(adc_cal_ratio 孪生,见 .h 声明) + * 入口参数:sem - 语义通道 adc_semantic_t + * 返 回 值:r[Q16];TEMP/越界/带外/0 均返 0 int32_t + * 限定条件:无(任意上下文,纯读) + * 函数说明:与 adc_cal_ratio() 共用 s_eff_r 生效视图,差别只在不做浮点还原—— + * 录波段头按 LSB 精确记账必须走本口(docs/子系统设计/故障录波设计说明.md §6.3) + */ +int32_t adc_cal_ratio_q16(adc_semantic_t sem) { + if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) { + return 0; + } + return s_eff_r((unsigned)sem); +} + +/* + * 函数功能:该路变比是否已配置(**整帧量纲标签的唯一判据源**) + * 入口参数:sem - 语义通道 adc_semantic_t + * 返 回 值:1 - 已配置(该路输出是二次值);0 - 未配置(输出是引脚电压) uint8_t + * 限定条件:无(任意上下文,纯读) + * 函数说明:判据与折算**同源**(都走 s_eff_r),因此不可能出现"标签说二次值、 + * 实际乘的是 1.0"这种分叉——这正是把它做成独立谓词而不是让调用方 + * 自己读 adc_cal_ratio() 比较的原因:调用方那一侧的比较迟早会漂 + */ +uint8_t adc_cal_ratio_configured(adc_semantic_t sem) { + if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) { + return 0u; + } + return (s_eff_r((unsigned)sem) != 0L) ? 1u : 0u; +} + /* * 函数功能:整定入口——写通道系数与零点,并置位 VALID(「数据可信」标定落盘与整定入口) * 入口参数:sem - 语义通道;k_q16 - 系数(µV/码×65536);b_codes - 零点(码值) @@ -402,6 +582,31 @@ int adc_cal_set(adc_semantic_t sem, int32_t k_q16, int32_t b_codes) { return 0; } +/* + * 函数功能:整定入口——写该路变比(标定链的第二个写入点,见 .h 同名声明) + * 入口参数:sem - 语义通道;r_q16 - 变比 × 65536(0 = 本路不折算) + * 返 回 值:0 - 成功;-1 - 通道非法 / 变比在合法带外(RAM 不动,[ERR] 留痕) int + * 限定条件:与 adc_cal_set() 同一条并发契约(引擎对齐 int32 单字;勿在 adcTask 调用) + * 函数说明:只写 RAM 副本,不置脏、不落盘(理由见 .h)。r 的合法带**不设上界比较** + * (见 s_rband_ok 注释),故此处只拦「非 0 且 < 1.0」这一种越带形态; + * r = 0 是合法输入(显式关掉本路折算),不走留痕 + */ +int adc_cal_set_ratio(adc_semantic_t sem, int32_t r_q16) { + if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) { + s_st_set_reject++; /* 观测:连通道号都非法,RAM 未动 */ + return -1; + } + if ((r_q16 != 0L) && (s_rband_ok(r_q16) == 0)) { + DBG_ERROR("set ch=%u rejected: r_q16=%ld below 1.0 ratio (0 or >=%ld)", + (unsigned)sem, (long)r_q16, (long)ADC_CAL_R_MIN); + s_st_set_reject++; + return -1; + } + s_rcfg.r[sem] = r_q16; + s_st_set_ok++; /* 观测:RAM 副本确实被改写了(与落盘无关) */ + return 0; +} + /* * 函数功能:读标定层观测计数(不取锁、不取时刻、任意上下文可读) * 返 回 值:无 void @@ -430,35 +635,54 @@ void adc_cal_note_persisted(void) { } /* - * 函数功能:两点标定公式(标定项的算术核,纯函数无状态) - * 入口参数: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 中间量,主机可测;台架注入待跑) + * 函数功能:两点标定公式(标定项的算术核,纯函数无状态;v2.0 起标 r 不标 k) + * 入口参数:c_lo/c_hi、v_lo_uv/v_hi_uv、k_q16、p_r_q16/p_b 见 adc_cal.h 同名声明 + * 返 回 值:0 成功 / -1 参数、斜率或变比越带 / -2 零点越 ±B_MAX int + * 限定条件:无(int64 中间量,主机可测) + * 函数说明:公式与**为什么这样分步**(直接一次乘除会顶穿 int64): + * v_uv = (code + b) × k_q16 × r / 65536 ← 折算式的 µV 形态 + * ⇒ 差分里零点抵消:dv = dc × k_q16 × r / 65536 + * ⇒ t = dv × 65536 / k_q16 = dc × r ← code·ratio 的混合量,量级 ≤ 2^31 + * ⇒ r_q16 = t × 65536 / dc ← 还原成 Q16 比值 + * ⇒ b = t_lo × 65536 / r_q16 − c_lo,其中 t_lo = v_lo × 65536 / k_q16 + * 若把 r_q16 = dv×2^32/(dc×k_q16) 一次算完,5 kV 端子注入时分子到 1e19 + * 已经越过 long long 上界(9.22e18)——分两步走,中间量 largest 约 1.4e14 */ 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) { + int32_t k_q16, int32_t *p_r_q16, int32_t *p_b) { long long dc; long long dv; - long long kq; + long long t; + long long rq; + long long tl; long long bb; - if ((p_k_q16 == NULL) || (p_b == NULL)) { + if ((p_r_q16 == NULL) || (p_b == NULL)) { return -1; } + if ((k_q16 <= 0L) || (k_q16 > 100000000L)) { + return -1; /* 系数不在合理域(1526 µV/码 上界)= 调用方取错了 */ + } + if ((v_lo_uv <= -ADC_CAL_V_MAX) || (v_lo_uv >= ADC_CAL_V_MAX) || + (v_hi_uv <= -ADC_CAL_V_MAX) || (v_hi_uv >= ADC_CAL_V_MAX)) { + return -1; /* ±1000 V:超了几乎必然是把 mV 或码值当 µV 填了 */ + } 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; /* 斜率必须为正:码与电压同向,反向 = 映射/接线错 */ + 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:越界 = 单位/量级错 */ + t = ((dv << 16) + ((long long)k_q16 >> 1)) / (long long)k_q16; /* = dc × r */ + rq = ((t << 16) + (dc >> 1)) / dc; /* = r × 65536 */ + if ((rq < (long long)ADC_CAL_R_MIN) || (rq > (long long)ADC_CAL_R_MAX)) { + return -1; /* 变比越带:<1.0 = 把 mV 当 V,>32767 = 单位/量级错 */ } - bb = (((long long)v_lo_uv) << 16) / kq - (long long)c_lo; + tl = (((long long)v_lo_uv << 16) + ((long long)k_q16 >> 1)) / (long long)k_q16; /* (c_lo+b) × r */ + bb = ((tl << 16) / rq) - (long long)c_lo; if ((bb > (long long)ADC_CAL_B_MAX) || (bb < -(long long)ADC_CAL_B_MAX)) { - return -2; /* 零点异常 → 先查前端,别用系数凑(§8 风险表) */ + return -2; /* 零点异常 → 先查前端,别用变比凑(§8 风险表) */ } - *p_k_q16 = (int32_t)kq; + *p_r_q16 = (int32_t)rq; *p_b = (int32_t)bb; return 0; } diff --git a/App/algo/adc_cal.h b/App/algo/adc_cal.h index 3aba3ff5..4757b3d7 100644 --- a/App/algo/adc_cal.h +++ b/App/algo/adc_cal.h @@ -1,22 +1,38 @@ /* * 模块名称:通道标定与量纲折算(标定折算,「数据可信」标定折算与落盘整定) * 模块功能:把 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 §13.8);**不挂回调**——RAM 副本 - * 即生效值,逐点折算现取现用(1 kHz × 9 通道 = 9k 次/秒浮点乘,可忽略)。 - * 三态可用性:ADC_CAL_F_VALID(已按变比标定 → 工程量为二次值)与 + * 工程量 = (code + b) × k_f × r_f + * k_f = k[µV/码] × 1e-6(前端斜率,ROM 固化的板级事实) + * r_f = r[Q16] / 65536(端子→引脚变比,现场标定产物;r = 0 = 本路不折算) + * 三维系数各管一件事,**不许合并**: + * k 管「引脚电压 ↔ 码值」——板级量程事实(TPAFE 152.59 µV/码、STM32 805.66 µV/码), + * 由 ROM 固化,合法带 = ROM × [0.5, 2.0](只容纳晶体/基准的板间差); + * b 管零点偏移(码值,±ADC_CAL_B_MAX); + * r 管「端子二次值 ↔ 引脚电压」——由端子/子板分压与 CT 负担电阻决定, + * 出厂默认按台架实测反算(电压 1000 V/V、电流 29.4118 A/V), + * 两点标定今后标的就是它。 + * 为什么必须分开:把变比折进 k 会溢出存储口径——UA 端子域 k = 152 587.9 µV/码, + * Q16 = 1.0e10 超出 int32 上限 2.147e9;而 r 单独存 Q16 完全够(1000 → 65 536 000)。 + * 合并还会把「硬件分压比」伪装成「前端斜率」,正是 + * docs/硬件平台/ADC通道映射说明.md §7.2 反对的「用系数凑硬件」。 + * k / b / r 三份 RAM 副本注册进 cfg_file 引擎([adc_cal] / [adc_cal_ofs] / + * [adc_cal_sec] 三段,布局见 docs/算法/采集与算法层设计说明.md §13.8); + * **不挂回调**——RAM 副本即生效值,逐点折算现取现用(1 kHz × 9 通道 = 9k 次/秒 + * 浮点乘,可忽略)。 + * 三态可用性:ADC_CAL_F_VALID(该路已两点标定确认)与 * ADC_CAL_F_CLIPPED(V0.1 硬件削顶)由 adc_cal_flags() 单点查询, * 104 / UI / 保护闭锁(未标定即闭锁)共用(docs/算法/采集与算法层设计说明.md §13.3)。 * **未标定闭锁的判定本身也归本层**(`adc_cal_prot_blocked()`):它讲的是 * 「这几路标定齐了没」,属标定层事实,不是任务编排逻辑——保护任务与 UI * 组装器都只是它的消费者(道F task-16 把此前的两份实现收成一份)。 - * ⚠ 输出单位口径 = **ADC 引脚电压 V**(eng=0):ROM 默认系数按量程口径冻结 - * 定死的引脚侧 LSB(TPAFE 152.59 µV/码、STM32 805.66 µV/码,见 - * docs/硬件平台/ADC通道映射说明.md §7.1);端子/子板变比确认与两点 - * 标定完成前不得把输出当二次值消费。TEMP 不参与线性标定 - * (决策 5),adc_cal_apply() 原样透传码值。 + * ⚠ 两个**互不派生**的事实,别互相反推: + * · 数值量纲由 r 决定——r ≠ 0 的通道,adc_cal_apply() 输出的就是二次值(V 或 A); + * · VALID 由两点标定置位——它回答"这个数准不准",回答不了"这个数是什么单位"。 + * 屏面第 7 行的量纲标签(eng)只看前者,保护闭锁只看后者。 + * 一次值(eng=2)仍留预留:其变比的家是 prot_set 已在册的 + * PS_PT_PRI/PS_PT_SEC 与 PS_CT_PRI/PS_CT_SEC 四项(**不再新开 cfg 段**, + * 否则同一事实两个真源),当前无消费者,故本层不产出一次值。 + * TEMP 不参与线性标定(决策 5),adc_cal_apply() 原样透传码值。 * 本层**纯算法 + 纯数据**:不碰寄存器、不等事件;adc_cal_init() 内核前 * 可调(注册进引擎必须先于 cfg_file_load,FTP 鉴权确立的契约);apply/flags * 任意上下文可读(cfg RAM 副本 4 字节对齐字读写在 Cortex-M4 上原子, @@ -35,8 +51,15 @@ * Q16 原值直读口(adc_cal_k_uv 的孪生)。录波段头 k 字段 * 是 LSB 精确记账,float 在 5.28e7 码字上掉尾数, * 禁止经浮点反推(docs/子系统设计/故障录波设计说明.md §6.3) + * v2.0 2026-11-11 DTU 项目组 新增变比维:折算改 (code+b)×k×1e-6×r, + * r 存新段 [adc_cal_sec](int32 Q16,0 = 本路不折算)。 + * 理由 = 端子域 k 的 Q16 溢出 int32(UA 152587.9 µV/码 + * → 1.0e10),变比并进 k 这条路不通;且变比是硬件 + * 事实、k 是前端斜率,混一起就是「用系数凑硬件」。 + * 同步:adc_cal_two_point() 改标 r(k 由 ROM 固化)、 + * +adc_cal_ratio_q16()/+adc_cal_ratio()/+adc_cal_ratio_configured() + * +adc_cal_set_ratio() */ - #ifndef ADC_CAL_H #define ADC_CAL_H @@ -49,7 +72,7 @@ extern "C" { #endif /* 通道可用性标志(adc_cal_flags() 返回值位) */ -#define ADC_CAL_F_VALID (0x01u) /* k 已按变比标定 → 工程量为二次值(两点标定后置位) */ +#define ADC_CAL_F_VALID (0x01u) /* 该路已两点标定确认(k/b/r 合法带内 → 读数可信) */ #define ADC_CAL_F_CLIPPED (0x02u) /* 硬件削顶(V0.1 STM32 侧单极性无偏置)→ 交流量不可信 */ /* @@ -62,21 +85,56 @@ extern "C" { #define ADC_CAL_K_Q16_TPAFE (10000000L) #define ADC_CAL_K_Q16_STM32 (52800000L) -/* 系数合法带(docs/算法/采集与算法层设计说明.md §13.2 尾注):k 必须落在板级 ROM 系数的 ×[0.5, 2.0] - * 区间内;带宽用 ROM 值移位实现(>>1 / <<1),不设独立宏防两处真相 */ +/* + * ROM 默认变比(int32 Q16 = 比值 × 65536)——**出厂就编进镜像,不是缺省 0**: + * 电压族 1000.0 V/V → 65 536 000(Q16 无舍入) + * 电流族 29.4118 A/V → 1 927 532(= 29.4118 × 65536,回落 29.41180,相对误差 < 2e-7) + * 出处 = 2026-11-11 台架实测反算(继保仪注 UA 端子 5.000 V → 屏上 0.005 V, + * 5.000/0.005 = 1000.0;注 IA 端子 0.500 A → 屏上 0.017 V,0.5/0.017 = 29.4118), + * 电流族与 docs/硬件平台/ADC通道映射说明.md §7.2 的登记值精确吻合。 + * ⚠ 电压族的 1000.0 与 §7.2 早先按子板参数算定的 606.06 **不一致**——606.06 的依据是 + * 200 kΩ/330 Ω 假定值,而台架读数差 60 %(0.005 vs 0.008),远超屏面分辨率, + * 故以实测为准;§7.2 已按本次结论改写(子板原理图仍不在仓库,见该节「复核边界」)。 + * ⚠ 这两个数是**单点粗测**(5 V 注入只有 33 码,1 LSB = 3 %), + * 变比真值区间约 909~1111(电压族);送检前必须用标准源两点标定重写 r(见 + * adc_cal_two_point() 的语义变更)。 + * r = 0 是合法值,语义 = 「本路不折算,输出停在 ADC 引脚电压」——TEMP 恒此态, + * 也是现场关掉某路折算的显式手段(不是 1.0)。 + */ +#define ADC_CAL_R_Q16_VOLT (65536000L) /* 1000.0 V/V */ +#define ADC_CAL_R_Q16_CURR (1927532L) /* 29.4118 A/V */ + +/* 变比合法带(Q16):**0 = 本路不折算**,或落在 [ADC_CAL_R_MIN, ADC_CAL_R_MAX](比值 1.0 ~ 32767.99)。 + * 为什么用绝对带而不是像 k 那样用 ROM×[0.5,2.0]:k 的窄带是为了容纳晶体/基准的板间差, + * 而 r 是现场标定量(不同 CT/PT 变比差几个数量级),窄带没有意义。下界取 1.0 是因为 + * 分压与负担电阻都只会把端子值变小——比「比值 < 1」更可能的是把 mV 当 V 填错。 + * 上界 = int32 上限(Q16 下最大比值 32767.99),1000 与 29.41 都远在带内。 + * ⚠ 工程实现上**只在下界用 ADC_CAL_R_MIN 比较**:r 是 int32,`r > ADC_CAL_R_MAX` + * 是恒假分支(cppcheck 报 knownConditionTrueFalse)。ADC_CAL_R_MAX 留给 + * int64 中间量(两点标定算出的 kq / 除法前的分子)做上界闸。 */ +#define ADC_CAL_R_MIN (65536L) /* 1.0 */ +#define ADC_CAL_R_MAX (2147483647L) /* 零点偏移幅值上限(码值):超此即判文件损坏/异常标定,回退 ROM 并留痕 */ #define ADC_CAL_B_MAX (8192L) +/* 两点标定的端子输入域上界(µV,含电流按 µA 同域):±1000 V / 1000 A。 + * 它只拦「把 mV 或码值当 µV 填」这一类量级错——本平台 V0.1 端子量程远低于此, + * 定值本身不是量程声明,别拿它当"支持 1000 V"的依据 */ +#define ADC_CAL_V_MAX (1000000000L) + /* - * 函数功能:标定层初始化——RAM 副本置 ROM 默认 + 注册两段进 cfg 引擎(幂等) + * 函数功能:标定层初始化——RAM 副本置 ROM 默认 + 注册三段进 cfg 引擎(幂等) * 入口参数:无 * 返 回 值:无 * 限定条件:内核启动前调用(app_main_init);注册必须先于 cfg_file_load * (「所有受管段先注册、后 load」契约,见 Drivers/BSP/NETCFG/net_cfg.c 的 * net_cfg_load 注释) * 函数说明:段属主为 defaultTask(配置引擎),本模块只提供 RAM 副本与默认值; - * 文件值由引擎 load/poll 原地写入 RAM 副本,本层逐点折算现读 + * 文件值由引擎 load/poll 原地写入 RAM 副本,本层逐点折算现读。 + * ⚠ 变比段 [adc_cal_sec] 的 ROM 默认**不是 0**(电压 1000 / 电流 29.4118), + * 因此烧进新固件后屏上立刻显二次值——这是刻意的:变比是硬件事实, + * 出厂就该带着;而 VALID(准不准)仍然要求两点标定,两者互不代替。 */ void adc_cal_init(void); @@ -84,11 +142,13 @@ void adc_cal_init(void); * 函数功能:码值 → 工程量折算(docs/算法/采集与算法层设计说明.md §13.2 模型) * 入口参数:sem - 语义通道(TEMP 透传不折算) adc_semantic_t * code - 原始码值(TPAFE 16 位补码 / STM32 0..4095) int32_t - * 返 回 值:工程量浮点(未标定时单位 = ADC 引脚电压 V;VALID 后置位 = 二次值) float + * 返 回 值:工程量浮点(r≠0 → 二次值 V/A;r=0 → ADC 引脚电压 V) float * 限定条件:无(任意上下文,纯读) - * 函数说明:eng = (code + b) × k[µV/码] × 1e-6,由 Q16 现值派生,无缓存副本 - * (避免无回调下的陈旧系数);k/b 越合法带 → 该通道回退 ROM 默认 + - * 首见沿 [ERR] 留痕(adc_cal_init 重置留痕位),flags 同时不报 VALID + * 函数说明:eng = (code + b) × k[µV/码] × 1e-6 × r[Q16]/65536,由三份 Q16/int32 + * 现值派生,无缓存副本(避免无回调下的陈旧系数);k/b 越合法带 → 该通道 + * 回退 ROM 默认 + 首见沿 [ERR] 留痕(adc_cal_init 重置留痕位), + * flags 同时不报 VALID。r 越带或为 0 → 该路按 1.0 处理(不折算), + * 同样不报 VALID。 */ float adc_cal_apply(adc_semantic_t sem, int32_t code); @@ -97,8 +157,12 @@ 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) + * 函数说明:VALID = 文件位图置位 **且** 该通道 k/b 在合法带内 **且** 变比已配置 + * (r ≠ 0 且在带内)——三个条件一起才叫"这路标定过"。哨兵不落状态, + * 天然跟随 load/poll 重载。CLIPPED = V0.1 静态表(UB/UC/IB/IC/IN)。 + * ⚠ VALID 回答「准不准」,**不**回答「什么单位」:单位由 r 决定 + * (r≠0 → 二次值;r=0 → 引脚电压),而 r=0 时 VALID 必为 0, + * 所以「VALID 未置位却有二次值读数」这种组合不可能出现。 */ uint32_t adc_cal_flags(adc_semantic_t sem); @@ -120,11 +184,12 @@ uint32_t adc_cal_flags(adc_semantic_t sem); * (未标定);(c) 定值校验和失配闭锁(`prot_set_locked()`);(d) 全局软压板 * 退出(`PS_GLOBAL_EN` = 0)。要判「这拍到底评估了没有」必须四路一起看 * (编排顺序见 `App/task/protect_task.c`)。 - * ③ ⚠ **两个同族谓词与它不是一件事,不要顺手合并**(三种口径各有用途): - * · 电气 **8** 路全 VALID 才升 eng=二次值(量纲标签,判据在 - * `App/app/ui_data_feed.c` 的 `feed_eng()`)——并进来会让部分标定的整帧 - * 被误标二次值;反过来把本三路口径当 8 路用,V0.1 下 UB/UC/IB/IC/IN - * 永不置位 → 徽标与停走**永不解除**; + * ③ ⚠ **两个同族谓词与它不是一件事,不要顺手合并**(两种口径各有用途): + * · 整帧量纲标签 eng(`App/app/ui_data_feed.c` 的 `feed_eng()`)看的是 + * **每路 r 是否配置**(配了就出二次值),判据 = 「每路要么已配变比、 + * 要么削顶不显示」——削顶通道页面本来就显 "--",不参与整帧量纲断言; + * 并进来会让"三路标定齐但八路变比未全配"的整帧被误标引脚电压, + * 反过来把本三路口径当量纲用,V0.1 下五路削顶会让徽标永不解除; * · 逐通道打印 VALID/CLIPPED 位图是诊断输出(`App/task/adc_task.c`), * 不是判定。 * ④ 台架调试旁路 = 编译期 `-DDTU_BENCH`(`docs/算法/采集与算法层设计说明.md` @@ -165,6 +230,42 @@ float adc_cal_b_codes(adc_semantic_t sem); */ int32_t adc_cal_k_q16(adc_semantic_t sem); +/* + * 函数功能:读该路变比(浮点,V/V 或 A/V)——屏面/104 回显与标定链的读数口 + * 入口参数:sem - 语义通道 adc_semantic_t + * 返 回 值:r(比值);**r=0 表示本路不折算**(输出停在引脚电压);TEMP/越界返 0 float + * 限定条件:无(任意上下文,纯读) + * 函数说明:与折算同视图(0 或越带即按 0 报,不给"带外值"这种第三态)。 + * 别拿它反推 VALID——VALID 是"标定过没有",r 是"折算比是多少", + * 两者由两个字段各记各的(见 adc_cal_flags 的注释)。 + * ⚠ 本值是**整体折算比**(端子二次值 ÷ 引脚电压),不是纯硬件分压比: + * k 由 ROM 固化成板级名义斜率,其残差被 r 吸收(见 adc_cal_two_point 说明)。 + */ +float adc_cal_ratio(adc_semantic_t sem); + +/* + * 函数功能:读该路变比的 **int32 Q16 原值**(比值 × 65536)——录波段头记账口 + * 入口参数:sem - 语义通道 adc_semantic_t + * 返 回 值:r[Q16];TEMP/越界/带外/0 均返 0(0 = 本路不折算) int32_t + * 限定条件:无(任意上下文,纯读) + * 函数说明:理由与 adc_cal_k_q16() 同一条——录波头必须 LSB 精确, + * 65000×65536 这类值经 float 往返会掉尾数(docs/子系统设计/故障录波设计说明.md §6.3)。 + */ +int32_t adc_cal_ratio_q16(adc_semantic_t sem); + +/* + * 函数功能:该路变比是否"已配置"(r 非 0 且落在合法带内)——**整帧量纲标签的唯一判据源** + * 入口参数:sem - 语义通道 adc_semantic_t + * 返 回 值:1 - 已配置(该路输出是二次值);0 - 未配置(输出是引脚电压) uint8_t + * 限定条件:无(任意上下文,纯读) + * 函数说明:存在的唯一理由:`App/app/ui_data_feed.c` 的 `feed_eng()` 要给整帧贴 + * 「二次值/未标定」标签,而它需要的是**量纲事实**(配了变比没有), + * 不是**准确度事实**(VALID)。这两件事在 V0.1 上必然分叉——五路削顶通道 + * 永远拿不到 VALID,但它们配了变比后输出确实已经是二次值。 + * TEMP 恒返 0(不参与线性标定)。 + */ +uint8_t adc_cal_ratio_configured(adc_semantic_t sem); + /* * 函数功能:整定入口——写通道系数与零点,并置位 VALID(「数据可信」标定落盘与整定入口) * 入口参数:sem - 语义通道(电气 8 路) adc_semantic_t @@ -182,19 +283,44 @@ int32_t adc_cal_k_q16(adc_semantic_t sem); */ int adc_cal_set(adc_semantic_t sem, int32_t k_q16, int32_t b_codes); +/* + * 函数功能:整定入口——写该路变比(「数据可信」标定链的第二个写入点) + * 入口参数:sem - 语义通道(电气 8 路) adc_semantic_t + * r_q16 - 变比,比值 × 65536(0 = 本路不折算) int32_t + * 返 回 值:0 - 成功;-1 - 通道非法 / 变比在合法带外(RAM 不动,[ERR] 留痕) int + * 限定条件:与 adc_cal_set() 同一条并发契约(引擎对齐 int32 单字、µs 级混窗; + * 勿在 adcTask 调用) + * 函数说明:**只写 RAM 副本,不置脏、不落盘**——与 adc_cal_set() 完全同一条理由, + * 落盘的唯一真源仍是 run_par 的脏标志(授权写者 = run_par 的语义 setter)。 + * ⚠ 与 adc_cal_set() **没有先后依赖**:VALID 是每次调用现算的三个条件 + * (位图 ∧ k/b ∧ r),不是"置过的位",所以先写 r 再写 k/b 和反过来 + * 结果一样。这也意味着**标定一路要调两个函数**(本函数管量纲、 + * adc_cal_set 管零点并置位),少调一个 VALID 就不会亮——这是刻意的: + * 只配变比 = 出厂默认态(量纲对、准不准不知道)。 + */ +int adc_cal_set_ratio(adc_semantic_t sem, int32_t r_q16); + /* * 函数功能:两点标定公式(「数据可信」标定项的算术核,纯函数无状态) * 入口参数: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(先查前端, - * 不得用系数凑错斜率,docs/算法/采集与算法层设计说明.md §13.2 尾注) int + * v_lo_uv/v_hi_uv - 两点**端子侧**实测值(µV:电压填 1e6 域,电流填 + * 1e6 域但单位是 µA,即「0.5 A → 500000」) int32_t + * k_q16 - 该路生效系数(走 adc_cal_k_q16() 取,不要自己填) int32_t + * p_r_q16 / p_b - 输出:变比(Q16)与零点(码值) int32_t * + * 返 回 值:0 - 成功;-1 - 参数/斜率非法或算出的变比越出 [ADC_CAL_R_MIN, ADC_CAL_R_MAX]; + * -2 - 零点越 ±ADC_CAL_B_MAX(先查前端,不得用系数凑错斜率, + * docs/算法/采集与算法层设计说明.md §13.2 尾注) int * 限定条件:无(纯整数运算,主机侧可直接单测) - * 函数说明:k = (v_hi−v_lo)×65536/(c_hi−c_lo)(四舍五入),b = v_lo×65536/k − c_lo; - * 实测注入未做(台架待跑),本函数即「公式齐备」的交付物 + * 函数说明:**v2.0 起标的是 r,不再标 k**——k 是板级前端斜率(ROM 固化,现场 + * 不该改也改不动),真正未知的是「端子↔引脚」这一层。公式: + * r = (v_hi−v_lo) / [ (c_hi−c_lo) × k ] ← 零点在差分里抵消 + * b = v_lo / (k × r) − c_lo + * 两条都用 int64 中间量;`k × r` 的量级先按 Q16 归一到同一个 2^32 再除, + * 避免先乘后除把 int64 顶到溢出边。v 上限 1e9 µV(1000 V / 1000 A)—— + * 超了几乎必然是把 mV 或码值当 µV 填了,直接判参数错而不是算个假变比出来。 */ 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); + int32_t k_q16, int32_t *p_r_q16, int32_t *p_b); /* * 函数功能:通道码值域「贴轨带」门限(采样通道自检 越界检出的门限来源,按 FS 百分比) diff --git a/App/algo/run_par.c b/App/algo/run_par.c index 08507c7d..674e6d05 100644 --- a/App/algo/run_par.c +++ b/App/algo/run_par.c @@ -3,14 +3,16 @@ * 模块功能:见 run_par.h。工厂默认 = 「数据可信」下游常数按工程量重定后的额定 0.5 % 档(电压 500 * mV / 电流 5 mA / 频率 500 mHz,毫单位 int32 定点落盘);[run_par] * 段 9 键(db_ua…db_freq,均 ≤15 字符)≤ CFG_FILE_MAX_KEYS 不拆段 - * (通道系数覆盖**不落本段**——直通 adc_cal 的 [adc_cal]/[adc_cal_ofs], - * 系数唯一真源在那两段,lead 裁②a)。 + * (通道系数覆盖**不落本段**——直通 adc_cal 的 [adc_cal]/[adc_cal_ofs]/ + * [adc_cal_sec] 三段,系数唯一真源在那三段,lead 裁②a)。 * 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU) * 作者:DTU 项目组 * 创建日期:2026-10-25(道D 运行参数段) * 修改记录: * v1.0 2026-10-25 DTU 项目组 创建:[run_par] 段 + 104 映射 + 直通 adc_cal * + B7 式脏标志落盘请求 + * v1.2 2026-11-11 DTU 项目组 变比维落地:+run_par_cal_r_set(写值+置脏同一 + * 函数)+ 104 地址 0x8050~0x8057 读写两支 */ /* 头文件包含区 */ @@ -36,6 +38,7 @@ #define RUN_PAR_IOA_DB0 0x8030u /* 9 点:0x8030~0x8038 */ #define RUN_PAR_IOA_K0 0x8040u /* k 覆盖 8 路:0x8040~0x8047 */ #define RUN_PAR_IOA_B0 0x8048u /* b 覆盖 8 路:0x8048~0x804F */ +#define RUN_PAR_IOA_R0 0x8050u /* 变比 r 覆盖 8 路:0x8050~0x8057(比值,无单位) */ /* 死区合法域上限(毫单位):电压 50 V / 电流 1 A / 频率 5 Hz——守卫挡误写, * 不限工况;下限统一 0(负死区 = 配置错误) */ @@ -227,6 +230,10 @@ int run_par_104_read(uint32_t addr, float *out) { *out = adc_cal_b_codes((adc_semantic_t)(addr - RUN_PAR_IOA_B0)); return 0; } + if ((addr >= RUN_PAR_IOA_R0) && (addr < RUN_PAR_IOA_R0 + RUN_PAR_CH_N)) { + *out = adc_cal_ratio((adc_semantic_t)(addr - RUN_PAR_IOA_R0)); + return 0; + } return -1; } @@ -288,14 +295,43 @@ int run_par_cal_b_set(adc_semantic_t sem, int32_t b_codes) { } /* - * 函数功能:104 运行参数地址写(两段式;k/b 经本模块语义 setter——**run_par 不存 - * 系数**,系数唯一真源 = [adc_cal]/[adc_cal_ofs],lead 裁②a) + * 函数功能:写某路变比 r(比值,1.0 ~ 32767.99;0 = 本路不折算)并置脏 + * 入口参数:sem - 电气语义通道 adc_semantic_t;r - 变比(无单位) float + * 返回值:0 - 已写并置脏;-1 - 粗守或执行段拒(副本未动、不置脏) int + * 限定条件:同 run_par_cal_k_set() + * 函数说明:与 k/b 两支**同一条裁②理由**——写值与置脏必须长在一个函数里, + * 所以这里不再给"单独的置脏口"。粗守只为挡误写(0 或 [1, 32767.99]), + * 真带由 adc_cal_set_ratio() 执行段裁,两层各说各的。 + * ⚠ 0 是**合法入参**(显式关掉本路折算),不是"清空/非法"——粗守 + * 因此写成「0 或 ≥1」两条并集,而不是 `> 0`。写成 `> 0` 会让现场 + * 无法把某一路退回引脚电压(只能改成 1.0,而那是个假变比)。 + */ +int run_par_cal_r_set(adc_semantic_t sem, float r) { + int32_t r_q16; + + if (!isfinite(r) || ((r != 0.0f) && ((r < 1.0f) || (r > 32767.99f)))) { + return -1; + } + r_q16 = (int32_t)lrintf(r * 65536.0f); + if (adc_cal_set_ratio(sem, r_q16) != 0) { + return -1; /* 执行段拒:副本未动,脏位也不置 = 不承诺任何持久化 */ + } + s_dirty = 1u; + return 0; +} + +/* + * 函数功能:104 运行参数地址写(两段式;k/b/r 经本模块语义 setter——**run_par + * 不存系数**,系数与变比的唯一真源 = [adc_cal]/[adc_cal_ofs]/[adc_cal_sec], + * lead 裁②a) + * 入口参数:addr - 104 信息体地址;v - 工程量/系数值;select - 1 选择 / 0 执行 * 返 回 值:0 接受 / -1 拒绝 / -2 非本段(调用方落 prot_set) int - * 函数说明:选择段 = 校验不提交(死区走与 set 同源守卫;k/b 的 adc_cal 合法带 + * 函数说明:选择段 = 校验不提交(死区走与 set 同源守卫;k/b/r 的 adc_cal 合法带 * 由执行段裁决——IEC104 允许执行段 PN,两段结果可不一致,注释在此 - * 明说防"选择过了怎么还 PN"的现场困惑)。k/b 两支自 2026-10-27 起一律 - * 走 run_par_cal_k_set() / run_par_cal_b_set():写值与置脏在同一个函数 - * 里发生,本函数不再自己碰 s_dirty(lead 裁②:消除两份置脏逻辑)。 + * 明说防"选择过了怎么还 PN"的现场困惑)。k/b/r 三支一律走 + * run_par_cal_k_set() / run_par_cal_b_set() / run_par_cal_r_set(): + * 写值与置脏在同一个函数里发生,本函数不再自己碰 s_dirty + * (lead 裁②:消除两份置脏逻辑)。 */ int run_par_104_write(uint32_t addr, float v, uint8_t select) { uint8_t slot; /* 每个命中分支先赋值再使用(无未init读路径) */ @@ -337,6 +373,16 @@ int run_par_104_write(uint32_t addr, float v, uint8_t select) { /* 同款语义 setter:粗守 ±1e6 码在 setter 内,±8192 真带由 adc_cal 执行段裁 */ return run_par_cal_b_set((adc_semantic_t)slot, (int32_t)lrintf(v)); } + if ((addr >= RUN_PAR_IOA_R0) && (addr < RUN_PAR_IOA_R0 + RUN_PAR_CH_N)) { + slot = (uint8_t)(addr - RUN_PAR_IOA_R0); + if (!isfinite(v)) { + return -1; /* NaN/Inf 不得进 lrintf(该转换是 UB) */ + } + if (select != 0u) { + return 0; /* 选择段只粗守:真带裁决在执行段(同 k/b 两支) */ + } + return run_par_cal_r_set((adc_semantic_t)slot, v); + } return -2; } @@ -352,8 +398,9 @@ uint8_t run_par_dirty_peek(void) { * 函数功能:执行落盘(整档外科保存成功后清脏;失败脏保持 = 下轮重试) * 返 回 值:0 成功/无脏;-1 写盘失败 int * 限定条件:defaultTask(配置引擎属主)上下文,SYS_EVT_RUN_PAR_SAVE 驱动 - * 函数说明:一次 save 同时覆盖 [run_par] 9 键与刚被 adc_cal_set 改写的 - * [adc_cal]/[adc_cal_ofs] 副本(lead 裁②:一条事件位一次 save)。 + * 函数说明:一次 save 同时覆盖 [run_par] 9 键与刚被 adc_cal_set()/adc_cal_set_ratio() + * 改写的 [adc_cal]/[adc_cal_ofs]/[adc_cal_sec] 三段副本(lead 裁②: + * 一条事件位一次 save)。 * 成功路径上给标定层盖一次落盘水位(adc_cal_note_persisted())——它是 * **观测回执**,不参与本函数任何判定;失败路径不盖,于是"save 一直失败" * 在标定层侧表现为差额长期不归零(道G task-43 第②项)。 diff --git a/App/algo/run_par.h b/App/algo/run_par.h index aa68e6c3..f64d737c 100644 --- a/App/algo/run_par.h +++ b/App/algo/run_par.h @@ -6,7 +6,8 @@ * (App/protocol/iec104/iec104_pts.c)每拍实时读取——无投递、无缓存 * 副本(104 改读快照曾在 pts 侧留运行时副本 s_yc_db,运行参数段落地后收编于此, * 双源必打架); - * - 系数覆盖 = **直通 adc_cal_set()**([adc_cal]/[adc_cal_ofs] 是系数 + * - 系数覆盖 = **直通 adc_cal_set() / adc_cal_set_ratio()** + * ([adc_cal]/[adc_cal_ofs]/[adc_cal_sec] 是系数与变比的 * 唯一真源,标定落盘与整定入口已定;本模块只加 104 地址映射 + 落盘请求,不另存 * 一份系数——同 D1 裁"两处判断必打架"逻辑)。 * 104 地址映射(docs/算法/继电保护功能复现规划.md §5.1 运行参数类 @@ -14,13 +15,16 @@ * 不在仓),本表即运行参数段的落实,送检点表若另给权威地址只改常量段: * 0x8030~0x8038 = 死区 UA/UB/UC/UN/IA/IB/IC/IN/FREQ(工程量 V/A/Hz); * 0x8040~0x8047 = k 覆盖(µV/码);0x8048~0x804F = b 覆盖(码值,可负); + * 0x8050~0x8057 = 变比 r 覆盖(比值,无单位;0 = 本路不折算,见 + * run_par_cal_r_set 的说明); * 其余留白(未启用类别),**0x8027~0x802A 属 prot_set 告警段**,本表 * 永不让位(两表按精确地址分流,见 iec104_pts.c set 路由)。 * 持久化(B7 prot_set 先例同款):任一写成功 → 脏标志只置不清; * iec104Task 主循环 peek → 置 SYS_EVT_RUN_PAR_SAVE → defaultTask * (配置引擎属主)run_par_store_save() 调 cfg_file_save() 整档外科 - * 保存(含 [run_par] 与刚被 adc_cal_set 改写的 [adc_cal]/[adc_cal_ofs] - * 副本)→ 成功清脏;失败留脏 = 下轮天然重试。本模块自身零文件 IO + * 保存(含 [run_par] 与刚被 adc_cal_set()/adc_cal_set_ratio() 改写的 + * [adc_cal]/[adc_cal_ofs]/[adc_cal_sec] 副本)→ 成功清脏; + * 失败留脏 = 下轮天然重试。本模块自身零文件 IO * (属主纪律)。 * ⚠ 三份职责别混(道G task-43 第③项,此前只有意图式陈述): * - **谁写 RAM**:系数/零点的唯一真源是 adc_cal 的引擎副本,而本模块那两支 @@ -57,6 +61,8 @@ * 给标定层盖一次落盘水位(纯观测回执,不参与判定); * 本头把"谁写 RAM / 谁落盘 / 什么时机 / poll 不是保存 * 路径"从意图式陈述改成逐条契约 + * v1.3 2026-11-11 DTU 项目组 变比维:+run_par_cal_r_set()(0 = 本路不折算) + * + 104 地址 0x8050~0x8057 读写 */ #ifndef RUN_PAR_H @@ -163,6 +169,18 @@ int run_par_cal_k_set(adc_semantic_t sem, float k_uv); */ int run_par_cal_b_set(adc_semantic_t sem, int32_t b_codes); +/* + * 函数功能:写某路变比 r(比值)**并在同一函数内置脏**(与 k/b 同款语义) + * 入口参数:sem - 电气语义通道 adc_semantic_t + * r - 变比(无单位;**0 = 本路不折算**,否则须落在 1.0 ~ 32767.99) float + * 返回值:0 - 已写并置脏;-1 - 粗守或执行段拒(副本未动、不置脏) int + * 限定条件:同 run_par_cal_k_set() + * 函数说明:**0 是合法入参**,语义 = 「这一路退回引脚电压」——不是清零、不是非法。 + * 104 上写 0 就是现场把某路折算关掉的正规手段;写 1.0 反而会留下一个 + * 假变比(端子值 = 引脚电压),那是错的事实而不是"关掉"。 + */ +int run_par_cal_r_set(adc_semantic_t sem, float r); + /* * 函数功能:取第 idx 路死区的合法域(工程量)——**合法域的唯一出口** * 入口参数:idx - 0..8 uint8_t;lo/hi - 输出下限/上限(V/A/Hz) float * diff --git a/App/algo/wave_ring.c b/App/algo/wave_ring.c index 07e8726a..bd5bccb7 100644 --- a/App/algo/wave_ring.c +++ b/App/algo/wave_ring.c @@ -127,6 +127,7 @@ static void wave_rg_seal_reset(uint32_t a_now_ms, uint32_t a_frame_seq) { s_sealed.ch[i].avail = 0u; s_sealed.ch[i].k_q16 = 0; s_sealed.ch[i].b_codes = 0; + s_sealed.ch[i].r_q16 = 0; } } @@ -343,6 +344,7 @@ int32_t wave_ring_latch_meta(const wave_seal_cal_t *p_cal) { s_sealed.ch[i].avail = p_cal->ch[i].avail; s_sealed.ch[i].k_q16 = p_cal->ch[i].k_q16; s_sealed.ch[i].b_codes = p_cal->ch[i].b_codes; + s_sealed.ch[i].r_q16 = p_cal->ch[i].r_q16; } s_meta_ready = 1u; return WAVE_ST_OK; @@ -425,7 +427,7 @@ int32_t wave_ring_sealed_meta(wave_seg_meta_t *p_out) { return WAVE_ST_E_STATE; } /* 逐字节拷出而不是 *p_out = s_sealed:结构体赋值在 ARMCC 上会调 __aeabi_memcpy, - * 这里没必要为一个赋值多拉一层依赖,量也就 136 字节 */ + * 这里没必要为一个赋值多拉一层依赖,量也就 172 字节 */ p_src = (const uint8_t *)&s_sealed; p_dst = (uint8_t *)p_out; for (i = 0u; i < (uint16_t)sizeof(wave_seg_meta_t); i++) { diff --git a/App/algo/wave_ring.h b/App/algo/wave_ring.h index 261792fd..4fabd016 100644 --- a/App/algo/wave_ring.h +++ b/App/algo/wave_ring.h @@ -8,7 +8,7 @@ * 为准并回报 lead,不在代码里单方面「修正」。 * 纯逻辑层纪律:不 include HAL / CMSIS / RTOS 头、不建任务、不打日志、不碰文件系统, * 也不链 App/algo/adc_cal —— 折算档位、有效性位图、削顶位图、epoch 秒与每路 - * k_q16 / b_codes 由 adcTask 在「刚封存那一拍」作为参数交棒(见 wave_ring_latch_meta + * k_q16 / b_codes / r_q16 由 adcTask 在「刚封存那一拍」作为参数交棒(见 wave_ring_latch_meta * 与下面的「封存握手顺序」),本层只负责把它们随段头一起锁存。因此本层可被 * 主机侧单测 test/host/wave_ring/ 与 test/host/wave_trig/ 原样纳入断言、零桩。 * 与 sample_ring 的关系(为什么另起一个环):App/algo/sample_ring.c 是相量与保护的 @@ -35,8 +35,8 @@ * 1) adcTask 每拍 wave_ring_push() 然后 wave_ring_tick(); * 2) tick 计满后触发点返回 WAVE_RING_EVT_SEALED —— 此刻状态进 WAVE_RING_ST_SEALED 但 * **尚未就绪**,wave_ring_sealed() 仍返 0; - * 3) adcTask 在**同一拍**现取封存瞬间事实(adc_cal_k_q16 / adc_cal_flags / sys_clock / - * 削顶位图)并调 wave_ring_latch_meta() 一次 —— 只在第 2 步那一拍取一次, + * 3) adcTask 在**同一拍**现取封存瞬间事实(adc_cal_k_q16 / adc_cal_ratio_q16 / + * adc_cal_flags / sys_clock / 削顶位图)并调 wave_ring_latch_meta() 一次 —— 只在第 2 步那一拍取一次, * 既不每拍读 adc_cal(1 kHz 下 8 路白读),也不低频刷新(低频刷新等于快照可能 * 来自标定之前的旧值,正是 §12「标定落地后老文件被解释错」的入口); * 4) 此后 wave_ring_sealed() 才返 1,defaultTask 才准开文件落盘;meta 未就绪时 @@ -161,10 +161,13 @@ typedef struct { uint8_t flags_seq; /* bit s = 语义通道 s 的 ADC_CAL_F_VALID(§6.3) */ uint16_t clip_mask; /* bit s = 语义通道 s 的 ADC_CAL_F_CLIPPED(V0.1 恒 0xE6) */ uint32_t wall; /* 对时后的 epoch 秒(未对时 0;未对时就写 0,不许拿上电毫秒冒充) */ - wave_ch_desc_t ch[WAVE_ST_CH_MAX]; /* 通道表前 8 项的 avail / k_q16 / b_codes: - 必须取 adc_cal_k_q16() 的 int32 原值, - 不许经 adc_cal_k_uv() 的 float 回算(§6.3: - float 在 5.28e7 量级掉尾数); + wave_ch_desc_t ch[WAVE_ST_CH_MAX]; /* 通道表前 8 项的 avail / k_q16 / b_codes / r_q16: + k 与 r **必须取 int32 原值入口** + (adc_cal_k_q16 / adc_cal_ratio_q16)—— + 不许经 adc_cal_k_uv() / adc_cal_ratio() 的 float + 回算(§6.3:float 在 5.28e7 / 6.5e7 量级掉尾数); + b 的 float 回乘在本域无损(|b| ≤ 8192 << 2^24, + float32 精确表示该范围整数); conv 与 bits 也在这里给(本层无从知道两片 转换器的归属);sem 由本层按「表内语义号 == 下标」 自己钉,传进来的 sem 被忽略 */ @@ -275,7 +278,7 @@ int32_t wave_ring_tick(uint32_t a_now_ms, uint32_t a_frame_seq); /* * 函数功能:封存交棒 —— 把「环已知字段 + adcTask 现取的封存瞬间事实」拼成完整段头并锁存 * 入口参数:p_cal 封存瞬间事实(折算档位 / 有效性位图 / 削顶位图 / epoch 秒 / 通道表的 - * conv、bits、avail、k_q16、b_codes;不可为 NULL) + * conv、bits、avail、k_q16、b_codes、r_q16;不可为 NULL) * 返 回 值:WAVE_ST_OK = 已锁存,wave_ring_sealed() 从此为 1; * WAVE_ST_E_STATE = 不在「刚封段待交棒」态(含已 latch 过第二次、非 SEALED); * WAVE_ST_E_PARAM = 空指针或折算档位非法 int32_t @@ -333,7 +336,7 @@ uint8_t wave_ring_sealed(void); /* * 函数功能:取已封存段的段头快照(落盘道编头、回放页画标题都用它) - * 入口参数:p_out 输出(整份拷出,约 136 字节;不可为 NULL) + * 入口参数:p_out 输出(整份拷出,约 172 字节;不可为 NULL) * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_STATE 无就绪段 int32_t * 限定条件:任意上下文(defaultTask / uiTask 只读)。整份拷贝、不暴露内部指针, * 免得调用方攥着指针跨到下一段去看 diff --git a/App/algo/wave_store.c b/App/algo/wave_store.c index 823d7a40..fd41297c 100644 --- a/App/algo/wave_store.c +++ b/App/algo/wave_store.c @@ -9,6 +9,9 @@ * 创建日期:2026-10-29(故障录波 task-1 · 数据面纯逻辑层) * 修改记录: * v1.0 2026-10-29 DTU 项目组 创建 + * v2.0 2026-11-11 DTU 项目组 格式升 v2:通道表加 int32 r_q16(变比 Q16,一 路 12 -> 16 字节), + * 整头 128 -> 160、整段 5250 -> 5282;wave_store_code_to_milli + * 输出改为「录波域毫单位」(余数分解换等价单次圆整) */ #include "wave_store.h" @@ -83,6 +86,24 @@ static void wave_st_put_code(uint8_t *p, int16_t v) { wave_st_put16(p, (uint16_t)v); } +/* + * 函数功能:int64 定点除法并圆整(远离零方向 + 半量进位) + * 入口参数:a_num 分子(有符号) + * a_den 分母(必须是正数) + * 返 回 值:圆整后的商 + * 限定条件:a_den > 0;|a_num| 与结果都须落在 int64 内 + * 函数说明:录波折算的三处分母(1000 / 65536 / 65536×65536)共用这一把尺子, + * 免得三处各写一遍「负数的向零截断」这种容易写反的分支。 + * 为什么要「远离零方向」:向零截断会让负半周比正半周小一个 LSB, + * 波形出现偶次谐波假象——这正是 §8.1 忌讳的「画出不存在的东西」。 + */ +static int64_t wave_st_div_round(int64_t a_num, int64_t a_den) { + if (a_num >= 0) { + return (a_num + (a_den / 2)) / a_den; + } + return -(((-a_num) + (a_den / 2)) / a_den); +} + /* ===== 内部:段头字段一致性自检(编码前与解码后各跑一次,同一把尺子) ===== */ /* @@ -239,11 +260,13 @@ static const uint8_t s_sem_bits[(uint8_t)WAVE_ST_CH_MAX] = { * docs/硬件平台/ADC通道映射说明.md 的抄本,本层刻意不带 adc_input.h, * 这样格式层在主机侧不需要任何假件就能编译;单测里 0xE6 的算术交叉验证 * 会同时钉住这套顺序,抄错就会红)。 - * 不填 avail / k_q16 / b_codes:那些是封存瞬间的事实,必须由调用方现取现填 - * (adc_cal_k_q16 / adc_cal_flags / sys_clock),本层不链它们; + * 不填 avail / k_q16 / b_codes / r_q16:那些是封存瞬间的事实,必须由调用方现取现填 + * (adc_cal_k_q16 / adc_cal_ratio_q16 / adc_cal_flags / sys_clock),本层不链它们; * 调用方忘填 k_q16 与 avail 不会让编码失败(0 是合法整数), * 但会让 PC 侧把波形解释成一条 0 线——所以单测里有一条专门检查 * 「填了系数的往返」与「没填系数时段的形状仍然正确、只是幅值归零」。 + * r_q16 = 0 是**合法值**(本路不折算),忘填的后果只是量纲停在引脚域, + * 与「刻意不折算」在文件里长得一样——这是设计取舍,不是缺陷。 */ int wave_store_meta_init(wave_seg_meta_t *p_meta) { uint8_t i; @@ -271,6 +294,7 @@ int wave_store_meta_init(wave_seg_meta_t *p_meta) { p_meta->ch[i].avail = 0u; p_meta->ch[i].k_q16 = 0; p_meta->ch[i].b_codes = 0; + p_meta->ch[i].r_q16 = 0; } return WAVE_ST_OK; } @@ -315,6 +339,8 @@ static uint32_t wave_st_head_raw(const wave_seg_meta_t *p_meta, uint8_t *p_out) * 保证板端 ARMCC 与主机 CRT 写出同一串字节 */ wave_st_put32(&p_out[pos + WAVE_ST_CH_OFF_KQ16], (uint32_t)p_meta->ch[i].k_q16); wave_st_put32(&p_out[pos + WAVE_ST_CH_OFF_BCODE], (uint32_t)p_meta->ch[i].b_codes); + /* v2:变比 Q16 原值(0 = 本路不折算)。同样按 int32 小端原样落 4 字节 */ + wave_st_put32(&p_out[pos + WAVE_ST_CH_OFF_RQ16], (uint32_t)p_meta->ch[i].r_q16); } return (uint32_t)WAVE_ST_OFF_TBL + tbl_n; } @@ -324,7 +350,7 @@ static uint32_t wave_st_head_raw(const wave_seg_meta_t *p_meta, uint8_t *p_out) * 入口参数:p_meta 段头元数据;p_out 输出缓冲;a_cap 容量 * 返 回 值:>= 0 写入字节数;负 = WAVE_ST_E_* int * 限定条件:任意上下文;无静态缓冲、可重入 - * 函数说明:头 CRC 覆盖 [0, 30+12×ch_count)、自身紧随其后、低字节在前(§6.3)。 + * 函数说明:头 CRC 覆盖 [0, 30+16×ch_count)、自身紧随其后、低字节在前(§6.3)。 */ int wave_store_head_build(const wave_seg_meta_t *p_meta, uint8_t *p_out, uint32_t a_cap) { uint32_t crc_pos; @@ -419,6 +445,7 @@ int wave_store_head_parse(const uint8_t *p_buf, uint32_t a_len, wave_seg_meta_t tmp.ch[ch_count].avail = 0u; tmp.ch[ch_count].k_q16 = 0; tmp.ch[ch_count].b_codes = 0; + tmp.ch[ch_count].r_q16 = 0; } for (ch_count = 0u; ch_count < p_buf[WAVE_ST_OFF_CH_CNT]; ch_count++) { uint32_t pos = (uint32_t)WAVE_ST_OFF_TBL + ((uint32_t)ch_count * WAVE_ST_CH_BYTES); @@ -428,6 +455,7 @@ int wave_store_head_parse(const uint8_t *p_buf, uint32_t a_len, wave_seg_meta_t tmp.ch[ch_count].avail = p_buf[pos + WAVE_ST_CH_OFF_AVAIL]; tmp.ch[ch_count].k_q16 = (int32_t)wave_st_get32(&p_buf[pos + WAVE_ST_CH_OFF_KQ16]); tmp.ch[ch_count].b_codes = (int32_t)wave_st_get32(&p_buf[pos + WAVE_ST_CH_OFF_BCODE]); + tmp.ch[ch_count].r_q16 = (int32_t)wave_st_get32(&p_buf[pos + WAVE_ST_CH_OFF_RQ16]); } rc = wave_st_meta_check(&tmp); if (rc != WAVE_ST_OK) { @@ -457,8 +485,8 @@ uint32_t wave_store_head_bytes(uint8_t a_ch_count) { * 入口参数:p_meta 段头 * 返 回 值:期望字节数;字段非法返回 0 uint32_t * 限定条件:任意上下文 - * 函数说明:等式 = 32 + 12×ch_count + pts_per_ch×ch_count×2 + 2(§6.3; - * 8 路 320 点 = 5250)。落盘道拿它比对 f_write 写回字节数与 f_size。 + * 函数说明:等式 = 32 + 16×ch_count + pts_per_ch×ch_count×2 + 2(§6.3; + * 8 路 320 点 = 5282)。落盘道拿它比对 f_write 写回字节数与 f_size。 */ uint32_t wave_store_seg_size(const wave_seg_meta_t *p_meta) { if (p_meta == NULL) { @@ -547,30 +575,82 @@ int wave_store_le_decode(const uint8_t *p_raw, uint32_t a_len, } /* - * 函数功能:码值 → 定点毫单位(mV) - * 入口参数:p_desc 通道表项(必须来自段文件头);a_code 码值;p_milli 输出 mV + * 函数功能:码值 → 定点毫单位(录波域:二次 mV/mA 或引脚 mV) + * 入口参数:p_desc 通道表项(必须来自段文件头);a_code 码值;p_milli 输出毫单位 * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int - * 限定条件:任意上下文(只在回放页与单测用,不在 1 kHz 采集链上;用 int64 中间量) - * 函数说明:模型 eng = (code + b) × k × 1e-6(§6.3,与 adc_cal_apply() 同式), - * k = k_q16 / 65536 µV/码 → mV = (code + b) × k_q16 / 65536000,四舍五入。 - * 中间量最大约 (32767 + 8192) × 5.28e7 ≈ 2.2e12,int64 宽裕。 + * 限定条件:任意上下文(只在回放页与单测用,不在 1 kHz 采集链上;全程 int64) + * 函数说明:模型 eng = (code + b) × k × 1e-6 × r(§6.3,与 adc_cal_apply() 同式)。 + * 目标量(Q16 全定点,**只在最后圆整一次**): + * t = (code + b) × k_q16 // µV × 65536 + * r_q16 = 0 → m = 圆整(t / 65536000) // 引脚域 mV,与 v1 同式 + * r_q16 ≠ 0 → m = 圆整(t × r_q16 / (65536000 × 65536)) + * + * 两个坑,都必须显式躲开: + * ① 不能一次乘完:t ≤ 4.3e12、r_q16 ≤ 2.147e9,乘积 9e21 溢出 int64; + * ② 不能「先圆整成整数 mV,再乘变比」:中间那次圆整会让 r_q16 = 65536 + * (比值 1.0,即 v1 的引脚域口径)的输出差 ±1 LSB,而「r = 1.0 必须与 + * 旧口径逐位一致」正是两份独立实现对齐的锚点(PC 解码器与单测都靠它)。 + * 故用**余数分解**换出无溢出的等价单次圆整: + * 令 t = q × 65536000 + rem(rem ∈ [0, 65536000)), + * 则 t/65536000 = q + rem/65536000, + * q×r/65536 = P + rem2/65536(P = 商、rem2 ∈ [0, 65536)), + * 合到同一分母 den = 65536000 × 65536 后一次圆整: + * m = P + (rem2 × 65536000 + rem × r + den/2) / den + * 各项上界:q ≤ 1.3e5、q×r ≤ 2.8e14;rem2×65536000 ≤ 4.3e12; + * rem×r ≤ 1.4e17 —— 全在 int64(9.2e18)以内。 + * 圆整 = 远离零方向 + 半量进位(先取绝对值再补符号,等价于对零对称)。 + * ⚠ 防御:合法头里 |b| ≤ 8192、k ≤ 2 × ROM = 1.056e8,故 |t| ≤ 4.3e12; + * CRC 通过但字段是垃圾的头也能进来,故对 |t| 与输出各设一道饱和闸, + * 宁可屏上饱和也不要在 int64 里溢出(C 的溢出是 UB,不是「结果偏大」)。 */ int wave_store_code_to_milli(const wave_ch_desc_t *p_desc, int16_t a_code, int32_t *p_milli) { - int64_t num; - int64_t den = 65536000; - int64_t v; + const int64_t den_k = 65536000; /* 1000 × 2^16 */ + const int64_t den_m = 65536000LL * 65536LL; /* 1000 × 2^32 */ + int64_t t; + int64_t r; + int64_t q; + int64_t rem; + int64_t p; + int64_t rem2; + int64_t qr; + int64_t m; + int neg; if ((p_desc == NULL) || (p_milli == NULL)) { return WAVE_ST_E_PARAM; } - num = ((int64_t)a_code + (int64_t)p_desc->b_codes) * (int64_t)p_desc->k_q16; - if (num >= 0) { - v = (num + (den / 2)) / den; - } else { - v = -(((-num) + (den / 2)) / den); + t = ((int64_t)a_code + (int64_t)p_desc->b_codes) * (int64_t)p_desc->k_q16; + r = (int64_t)p_desc->r_q16; + if (r == 0) { + /* 本路不折算:引脚域整数 mV —— 与 v1 逐位一致(单次圆整) */ + *p_milli = (int32_t)wave_st_div_round(t, den_k); + return WAVE_ST_OK; } - *p_milli = (int32_t)v; + /* 绝对值域分解:圆整对零对称,最后补回符号 */ + neg = ((t < 0) != (r < 0)) ? 1 : 0; + if (t < 0) { + t = -t; + } + if (r < 0) { + r = -r; + } + if (t > 8000000000000LL) { + /* 字段是垃圾(合法上限约 4.3e12):饱和而不溢出 */ + *p_milli = (neg != 0) ? (-2147483647L - 1) : 2147483647L; + return WAVE_ST_OK; + } + q = t / den_k; + rem = t % den_k; + qr = q * r; + p = qr / 65536; + rem2 = qr % 65536; + /* 一次圆整:分子 = 两段余数并到 den_m 定标 + 半量 */ + m = p + ((rem2 * den_k) + (rem * r) + (den_m / 2)) / den_m; + if (m > 2147483647LL) { + m = 2147483647LL; + } + *p_milli = (int32_t)((neg != 0) ? -m : m); return WAVE_ST_OK; } @@ -580,7 +660,7 @@ int wave_store_code_to_milli(const wave_ch_desc_t *p_desc, int16_t a_code, * 函数功能:整段镜像自检(头 + 数据区 + 数据 CRC) * 入口参数:p_img 镜像;a_len 长度;p_meta 输出段头(可空) * 返 回 值:WAVE_ST_OK 或 WAVE_ST_E_*(见 wave_store.h) int - * 限定条件:任意上下文;整段驻留时用(本波 5250 字节) + * 限定条件:任意上下文;整段驻留时用(本波 5282 字节) * 函数说明:三条判废一条都不能省:长度 0 或不等、头 CRC、数据 CRC。 * 期望总长用**头里的字段**算,不用编译期常量——文件是自己说它多长, * 与镜像实际长度不一致就是坏段(§6.2 末行)。 @@ -692,7 +772,7 @@ int wave_store_pack_next(wave_pack_ctx_t *p_ctx, uint8_t *p_out, return WAVE_ST_DONE; } - /* --- 头:整头一次产完(接线索按 §7 给 256 字节缓冲,足够 128 字节整头) --- */ + /* --- 头:整头一次产完(接线索按 §7 给 256 字节缓冲,足够 160 字节整头) --- */ if (p_ctx->phase == (uint8_t)WAVE_PACK_HEAD) { uint32_t head_sz; int rc; @@ -702,7 +782,7 @@ int wave_store_pack_next(wave_pack_ctx_t *p_ctx, uint8_t *p_out, return WAVE_ST_E_SIZE; } if (a_cap < head_sz) { - /* 不拆着头给:半截头意味着游标要存 128 字节中间态,纯浪费 */ + /* 不拆着头给:半截头意味着游标要存 160 字节中间态,纯浪费 */ return WAVE_ST_E_PARAM; } rc = wave_store_head_build(&p_ctx->meta, p_out, a_cap); diff --git a/App/algo/wave_store.h b/App/algo/wave_store.h index c39b26d9..66c24bfe 100644 --- a/App/algo/wave_store.h +++ b/App/algo/wave_store.h @@ -11,15 +11,16 @@ * 职责边界(为什么这样切): * 1) 固定头 + 通道表编解码、头 CRC 与数据区 CRC 自检 —— 格式知识只此一处; * 2) 打包游标(分次搬运):文件系统唯一写者只要「每次给我不超过 256 字节的下一片」, - * 整段 5250 字节不驻留 RAM(设计说明 §7:搬运缓冲 256 B × 2,不做整段驻留); + * 整段 5282 字节不驻留 RAM(设计说明 §7:搬运缓冲 256 B × 2,不做整段驻留); * 3) 段文件名轮转与反解:只算名字,不打开文件(FatFs FF_USE_LFN=0 只能严格 * 8.3 短名,见 docs/架构设计/STM32F4-Base存储架构说明.md §10.1); * 4) 数据区偏移换算 + 小端解码:回放页只画一路(设计说明 §8.1),按通道分列 * 让「只读那一路」成为一次连续读。 - * 不链 App/algo/adc_cal(零依赖 = 主机免桩可测):通道表里的 k_q16 / b_codes / + * 不链 App/algo/adc_cal(零依赖 = 主机免桩可测):通道表里的 k_q16 / b_codes / r_q16 / * 可用性由调用方在封存瞬间取 int32 原值填进 meta 后交给本层编码。系数必须走 - * adc_cal_k_q16()(接线道补的 int32 直读入口),不得经 adc_cal_k_uv() 的 float - * 通道回算 —— float 在 5.28e7 量级掉尾数(设计说明 §6.3「k / b 宽度与角色」行)。 + * adc_cal_k_q16() / adc_cal_ratio_q16()(接线道补的 int32 直读入口),不得经 + * adc_cal_k_uv() / adc_cal_ratio() 的 float 通道回算 —— float 在 5.28e7 量级掉尾数 + * (设计说明 §6.3「k / b 宽度与角色」行)。 * CRC 一张表的原始说法不成立,lead 已在 §6.3 记录实测依据:全仓无可复用的 * CRC-16 表(App/util/README.md 记 crc.c 与 crc.h 已于 2026-10-02 删除;屏板链路 * Drivers/BSP/UI/ui_net_sink.c 的 remo_crc() 是单字节异或、不是 CRC-16)。 @@ -30,6 +31,9 @@ * 修改记录: * v1.0 2026-10-29 DTU 项目组 创建:段头/通道表编解码、CRC-16/MODBUS 无表实现、 * 打包游标、段名轮转与反解、数据区偏移换算与小端解码 + * v2.0 2026-11-11 DTU 项目组 格式升 v2:通道表加 int32 r_q16(变比 Q16,12 -> 16 字节/路), + * 整头 128 -> 160、整段 5250 -> 5282;折算口径加 ×r/65536 + * (只在最后圆整一次:一次乘完溢出 int64)。理由见 adc_cal.h v2.0 * * 二期不返工的结构约束(设计说明 §2.1,改本层前先读): * 1) 采样率与每周波点数是写进文件头的数据,不是编译期假设 —— 回放端与 PC 脚本一律 @@ -55,8 +59,10 @@ extern "C" { /* 魔数前两字节固定 'W' 与 'R',第三字节 = 版本号(设计说明 §6.2 偏移 0 行) */ #define WAVE_ST_MAGIC0 (0x57u) /* 'W' */ #define WAVE_ST_MAGIC1 (0x52u) /* 'R' */ -/* 版本:本波恒 1。ver 字段必须同时等于魔数第三字节与本宏,否则整段判废(§6.2 末行) */ -#define WAVE_ST_VER (1u) +/* 版本:本波恒 2。ver 字段必须同时等于魔数第三字节与本宏,否则整段判废(§6.2 末行)。 + * v1 -> v2 的理由:通道表加 int32 变比 r_q16(一路 12 -> 16 字节),字节布局变了必须升版本, + * 否则 v1 文件会被按 v2 布局错读(老文件一律判「版本不认识」,不做兼容解码)。 */ +#define WAVE_ST_VER (2u) /* 固定头字段偏移(字节,小端) */ #define WAVE_ST_OFF_MAGIC (0u) /* 3 字节:'W' 'R' ver */ @@ -76,15 +82,20 @@ extern "C" { #define WAVE_ST_OFF_SEQ (26u) /* 4 字节 u32 */ #define WAVE_ST_OFF_TBL (30u) /* 通道表起点 */ -/* 通道表一路 12 字节 = 语义号 1 + 来源转换器 1 + 位宽 1 + 可用性 1 + k_q16 4 + b_codes 4 - * 设计说明 §6.3「LSB 从哪来」:不设独立 LSB 字段,k_q16 就是 LSB(单位 µV/码 × 65536) */ +/* 通道表一路 16 字节 = 语义号 1 + 来源转换器 1 + 位宽 1 + 可用性 1 + k_q16 4 + b_codes 4 + * + r_q16 4(v2 新增) + * 设计说明 §6.3「LSB 从哪来」:不设独立 LSB 字段,k_q16 就是 LSB(单位 µV/码 × 65536) + * 为什么 r 单独一列而不是折进 k:k 是板级前端斜率(ROM 固化),r 是端子变比(现场标定), + * 两者量级差几个数量级——折进去后端子域的 k = 152587.9 µV/码,Q16 达 1.0e10,超出 int32 + * 上限 2.147e9,根本存不下;分开存 r(1000.0 → 65536000)完全够(见 App/algo/adc_cal.h)。 */ #define WAVE_ST_CH_OFF_SEM (0u) /* 1 字节 */ #define WAVE_ST_CH_OFF_CONV (1u) /* 1 字节 */ #define WAVE_ST_CH_OFF_BITS (2u) /* 1 字节 */ #define WAVE_ST_CH_OFF_AVAIL (3u) /* 1 字节 = adc_cal_flags(sem) 低 8 位原值,不自造位义 */ #define WAVE_ST_CH_OFF_KQ16 (4u) /* 4 字节 int32 */ #define WAVE_ST_CH_OFF_BCODE (8u) /* 4 字节 int32 */ -#define WAVE_ST_CH_BYTES (12u) +#define WAVE_ST_CH_OFF_RQ16 (12u) /* 4 字节 int32:变比 Q16(比值 × 65536);0 = 本路不折算 */ +#define WAVE_ST_CH_BYTES (16u) /* 本波通道数固定 8(TEMP 不录:理由见 §6.3 末段与 docs/算法/采集与算法层设计说明.md §13.3) */ #define WAVE_ST_CH_NUM (8u) @@ -97,8 +108,8 @@ extern "C" { #define WAVE_ST_PPC_DEF (20u) /* 尺寸一律由字段算出,不写死字面量(改通道数/点数时自动跟着走): - * 头 CRC 覆盖区 = 30 + 12 × ch_count;头 CRC 自身紧随其后 2 字节;数据区自 32 + 12 × ch 起 - * ch_count = 8 时:覆盖区 126、整头 128、数据区 5120、整段 5250(§6.3「CRC 覆盖范围」行) */ + * 头 CRC 覆盖区 = 30 + 16 × ch_count;头 CRC 自身紧随其后 2 字节;数据区自 32 + 16 × ch 起 + * ch_count = 8 时:覆盖区 158、整头 160、数据区 5120、整段 5282(§6.3「CRC 覆盖范围」行) */ #define WAVE_ST_HEAD_CRC_POS(ch) ((uint32_t)(WAVE_ST_OFF_TBL) + \ ((uint32_t)(ch) * WAVE_ST_CH_BYTES)) #define WAVE_ST_HEAD_BYTES(ch) ((uint32_t)(WAVE_ST_HEAD_CRC_POS(ch)) + 2u) @@ -106,8 +117,8 @@ extern "C" { #define WAVE_ST_SEG_TOTAL(ch, pts) (WAVE_ST_HEAD_BYTES(ch) + \ WAVE_ST_DATA_BYTES(ch, pts) + 2u) /* 本波实际值(只给注释与单测引用,代码里一律用上面的参数化形态) */ -#define WAVE_ST_HEAD_SZ_CUR (128u) -#define WAVE_ST_SEG_SZ_CUR (5250u) +#define WAVE_ST_HEAD_SZ_CUR (160u) +#define WAVE_ST_SEG_SZ_CUR (5282u) /* 段目录与轮转(设计说明 §5.2 第三条:只能严格 8.3 短名;目录与 /log 平级、FTP 根下可见) * 名字形态 = 目录 + 前缀 + 两位十进制 + 扩展名,号 0..99 共 100 个轮转覆盖 */ @@ -161,6 +172,7 @@ typedef struct { uint8_t avail; /* 可用性 = adc_cal_flags(sem) 低 8 位原值(不自造位义) */ int32_t k_q16; /* 封存瞬间生效系数:µV/码 × 65536,就是 LSB */ int32_t b_codes; /* 封存瞬间生效零点偏移:码值 */ + int32_t r_q16; /* 封存瞬间生效变比:比值 × 65536;0 = 本路不折算(引脚域) */ } wave_ch_desc_t; /* 段头元数据 = 固定头 + 通道表的内存形态。 @@ -270,9 +282,10 @@ int wave_store_crc_selftest(void); * (UA / UN / IA 走 TPAFE 16 位;UB / UC / IB / IC / IN 走片内 ADC 12 位 —— * 通道归属口径属主是 docs/硬件平台/ADC通道映射说明.md,本函数只是它的抄本, * 单测里对 0xE6 的算术交叉验证会同时钉住这套顺序)。 - * 不填 avail / k_q16 / b_codes / trig_* / t0_ms / wall / eng / flags_seq / + * 不填 avail / k_q16 / b_codes / r_q16 / trig_* / t0_ms / wall / eng / flags_seq / * clip_mask / frame_seq:那些是封存瞬间的事实,必须由调用方现取现填 - * (adc_cal_k_q16 / adc_cal_flags / sys_clock / wave_ring),本层不链它们。 + * (adc_cal_k_q16 / adc_cal_ratio_q16 / adc_cal_flags / sys_clock / wave_ring), + * 本层不链它们。 */ int wave_store_meta_init(wave_seg_meta_t *p_meta); @@ -313,7 +326,7 @@ int wave_store_head_parse(const uint8_t *p_buf, uint32_t a_len, wave_seg_meta_t * p_meta 输出段头(可为 NULL) * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_SIZE 长度为 0 或与期望总长不符; * WAVE_ST_E_CRC 任一 CRC 不符;WAVE_ST_E_FORMAT 头本身判废 int - * 限定条件:任意上下文;只在整段驻留(本波 5250 字节)时用。回放页不要走这条 —— + * 限定条件:任意上下文;只在整段驻留(本波 5282 字节)时用。回放页不要走这条 —— * 用 wave_store_head_parse + wave_store_data_off + wave_store_crc_update 分段查, * 才符合 §7 的 256 字节读窗预算 * 函数说明:这是「段文件 CRC 不符时脚本必须拒绝而不是画出东西」那条判据在板侧的同一份 @@ -324,7 +337,7 @@ int wave_store_seg_check(const uint8_t *p_img, uint32_t a_len, wave_seg_meta_t * /* * 函数功能:按头部字段算整段文件应有的字节数(写侧记账与读侧长度判废共用) * 入口参数:p_meta 段头元数据 - * 返 回 值:期望字节数(8 路 320 点 = 5250);字段非法(含零长度)返回 0 uint32_t + * 返 回 值:期望字节数(8 路 320 点 = 5282);字段非法(含零长度)返回 0 uint32_t * 限定条件:任意上下文 * 函数说明:返回 0 就是「这一段连长度都不成立」,据此判废。不要拿 0 去开缓冲, * 也不要拿 0 去比对 f_write 的写回字节数(那会把失败读成成功)。 @@ -381,16 +394,23 @@ int wave_store_le_decode(const uint8_t *p_raw, uint32_t a_len, /* * 函数功能:把一路的 int16 码值折成定点毫单位(回放页峰值标注用;纯整数,不用 float) - * 入口参数:p_desc 该路通道表项(k_q16 / b_codes 来自段文件头,不来自当前固件) + * 入口参数:p_desc 该路通道表项(k_q16 / b_codes / r_q16 来自段文件头,不来自当前固件) * a_code 原始码值 - * p_milli 输出四舍五入后的毫单位(k_q16 是 µV/码 → 输出单位 mV)(不可为 NULL) + * p_milli 输出四舍五入后的毫单位(**录波域**:eng=1 时是二次 mV/mA, + * eng=0 时是引脚 mV)(不可为 NULL) * 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int * 限定条件:任意上下文(只在回放页与单测里用,不在 1 kHz 采集链上;内部用 int64 中间量, * 落在这条路径上时可接受) - * 函数说明:算式 eng = (code + b) × k × 1e-6,与 App/algo/adc_cal.c 的 adc_cal_apply() - * 同一模型(§6.3)。输出单位是 mV;单位含义由段头 eng 字段决定(eng = 0 是 - * 引脚域,别读成二次值)。必须用段文件里的 k / b,不许拿当前固件的系数解释 - * 老文件(§12 风险表「标定落地后老文件被解释错」)。 + * 函数说明:算式 eng = (code + b) × k × 1e-6 × r,与 App/algo/adc_cal.c 的 adc_cal_apply() + * 同一模型(§6.3);r 以 Q16 存,折算即再乘 r_q16 / 65536。 + * 输出单位是「录波域毫单位」:段头 eng 决定这毫单位指什么(eng = 1 二次值, + * eng = 0 引脚电压),本函数只按 r_q16 是否非零给出对应域,不猜单位。 + * **只在最后圆整一次**:一次乘完会溢出 int64(分子到 9e21),而「先圆整成整数 + * mV 再乘变比」又会让 r_q16 = 65536(比值 1.0)差 ±1 LSB —— 后者是两份实现 + * 对齐的锚点,不能动。实现用余数分解换等价单次圆整(分解式与各上界见 + * App/algo/wave_store.c 的 wave_store_code_to_milli 注释)。 + * 必须用段文件里的 k / b / r,不许拿当前固件的系数解释老文件 + * (§12 风险表「标定落地后老文件被解释错」)。 */ int wave_store_code_to_milli(const wave_ch_desc_t *p_desc, int16_t a_code, int32_t *p_milli); @@ -423,8 +443,8 @@ int wave_store_pack_start(wave_pack_ctx_t *p_ctx, const wave_seg_meta_t *p_meta, * 负 = WAVE_ST_E_*(未 start / 容量为 0 / 数据源回调返错 / 游标状态错) int * 限定条件:任意上下文,但同一游标不得并发使用(文件系统唯一写者单线程推进) * 函数说明:头区字节流与 wave_store_head_build 完全一致(内部同一份写字段函数); - * **头那一步要求 a_cap 不小于整头字节数**(本波 8 路 = 128),否则返回 - * WAVE_ST_E_PARAM 且不推进游标——不拆着头给,是为了让游标不必存 128 字节 + * **头那一步要求 a_cap 不小于整头字节数**(本波 8 路 = 160),否则返回 + * WAVE_ST_E_PARAM 且不推进游标——不拆着头给,是为了让游标不必存 160 字节 * 中间态;§7 的 256 字节搬运缓冲天然满足。 * 数据区 CRC 走增量中间值,整段最后两片是数据 CRC 的低字节、高字节。 * 调用方每写完一片都必须比对 f_write 写回字节数,短写按失败处理(§5.2)。 diff --git a/App/app/app_main.c b/App/app/app_main.c index 4f25fa7d..565efcbe 100644 --- a/App/app/app_main.c +++ b/App/app/app_main.c @@ -26,7 +26,7 @@ #include "gd5f2gq5ue.h" #include "tpafe5160.h" #include "adc_input.h" /* 语义取数与有效性层:adc_input_init()(语义映射 + STM32 ADC1 扫描) */ -#include "adc_cal.h" /* 标定折算层:adc_cal_init()(ROM 系数 + cfg 两段注册,道A 标定折算与落盘整定) */ +#include "adc_cal.h" /* 标定折算层:adc_cal_init()(ROM 系数 + cfg 三段注册,道A 标定折算与落盘整定) */ #include "run_par.h" /* 运行参数段:run_par_init()([run_par] 注册 + 104 地址映射真源,道D 这一项) */ #include "iec104_comm.h" /* [comm] 段:iec104_comm_init()(ca/端口注册,道G task-8) */ #include "phasor.h" /* 全周傅氏相量层:phasor_init()(系数表 + 实例 + 双区交换) */ @@ -292,14 +292,16 @@ int app_main_init(void) { DBG_INFO("freq layer init: OK"); /* 4c-3) 通道标定层(标定折算,「数据可信」标定折算与落盘整定)初始化:RAM 系数副本置 - * ROM 默认(量程口径冻结定死的引脚侧 LSB,docs/硬件平台/ADC通道映射说明.md §7.1) - * + 向配置引擎注册 [adc_cal] / [adc_cal_ofs] 两段。注册必须先于 + * ROM 默认(k = 量程口径冻结定死的引脚侧 LSB,docs/硬件平台/ADC通道映射说明.md §7.1; + * r = 台架实测反算的端子变比,同文档 §7.2) + * + 向配置引擎注册 [adc_cal] / [adc_cal_ofs] / [adc_cal_sec] 三段。注册必须先于 * 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); + DBG_INFO("adc cal init: OK (k_q16 tpafe=%ld stm32=%ld r_q16 v=%ld i=%ld)", + (long)ADC_CAL_K_Q16_TPAFE, (long)ADC_CAL_K_Q16_STM32, + (long)ADC_CAL_R_Q16_VOLT, (long)ADC_CAL_R_Q16_CURR); /* 4c2) 运行参数段初始化(「数据可信」这一项):死区 9 点工厂默认 + 注册 * [run_par] 段。同 adc_cal 的「先注册后 load」契约 → 必须内核前; diff --git a/App/app/ui_data_feed.c b/App/app/ui_data_feed.c index b3fcc992..3ddb4e2c 100644 --- a/App/app/ui_data_feed.c +++ b/App/app/ui_data_feed.c @@ -218,16 +218,25 @@ static void feed_map_phasor(void) { * 入口参数:无 * 返回值:UI_DATA_ENG_PIN 或 UI_DATA_ENG_SEC uint8_t * 限定条件:无(纯读) - * 函数说明:**全或无**——电气 8 路(TEMP 除外,它不参与线性标定)的 - * ADC_CAL_F_VALID 全置位才升二次值。取全或无是因为整帧共用一个 eng: - * 只要还有一路是引脚电压,页面挂"二次值"就会把那一路的读数误标 - * (宁可整体显示"未标定",也不给部分真值贴真值标签) + * 函数说明:判据 = 「每路要么已配变比、要么削顶不显示」(2026-11-11 口径变更): + * 电气 8 路里,凡**未被削顶**的通道都必须 `adc_cal_ratio_configured()`, + * 全部满足才升二次值;削顶通道不参与断言——它们页面本来就显 "--", + * 不进任何读数,拿它们卡住整帧标签只会让 V0.1 那五路削顶把徽标永久钉在 + * "未标定"上。 + * ⚠ 与 `adc_cal_prot_blocked()`(判据闭锁看 UA/UN/IA 三路 VALID)**不是 + * 同一件事,别合并**:那个问"准不准"、这个问"什么单位"。也没用 VALID: + * 只配了变比、没做两点标定时输出**确实是**二次值,只是精度未定—— + * 量纲标签若跟着 VALID 走,屏上就会出现"数值是 5.000(二次)却标未标定 V" + * 这种自相矛盾。 */ static uint8_t feed_eng(void) { uint8_t sem; for (sem = 0u; sem < (uint8_t)ADC_SEM_TEMP; sem++) { - if ((adc_cal_flags((adc_semantic_t)sem) & ADC_CAL_F_VALID) == 0u) { + if ((adc_cal_flags((adc_semantic_t)sem) & ADC_CAL_F_CLIPPED) != 0u) { + continue; /* 削顶通道不显示读数,不参与整帧量纲断言 */ + } + if (adc_cal_ratio_configured((adc_semantic_t)sem) == 0u) { return UI_DATA_ENG_PIN; } } @@ -322,7 +331,8 @@ static void feed_fill_rt(ui_data_rt_t *p_snap, uint32_t tick_ms) { p_snap->valid = 1u; p_snap->eng = feed_eng(); - /* 遥测:语义通道序的基波有效值(采样环已按 adc_cal 折算 → 当前为引脚电压域) */ + /* 遥测:语义通道序的基波有效值(采样环已按 adc_cal 折算 → 量纲看 eng: + * 已配变比的路是二次值 V/A,未配的是引脚电压 V) */ for (sem = 0u; sem < (uint8_t)ADC_SEM_COUNT; sem++) { int8_t pi = s_yc_phasor[sem]; diff --git a/App/task/adc_task.c b/App/task/adc_task.c index 6ee0ff66..0cdc61a9 100644 --- a/App/task/adc_task.c +++ b/App/task/adc_task.c @@ -519,38 +519,55 @@ void adc_task_func(void *arg) { wev = wave_ring_tick(now, s_frames_consumed); if (wev == WAVE_RING_EVT_SEALED) { - uint8_t c; /* 封存瞬间事实(交棒结构 s_wave_cal 为文件级 static,不上栈): - * eng / VALID 位图 / CLIPPED 位图 / 墙钟 / 逐路 conv+bits+avail+k+b。 + * eng / VALID 位图 / CLIPPED 位图 / 墙钟 / 逐路 conv+bits+avail+k+b+r。 * eng 口径 = adc_cal.h 注③ 的 8 路量纲标签(判据源 * App/app/ui_data_feed.c feed_eng(),与 prot_blocked 的三路口径**不是** - * 一件事):8 路电气量全 VALID 才是二次值;V0.1 无 CT 变比,一次值不可达。 + * 一件事):**每路要么已配变比、要么削顶(页面本来就显 "-",不参与 + * 整帧量纲断言)——八路全满足即二次值,否则引脚域**。不再看 VALID: + * VALID 回答"准不准"、r 回答"什么单位",V0.1 五路削顶永远拿不到 VALID, + * 用 VALID 当量纲判据会让徽标永不解除。 * wall 未对时写 0(sys_clock_get 失败 = UINT32_MAX,不许拿上电毫秒冒充)。 - * k 取 adc_cal_k_q16() int32 原值;b 的 float 回乘在本域无损 - * (|b| 限幅 8192 << 2^24,float32 精确表示整数;禁用浮点只针对 k)。 */ + * k / r 取 adc_cal_k_q16() / adc_cal_ratio_q16() 的 int32 原值(float 在 + * 5.28e7 / 6.5e7 量级掉尾数);b 的 float 回乘在本域无损 + * (|b| 限幅 8192 << 2^24,float32 精确表示整数;禁用浮点只针对 k / r)。 */ s_wave_cal.flags_seq = 0u; s_wave_cal.clip_mask = 0u; - for (c = 0u; c < WAVE_RING_CH; c++) { - const uint32_t f = adc_cal_flags((adc_semantic_t)c); - const adc_channel_desc_t *d = adc_input_desc((adc_semantic_t)c); + /* 整帧量纲:每路要么已配变比、要么削顶不显示 → 全满足即二次值。 + * 削顶通道不参与断言的理由:它们页面本来就显 "-",把它们的量纲也算进 + * 整帧会把「已全部配好变比」的段误标成引脚域(V0.1 五路削顶恒成立)。 + * 与逐路取数**同一遍循环**算完:adc_cal 每拍只走一遍(逐路 flags / + * ratio_configured / k_q16 / b_codes / ratio_q16 五个纯读子查询), + * 不为了一个派生量再走第二趟。 */ + { + uint8_t eng_ok = 1u; + uint8_t c; - if ((f & ADC_CAL_F_VALID) != 0u) { - s_wave_cal.flags_seq |= (uint8_t)(1u << c); + for (c = 0u; c < WAVE_RING_CH; c++) { + const uint32_t f = adc_cal_flags((adc_semantic_t)c); + const adc_channel_desc_t *d = adc_input_desc((adc_semantic_t)c); + + if ((f & ADC_CAL_F_VALID) != 0u) { + s_wave_cal.flags_seq |= (uint8_t)(1u << c); + } + if ((f & ADC_CAL_F_CLIPPED) != 0u) { + s_wave_cal.clip_mask |= (uint16_t)(1u << c); + } else if (adc_cal_ratio_configured((adc_semantic_t)c) == 0u) { + eng_ok = 0u; + } + s_wave_cal.ch[c].conv = (d != NULL) ? (uint8_t)d->source + : (uint8_t)ADC_SRC_STM32; + s_wave_cal.ch[c].bits = (s_wave_cal.ch[c].conv == (uint8_t)ADC_SRC_TPAFE) + ? 16u : 12u; + s_wave_cal.ch[c].avail = (uint8_t)f; + s_wave_cal.ch[c].k_q16 = adc_cal_k_q16((adc_semantic_t)c); + s_wave_cal.ch[c].b_codes = (int32_t)adc_cal_b_codes((adc_semantic_t)c); + s_wave_cal.ch[c].r_q16 = adc_cal_ratio_q16((adc_semantic_t)c); } - if ((f & ADC_CAL_F_CLIPPED) != 0u) { - s_wave_cal.clip_mask |= (uint16_t)(1u << c); - } - s_wave_cal.ch[c].conv = (d != NULL) ? (uint8_t)d->source - : (uint8_t)ADC_SRC_STM32; - s_wave_cal.ch[c].bits = (s_wave_cal.ch[c].conv == (uint8_t)ADC_SRC_TPAFE) - ? 16u : 12u; - s_wave_cal.ch[c].avail = (uint8_t)f; - s_wave_cal.ch[c].k_q16 = adc_cal_k_q16((adc_semantic_t)c); - s_wave_cal.ch[c].b_codes = (int32_t)adc_cal_b_codes((adc_semantic_t)c); + s_wave_cal.eng = (eng_ok != 0u) + ? (uint8_t)WAVE_ENG_SEC : (uint8_t)WAVE_ENG_PIN; } - s_wave_cal.eng = (s_wave_cal.flags_seq == 0xFFu) - ? (uint8_t)WAVE_ENG_SEC : (uint8_t)WAVE_ENG_PIN; { uint32_t wall = sys_clock_get(); diff --git a/App/task/default_task.c b/App/task/default_task.c index 6273d031..06da9c86 100644 --- a/App/task/default_task.c +++ b/App/task/default_task.c @@ -87,7 +87,7 @@ * (自检已上报、卷保留、不重格式化),等下次上电重新 resume */ #define STORAGE_BRINGUP_RETRIES 3u -/* 录波段落盘持锁耗时告警线(lead 裁③i):一次心跳整段 5250 B(约 21 次 256 B +/* 录波段落盘持锁耗时告警线(lead 裁③i):一次心跳整段 5282 B(约 21 次 256 B * f_write),设计 §4.2 预估落盘窗「几十 ms」;越过本线说明本段在占用 defaultTask * 喂狗链前的最慢一段,按事件补一条 WARN(不是周期行),并把历史峰值留在本层 * 静态里供收工报告核喂狗余量(vol_lock.h 铁律「等锁即毁喂狗链」的前例在案) */ @@ -267,7 +267,8 @@ void default_task_func(void *argument) { syslog_post(SYSLOG_OPER, "settings saved"); } /* 运行参数段落盘(「数据可信」这一项):同款脏语义;save = 引擎整档外科写 - * ([run_par] 9 键 + 被 adc_cal_set 联动改写的 [adc_cal] 两段一趟落) */ + * ([run_par] 9 键 + 被 adc_cal_set() / adc_cal_set_ratio() 联动改写的 + * [adc_cal] / [adc_cal_ofs] / [adc_cal_sec] 三段一趟落) */ (void)run_par_store_save(); /* [comm] 段落盘(道G task-9 通讯设置页):同款脏位、每轮无条件, * 无事件位(task-8 D5 口径——ca 改后重启生效,落盘只保断电不丢) */ @@ -624,7 +625,7 @@ static FRESULT storage_bring_up(FATFS *fs) { * 入口参数:无 void * 返回值:无 void * 限定条件:仅 defaultTask 主循环调用(vol_idle_sync_poll 之后);内部绝不阻塞—— - * try 锁 + 有界段长(5250 B / 256 B ≈ 21 次 f_write,设计 §4.2 预估几十 ms), + * try 锁 + 有界段长(5282 B / 256 B ≈ 21 次 f_write,设计 §4.2 预估几十 ms), * 绝不把喂狗点做成等待态(vol_lock.h 使用约定:defaultTask 一律 try) * 函数说明:所有权按 docs/子系统设计/故障录波设计说明.md §5.1——环的生产者是 * adcTask,本道是唯一落盘消费者;跨任务握手只认 wave_ring_sealed()。 @@ -632,9 +633,10 @@ static FRESULT storage_bring_up(FATFS *fs) { * ① 打开一律 FA_WRITE|FA_CREATE_ALWAYS(**覆盖,不是追加**):段名是 100 个 * 槽位的轮转复用(wave_store.h 的「号 0..99 轮转覆盖,写满回绕盖最老的」), * 槽位被复用时同名文件已存在——用 FA_OPEN_APPEND 会把新段接在旧段尾巴上, - * 文件长成 5250×N,而回放页的单段读侧要求 size == 5250,于是逐件判 + * 文件长成 段长×N,而回放页的单段读侧要求 size == 段长,于是逐件判 * 「长度废」(reject=2),全板没有一段放得出来。2026-11-10 板级实测: - * 100 个文件 / 1868 段全部合法、零损坏,纯粹是容器尺寸与写侧追加语义相抵。 + * 100 个文件 / 1868 段全部合法、零损坏,纯粹是容器尺寸与写侧追加语义相抵 + * (当时段长 5250 B = v1 布局;v2 加变比维后为 5282 B,判据形状不变)。 * 本移植 FA_CREATE_ALWAYS 会 remove_chain 并清零目录项的 FileSize, * 是真正的截断(Lib/FatFs/ff.c 的 f_open 覆盖分支); * 注意 FA_CREATE_ALWAYS 单独用**不含 FA_WRITE**(本移植旧式标志位), @@ -802,7 +804,7 @@ static void wave_store_drain(void) { if (rc == WAVE_ST_DONE) { break; } - if (++iter > 32u) { /* 5250/256 = 21 片;游标卡死时的有界保险 */ + if (++iter > 32u) { /* 5282/256 ≈ 21 片;游标卡死时的有界保险 */ why = "pack stall"; break; } diff --git a/App/task/ui_task.c b/App/task/ui_task.c index 76861d41..d390e0da 100644 --- a/App/task/ui_task.c +++ b/App/task/ui_task.c @@ -84,7 +84,7 @@ * wave_store.h 给段格式解析与定点 mV 换算(唯一口径,页面不复述算式); * vol_lock.h + ff.h:/wave 只读遍历按 soe_store.c 读者同款纪律—— * try 锁非阻塞(卷忙本轮按「无段」处理,uiTask 绘制节拍绝不等待)、 - * 锁跨完整文件操作序列、小窗口分次读、整段 5250B 绝不驻留 RAM */ + * 锁跨完整文件操作序列、小窗口分次读、整段 5282B 绝不驻留 RAM */ #include "ff.h" #include "vol_lock.h" #include "wave_ring.h" @@ -404,7 +404,7 @@ static const ui_soe_reader_t s_soe_reader = { ui_task_soe_count, ui_task_soe_get * - 段完整性判定只信 wave_store 自己的判据(尺寸等式 / magic+ver / 双 CRC / * sem==index),本胶水不读 avail/flags 做二次裁断(§6.3 与 2026-10-29 * 「VALID+CLIPPED 非矛盾」裁定同源:可用性不是完整性); - * - 整段 5250B 不驻留:256B 静态窗口分次读,头区一次、数据区流式过 CRC。 + * - 整段 5282B 不驻留:256B 静态窗口分次读,头区一次、数据区流式过 CRC。 * uiTask 阻塞上界(IWDG 800ms 窗、uiTask 在 required_mask 内;写法对齐上方 * 维护通道那段上界,同文件先例): * - 第零项是「本块不等待」:vol_lock_wait(0) 拿不到锁即按「本轮无段」 @@ -412,7 +412,7 @@ static const ui_soe_reader_t s_soe_reader = { ui_task_soe_count, ui_task_soe_get * - 一次渲染实际发生的 I/O(进回放页首绘、或按键后重绘那帧)三件叠加: * ① 表重扫 = 1 次 f_opendir + 至多 WAVE_ST_SEG_MAX(100)次 f_readdir, * 收尾是 100 元插入排序(纯内存); - * ② 段头装载 = 仅未缓存段才发生:1 次 f_open + 头区 1 次 128B 读 + + * ② 段头装载 = 仅未缓存段才发生:1 次 f_open + 头区 1 次 160B 读 + * head_parse 内的头 CRC + 数据区 20 次 256B 流式读 + 尾 2B 一次 * = 最坏 22 次小读;5120B 逐字节 CRC 与 sem 表校验是纯计算, * 168MHz 下微秒到毫秒量级;缓存命中零卷操作; @@ -589,7 +589,7 @@ static uint8_t ui_task_wave_find(uint8_t a_idx, uint8_t *a_pos) { * 成功则把段头留在 s_wave_meta 供 seg_win 免重读 * 入口参数:a_idx - 段号 * 返回值:UI_WAVE_OK / UI_WAVE_NONE(卷忙/文件没了)/ UI_WAVE_BAD(判废) int - * 限定条件:uiTask;锁跨 open→close 完整序列;头 128B 一次、数据区 256B + * 限定条件:uiTask;锁跨 open→close 完整序列;头 160B 一次、数据区 256B * 分窗流式(共约 1 + 20 次 f_read),整段不驻留 * 耗时账(lead 裁定 2026-10-29 第 1 条·第三条并存):未缓存段的完整性 * 判定最坏约 21 次小读 + 5120B 逐位 CRC,全部在卷锁内、uiTask 上; @@ -597,7 +597,7 @@ static uint8_t ui_task_wave_find(uint8_t a_idx, uint8_t *a_pos) { * 缓存只留一段:左右切段 A→B→A 会把 B 的校验再来一遍——接受该 * 成本的代价换「CRC 不符必须拒收」永远现算,不做跨键存的位图 * 函数说明:缓存命中(同段且已验)直接返 OK——左右切段回到上一段、上下切 - * 通道都靠它把重复的 5250B 全扫压成一次;文件读中途消失(被别的 + * 通道都靠它把重复的 5282B 全扫压成一次;文件读中途消失(被别的 * 写者轮转掉)按 NONE 处理,页面下轮自然重扫;五类判废(reject= * 原因码见上方 UI_WAVE_REJ_* 组)在本函数收尾统一收敛成一条 * DBG_ERROR——只有串口在手的远程轮也要有判废可查面(#93 F2, @@ -641,7 +641,7 @@ static int ui_task_wave_load_head(uint8_t a_idx) { size = (uint32_t)f_size(&f); total = 0u; rc = UI_WAVE_BAD; - /* 第一步:头区(首窗 128B = 当前形态整头);head_parse 自带判废 */ + /* 第一步:头区(首窗 160B = 当前形态整头);head_parse 自带判废 */ n = (size < (uint32_t)WAVE_ST_HEAD_SZ_CUR) ? size : (uint32_t)WAVE_ST_HEAD_SZ_CUR; fr = f_read(&f, s_wave_buf, n, &br); @@ -656,7 +656,7 @@ static int ui_task_wave_load_head(uint8_t a_idx) { uint16_t crc; uint8_t tail[2]; - /* 第三步:数据区流式 CRC(128B 窗;CRC 覆盖不含尾两字节自身) */ + /* 第三步:数据区流式 CRC(256B 窗;CRC 覆盖不含尾两字节自身) */ crc = WAVE_ST_CRC_INIT; off = wave_store_head_bytes(s_wave_meta.ch_count); rc = UI_WAVE_BAD; @@ -876,15 +876,17 @@ static int ui_task_wave_seg_head(uint8_t a_idx, uint8_t *a_eng, /* * 函数功能:读固定显示窗(触发点前后各一周波)的码值与峰值 * 入口参数:a_idx - 段号;a_ch - 通道号;a_codes - UI_WAVE_WIN_PTS 个码值 - * (已并入段头 b_codes);a_peak_milli - 窗内绝对值最大点的 mV + * (已并入段头 b_codes);a_peak_milli - 窗内绝对值最大点的**录波域毫单位** + * (eng=1 二次 mV/mA,eng=0 引脚 mV;变比由 wave_store_code_to_milli + * 按段头 r_q16 现折,本层不重算) * 返回值:UI_WAVE_OK / NONE(段头未验过/卷忙)/ BAD(数据区读不齐) int * 限定条件:uiTask;先段头后窗口(未过完整性判定的段不供数据,页面显示 * 「该段不可读」而不是半截波形——判废优先于展示) * 函数说明:窗口起点用段头 pre_pts 现算(§8.2「数据不是编译常量」: * 起点 = pre_pts - 窗长一半,与光标居中是同一个 20 的一半, - * 改窗宽只改 ui_data.h 的 UI_WAVE_WIN_PTS 一处);峰值的 mV - * 换算逐点走 wave_store_code_to_milli —— 全仓唯一口径, - * 这里不重抄公式(§6.3;PC 道同一条款) + * 改窗宽只改 ui_data.h 的 UI_WAVE_WIN_PTS 一处);峰值的米制单位 + * 换算逐点走 wave_store_code_to_milli —— 全仓唯一口径(v2 起自带变比, + * peak 自动跟着录波域走),这里不重抄公式(§6.3;PC 道同一条款) */ static int ui_task_wave_seg_win(uint8_t a_idx, uint8_t a_ch, int16_t *a_codes, int32_t *a_peak_milli) { @@ -959,10 +961,13 @@ static int ui_task_wave_seg_win(uint8_t a_idx, uint8_t a_ch, a_codes[i] = (int16_t)v; /* 显示与度量同一把尺(「谁被显示谁被度量」,§6.3): * a_codes 画的是 raw+b,而 wave_store_code_to_milli 的公式是 - * (a_code + b_codes) × k_q16 / 65536000 取整(App/algo/ - * wave_store.c:567 实测口径)——传 raw 进去,b 已由唯一口径 - * 自己加过,PK 数与屏上波形天然同尺,这里不再加第二遍 - * (加两遍才是错);clamp 只护 int16 画布,b 的 V0.1 恒 0 */ + * (a_code + b_codes) × k_q16 × r_q16 的定点折算(口径唯一属主 + * App/algo/wave_store.c 的 wave_store_code_to_milli)——传 raw 进去, + * b 与 r 都已由唯一口径自己算过,PK 数与屏上波形天然同尺, + * **这里不再加第二遍 b,也不乘 r**(重复折算才是错); + * 输出是「录波域毫单位」:eng=1 时是二次 mV/mA,eng=0 时是引脚 mV, + * 本段不重算公式、也不按当前固件系数猜单位。 + * clamp 只护 int16 画布,b 的 V0.1 恒 0 */ if (wave_store_code_to_milli(&s_wave_meta.ch[a_ch], raw[i], &milli) == 0) { if ((milli < 0 ? -milli : milli) > diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index 5e6e8693..5cda1b5c 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -447,9 +447,12 @@ flowchart LR - 判据:`ADC input layer init: OK`;8 路 + 温度首帧全部读出、互异、非饱和。 **FR-ADC-3 标定与量纲折算(标定折算)** -- 需求:把码值折算为工程量 `(code + b) × k`,系数与零点可落盘、掉电保持;未标定不得冒充二次值。 -- 实现:`App/algo/adc_cal.c`——`k` 以 Q16(µV/码 ×65536)、`b` 以码值存 `[adc_cal]` / `[adc_cal_ofs]` - 段;ROM 默认 = 引脚侧 LSB(TPAFE 152.59 µV/码、STM32 805.66 µV/码); +- 需求:把码值折算为工程量 `(code + b) × k × r`(`k` 前端斜率、`b` 零点、`r` 端子变比), + 三者可落盘、掉电保持;未标定不得冒充二次值("准不准"由 `VALID` 回答,"什么单位"由 `r` 决定)。 +- 实现:`App/algo/adc_cal.c`——`k` 以 Q16(µV/码 ×65536)存 `[adc_cal]`、`b` 以码值存 `[adc_cal_ofs]`、 + `r` 以 Q16 比值存 `[adc_cal_sec]`;`k` 的 ROM 默认 = 引脚侧 LSB(TPAFE 152.59 µV/码、 + STM32 805.66 µV/码),`r` 的 ROM 默认 = 台架实测反算的端子变比(电压 1000 V/V、电流 + 29.4118 A/V,`docs/硬件平台/ADC通道映射说明.md` §7.2),`r = 0` = 本路不折算; 三态可用性 `ADC_CAL_F_VALID` / `ADC_CAL_F_CLIPPED` 由 `adc_cal_flags()` 单点查询,104 / UI / 保护闭锁共用。 - 判据:已知电压注入斜率与理论 LSB 偏差 ≤2%(2026-10-27 台架实测端子侧 **6544.2 码/V**,与引脚侧 理论 6553.6 差 **−0.14%**,零点 0~5 码、R²=0.9999997、极性对称 ≤0.34%);8 通道系数/零点往返一致。 diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 70251cff..011d300f 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 286,364 B (279.65 KB) | 1,048,576 B (1024 KB) | 27.3% | -| RW_IRAM1 | 96,808 B (94.54 KB) | 131,072 B (128 KB) | 73.9% | +| ER_IROM1 | 287,784 B (281.04 KB) | 1,048,576 B (1024 KB) | 27.4% | +| RW_IRAM1 | 97,024 B (94.75 KB) | 131,072 B (128 KB) | 74.0% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **288,444 B (281.7 KB)** | 1,048,576 B (1024 KB) | **27.5%** | +| **Flash (ROM 合计)** | **289,864 B (283.1 KB)** | 1,048,576 B (1024 KB) | **27.6%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 0af41aeb..6337e687 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,18 +15,18 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 286,364 B | 1,048,576 B | 27.3% | 参考 | +| ER_IROM1 (Flash) | 287,784 B | 1,048,576 B | 27.4% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 96,808 B | 131,072 B | 73.9% | PASS | +| RW_IRAM1 | 97,024 B | 131,072 B | 74.0% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| | 网络 | 13,958 B | 12 KB | ⚠ 超 1.6 KB | | 存储 | 9,144 B | 12 KB | OK | -| UI | 36,475 B | 16 KB | ⚠ 超 19.6 KB | +| UI | 36,511 B | 16 KB | ⚠ 超 19.7 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | -| 应用 | 26,433 B | 8 KB | ⚠ 超 17.8 KB | +| 应用 | 26,609 B | 8 KB | ⚠ 超 18.0 KB | | 其它 | 10,792 B | — | 参考 | diff --git a/docs/内存管理/内存内容分配策略.md b/docs/内存管理/内存内容分配策略.md index 1985efa4..7b1810cf 100644 --- a/docs/内存管理/内存内容分配策略.md +++ b/docs/内存管理/内存内容分配策略.md @@ -110,7 +110,7 @@ CCM 64KB 只能容纳 {FreeRTOS 堆 48KB} 与 {LVGL 集合 ~52.5KB} 之一(两 触发条件:测试构建 SRAM 余量不足,**且**已耗尽 `RAM使用分析与优化方案.md` 的帧副本去重(−6.4~9.6KB)与 LVGL 池缩减等低代价手段。 流程:改 scatter → 走 §7 完整验证阶梯 → 同步更新本文档、`AGENTS.md`、`.sct` 头注释。在此之前,方案 A 是唯一有效布局。 -## 6. 当前容量台账(2026-10-29,产品构建 task/wave-record 分支终值;2026-09-21 最坏构建=存储套件的历史基线见 `docs/内存管理/RAM预算台账.md` 趋势) +## 6. 容量台账(2026-10-29 冻结值;**本次改动后的当前值见 §6.1**,两处链接时刻不同、不互相加减;原为产品构建 task/wave-record 分支终值;2026-09-21 最坏构建=存储套件的历史基线见 `docs/内存管理/RAM预算台账.md` 趋势) | 区域 | 总量 | 已用 | 空闲 | 驻留内容 | |---|---|---|---|---| @@ -139,6 +139,28 @@ LVGL 集合明细(均在 SRAM):LVGL 池 16,384B(`lv_mem_core_builtin.o` - **为什么比上一版低**:本表 2026-10-12 版记 95,816 B / 73.1 %,高于今天的 89,936 B。原因是此后 LVGL 内部池由 40 KB 缩到 16 KB(`docs/内存管理/RAM预算台账.md` §3「缩池 ✅ 2026-10-15」行;`scripts/ram_baseline.tsv` 2026-10-12 与 2026-10-24 两行的 `top_rwzi` 列可见 `lv_mem_core_builtin.o` 40960 → 16384)——**不是本波回收了什么**。 - **子系统归集的坑(本卡实测 → 2026-10-29 已修)**:`scripts/ram_baseline.py` 的 `subsystem_of()` 按对象名前缀首匹配,当时表里没有 `wave_` 前缀 → 录波环 `wave_ring.o` 5,300 B 落进 **「其它」(当时 15,853 B,参考值、无配额)**,配额 8 KB 的「应用」行看不见它;落盘道 `default_task.o`(+1,204 B)与 `adc_task.o` 才计入「应用」(当时现值 16,888 B,本波前该行已在配额外,成因见 `docs/内存管理/RAM预算台账.md` §2 的应用条目)。**同日后续**:补齐 12 个 `App/` 对象(9,445 B)的前缀,并加兜底——`App/**/*.c` 编出的对象只要落进「其它」,`python scripts/ram_baseline.py --gate` 就逐条 WARN 点名(软提醒不拦)。结论仍然有效:**子系统配额行不是录波预算的判据**,要按本节 per-file 台账读;差别只在「应用」行现在真含录波支路,两个口径不再互相看不见。 +### 6.1 变比维 + 录波 v2 布局(2026-11-11 权威构建) + +**这一节记录本次改动后 RAM 的当前实测值与它的构建身份**:上面 §6 主表是 2026-10-29 +那次构建的冻结值(历史行不改);本节是 2026-11-11 新一次 `/keil-build`(0 错误 0 警告) +后的独立测量,两处各有自己的链接时刻,不互相加减。结构增量的推导(通道表 12 → 16 B/路、 +每份段头副本 +36 B、共 5 份)在 `docs/子系统设计/故障录波设计说明.md` §7,本节只留实测数。 + +- **测量身份**:本工作树,2026-11-11 10:52 链接, + `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`;`scripts/ram_baseline.tsv` 当日行 + Code/RO/RW/ZI = 256,024 / 31,760 / 2,080 / 152,368。 +- **区域水位**:RW_IRAM1 **97,024 B / 131,072 B(74.0%)**、RW_CCM **57,376 B / 65,280 B + (87.9%,零增量)**、ER_IROM1 287,784 B(27.4%)、Flash 合计 289,864 B(27.6%)。 + 受管快照区(`docs/内存管理/RAM使用分析与优化方案.md`)与子系统配额表 + (`docs/内存管理/RAM预算台账.md`)已由 `check_mem_numbers.py --write` / + `ram_baseline.py --write` 从本份 .map 重渲染,`--gate` / `--scan` 均返 0。 +- **逐对象 RW+ZI(供下次对账)**:`wave_ring.o` 5,332 B(ZI 5,292 + RW 40)、 + `default_task.o` 4,012 B(ZI 3,992 + RW 20)、`ui_task.o` 1,681 B(ZI 1,676 + RW 5)、 + `adc_task.o` 200 B(ZI 168 + RW 32)、`adc_cal.o` 116 B(ZI 100 + RW 16,含新增 + `[adc_cal_sec]` RAM 副本 `s_rcfg` 32 B)。 +- **栈与 CCM**:本次只加静态结构,未抬栈、未加大局部缓冲;变比段副本与全部段头副本 + 都落 RW_IRAM1(无 section 属性),设计文档 §7「不进 CCM」的落位继续成立。 + ## 7. 布局变更验证阶梯(强制执行) 任何 scatter / CCM / 堆大小变更后,依次通过以下全部关卡方可认定生效: diff --git a/docs/子系统设计/IEC104上行设计说明.md b/docs/子系统设计/IEC104上行设计说明.md index e5d31198..90d37a0c 100644 --- a/docs/子系统设计/IEC104上行设计说明.md +++ b/docs/子系统设计/IEC104上行设计说明.md @@ -135,7 +135,7 @@ `run_par_104_write` 执行段入口,加一处即可。 - 持久化(运行参数段):任一写成功置 `run_par` 脏 → iec104Task 同款 peek 投递 `SYS_EVT_RUN_PAR_SAVE`(1<<8) → defaultTask `run_par_store_save()` 调 `cfg_file_save()` - 整档外科保存(一趟覆盖 [run_par] 9 键 + 被代理改写的 [adc_cal]/[adc_cal_ofs], + 整档外科保存(一趟覆盖 [run_par] 9 键 + 被代理改写的 [adc_cal]/[adc_cal_ofs]/[adc_cal_sec], lead 裁②一条事件位一趟够用);失败脏保留 = 下轮重试。消费侧(COV 判定)**实时读 引擎副本**,无缓存无投递(104 改读快照时的 pts 层 s_yc_db 副本已收编进 run_par)。 diff --git a/docs/子系统设计/故障录波设计说明.md b/docs/子系统设计/故障录波设计说明.md index 90865357..5c7376af 100644 --- a/docs/子系统设计/故障录波设计说明.md +++ b/docs/子系统设计/故障录波设计说明.md @@ -185,8 +185,8 @@ flowchart TD | 偏移 | 字段 | 宽度 | 说明 | |---|---|---|---| -| 0 | magic | 3 | 前两字节固定 WR,第三字节是版本号(本波写 1);版本号并进 magic,换格式即变(§2.1 第 4 条) | -| 3 | ver | 1 | 恒 1 | +| 0 | magic | 3 | 前两字节固定 WR,第三字节是版本号(本波写 2);版本号并进 magic,换格式即变(§2.1 第 4 条) | +| 3 | ver | 1 | 恒 2(v1 旧段一律判废,见 §6.3「v1 旧段兼容」行) | | 4 | fs_hz | 2 | 采样率,本波恒 1000 | | 6 | pts_per_cycle | 1 | 每周波点数,本波 20 | | 7 | ch_count | 1 | 8(带 TEMP 则 9) | @@ -200,10 +200,10 @@ flowchart TD | 23 | flags_seq | 1 | 每路 VALID 压成的位图:bit s = 语义通道 s 的 `ADC_CAL_F_VALID`(电气 8 路恰好占满 1 字节;TEMP 不参与标定,若要带 TEMP 连本行与下一行一起升版本) | | 24 | clip_mask | 2 | 削顶归属位图:bit s = 语义通道 s,与 `App/algo/meas.h` 同名参数同口径(封存瞬间 `ADC_CAL_F_CLIPPED` 快照,V0.1 恒 0xE6 = UB/UC/IB/IC/IN,见 `docs/算法/采集与算法层设计说明.md §13.5`)。⚠ **这个位图的本体是「链路可信性标记(按来源派生)」,不是「本段实测饱和标记」**:置位条件一次样本都不看(`App/algo/adc_cal.c` 291 到 296 行,该路 ROM 系数属片内 ADC 侧即置位);「这一段有没有真钉轨」走另一条链——`App/algo/adc_health.c` 的 `adc_health_rail_watch()`(280 行起)与 `adc_health_rail_sticky()`,门限按满量程百分比由 `adc_cal_rail_ths()` 给、须连续 `ADC_RAIL_N_RUN` 拍。V0.2(8 路全接 TPAFE)后本位自动恒 0,届时「与 clip_mask 对表」这条判据退化为只查钉轨实测(§10) | | 26 | frame_seq | 4 | 段首帧序号,可与串口周期行的 frames 对账 | -| 30 | 通道表 | 每路 12 | 语义号 1 + 来源转换器 1 + 位宽 1 + 可用性 1(= `adc_cal_flags(sem)` 低 8 位原值,不自造位义)+ `k_q16` 4(int32,µV/码 × 65536)+ `b_codes` 4(int32,码值)。**没有独立 LSB 字段**,LSB 就是 `k_q16`,见 §6.3 | -| — | CRC16(头) | 2 | CRC-16/MODBUS,**低字节在前**(与全文件小端统一);覆盖 `[0, 30+12×ch_count)`,本字段自身紧随其后 | +| 30 | 通道表 | 每路 16 | 语义号 1 + 来源转换器 1 + 位宽 1 + 可用性 1(= `adc_cal_flags(sem)` 低 8 位原值,不自造位义)+ `k_q16` 4(int32,µV/码 × 65536)+ `b_codes` 4(int32,码值)+ `r_q16` 4(int32,变比 × 65536,0 = 本路不折算)。**没有独立 LSB 字段**,LSB 就是 `k_q16`;**变比也不并进 k_q16**(并入会超 int32,见 §6.3),见 §6.3 | +| — | CRC16(头) | 2 | CRC-16/MODBUS,**低字节在前**(与全文件小端统一);覆盖 `[0, 30+16×ch_count)`,本字段自身紧随其后 | | — | 数据区 | pts_per_ch × ch_count × 2 | **按通道分列**:一路连续 320 点,单通道回放只读那一路 | -| — | CRC16(数据区) | 2 | 同上字节序;覆盖整个数据区、自身在文件尾(8 路 320 点整段 5250 B);长度为 0、任一 CRC 不符、或 `ver` 与魔数第三字节不等 → 整段作废,页面必须显示「该段不可读」 | +| — | CRC16(数据区) | 2 | 同上字节序;覆盖整个数据区、自身在文件尾(8 路 320 点整段 5282 B = 160 头 + 5120 数据 + 2;v1 为 5250 B);长度为 0、任一 CRC 不符、或 `ver` 与魔数第三字节不等 → 整段作废,页面必须显示「该段不可读」 | **段名轮转的呈现语义**(回放页与 PC 侧取「哪一段最新」都按这条):段表按**数值升序**呈现,开屏落点取**数值最大的段号**。回绕(99 之后回到 00)之后「数值最大」不再等于「时间最新」——要真时间序必须逐段读段头 `frame_seq` / `t0_ms`,代价是 100 次开文件,**本波不做**。所以页顶把段号本身显示出来(§8.1 顶部行),让读者自己看见这是第几号,不去伪造一个「最新」的假象。 @@ -214,13 +214,16 @@ flowchart TD | 问题 | 裁定 | |---|---| | CRC 字节序 | **低字节在前**,与全文件小端一致。原稿「与屏板链路同一张 CRC-16/MODBUS 表」不成立:全仓**没有**可复用的 CRC-16 表——`App/util/README.md` 记 `crc.c/h` 已于 2026-10-02 删除,屏板链路 `Drivers/BSP/UI/ui_net_sink.c` 的 `remo_crc()` 是单字节异或、不是 CRC-16。本波在 `wave_store.c` 内自带**无表位运算** CRC-16/MODBUS(0x8005 反射、初值 0xFFFF、异或输出 0),单测锚点:串 123456789 的 CRC = 0x4B37 | -| CRC 覆盖范围 | 头 CRC 覆盖 `[0, 30+12×ch_count)`、自身在 `30+12×ch_count`;数据区自 `32+12×ch_count` 起;数据 CRC 覆盖整个数据区、自身在文件尾。8 路 320 点整段 5250 B | +| CRC 覆盖范围 | 头 CRC 覆盖 `[0, 30+16×ch_count)`、自身在 `30+16×ch_count`;数据区自 `32+16×ch_count` 起;数据 CRC 覆盖整个数据区、自身在文件尾。8 路 320 点:头 158 字节 CRC 覆盖区 + 2 = 160 整头,数据区 5120,整段 5282 B(v1 口径为 12 字节表项 / 126 / 128 / 5250,v2 一并升位) | | LSB 从哪来 | **不设独立 LSB 字段**:通道表的 `k_q16` 就是 LSB(µV/码 × 65536),与 `App/algo/adc_cal.h` 的 `ADC_CAL_K_Q16_TPAFE 10000000` / `ADC_CAL_K_Q16_STM32 52800000` 同口径,Q16 下无舍入(选型依据 `docs/算法/采集与算法层设计说明.md §13.2`)。PC 侧 `µV = k_q16 / 65536`,不许再乘一遍 | -| k / b 宽度与角色 | `k_q16` int32、`b_codes` int32 = 封存瞬间该路生效的两个标定量。`adc_cal_apply()` 的算式是 `eng = (code + b) × k × 1e-6`,文件里存的正是等号右边两个数加原始 code,PC 照此重算。**必须取 int32 原值**,不得经 `adc_cal_k_uv()` 的 float 通道回算(float 在 5.28e7 量级掉尾数)→ 接线道在 `App/algo/adc_cal.[ch]` 补一个 int32 直读入口 `adc_cal_k_q16()`(与 `adc_cal_k_uv()` 孪生);eng=0 时两者是 ROM 默认值,PC 折出来的单位是**引脚域 µV** 而非二次值——单位一律以头里的 `eng` 为准,PC 不许按当前固件口径猜 | +| k / b / r 宽度与角色 | `k_q16` int32、`b_codes` int32、`r_q16` int32 = 封存瞬间该路生效的三个标定量。`adc_cal_apply()` 的算式是 `eng = (code + b) × k × 1e-6 × r`,文件里存的正是等号右边三个数加原始 code,PC 照此重算。**k 与 r 必须取 int32 原值**,不得经 `adc_cal_k_uv()` / `adc_cal_ratio()` 的 float 通道回算(float 在 5.28e7 / 6.5e7 量级掉尾数)→ 接线道用 `App/algo/adc_cal.[ch]` 的两个 int32 直读入口 `adc_cal_k_q16()` / `adc_cal_ratio_q16()`;`b_codes` 经 float 回乘在本域无损(|b| 限幅 8192 远小于 2^24,float32 精确表示该范围整数)。`r_q16 = 0` 表示**本路不折算**(停在引脚域),不是「乘 0」也不是缺省错误:这是合法值,与「刻意不折算」在文件里长得一样,属设计取舍。eng=0 时两者是 ROM 默认值,PC 折出来的单位是**引脚域 mV** 而非二次值——单位一律以头里的 `eng` 为准,PC 不许按当前固件口径猜 | +| 折算算式与防溢出 | PC 与板端同一条定点算式:`t = (code + b_codes) × k_q16`;`r_q16 = 0` → `m = 圆整(t / 65536000)`(引脚域 mV,与 v1 逐位一致);`r_q16 ≠ 0` → `m = 圆整(t × r_q16 / (65536000 × 65536))`(录波域毫单位)。**只允许在最后圆整一次**:先圆整成整数 mV 再乘变比,会让 `r_q16 = 65536`(比值 1.0)的输出与引脚域差 ±1 LSB,而「r = 1.0 等同引脚域」正是两份实现对齐的锚点。**一次乘完会溢出 int64**(t ≈ 4.3e12、r_q16 ≤ 2.147e9,乘积 9e21 > 9.2e18)→ 板端用余数分解换等价单次圆整(`App/algo/wave_store.c` 的 `wave_store_code_to_milli`,注释里写明分解式与各项上界);PC 侧 Python 用大整数直接算同一个值。输出是**录波域毫单位**:eng=1 时二次 mV/mA、eng=0 时引脚 mV,两端口径一致 | +| 变比为什么不并进 `k_q16` | `k_q16` 是**板级前端斜率**(µV/码 × 65536,TPAFE `10000000` / 片内 ADC `52800000`),`r_q16` 是**端子变比**(PT/CT 变比 × 65536)。若把 r 折进 k,电压族 = `10000000 × 65536000 / 65536 = 1.0e10`,已超 int32 上限 2.147e9 → Q16 定标下无法在同一个 int32 里同时表达两个量纲,故必须独立成字段 | | `flags_seq` 位义 | bit s = 语义通道 s 的 `ADC_CAL_F_VALID`;本波 `ch_count` 固定 8 | | 可用性字段真值表 | 两行,实现期按 `App/algo/adc_cal.c` 280 到 298 行核出(VALID 需文件位图与 k 合法带**同时**成立;CLIPPED 纯由来源派生——该路 ROM 系数属片内 ADC 侧即置位,与样本内容无关):① **未标定**(产品构建,两点标定未做)逐路 availability = UA / UN / IA 取 `0x00`,UB / UC / IB / IC / IN 取 `0x02`,`flags_seq` = `0x00`,`clip_mask` = 0xE6;② **已标定**(电气 8 路系数全部写入且落合法带)逐路 = `0x01`(TPAFE 三路)/ `0x03`(片内 ADC 五路,VALID 与 CLIPPED 同时为真),`flags_seq` = `0xFF`,`clip_mask` 仍是 0xE6。**VALID 与 CLIPPED 是两件独立事实**:写侧如实透传 `adc_cal_flags()` 逐路原值(`App/task/adc_task.c` 548 行),读侧不得据任何品质组合判废,PC 与回放页只给软告警(见下面「判废面与软告警面」一行) | -| eng 派生式 | `eng` 不是独立事实,是逐路有效位图的派生量:**电气 8 路全 VALID → 二次值(1),缺任意一路 → 引脚域(0)**。真机两处必须同口径,行号一并登记:判据本体 `App/app/ui_data_feed.c` 226 到 235 行的 `feed_eng()`(屏显侧),段头字段填写 `App/task/adc_task.c` 532 到 553 行(其中 552 到 553 行是段头侧的同一派生)。两处分布在**不同任务里现算**,理论上会撞上配置重载的毫秒级沉降窗而不一致——这是允许态,见下一行 | -| 判废面与软告警面 | **完整性判废面自始至终只有这些**:精确尺寸等式、`magic` 与 `ver`(含 `ver` 须等于魔数第三字节)、头 CRC 与数据 CRC、表序 `sem` 等于下标、`ch_count` 上限、`pts_per_cycle` / `pts_per_ch` / `pre_pts` 下限、长度为零、文件短读。**品质组合与单位标签永远不进这道门**。本轮新增的只是**软告警面**:`avail` 同时带 VALID 与 CLIPPED 时 PC 在 NOTE 列打「同时为真,两点标定完成后属正常,非故障」;`eng` 与 `flags_seq` 违反上一条派生式时打 WARN。两者都**不改退出码、不影响出图、不拒渲染**。理由:把「单位标签不一致」当数据损坏,等于让一次改标定文件的沉降窗销毁故障证据 | +| eng 派生式 | `eng` 不是独立事实,是**逐路变比配置状态**的派生量:**每一路电气量「要么已配变比、要么削顶(页面本来就显 `-`,不参与整帧量纲断言)」,八路全满足 → 二次值(1),缺任意一路 → 引脚域(0)**。判据本体 `App/app/ui_data_feed.c` 的 `feed_eng()`(屏显侧),段头字段填写在 `App/task/adc_task.c` 的封存交棒块里现算。**v2 起判据换成变比配置,不再看 `flags_seq`**:`ADC_CAL_F_VALID` 回答「准不准」、`r_q16` 回答「什么单位」,两件事;V0.1 五路片内 ADC 削顶永远拿不到 VALID,用 VALID 当量纲判据会让档位永不解除。两处分布在**不同任务里现算**,理论上会撞上配置重载的毫秒级沉降窗而不一致——这是允许态,见下一行 | +| 判废面与软告警面 | **完整性判废面自始至终只有这些**:精确尺寸等式、`magic` 与 `ver`(含 `ver` 须等于魔数第三字节)、头 CRC 与数据 CRC、表序 `sem` 等于下标、`ch_count` 上限、`pts_per_cycle` / `pts_per_ch` / `pre_pts` 下限、长度为零、文件短读。**品质组合与单位标签永远不进这道门**。软告警面:`avail` 同时带 VALID 与 CLIPPED 时 PC 在 NOTE 列打「同时为真,两点标定完成后属正常,非故障」;`eng` 与逐路变比配置违反上一条派生式时打 WARN。两者都**不改退出码、不影响出图、不拒渲染**。理由:把「单位标签不一致」当数据损坏,等于让一次改标定文件的沉降窗销毁故障证据 | +| v1 旧段兼容 | **不做**:`ver` 与 magic 第三字节必须都等于 2,v1 段(`ver` = 1)一律按「该段不可读」处理。理由:v1 没有 `r_q16`,同一份 code 在 v1 里只能解释成引脚域;让读侧按版本分支去猜「这一路到底折没折算」会把「格式已知」降级成「推断已知」,而旧段的有效寿命只到下一次清 `/wave`。升版时全部旧段作废是可接受的代价(`/wave` 是 100 槽轮转,本波不清库) | | 通道表序与列序 | 第 i 个通道表项 ↔ 数据区第 i 列,按语义号**升序**:UA=0 UB=1 UC=2 UN=3 IA=4 IB=5 IC=6 IN=7(`App/algo/adc_input.h` 的 `adc_semantic_t`)。PC 必须校验表内语义号等于下标,不等即整段作废。交叉验证:`clip_mask` 恒 0xE6 = 2+4+32+64+128,恰好是 UB/UC/IB/IC/IN 五个语义号位,这份算术就是这个顺序的证据 | | 样本编码 | int16 小端;TPAFE 侧 16 位补码双极性;STM32 侧 0..4095 原样存正数,**不加中点偏置**——V0.1 单极性无偏置是硬件现状,加偏置等于伪造双极性(`docs/算法/采集与算法层设计说明.md §13.5`) | | 三个时间字段 | `t0_ms`(上电毫秒)、`wall`(epoch 秒,未对时 0)、`frame_seq`(段首帧序号)均 u32 无符号小端 | @@ -264,6 +267,7 @@ flowchart TD | 任务栈 | 未估 | 九任务全 PASS、**未抬栈**:adcTask 静态最坏链 1,424 B(余 1,136 B / 44 %)、defaultTask 3,040 B(余 2,080 B / 40 %)、uiTask 1,336 B(余 15,048 B / 91 %) | - **差额构成(比预估多 860 B,可对账)**:原估漏记 `FIL` 528 B(`FF_FS_TINY=0`,落盘道刻意不上栈)、`DIR`/`FILINFO`、已封存段头副本 136 B、打包上下文(内部又含一份 136 B 段头);反向少记一项——落盘实际是**单缓冲 256 B**,不是预估的 512 B 双缓冲。**预估与实测都留在本节**:只写实测,下一次估算就看不见原估漏在哪。 +- ⚠ **上行是 v1 布局(通道表 12 B/路、`sizeof(wave_seg_meta_t)` = 136)的实测数,冻结不改**——它记的是 2026-10-29 那次权威构建,改写数字等于伪造一次没做过的测量。v2(通道表 16 B/路、`sizeof(wave_seg_meta_t)` = 172,`wave_ch_desc_t` 12→16 × `WAVE_ST_CH_MAX` = 9 即 +36 B **每份段头副本**)落地后本节各项都会变,届时按**新一次构建**的 `.map` 重测重填,而不是在这几个数上加减。段头副本的份数:`App/algo/wave_ring.c` 的 `s_sealed`、`App/task/default_task.c` 的 `s_wmeta` + 打包上下文内含的一份、`App/task/ui_task.c` 的 `s_wave_meta`(UI 道)、`App/task/adc_task.c` 的 `s_wave_cal`(`wave_seal_cal_t`,116→152,**同样 +36**)。**这次重测已完成**:v2 落地的 2026-11-11 权威构建实测值(RW_IRAM1 97,024 B / 74.0%、逐对象 RW+ZI)登记在 `docs/内存管理/内存内容分配策略.md` §6.1,本节冻结的 v1 数字保持原样,两处不互相加减。 - **回放页读窗不在数据面那 6,760 B 内**:它计入 UI 道的 +1,640 B;两张表不能相加当总量,逐对象拆分见 `docs/内存管理/内存内容分配策略.md` §6。 ## 8. 现场回放显示契约 @@ -646,7 +650,7 @@ Before/After 表与串口原文。 | 削顶五路被读成「系统有零序 / 有谐波」 | 现场误判 | 头里 `clip_mask` + 页面「削」徽标 + 空态不许画直线(§8.1) | | 有人把 U 与 I 两段叠图比相位 | 得出**没有物理意义**的相位差 | 回放不做多路叠加;头里写明来源转换器;与 `docs/硬件平台/ADC通道映射说明.md` §7 通道归属行的 V0.1 禁止行同源 | | 落盘把 defaultTask 拖住 | 日志与配置引擎变慢(defaultTask 是两者唯一写者) | 分次写(256 B 双缓冲)、一次心跳最多一段、`f_write` 前后有耗时观测;不做整段驻留再写 | -| **回放读路径把 uiTask 拖住**(2026-10-29 lead 评审补,此前风险表只有写侧没有读侧) | 一次渲染的实码 I/O(`App/task/ui_task.c` 376 到 398 行):段表刷新 1 次 `f_opendir` + 至多 100 次 `f_readdir`;未缓存段的段头装载 = 1 次 `f_open` + 1 次 128 B 头读 + **20 次 256 B 数据区读并边读边算数据 CRC** + 尾 2 B 一次 = **最坏 22 次小读**;已验过则只剩窗装载 = 1 次 `f_open` + 1 次 `f_lseek` + 1 次 80 B 窗读 + 40 点解码与峰值扫描(**一屏只碰一路通道一个窗,不是 8 路 320 点**)。uiTask 在 IWDG `required_mask` 内、窗 800 ms(口径同 `docs/子系统设计/看门狗设计说明.md` §11.3 的解耦心跳)→ **现象先是 LVGL 停摆与远程推流缺席,不是复位**(心跳与画屏解耦) | 卷锁 try 非阻塞(拿不到就显示「存储忙」,不等)+ 256 B 分次读 + 已验段结果缓存(同段同道不重复验)+ 只在首次进页 / 切段 / 切道时读。**单轮耗时上界未实测**:不新加埋点,用现成的 `task_prof` 读数(板级动作见 §10) | +| **回放读路径把 uiTask 拖住**(2026-10-29 lead 评审补,此前风险表只有写侧没有读侧) | 一次渲染的实码 I/O(`App/task/ui_task.c` 376 到 398 行):段表刷新 1 次 `f_opendir` + 至多 100 次 `f_readdir`;未缓存段的段头装载 = 1 次 `f_open` + 1 次 160 B 头读 + **20 次 256 B 数据区读并边读边算数据 CRC** + 尾 2 B 一次 = **最坏 22 次小读**;已验过则只剩窗装载 = 1 次 `f_open` + 1 次 `f_lseek` + 1 次 80 B 窗读 + 40 点解码与峰值扫描(**一屏只碰一路通道一个窗,不是 8 路 320 点**)。uiTask 在 IWDG `required_mask` 内、窗 800 ms(口径同 `docs/子系统设计/看门狗设计说明.md` §11.3 的解耦心跳)→ **现象先是 LVGL 停摆与远程推流缺席,不是复位**(心跳与画屏解耦) | 卷锁 try 非阻塞(拿不到就显示「存储忙」,不等)+ 256 B 分次读 + 已验段结果缓存(同段同道不重复验)+ 只在首次进页 / 切段 / 切道时读。**单轮耗时上界未实测**:不新加埋点,用现成的 `task_prof` 读数(板级动作见 §10) | | 触发风暴 + 轮转覆盖 | 现场唯一一段有效波形被后到的抖动冲掉 | 冷却期 + `ovf` 计数;「最新段号」进快照;分段优先级属二期 | | 定时自抓忘关 | 白烧 NAND 写次数(持久代价) | 默认关 + 上限 + 告警(§10 红线) | | 标定落地后老文件被解释错 | 同一个系数字段值变了含义漂移 | 头里存**封存瞬间**的 `eng` 与系数快照;PC 脚本按头解释,绝不按当前固件口径解释 | diff --git a/docs/子系统设计/网络配置说明.md b/docs/子系统设计/网络配置说明.md index d21cef58..672e658a 100644 --- a/docs/子系统设计/网络配置说明.md +++ b/docs/子系统设计/网络配置说明.md @@ -163,9 +163,10 @@ write=1 | `db_ia` `db_ib` `db_ic` `db_in` | 电流死区 | 5 | 0 ~ 1000(1 A) | | `db_freq` | 频率死区 | 500 | 0 ~ 5000(5 Hz) | -- 键数 9 ≤ 16,**通道系数覆盖不落本段**——系数唯一真源 = `[adc_cal]` / `[adc_cal_ofs]` - 两段(标定落盘与整定入口布局,docs/算法/采集与算法层设计说明.md §13.8;104 `0x8040~0x804F` 写 = - `adc_cal_set` 代理,lead 裁②a)。 +- 键数 9 ≤ 16,**通道系数覆盖不落本段**——系数与变比的唯一真源 = + `[adc_cal]` / `[adc_cal_ofs]` / `[adc_cal_sec]` 三段(标定落盘与整定入口布局, + docs/算法/采集与算法层设计说明.md §13.8;104 `0x8040~0x804F` 写 k/b、 + `0x8050~0x8057` 写 r,均经 `adc_cal_set` / `adc_cal_set_ratio` 代理,lead 裁②a)。 - 守卫在**写路径**(104/壳 API 拒 NaN/负/越程);**文件通路引擎照收**(同 adc_cal 的 「人工编辑 + 沉降去抖」契约)——死区超程只让上送变钝、不毁量纲,无合法带哨兵。 - 生效链路:load/poll 原地改副本 → 104 COV 每拍实时读(无回调,104 改读快照与运行参数段契约); @@ -194,9 +195,13 @@ write=1 配错即锁死真主站,待送检/现场需求立项;读者勿假设该键已存在。 - 落盘:UI 保存(task-9)经 `net_cfg_save_request()` → `SYS_EVT_CFG_SAVE` 整档 外科 save;引擎属主 = defaultTask 不变。 -- 容量台账:本段上线后 `/config.ini` 受管段 = 6/8([network] 4 键 / [ftp] 3 键 / - [adc_cal] 9 键 / [adc_cal_ofs] 8 键 / [run_par] 9 键 / [comm] 2 键);段/键上限 - 的机器钉 = `test/host/cfg_file` TC-CONFIG-056/057。 +- 容量台账:本段上线后 `/config.ini` 受管段 = **8/8——已满**([network] 4 键 / [ftp] 3 键 / + [adc_cal] 9 键 / [adc_cal_ofs] 8 键 / [adc_cal_sec] 8 键 / [run_par] 9 键 / [comm] 2 键 / + [sntp] 4 键)。⚠ **下一个受管段无处可放**:第九次 `cfg_file_register()` 会被直接拒, + 只留一行 `[ERR]`,且**注册顺序决定谁被拒**(内核前 adc_cal 三段 + run_par + comm 先占 5 位, + defaultTask 的 net_cfg_load 再占 network / ftp / sntp 3 位)——再加段前必须先抬 + `CFG_FILE_MAX_SECTIONS`(8)或合并既有段。 + 段/键上限的机器钉 = `test/host/cfg_file` TC-CONFIG-056/057。 ### 3.7 `[sntp]` 段:SNTP 校时参数(2026-11-04 新增,板端 SNTP 客户端接入,#57 调研结论) @@ -227,7 +232,7 @@ layer-deps 口径),段描述符由 `net_cfg.c` 的 `s_sntp_register()` 组 不拨针(防 RTC 无谓磨损)。冷启动(钟面无效)直接采用服务器 T3。 防欺骗/退避/KoD 细则 → `App/sync/time_sntp.c` 文件头 + `test/host/sntp`(TC 命名即判据)。 -- 容量台账:本段上线后 `/config.ini` 受管段 = **7/8**(前 6 段见 §3.6,本段 4 键)。 +- 容量台账:本段上线后 `/config.ini` 受管段 = **8/8 满额**(前 7 段见 §3.6,本段 4 键)。 --- diff --git a/docs/子系统设计/配置文件设计说明.md b/docs/子系统设计/配置文件设计说明.md index b85eca17..40bf65e8 100644 --- a/docs/子系统设计/配置文件设计说明.md +++ b/docs/子系统设计/配置文件设计说明.md @@ -594,3 +594,4 @@ void adc_cfg_init(void) | 2026-10-26 | v1.15:**新增 `[comm]` 段(道G task-8,「UI 补齐」波次里的非 UI 前置项)**——`App/protocol/iec104/iec104_comm.h/.c` 配对模块(104 公共地址 / 监听端口 2 键,getter 合法带自愈,adc_cal 先例);内核前 `iec104_comm_init()` 注册([adc_cal] / [run_par] 同「先注册后 load」契约);生效 = **改后重启**(不挂回调、不新增事件位,区别于 [network] 免重启重建);段占位 5→6 / 8,键表与主站地址不做理由 → `docs/子系统设计/网络配置说明.md` §3.6;机器钉 = `test/host/cfg_file` TC-CONFIG-056/057(段/键上限)+ `test/host/iec104` TC-IEC104-031~033(模板/重载/带外/ca 端到端) | 2026-11-04 | v1.16:**新增 `[sntp]` 段(板端 SNTP 客户端接入,#57 调研结论落地)**——`Drivers/BSP/NETCFG/sntp_cfg.h/.c`(enable / server1 / server2 / poll_min 4 键;出厂默认**关闭 + 服务器留空 + 60 分钟** = "未配置零网络足迹"口径;`poll_min` 带外自愈 1..1440 → 60 不写回 RAM);数据本体 = `Drivers/BSP/NETCFG/sntp_cfg.h/.c`(不依赖引擎头,分层门禁 layer-deps 口径),段描述符由 `net_cfg.c` 的 `s_sntp_register()` 组装,注册编排 = `net_cfg_load()`(与 [ftp] 同编排,先注册后 load);消费方 = `App/sync/time_sntp.c`(挂 timeSyncTask 既有循环,vendor `Lib/coreSNTP` serializer 做 48B 报文编解码/防欺骗校验);保存立即生效 = 每轮直读 RAM 副本 + 网络代次重建弃 UDP socket 重散布,**无段回调、不新增事件位**;段占位 6→7 / 8;键表与生效细则 → `docs/子系统设计/网络配置说明.md` §3.7;机器钉 = `test/host/net_cfg` TC-NETCFG-005/006b..d/007(段注册契约)+ `test/host/sntp`(RFC 4330 §10 节奏 / §5.3 防欺骗 / §5.5 步进策略 / poll 钳位) | 2026-11-05 | v1.17:**热重载/保存路径视图一致性守卫(issue #73 台架实证根治)**——真机 8/8 复现"运行中改配置触发热重载打回出厂默认"且此后任何保存吞段(1066B→986B):卷层在 stat→read 窗口给回旧/短视图,引擎照单应用(全段 memcpy 默认、零解析错误行)或照短视图外科重写文件。① `cfg_file_poll` 沉降读取后三点校验 + 内容判据(读长 == `st.size` + 读后复 stat (size, mtime) 不变 + `s_view_bad`:INI 文本任意偏移含 NUL = 坏块——台架 15:21 再实证**同尺寸整块全 0 旧映射块**可骗过纯三点校验,解析零错误 RAM 照打回,内容判据是该形态唯一引擎侧拦截):不符 → **拒绝应用**(RAM 原样 / 基线不刷新 / 2s 重试自愈),首次拒绝 `cfg poll view stale` 自检 + ERROR 一行(该行打印"读长B != stat B",内容判据或复 stat 命中时两个数可相等,命中的是内容/读中改写项),此后每 30 趟(≈60s)一条 WARN 计数;② `cfg_file_save` 同族守卫:视图与 stat 不符或内容坏 → **拒存返回 -1**(调用方保留 dirty 重试)原文件一字节不动 + `cfg save view stale` 自检——"短/坏视图 → 外科重写 → 非默认段被删"链就此斩断;③ 边界如实披露:一致但陈旧的整卷视图引擎层无法检出(dhara/FTL 属卷层),重启必然收敛,卷层根治前"改过配置后重启一次"仍是最稳口径。机器钉 = TC-CONFIG-058(poll HELD + 自愈)/ 059(save 拒存文件不动)/ 060(读中改写检出 + 日志限速)/ 061(同尺寸全 0 块内容判据拦截 + 自愈)/ 062(save 遇全 0 块拒存不动盘);`cfg_file.c` v1.6 / `cfg_file.h` v1.5 契约同步 +| 2026-11-11 | v1.18:**新增 `[adc_cal_sec]` 段(端子变比 r,8 键 int32 Q16)**——标定折算由二维 `(code+b)×k` 升为三维 `(code+b)×k×r`(k = 引脚侧 LSB、b = 零点、r = 端子二次值↔引脚电压);**段数 7→8 = 达 `CFG_FILE_MAX_SECTIONS` 上限**,此后第九段必被 `cfg_file_register()` 拒(只留一行 `[ERR]`,且先注册者留位),再加受管段须先抬上限或合并段;注册在 `adc_cal_init()`(内核前,与另两段同批),`p_cb = NULL`、段 ROM 默认**非 0**(电压族 65536000 = 1000.0、电流族 1927532 = 29.4118,台架实测反算);键表 / 合法带 / 与 VALID 的关系 → `docs/算法/采集与算法层设计说明.md` §13.2 / §13.8,台架来源 → `docs/硬件平台/ADC通道映射说明.md` §7.2 | diff --git a/docs/硬件平台/ADC通道映射说明.md b/docs/硬件平台/ADC通道映射说明.md index d29f95d8..82cb1c03 100644 --- a/docs/硬件平台/ADC通道映射说明.md +++ b/docs/硬件平台/ADC通道映射说明.md @@ -156,61 +156,64 @@ STM32(U17)为 LQFP144 脚号。 3. **满量程必须写明参考点**:TPAFE 通道满量程 = ±5 V **@ 引脚**(跨度 10 V);③④ 均已定死(④ 值见 §7.2),端子侧换算允许写"引脚侧数值 × ④ 段变比";但两点标定置 `VALID` 之前**严禁**把"码 × 152.59 µV"当端子二次值写进代码/文档(那是引脚电压),§7.2 的标称系数只是算定值、不是已验证二次值。 4. **仍待实测的只有两点标定**(给端子域系数、置 `VALID`,`eng` 随之升 1);完成前 `eng` 保持 0、输出单位仍是 ADC 引脚电压。实测斜率与 ② 段理论值偏差 >2 % 时**先查前端**(分压/变比/焊接),**不得用系数凑错斜率**——那会把硬件的错固化成"标定值"。 -### 7.2 ④ 段变比登记(2026-11-06 定死) +### 7.2 ④ 段变比登记(2026-11-11 台架实测修正) -> **来源**:用户现场报的子板参数(2026-11-06,Issue #41),按 `docs/算法/采集与算法层设计说明.md §13.12` #2 的路 ①「按图(参数)直接算」 -> 算得;子板原理图仍不在仓库内,路 ②「万用表同时量端子与引脚」未做——数值上的理想化偏差与逐路差归两点标定 -> (见本节末「复核口径」)。 +> **这一节要你干什么**:写清"端子二次值 ÷ ADC 引脚电压"这个比值到底是多少、凭什么。 +> 它是固件里 `[adc_cal_sec]` 段出厂默认值的唯一来源(`App/algo/adc_cal.h` 的 `ADC_CAL_R_Q16_*`), +> 也是屏上读数从"引脚电压"变成"二次值"的唯一依据。 +> +> **2026-11-11 修正**:本节原先按用户报的子板参数(200 kΩ / 330 Ω)算定电压族为 **606.06 : 1**。 +> 台架用继电保护仪在**端子上**注入标准量、读屏面示数反算,得到的是 **1000.0 : 1**—— +> 两者差 60 %,远超屏面分辨能力,**实测为准,606.06 作废**(保留在本节末的「历史值与作废依据」里, +> 供后来者判断"是不是又算错了一遍")。子板原理图仍不在仓库内,所以这条比值目前只有实测证据、 +> 没有图纸旁证。 -**电压族 UA/UB/UC/UN——子板 PT:2 mA/2 mA,输入侧 200 kΩ,输出侧 330 Ω**: +**实测数据(2026-11-11,继保仪端子注入,屏面 3 位小数)**: -- 变比 = U端子 ÷ UAINn = (2 mA × 200 kΩ) ÷ (2 mA × 330 Ω):电流比 1:1 电流相消,只剩两侧负担电阻之比 = - 200000 ÷ 330 = **606.060606… ≈ 606.06 : 1**(V/V,无量纲); -- 额定点(按参数推读):端子 **≈400 V**(2 mA × 200 kΩ)→ `AINn` 侧 **0.66 V**(2 mA × 330 Ω)。 +| 通道 | 端子注入(有效值) | 屏面读数 | 反算变比 | 该点码值(RMS) | 1 LSB 占读数 | +|---|---|---|---|---|---| +| UA | 5.000 V | 0.005 V | 5.000 / 0.005 = **1000.0 V/V** | 约 33 码 | 3.05 % | +| IA | 0.500 A | 0.017 V(折算前是引脚电压) | 0.500 / 0.017 = **29.4118 A/V** | 约 112 码 | — | -**电流族 IA/IB/IC/IN——子板 CT:5 A/2.5 mA,输出侧负担电阻 68 Ω**: +- **电流族 29.4118 A/V 与本节原登记值精确一致**(= 匝比 2000 ÷ 68 Ω),两条独立来路 + (参数算定 + 实测反算)互证,可信度最高。 +- **电压族 1000.0 V/V 只有单点粗测**:5 V 注入时引脚侧只有约 5 mV ≈ 33 码,1 LSB = 3 % 读数, + 所以 3 位小数给出的比值区间是 **909 ~ 1111**。这个数是"量级对、精度未定", + **不是**可以写进铭牌的精度指标——送检前必须用标准源两点标定重写(见本节末「精度从哪来」)。 +- 若真值是 606.06,5 V 注入应显 0.008 V(差 60 %),而屏面实际给 0.005——分辨率完全够分辨这两个值, + 所以这不是"测量误差",是原参数算定假设与实物不符(最可能是分压电阻实际值≠报参数, + 或子板型号与报参不是同一款)。 -- ADC 读的是负担电阻两端电压,所以这个"变比"是**带单位的比**:I端子 ÷ UAINn = 5 A ÷ (2.5 mA × 68 Ω) = - 5 ÷ 0.17 = **29.4118 A/V**(= 匝比 2000 ÷ 68);与 V/V 无量纲量不可混写,进系数表须分注单位; -- 额定点:端子 **5 A** → `AINn` 侧 **0.17 V**。 +**码值量化与标定点怎么选**(同一条链上的实用结论):注入点越高,1 LSB 占读数越小—— -**算术复核**:200000 ÷ 330 = 606.060606… ✓(用户给值 606.0606060606061);5 ÷ 0.17 = 29.41176470… ✓ -(用户给值 29.41176470588235,与 2000 ÷ 68 同源)。两数与报参口径一致,未发现算误。 +| 端子注入 | 引脚侧 | 码值(RMS) | 1 LSB 占读数 | +|---|---|---|---| +| 5 V | 5 mV | 33 | 3.05 % | +| 20 V | 20 mV | 131 | 0.76 % | +| 57.7 V(额定相电压) | 57.7 mV | 378 | 0.26 % | +| 100 V | 100 mV | 655 | 0.15 % | +| 120 V | 120 mV | 786 | 0.13 % | -**②×③×④ 端到端标称系数**(③ 已定 1:1)——只作两点标定的初值与量级核对用,**ROM 不动**(ROM 出厂表 -仍按 ② 引脚口径,§7.1 可评审结论 1): +故两点标定**不要用 5 V 这种小注入**:建议取 20 V 与 120 V 两点(都在 TPAFE ±5 V 引脚量程内, +且码值足够大)。这也是本节的 1000.0 只能算"粗测"的原因。 -| 族 | 转换器 | 端子域系数(标称) | 额定点码数(RMS) | 占转换器跨度 | -|---|---|---|---|---| -| 电压 UA/UN | TPAFE | 152.59 µV/码 × 606.06 = **92.48 mV/码** | 0.66 V ≈ 4325 码 | 10 V 跨度的 6.6 % | -| 电压 UB/UC | STM32 | 805.66 µV/码 × 606.06 = **488.28 mV/码** | 0.66 V ≈ 819 码 | 3.3 V 跨度的 20 % | -| 电流 IA | TPAFE | 152.59 µV/码 × 29.4118 A/V = **4.488 mA/码** | 0.17 V ≈ 1114 码 | 10 V 跨度的 1.7 % | -| 电流 IB/IC/IN | STM32 | 805.66 µV/码 × 29.4118 A/V = **23.70 mA/码** | 0.17 V ≈ 211 码 | 3.3 V 跨度的 5.2 % | +**变比进固件的形式(为什么是独立一维,不并进 `k`)**:`r` 存 int32 Q16 比值, +电压族 `ADC_CAL_R_Q16_VOLT = 65 536 000`(= 1000.0 × 65536,无舍入)、 +电流族 `ADC_CAL_R_Q16_CURR = 1 927 532`(= 29.4118 × 65536)。 +**不能**把它乘进 `k`:UA 端子域系数 = 152.59 µV/码 × 1000 = 152 587.9 µV/码, +Q16 后是 1.0e10,而 int32 上限只有 2.147e9;UB/UC 走 STM32 更到 5.28e10。折算模型见 +`docs/算法/采集与算法层设计说明.md` §13.2。 -补一句:额定点正弦峰值 = RMS × √2(电压族 ≈0.93 V、电流族 ≈0.24 V),两片转换器的量程都装得下;STM32 -五路交流负半周削到 0 的问题仍是 §7「前端偏置」行的 V0.1 既有口径,不是本节新发现。变比口径只圈电气 8 路, -温度与备用路不进 ④ 换算。 +**精度从哪来**:出厂默认值只保证量纲与量级;**精度**必须靠标准源两点标定: +在端子上加两个已知量、读两点的原始码值(串口 `adc:` 行),代进 +`adc_cal_two_point()` 算出该路的 `r` 与 `b`,经 `adc_cal_set_ratio()` / `adc_cal_set()` 写入 +并落盘。这一步做完才有 `ADC_CAL_F_VALID`,保护判据才会解卡 +(`docs/算法/采集与算法层设计说明.md` §13.3 / §13.4)。 -**eng 判定(「端子/子板变比确认」卡(#41)的完成判据)**: - -- `eng = 1`(出端子二次值)两条件:④ 变比到手——**已满足**(本节,电压/电流两族);电气 8 路 - `ADC_CAL_F_VALID` 全置位——**未满足**(`App/app/ui_data_feed.c` 的 `feed_eng()` 门,「两点标定与精度验收」卡承担)。 -- 判定:**`eng` 保持 0**,输出单位仍是 ADC 引脚电压,M4 承诺边界不变。本节改变的是:两点标定从 - 「开不了工」变成「可开工」——端子侧参照已定死,本节标称系数即其起点。 - -**复核口径(别把本节数值当本板实测)**: - -- 参数是铭牌/板面值:不含理想变压器偏差(励磁电流、绕组阻抗、负担误差),不含负担电阻容差,也不含逐路差 - (电压 4 路共用 606.06、电流 4 路共用 29.4118,逐路真值以标定实测为准)。 -- 下游机器面核对跟着在:两点标定的逐路实测系数吸收 ④ 段实际偏差;`App/algo/adc_cal.c` 合法带哨兵 - (k ∉ ROM×[0.5, 2.0] → 回退 ROM)是「④ 数量级错了」的兜底。 -- **本节标称系数是给「两点标定与精度验收」卡的输入,且带着两道存储口径题**(本卡不替它定案): - k 按 µV/码 int32 Q16 存储、上限 32767 µV/码(`docs/算法/采集与算法层设计说明.md §13.2` 的"远够"论断做在引脚域 - 假设上)——电压族端子域标称已越上限(92.48 mV/码 ≈ 92479 µV/码,UB/UC 达 488.28 mV/码 ≈ 488278 µV/码), - 电流族两档(4488 / 23696 µV 等效)尚在带内;且合法带基准读 ROM 出厂表(现为引脚域系数),端子域 k - 会被 [0.5, 2.0] 哨兵顶回。标定写入端子域系数之前,存储单位(或每路标度因子)与出厂表基准须一并换血, - 否则正确的标定值也会被拒。 -- 端子级上额定电压仍是独立动作:变比有数不等于可以在端子上加额定电压,先量端子对地通断的前置与风险次序不变,本节点不动高压。 +**历史值与作废依据**(别照着旧数算):原按"子板 PT 2 mA/2 mA、输入侧 200 kΩ、输出侧 330 Ω"算得 +变比 = 200000 ÷ 330 = 606.0606…。算术没问题(复核过两遍),**错在前提**:这组参数与实物不符。 +同一组参数推出的"额定点端子 ≈400 V → `AINn` 侧 0.66 V"也因此不再成立。若将来拿到子板原理图, +正确的做法是**用图纸复核实测量级**、而不是用图纸替换实测——两者冲突时以实测为准。 --- @@ -299,7 +302,7 @@ STM32(U17)为 LQFP144 脚号。 |---|---|---| | ① 至少 1 路电压通道经已知电压注入验证,斜率与理论 LSB 偏差 ≤2 % | ✅ 2026-10-27 | UA 6544.2 vs 6553.6 码/V(−0.14 %),§8.1 | | ② `adc_cal` 折算落地,采样环里是**引脚电压 V** 而非码值 | ✅ | `App/task/adc_task.c:425`(全仓唯一折算点) | -| ③ 8 通道系数 / 零点可落盘,加载失败回退 ROM 默认 | ✅ | `[adc_cal]` / `[adc_cal_ofs]`,口径 `docs/算法/采集与算法层设计说明.md` §13.8 | +| ③ 8 通道系数 / 零点 / 变比可落盘,加载失败回退 ROM 默认 | ✅ | `[adc_cal]` / `[adc_cal_ofs]` / `[adc_cal_sec]`,口径 `docs/算法/采集与算法层设计说明.md` §13.8 | | ④ 三处下游常数按工程量重定并注明依据 | ✅(有尾巴) | `docs/算法/采集与算法层设计说明.md` §13.7;幅值门在 `eng=0` 恒失效已记因于 `freq.h:58-60` | | ⑤ "未标定 → 保护闭锁"生效 | 🟡 软件道 ✅;**"解锁"这半程要等 `VALID` 置位**才能上台架验 | `adc_cal_prot_blocked()` + `App/task/protect_task.c:268` | | ⑥ 量纲回归用例(不该动作时不动作)通过 | ✅ | `test/host/prot_dim/`,口径 `docs/算法/采集与算法层设计说明.md` §13.6 | diff --git a/docs/算法/采集与算法层设计说明.md b/docs/算法/采集与算法层设计说明.md index 39bca4fd..513516bd 100644 --- a/docs/算法/采集与算法层设计说明.md +++ b/docs/算法/采集与算法层设计说明.md @@ -434,8 +434,8 @@ UA 无幅值落 IA → 0.0020 Hz。 | 问题 | 唯一答案 | 代码落点 | |---|---|---| -| 谁是 k/b 的真源 | `[adc_cal]` / `[adc_cal_ofs]` 两段的引擎 RAM 副本(`App/algo/adc_cal.c`),别处不得再存一份系数 | `s_kcfg` / `s_bcfg`;`App/algo/run_par.h` 头注释「不另存一份系数」 | -| 谁能写 RAM | **只有 run_par 的两支语义 setter** `run_par_cal_k_set()` / `run_par_cal_b_set()`——"写值 + 置脏"成对发生(task-7 落地,lead 裁②消除两份置脏逻辑);`adc_cal_set()` 本身**只写 RAM、不置脏、不落盘、不返回"稍后会存"** | `App/algo/run_par.c` 语义 setter 内 `adc_cal_set()` 之后才 `s_dirty = 1u` | +| 谁是 k/b/r 的真源 | `[adc_cal]` / `[adc_cal_ofs]` / `[adc_cal_sec]` 三段的引擎 RAM 副本(`App/algo/adc_cal.c`),别处不得再存一份系数或变比 | `s_kcfg` / `s_bcfg` / `s_rcfg`;`App/algo/run_par.h` 头注释「不另存一份系数」 | +| 谁能写 RAM | **只有 run_par 的三支语义 setter** `run_par_cal_k_set()` / `run_par_cal_b_set()` / `run_par_cal_r_set()`——「写值 + 置脏」成对发生(lead 裁②消除两份置脏逻辑);`adc_cal_set()` / `adc_cal_set_ratio()` 本身**只写 RAM、不置脏、不落盘、不返回「稍后会存」** | `App/algo/run_par.c` 语义 setter 内 `adc_cal_set()` / `adc_cal_set_ratio()` 之后才 `s_dirty = 1u` | | 谁落盘、什么时机 | 只有 defaultTask(配置引擎属主)的 `run_par_store_save()`:iec104Task 主循环 `run_par_dirty_peek()` → 置 `SYS_EVT_RUN_PAR_SAVE` → 属主 `cfg_file_save()` 整档外科保存 → 成功清脏,失败留脏下轮重试 | `App/algo/run_par.c` 落盘函数;本模块自身零文件 IO | **`cfg_file_poll()` 不是保存路径**:它的方向是**盘 → RAM 的重载**(外部改档 → 沉降去抖 → 重载,见 @@ -525,8 +525,10 @@ adcTask 静态最坏链与余量的**台账在** `docs/内存管理/RAM使用分 104 与 UI 拿到的数从哪来一份**。它是 `App/algo/adc_cal.*`、`meas.*`、`App/cfg/cfg_file.*`、 `App/task/protect_task.c`、`Drivers/BSP/UI/ui_data*` 这些模块头注释里引用的口径出处。 -**当前状态**:折算链已落地,量纲停在 **ADC 引脚电压 V**(`eng = 0`);保护判据因此处于 -**未标定闭锁**态(§13.3)。精度、端子二次值都在等外部资源(子板变比、标准源)。 +**当前状态**(2026-11-11 起):折算链已落地,量纲由**每路变比 `r`** 决定——出厂默认已带台架 +实测反算的变比(电压 1000 V/V、电流 29.4118 A/V),所以烧进新固件后屏上直接显**二次值**; +精度仍待标准源两点标定改写 `r`。保护判据仍处于**未标定闭锁**态(§13.3),因为 VALID 还要求 +两点标定这一步真的做过。 ### 13.1 通道量纲与折算点 @@ -541,52 +543,82 @@ adcTask 静态最坏链与余量的**台账在** `docs/内存管理/RAM使用分 - 通道数 `ADC_SEM_COUNT = 9`(8 电气 + 温度),枚举定义 `App/algo/adc_input.h:38-49`(`adc_semantic_t`)。 - **唯一折算点**:`App/task/adc_task.c:425` 的 `frame[s] = adc_cal_apply((adc_semantic_t)s, code)`—— - 全仓只有这一处把码值变成工程量,现单位 = **ADC 引脚电压 V**(`eng = 0`)。 + 全仓只有这一处把码值变成工程量,输出单位由该路变比 `r` 决定(§13.2):`r ≠ 0` → 二次值 V/A, + `r = 0` → ADC 引脚电压 V。 - 端到端链路分四段(① 语义↔网名↔通道、② 引脚电压↔码值、③ `AINn` ↔ 引脚、④ 端子 ↔ `AINn`), - 属主 `docs/硬件平台/ADC通道映射说明.md` §7.1:①②③ 已定死(③ 串阻不分压、比值 1:1),**只有 ④ 是缺口**。 + 属主 `docs/硬件平台/ADC通道映射说明.md` §7.1:①②③ 已定死(③ 串阻不分压、比值 1:1), + ④ 的登记值 = 电压族 1000 : 1、电流族 29.4118 A/V(§7.2,2026-11-11 台架实测修正, + 该节原按子板参数算定的 606.06 已被实测推翻)。 -### 13.2 折算模型:板级事实进 ROM,现场标定进 cfg +### 13.2 折算模型:三个系数各管一件事 ``` -工程量[V 或 A] = (code + b) × k_f - k_f = k[µV/码] × 1e-6 (float,init/load 时算一次) +工程量[V 或 A] = (code + b) × k[µV/码] × 1e-6 × r ``` -| 符号 | 含义 | 存哪 | 定标 | +| 符号 | 管什么 | 存哪 | 默认值 | |---|---|---|---| -| `k` | µV/码(通道灵敏度) | cfg 段,int32 **Q16**(µV/码 × 65536) | 默认 = 板级理论系数(§13.1 表) | -| `b` | 零点偏移(码值) | cfg 段,int32 | 默认 0 | -| `k_f` | V/码(运行时) | RAM,`adc_cal_init/load` 时由 `k` 派生 | 不落盘 | +| `k` | **前端斜率**:引脚电压 ↔ 码值(板级量程事实) | `[adc_cal]`,int32 **Q16**(µV/码 × 65536) | TPAFE 10000000(152.59 µV/码)、STM32 52800000(805.66 µV/码) | +| `b` | **零点偏移**(码值) | `[adc_cal_ofs]`,int32(可负) | 0 | +| `r` | **端子二次值 ↔ 引脚电压的变比**(现场标定产物) | `[adc_cal_sec]`,int32 **Q16**(比值 × 65536) | 电压族 65536000(1000.0 V/V)、电流族 1927532(≈29.4118 A/V) | +- **为什么必须拆成三个、不能把变比并进 `k`**:并进 `k` 会直接溢出存储口径——UA 端子域 + `k` = 152.59 × 1000 = 152 587.9 µV/码,Q16 就是 1.0e10,而 int32 上限只有 2.147e9。 + 拆开之后 `r` 单独存 Q16 绰绰有余(1000 → 65 536 000)。更根本的理由:`k` 是板级前端斜率、 + `r` 是子板/端子板的分压比,两者是**不同的事实**,合并就是把硬件比伪装成标定系数 + (`docs/硬件平台/ADC通道映射说明.md` §7.2 明令禁止"用系数凑硬件")。 +- **换算写成"先引脚电压、再乘变比"两步**,不把 `k×r` 先并成一个大系数:`k` 的 Q16 上界 + 5.28e7 乘码值仍落在 float32 的 2^24 精度区内;若先并成 60 万级 Q16(→4e10)再乘码值, + float32 只剩 7 位有效数字,末位抖动会跑到屏面 3 位小数上。 - **为什么存 µV 而不是 V/码**:`k` 在 V/码 量级是 3×10⁻⁴,Q16 只有 20 个刻度(5 % 分辨率); 换成 µV/码是 305,Q16 有 2000 万个刻度。 - **为什么 Q16 而不是参考项目的 Q10**:TPAFE 通道 Q10 只能表达 312(+0.16 %),Q16 误差 < 0.001 %; 两者都占 4 字节,Q16 把"系数定点舍入"这一项从误差预算里直接抹掉。参考项目用 2^10 是因为定点 DSP 乘法成本,本机有 FPU、运行时本来就是 float。int32 上限可表达 32767 µV/码,远够。 -- **运行时不走定点**:`k` 预乘成 `float k_f`,逐点只做一次浮点乘——1 kHz × 9 通道 = 9k 次/秒,可忽略。 -- **系数合法带**:`adc_cal_set()` / `adc_cal_two_point()` 只接受落在板级 ROM 系数 ×[0.5, 2.0] 内的 `k`。 - 实测斜率与理论值偏差大时**先查前端**(分压 / 变比 / 焊接),不许用系数把硬件的错凑回来—— - 那等于把故障固化成"标定值"。零点越 ±`ADC_CAL_B_MAX` 同样直接拒绝(口径在 `App/algo/adc_cal.h`)。 +- **运行时不走定点**:逐点只做两次浮点乘(`×k` 再 `×r`)——1 kHz × 9 通道 = 9k 次/秒,可忽略。 +- **`k` 的合法带**:`adc_cal_set()` 只接受落在板级 ROM 系数 ×[0.5, 2.0] 内的 `k`。实测斜率与理论值 + 偏差大时**先查前端**(分压 / 变比 / 焊接),不许用系数把硬件的错凑回来——那等于把故障固化成 + "标定值"。零点越 ±`ADC_CAL_B_MAX`(8192 码)同样直接拒绝。 +- **`r` 的合法带是绝对带,不是 ROM×[0.5,2.0]**:`r` 是现场标定量,不同 CT/PT 变比差几个数量级, + 窄带没有意义;下界取 1.0(分压与负担电阻只会把端子值变小,比"比值 < 1"更可能的是把 mV 当 V 填错), + 上界是 int32 能表达的 32767.99。**`r = 0` 是合法值**,语义 = "本路不折算,输出停在引脚电压", + 这也是现场关掉某一路折算的正规手段(改成 1.0 反而会留下一个假变比)。 +- **两点标定今后标的是 `r`,不是 `k`**(`adc_cal_two_point()` 已改签名,`k_q16` 变成入参): + `k` 是板级事实、现场不该改也改不动,真正未知的是"端子 ↔ 引脚"这一层。公式在公式核的注释里, + 要点是分两步算(`t = dc × r` 先出来,再还原 Q16)——一次算 `dv×2^32/(dc×k_q16)` 会在 + 高压注入时把 long long 顶穿。 ### 13.3 三态可用性:标定 ≠ 可测 -`k` 的默认值是**板级理论系数**,此时工程量单位是"ADC 引脚电压",不是二次值——还缺互感器/子板变比。 -所以通道状态必须让消费者区分三档,不能把引脚电压当二次值用: +`r` 的出厂默认值是**台架实测反算的变比**,所以出厂状态下每路的**量纲**就已经是二次值; +但"这个数准不准"要靠标准源两点标定才能回答。所以通道状态仍必须让消费者区分三档: ```c -#define ADC_CAL_F_VALID 0x01u /* 该通道 k 已按变比标定 → 工程量为二次值 */ +#define ADC_CAL_F_VALID 0x01u /* 该路已两点标定确认(k/b 在带内 且 r 已配置)→ 读数可信 */ #define ADC_CAL_F_CLIPPED 0x02u /* 硬件削顶(单极性无偏置)→ 交流量不可信 */ ``` +**两个事实互不派生、别互相反推**:`VALID` 回答"准不准",`r` 回答"什么单位"。 +`r = 0` 时 `VALID` 必为 0(合法带要求 r 已配置),所以"VALID 未置位却有二次值读数" +这种组合不可能出现;反过来说,只配了变比、没做两点标定的通道,输出**确实是**二次值、 +只是精度未定——量纲标签若跟着 VALID 走,屏上就会出现"数值是 5.000(二次)却标未标定 V" +这种自相矛盾。 + | 通道 | `VALID` | `CLIPPED` | 消费者应如何对待 | |---|---|---|---| -| UA / UN / IA | 标定后置位 | 0 | 可用于保护、计量、上送 | +| UA / UN / IA | 两点标定后置位 | 0 | 可用于保护、计量、上送 | | UB / UC / IB / IC / IN | 永不置位(V0.2 硬件改版前) | **1,且是来源派生**(不是实测削顶,判据见 §13.5) | 只作码值监视;点表**已绑定**,靠品质位把 104 侧驱动成 IV、值清零 | -| TEMP | 0 | 0 | 不参与线性标定,页面显示 `--` | +| TEMP | 0 | 0 | 不参与线性标定(`r` 恒 0),页面显示 `--` | `adc_cal_flags(sem)` 是三处(UI / 104 / 保护)唯一的可用性查询入口;`adc_cal_apply()` **不返回**可用性, 所以"只看系数不看 flags"这种用法在接口层就被堵掉(CLIPPED 通道恒报 IV 的单测见 `test/host/prot_dim/`)。 +**整帧量纲标签(`eng`)的判据与上面两个都不同**:它看的是"每路要么已配变比、要么削顶不显示" +(`adc_cal_ratio_configured()`),与 `VALID` 无关。理由:削顶通道页面本来就显 `--`,不进任何读数, +拿它们卡住整帧标签只会让 V0.1 那五路削顶把徽标永久钉在"未标定"上。判据在 +`App/app/ui_data_feed.c` 的 `feed_eng()`,口径真源写在 `App/algo/adc_cal.h` 的 +`adc_cal_prot_blocked()` 注释 ③。 + ### 13.4 未标定即闭锁 保护判据拿"ADC 引脚电压"跟"二次值定值"比必然误动作,所以 `protect_task` 多一个闭锁源: @@ -620,9 +652,9 @@ V0.1 恒命中 UB/UC/IB/IC/IN,位图 `0xE6`),它表示"这路不能测交 `cal:` 行的 `rail=` 位图与 `rail: edge=…` 那行 `[ERR]`——这条路径至今只在直流饱和里起过(判据见 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §8.5 削顶卡)。 -> **承诺边界**:只到 **ADC 引脚电压**(`eng = 0`)。"端子二次值"要等子板变比确认、 -> 标准源两点标定写入 `[adc_cal]` 并置 `VALID`;`eng` 升 1 的条件是电气 8 路 VALID **全置位** -> (`App/app/ui_data_feed.c` 的 `feed_eng()`)。两点标定怎么开工见 §13.13。 +> **承诺边界**(2026-11-11 更新):**量纲**已到端子二次值(出厂默认变比已带硬件事实, +> `eng` 判据见 §13.3);**精度**仍只到"ADC 引脚电压这一层的 ±2 %"——标准源两点标定 +> 写入 `[adc_cal_sec]` 的 `r` 并置 `VALID` 之后,二次值才谈得上精度。两点标定怎么开工见 §13.13。 **动态验证已跑两轮(2026-11-02 / 11-04),六轮全部闭合**——幅值 / 波形 / 相位 / 测频 / 谐波 / 削顶直流档都过了;实测数与逐轮结论只详述在 `docs/硬件平台/ADC通道映射说明.md` §8.2,本节不抄第二份。 @@ -630,7 +662,7 @@ V0.1 恒命中 UB/UC/IB/IC/IN,位图 `0xE6`),它表示"这路不能测交 | 下游 | 现在 | 为什么 | |---|---|---| -| 基波有效值 | ✅ 过(±2 % 内,三档幅值两点复现) | 台架幅值轮 + 波形轮(基波分量口径)实证;**端子二次值仍不归这一档**——从引脚到端子还隔着 ④ 段变比 | +| 基波有效值 | ✅ 引脚层过(±2 % 内,三档幅值两点复现) | 台架幅值轮 + 波形轮(基波分量口径)实证;**二次值这一档的精度仍未验**——出厂变比是单点粗测反算(5 V 注入只有 33 码,1 LSB = 3 %,电压族真值区间约 909~1111),标准源两点标定后才算数 | | 相位 | ✅ 同片内过(录波档 ≤2° 判据) | 同段 UA/UN 两路实测(0° 与 90° 两工况);**跨转换器仍禁比**——两片转换器无共同采样触发,是 V0.1 硬件边界(`docs/硬件平台/ADC通道映射说明.md` §7 通道归属行),不是待修缺陷 | | 频率 | ✅ 「如实报不可测」成立(这就是现状判据) | 含满幅注入段全不可测、UI 恒 `--`;**45~55 Hz 精度仍未验**——要验须降 `FREQ_AMP_MIN` 单跑一轮 | | 削顶 / 品质 | ✅ DC −1.00 V 档锁存 + sticky 实证;2.0 Hz 低频档与复位复核经用户裁定 2026-11-04 关闭 | `[ERR] rail: edge=0x2 sticky=0x2` 唯一一条、撤注恒持 | @@ -652,7 +684,7 @@ V0.1 恒命中 UB/UC/IB/IC/IN,位图 `0xE6`),它表示"这路不能测交 | 量纲档 | 怎么进入这个档 | 现在的处置 | |---|---|---| | `eng = 0`(ADC 引脚电压 V) | `VALID` 未置位——**当前产品构建恒此态** | §13.4 闭锁停走整条判据链;UI 第 7 行显示"未标定"徽标 | -| `eng = 1`(二次值 V / A) | ④ 变比到手 **且** 电气 8 路 `adc_cal_flags()` 的 VALID 全置位(`App/app/ui_data_feed.c` 的 `feed_eng()`) | 定值与工程量同域,判据才名副其实 | +| `eng = 1`(二次值 V / A) | 每路要么已配变比(`adc_cal_ratio_configured()`)、要么削顶不显示(`App/app/ui_data_feed.c` 的 `feed_eng()`) | 定值与工程量同域,判据才名副其实 | | 混档(最坏情况) | 只改折算系数、不改下游常数 | §13.7 那三处必须跟着动,这正是 §13.7 存在的理由 | ### 13.7 三个必须同步重定的下游常数 @@ -665,22 +697,33 @@ V0.1 恒命中 UB/UC/IB/IC/IN,位图 `0xE6`),它表示"这路不能测交 | 2 | `App/protocol/iec104/iec104_pts.c:69-80` | `k_yc[].deadband` 已按点重定:电压 0.5 V、电流 0.005 A、频率 0.5 Hz(真源在 `run_par`,UI 可改) | ✅ 已重定;④ 变比到手后需再复核一次量纲 | | 3 | `App/algo/prot_set.c:25-70` | `g_prot_set_meta[]` 定值 = 二次值 A / V(基准 100 V / 1 A):OC1 10 A、OV 120 V、UV 20 V、3U0 15 V… | 🟡 口径对,但**标定前无法在板上验真**;精度目标定了之后连同 `min`/`max` 复核 | -### 13.8 落盘:拆两段,不碰引擎 +### 13.8 落盘:拆三段,不碰引擎 -`CFG_FILE_MAX_KEYS = 16`,8 通道 ×(k + b)= 16 正好顶格,将来加一个键就爆。故拆两段: +`CFG_FILE_MAX_KEYS = 16`,8 通道 ×(k + b + r)= 24 个键,一段放不下 +(8 通道 ×(k + b)= 16 已经正好顶格,加第三个维必爆)。故拆三段: | 段名 | 键 | 字节 | 回调 | |---|---|---|---| | `[adc_cal]` | `k_ua` `k_un` `k_ia` `k_ub` `k_uc` `k_ib` `k_ic` `k_in` + `valid`(uint8 位图) | 8×4 + 1 = 33 | 不挂(RAM 副本即生效值) | | `[adc_cal_ofs]` | `b_ua` … `b_in` | 8×4 = 32 | 不挂 | +| `[adc_cal_sec]` | `r_ua` `r_un` `r_ia` `r_ub` `r_uc` `r_ib` `r_ic` `r_in`(int32 Q16 比值) | 8×4 = 32 | 不挂 | -段名 7 / 11 字符、键名 4 字符,均在 15 字符上限内;段数总计 5/8(含 `[run_par]` 9 键死区),未逼近上限。 +段名 7 / 11 / 11 字符、键名 4~5 字符,均在 15 字符上限内;**段数总计 8/8——段位已满** +(本层三段 + `[run_par]` 9 键 + `[comm]` 2 键 + `[network]` / `[ftp]` / `[sntp]`, +与全仓 8 处 `cfg_file_register()` 调用一一对应,上限 `CFG_FILE_MAX_SECTIONS = 8`)。 +⚠ **再加受管段必须先抬上限或合并段**:第九段会被 `cfg_file_register()` 直接拒掉, +只剩一行 `[ERR]`,而注册顺序决定谁被拒(谁先注册谁留下)。 -- 系数唯一真源仍是这两段——104 写 k/b 经 `adc_cal_set()` 代理,`run_par` **不存第二份**; - 落盘链走 `SYS_EVT_RUN_PAR_SAVE`(iec104Task peek 投递 → defaultTask 整档 save),`p_cb` 依旧未用。 - 谁写 RAM / 谁落盘 / 什么时机的完整链条见 §10.1。 +- 系数唯一真源仍是这三段——104 写 k/b/r 经 `adc_cal_set()` / `adc_cal_set_ratio()` 代理, + `run_par` **不存第二份**;落盘链走 `SYS_EVT_RUN_PAR_SAVE`(iec104Task peek 投递 → + defaultTask 整档 save),`p_cb` 依旧未用。谁写 RAM / 谁落盘 / 什么时机的完整链条见 §10.1。 +- ⚠ **变比段的 ROM 默认不是 0**:烧进新固件后屏上立刻显二次值。这与"没标定过"不矛盾—— + 变比是硬件事实(出厂就该带着),`VALID`(准不准)仍然要求两点标定,两者互不代替(§13.3)。 - ⚠ **负零点**:零点偏移是带符号量,引擎只有 `CFG_VAL_UINT`(`strtoul` + 上界校验)时 −5 会落成 `4294967291`。所以 `[adc_cal_ofs]` 走 `CFG_VAL_INT`(定案口径见 §13.12)。 +- **一次变比不新开段**:PT/CT 一次额定已在 `prot_set` 里(`PS_PT_PRI` / `PS_PT_SEC` / `PS_CT_PRI` / + `PS_CT_SEC`,104 地址 0x8820/0x8821/0x8825/0x8826),再开一段就是同一事实两个真源。 + 变比段只登记**二次值口径**的 `r`;`eng = 2`(一次值)继续预留。 ### 13.9 遥测快照:让 104 与 UI 共用一份上游 @@ -765,6 +808,12 @@ typedef struct { | 零点负值怎么存 | 配置引擎**新增 `CFG_VAL_INT`**(有符号十进制、小端补码),而不是把零点整体偏置成正数再回读时减法还原 | 偏置方案要在每个读点都记得减,漏一处就是静默错值;INT 类型是一次性成本 | | 系数落盘用什么单位 | **µV/码的 Q16 定点**,不存 V/码 浮点 | Q16 只剩 20 个刻度,V/码 直存会把有效位丢掉 | | 系数的合法带 | 只接受板级 ROM 系数的 **×[0.5, 2.0]** | 超带说明前端错了,应该去查硬件而不是用系数凑 | +| 变比 `r` 存哪 | **新开一段 `[adc_cal_sec]`,int32 Q16 比值**,不并进 `k` | 并进 `k` 直接溢出 int32(UA 端子域 k 的 Q16 = 1.0e10 > 2.147e9);且变比是硬件事实、`k` 是前端斜率,混一起就是"用系数凑硬件" | +| `r` 的合法带 | **绝对带**:`0`(不折算)或 `≥ 1.0`,上界 int32 能表达的 32767.99 | `r` 是现场标定量,不同 CT/PT 差几个数量级,用 ROM×[0.5,2.0] 那种窄带没有意义 | +| `r` 的出厂默认 | **非 0**:电压族 1000 V/V、电流族 29.4118 A/V | 变比是硬件事实,出厂就该带着;"准不准"由 VALID 回答,两者互不代替(§13.3) | +| 两点标定标谁 | **标 `r`**,`k` 由 ROM 固化 | `k` 是板级事实、现场不该改也改不动;真正未知的是"端子 ↔ 引脚"这一层 | +| 整帧量纲标签 `eng` 的判据 | **每路要么已配变比、要么削顶不显示** | 削顶通道页面本来就显 `--`、不进读数;拿它们卡标签会让 V0.1 五路削顶把徽标永久钉在"未标定" | +| 一次变比(PT/CT 一次额定)存哪 | **复用 `prot_set` 已有四项**,不新开段 | 已经在册(`PS_PT_PRI` / `PS_PT_SEC` / `PS_CT_PRI` / `PS_CT_SEC`),再开一段就是同一事实两个真源 | | 遥测快照的字段布局 | 由协议侧定、单测按偏移钉死(72 B × 双区) | 布局漂移只能靠"读侧按名字取"掩盖,抓不到真正的错位 | | 非交流量(TEMP) | **不参与线性标定**,`qds` 恒置 `NT`;也不进录波 | 温度是片内 12 位单极性原始值、非线性;进录波只多占 640 字节,还会让回放页多一格被误读成电气量 | | 快照的并发读 | **双区 + 指针原子交换**,读侧无锁 | 加锁会把读侧拖进临界区,而快照是 200 ms 一次的发布、读侧是 104 与 UI 两个消费者 | @@ -780,7 +829,7 @@ typedef struct { | 待定 | 备选 | 为什么必须先定 | |---|---|---| | **精度目标** | 0.5 级 / 1 级 | 决定两点标定的判据,否则"误差 ≤ 目标精度"这条无法判定。保护判据本身只需 3~5 %,0.5 级是计量口径 | -| **端子 / 子板变比从哪来** | ① 有原理图(直接算)② 只有实物(万用表同时量端子与引脚)③ 都没有 | 决定 `eng` 能否置 1。现状已按 ① 的参数算定(`docs/硬件平台/ADC通道映射说明.md` §7.2),等实物复核;② 同样可行且够用;③ 则只交付引脚电压、二次值登记为缺口 | +| **端子 / 子板变比从哪来** | ① 有原理图(直接算)② 只有实物(万用表同时量端子与引脚)③ 都没有 | 决定变比登记的**可信度**(注意:量纲已不依赖它——出厂默认变比已按台架实测反算写进 `[adc_cal_sec]`,`eng` 能升 1;缺的是精度)。2026-11-11 用继保仪单点注入反算得 1000.0 / 29.4118,推翻了原先按子板参数算定的 606.06(详见 `docs/硬件平台/ADC通道映射说明.md` §7.2);② 两点标定是拿到精度的正规路径;③ 则二次值只有单点粗测精度(电压族真值区间约 909~1111) | | **`eng = 0` 期间测频幅值门怎么处置** | 保持恒失效(现状,UI 恒 `--`)/ 台架临时把 `FREQ_AMP_MIN` 降到引脚域量级跑一轮再还原 / 换成相对量门限 | 建议**常态保持恒失效 + 需要时临时降门跑一轮**;换相对量门限要先重新论证,别在过渡期改。临时降门会改产品头文件,收尾必须还原到 `git diff` 为空并重跑构建 | **送检顺序不可倒过来**:产品构建当前要求 UA/UN/IA 的标定 `VALID` 全部置位才走保护判据。 diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 8fd6dbd9..0cf1d94f 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -20,3 +20,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-11-05 254820 30896 2072 150104 285716 27.2 94752 72.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6236;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 2026-11-09 255160 31204 2080 152152 286364 27.3 96808 73.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;nand_ftl.o=8288;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 2026-11-10 255160 31204 2080 152152 286364 27.3 96808 73.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;nand_ftl.o=8288;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 +2026-11-11 256024 31760 2080 152368 287784 27.4 97024 74.0 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;nand_ftl.o=8288;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;wave_ring.o=5332;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index e112533d..fa4f08ea 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,25 +1,25 @@ { - "built_at": "2026-11-10T22:31:07+08:00", - "fingerprint": "5080b00d80af5604476422c1afa383cb09e782e6bb76aa2abf9e96a77d5c60fd", + "built_at": "2026-11-11T11:30:01+08:00", + "fingerprint": "c18868f55e079c317871dfdb107229bb460d159c3ac631a0db2648c39554454d", "fingerprint_files": 1479, "fingerprint_source": "git-index", "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "7b05d7bfd40fafa514e166d3fda5270f2b040c2b", + "git_head": "8115fb9a87c5f04a9916828fabedb75e4fc9b82d", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1794321057, + "map_mtime": 1794366713, "pct": { - "ER_IROM1": 27.31, + "ER_IROM1": 27.45, "RW_CCM": 87.89, - "RW_IRAM1": 73.86, + "RW_IRAM1": 74.02, "UNINIT_CCM": 18.75 }, "regions": { "ER_IROM1": { "max": 1048576, - "size": 286364 + "size": 287784 }, "RW_CCM": { "max": 65280, @@ -27,7 +27,7 @@ }, "RW_IRAM1": { "max": 131072, - "size": 96808 + "size": 97024 }, "UNINIT_CCM": { "max": 256, @@ -37,10 +37,10 @@ "schema": 2, "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": 255160, - "ro_data": 31204, + "code": 256024, + "ro_data": 31760, "rw_data": 2080, - "zi_data": 152152 + "zi_data": 152368 }, "threshold_pct": 92.0, "worktree_dirt_at_stamp": 0, diff --git a/test/host/adc_cal/adc_cal_test.c b/test/host/adc_cal/adc_cal_test.c index 4321ecde..4769e22f 100644 --- a/test/host/adc_cal/adc_cal_test.c +++ b/test/host/adc_cal/adc_cal_test.c @@ -11,19 +11,20 @@ * 哨兵漏放,只有离线端能用独立推导的期望值 + 坏文件注入一次性钉死。 * * 覆盖点(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 码值点,标定折算落地 DoD) + * TC-ADCCAL-001 init 注册三段:名称/键数/data_size/无回调/默认表指向 + * TC-ADCCAL-002 UA(TPAFE)折算:0 / 8192 / 32767 / -32768,期望 = 引脚值 × 电压族变比 + * TC-ADCCAL-003 UB(STM32)折算:0 / 2048 / 4095,期望 = 引脚值 × 电压族变比 + * TC-ADCCAL-004 其余 6 路各 1 点(变比按族逐路挑:UC/UN 电压、IA/IB/IC/IN 电流) * 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)守卫(两点标定的算术核,台架注入待跑) + * TC-ADCCAL-014 零点 b 生效(set 路径):(code+b)×k×r 逐项可算 + * TC-ADCCAL-015 两点标定公式:合成 r/b 回算(独立推导、相对误差 1e-6)+ + * 写回往返(set k/b + set_ratio 后两点复现端子值)+ 参数/斜率/ + * k 合法域/v 越 ±1e9/r < 1.0(-1)/零点越界(-2)守卫 * TC-ADCCAL-016 rail_ths 门限:TPAFE ±32112 / STM32 4013·81 / TEMP 不监视 * TC-ADCCAL-017 init 幂等:重复调用 RAM 复位默认 + 重新注册 * —— 以下走真实 cfg_file 引擎(标定落盘与整定入口 DoD)—— @@ -36,6 +37,13 @@ * 闭锁 / 两路齐第三路缺仍闭锁(全或无)/ 三路齐放行且 UB 仍无 VALID * (证明它是"三路"而不是 feed_eng 那个"8 路")/ 复位后立刻回闭锁 * (现查、无自有状态可残留) + * TC-ADCCAL-024 变比三维读数口初值 = ROM 默认(电压族 1000.0 / 电流族 29.4118), + * ratio_configured 全 1,TEMP/越界恒 0 + * TC-ADCCAL-025 set_ratio(sem, 0) 成功路径(返 0):该路输出退化到引脚电压、 + * ratio_configured 归 0、VALID 清;再写回非 0 即恢复 + * TC-ADCCAL-026 set_ratio 拒绝面:比值 < 1.0 拒(-1,RAM 不动)、TEMP 拒、越界通道拒 + * TC-ADCCAL-027 坏文件注入 [adc_cal_sec]:r_ua=0 与 r_ua=1(< 1.0)都不折算、 + * VALID 不亮;未列键保留 ROM 变比(= 只坏一路不牵连别路) * —— 以下四项是"改了没落盘"的可诊断化(道G task-43 第②项;四态**定义只在 * App/algo/adc_cal.h 一处**,本表只描述断言、不重定义)—— * TC-ADCCAL-019 观测件不得反过来制造故障:get_stats(NULL) 静默返回、随时可读 @@ -176,12 +184,57 @@ static int s_total = 0, s_passed = 0, s_failed = 0; /* ===================== 独立推导的期望值 ===================== */ /* 期望一律用 **double 直接除**(10/65536、3.3/4096),不引用被测的 Q16 常量, - * 满足标定折算落地 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; + * 满足标定折算落地 DoD「期望值由 LSB 常数独立算得(不是抄实现)」。三层口径: + * ① 引脚域 exp_pin():按通道来源给 LSB——TPAFE 10 V 跨度 / 65536 码, + * STM32 0~3.3 V / 4096 码; + * ② 端子(二次)域 exp_eng():引脚值 × 该路变比。变比按族在此**独立写出十进制 + * 常量**(电压族 1000.0 V/V、电流族 29.4118 A/V = 1927532/65536 回落两位), + * 不引 ADC_CAL_R_Q16_* 宏——引宏就等于把实现里的数抄进期望,正是 DoD 禁止的; + * ③ 整定域 exp_q16():先按 Q16 系数还原引脚电压,再乘该路变比。 + * ⚠ 变比必须**按通道**挑,不能一刀切:TPAFE 侧 UA/UN 是电压族、IA 是电流族; + * STM32 侧 UB/UC 是电压族、IB/IC/IN 是电流族(V0.1 双转换器分配,见 + * docs/硬件平台/ADC通道映射说明.md 与 adc_cal.c 的 ROM 表)。 + * 容差只需盖住 float32 中间积舍入(相对 ~1e-7)与打印粒度。 */ +#define EXP_R_VOLT (1000.0) /* 电压族变比 V/V */ +#define EXP_R_CURR (29.4118) /* 电流族变比 A/V(1927532/65536 的十进制口径) */ + +static double exp_ratio(adc_semantic_t sem) { + switch (sem) { + case ADC_SEM_UA: + case ADC_SEM_UB: + case ADC_SEM_UC: + case ADC_SEM_UN: + return EXP_R_VOLT; + case ADC_SEM_IA: + case ADC_SEM_IB: + case ADC_SEM_IC: + case ADC_SEM_IN: + return EXP_R_CURR; + default: + return 0.0; + } +} + +/* 引脚域:来源决定 LSB(TPAFE 是 16 位双极性 10 V 跨度,STM32 是单极性 3.3 V) */ +static double exp_pin(int32_t code, adc_semantic_t sem) { + switch (sem) { + case ADC_SEM_UA: + case ADC_SEM_UN: + case ADC_SEM_IA: + return (double)code * (10.0 / 65536.0); + default: + return (double)code * (3.3 / 4096.0); + } +} + +/* 端子域 = 引脚域 × 该路变比 */ +static double exp_eng(int32_t code, adc_semantic_t sem) { + return exp_pin(code, sem) * exp_ratio(sem); +} + +/* 整定域:Q16 系数还原引脚电压,再乘该路变比(= adc_cal_apply 的三维口径) */ +static double exp_q16(adc_semantic_t sem, int32_t code, int32_t b, int32_t k_q16) { + return ((double)code + (double)b) * ((double)k_q16 / 65536.0) / 1.0e6 * exp_ratio(sem); } static int near_d(double got, double want) { double d = got - want; @@ -222,9 +275,9 @@ static const char *vfs_get(void) { return (const char *)s_vfs; } /* ===================== 用例(白盒组) ===================== */ -/* TC-ADCCAL-001:init 注册两段(名称/键数/data_size/无回调) */ +/* 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"); + printf("[TC-ADCCAL-001] init registers [adc_cal]/[adc_cal_ofs]/[adc_cal_sec]\n"); reset_all(); adc_cal_init(); CHECK(s_sections[0].used == 1u, "section slot0 used"); @@ -236,31 +289,38 @@ static void tc_001_init_registers(void) { "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"); + CHECK(s_sections[2].used == 1u && strcmp(s_sections[2].name, "adc_cal_sec") == 0, + "sec2 name = adc_cal_sec (ratio dimension)"); + CHECK(s_sections[2].key_count == 8u, "sec2 keys = 8r"); + CHECK(s_sections[2].data_size == 32u, "sec2 data_size = 32"); + CHECK(s_sections[2].p_cb == NULL, "sec2 no callback"); + CHECK(s_sections[3].used == 0u, "only three sections registered"); } -/* TC-ADCCAL-002/003/004:8 通道 × ≥3 码值点折算,期望独立推导(标定折算落地 DoD) */ +/* TC-ADCCAL-002/003/004:8 通道 × ≥3 码值点折算,期望独立推导(标定折算落地 DoD) + * v2.0 起输出是**二次值**(引脚值 × 该路变比):TPAFE 侧 UA/UN 走电压族 1000、 + * IA 走电流族 29.4118;STM32 侧 UB/UC 走电压族、IB/IC/IN 走电流族。 */ static void tc_002_004_apply(void) { - printf("[TC-ADCCAL-002] UA (TPAFE) apply\n"); + printf("[TC-ADCCAL-002] UA (TPAFE, voltage family) 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)"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 0), exp_eng(0, ADC_SEM_UA)), "UA code 0 = 0V"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 8192), exp_eng(8192, ADC_SEM_UA)), "UA 8192 = 1.25V pin -> 1250V terminal"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 32767), exp_eng(32767, ADC_SEM_UA)), "UA full scale minus 1 (bipolar top code)"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UA, -32768), exp_eng(-32768, ADC_SEM_UA)), "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-003] UB (STM32, voltage family) apply\n"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UB, 0), exp_eng(0, ADC_SEM_UB)), "UB code 0 = 0V"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UB, 2048), exp_eng(2048, ADC_SEM_UB)), "UB mid = 1.65V pin -> 1650V terminal"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UB, 4095), exp_eng(4095, ADC_SEM_UB)), "UB +FS 3.2992V pin"); 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"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UC, 123), exp_eng(123, ADC_SEM_UC)), "UC 123 (voltage family)"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UN, -10000), exp_eng(-10000, ADC_SEM_UN)), "UN -10000 (voltage family)"); + CHECK(near_d(adc_cal_apply(ADC_SEM_IA, 11452), exp_eng(11452, ADC_SEM_IA)), "IA 11452 (current family, bench first-frame code)"); + CHECK(near_d(adc_cal_apply(ADC_SEM_IB, 4000), exp_eng(4000, ADC_SEM_IB)), "IB 4000 (current family)"); + CHECK(near_d(adc_cal_apply(ADC_SEM_IC, 1), exp_eng(1, ADC_SEM_IC)), "IC 1 (current family)"); + CHECK(near_d(adc_cal_apply(ADC_SEM_IN, 4094), exp_eng(4094, ADC_SEM_IN)), "IN 4094 (current family)"); } /* TC-ADCCAL-005:TEMP 透传与守卫 */ @@ -315,7 +375,7 @@ static void tc_008_set(void) { 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)), + CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 1000), exp_q16(ADC_SEM_UA, 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"); @@ -337,7 +397,7 @@ static void tc_009_sanitize(void) { adc_cal_init(); s_kcfg.k[0] = 30000000L; /* > ROM*2:模拟坏文件/坏标定已写入 RAM */ s_kcfg.valid |= 0x01u; /* 位图也说「已标定」——更要用哨兵兜住 */ - want = exp_tpafe(1000); + want = exp_eng(1000, ADC_SEM_UA); 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"); @@ -346,7 +406,7 @@ static void tc_009_sanitize(void) { 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(near_d(adc_cal_apply(ADC_SEM_UB, 1000), exp_eng(1000, ADC_SEM_UB)), "bad b -> ROM + b=0"); CHECK(g_caltest_err_count == 2, "bad b noted on its own channel"); /* init 重置留痕位 = 新观测窗 */ adc_cal_init(); @@ -366,43 +426,66 @@ static void tc_00a_b_effect(void) { 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"); + CHECK(near_d(adc_cal_apply(ADC_SEM_IA, 1077), exp_q16(ADC_SEM_IA, 1077, -77, 10000000)), + "IA apply = (code-77)*ROMk*r_curr"); } /* 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 rq = 0, bb = 0; + const int32_t k0 = 10000000L; /* 该路生效 k(TPAFE 152.587890625 uV/code) */ + const double r0 = 1000.0; /* 待标定的端子 vs 引脚变比(电压族量级) */ + const int32_t b0 = -30L; /* 真零点,码值 */ + const int32_t c1 = 300, c2 = 800; /* 两点码值(合端子 41 V / 117 V 量级) */ int32_t v1, v2; int rc; - printf("[TC-ADCCAL-015] two-point formula recovers synthetic k/b\n"); + printf("[TC-ADCCAL-015] two-point formula recovers synthetic r/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); + /* 合成测量:端子 uV = (c + b0) x (k0/65536) uV/code x r0(lround 模拟量化读数) */ + v1 = (int32_t)my_lround(((double)c1 + (double)b0) * ((double)k0 / 65536.0) * r0); + v2 = (int32_t)my_lround(((double)c2 + (double)b0) * ((double)k0 / 65536.0) * r0); + rc = adc_cal_two_point(c1, c2, v1, v2, k0, &rq, &bb); CHECK(rc == 0, "two_point ok"); - CHECK(kq > (k0 - 5) && kq < (k0 + 5), "k recovered within +-5 q16"); + CHECK(((double)rq > (r0 * 65536.0 * (1.0 - 1.0e-6))) && + ((double)rq < (r0 * 65536.0 * (1.0 + 1.0e-6))), + "r recovered within 1e-6 relative"); CHECK(bb > (b0 - 3) && bb < (b0 + 3), "b recovered within +-3 codes"); - /* 写回往返:set 后 apply(c2) 应还原 v2(µV 级) */ + /* 写回往返:set(k,b) 置位 + set_ratio 定量纲,两点都应复现端子值 */ 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"); + CHECK(adc_cal_set(ADC_SEM_UA, k0, bb) == 0, "set recovered k/b"); + CHECK(adc_cal_set_ratio(ADC_SEM_UA, rq) == 0, "set recovered ratio"); + CHECK((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) != 0u, + "VALID needs both writes (k/b band + ratio configured)"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UA, c1), (double)v1 / 1.0e6), + "roundtrip v(c_lo) = measured"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UA, c2), (double)v2 / 1.0e6), + "roundtrip v(c_hi) = 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"); + CHECK(adc_cal_two_point(100, 100, 0L, 100000L, k0, &rq, &bb) == -1, "dc=0 rejected"); + CHECK(adc_cal_two_point(100, 200, 50000L, 50000L, k0, &rq, &bb) == -1, "dv=0 rejected"); + CHECK(adc_cal_two_point(100, 200, 100000L, 50000L, k0, &rq, &bb) == -1, "inverted slope rejected"); + CHECK(adc_cal_two_point(100, 200, 0L, 1000L, 0L, &rq, &bb) == -1, "k_q16 = 0 rejected"); + CHECK(adc_cal_two_point(100, 200, 0L, 1000L, 100000001L, &rq, &bb) == -1, "k_q16 > 1e8 rejected"); + CHECK(adc_cal_two_point(100, 200, 0L, 1000L, k0, NULL, &bb) == -1, "NULL r rejected"); + CHECK(adc_cal_two_point(100, 200, 0L, 1000L, k0, &rq, NULL) == -1, "NULL b rejected"); + /* v 超 +-1e9 uV(= +-1000 V):几乎必然把 mV/码值当 uV 填了 */ + CHECK(adc_cal_two_point(100, 200, 1000000000L, 1000100000L, k0, &rq, &bb) == -1, + "v beyond +1e9 rejected"); + CHECK(adc_cal_two_point(100, 200, -1000100000L, -1000000000L, k0, &rq, &bb) == -1, + "v beyond -1e9 rejected"); + /* 算出的变比 < 1.0(dc=100、k=1e7 时 dv 仅 1000 uV -> r 约 0.065)-> -1 */ + CHECK(adc_cal_two_point(100, 200, 0L, 1000L, k0, &rq, &bb) == -1, "computed r < 1.0 -> -1"); + /* 真零点 +9000 码 > B_MAX:斜率对(r = 1.5)但 b 越界 -> -2(先查前端,不用系数凑) */ + v1 = (int32_t)my_lround((100.0 + 9000.0) * ((double)k0 / 65536.0) * 1.5); + v2 = (int32_t)my_lround((2000.0 + 9000.0) * ((double)k0 / 65536.0) * 1.5); + CHECK(adc_cal_two_point(100, 2000, v1, v2, k0, &rq, &bb) == -2, "b beyond +-8192 -> -2"); + /* 真零点 +9000 码 > B_MAX:斜率对(r = 1.5)但 b 越界 -> -2(先查前端,不用系数凑) */ + v1 = (int32_t)my_lround((100.0 + 9000.0) * ((double)k0 / 65536.0) * 1.5); + v2 = (int32_t)my_lround((2000.0 + 9000.0) * ((double)k0 / 65536.0) * 1.5); + CHECK(adc_cal_two_point(100, 2000, v1, v2, k0, &rq, &bb) == -2, "b beyond +-8192 -> -2"); } /* TC-ADCCAL-016:贴轨门限表 */ @@ -423,14 +506,83 @@ 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"); + CHECK(adc_cal_set(ADC_SEM_UA, 12345678L, 5L) == 0, "pre-check tuned"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 1000), exp_q16(ADC_SEM_UA, 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(near_d(adc_cal_apply(ADC_SEM_UA, 1000), exp_eng(1000, ADC_SEM_UA)), "re-init back to ROM"); CHECK(adc_cal_flags(ADC_SEM_UA) == 0u, "re-init clears VALID too"); } -/* ===================== 用例(引擎集成组,标定落盘与整定入口 DoD) ===================== */ +/* TC-ADCCAL-024:变比维读数口初值 = ROM 默认(出厂即配,不是缺省 0) */ +static void tc_024_ratio_defaults(void) { + printf("[TC-ADCCAL-024] ratio readouts start at ROM defaults\n"); + reset_all(); + adc_cal_init(); + /* 电压族 1000.0 V/V、电流族 29.4118 A/V——十进制口径在此独立写出, + * Q16 原值由 比值 × 65536 就地算出(1927532 = 29.4118 x 65536 的回落) */ + CHECK(near_d((double)adc_cal_ratio(ADC_SEM_UA), EXP_R_VOLT), "UA ratio = 1000.0 V/V"); + CHECK(near_d((double)adc_cal_ratio(ADC_SEM_UN), EXP_R_VOLT), "UN ratio = 1000.0 V/V"); + CHECK(near_d((double)adc_cal_ratio(ADC_SEM_IA), EXP_R_CURR), "IA ratio = 29.4118 A/V"); + CHECK(near_d((double)adc_cal_ratio(ADC_SEM_IN), EXP_R_CURR), "IN ratio = 29.4118 A/V"); + CHECK(adc_cal_ratio_q16(ADC_SEM_UA) == (int32_t)my_lround(EXP_R_VOLT * 65536.0), + "UA ratio q16 = 65536000 (LSB exact)"); + CHECK(adc_cal_ratio_q16(ADC_SEM_IA) == (int32_t)my_lround(EXP_R_CURR * 65536.0), + "IA ratio q16 = 1927532 (LSB exact)"); + CHECK(adc_cal_ratio_configured(ADC_SEM_UA) == 1u, "UA ratio configured at boot"); + CHECK(adc_cal_ratio_configured(ADC_SEM_IA) == 1u, "IA ratio configured at boot"); + /* TEMP 不参与线性标定;越界通道同守卫 */ + CHECK((adc_cal_ratio(ADC_SEM_TEMP) == 0.0f) && (adc_cal_ratio_q16(ADC_SEM_TEMP) == 0) && + (adc_cal_ratio_configured(ADC_SEM_TEMP) == 0u), "TEMP ratio all zero"); + CHECK((adc_cal_ratio((adc_semantic_t)ADC_SEM_COUNT) == 0.0f) && + (adc_cal_ratio_q16((adc_semantic_t)ADC_SEM_COUNT) == 0) && + (adc_cal_ratio_configured((adc_semantic_t)ADC_SEM_COUNT) == 0u), + "OOB sem ratio all zero"); +} + +/* TC-ADCCAL-025:set_ratio(sem, 0) = 显式关掉本路折算(成功路径,不是拒绝) */ +static void tc_025_ratio_zero_disables(void) { + printf("[TC-ADCCAL-025] set_ratio(0) disables scaling and drops VALID\n"); + reset_all(); + adc_cal_init(); + /* 先走正规 k/b 整定把 VALID 点亮(r 出厂已配 → 三条件齐) */ + CHECK(adc_cal_set(ADC_SEM_UA, ADC_CAL_K_Q16_TPAFE, 0L) == 0, "seed k/b accepted"); + CHECK((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) != 0u, + "VALID lit with ROM ratio already configured"); + + CHECK(adc_cal_set_ratio(ADC_SEM_UA, 0L) == 0, "set_ratio(0) accepted (explicit off)"); + /* 折算退化到引脚域:期望 = 码值 × 10/65536,与变比无关 */ + CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 8192), exp_pin(8192, ADC_SEM_UA)), + "apply falls back to pin volts (x1000 gone)"); + CHECK((adc_cal_ratio(ADC_SEM_UA) == 0.0f) && (adc_cal_ratio_q16(ADC_SEM_UA) == 0), + "ratio readouts report 0 (not a third out-of-band state)"); + CHECK(adc_cal_ratio_configured(ADC_SEM_UA) == 0u, "ratio_configured = 0"); + CHECK((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) == 0u, + "VALID cleared by r = 0 (three-condition rule)"); + + /* 写回非 0 即恢复(r 与 k/b 无先后依赖,现算三条件) */ + CHECK(adc_cal_set_ratio(ADC_SEM_UA, (int32_t)my_lround(EXP_R_VOLT * 65536.0)) == 0, + "set_ratio back to 1000 accepted"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 8192), exp_eng(8192, ADC_SEM_UA)), + "apply scales again"); + CHECK((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) != 0u, "VALID back on"); +} + +/* TC-ADCCAL-026:set_ratio 的拒绝面(比值 < 1.0 / TEMP / 越界通道;RAM 不动) */ +static void tc_026_ratio_set_guards(void) { + float before; + + printf("[TC-ADCCAL-026] set_ratio rejects sub-1.0 / TEMP / OOB, RAM intact\n"); + reset_all(); + adc_cal_init(); + before = adc_cal_apply(ADC_SEM_UA, 1000); + CHECK(adc_cal_set_ratio(ADC_SEM_UA, 60000L) == -1, "ratio 0.9155 (< 1.0) rejected"); + CHECK(adc_cal_set_ratio(ADC_SEM_UA, -65536L) == -1, "negative ratio rejected"); + CHECK(adc_cal_set_ratio(ADC_SEM_TEMP, 65536000L) == -1, "TEMP rejected"); + CHECK(adc_cal_set_ratio((adc_semantic_t)ADC_SEM_COUNT, 65536000L) == -1, "OOB sem rejected"); + CHECK(adc_cal_apply(ADC_SEM_UA, 1000) == before, "RAM untouched after all rejects"); + CHECK(adc_cal_ratio_q16(ADC_SEM_UA) == (int32_t)my_lround(EXP_R_VOLT * 65536.0), + "UA ratio still ROM default after rejects"); +} /* TC-ADCCAL-010:文件值经 load 生效 + VALID */ static void tc_010_file_apply(void) { @@ -442,7 +594,7 @@ static void tc_010_file_apply(void) { 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)), + CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 6557), exp_q16(ADC_SEM_UA, 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"); /* 未列键保留默认 */ @@ -457,8 +609,8 @@ static void tc_011_file_bad(void) { 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(near_d(adc_cal_apply(ADC_SEM_UA, 500), exp_eng(500, ADC_SEM_UA)), "UA sanitized to ROM"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UB, 500), exp_eng(500, ADC_SEM_UB)), "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"); @@ -480,7 +632,7 @@ static void tc_012_file_corrupt(void) { 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(near_d(adc_cal_apply(ADC_SEM_UA, 4096), exp_eng(4096, ADC_SEM_UA)), "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"); } @@ -492,10 +644,44 @@ static void tc_013_section_missing(void) { 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(near_d(adc_cal_apply(ADC_SEM_IN, 2048), exp_eng(2048, ADC_SEM_IN)), "IN still ROM"); CHECK(adc_cal_flags(ADC_SEM_IA) == 0u, "no VALID with missing sections"); } +/* TC-ADCCAL-027:坏文件注入 [adc_cal_sec]——r 越带/为 0 都不折算、VALID 不亮 */ +static void tc_027_file_ratio_bad(void) { + printf("[TC-ADCCAL-027] bad [adc_cal_sec] entries -> pin volts, no VALID\n"); + reset_all(); + adc_cal_init(); + /* r_ua=0:该路退回引脚域;位图与 k 都合法,挡不到它——只有变比条件能挡 */ + vfs_put("[adc_cal]\r\nk_ua=10000000\r\nvalid=1\r\n" + "[adc_cal_sec]\r\nr_ua=0\r\n"); + CHECK(cfg_file_load() == 0, "load ok (r_ua = 0)"); + CHECK(adc_cal_ratio_q16(ADC_SEM_UA) == 0, "r_ua = 0 read back as 0"); + CHECK(adc_cal_ratio_configured(ADC_SEM_UA) == 0u, "r_ua = 0 -> not configured"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 500), exp_pin(500, ADC_SEM_UA)), + "r_ua = 0 -> apply stays in pin domain"); + CHECK((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) == 0u, + "r_ua = 0 vetoes VALID even with valid bit + k in band"); + /* 未列键保留 ROM 变比:坏一路不牵连别路 */ + CHECK(near_d((double)adc_cal_ratio(ADC_SEM_IA), EXP_R_CURR), "IA ratio still ROM default"); + CHECK(near_d(adc_cal_apply(ADC_SEM_IA, 500), exp_eng(500, ADC_SEM_IA)), + "IA still scales (only UA was zeroed)"); + + /* r_ua=1:比值 1/65536 < 1.0,同样不折算 + 不报 VALID */ + reset_all(); + adc_cal_init(); + vfs_put("[adc_cal]\r\nk_ua=10000000\r\nvalid=1\r\n" + "[adc_cal_sec]\r\nr_ua=1\r\n"); + CHECK(cfg_file_load() == 0, "load ok (r_ua = 1)"); + CHECK(adc_cal_ratio_q16(ADC_SEM_UA) == 0, "sub-1.0 ratio reported as 0 (no third state)"); + CHECK(adc_cal_ratio_configured(ADC_SEM_UA) == 0u, "sub-1.0 ratio -> not configured"); + CHECK(near_d(adc_cal_apply(ADC_SEM_UA, 500), exp_pin(500, ADC_SEM_UA)), + "sub-1.0 ratio -> apply stays in pin domain"); + CHECK((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) == 0u, "sub-1.0 ratio vetoes VALID"); + CHECK(g_caltest_err_count >= 1, "sub-1.0 ratio left its own ERR trace"); +} + /* TC-ADCCAL-018:未标定闭锁判定单点(「数据可信」未标定即闭锁 / 道F task-16) * 为什么值得单独钉:这条谓词此前在 App/task/protect_task.c 与 * App/app/ui_data_feed.c 各写一份,**没有任何一份被主机测到**(ui_data 套件只编 @@ -614,6 +800,10 @@ int main(void) { tc_013_section_missing(); tc_018_prot_blocked(); tc_019_023_persist_obs(); + tc_024_ratio_defaults(); + tc_025_ratio_zero_disables(); + tc_026_ratio_set_guards(); + tc_027_file_ratio_bad(); printf("\n=== ADC Cal Layer Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); diff --git a/test/host/cfg_file/cfg_file_test.c b/test/host/cfg_file/cfg_file_test.c index 903ec523..bbb5a683 100644 --- a/test/host/cfg_file/cfg_file_test.c +++ b/test/host/cfg_file/cfg_file_test.c @@ -435,8 +435,10 @@ static int register_fill(int n) return ok; } -/* TC-CONFIG-056/057:段数与键数容量钉(道G task-8——[comm] 上线后生产占 6/8, - * 容量与"满则拒"行为必须先在这里显式化,别让第 7 段的接入者在真机上才发现顶格) */ +/* TC-CONFIG-056/057:段数与键数容量钉(2026-11-11 [adc_cal_sec] 上线后生产已占满 + * 8/8:adc_cal / adc_cal_ofs / adc_cal_sec / run_par / comm / network / ftp / sntp, + * 与全仓 8 处 cfg_file_register() 调用一一对应——下一个受管段无处可放, + * 必须先在真机上暴露"满则拒"的语义,而不是等第 9 段的接入者现场发现顶格) */ static void tc_capacity(void) { printf("[TC-CONFIG-056] section capacity: MAX_SECTIONS pinned, 9th rejected\n"); @@ -444,8 +446,10 @@ static void tc_capacity(void) char nm[8]; cfg_section_reg_t r; - /* 数字钉:段上限必须是 8(cfg_file.h:39);改数字 = 容量台账变化,必须 - * 同步 docs/子系统设计/配置文件设计说明.md 与本用例生产占位说明 */ + /* 数字钉:段上限必须是 8(cfg_file.h:43);改数字 = 容量台账变化,必须 + * 同步 docs/子系统设计/配置文件设计说明.md 与本用例生产占位说明。 + * ⚠ 生产已占满 8/8(见本函数头注释)——这个数字一旦被改大, + * 同时要复核 [adc_cal_sec] 之后的段不会被静默拒 */ CHECK(CFG_FILE_MAX_SECTIONS == 8u, "CFG_FILE_MAX_SECTIONS pinned to 8"); vfs_reset(); CHECK(register_fill((int)CFG_FILE_MAX_SECTIONS) == 1, "filling exactly 8 sections all accepted"); diff --git a/test/host/prot_dim/prot_dim_test.c b/test/host/prot_dim/prot_dim_test.c index 6fe8a8b9..1ee136f9 100644 --- a/test/host/prot_dim/prot_dim_test.c +++ b/test/host/prot_dim/prot_dim_test.c @@ -15,6 +15,10 @@ * ROM 系数覆写(#undef/#define):把"板级理论系数"替换为合成端子侧口径 * (10 mV/码,K_DIM),模拟两点标定完成后的 ROM/默认状态——合法带随之 * 锚定合成值,adc_cal_set 走正规路径置 VALID,测试不白盒改引擎内存。 + * 变比 r 同步合成化为 1.0:本套件证的是「折算链别丢量纲」,不是变比数值; + * r = 1.0 让上面那对互逆换算(eng ↔ code)继续成立,也就与 + * docs/算法/采集与算法层设计说明.md §13.6 的合成口径一致。r ≠ 1.0 的 + * 放大效应单独由 TC-PROTDIM-011 钉(那时故意让判据输入被放大 1000 倍)。 * * 覆盖点(族 TC-HOST-PROTDIM,属主登记处 = 本目录): * TC-PROTDIM-001 标定链基线:0.5 A/100 V/0 V 注入 → 七类元件全静默(含 @@ -36,6 +40,11 @@ * 14.4 保持(ret 14.25)、14.1 释放 * TC-PROTDIM-010 聚合与独立性:OC1+OV 同拍动作位图 = 双位;撤 IA → * OC1 返回、OV 保持(互不污染) + * TC-PROTDIM-011 变比确实进折算(不是只存在配置里):把 IA 的 r 从合成 1.0 + * 改成 1000.0 后,同一批码值的判据输入被放大 1000 倍 → + * 0.5 A 立刻顶穿 OC1(10 A);改回 1.0 后恢复静默。 + * 反错配方向:若哪天有人把 r 从 apply() 里摘掉、只留配置段, + * 本用例立刻翻红。 * * 通过:全部 TC PASS(main 返回 0)。 * 创建日期:2026-10-25(道A 量纲回归用例) @@ -91,12 +100,17 @@ static int s_total = 0, s_passed = 0, s_failed = 0; * K_DIM = 655360000 Q16 = 10000 µV/码 = 10 mV/码: * 电压类 100 V rms → 峰值 141.42 V → 14142 码(TPAFE 半量程内,量级刻意 * 贴近端子变比两点标定完成后的真实形态);电流类二次值经变换器落到同一定标。 - * B_IA = -30 码(零点偏移,经 apply 精确对消——验证 b 参与全链而非仅落盘)。 */ + * B_IA = -30 码(零点偏移,经 apply 精确对消——验证 b 参与全链而非仅落盘)。 + * r 的合成口径 = 1.0(Q16 = 65536):见文件头「变比 r 同步合成化为 1.0」那段。 */ #include "adc_cal.h" #undef ADC_CAL_K_Q16_TPAFE #undef ADC_CAL_K_Q16_STM32 +#undef ADC_CAL_R_Q16_VOLT +#undef ADC_CAL_R_Q16_CURR #define ADC_CAL_K_Q16_TPAFE (655360000L) #define ADC_CAL_K_Q16_STM32 (655360000L) +#define ADC_CAL_R_Q16_VOLT (65536L) /* 合成口径 r = 1.0:本套件证"链条别丢量纲",不是变比数值 */ +#define ADC_CAL_R_Q16_CURR (65536L) #define K_DIM ADC_CAL_K_Q16_TPAFE #define B_IA_DIM (-30L) @@ -456,6 +470,42 @@ static void tc_010_mask(void) { CHECK(prot_elm_active_mask() == (1uL << PE_OV), "010: mask = OV only"); } +/* ===================== TC-PROTDIM-011 变比确实进折算 ===================== */ +/* 这条是「r 进折算」的反错配锁:配置段里写着 1000.0 还不够,必须证明它真的 + * 乘进了 adc_cal_apply——否则 r 就退化成「只存在配置里的一个数」,屏上与判据 + * 都会拿着引脚电压冒充二次值。手法:同一批码值,只改 IA 的变比,OC 判据 + * 收到的量必须恰好放大 1000 倍(期望值独立写出,不引实现常量)。 */ +static void tc_011_ratio_scales(void) { + uint32_t i; + double got; + + printf("[TC-PROTDIM-011] ratio enters apply(): 1000x on IA trips OC1\n"); + dim_setup(); + isolate_oc_family(); /* 只留 OC1(10 A,dly=0 出厂档) */ + + /* 基线:合成口径 r = 1.0 -> 0.5 A 远低于 OC1 定值,静默 */ + for (i = 0u; i < 45u; i++) { dim_beat(0.5, 100.0, 0.0); } + CHECK((pick_of(PE_OC1) == 0) && (assert_of(PE_OC1) == 0), + "011: r = 1.0 -> 0.5 A silent (baseline)"); + + /* 把 IA 变比改成 1000.0(Q16 = 1000 x 65536):同一码值,折算输出 x1000 */ + CHECK(adc_cal_set_ratio(ADC_SEM_IA, 65536000L) == 0, "011: IA ratio set to 1000.0"); + got = (double)adc_cal_apply(ADC_SEM_IA, dim_code(0.5, (double)B_IA_DIM)); + CHECK((got > 0.5 * 1000.0 * 0.999) && (got < 0.5 * 1000.0 * 1.001), + "011: apply(code for 0.5 A) = 500 A (ratio really multiplies)"); + + /* 同一物理注入现在应顶穿 OC1(10 A) —— 变比真到了判据手里 */ + for (i = 0u; i < 5u; i++) { dim_beat(0.5, 100.0, 0.0); } + CHECK((pick_of(PE_OC1) != 0) && (assert_of(PE_OC1) != 0), + "011: same 0.5 A now trips OC1(10 A) -> ratio reached the criteria"); + + /* 改回 1.0:判据看到的又只是 0.5 A,应当返回 */ + CHECK(adc_cal_set_ratio(ADC_SEM_IA, 65536L) == 0, "011: IA ratio back to 1.0"); + for (i = 0u; i < 45u; i++) { dim_beat(0.5, 100.0, 0.0); } + CHECK((pick_of(PE_OC1) == 0) && (assert_of(PE_OC1) == 0), + "011: ratio back to 1.0 -> OC1 returns (the effect is reversible)"); +} + int main(void) { printf("=== Prot Dim Regression Tests ===\n"); adc_cal_init(); /* 先把 RAM 系数置成合成 ROM(各 TC 内还会重跑) */ @@ -469,6 +519,7 @@ int main(void) { tc_008_uv(); tc_009_3u0(); tc_010_mask(); + tc_011_ratio_scales(); 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/run_par/run_par_test.c b/test/host/run_par/run_par_test.c index 1a51dade..b5c16f6f 100644 --- a/test/host/run_par/run_par_test.c +++ b/test/host/run_par/run_par_test.c @@ -15,12 +15,15 @@ * TC-RUNPAR-002 死区写往返 + 分档上限(V/A/Hz)+ NaN/负值拒 + 定点量化 * TC-RUNPAR-003 落盘链:set→脏→store_save 成功清脏 + 文件含 9 键 + * load 回读往返 + 外部改文件值生效 + 无脏 save 空操作 - * TC-RUNPAR-004 104 读映射:0x8030~0x8038 命中 / 空隙与邻段 -1 / k·b 回显 + * TC-RUNPAR-004 104 读映射:0x8030~0x8038 命中 / 空隙与邻段 -1 / k·b·r 回显 * TC-RUNPAR-005 104 两段式:选择段不提交不置脏,执行段提交置脏 * TC-RUNPAR-006 k/b 直通:合法带接受与拒绝 / 非正 k 拒 / b 单侧 RMW 保 k / * 非法带值经 load 注入后回显 = 生效值(哨兵视图) * TC-RUNPAR-007 init 幂等:重复调用复位默认 + 清脏;重注册被引擎拒(留痕) * TC-RUNPAR-008 引擎副本 = 生效值(无回调):load 原地改副本即时可见 + * TC-RUNPAR-009 变比 r 的 104 读写:0x8050~0x8057 写 2000.0 回读一致 / 写 0 + * 接受且回读 0 / 0.5 与 -1 与 40000 拒 / 0x8058 非本段 / 选择段 + * 粗守只挡 NaN·Inf(0.5 在选择段返 0,执行段才拒——两段可不同) * * 通过:全部 TC PASS(main 返回 0)。 * 创建日期:2026-10-25(道D 运行参数段) @@ -279,7 +282,11 @@ static void tc_104_read(void) "TC-RUNPAR-004 k echo last channel (IN, STM32 ROM)"); CHECK((run_par_104_read(0x8048u, &v) == 0) && (v == 0.0f), "TC-RUNPAR-004 b echo factory zero"); - CHECK(run_par_104_read(0x8050u, &v) == -1, "TC-RUNPAR-004 above b block not owned"); + CHECK((run_par_104_read(0x8050u, &v) == 0) && (fabsf(v - 1000.0f) <= 1e-3f), + "TC-RUNPAR-004 r echo first slot = ROM voltage family 1000.0"); + CHECK((run_par_104_read(0x8054u, &v) == 0) && (fabsf(v - 29.4118f) <= 1e-3f), + "TC-RUNPAR-004 r echo current family = 29.4118"); + CHECK(run_par_104_read(0x8058u, &v) == -1, "TC-RUNPAR-004 above r block not owned"); } static void tc_104_two_step(void) @@ -376,6 +383,80 @@ static void tc_copy_is_effective(void) CHECK(run_par_dirty_peek() == 0u, "TC-RUNPAR-008 load itself is not a local edit"); } +/* TC-RUNPAR-009:变比 r 的 104 读写(0x8050~0x8057)与两段式粗守面 */ +static void tc_104_ratio(void) +{ + float v; + + vfs_reset(); + reset_all_rp(); + /* 写 2000.0:电压族量级(2 倍于 ROM 默认 1000),回读 = 生效值 */ + CHECK(run_par_104_write(0x8050u, 2000.0f, 0u) == 0, "TC-RUNPAR-009 r 2000.0 accepted"); + CHECK((run_par_104_read(0x8050u, &v) == 0) && (fabsf(v - 2000.0f) <= 0.01f), + "TC-RUNPAR-009 r readback = 2000.0"); + CHECK((adc_cal_ratio(ADC_SEM_UA) > 1999.99f) && (adc_cal_ratio(ADC_SEM_UA) < 2000.01f), + "TC-RUNPAR-009 r reached adc_cal (write-through)"); + CHECK(run_par_dirty_peek() == 1u, "TC-RUNPAR-009 r execute marks dirty"); + + /* 写 0 = 显式关掉本路折算(成功路径,不是拒绝) */ + CHECK(run_par_104_write(0x8050u, 0.0f, 0u) == 0, "TC-RUNPAR-009 r 0 accepted (explicit off)"); + CHECK((run_par_104_read(0x8050u, &v) == 0) && (v == 0.0f), "TC-RUNPAR-009 r readback = 0"); + CHECK(adc_cal_ratio_configured(ADC_SEM_UA) == 0u, + "TC-RUNPAR-009 r 0 -> channel reports not configured"); + + /* 拒绝面:0 与 >=1.0 之外、以及非有限值 */ + CHECK(run_par_104_write(0x8050u, 0.5f, 0u) == -1, "TC-RUNPAR-009 r 0.5 rejected"); + CHECK(run_par_104_write(0x8050u, -1.0f, 0u) == -1, "TC-RUNPAR-009 r -1 rejected"); + CHECK(run_par_104_write(0x8050u, 40000.0f, 0u) == -1, "TC-RUNPAR-009 r 40000 rejected"); + { + volatile float fnan = 0.0f; + fnan = fnan / fnan; + CHECK(run_par_104_write(0x8050u, fnan, 0u) == -1, "TC-RUNPAR-009 r NaN rejected"); + CHECK(run_par_104_write(0x8050u, fnan, 1u) == -1, "TC-RUNPAR-009 r NaN rejected in select too"); + } + /* 上限端点(32767.99)在带内,接受 */ + CHECK(run_par_104_write(0x8050u, 32767.99f, 0u) == 0, "TC-RUNPAR-009 r upper band endpoint accepted"); + + /* 选择段:只粗守(NaN/Inf),0.5 这类"执行段会拒"的值在选择段返 0 */ + CHECK(run_par_104_write(0x8051u, 0.5f, 1u) == 0, + "TC-RUNPAR-009 select only guards finiteness (0.5 passes)"); + CHECK((run_par_104_read(0x8051u, &v) == 0) && (fabsf(v - 1000.0f) <= 1e-3f), + "TC-RUNPAR-009 select did not touch the ratio"); + CHECK(run_par_104_write(0x8051u, 0.5f, 0u) == -1, "TC-RUNPAR-009 execute rejects the same 0.5"); + CHECK((run_par_104_read(0x8051u, &v) == 0) && (fabsf(v - 1000.0f) <= 1e-3f), + "TC-RUNPAR-009 rejected execute left the ratio intact"); + + /* 段边界:0x8057 是 r 块末地址,0x8058 起非本段 */ + CHECK(run_par_104_write(0x8057u, 29.4118f, 0u) == 0, "TC-RUNPAR-009 last r slot writable"); + CHECK(run_par_104_write(0x8058u, 2.0f, 0u) == -2, "TC-RUNPAR-009 0x8058 passes through to prot_set"); + CHECK(run_par_104_read(0x8058u, &v) == -1, "TC-RUNPAR-009 0x8058 not owned on read"); + + /* 落盘往返:脏 → save → 文件带 r_ua 键 → load 回读生效 */ + CHECK(run_par_104_write(0x8050u, 2000.0f, 0u) == 0, "TC-RUNPAR-009 seed 2000.0 before save"); + CHECK(run_par_store_save() == 0, "TC-RUNPAR-009 save ok"); + CHECK((run_par_dirty_peek() == 0u) && (s_vfs_exists == 1), + "TC-RUNPAR-009 save cleared dirty and wrote file"); + CHECK(strstr((const char *)s_vfs, "r_ua=131072000") != NULL, + "TC-RUNPAR-009 file carries r_ua=131072000 (= 2000 x 65536)"); + (void)run_par_104_write(0x8050u, 1000.0f, 0u); /* 覆盖内存态 */ + CHECK(cfg_file_load() == 0, "TC-RUNPAR-009 engine reload ok"); + CHECK((run_par_104_read(0x8050u, &v) == 0) && (fabsf(v - 2000.0f) <= 0.01f), + "TC-RUNPAR-009 reload restores saved ratio"); + + /* 回显 = 生效值视图:文件塞越带 r(< 1.0)→ 哨兵按 0 报,不回显原始坏值 */ + { + static const char s_badr[] = + "[adc_cal_sec]\r\nr_ua=1\r\n"; + (void)memcpy(s_vfs, s_badr, sizeof(s_badr) - 1u); + s_vfs_len = sizeof(s_badr) - 1u; + s_vfs_exists = 1; + s_vfs_mtime++; + } + CHECK(cfg_file_load() == 0, "TC-RUNPAR-009 poisoned ratio file loaded"); + CHECK((run_par_104_read(0x8050u, &v) == 0) && (v == 0.0f), + "TC-RUNPAR-009 echo = effective (out-of-band reported as 0)"); +} + int main(void) { printf("=== RunPar Layer Host Tests ===\n"); @@ -387,6 +468,7 @@ int main(void) tc_104_cal_proxy(); tc_init_idempotent(); tc_copy_is_effective(); + tc_104_ratio(); printf("=== RunPar Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; diff --git a/test/host/wave_read.py b/test/host/wave_read.py index 6425d805..aa2e18ef 100644 --- a/test/host/wave_read.py +++ b/test/host/wave_read.py @@ -74,15 +74,18 @@ App/algo/wave_store.c must agree byte-for-byte -- if they ever diverge, report it, do not silently re-rule from one side): - CRC-16/MODBUS table-free (0x8005 reflected / init 0xFFFF / xorout 0), both CRC fields stored LOW byte first; anchor "123456789" -> 0x4B37. - Header CRC covers [0, 30 + 12*ch_count); data CRC covers the data area - and sits at end of file; 8ch/320pts whole segment = 5250 B. - - Channel table entry (12 B): sem 1 + src 1 + bits 1 + availability 1 + Header CRC covers [0, 30 + 16*ch_count); data CRC covers the data area + and sits at end of file; 8ch/320pts whole segment = 5282 B. + - Channel table entry (16 B, v2): sem 1 + src 1 + bits 1 + availability 1 (= adc_cal_flags(sem) low 8 bits, raw, no invented bit meanings) + - k_q16 i32 (uV/code * 65536) + b_codes i32 (code domain). There is NO + k_q16 i32 (uV/code * 65536) + b_codes i32 (code domain) + + r_q16 i32 (ratio * 65536, 0 = no scaling on this channel). There is NO separate LSB field: LSB == k_q16, uV = k_q16/65536, never re-scaled. - mV = (code + b_codes) * k_q16/65536/1000 -- exactly the - adc_cal_apply() formula. Header eng only LABELS the unit (0 = - pin-domain, 1 secondary, 2 primary); it never changes arithmetic. + mV = (code + b_codes) * k_q16/65536/1000, then * r_q16/65536 when + r_q16 != 0 -- exactly the adc_cal_apply() formula, computed in the same + TWO integer steps as the C side (one expression overflows int64). + Header eng only LABELS the unit (0 = pin-domain, 1 = secondary, + 2 = primary); it never changes arithmetic. - flags_seq bit s = ADC_CAL_F_VALID of sem s; clip_mask bit s = sem s (same mapping as App/algo/meas.h; V0.1 constant 0xE6 = UB UC IB IC IN). avail is the RAW pass-through of adc_cal_flags(): VALID and CLIPPED are @@ -91,17 +94,19 @@ report it, do not silently re-rule from one side): VALID/CLIPPED relation nor on avail-vs-clip_mask drift -- those get a soft note only ("VALID+CLIPPED(both)" in dump/fft, a WARN line in dump), no exit-code change, nothing refused. - eng likewise derives from flags_seq on the board (feed_eng(): all 8 - electrical channels VALID -> secondary(1), else pin-domain(0)); a - pairing mismatch here is also a SOFT WARN only, never a rejection. - - v1 pins ch_count = 8 (TEMP not recorded; TEMP needs a version bump - together with the TEMP-in-flags question); PC must verify table entry + eng derives from the per-channel RATIO on the board (feed_eng(): every + electrical channel either has r configured or is clipped-and-not-shown); + a pairing mismatch here is also a SOFT WARN only, never a rejection. + - v1/v2 pins ch_count = 8 (TEMP not recorded; TEMP needs a version bump + together with the TEMP-in-flags question); v2 = v1 + i32 r_q16 per + channel-table entry (12 -> 16 B, head 128 -> 160, segment 5250 -> 5282). + PC must verify table entry i carries sem == i (ascending UA..IN), mismatch voids the whole segment; data column i == table entry i; samples int16 LE, TPAFE two's complement, STM32 0..4095 stored positive with NO midpoint bias (biasing it would fabricate bipolarity). - t0_ms / wall / frame_seq are u32 little-endian. ver must equal magic - byte 2 and both must equal 1, else refuse. + byte 2 and both must equal FMT_VER, else refuse. """ import argparse import math @@ -114,7 +119,7 @@ import tempfile # ---- format constants (design section 6.2, little-endian) ------------------- MAGIC_W = 0x57 # 'W' MAGIC_R = 0x52 # 'R' -FMT_VER = 1 # byte 2 of magic AND byte 3 (ver) both == 1 +FMT_VER = 2 # byte 2 of magic AND byte 3 (ver) both == 2 SEG_LABEL_SAMPLE = "WAVE%02d" % 0 # sample segment label, produced by format so # this file never carries a hardcoded letter+digit identifier token @@ -133,10 +138,11 @@ OFF_FLAGS = 23 # 1 validity bitmap (see Q5) OFF_CLIP = 24 # 2 clip bitmap, bit s = sem s (see Q6) OFF_FSEQ = 26 # 4 frame_seq at segment head HDR_FIXED = 30 # bytes before channel table -ENT_SIZE = 12 # bytes per channel-table entry +ENT_SIZE = 16 # bytes per channel-table entry (v2: 12 -> 16) ENT_SEM, ENT_SRC, ENT_BITS, ENT_AVAIL = 0, 1, 2, 3 ENT_KQ = 4 # i32 Q16 uV/code == the LSB (sections 6.2/6.3) ENT_B = 8 # i32 code-domain zero offset +ENT_RQ16 = 12 # i32 Q16 ratio (terminal/primary ratio); 0 = no scaling SRC_TPAFE, SRC_STM32 = 0, 1 SRC_NAMES = {SRC_TPAFE: "TPAFE5160", SRC_STM32: "STM32-ADC1"} @@ -198,17 +204,55 @@ def put_crc16(value): # ---- decode helpers ---------------------------------------------------------- def lsb_uv(ent): """LSB in micro-volts per code = k_q16 / 65536 (section 6.3: k_q16 IS - the LSB; never re-multiplied).""" + the LSB; never re-multiplied). This is the PIN-side slope only -- the + terminal ratio (r_q16) is a separate factor, see code_to_mv().""" return ent["k_q16"] / 65536.0 +def ent_scale_mv(ent): + """Floating-point mV per code in the RECORDING DOMAIN (test expectation + helper): k slope via lsb_uv(), times the Q16 ratio when r_q16 != 0. + Analytic expectations (e.g. sine RMS) use this so they stay in step with + code_to_mv() without duplicating the two-step integer rounding.""" + base = lsb_uv(ent) / 1000.0 + if ent["r_q16"] == 0: + return base + return base * ent["r_q16"] / 65536.0 + + def code_to_mv(ent, code): - """Code -> mV via exactly the adc_cal_apply() formula: - value = (code + b) * k[us/code] * 1e-6 [V] -> mV = that * 1000. - The header eng field only labels the unit (0 = pin-domain); per - section 6.3 it must never change this arithmetic, and the PC must not - re-guess units from the current firmware state.""" - return (code + ent["b_codes"]) * lsb_uv(ent) / 1000.0 + """Code -> engineering millivolt, mathematically identical to the C side + (App/algo/wave_store.c wave_store_code_to_milli); a divergent rounding + rule would make the two independent implementations disagree on the last + digit of every sample, so the rule is mirrored on purpose. + + t = (code + b_codes) * k_q16 uV * 65536, up to ~2.2e12 + r_q16 == 0 -> mv = round_half_away(t / 65536000) (= 1000 * 2**16) + r_q16 != 0 -> mv = round_half_away(t * r_q16 / (1000 * 2**32)) + + One rounding, at the end, in the recording DOMAIN: secondary mV/mA when + r_q16 != 0, pin mV when r_q16 == 0. The header eng field only labels + which of those it is; it never changes this arithmetic. + + Why the C side cannot write that one expression: t * r_q16 reaches + 4.6e21, past int64 (9.2e18), so it splits t = 1000*q + s and recombines + the remainders -- algebraically the same single rounding. Python has + arbitrary-precision ints, so this reader evaluates the exact value and + keeps the two implementations in step. + """ + t = (code + ent["b_codes"]) * ent["k_q16"] + r = ent["r_q16"] + if r == 0: + return _div_round(t, 65536000) + return _div_round(t * r, 1000 * (1 << 32)) + + +def _div_round(num, den): + """Integer division rounding half AWAY from zero -- mirrors + wave_store.c wave_st_div_round() including the negative branch.""" + if num >= 0: + return (num + den // 2) // den + return -((-num + den // 2) // den) def rail_thresholds(ent): @@ -303,6 +347,7 @@ def parse_segment(buf, name="segment"): "avail": buf[o + ENT_AVAIL], "k_q16": struct.unpack_from("ch_count; i++) { + if (((p_m->ch[i].avail & 0x02u) == 0u) && (p_m->ch[i].r_q16 == 0)) { + all_ok = 0u; + } + } + return (all_ok != 0u) ? (uint8_t)WAVE_ENG_SEC : (uint8_t)WAVE_ENG_PIN; } static int eng_selfconsistent(const wave_seg_meta_t *p_m) { - return (p_m->eng == expect_eng(p_m->flags_seq)) ? 1 : 0; + return (p_m->eng == expect_eng(p_m)) ? 1 : 0; +} + +/* 字节级:把整头解出来再看派生式是否成立(判据只看已落进字节的事实) */ +static int head_eng_consistent(const uint8_t *p_head) { + wave_seg_meta_t m; + + if (wave_store_head_parse(p_head, (uint32_t)WAVE_ST_HEAD_SZ_CUR, &m) != WAVE_ST_OK) { + return 0; + } + return (m.eng == expect_eng(&m)) ? 1 : 0; } /* 一份「像样的」段头:格式侧默认值 + 封存瞬间事实(与 PC 侧口径一致) */ @@ -104,6 +125,11 @@ static void fill_meta(wave_seg_meta_t *p_m) { * 解码器不据此判废(lead 裁定 2026-10-29)。削顶位与 clip_mask 的对应关系仍要保持 * 自洽(bit1 = 削顶,聚合起来正好 0xE6),那是数据自身的一致性,不是格式不变式。 */ p_m->ch[i].avail = (uint8_t)(tpa ? 0x01u : 0x03u); + /* v2:变比维。本份「已标定后仍削顶」形态里,非削顶的三路 TPAFE 已配变比 + * (ROM 电压族 65536000 = 1000.0),削顶的五路片内 ADC 也照 ROM 电流族 + * (1927532)填上 —— 削顶与否不影响整帧量纲断言,但填了才与 eng = 二次值 + * 自洽(见 expect_eng)。真值形态(一路都没配变比、eng = 引脚域)由 v01 覆盖。 */ + p_m->ch[i].r_q16 = tpa ? 65536000 : 1927532; } } @@ -206,7 +232,8 @@ static int meta_same(const wave_seg_meta_t *p_a, const wave_seg_meta_t *p_b) { for (i = 0u; i < p_a->ch_count; i++) { if ((p_a->ch[i].sem != p_b->ch[i].sem) || (p_a->ch[i].conv != p_b->ch[i].conv) || (p_a->ch[i].bits != p_b->ch[i].bits) || (p_a->ch[i].avail != p_b->ch[i].avail) || - (p_a->ch[i].k_q16 != p_b->ch[i].k_q16) || (p_a->ch[i].b_codes != p_b->ch[i].b_codes)) { + (p_a->ch[i].k_q16 != p_b->ch[i].k_q16) || (p_a->ch[i].b_codes != p_b->ch[i].b_codes) || + (p_a->ch[i].r_q16 != p_b->ch[i].r_q16)) { return 0; } } @@ -241,23 +268,23 @@ int main(void) { /* ---------- 用例:尺寸等式 ---------- */ printf("-- 用例:尺寸等式 --\n"); - CHECK(WAVE_ST_HEAD_CRC_POS(8u) == 126u, "头 CRC 位置 = 30 + 12*8"); - CHECK(WAVE_ST_HEAD_BYTES(8u) == 128u, "整头 = 128 字节"); + CHECK(WAVE_ST_HEAD_CRC_POS(8u) == 158u, "头 CRC 位置 = 30 + 16*8"); + CHECK(WAVE_ST_HEAD_BYTES(8u) == 160u, "整头 = 160 字节"); CHECK(WAVE_ST_HEAD_SZ_CUR == WAVE_ST_HEAD_BYTES((uint8_t)WAVE_ST_CH_NUM), "当前值宏与参数化宏同源"); CHECK(WAVE_ST_DATA_BYTES(8u, 320u) == 5120u, "数据区 = 5120 字节"); - CHECK(WAVE_ST_SEG_TOTAL(8u, 320u) == 5250u, "整段 = 5250 字节"); + CHECK(WAVE_ST_SEG_TOTAL(8u, 320u) == 5282u, "整段 = 5282 字节"); CHECK(WAVE_ST_SEG_SZ_CUR == WAVE_ST_SEG_TOTAL((uint8_t)WAVE_ST_CH_NUM, (uint16_t)WAVE_ST_PTS_DEF), "整段当前值宏与参数化宏同源"); - CHECK((32u + (12u * 8u) + (320u * 8u * 2u) + 2u) == 5250u, - "等式 32 + 12*ch + pts*ch*2 + 2 == 5250(接线道拿它比对 f_write 写回数)"); - CHECK(WAVE_ST_SEG_TOTAL(9u, 1000u) == (32u + 108u + 18000u + 2u), "9 路 1000 点变体同式(每周波点数是数据)"); + CHECK((32u + (16u * 8u) + (320u * 8u * 2u) + 2u) == 5282u, + "等式 32 + 16*ch + pts*ch*2 + 2 == 5282(接线道拿它比对 f_write 写回数)"); + CHECK(WAVE_ST_SEG_TOTAL(9u, 1000u) == (32u + 144u + 18000u + 2u), "9 路 1000 点变体同式(每周波点数是数据)"); fill_meta(&s_meta); - CHECK(wave_store_seg_size(&s_meta) == 5250u, "seg_size 按头字段算出 5250"); - CHECK(wave_store_head_bytes(8u) == 128u, "head_bytes(8)"); + CHECK(wave_store_seg_size(&s_meta) == 5282u, "seg_size 按头字段算出 5282"); + CHECK(wave_store_head_bytes(8u) == 160u, "head_bytes(8)"); CHECK((wave_store_head_bytes(0u) == 0u) && (wave_store_head_bytes(10u) == 0u), "非法通道数返回 0"); - CHECK(wave_store_data_off(8u, 320u, 0u, 0u) == 128u, "第 0 列点 0 从整头之后开始"); - CHECK(wave_store_data_off(8u, 320u, 1u, 0u) == (128u + 640u), "按通道分列:第 1 列 = 128 + 320*2"); - CHECK(wave_store_data_off(8u, 320u, 7u, 319u) == (5250u - 2u - 2u), "最后一列最后一点贴数据 CRC"); + CHECK(wave_store_data_off(8u, 320u, 0u, 0u) == 160u, "第 0 列点 0 从整头之后开始"); + CHECK(wave_store_data_off(8u, 320u, 1u, 0u) == (160u + 640u), "按通道分列:第 1 列 = 160 + 320*2"); + CHECK(wave_store_data_off(8u, 320u, 7u, 319u) == (5282u - 2u - 2u), "最后一列最后一点贴数据 CRC"); CHECK(wave_store_data_off(0u, 320u, 0u, 0u) == 0xFFFFFFFFu, "通道数非法 -> 哨兵"); CHECK(wave_store_data_off(8u, 320u, 8u, 0u) == 0xFFFFFFFFu, "列号越界 -> 哨兵"); CHECK(wave_store_data_off(8u, 320u, 0u, 320u) == 0xFFFFFFFFu, "点越界 -> 哨兵"); @@ -286,13 +313,14 @@ int main(void) { if ((s_back.ch[i].conv != want_conv[i]) || (s_back.ch[i].bits != want_bits[i])) { ok_map = 0; } - if ((s_back.ch[i].avail != 0u) || (s_back.ch[i].k_q16 != 0) || (s_back.ch[i].b_codes != 0)) { + if ((s_back.ch[i].avail != 0u) || (s_back.ch[i].k_q16 != 0) || + (s_back.ch[i].b_codes != 0) || (s_back.ch[i].r_q16 != 0)) { ok_zero = 0; } } CHECK(ok_sem != 0, "sem = 下标(PC 侧必须校验的一条)"); CHECK(ok_map != 0, "conv/bits 映射:UA UN IA = TPAFE 16 位,其余五路 = 片内 ADC 12 位"); - CHECK(ok_zero != 0, "avail / k_q16 / b_codes 留 0 等 adcTask 现取现填(本层不链 adc_cal)"); + CHECK(ok_zero != 0, "avail / k_q16 / b_codes / r_q16 留 0 等 adcTask 现取现填(本层不链 adc_cal)"); } { uint16_t m = (uint16_t)((1u << 1) | (1u << 2) | (1u << 5) | (1u << 6) | (1u << 7)); @@ -326,6 +354,8 @@ int main(void) { for (i = 0u; i < v01.ch_count; i++) { /* 三路 TPAFE 既不VALID也不削顶;五路片内 ADC 削顶但未标定 */ v01.ch[i].avail = (uint8_t)(((i == 0u) || (i == 3u) || (i == 4u)) ? 0x00u : 0x02u); + /* 真值形态:一路都没配变比(V0.1 无 CT/PT 变比),与 eng = 引脚域自洽 */ + v01.ch[i].r_q16 = 0; } /* 注意:head_build 成功时返回的是写出的字节数(不是 WAVE_ST_OK)—— * 与 wave_ring_read_ch 同一类「返回实际长度」的契约 */ @@ -361,25 +391,44 @@ int main(void) { } } } - CHECK(buf[WAVE_ST_OFF_ENG] == expect_eng(buf[WAVE_ST_OFF_FLAGS]), - "构造形态落进字节之后 eng 与逐路有效位图仍自洽"); + CHECK(head_eng_consistent(buf) != 0, + "构造形态落进字节之后 eng 与逐路变比仍自洽"); fill_meta(&v01); v01.flags_seq = 0x00u; v01.eng = (uint8_t)WAVE_ENG_PIN; + for (i = 0u; i < v01.ch_count; i++) { + v01.ch[i].avail = (uint8_t)(((i == 0u) || (i == 3u) || (i == 4u)) ? 0x00u : 0x02u); + v01.ch[i].r_q16 = 0; /* 一路都没配变比 -> 引脚域 */ + } if ((wave_store_head_build(&v01, buf, (uint32_t)sizeof(buf)) == (int)WAVE_ST_HEAD_SZ_CUR) && (buf[WAVE_ST_OFF_ENG] == (uint8_t)WAVE_ENG_PIN) && (buf[WAVE_ST_OFF_FLAGS] == 0x00u)) { - CHECK(buf[WAVE_ST_OFF_ENG] == expect_eng(buf[WAVE_ST_OFF_FLAGS]), - "本波真值形态落进字节之后 eng 仍停引脚域(0x00 不是 0xFF,不许谎报二次值)"); + CHECK(head_eng_consistent(buf) != 0, + "本波真值形态落进字节之后 eng 仍停引脚域(变比全 0,不许谎报二次值)"); } else { CHECK(0, "本波真值形态编码后 eng 与逐路有效位图字节不符"); } - CHECK(expect_eng(0xFFu) == (uint8_t)WAVE_ENG_SEC, "派生式:电气 8 路全有效才升二次值档位"); - CHECK(expect_eng(0x00u) == (uint8_t)WAVE_ENG_PIN, "派生式:一路都没标定则停引脚域"); - CHECK(expect_eng(0x7Fu) == (uint8_t)WAVE_ENG_PIN, "派生式:部分标定(缺 IN 那一路)整帧仍算引脚域"); - CHECK(eng_selfconsistent(&v01) != 0, "夹具自洽:真值形态(全零 / 引脚域)"); - CHECK(eng_selfconsistent(&both) != 0, "夹具自洽:构造形态(全有效 / 二次值)"); + CHECK(eng_selfconsistent(&v01) != 0, "夹具自洽:真值形态(变比全 0 / 引脚域)"); + CHECK(eng_selfconsistent(&both) != 0, "夹具自洽:构造形态(变比全配 / 二次值)"); CHECK(eng_selfconsistent(&s_meta) != 0, "夹具自洽:本套所有合成段共用的那份段头"); + /* 派生式的三档探针(用段头对象探,不依赖字节):元本体是「每路要么已配变比、 + * 要么削顶不显示」——削顶那几路配不配变比都不影响结论 */ + { + wave_seg_meta_t p; + + fill_meta(&p); + CHECK(expect_eng(&p) == (uint8_t)WAVE_ENG_SEC, + "派生式:非削顶的八路都配了变比 -> 二次值档位"); + fill_meta(&p); + p.ch[7].avail = 0x00u; /* IN 非削顶 */ + p.ch[7].r_q16 = 0; /* 且没配变比 */ + CHECK(expect_eng(&p) == (uint8_t)WAVE_ENG_PIN, + "派生式:一路非削顶且无变比 -> 整帧仍算引脚域"); + fill_meta(&p); + p.ch[7].r_q16 = 0; /* 只清变比,仍削顶 */ + CHECK(expect_eng(&p) == (uint8_t)WAVE_ENG_SEC, + "派生式:削顶那路配不配变比都不影响(页面本来就显 '-',不参与整帧量纲断言)"); + } /* 反证本层**不参与**单位标签派生:故意造一份违反派生式的段头(全有效却仍标引脚域), * 编码与解码都该照样通过——判废只留给完整性判据,派生式不一致是 PC 侧软告警的活。 * 这份只当探针用,绝不写进任何互验产物(真机写不出这串字节) */ @@ -409,24 +458,38 @@ int main(void) { fill_meta(&s_meta); s_meta.ch[0].k_q16 = 10000000; s_meta.ch[0].b_codes = -8192; + s_meta.ch[0].r_q16 = 0; s_meta.ch[7].k_q16 = 52800000; s_meta.ch[7].b_codes = 8191; + /* v2:变比原值往返(含 ROM 默认的电压族 65536000 = 1000.0 V/V 与电流族 + * 1927532 = 29.4118 A/V);0 也要能往返(= 本路不折算,不是缺省) */ + s_meta.ch[0].r_q16 = 65536000; + s_meta.ch[1].r_q16 = 1927532; + s_meta.ch[2].r_q16 = 0; s_meta.wall = 0xFFFFFFFFu; s_meta.frame_seq = 0u; { uint32_t hn = build_head(); - CHECK(hn == 128u, "head_build 产出整头 128 字节"); + CHECK(hn == 160u, "head_build 产出整头 160 字节"); CHECK((s_img[0] == (uint8_t)WAVE_ST_MAGIC0) && (s_img[1] == (uint8_t)WAVE_ST_MAGIC1) && - (s_img[2] == (uint8_t)WAVE_ST_VER), "魔数 = 57 52 01(第三字节就是版本号)"); + (s_img[2] == (uint8_t)WAVE_ST_VER), "魔数 = 57 52 02(第三字节就是版本号)"); + CHECK(WAVE_ST_VER == 2u, "格式版本 = 2(通道表 16 字节/路)"); CHECK(s_img[WAVE_ST_OFF_VER] == s_img[WAVE_ST_OFF_MAGIC + 2u], "ver 字段与魔数第三字节同值"); - CHECK((s_img[30u + (12u * 8u)] != 0u) || (s_img[31u + (12u * 8u)] != 0u), "头 CRC 落在 126/127"); + CHECK((s_img[30u + (16u * 8u)] != 0u) || (s_img[31u + (16u * 8u)] != 0u), "头 CRC 落在 158/159"); { - uint16_t crc = wave_store_crc(s_img, 126u); - CHECK((s_img[126] == (uint8_t)(crc & 0xFFu)) && (s_img[127] == (uint8_t)(crc >> 8)), + uint16_t crc = wave_store_crc(s_img, 158u); + CHECK((s_img[158] == (uint8_t)(crc & 0xFFu)) && (s_img[159] == (uint8_t)(crc >> 8)), "头 CRC 低字节在前(§6.3)"); } CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_OK, "head_parse OK"); - CHECK(meta_same(&s_meta, &s_back) != 0, "13 个标量 + 8 项表逐字段往返一致(含负系数)"); + CHECK(meta_same(&s_meta, &s_back) != 0, "13 个标量 + 8 项表逐字段往返一致(含负系数与 r_q16)"); + /* v2 新增断言:段头往返后 r_q16 原值不变(int32 原值,不经 float) */ + CHECK((s_back.ch[0].r_q16 == 65536000) && (s_back.ch[1].r_q16 == 1927532) && + (s_back.ch[2].r_q16 == 0), "段头往返后 r_q16 原值不变(1000.0 / 29.4118 / 不折算)"); + /* r_q16 落在 12 字节偏移、小端 int32 */ + CHECK((s_img[30u + WAVE_ST_CH_OFF_RQ16 + 0u] == 0x00u) && + (s_img[30u + WAVE_ST_CH_OFF_RQ16 + 3u] == 0x03u), + "r_q16 在表项偏移 12、小端(65536000 = 0x03E80000)"); } /* ---------- 用例:拒绝清单 ---------- */ @@ -476,7 +539,7 @@ int main(void) { m.trig_src = 5u; CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "触发源 5 拒"); fill_meta(&m); - CHECK(wave_store_head_build(&m, s_img, 127u) == WAVE_ST_E_PARAM, "缓冲装不下整头拒"); + CHECK(wave_store_head_build(&m, s_img, 159u) == WAVE_ST_E_PARAM, "缓冲装不下整头拒"); CHECK(wave_store_head_build(NULL, s_img, IMG_CAP) == WAVE_ST_E_PARAM, "空段头指针拒"); CHECK(wave_store_seg_size(NULL) == 0u, "seg_size 空指针返 0"); } @@ -485,18 +548,18 @@ int main(void) { { uint32_t hn = build_head(); uint8_t save = s_img[0]; - CHECK(hn == 128u, "参考头重编 OK"); + CHECK(hn == 160u, "参考头重编 OK"); s_img[0] = (uint8_t)'X'; CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_FORMAT, "魔数第一字节错 -> 格式错"); s_img[0] = save; - s_img[WAVE_ST_OFF_VER] = 2u; + s_img[WAVE_ST_OFF_VER] = 3u; CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_FORMAT, "ver 与魔数第三字节不一致 -> 格式错(不许按新布局猜)"); s_img[WAVE_ST_OFF_VER] = (uint8_t)WAVE_ST_VER; - s_img[WAVE_ST_OFF_MAGIC + 2u] = 2u; - CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_FORMAT, "版本号 2 -> 拒"); + s_img[WAVE_ST_OFF_MAGIC + 2u] = 1u; + CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_FORMAT, "版本号 1(老 v1 段)-> 拒"); s_img[WAVE_ST_OFF_MAGIC + 2u] = (uint8_t)WAVE_ST_VER; - CHECK(wave_store_head_parse(s_img, 127u, &s_back) == WAVE_ST_E_PARAM, "缓冲不足整头 -> 参数错"); + CHECK(wave_store_head_parse(s_img, 159u, &s_back) == WAVE_ST_E_PARAM, "缓冲不足整头 -> 参数错"); CHECK(wave_store_head_parse(s_img, 7u, &s_back) == WAVE_ST_E_PARAM, "连通道数字节都读不到 -> 参数错"); s_img[40] = (uint8_t)(s_img[40] ^ 0xA5u); CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_CRC, "头覆盖区被改一字节 -> CRC 不符"); @@ -520,7 +583,8 @@ int main(void) { { wave_ch_desc_t d; int32_t mv = 0; - d.sem = 0u; d.conv = 0u; d.bits = 16u; d.avail = 1u; d.k_q16 = 10000000; d.b_codes = 0; + d.sem = 0u; d.conv = 0u; d.bits = 16u; d.avail = 1u; + d.k_q16 = 10000000; d.b_codes = 0; d.r_q16 = 0; CHECK(wave_store_code_to_milli(&d, 0, &mv) == WAVE_ST_OK, "TPAFE 零点折算 OK"); CHECK(mv == 0, "零点 = 0 mV"); (void)wave_store_code_to_milli(&d, 9261, &mv); @@ -539,6 +603,36 @@ int main(void) { CHECK(mv == 0, "系数没填时幅值归零(形状仍在,PC 侧会看到平线)"); CHECK(wave_store_code_to_milli(NULL, 0, &mv) == WAVE_ST_E_PARAM, "空表项拒"); CHECK(wave_store_code_to_milli(&d, 0, NULL) == WAVE_ST_E_PARAM, "空输出拒"); + + /* ---- v2:变比维(口径锚点三条,都必须与 PC 解码器同解) ---- */ + d.k_q16 = 10000000; + d.b_codes = 0; + (void)wave_store_code_to_milli(&d, 9261, &mv); + { + int32_t pin = mv; /* r_q16 = 0:引脚域基准 */ + d.r_q16 = 65536; /* 比值 1.0 */ + (void)wave_store_code_to_milli(&d, 9261, &mv); + CHECK((pin == 1413) && (mv == pin), "r_q16 = 65536(1.0)与 r_q16 = 0 的引脚域逐位一致"); + d.r_q16 = 0; + (void)wave_store_code_to_milli(&d, -9261, &mv); + CHECK(mv == -1413, "r_q16 = 0 退化成引脚域(负半周同理)"); + d.r_q16 = 65536; + (void)wave_store_code_to_milli(&d, -9261, &mv); + CHECK(mv == -1413, "r_q16 = 1.0 时负半周同样与引脚域一致(不是 -1414)"); + } + d.r_q16 = 65536000; /* 1000.0 V/V:ROM 电压族默认 */ + (void)wave_store_code_to_milli(&d, 9261, &mv); + /* 精确值 = 1413.116 mV × 1000 = 1413116(**不是** 先圆整再乘的 1413000 —— + * 这正是「只在最后圆整一次」的可观测证据:中间圆整会少掉 116 个毫单位) */ + CHECK(mv == 1413116, "r_q16 = 65536000(1000.0)时放大 1000 倍(1413.116 -> 1413116)"); + (void)wave_store_code_to_milli(&d, -9261, &mv); + CHECK(mv == -1413116, "放大 1000 倍在负半周同样成立(符号不错)"); + d.r_q16 = 1927532; /* 29.4118 A/V:ROM 电流族默认 */ + d.k_q16 = 10000000; + (void)wave_store_code_to_milli(&d, 9261, &mv); + /* 1413 mV * 29.4118 = 41562 毫单位(二次域 mA) */ + CHECK(mv == 41562, "r_q16 = 1927532(29.4118)时 9261 码 -> 41562 毫单位(二次 mA)"); + d.r_q16 = 0; } /* ---------- 用例:打包游标 ---------- */ @@ -550,15 +644,15 @@ int main(void) { s_read_calls = 0u; s_read_points = 0u; CHECK(pack_whole(256u, &len, &rc) == 0, "整段跑完(256 字节分片)"); - CHECK(s_img[WAVE_ST_OFF_ENG] == expect_eng(s_img[WAVE_ST_OFF_FLAGS]), - "整段头字节自洽:写出去的 eng 与逐路有效位图满足平台派生式"); - CHECK(len == 5250u, "实际产出字节数 = 5250"); + CHECK(head_eng_consistent(s_img) != 0, + "整段头字节自洽:写出去的 eng 与逐路变比满足平台派生式"); + CHECK(len == 5282u, "实际产出字节数 = 5282"); CHECK(s_read_points == (320u * 8u), "数据源被取的点数 = 320*8(不多取不少取)"); CHECK(s_read_calls == 40u, "每列 5 批 x 8 列 = 40 次回调(批量取数,不是逐点)"); CHECK(wave_store_seg_check(s_img, len, &s_back) == WAVE_ST_OK, "自产字节过整段自检"); CHECK(meta_same(&s_meta, &s_back) != 0, "自检回填的段头与原段头一致"); { - uint16_t dcrc = wave_store_crc(&s_img[128], 5120u); + uint16_t dcrc = wave_store_crc(&s_img[160], 5120u); CHECK((s_img[len - 2u] == (uint8_t)(dcrc & 0xFFu)) && (s_img[len - 1u] == (uint8_t)(dcrc >> 8)), "数据 CRC 覆盖数据区、低字节在前、贴在文件尾"); } @@ -566,8 +660,8 @@ int main(void) { { uint32_t len = 0u; int rc = 0; - CHECK(pack_whole(128u, &len, &rc) == 0, "128 字节分片(= 整头那么大)也能跑完"); - CHECK(len == 5250u, "128 分片总长仍 5250(产出与分片大小无关)"); + CHECK(pack_whole(160u, &len, &rc) == 0, "160 字节分片(= 整头那么大)也能跑完"); + CHECK(len == 5282u, "160 分片总长仍 5282(产出与分片大小无关)"); } { uint32_t len = 0u; @@ -579,10 +673,10 @@ int main(void) { uint32_t n = 99u; int rc; CHECK(wave_store_pack_start(&ctx, &s_meta, fake_reader, NULL) == WAVE_ST_OK, "pack_start OK"); - CHECK(wave_store_pack_next(&ctx, s_img, 127u, &n) == WAVE_ST_E_PARAM, "头一步容量不足 -> E_PARAM"); + CHECK(wave_store_pack_next(&ctx, s_img, 159u, &n) == WAVE_ST_E_PARAM, "头一步容量不足 -> E_PARAM"); CHECK(n == 0u, "拒绝时一个字节都不产(不写半截头)"); rc = wave_store_pack_next(&ctx, s_img, 256u, &n); - CHECK((rc == WAVE_ST_OK) && (n == 128u), "同一游标重试成功(拒绝不留副作用)"); + CHECK((rc == WAVE_ST_OK) && (n == 160u), "同一游标重试成功(拒绝不留副作用)"); rc = wave_store_pack_next(&ctx, s_img, 1u, &n); CHECK(rc == WAVE_ST_E_PARAM, "数据区容量装不下一个样本 -> 拒"); CHECK(n == 0u, "同样不产半个样本的字节"); @@ -603,7 +697,7 @@ int main(void) { int rc; CHECK(wave_store_pack_start(&ctx, &s_meta, NULL, NULL) == WAVE_ST_OK, "无数据源也能 start(只产头)"); rc = wave_store_pack_next(&ctx, s_img, 256u, &n); - CHECK((rc == WAVE_ST_OK) && (n == 128u), "头能产"); + CHECK((rc == WAVE_ST_OK) && (n == 160u), "头能产"); rc = wave_store_pack_next(&ctx, s_img, 256u, &n); CHECK((rc == WAVE_ST_E_STATE) && (n == 0u), "没有数据源却进数据区 -> 拒且不产空字节(产空字节 = 伪造一条直线)"); @@ -742,7 +836,7 @@ int main(void) { char path[512]; FILE *fp; size_t wrote; - CHECK(len == 5250u, "产物长度 5250"); + CHECK(len == 5282u, "产物长度 5282"); if ((tmp != NULL) && (strlen(tmp) < 400u)) { strcpy(path, tmp); strcat(path, "\\wave_algo_synth8.bin"); @@ -751,8 +845,8 @@ int main(void) { wrote = fwrite(s_img, 1u, len, fp); (void)fclose(fp); CHECK(wrote == (size_t)len, "合成段落盘成功"); - CHECK(s_img[WAVE_ST_OFF_ENG] == expect_eng(s_img[WAVE_ST_OFF_FLAGS]), - "互验产物自洽:这份构造段的单位标签与逐路有效位图对得上(真机写得出)"); + CHECK(head_eng_consistent(s_img) != 0, + "互验产物自洽:这份构造段的单位标签与逐路变比对得上(真机写得出)"); /* 形态在日志里说清楚:本套是「同时带有效与削顶」的构造形态, * 本波真值形态(逐路有效位图全零、片内 ADC 只带削顶位)由 wave_trig 那套给 */ printf(" [INFO] 互验产物(构造形态 = 已标定后仍削顶:逐路有效位图 0xFF、avail 0x01 / 0x03、" diff --git a/test/host/wave_trig/wave_trig_test.c b/test/host/wave_trig/wave_trig_test.c index 977a605d..bcdc76cc 100644 --- a/test/host/wave_trig/wave_trig_test.c +++ b/test/host/wave_trig/wave_trig_test.c @@ -19,7 +19,7 @@ * 而那正是「无波可录」的时刻。用 1 kHz 逐拍模拟在主机上跑满整段(360 拍一段)成本几乎为零。 * * 桩的边界:本套不桩任何东西——wave_ring.c 与 wave_store.c 零平台依赖, - * 封存瞬间事实(adc_cal 的 avail / k_q16 / b_codes)由本测试按 §6.3 口径 + * 封存瞬间事实(adc_cal 的 avail / k_q16 / b_codes / r_q16)由本测试按 §6.3 口径 * 手工填进 wave_seal_cal_t,模拟接线道那一拍该交棒的内容。 * 真 adc_cal、真 FatFs、真 FTP、注入对表属台架(docs/子系统设计/故障录波设计说明.md §10)。 * @@ -198,6 +198,10 @@ static void cal_init(void) { s_cal.ch[j].avail = (uint8_t)((tpa != 0) ? 0x00 : 0x02); s_cal.ch[j].k_q16 = (tpa != 0) ? 10000000 : 52800000; s_cal.ch[j].b_codes = 0; + /* v2:变比原值。本套是 V0.1 未标定形态,一路都还没配变比 -> 全 0 + * (= 本路不折算,输出停在引脚域,与 s_cal.eng = WAVE_ENG_PIN 自洽); + * 非 0 变比的往返与折算由 test/host/wave_store 那套与它的 PC 解码器覆盖。 */ + s_cal.ch[j].r_q16 = 0; } } @@ -496,7 +500,7 @@ int main(void) { CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_PROTECT, 12u, 200u) != 0, "全链路:先滚 200 拍再触发封段"); CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "全链路:交棒封存瞬间事实"); CHECK(pack_seg() == 0, "全链路:打包游标跑到 DONE(数据源就是 wave_ring 的读口)"); - CHECK(s_len == (uint32_t)WAVE_ST_SEG_SZ_CUR, "整段字节数 = 5250(§6.3 等式)"); + CHECK(s_len == (uint32_t)WAVE_ST_SEG_SZ_CUR, "整段字节数 = 5282(§6.3 等式)"); CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "自产段过整段自检(两个 CRC 都对)"); CHECK(wave_store_seg_check(s_buf, s_len, &s_m) == WAVE_ST_OK, "自检并回填段头"); CHECK((s_buf[0] == (uint8_t)WAVE_ST_MAGIC0) && (s_buf[1] == (uint8_t)WAVE_ST_MAGIC1) && @@ -546,15 +550,18 @@ int main(void) { if (wave_st_get32(&s_buf[pos + WAVE_ST_CH_OFF_BCODE]) != (uint32_t)s_cal.ch[j].b_codes) { ok_tbl = 0; } + if (wave_st_get32(&s_buf[pos + WAVE_ST_CH_OFF_RQ16]) != (uint32_t)s_cal.ch[j].r_q16) { + ok_tbl = 0; + } } - CHECK(ok_tbl != 0, "通道表 8 项:sem = 下标 + conv/bits/avail/k_q16/b_codes 全部落进字节"); + CHECK(ok_tbl != 0, "通道表 8 项:sem = 下标 + conv/bits/avail/k_q16/b_codes/r_q16 全部落进字节"); } { uint16_t hcrc = wave_store_crc(s_buf, WAVE_ST_HEAD_CRC_POS((uint8_t)WAVE_ST_CH_NUM)); CHECK((s_buf[WAVE_ST_HEAD_CRC_POS((uint8_t)WAVE_ST_CH_NUM)] == (uint8_t)(hcrc & 0xFFu)) && (s_buf[WAVE_ST_HEAD_CRC_POS((uint8_t)WAVE_ST_CH_NUM) + 1u] == (uint8_t)(hcrc >> 8)), - "头 CRC 覆盖 [0, 30+12*ch) 且低字节在前"); + "头 CRC 覆盖 [0, 30+16*ch) 且低字节在前"); } { uint16_t dcrc = wave_store_crc(&s_buf[WAVE_ST_HEAD_SZ_CUR],