Files
STM32F4-Base/App/algo/adc_cal.c
T
wandering1 3675b89e8e feat(adc): 新增变比维 r 三维折算,录波段格式升 v2
采样板注入 Ua=5V/Ia=0.5A 读数偏小,根因是固件只输出引脚电压、
缺「端子→引脚」变比。折算改三维:

工程量 = (code + b) × k[µV/码] × 1e-6 × r

k 保持 ROM 引脚 LSB,b 为零点,r 为端子变比(int32 Q16)。
变比不能并进 k:UA 端子域 k 达 1.5e5 µV/码、Q16 溢出 int32;
r 单独存 Q16 不溢出。两点标定改标 r,k 仍是板级事实。

- 新开单段 [adc_cal_sec](8 键 r_ua..r_in),cfg 段数达 8/8 上限
- 一次变比复用 prot_set 四项,不新开段;r=0 表示本路不折算
- ROM 默认带台架实测变比(电压族 1000、电流族 29.4118),
  烧进固件即显二次值;VALID 仍须两点标定,两者互不派生
- eng 判据改「每路要么已配变比、要么削顶不显示」,不再看 VALID
- 104 地址 0x8050~0x8057 写 r,run_par_cal_r_set 写值与置脏同函数
- 录波 v2:通道表加 int32 r_q16,头 128→160、整段 5250→5282;
  code_to_milli 用余数分解换等价单次圆整,20 万组大整数对拍 0 不符
- 文档同步:采集与算法层/通道映射/故障录波/网络配置/配置文件/
  产品需求/内存台账,历史带日期实测数字按冻结口径不改
- 单测 13 套全绿,/keil-build 0 错误 0 警告(256024/31760/
  2080/152368),RAM 实测回填内存内容分配策略 §6.1
2026-11-11 11:31:34 +08:00

712 lines
34 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:通道标定与量纲折算(标定折算)——实现
* 模块功能:见 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)
* + 合法带哨兵回退 + 两点标定公式(台架注入待跑)
* 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 已配置」条件
*/
/* 头文件包含区 */
#include <stddef.h>
#include <string.h>
#include "adc_cal.h"
#include "cfg_file.h"
#define DBG_TAG "[CAL]"
#include "dbg_log.h"
/* 常量定义区 */
/* 段名(≤ CFG_FILE_MAX_NAME_LEN)与键名(k_ua…b_in…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(&reg, 0, sizeof(reg));
reg.name = ADC_CAL_SEC_K;
reg.p_data = &s_kcfg;
reg.p_defaults = &s_kcfg_rom;
reg.data_size = (offsetof(adc_cal_kcfg_t, valid) + 1u); /* 33:覆盖到 valid */
reg.p_keys = s_k_keys_tbl;
reg.key_count = 9u;
reg.p_cb = NULL; /* 不挂回调:RAM 副本即生效值(docs/算法/采集与算法层设计说明.md §13.8) */
if (cfg_file_register(&reg) != 0) {
DBG_ERROR("register '%s' FAIL (dup/full?) - calibration file values inactive",
ADC_CAL_SEC_K);
}
(void)memset(&reg, 0, sizeof(reg));
reg.name = ADC_CAL_SEC_B;
reg.p_data = &s_bcfg;
reg.p_defaults = &s_bcfg_rom;
reg.data_size = sizeof(s_bcfg); /* 32 */
reg.p_keys = s_b_keys_tbl;
reg.key_count = 8u;
reg.p_cb = NULL;
if (cfg_file_register(&reg) != 0) {
DBG_ERROR("register '%s' FAIL (dup/full?) - zero-offset file values inactive",
ADC_CAL_SEC_B);
}
(void)memset(&reg, 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(&reg) != 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; /* 水位只会追平,不会越过 */
}
/*
* 函数功能:两点标定公式(标定项的算术核,纯函数无状态;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 k_q16, int32_t *p_r_q16, int32_t *p_b) {
long long dc;
long long dv;
long long t;
long long rq;
long long tl;
long long bb;
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; /* 斜率必须为正:端子值与码值同向,反向 = 映射/接线错 */
}
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 = 单位/量级错 */
}
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 风险表) */
}
*p_r_q16 = (int32_t)rq;
*p_b = (int32_t)bb;
return 0;
}
/*
* 函数功能:通道码值域贴轨带门限(采样通道自检 判定的门限来源,按 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;
}