/* * 模块名称:通道标定与量纲折算(标定折算)——实现 * 模块功能:见 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,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;r 不在 * [65536, ADC_CAL_R_MAX] → 该路按 1.0 处理(输出停在引脚电压)。 * 两种越带都让 flags 不报 VALID,每通道每次开机首见沿打一条 [ERR] * (adc_cal_init 重置留痕)。 * 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU) * 作者:DTU 项目组 * 创建日期:2026-10-25(道A 标定折算与落盘整定) * 修改记录: * v1.0 2026-10-25 DTU 项目组 创建:Q16 系数/零点两段落盘 + 折算 + 三态 flags * v1.1 2026-10-25 DTU 项目组 运行参数段(道D):新增 adc_cal_b_codes() 生效零点 * 读取口;set() 授权写者扩至 iec104Task 直通路径 * (run_par 0x8040~/0x8048~,契约同 §4.4) * + 合法带哨兵回退 + 标定公式(v2.1 起改在 PC 侧做 N 点 * 最小二乘,固件不再持有拟合算术) * v1.2 2026-10-26 DTU 项目组 道F task-16:新增 adc_cal_prot_blocked()——未标定 * 闭锁(UA/UN/IA 三路 VALID 全齐才放行)收成本层单点, * DTU_BENCH 旁路同时收成全仓唯一一处;此前该判据在 * protect_task.c 与 ui_data_feed.c 各写一份,两处一旦 * 漂移,屏上「未标定」与判据实际停走就不是同一件事 * 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 已配置」条件 * v2.1 2026-11-13 DTU 项目组 两点 → N 点最小二乘:删两点公式(全仓 * 生产代码零调用,只有单测调用,故对运行中的固件无影响), * 新增固件侧拟合函数 + 配套拒绝码: * N 组 (读数, 注入) 做普通最小二乘 y=a·x+c,再一次项 * r_new=a·r_old、常数项 b_new=b_old+c/(k·1e-3·r_new) * 折回 r/b。拒绝判据 n<3 / den<=0 / 单调性反转 / a<=0 / * r_new 越带 / |b_new|>B_MAX / 最大残差 > 满量程 2%。 * 全程 int64 定点,与 test/host/cal_fit.py 逐位对拍 * (金标向量钉在 test/host/adc_cal/adc_cal_test.c) * v2.2 2026-11-13 DTU 项目组 拟合移出固件:删 v2.1 新增的固件侧拟合函数及其算术核 * (圆整除法 + 累加器)、5 个域上界常量与 8 个拒绝码宏。 * 理由:标定是台架一次性离线动作(注入已知量 → 读屏上 * meas: → PC 解 r/b → 经 104 写入),设备只需接受校正后的 * 参数;固件版生产零调用者(被 linker 移除),留着就是 * 第二个真相源。设备侧安全闸(set/set_ratio 的 k/r/b * 合法带拦截)**原样保留**——那是防工具写坏参数的闸。 * 拟合唯一实现移到 test/host/cal_fit.py,原金标向量随之 * 搬进其 --selftest。 */ /* 头文件包含区 */ #include #include #include "adc_cal.h" #include "cfg_file.h" #define DBG_TAG "[CAL]" #include "dbg_log.h" /* 常量定义区 */ /* 段名(≤ CFG_FILE_MAX_NAME_LEN)与键名(k_ua…b_in…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=语义下标。 * data_size 取 offsetof(valid)+1 = 33(尾随对齐填充不属于引擎视图, * 引擎「复位默认」按 data_size memcpy,必须覆盖 valid,见单测 TC-ADCCAL-001) */ typedef struct { int32_t k[8]; uint8_t valid; } adc_cal_kcfg_t; /* [adc_cal_ofs] 段 RAM 副本:b 按语义下标 0..7(int32 有符号,有符号零点类型 * CFG_VAL_INT 是负零点偏移能干净落盘的前提) */ 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 * (16 位 ±5 V 档),UB/UC/IB/IC/IN 走 STM32 ADC1;逐路对过 * docs/硬件平台/ADC通道映射说明.md §3(IN=ADC11/STM32,勿再按分组顺序误写)。 * ROM 表按**当前 V0.1 来源排布**显式书写(不查 desc 运行期派生),因为它是 * 量程口径冻结定稿的存档事实;V0.2 改版(消除双转换器无共同采样触发)时本表与 adc_input 映射表同步改, * 上层语义与 API 不动。CLIPPED 位则**运行期按来源派生**(见 adc_cal_flags) */ static const adc_cal_kcfg_t s_kcfg_rom = { /* idx0 UA / idx1 UB / idx2 UC / idx3 UN */ { ADC_CAL_K_Q16_TPAFE, ADC_CAL_K_Q16_STM32, ADC_CAL_K_Q16_STM32, ADC_CAL_K_Q16_TPAFE, /* idx4 IA / idx5 IB / idx6 IC / idx7 IN */ ADC_CAL_K_Q16_TPAFE, ADC_CAL_K_Q16_STM32, ADC_CAL_K_Q16_STM32, ADC_CAL_K_Q16_STM32 }, 0u /* 出厂未标定:VALID 全 0 */ }; static const adc_cal_bcfg_t s_bcfg_rom = { { 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L } }; /* 变比工厂默认:**下标 = 语义枚举**,按族给(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() 刻意不清** * ——清了水位与计数就失配,会凭空造出一段"假未落盘"差额。 */ static volatile uint32_t s_st_set_ok; /* 成功改写 RAM 副本的次数 */ static volatile uint32_t s_st_set_reject; /* 被拒次数(非法通道 / 越出合法带) */ static volatile uint32_t s_st_persist_water; /* 落盘水位:盖写,绝不自增 */ /* 键表:offset 用 offsetof 取真值(k[0]@0 / k[1]@4 … valid@32) */ static const cfg_key_def_t s_k_keys_tbl[9] = { { "k_ua", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[0]), 4u }, { "k_un", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[3]), 4u }, { "k_ia", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[4]), 4u }, { "k_ub", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[1]), 4u }, { "k_uc", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[2]), 4u }, { "k_ib", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[5]), 4u }, { "k_ic", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[6]), 4u }, { "k_in", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[7]), 4u }, { "valid", CFG_VAL_UINT, offsetof(adc_cal_kcfg_t, valid), 1u }, }; static const cfg_key_def_t s_b_keys_tbl[8] = { { "b_ua", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[0]), 4u }, { "b_un", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[3]), 4u }, { "b_ia", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[4]), 4u }, { "b_ub", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[1]), 4u }, { "b_uc", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[2]), 4u }, { "b_ib", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[5]), 4u }, { "b_ic", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[6]), 4u }, { "b_in", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[7]), 4u }, }; /* 变比键表:键的书写顺序照 §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 }, }; /* 函数定义区 */ /* * 函数功能:判定语义通道是否 STM32 源(经 adc_input 权威映射表,不另立第二份表) * 入口参数:sem - 语义通道 unsigned * 返 回 值:1 - STM32;0 - TPAFE(含 desc 取不到时的保守分支) int * 限定条件:无 */ static int s_rom_k_stm32(unsigned sem) { const adc_channel_desc_t *d = adc_input_desc((adc_semantic_t)sem); return ((d != NULL) && (d->source == ADC_SRC_STM32)) ? 1 : 0; } /* * 函数功能:取语义通道的 ROM 默认系数(合法带/回退参照) * 入口参数:sem - 语义下标(0..7) unsigned * 返 回 值:Q16 系数 int32_t * 限定条件:sem < 8(调用方已先行拦截 TEMP/越界) * 函数说明:直接读出厂表本体——保证「哨兵合法带基准」与「引擎工厂默认」 * 永远同一份数(两者若靠重复表达式各算各的,改一处漏一处就是暗雷) */ static int32_t s_rom_k(unsigned sem) { return s_kcfg_rom.k[sem]; } /* * 函数功能:k/b 合法带检查(ROM×[0.5,2.0] / |b| ≤ ADC_CAL_B_MAX) * 入口参数:sem - 语义下标(0..7) unsigned * k - 待检系数 Q16 int32_t * b - 待检零点 码值 int32_t * 返 回 值:1 - 合法;0 - 越带 int * 限定条件:sem < 8 */ static int s_kband_ok(unsigned sem, int32_t k, int32_t b) { const int32_t rom = s_rom_k(sem); if (k < (rom >> 1) || k > (rom << 1)) { return 0; } if (b > ADC_CAL_B_MAX || b < -ADC_CAL_B_MAX) { return 0; } return 1; } /* * 函数功能:越带留痕(每通道每次开机首见沿一条 [ERR],init 重置) * 入口参数:sem - 语义下标 0..7;k/b - 现场值 * 返 回 值:无 * 限定条件:仅 adc_cal_apply/set 内部 */ static void s_note_bad(unsigned sem, int32_t k, int32_t b) { const uint8_t bit = (uint8_t)(1u << sem); if ((s_err_latched & bit) == 0u) { s_err_latched |= bit; DBG_ERROR("ch=%u k_q16=%ld b=%ld out of ROM band [0.5x,2x]/|b|<=%ld -> ROM fallback", (unsigned)sem, (long)k, (long)b, (long)ADC_CAL_B_MAX); } } /* * 函数功能:变比合法带检查(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 - 输出 * 返 回 值:1 - 用了合法值;0 - 已回退 ROM * 限定条件:sem < 8 */ static int s_eff_kb(unsigned sem, int32_t *p_k, int32_t *p_b) { const int32_t k = s_kcfg.k[sem]; const int32_t b = s_bcfg.b[sem]; if (s_kband_ok(sem, k, b) != 0) { *p_k = k; *p_b = b; return 1; } s_note_bad(sem, k, b); *p_k = s_rom_k(sem); *p_b = 0L; return 0; } /* * 函数功能:变比越带留痕(每通道每次开机首见沿一条 [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 * 函数说明:完整契约见 adc_cal_init() 同名声明(adc_cal.h);注册失败(段名重复/ * 段数超限)打 [ERR] 后继续——ROM 默认系数照常生效,仅文件整定失效 */ void adc_cal_init(void) { cfg_section_reg_t reg; (void)memcpy(&s_kcfg, &s_kcfg_rom, sizeof(s_kcfg)); (void)memcpy(&s_bcfg, &s_bcfg_rom, sizeof(s_bcfg)); (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; reg.p_data = &s_kcfg; reg.p_defaults = &s_kcfg_rom; reg.data_size = (offsetof(adc_cal_kcfg_t, valid) + 1u); /* 33:覆盖到 valid */ reg.p_keys = s_k_keys_tbl; reg.key_count = 9u; reg.p_cb = NULL; /* 不挂回调:RAM 副本即生效值(docs/算法/采集与算法层设计说明.md §13.8) */ if (cfg_file_register(®) != 0) { DBG_ERROR("register '%s' FAIL (dup/full?) - calibration file values inactive", ADC_CAL_SEC_K); } (void)memset(®, 0, sizeof(reg)); reg.name = ADC_CAL_SEC_B; reg.p_data = &s_bcfg; reg.p_defaults = &s_bcfg_rom; reg.data_size = sizeof(s_bcfg); /* 32 */ reg.p_keys = s_b_keys_tbl; reg.key_count = 8u; reg.p_cb = NULL; if (cfg_file_register(®) != 0) { DBG_ERROR("register '%s' FAIL (dup/full?) - zero-offset file values inactive", ADC_CAL_SEC_B); } (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 * 返 回 值:工程量浮点(r=0 = ADC 引脚电压 V;TEMP/越界 = 原样透传码值) float * 限定条件:无(任意上下文,纯读) * 函数说明:见 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); 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; } /* * 函数功能:读通道可用性标志(消费者判「能不能信」的唯一入口) * 入口参数:sem - 语义通道 adc_semantic_t * 返 回 值:ADC_CAL_F_VALID / ADC_CAL_F_CLIPPED 组合;TEMP/越界恒 0 uint32_t * 限定条件:无 * 函数说明:VALID 需文件位图、k/b 合法带、变比已配置**三者**同时成立 * (哨兵压制说谎位图,见文件头)。三者里任一不成立就整条不报 VALID—— * 宁可让现场看到「未标定」,也不能让一个「引脚电压乘以一个猜的变比」 * 冒充二次值去解锁保护判据 */ uint32_t adc_cal_flags(adc_semantic_t sem) { uint32_t f = 0u; if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) { return 0u; } /* 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_rband_ok(s_rcfg.r[sem]) != 0)) { f |= ADC_CAL_F_VALID; } /* CLIPPED 由**来源**派生(R5:STM32 侧单极性无偏置 → 负半周削顶)—— * V0.2 改版(8 路全接 TPAFE,消除双转换器无共同采样触发)后本位自动恒 0,无需改码, * 对应docs/算法/采集与算法层设计说明.md §13.3 三态可用性表「页面与 104 绑定零改动」的要求 */ if (s_rom_k_stm32((unsigned)sem) != 0) { f |= ADC_CAL_F_CLIPPED; } return f; } /* * 函数功能:未标定闭锁判定(判据所需的 UA/UN/IA 三路是否齐) * 返 回 值:1 - 闭锁;0 - 放行 uint8_t(契约、三路集合的理由、判据停走的四路 * 来源、两个同族谓词的边界都写在 adc_cal.h,本处不重述——重述就是第二真源) * 函数说明:**现查 flags(),本函数不留任何状态**。刻意不复用保护任务里那份 * 「上拍闭锁态」——那是给边沿日志用的,开机首拍前恒 0;拿它当判据等于在 * 「还没判过」的时候谎称「已标定」,而这正是本谓词最贵的失效(屏上 * 「未标定」徽标一消失,现场读到的就是「改了定值就会动作」)。 * DTU_BENCH 旁路留在这里、且全仓只此一处;用 #ifndef 把产品默认路径留在 * 主分支——#ifdef 双分支会让 cppcheck 对未定义宏取真分支、把静态分析引到 * 旁路上报 knownConditionTrueFalse 假信号(实测 2026-10-25,这条形态说明原先 * 写在 protect_task.c 的实现处,随判据一起搬到这里) */ uint8_t adc_cal_prot_blocked(void) { #ifndef DTU_BENCH if (((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) == 0u) || ((adc_cal_flags(ADC_SEM_UN) & ADC_CAL_F_VALID) == 0u) || ((adc_cal_flags(ADC_SEM_IA) & ADC_CAL_F_VALID) == 0u)) { return 1u; } #endif return 0u; } /* * 函数功能:读通道当前生效系数(µV/码,回退后口径) * 入口参数:sem - 语义通道 adc_semantic_t * 返 回 值:k[µV/码];TEMP/越界返 0 float * 限定条件:无 * 函数说明:Q16 现值 ÷ 65536 浮点还原;勿据其反推二次值(可用性看 flags) */ float adc_cal_k_uv(adc_semantic_t sem) { int32_t k; int32_t b; if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) { return 0.0f; } (void)s_eff_kb((unsigned)sem, &k, &b); return ((float)k) / 65536.0f; } /* * 函数功能:读通道当前生效系数的 int32 Q16 原值(adc_cal_k_uv 孪生,见 .h 声明) * 入口参数:sem - 语义通道 adc_semantic_t * 返 回 值:k[Q16];TEMP/越界返 0 int32_t * 限定条件:无(任意上下文,纯读) * 函数说明:与 adc_cal_k_uv() 共用 s_eff_kb 生效视图(越带回退 ROM 口径一致), * 差别只在不做浮点还原——录波段头按 LSB 精确记账必须走本口(§6.3) */ int32_t adc_cal_k_q16(adc_semantic_t sem) { int32_t k; int32_t b; if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) { return 0; } (void)s_eff_kb((unsigned)sem, &k, &b); return k; } /* * 函数功能:读通道当前生效零点偏移(码值,浮点)——adc_cal_k_uv 孪生入口 * 入口参数:sem - 语义通道 adc_semantic_t * 返 回 值:b[码值](越带时 s_eff_kb 已回 0);TEMP/越界返 0 float * 限定条件:无(任意上下文,纯读) * 函数说明:运行参数段的 104 b 覆盖回显 + k 单侧改写 RMW 读侧(App/algo/run_par.c) */ float adc_cal_b_codes(adc_semantic_t sem) { int32_t k; int32_t b; if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) { return 0.0f; } (void)s_eff_kb((unsigned)sem, &k, &b); return ((float)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 - 零点(码值) * 返 回 值:0 - 成功;-1 - 参数非法/越出合法带(RAM 不动,[ERR] 留痕) int * 限定条件:整定路径上下文(defaultTask 整定/标定链;iec104Task 经 run_par * 直通为运行参数段授权写者,单字原子契约同 §4.4);落盘由属主经 cfg_file_save * 函数说明:见 adc_cal.h 同名声明 */ int adc_cal_set(adc_semantic_t sem, int32_t k_q16, int32_t b_codes) { if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) { s_st_set_reject++; /* 观测:连通道号都非法,RAM 未动 */ return -1; } if (s_kband_ok((unsigned)sem, k_q16, b_codes) == 0) { DBG_ERROR("set ch=%u rejected: k_q16=%ld b=%ld out of ROM band [0.5x,2x]/|b|<=%ld", (unsigned)sem, (long)k_q16, (long)b_codes, (long)ADC_CAL_B_MAX); s_st_set_reject++; /* 观测:越出合法带,RAM 未动([ERR] 由上一行打) */ return -1; } s_kcfg.k[sem] = k_q16; s_bcfg.b[sem] = b_codes; s_kcfg.valid |= (uint8_t)(1u << (unsigned)sem); s_st_set_ok++; /* 观测:RAM 副本确实被改写了(与落盘无关) */ 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 * 限定条件:无;adc_cal_init() 之前可读(全 0 = 本层还没被改写过) * 函数说明:见 adc_cal.h 同名声明(三态语义与那张成对读表都在那里,本处只取数) */ void adc_cal_get_stats(adc_cal_stats_t *p_out) { if (p_out == NULL) { return; /* 观测件不得反过来制造故障 */ } p_out->set_ok = s_st_set_ok; p_out->set_reject = s_st_set_reject; p_out->persisted_set_ok = s_st_persist_water; } /* * 函数功能:落盘成功回执——把水位盖写为当前的 set_ok(不是自增) * 返 回 值:无 void * 限定条件:仅"整档保存已成功"那一条路径(当前唯一调用点 run_par_store_save()) * 函数说明:盖写而非自增的理由与回归用例见 adc_cal.h 同名声明。水位直接取 set_ok 的 * 当前值,因此它**在结构上不可能越过** set_ok——差额是 uint32 相减,一旦越过 * 就绕回成一个巨大的假未落盘数,这里不留那种可能 */ void adc_cal_note_persisted(void) { s_st_persist_water = s_st_set_ok; /* 水位只会追平,不会越过 */ } /* * 函数功能:通道码值域贴轨带门限(采样通道自检 判定的门限来源,按 FS 百分比) *(保护功能清单见 docs/算法/继电保护功能复现规划.md) * 入口参数:sem - 语义通道;p_lo_th/p_hi_th - 输出下/上轨带界 int32_t * * 返 回 值:0 - 已给出门限;-1 - TEMP/越界/参数空(不做贴轨监视) int * 限定条件:无 * 函数说明:状态机在 adc_health_rail_watch();本函数只供门限,无状态 */ int adc_cal_rail_ths(adc_semantic_t sem, int32_t *p_lo_th, int32_t *p_hi_th) { if ((p_lo_th == NULL) || (p_hi_th == NULL) || ((unsigned)sem >= (unsigned)ADC_SEM_TEMP)) { return -1; /* TEMP(热敏非线性,不判「轨」)与越界不做监视 */ } if (s_rom_k_stm32((unsigned)sem) != 0) { *p_hi_th = (int32_t)((4095L * 98L) / 100L); /* 4013 = 98 % FS */ *p_lo_th = (int32_t)((4095L * 2L) / 100L); /* 81 = 触底带(削顶签名) */ } else { *p_hi_th = 32112L; /* +98 % 半量程 */ *p_lo_th = -32112L; /* -98 % 半量程 */ } return 0; }