Files
STM32F4-Base/test/host/cal_two_point.py
T
wandering1 1355dbe1b1 fix(cfg): 配置热重载吞段缺陷修复 + 两点标定工具链与台架实测落档
修复运行期改配置被打回出厂值的缺陷: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 挡住)。
2026-11-13 01:35:58 +08:00

225 lines
9.2 KiB
Python
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.
#!/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())