refactor(adc): 最小二乘拟合移出固件,只留 PC 侧唯一实现
用户裁定(2026-11-13):设备本体只需要校正后的参数,不需要自己拟合 标定曲线,故固件侧最小二乘拟合整体移出,拟合的唯一实现留在 PC 侧 test/host/cal_fit.py。 删除的死代码(生产零调用、上一轮 map 里已被 linker 标 Removing): - adc_cal.h:adc_cal_fit() 声明、8 个拒绝码、5 个常量 - adc_cal.c:s_div_round() / s_fit_accum() / adc_cal_fit() 共 224 行 - adc_cal_test.c:tc_00b_fit 226 行(184 -> 150 条) 原样保留的设备侧安全闸: - adc_cal_apply() 折算、adc_cal_set() / adc_cal_set_ratio() 参数合法 带拦截(防工具写坏参数)、adc_cal_flags() / adc_cal_prot_blocked() / adc_cal_rail_ths()。设备不「算」,但必须「用」和「验」。 测试面: - cal_fit.py 转为唯一实现,新增 --selftest:承接原 C 侧金标向量 (r_q16=39322000 / b=12)与 4 条拒绝路径,共 11 条断言;已做变异 测试证明它会红(改坏圆整口径 -> 2 条 FAIL) - cal_two_point.py 删除,两点工具由多点拟合取代 文档口径统一为「求参在 PC、设备只做折算与合法带校验」,6 篇 docs 同步; 执行卡改名 ADC多点标定执行流程.md(正文同步 5 点最小二乘口径)。 cfg 热重载吞段缺陷的解析器契约与 TC-CONFIG-063 机器钉一并入库。 Program Size 不变(Code=256196):删掉的是从未链接进镜像的死代码。 验证:Keil 0 errors 0 warnings;adc_cal 150/150;selftest 11/11; ci_local.py RC=0(static 48/48、manual 三根、size-gate、host-unit 4341/4341);doc-links / mermaid / mojibake 全 0。
This commit is contained in:
+2
-2
@@ -31,8 +31,8 @@
|
||||
| `ch395f_reuse_host.py` | Socket 复用/中止形态 PC 对端(bench 配套) |
|
||||
| `ftp_auth_bench.py` | FTP 鉴权/锁定台架腿 |
|
||||
| `iec104_master.py` | **IEC 60870-5-104 主站客户端(台架复测/保护定值持久化)**:连设备 :2404(工厂默认,`--port` 可指别的)跑 STARTDT → 总召(actcon/四组背景扫描/actterm)→ 经 C_SE 两段写临时压低 OC1 定值确定性触发 YX16 变位(COT=3)→ 恢复 → YK11 两段式 → C_RD 读回;**保护定值持久化模式 `--b7-pre/--b7-post/--b7-mark/--b7-check`**(中间 ST-LINK 复位):YK12 两段投退→保护退出/联动、定值与压板态跨复位保持(含非默认 7.5 往返);纯标准库,输出纯 ASCII 可直接重定向归档为台架证据(判据 exit 0/1/2) |
|
||||
| `cal_two_point.py` | **两点标定离线计算器**:`--from-doc` 直接读执行卡 §3 `CALDATA` 标记表批量解所有已填路(空行跳过、合并 `WRITE-ALL` 命令);单路 `--sem/--lo-mv/--hi-mv/--known-*` 亦可——镜像 `adc_cal_two_point()` 整数算术(已对拍 `test/host/adc_cal/` 单测 TC-ADCCAL-015 合成参数零偏差);执行卡 `docs/测试说明/专项验证/ADC两点标定执行流程.md` §3;exit 0 可写 / 2 越带拒绝或未填 |
|
||||
| `cal_write.py` | **两点标定写入器**(库层复用 `iec104_master.py`):C_SE 两段写下发定值段 `0x8050~`(变比 r)/`0x8048~`(零点 b,落笔置 VALID),C_RD 读回复核;每路先 r 后 b,输出纯 ASCII 可归档;执行卡 `docs/测试说明/专项验证/ADC两点标定执行流程.md` §4;exit 0 全过 / 1 有失败 / 2 连接或协议错误 |
|
||||
| `cal_fit.py` | **N 点最小二乘标定参数求解(全仓唯一实现,v2.2 起固件侧拟合已删除)**(**3~8 点**,推荐 5 点):`--selftest` 跑金标向量回归(原 C 侧 `r_q16=39322000 / b=12` + 4 条拒绝路径,exit 0 全过 / 1 有失败,改动算法后必跑);`--from-doc` 直接读执行卡 §3 `CALDATA` 长表(每行一个点:路 \| 序号 \| 屏上读数 \| 注入已知量)按路分组拟合,输出合并 `WRITE-ALL` 命令;单路 `--sem/--x/--y` 亦可(`--x` = `meas:` 行 mV/mA 原样、`--y` = V/A 内部 ×1000,`--r-old/--b-old` 指读数采集时的设备参数);执行卡 `docs/测试说明/专项验证/ADC多点标定执行流程.md` §3;exit 0 可写 / 2 拒绝(入参越界 `bad argument`、点数越界、点位没拉开、单调性反转、斜率非正、r/b 越带、残差 >2%、表还是旧 6 列)或未填 |
|
||||
| `cal_write.py` | **标定参数写入器**(库层复用 `iec104_master.py`):C_SE 两段写下发定值段 `0x8050~`(变比 r)/`0x8048~`(零点 b,落笔置 VALID),C_RD 读回复核;每路先 r 后 b,输出纯 ASCII 可归档;执行卡 `docs/测试说明/专项验证/ADC多点标定执行流程.md` §4;exit 0 全过 / 1 有失败 / 2 连接或协议错误 |
|
||||
|
||||
### 时间 / 时延
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
* TC-ADCCAL-009 合法带哨兵回退:坏 k/坏 b → ROM 值 + 每通道一次 [ERR] +
|
||||
* flags 不报 VALID(即便位图置位)+ init 重置观测窗
|
||||
* 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-015 (已移除)N 点最小二乘拟合:v2.2 起拟合移出固件,算术核唯一实现
|
||||
* 在主机侧 test/host/cal_fit.py,其 --selftest 承接原金标向量
|
||||
* (r_q16=39322000 / b=12)与拒绝路径断言。本文件不再覆盖。
|
||||
* TC-ADCCAL-016 rail_ths 门限:TPAFE ±32112 / STM32 4013·81 / TEMP 不监视
|
||||
* TC-ADCCAL-017 init 幂等:重复调用 RAM 复位默认 + 重新注册
|
||||
* —— 以下走真实 cfg_file 引擎(标定落盘与整定入口 DoD)——
|
||||
@@ -430,63 +430,6 @@ static void tc_00a_b_effect(void) {
|
||||
"IA apply = (code-77)*ROMk*r_curr");
|
||||
}
|
||||
|
||||
/* TC-ADCCAL-015:两点标定公式 + 写回往返 */
|
||||
static void tc_00b_two_point(void) {
|
||||
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 r/b\n");
|
||||
/* 合成测量:v = (c + b0) * k0/65536 µV(lround 模拟量化读数) */
|
||||
/* 合成测量:端子 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(((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(k,b) 置位 + set_ratio 定量纲,两点都应复现端子值 */
|
||||
reset_all();
|
||||
adc_cal_init();
|
||||
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, 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:贴轨门限表 */
|
||||
static void tc_00c_rail_ths(void) {
|
||||
@@ -791,7 +734,6 @@ int main(void) {
|
||||
tc_008_set();
|
||||
tc_009_sanitize();
|
||||
tc_00a_b_effect();
|
||||
tc_00b_two_point();
|
||||
tc_00c_rail_ths();
|
||||
tc_00d_idempotent();
|
||||
tc_010_file_apply();
|
||||
|
||||
@@ -0,0 +1,523 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""N 点最小二乘标定求解器:**全仓唯一实现**(v2.2 起固件侧拟合已删除)。
|
||||
|
||||
为什么是唯一实现:标定是台架一次性离线动作——注入已知量 → 读屏上 `meas:` →
|
||||
在 PC 上解出 r/b → 经 104 写入设备。设备本体只需要接受校正后的参数,不需要
|
||||
自己拟合曲线。固件里曾有过一份 C 实现,生产代码零调用(被 linker 移除),
|
||||
留着只会变成第二个真相源,v2.2 已整段删除。所以**本脚本的整数算术就是口径**,
|
||||
`--selftest` 是它的回归锚点(原 C 侧金标向量已搬进来)。
|
||||
|
||||
把台架注入的一串 (屏上读数, 注入已知量) 点拟合成 y = a*x + c(普通最小二乘),
|
||||
再折回标定层能落盘的 (r_q16, b):
|
||||
|
||||
r_new_q16 = round(a * r_old_q16) # 一次项 = 端子↔引脚变比
|
||||
c_pin_mV = c * 65536 / r_new_q16 # 截距从二次值域折回引脚域
|
||||
b_new = b_old + round(c_pin_mV * 65536 * 1000 / k_q16)
|
||||
|
||||
推导(disp 单位 mV):折算模型 disp = (code + b) × (k_q16/65536) × 1e-3 ×
|
||||
(r_q16/65536),把 disp_new = a×disp_old + c 的 code 一次项与常数项分别对齐,
|
||||
一次项给出 r_new = a·r_old,常数项给出上式 b_new(b_old = 写之前设备里的零点,
|
||||
必须作为入参传进来,因为读数是在那个零点下读的)。
|
||||
|
||||
为什么用两点之外的算法:两点没有冗余,单点读数一抖解出的 r 就整条平移;N 点最小二乘
|
||||
把残差摊到所有点上,并且能顺手判「非线性/削顶/接线反」——这正是两点的盲区。
|
||||
|
||||
全程 int64 定点(不用 float 做累加):结果确定、可跨平台复现,同一组输入永远
|
||||
解出同一个 r_q16。整数路径的圆整次序是本脚本语义的一部分,改一处就要同步
|
||||
--selftest 的期望值。
|
||||
|
||||
用法(仓库根目录,解释器用 .venv):
|
||||
|
||||
自检(回归锚点,改动算法后必跑):
|
||||
.venv/Scripts/python.exe test/host/cal_fit.py --selftest
|
||||
|
||||
单路(x = `meas:` 行原样读数 mV/mA;y = 继保仪注入值 V/A,内部 ×1000):
|
||||
.venv/Scripts/python.exe test/host/cal_fit.py --sem ua \\
|
||||
--x 5021.5,10041.77,25000,50024.09,99933.02 --y 5,10,25,50,100
|
||||
|
||||
批量(读执行卡 <!-- CALDATA:BEGIN/END --> 之间的长表,每行一个点):
|
||||
.venv/Scripts/python.exe test/host/cal_fit.py --from-doc \\
|
||||
docs/测试说明/专项验证/ADC多点标定执行流程.md
|
||||
|
||||
公共参数:
|
||||
--r-old 本次读数采集时设备里的变比(浮点):默认 ua/un=1000.0、ia=29.4118;
|
||||
也可 ua=1000,un=1000,ia=29.4118 形式逐路给
|
||||
--b-old 本次读数采集时设备里的零点(整数码值,默认 0):裸值或 ua=0,ia=-2 形式
|
||||
|
||||
⚠ **同一路的 x 必须在同一个 r 下采集**:拿旧 r 时代的读数和新 r 时代的读数混着
|
||||
做直线拟合,斜率会被系统性掰弯(表现是解出的 r 差几十个百分点,而合法带仍然放行)。
|
||||
|r_new/r_old - 1| > 10% 时打 WARN 就是给这件事提个醒。
|
||||
|
||||
判据(任一不满足即 REJECT,打印原因,不算可写):
|
||||
n 不在 [3, 8] 冗余不足或超上限
|
||||
den <= 0 示值全同 / 点位没拉开
|
||||
x 与 y 相邻增量方向相反 接线反、通道串、削顶的典型征兆
|
||||
a <= 0 零斜率/负斜率
|
||||
r_new_q16 不在 [1.0, 32767.99] 与 run_par_cal_r_set() 的粗守同带
|
||||
|b_new| > 8192 零点越带(先查前端,不用系数凑)
|
||||
最大残差 > 满量程跨度 2% 非线性/削顶
|
||||
|
||||
输出纯 ASCII(中文只在本注释里);exit 0 = 全部解出且合法,2 = 有拒绝/未填行;
|
||||
--selftest 的 exit 0 = 全部断言通过,1 = 有失败。
|
||||
创建日期:2026-11-13(两点 → N 点最小二乘;同日 v2.2 固件侧拟合删除,本脚本转为唯一实现)
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
|
||||
# ---- 判据参数(本脚本是唯一实现,这些值即为口径;改动会使 GOLDEN 期望值失效)----
|
||||
# ⚠ 前 5 个是**独立于固件**的算法参数(原先与已删除的 ADC_CAL_FIT_* 同值,
|
||||
# 现在以本文件为准);后 4 个是设备侧合法带,必须与 App/algo/adc_cal.h /
|
||||
# run_par.c 保持一致——那两处仍是设备侧安全闸(写参数时的拦截),不随拟合删除。
|
||||
K_Q16 = 10000000 # 该路前端斜率(UA/UN/IA 三路同源,同值)
|
||||
B_MAX = 8192 # 设备侧 ADC_CAL_B_MAX(零点幅值上限,码值)
|
||||
R_MIN = 65536 # 设备侧 ADC_CAL_R_MIN(比值 1.0)
|
||||
R_MAX = 2147483647 # 设备侧 ADC_CAL_R_MAX(int32 上限)
|
||||
MILLI_MAX = 200000 # 单点值域上界(mV / mA),±200 V / ±200 A
|
||||
C_MAX = 100000 # 截距 c 上界(mV),±100 V
|
||||
N_MIN = 3 # 点数下限(少于 3 点无冗余)
|
||||
N_MAX = 8 # 点数上限
|
||||
RES_PCT = 2 # 最大残差 ÷ 满量程跨度上限(百分比)
|
||||
# 比值上界的两位小数**向下取整**得 32767.99,不能用 R_MAX/65536.0 直接 %.2f:
|
||||
# 2147483647/65536 = 32767.99998474,四舍五入会印成 32768.00,与 adc_cal.h /
|
||||
# run_par.c 的"1.0 ~ 32767.99"口径差一个数(提示文案里的带边界必须和真带一致)。
|
||||
R_RATIO_MAX = (R_MAX * 100 // 65536) / 100.0 # 32767.99(仅用于提示文案)
|
||||
|
||||
# 语义槽位(App/algo/adc_input.h 枚举序:UA0/UB1/UC2/UN3/IA4/IB5/IC6/IN7)
|
||||
SEM = {
|
||||
"ua": dict(slot=0, r_old=1000.0), # 电压族 TPAFE
|
||||
"un": dict(slot=3, r_old=1000.0), # 电压族 TPAFE
|
||||
"ia": dict(slot=4, r_old=29.4118), # 电流族 TPAFE
|
||||
}
|
||||
|
||||
# 拒绝原因码(负数):本脚本的对外契约,--selftest 与 REASON 文案都按这张表
|
||||
ERR_ARG = -1
|
||||
ERR_POINTS = -2
|
||||
ERR_DEN = -3
|
||||
ERR_MONO = -4
|
||||
ERR_SLOPE = -5
|
||||
ERR_R_BAND = -6
|
||||
ERR_B_BAND = -7
|
||||
ERR_RESID = -8
|
||||
|
||||
REASON = {
|
||||
ERR_ARG: "bad argument (|x|,|y| < " + str(MILLI_MAX) +
|
||||
", k_q16 in (0,1e8], r_old in band)",
|
||||
ERR_POINTS: "n outside [%d,%d]: got %%d point(s)" % (N_MIN, N_MAX),
|
||||
ERR_DEN: "x span collapsed (den<=0): readings not spread",
|
||||
ERR_MONO: "non-monotonic: adjacent x/y increments oppose",
|
||||
ERR_SLOPE: "slope <= 0: readings flat or reversed vs injected value",
|
||||
ERR_R_BAND: "r_new out of band [1.0, %.2f]" % R_RATIO_MAX,
|
||||
ERR_B_BAND: "|b_new| beyond %d codes" % B_MAX,
|
||||
ERR_RESID: "residual > %d%% of y span (non-linearity / clipping)" % RES_PCT,
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------ 整数算术(唯一实现)
|
||||
# v2.2 起这里不再是"镜像":App/algo/adc_cal.c 的拟合算术已整段删除,本文件就是口径。
|
||||
def div_trunc(num, den):
|
||||
"""整数除法:向零截断(Python // 向负无穷取整,不能直接用)。"""
|
||||
q = abs(num) // abs(den)
|
||||
return q if (num < 0) == (den < 0) else -q
|
||||
|
||||
|
||||
def div_round(num, den):
|
||||
"""四舍五入(half away from zero);den 恒 > 0。
|
||||
|
||||
这是本仓库拟合算术**唯一的圆整口径**,不要换成 `(num + den/2)//den` 那种
|
||||
写法:Python 的 // 对负数是向负无穷取整,负数会在 -0.5 处折向 0,
|
||||
与期望的 half-away-from-zero 不同。先判符号再取绝对值,不依赖语言行为。
|
||||
"""
|
||||
if num >= 0:
|
||||
return (num + den // 2) // den
|
||||
return -((-num + den // 2) // den)
|
||||
|
||||
|
||||
def round_int(v):
|
||||
"""浮点 → 整数:二分之一远离零圆整(入参量化口径,与 to_milli 配对)。"""
|
||||
return int(v + 0.5) if v >= 0 else -int(-v + 0.5)
|
||||
|
||||
|
||||
def to_milli(v):
|
||||
"""读数 → 整数毫单位(mV / mA 域,即 fit() 的直接入参)。"""
|
||||
return round_int(v)
|
||||
|
||||
|
||||
def fit(xs, ys, k_q16=K_Q16, r_old_q16=None, b_old=0):
|
||||
"""N 点最小二乘标定求解(唯一实现)。返回 (rc, info);rc == 0 时 info 是解。
|
||||
|
||||
xs / ys 已是整数 mV / mA;r_old_q16 = 本轮读数采集时的变比 Q16 原值。
|
||||
整数路径决定 rc 与 r_q16/b;info 里的 a/res%/R2 是报告用浮点量,不参与判定。
|
||||
"""
|
||||
n = len(xs)
|
||||
if r_old_q16 is None:
|
||||
return ERR_ARG, None
|
||||
if (k_q16 <= 0) or (k_q16 > 100000000):
|
||||
return ERR_ARG, None
|
||||
if (r_old_q16 < R_MIN) or (r_old_q16 > R_MAX):
|
||||
return ERR_ARG, None
|
||||
if (n < N_MIN) or (n > N_MAX):
|
||||
return ERR_POINTS, None
|
||||
for v in list(xs) + list(ys):
|
||||
if abs(v) >= MILLI_MAX:
|
||||
return ERR_ARG, None
|
||||
|
||||
# 一趟:累加 + 单调性(相邻点方向必须一致)
|
||||
sx = sy = sxy = sxx = 0
|
||||
for i in range(n):
|
||||
x = xs[i]
|
||||
y = ys[i]
|
||||
sx += x
|
||||
sy += y
|
||||
sxy += x * y
|
||||
sxx += x * x
|
||||
if i > 0:
|
||||
dx = x - xs[i - 1]
|
||||
dy = y - ys[i - 1]
|
||||
if (dx > 0 and dy < 0) or (dx < 0 and dy > 0):
|
||||
return ERR_MONO, None
|
||||
|
||||
num = n * sxy - sx * sy
|
||||
den = n * sxx - sx * sx
|
||||
if den <= 0:
|
||||
return ERR_DEN, None
|
||||
|
||||
a_q16 = div_round(num * 65536, den)
|
||||
if a_q16 <= 0:
|
||||
return ERR_SLOPE, None
|
||||
|
||||
# 先按除法把 a_q16 夹到"r_new 不会越上界"的范围,再做 a_q16 × r_old——
|
||||
# 顺序反了会在极端点位下把 int64 顶穿(a_q16 可到 1e17 量级)
|
||||
if a_q16 > (R_MAX * 65536) // r_old_q16:
|
||||
return ERR_R_BAND, None
|
||||
r_new_q16 = (a_q16 * r_old_q16 + 32768) >> 16
|
||||
if (r_new_q16 < R_MIN) or (r_new_q16 > R_MAX):
|
||||
return ERR_R_BAND, None
|
||||
|
||||
# 截距 c:y = a·x + c ⇒ c = (Σy − a·Σx) / n。两遍式(用已算好的 a_q16)。
|
||||
# 若改成从 num/den 代数展开的 (Σy·den − num·Σx)/(n·den),分子峰值到 1.2e19,
|
||||
# 超出 int64 上界——Python 的大整数不会溢出,但这条路径要能直接照抄成
|
||||
# 定点实现,所以这里**刻意保持 int64 可容纳的两遍式**,别改成展开式
|
||||
c_milli = div_trunc(sy * 65536 - a_q16 * sx, n * 65536)
|
||||
if c_milli > C_MAX or c_milli < -C_MAX:
|
||||
return ERR_B_BAND, None # 截距域离谱 = b 必然越带(且护住下一步的 ×2^32)
|
||||
|
||||
# 截距折回引脚域,保留 Q16 亚毫伏精度(截到整 mV 会带来约 6.5 码 / mV 的 b
|
||||
# 量化,把真正的细分零点修正抹平);两步走,别一次乘完(会顶穿 int64)
|
||||
c_pin_q16 = div_round(c_milli * 65536 * 65536, r_new_q16)
|
||||
db = div_round(c_pin_q16 * 1000, k_q16)
|
||||
b_new = b_old + db
|
||||
if (b_new > B_MAX) or (b_new < -B_MAX):
|
||||
return ERR_B_BAND, None
|
||||
|
||||
# 残差:|y_i − (a·x_i + c)| 与满量程跨度比
|
||||
y_min = min(ys)
|
||||
y_max = max(ys)
|
||||
y_span = y_max - y_min
|
||||
res_max = 0
|
||||
for i in range(n):
|
||||
pred = div_round(a_q16 * xs[i], 65536) + c_milli
|
||||
res = ys[i] - pred
|
||||
if res < 0:
|
||||
res = -res
|
||||
if res > res_max:
|
||||
res_max = res
|
||||
if (res_max * 100) > (y_span * RES_PCT):
|
||||
return ERR_RESID, None
|
||||
|
||||
# 报告用浮点量(不参与判定)
|
||||
a_f = a_q16 / 65536.0
|
||||
y_mean = float(sy) / n
|
||||
ss_res = 0.0
|
||||
ss_tot = 0.0
|
||||
for i in range(n):
|
||||
d = ys[i] - (a_f * xs[i] + c_milli)
|
||||
t = ys[i] - y_mean
|
||||
ss_res += d * d
|
||||
ss_tot += t * t
|
||||
r2 = (1.0 - ss_res / ss_tot) if ss_tot > 0.0 else 0.0
|
||||
|
||||
return 0, dict(n=n, a_q16=a_q16, a=a_f, c_milli=c_milli,
|
||||
r_new_q16=r_new_q16, r_new=r_new_q16 / 65536.0,
|
||||
c_pin_mV=c_pin_q16 / 65536.0, db=db, b=b_new,
|
||||
res_max=res_max,
|
||||
res_pct=(100.0 * res_max / y_span) if y_span else 0.0,
|
||||
r2=r2, y_span=y_span,
|
||||
dev_pct=100.0 * (r_new_q16 / float(r_old_q16) - 1.0))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 输入解析
|
||||
def parse_floats(text):
|
||||
return [float(s) for s in text.split(",") if s.strip() != ""]
|
||||
|
||||
|
||||
def parse_map(text, defaults, conv):
|
||||
"""--r-old / --b-old:裸值作用于所有路,或 "ua=…,ia=…" 逐路给。"""
|
||||
if text is None:
|
||||
return dict(defaults)
|
||||
if "=" not in text:
|
||||
v = conv(text)
|
||||
return {k: v for k in defaults}
|
||||
out = dict(defaults)
|
||||
for part in text.split(","):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
k, v = part.split("=", 1)
|
||||
k = k.strip().lower()
|
||||
if k not in defaults:
|
||||
raise ValueError("unknown sem %r (use ua/un/ia)" % k)
|
||||
out[k] = conv(v)
|
||||
return out
|
||||
|
||||
|
||||
def parse_cal_table(path):
|
||||
"""提取 <!-- CALDATA:BEGIN --> ~ <!-- CALDATA:END --> 之间的长表。
|
||||
|
||||
列序:路 | 序号 | 屏上读数(mV/mA) | 注入已知量(V/A),每行一个点。
|
||||
返回 {sem: [(idx, x_s, y_s), ...]};标记不存在返回 None。
|
||||
两格全空 = 该点本轮没测(调用方跳过);只填一格 = 半填错误(报 INCOMPLETE)。
|
||||
"""
|
||||
with open(path, encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
m = re.search(r"<!--\s*CALDATA:BEGIN\s*-->(.*?)<!--\s*CALDATA:END\s*-->",
|
||||
text, re.S)
|
||||
if m is None:
|
||||
return None
|
||||
groups = {}
|
||||
for line in m.group(1).splitlines():
|
||||
line = line.strip()
|
||||
if not line.startswith("|"):
|
||||
continue
|
||||
cells = [c.strip() for c in line.strip("|").split("|")]
|
||||
sem = cells[0].lower()
|
||||
if sem not in SEM:
|
||||
continue # 表头行 / 分隔行自然跳过
|
||||
if len(cells) < 4:
|
||||
raise ValueError("row for %r has %d columns; need 4 "
|
||||
"[sem|idx|reading|injected]" % (sem, len(cells)))
|
||||
if len(cells) > 4:
|
||||
# 典型来源:没清空的**旧两点表**(6 列)。静默按前四列解会解出垃圾,
|
||||
# 所以这里明说——比让用户去猜"怎么 REJECT 了"省一轮台架
|
||||
raise ValueError("row for %r has %d columns (>4): that is the old "
|
||||
"two-point table; the N-point table needs exactly "
|
||||
"4 columns [sem|idx|reading|injected]"
|
||||
% (sem, len(cells)))
|
||||
groups.setdefault(sem, []).append((cells[1], cells[2], cells[3]))
|
||||
return groups
|
||||
|
||||
|
||||
def parse_cell(s):
|
||||
if s in ("", "-", "—"):
|
||||
return None
|
||||
return float(s)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 求解与输出
|
||||
def solve_and_print(sem, xs, ys, r_old_q16, b_old):
|
||||
"""解一路并打印判读;成功返回 (sem, slot, r_new, b),拒绝返回 None。"""
|
||||
ch = SEM[sem]
|
||||
rc, info = fit(xs, ys, K_Q16, r_old_q16, b_old)
|
||||
if rc != 0:
|
||||
reason = REASON[rc]
|
||||
if rc == ERR_POINTS:
|
||||
reason = reason % len(xs)
|
||||
print("sem=%s slot=%d n=%d REJECT %s" % (sem, ch["slot"], len(xs), reason))
|
||||
return None
|
||||
print("sem=%s slot=%d n=%d OK r=%.6f r_q16=%d b=%d a=%.9f res%%=%.3f "
|
||||
"R2=%.8f" % (sem, ch["slot"], info["n"], info["r_new"],
|
||||
info["r_new_q16"], info["b"], info["a"],
|
||||
info["res_pct"], info["r2"]))
|
||||
if abs(info["dev_pct"]) > 10.0:
|
||||
print("sem=%s WARN r_new dev %+.2f%% vs r_old=%.6f - all points must be "
|
||||
"read at the same r, and units are mV vs V"
|
||||
% (sem, info["dev_pct"], r_old_q16 / 65536.0))
|
||||
return (sem, ch["slot"], info["r_new"], info["b"])
|
||||
|
||||
|
||||
def write_all(done):
|
||||
"""末尾合并写入命令:先全部 r,再全部 b(cal_write.py 的两段顺序契约)。"""
|
||||
parts = ["--r 0x%04X=%.6f" % (0x8050 + s, r) for _, s, r, _ in done]
|
||||
parts += ["--b 0x%04X=%d" % (0x8048 + s, b) for _, s, _, b in done]
|
||||
print("WRITE-ALL: test/host/cal_write.py " + " ".join(parts))
|
||||
|
||||
|
||||
def run_single(a):
|
||||
missing = [n for n, v in (("--sem", a.sem), ("--x", a.x), ("--y", a.y))
|
||||
if v is None]
|
||||
if missing:
|
||||
print("ERROR: single-route mode needs %s (or use --from-doc)"
|
||||
% ",".join(missing))
|
||||
return 2
|
||||
try:
|
||||
xv = parse_floats(a.x)
|
||||
yv = parse_floats(a.y)
|
||||
r_map = parse_map(a.r_old, {s: SEM[s]["r_old"] for s in SEM}, float)
|
||||
b_map = parse_map(a.b_old, {s: 0 for s in SEM}, int)
|
||||
except ValueError as exc:
|
||||
print("ERROR: %s" % exc)
|
||||
return 2
|
||||
if len(xv) != len(yv):
|
||||
print("ERROR: --x has %d points, --y has %d" % (len(xv), len(yv)))
|
||||
return 2
|
||||
xs = [to_milli(v) for v in xv] # 屏上读数本来就是 mV/mA
|
||||
ys = [to_milli(v * 1000.0) for v in yv] # 注入已知量 V/A → mV/mA
|
||||
r_old_q16 = round_int(r_map[a.sem] * 65536.0)
|
||||
res = solve_and_print(a.sem, xs, ys, r_old_q16, b_map[a.sem])
|
||||
if res is None:
|
||||
return 2
|
||||
write_all([res])
|
||||
return 0
|
||||
|
||||
|
||||
def run_from_doc(a):
|
||||
try:
|
||||
groups = parse_cal_table(a.from_doc)
|
||||
except OSError as exc:
|
||||
print("BAD-DOC: %s" % exc)
|
||||
return 2
|
||||
except ValueError as exc:
|
||||
print("BAD-TABLE: %s" % exc)
|
||||
return 2
|
||||
if groups is None:
|
||||
print("NO-MARKER: no <!-- CALDATA:BEGIN/END --> table in %s" % a.from_doc)
|
||||
return 2
|
||||
if not groups:
|
||||
print("EMPTY: CALDATA table has no ua/un/ia rows")
|
||||
return 2
|
||||
try:
|
||||
r_map = parse_map(a.r_old, {s: SEM[s]["r_old"] for s in SEM}, float)
|
||||
b_map = parse_map(a.b_old, {s: 0 for s in SEM}, int)
|
||||
except ValueError as exc:
|
||||
print("ERROR: %s" % exc)
|
||||
return 2
|
||||
|
||||
done = []
|
||||
failed = 0
|
||||
for sem in sorted(groups):
|
||||
xs, ys = [], []
|
||||
for idx, x_s, y_s in groups[sem]:
|
||||
xv = parse_cell(x_s)
|
||||
yv = parse_cell(y_s)
|
||||
if xv is None and yv is None:
|
||||
continue # 整行空 = 本轮没测这个点
|
||||
if xv is None or yv is None:
|
||||
print("sem=%s idx=%s INCOMPLETE: need both reading and injected "
|
||||
"value (or leave both empty); skipped" % (sem, idx))
|
||||
failed += 1
|
||||
continue
|
||||
xs.append(to_milli(xv))
|
||||
ys.append(to_milli(yv * 1000.0))
|
||||
if not xs:
|
||||
print("sem=%s SKIP: no filled points" % sem)
|
||||
continue
|
||||
r_old_q16 = round_int(r_map[sem] * 65536.0)
|
||||
res = solve_and_print(sem, xs, ys, r_old_q16, b_map[sem])
|
||||
if res is None:
|
||||
failed += 1
|
||||
else:
|
||||
done.append(res)
|
||||
if done:
|
||||
write_all(done)
|
||||
return 0 if (not failed and done) else 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- selftest 回归
|
||||
# 金标向量:原固件单测(v2.1 的 tc_00b_fit)里的那一组,v2.2 固件侧拟合删除后
|
||||
# 搬到这里。输入与期望值**全部写死**,不在这里现算——现算等于自证,锚点就没意义了。
|
||||
#
|
||||
# 数据怎么来的(推导留档,便于将来重构时重新生成):
|
||||
# code = 500/700/900/1100/1300,真值 r_true=600.0、b_true=+12,
|
||||
# k_q16=1e7(TPAFE 152.587890625 µV/码),r_old=1000.0,b_old=0
|
||||
# 引脚电压 = code × 0.152587890625 mV → x(旧 r 下的屏上读数)= 该值 × 1000/1
|
||||
# y(注入已知量)= (code + 12) × 0.152587890625 × 600.0
|
||||
GOLDEN = {
|
||||
"x": [76294, 106812, 137329, 167847, 198364],
|
||||
"y": [46875, 65186, 83496, 101807, 120117],
|
||||
"k_q16": K_Q16,
|
||||
"r_old_q16": 1000 * 65536,
|
||||
"b_old": 0,
|
||||
"expect_rc": 0,
|
||||
"expect_r_q16": 39322000,
|
||||
"expect_b": 12,
|
||||
}
|
||||
|
||||
# 拒绝路径断言:(名称, xs, ys, r_old_q16, b_old, 期望 rc, 期望原因前缀)
|
||||
NG_CASES = [
|
||||
("n<3", [1000, 2000], [1000, 2000], 1000 * 65536, 0, ERR_POINTS, "[3,8]"),
|
||||
("x all same", [5000, 5000, 5000], [1000, 2000, 3000],
|
||||
1000 * 65536, 0, ERR_DEN, "x span collapsed"),
|
||||
("mono reversed", [1000, 2000, 3000, 4000, 5000],
|
||||
[1000, 2000, 3000, 2000, 1000], 1000 * 65536, 0, ERR_MONO, "non-monotonic"),
|
||||
("r out of band", [1000, 101000, 199999], [1000, 1001, 1002],
|
||||
1000 * 65536, 0, ERR_R_BAND, "out of band"),
|
||||
]
|
||||
|
||||
|
||||
def run_selftest():
|
||||
"""跑金标向量 + 拒绝路径断言。exit 0 = 全过,1 = 有失败。"""
|
||||
fails = 0
|
||||
total = 0
|
||||
|
||||
def chk(cond, label):
|
||||
nonlocal fails, total
|
||||
total += 1
|
||||
if cond:
|
||||
print("[PASS] %s" % label)
|
||||
else:
|
||||
print("[FAIL] %s" % label)
|
||||
fails += 1
|
||||
|
||||
g = GOLDEN
|
||||
rc, info = fit(g["x"], g["y"], g["k_q16"], g["r_old_q16"], g["b_old"])
|
||||
chk(rc == g["expect_rc"], "golden: rc == %d (got %d)" % (g["expect_rc"], rc))
|
||||
if info is None:
|
||||
chk(False, "golden: solver returned no solution")
|
||||
else:
|
||||
chk(info["r_new_q16"] == g["expect_r_q16"],
|
||||
"golden: r_q16 == %d (got %d)" % (g["expect_r_q16"], info["r_new_q16"]))
|
||||
chk(info["b"] == g["expect_b"],
|
||||
"golden: b == %d (got %d)" % (g["expect_b"], info["b"]))
|
||||
|
||||
for name, xs, ys, ro, bo, want_rc, want_txt in NG_CASES:
|
||||
rc, _ = fit(xs, ys, K_Q16, ro, bo)
|
||||
chk(rc == want_rc, "reject %-14s rc == %d (got %d)" % (name, want_rc, rc))
|
||||
chk(want_txt in REASON.get(want_rc, ""),
|
||||
"reject %-14s message mentions %r" % (name, want_txt))
|
||||
|
||||
print("=== cal_fit selftest: %d/%d PASSED (failed=%d) ===" %
|
||||
(total - fails, total, fails))
|
||||
return 0 if fails == 0 else 1
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description="N-point least-squares calibration solver "
|
||||
"(sole implementation; firmware no longer fits)")
|
||||
ap.add_argument("--selftest", action="store_true",
|
||||
help="run the golden-vector regression (exit 0 = all passed)")
|
||||
ap.add_argument("--sem", choices=sorted(SEM), default=None)
|
||||
ap.add_argument("--x", default=None,
|
||||
help="comma-separated on-screen readings (mV or mA)")
|
||||
ap.add_argument("--y", default=None,
|
||||
help="comma-separated injected values (V or A; x1000 inside)")
|
||||
ap.add_argument("--r-old", default=None,
|
||||
help="r in the device while readings were taken "
|
||||
"(default ua/un=1000.0, ia=29.4118; or ua=..,ia=..)")
|
||||
ap.add_argument("--b-old", default=None,
|
||||
help="b in the device while readings were taken "
|
||||
"(codes, default 0; or ua=..,ia=..)")
|
||||
ap.add_argument("--from-doc", default=None,
|
||||
help="read the CALDATA-marked long table from a doc")
|
||||
a = ap.parse_args()
|
||||
|
||||
if a.selftest:
|
||||
return run_selftest()
|
||||
if a.from_doc is not None:
|
||||
return run_from_doc(a)
|
||||
return run_single(a)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,224 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""两点标定离线计算器:镜像 App/algo/adc_cal.c adc_cal_two_point() 的整数算术。
|
||||
|
||||
台架执行卡:docs/测试说明/专项验证/ADC两点标定执行流程.md §3;
|
||||
公式与量化误差属主:docs/硬件平台/ADC通道映射说明.md §7.2;
|
||||
精度目标拍板前置:docs/算法/采集与算法层设计说明.md §13.13。
|
||||
|
||||
读数来源(见执行卡 §2):交流工况读串口 `meas:` 行 ua_mv/un_mv/ia_mv(= 端子域显示
|
||||
×1000;IA 一路该字段实为 mA);直流工况直接读 `adc:` 行码值并自行换算。
|
||||
|
||||
用法(两点量级 = 用户裁定 2026-11-12 安全优先,执行卡 §2):
|
||||
python test/host/cal_two_point.py --sem ua --lo-mv 5003 --hi-mv 50018 \
|
||||
--known-lo 5 --known-hi 50 [--zero-mv 4]
|
||||
python test/host/cal_two_point.py --sem ia --lo-mv 502 --hi-mv 2005 \
|
||||
--known-lo 0.5 --known-hi 2 [--zero-mv 3]
|
||||
|
||||
批量模式:把读数填进执行卡 §3 的标记表(<!-- CALDATA:BEGIN/END --> 之间),
|
||||
一条命令解所有已填路并给合并写入命令:
|
||||
python test/host/cal_two_point.py --from-doc \
|
||||
docs/测试说明/专项验证/ADC两点标定执行流程.md
|
||||
表列序:路(ua/un/ia) | 零点 meas | 低点 meas | 高点 meas | 已知低点 | 已知高点;
|
||||
空单元格=该路未填(跳过并提示),零点列可空。
|
||||
|
||||
--sem ua/un:电压族(r_old=1000.0 V/V,--known 填伏);ia:电流族
|
||||
(r_old=29.4118 A/V,--known 填安;µV 域按 µA 同域处理)。
|
||||
三路都是 TPAFE 源,k_q16 恒取 ROM 值 ADC_CAL_K_Q16_TPAFE。
|
||||
|
||||
输出纯 ASCII。exit 0 = 解出可写入;2 = 求解器拒绝(越带/方向错,先排查勿写)。
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
|
||||
# 语义槽位(App/algo/adc_input.h 枚举序:UA0/UB1/UC2/UN3/IA4/IB5/IC6/IN7)
|
||||
SEM = {
|
||||
"ua": dict(slot=0, r_old_q16=65536000), # 电压族 TPAFE
|
||||
"un": dict(slot=3, r_old_q16=65536000), # 电压族 TPAFE
|
||||
"ia": dict(slot=4, r_old_q16=1927532), # 电流族 TPAFE
|
||||
}
|
||||
K_Q16 = 10000000 # ADC_CAL_K_Q16_TPAFE(UA/UN/IA 三路同值)
|
||||
B_MAX = 8192 # ADC_CAL_B_MAX
|
||||
R_MIN = 65536 # ADC_CAL_R_MIN(=1.0)
|
||||
R_MAX = 2147483647 # ADC_CAL_R_MAX(Q16 上界)
|
||||
V_MAX = 1000000000 # ADC_CAL_V_MAX(±1000 域,严格不等号)
|
||||
# 工程合法带:run_par_cal_r_set() 粗守 [1.0, 32767.99](App/algo/run_par.c),
|
||||
# 求解侧同带,防「两点太近 / 没信号」解出天文数字直接写进设备。
|
||||
R_MIN_F = R_MIN / 65536.0
|
||||
R_MAX_F = 32767.99
|
||||
|
||||
|
||||
def to_uv(known):
|
||||
uv = int(round(known * 1.0e6))
|
||||
if uv <= -V_MAX or uv >= V_MAX:
|
||||
raise ValueError("known value outside +/-1000 domain (V or A)")
|
||||
return uv
|
||||
|
||||
|
||||
def two_point(c_lo, c_hi, v_lo, v_hi, k_q16):
|
||||
"""adc_cal_two_point() 的整数镜像(<<16、+half、截断除逐字一致)。"""
|
||||
dc = c_hi - c_lo
|
||||
dv = v_hi - v_lo
|
||||
if dc <= 0 or dv <= 0:
|
||||
return -1, 0, 0 # 两点必须同向递增
|
||||
t = ((dv << 16) + (k_q16 >> 1)) // k_q16 # = dc * r
|
||||
rq = ((t << 16) + (dc >> 1)) // dc # = r * 65536
|
||||
if rq < R_MIN or rq > 2147483647:
|
||||
return -1, 0, 0 # 变比越带:多半把 mV 当 V 填了
|
||||
tl = ((v_lo << 16) + (k_q16 >> 1)) // k_q16
|
||||
bb = ((tl << 16) // rq) - c_lo
|
||||
if bb > B_MAX or bb < -B_MAX:
|
||||
return -2, 0, 0 # 零点越 ±8192:先查前端,不用系数凑
|
||||
return 0, rq, bb
|
||||
|
||||
|
||||
def parse_cal_table(path):
|
||||
"""提取执行卡里 <!-- CALDATA:BEGIN --> ~ <!-- CALDATA:END --> 的 markdown 表。
|
||||
|
||||
返回 [(sem, [零点, 低点, 高点, 已知低, 已知高])](原始单元格字符串);
|
||||
标记不存在返回 None。表头行与分隔行按首列是否 ua/un/ia 自然跳过。
|
||||
"""
|
||||
with open(path, encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
m = re.search(r"<!--\s*CALDATA:BEGIN\s*-->(.*?)<!--\s*CALDATA:END\s*-->",
|
||||
text, re.S)
|
||||
if m is None:
|
||||
return None
|
||||
rows = []
|
||||
for line in m.group(1).splitlines():
|
||||
line = line.strip()
|
||||
if not line.startswith("|"):
|
||||
continue
|
||||
cells = [c.strip() for c in line.strip("|").split("|")]
|
||||
if len(cells) < 6 or cells[0].lower() not in SEM:
|
||||
continue
|
||||
rows.append((cells[0].lower(), cells[1:6]))
|
||||
return rows
|
||||
|
||||
|
||||
def cell_num(s):
|
||||
if s in ("", "-", "—"):
|
||||
return None
|
||||
return float(s)
|
||||
|
||||
|
||||
def solve_and_print(sem, zero, lo, hi, knlo, knhi):
|
||||
"""解一路并打印判读;成功返回 (sem, slot, r_new, b),拒绝返回 None。
|
||||
|
||||
直接在 meas 域比较(`meas:` 行已是乘过当前 r 的端子域读数,单位 mV/mA,
|
||||
低于 1 个码值时转码值会全 round 到 0,两点差为 0 无法求解)。
|
||||
公式:r_new = r_old × (known_hi - known_lo) × 1000 / (meas_hi - meas_lo)
|
||||
—— 读数与注入成正比,比例偏差全部由 r 吸收,故新 r = 旧 r × 真值比/读数比。
|
||||
b 由零点估计:b ≈ -zero_mv / (k_V × r_old × 1000)(code 域;零点是在旧 r
|
||||
下读的,故用 r_old 换算回码值,与 r 的新旧无关)。
|
||||
"""
|
||||
ch = SEM[sem]
|
||||
dm = hi - lo
|
||||
dk = knhi - knlo
|
||||
if dm <= 0 or dk <= 0:
|
||||
print("sem=%s slot=%d REJECT: hi>lo required (meas_hi=%.3f, known_hi=%s)" %
|
||||
(sem, ch["slot"], hi, knhi))
|
||||
return None
|
||||
r_old = ch["r_old_q16"] / 65536.0
|
||||
r_new = r_old * dk * 1000.0 / dm
|
||||
if r_new < R_MIN_F or r_new > R_MAX_F:
|
||||
print("sem=%s slot=%d REJECT: r_new=%.3f out of band [%.2f, %.2f] "
|
||||
"- two points too close or no signal (meas span=%.3f)" %
|
||||
(sem, ch["slot"], r_new, R_MIN_F, R_MAX_F, dm))
|
||||
return None
|
||||
dev = (r_new / r_old - 1.0) * 100.0
|
||||
# b:零点读数 → code 域偏移
|
||||
k_V = K_Q16 / 65536.0 * 1e-6 # V/code
|
||||
b = 0
|
||||
if zero is not None:
|
||||
b = -zero / (k_V * r_old * 1000.0)
|
||||
b = int(round(b))
|
||||
if abs(b) > B_MAX:
|
||||
print("sem=%s slot=%d REJECT: b=%d out of band +/-%d codes "
|
||||
"(zero reading off - check wiring/offset)" %
|
||||
(sem, ch["slot"], b, B_MAX))
|
||||
return None
|
||||
rq = int(round(r_new * 65536.0))
|
||||
print("sem=%s slot=%d meas_lo=%.3f meas_hi=%.3f known_lo=%s known_hi=%s" %
|
||||
(sem, ch["slot"], lo, hi, knlo, knhi))
|
||||
print("OK r_q16=%d r=%.6f b=%d codes (r_old=%.6f dev=%+.3f%%)" %
|
||||
(rq, r_new, b, r_old, dev))
|
||||
if abs(dev) > 50.0:
|
||||
print("WARN |dev|>50%%: suspect unit conversion or wiring, do not "
|
||||
"write yet")
|
||||
if zero is not None:
|
||||
print("ZERO CHECK: zero_mv=%.3f -> b=%d (should be small)" % (zero, b))
|
||||
return (sem, ch["slot"], r_new, b)
|
||||
|
||||
|
||||
def run_from_doc(path):
|
||||
rows = parse_cal_table(path)
|
||||
if rows is None:
|
||||
print("NO-MARKER: no <!-- CALDATA:BEGIN/END --> table in %s" % path)
|
||||
return 2
|
||||
if not rows:
|
||||
print("EMPTY: CALDATA table has no ua/un/ia rows")
|
||||
return 2
|
||||
done = []
|
||||
failed = 0
|
||||
for sem, cells in rows:
|
||||
try:
|
||||
zero, lo, hi, knlo, knhi = [cell_num(c) for c in cells]
|
||||
except ValueError:
|
||||
print("sem=%s bad-number cell %r: keep plain digits" % (sem, cells))
|
||||
failed += 1
|
||||
continue
|
||||
if lo is None or hi is None or knlo is None or knhi is None:
|
||||
print("sem=%s INCOMPLETE: need low/high meas + known-lo/known-hi "
|
||||
"(zero optional); skipped" % sem)
|
||||
failed += 1
|
||||
continue
|
||||
try:
|
||||
res = solve_and_print(sem, zero, lo, hi, knlo, knhi)
|
||||
except ValueError as e:
|
||||
print("sem=%s REJECT: %s" % (sem, e))
|
||||
res = None
|
||||
if res is None:
|
||||
failed += 1
|
||||
else:
|
||||
done.append(res)
|
||||
if done:
|
||||
parts = ["--r 0x%04X=%.6f" % (0x8050 + s, r) for _, s, r, _ in done]
|
||||
parts += ["--b 0x%04X=%d" % (0x8048 + s, b) for _, s, _, b in done]
|
||||
print("WRITE-ALL: test/host/cal_write.py " + " ".join(parts))
|
||||
return 2 if (failed or not done) else 0
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--sem", choices=sorted(SEM), default=None)
|
||||
ap.add_argument("--lo-mv", type=float, default=None)
|
||||
ap.add_argument("--hi-mv", type=float, default=None)
|
||||
ap.add_argument("--known-lo", type=float, default=None)
|
||||
ap.add_argument("--known-hi", type=float, default=None)
|
||||
ap.add_argument("--zero-mv", type=float, default=None)
|
||||
ap.add_argument("--from-doc", default=None,
|
||||
help="read CALDATA-marked table from execution doc")
|
||||
a = ap.parse_args()
|
||||
|
||||
if a.from_doc is not None:
|
||||
return run_from_doc(a.from_doc)
|
||||
|
||||
missing = [n for n, v in (("--sem", a.sem), ("--lo-mv", a.lo_mv),
|
||||
("--hi-mv", a.hi_mv), ("--known-lo", a.known_lo),
|
||||
("--known-hi", a.known_hi)) if v is None]
|
||||
if missing:
|
||||
ap.error("need %s (or use --from-doc)" % ",".join(missing))
|
||||
res = solve_and_print(a.sem, a.zero_mv, a.lo_mv, a.hi_mv,
|
||||
a.known_lo, a.known_hi)
|
||||
if res is None:
|
||||
return 2
|
||||
_, slot, r_new, b = res
|
||||
print("WRITE: test/host/cal_write.py --r 0x%04X=%.6f --b 0x%04X=%d" %
|
||||
(0x8050 + slot, r_new, 0x8048 + slot, b))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -5,7 +5,7 @@
|
||||
复用台架已验收主站库 test/host/iec104_master.py(不重写协议层)。
|
||||
地址(App/algo/run_par.c run_par_104_write):r = 0x8050+slot,
|
||||
b = 0x8048+slot;slot UA0/UN3/IA4。执行卡:
|
||||
docs/测试说明/专项验证/ADC两点标定执行流程.md §4。
|
||||
docs/测试说明/专项验证/ADC多点标定执行流程.md §4。
|
||||
|
||||
用法:
|
||||
python test/host/cal_write.py --r 0x8050=995.99 --b 0x8048=-1 \
|
||||
|
||||
Reference in New Issue
Block a user