feat(adc): UN 标定完成、标定脚本归集到 test/host/cal/
CI / 静态门禁 (pre-commit) (push) Failing after 4m15s
CI / 主机侧单测 (MSVC) (push) Failing after 1m41s

- UN 通道 5 点最小二乘标定完成:r=555.382141, b=-1, 残差 0.038%, R²=0.99999967
- UA/IA/UN 三路 VALID 置位,保护解卡
- ROM 默认值更新为当前标定值(ADC_CAL_R_Q16_VOLT=555.144958)
- UB/UC/UN 通过 104 写入 ROM 默认值,config.ini 同步
- cal_fit.py 新增 ROLD 表自动读取,不再需要手传 --r-old
- 执行卡精简至 126 行,新增 cal_collect.py 采集脚本与 ROLD 填表
- 标定脚本(cal_fit/cal_write/cal_collect)归集到 test/host/cal/
- 全库文档路径引用同步更新(8 文件)
- UN 当前态文档改写:明确零序电压 U0 语义、本台架未接线状态
- 删除已失效的外部知识底座文档链接
This commit is contained in:
2026-11-13 11:26:36 +08:00
parent 7c505b2457
commit 40a2816a99
23 changed files with 327 additions and 331 deletions
+8 -8
View File
@@ -195,8 +195,8 @@ static int s_total = 0, s_passed = 0, s_failed = 0;
* STM32 侧 UB/UC 是电压族、IB/IC/IN 是电流族(V0.1 双转换器分配,见
* docs/硬件平台/ADC通道映射说明.md 与 adc_cal.c 的 ROM 表)。
* 容差只需盖住 float32 中间积舍入(相对 ~1e-7)与打印粒度。 */
#define EXP_R_VOLT (1000.0) /* 电压族变比 V/V */
#define EXP_R_CURR (29.4118) /* 电流族变比 A/V(1927532/65536 的十进制口径) */
#define EXP_R_VOLT (555.144958) /* 电压族变比 V/V */
#define EXP_R_CURR (29.610748) /* 电流族变比 A/V(1927532/65536 的十进制口径) */
static double exp_ratio(adc_semantic_t sem) {
switch (sem) {
@@ -463,14 +463,14 @@ static void tc_024_ratio_defaults(void) {
adc_cal_init();
/* 电压族 1000.0 V/V、电流族 29.4118 A/V——十进制口径在此独立写出,
* Q16 原值由 比值 × 65536 就地算出(1927532 = 29.4118 x 65536 的回落) */
CHECK(near_d((double)adc_cal_ratio(ADC_SEM_UA), EXP_R_VOLT), "UA ratio = 1000.0 V/V");
CHECK(near_d((double)adc_cal_ratio(ADC_SEM_UN), EXP_R_VOLT), "UN ratio = 1000.0 V/V");
CHECK(near_d((double)adc_cal_ratio(ADC_SEM_IA), EXP_R_CURR), "IA ratio = 29.4118 A/V");
CHECK(near_d((double)adc_cal_ratio(ADC_SEM_IN), EXP_R_CURR), "IN ratio = 29.4118 A/V");
CHECK(near_d((double)adc_cal_ratio(ADC_SEM_UA), EXP_R_VOLT), "UA ratio = 555.144958 V/V");
CHECK(near_d((double)adc_cal_ratio(ADC_SEM_UN), EXP_R_VOLT), "UN ratio = 555.144958 V/V");
CHECK(near_d((double)adc_cal_ratio(ADC_SEM_IA), EXP_R_CURR), "IA ratio = 29.610748 A/V");
CHECK(near_d((double)adc_cal_ratio(ADC_SEM_IN), EXP_R_CURR), "IN ratio = 29.610748 A/V");
CHECK(adc_cal_ratio_q16(ADC_SEM_UA) == (int32_t)my_lround(EXP_R_VOLT * 65536.0),
"UA ratio q16 = 65536000 (LSB exact)");
"UA ratio q16 = 36381980 (LSB exact)");
CHECK(adc_cal_ratio_q16(ADC_SEM_IA) == (int32_t)my_lround(EXP_R_CURR * 65536.0),
"IA ratio q16 = 1927532 (LSB exact)");
"IA ratio q16 = 1940570 (LSB exact)");
CHECK(adc_cal_ratio_configured(ADC_SEM_UA) == 1u, "UA ratio configured at boot");
CHECK(adc_cal_ratio_configured(ADC_SEM_IA) == 1u, "IA ratio configured at boot");
/* TEMP 不参与线性标定;越界通道同守卫 */
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""从串口日志采集 meas: 读数并填入执行卡 CALDATA 表。
用法:
cal_collect.py --doc <执行卡路径> --sem ua --point 1 [--samples N] [--log <日志路径>]
自动读取 serial.log 最近 N 拍(默认 15),取中位数,写入 CALDATA 表对应行。
重复运行同一 --point 会覆盖旧值。
"""
import argparse
import re
import statistics
import sys
import time
from pathlib import Path
DEFAULT_LOG = ".dsh/skills/serial-assistant/scripts/state/serial.log"
SEM_FIELD = {"ua": "ua_mv", "un": "un_mv", "ia": "ia_mv"}
def read_median(log_path, sem, n_samples=15, min_val=None):
field = SEM_FIELD.get(sem)
if not field:
print("ERROR: unknown sem %r (use ua/un/ia)" % sem)
return None
text = Path(log_path).read_text(encoding="utf-8", errors="replace")
lines = text.strip().splitlines()
meas = [l for l in lines if "meas:" in l and field + "=" in l]
recent = meas[-n_samples * 2:] if len(meas) >= n_samples * 2 else meas
vals = []
for l in recent:
m = re.search(field + r"=(\d+\.?\d*)", l)
if m:
v = float(m.group(1))
if min_val is not None and v < min_val:
continue
vals.append(v)
if not vals:
return None
return round(statistics.median(vals))
def update_caldata(doc_path, sem, point, value):
text = Path(doc_path).read_text(encoding="utf-8")
m = re.search(r"(<!--\s*CALDATA:BEGIN\s*-->.*?<!--\s*CALDATA:END\s*-->)", text, re.S)
if not m:
print("ERROR: no CALDATA markers in %s" % doc_path)
return False
block = m.group(1)
pattern = r"(\|\s*%s\s*\|\s*%d\s*\|\s*)(\S*|)(\s*\|.*)" % (re.escape(sem), point)
new_block, count = re.subn(pattern, r"\g<1>%d\3" % value, block)
if count != 1:
print("ERROR: expected 1 match for %s point %d, got %d" % (sem, point, count))
return False
text = text.replace(block, new_block)
Path(doc_path).write_text(text, encoding="utf-8", newline="\n")
return True
def main():
ap = argparse.ArgumentParser(description="采集串口读数填入 CALDATA 表")
ap.add_argument("--doc", required=True, help="执行卡文档路径")
ap.add_argument("--sem", required=True, choices=["ua", "un", "ia"], help="通道")
ap.add_argument("--point", required=True, type=int, help="点序号 (1-5)")
ap.add_argument("--samples", type=int, default=15, help="采样拍数 (默认 15)")
ap.add_argument("--log", default=DEFAULT_LOG, help="串口日志路径")
ap.add_argument("--min-val", type=float, default=None,
help="过滤低于此值的读数(切换瞬态)")
ap.add_argument("--wait", type=float, default=3.0,
help="等待新数据到达的秒数 (默认 3)")
a = ap.parse_args()
if a.point < 1 or a.point > 8:
print("ERROR: point must be 1-8")
return 1
time.sleep(a.wait)
med = read_median(a.log, a.sem, a.samples, a.min_val)
if med is None:
print("ERROR: no valid %s data in last %d samples" % (a.sem, a.samples))
return 1
print("%s point %d: %d mV/mA (median of %d samples)" % (a.sem, a.point, med, a.samples))
if not update_caldata(a.doc, a.sem, a.point, med):
return 1
print("OK: updated CALDATA table in %s" % a.doc)
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -308,6 +308,34 @@ def parse_cal_table(path):
return groups
def parse_rold_table(path):
"""提取 <!-- ROLD:BEGIN --> ~ <!-- ROLD:END --> 之间的 r/b 旧值表。
列序:路 | r_old | b_old [| 备注],返回 {sem: {"r": float, "b": int}}。
标记不存在或表为空返回空 dict(调用方回退到 --r-old / SEM 默认)。
"""
with open(path, encoding="utf-8") as f:
text = f.read()
m = re.search(r"<!--\s*ROLD:BEGIN\s*-->(.*?)<!--\s*ROLD:END\s*-->",
text, re.S)
if m is None:
return {}
result = {}
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) < 3 or not cells[1] or not cells[2]:
continue
try:
result[sem] = {"r": float(cells[1]), "b": int(float(cells[2]))}
except ValueError:
continue
return result
def parse_cell(s):
if s in ("", "-", "—"):
return None
@@ -386,9 +414,16 @@ def run_from_doc(a):
if not groups:
print("EMPTY: CALDATA table has no ua/un/ia rows")
return 2
# ROLD table in doc overrides --r-old defaults; CLI --r-old still wins if given explicitly
rold = parse_rold_table(a.from_doc)
defaults = {s: SEM[s]["r_old"] for s in SEM}
if rold:
for s in rold:
defaults[s] = rold[s]["r"]
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)
r_map = parse_map(a.r_old, defaults, float)
b_defaults = {s: (rold[s]["b"] if s in rold else 0) for s in SEM}
b_map = parse_map(a.b_old, b_defaults, int)
except ValueError as exc:
print("ERROR: %s" % exc)
return 2
+8 -8
View File
@@ -282,10 +282,10 @@ static void tc_104_read(void)
"TC-RUNPAR-004 k echo last channel (IN, STM32 ROM)");
CHECK((run_par_104_read(0x8048u, &v) == 0) && (v == 0.0f),
"TC-RUNPAR-004 b echo factory zero");
CHECK((run_par_104_read(0x8050u, &v) == 0) && (fabsf(v - 1000.0f) <= 1e-3f),
"TC-RUNPAR-004 r echo first slot = ROM voltage family 1000.0");
CHECK((run_par_104_read(0x8054u, &v) == 0) && (fabsf(v - 29.4118f) <= 1e-3f),
"TC-RUNPAR-004 r echo current family = 29.4118");
CHECK((run_par_104_read(0x8050u, &v) == 0) && (fabsf(v - 555.144958f) <= 1e-3f),
"TC-RUNPAR-004 r echo first slot = ROM voltage family 555.144958");
CHECK((run_par_104_read(0x8054u, &v) == 0) && (fabsf(v - 29.610748f) <= 1e-3f),
"TC-RUNPAR-004 r echo current family = 29.610748");
CHECK(run_par_104_read(0x8058u, &v) == -1, "TC-RUNPAR-004 above r block not owned");
}
@@ -420,14 +420,14 @@ static void tc_104_ratio(void)
/* 选择段:只粗守(NaN/Inf),0.5 这类"执行段会拒"的值在选择段返 0 */
CHECK(run_par_104_write(0x8051u, 0.5f, 1u) == 0,
"TC-RUNPAR-009 select only guards finiteness (0.5 passes)");
CHECK((run_par_104_read(0x8051u, &v) == 0) && (fabsf(v - 1000.0f) <= 1e-3f),
CHECK((run_par_104_read(0x8051u, &v) == 0) && (fabsf(v - 555.144958f) <= 1e-3f),
"TC-RUNPAR-009 select did not touch the ratio");
CHECK(run_par_104_write(0x8051u, 0.5f, 0u) == -1, "TC-RUNPAR-009 execute rejects the same 0.5");
CHECK((run_par_104_read(0x8051u, &v) == 0) && (fabsf(v - 1000.0f) <= 1e-3f),
CHECK((run_par_104_read(0x8051u, &v) == 0) && (fabsf(v - 555.144958f) <= 1e-3f),
"TC-RUNPAR-009 rejected execute left the ratio intact");
/* 段边界:0x8057 是 r 块末地址,0x8058 起非本段 */
CHECK(run_par_104_write(0x8057u, 29.4118f, 0u) == 0, "TC-RUNPAR-009 last r slot writable");
CHECK(run_par_104_write(0x8057u, 29.610748f, 0u) == 0, "TC-RUNPAR-009 last r slot writable");
CHECK(run_par_104_write(0x8058u, 2.0f, 0u) == -2, "TC-RUNPAR-009 0x8058 passes through to prot_set");
CHECK(run_par_104_read(0x8058u, &v) == -1, "TC-RUNPAR-009 0x8058 not owned on read");
@@ -438,7 +438,7 @@ static void tc_104_ratio(void)
"TC-RUNPAR-009 save cleared dirty and wrote file");
CHECK(strstr((const char *)s_vfs, "r_ua=131072000") != NULL,
"TC-RUNPAR-009 file carries r_ua=131072000 (= 2000 x 65536)");
(void)run_par_104_write(0x8050u, 1000.0f, 0u); /* 覆盖内存态 */
(void)run_par_104_write(0x8050u, 555.144958f, 0u); /* 覆盖内存态 */
CHECK(cfg_file_load() == 0, "TC-RUNPAR-009 engine reload ok");
CHECK((run_par_104_read(0x8050u, &v) == 0) && (fabsf(v - 2000.0f) <= 0.01f),
"TC-RUNPAR-009 reload restores saved ratio");