#!/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 的标记表( 之间), 一条命令解所有已填路并给合并写入命令: 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): """提取执行卡里 ~ 的 markdown 表。 返回 [(sem, [零点, 低点, 高点, 已知低, 已知高])](原始单元格字符串); 标记不存在返回 None。表头行与分隔行按首列是否 ua/un/ia 自然跳过。 """ with open(path, encoding="utf-8") as f: text = f.read() m = re.search(r"(.*?)", 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 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())