1355dbe1b1
修复运行期改配置被打回出厂值的缺陷:cfg_file_poll() 按段循环复用同一 份读缓冲,而 s_parse() 通过 s_split_line()/s_key_value() 就地改写缓冲 (把换行与等号换成 NUL)。第一个段解析完,缓冲里已没有键值行,后续 7 个段静默保持出厂值;随后 run_par_store_save() 又把这份出厂值写回 config.ini,用户的标定值被永久覆盖。开机路径 cfg_file_load() 只解析一 轮,所以该缺陷长期不可见,只在运行中改配置时发作。 - s_parse() 改为非破坏性:入参 const char *,逐行 memcpy 到栈缓冲 (CFG_LINE_TEST_MAX + 1)后再交给会破坏缓冲的解析子函数;超长行跳 过(保存侧已拒绝落盘超长受管键行)。 - 新增 TC-CONFIG-063:同时改两段内容,断言第二个注册段也重载成功、 含负值 INT 键存活,把「热重载吞段」钉成机器面。 新增两点标定台架工具链(test/host/): - cal_two_point.py:整数算术镜像 adc_cal_two_point(),支持单路模式与 从执行卡 CALDATA 表批量求解(--from-doc),带合法带与零点自检拦截。 - cal_write.py:复用 iec104_master 作库,经 104 定值段写 r 后写 b 并 读回校验,是所有写入的唯一入口。 adc_task 的 meas: 心跳行精度提到 4 位小数,标定读数需要。 文档:新增《采样数据校正全流程》(端到端导航)与《ADC 两点标定执行 流程》(台架执行卡,含 CALDATA 可填表);ADC通道映射说明新增 §7.3 台 架实测结果(UA 554.687914、IA 29.634259,出厂默认值保持不变);配置 文件设计说明新增解析器契约 v1.19;采集与算法层设计说明 §13 系列与 README 索引同步。 台架实测:UA r=554.687914(出厂 1000.0 偏大 1.80 倍,根因是接线分压 使 PT 一次侧只拿到一半电压,实测与参数算定 606.06 只差 8.5%)、IA r=29.634259(与出厂 29.4118 只差 0.76%,反证链路未算错)。UN 按用户 裁定本轮不标定,保护闭锁(vl=0x11)是预期行为,未伪造 VALID。 同时入库 .dsh/roles 角色定义(原被 .gitignore 挡住)。
225 lines
9.2 KiB
Python
225 lines
9.2 KiB
Python
#!/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())
|