feat(adc): 新增变比维 r 三维折算,录波段格式升 v2
采样板注入 Ua=5V/Ia=0.5A 读数偏小,根因是固件只输出引脚电压、 缺「端子→引脚」变比。折算改三维: 工程量 = (code + b) × k[µV/码] × 1e-6 × r k 保持 ROM 引脚 LSB,b 为零点,r 为端子变比(int32 Q16)。 变比不能并进 k:UA 端子域 k 达 1.5e5 µV/码、Q16 溢出 int32; r 单独存 Q16 不溢出。两点标定改标 r,k 仍是板级事实。 - 新开单段 [adc_cal_sec](8 键 r_ua..r_in),cfg 段数达 8/8 上限 - 一次变比复用 prot_set 四项,不新开段;r=0 表示本路不折算 - ROM 默认带台架实测变比(电压族 1000、电流族 29.4118), 烧进固件即显二次值;VALID 仍须两点标定,两者互不派生 - eng 判据改「每路要么已配变比、要么削顶不显示」,不再看 VALID - 104 地址 0x8050~0x8057 写 r,run_par_cal_r_set 写值与置脏同函数 - 录波 v2:通道表加 int32 r_q16,头 128→160、整段 5250→5282; code_to_milli 用余数分解换等价单次圆整,20 万组大整数对拍 0 不符 - 文档同步:采集与算法层/通道映射/故障录波/网络配置/配置文件/ 产品需求/内存台账,历史带日期实测数字按冻结口径不改 - 单测 13 套全绿,/keil-build 0 错误 0 警告(256024/31760/ 2080/152368),RAM 实测回填内存内容分配策略 §6.1
This commit is contained in:
+114
-44
@@ -74,15 +74,18 @@ App/algo/wave_store.c must agree byte-for-byte -- if they ever diverge,
|
||||
report it, do not silently re-rule from one side):
|
||||
- CRC-16/MODBUS table-free (0x8005 reflected / init 0xFFFF / xorout 0),
|
||||
both CRC fields stored LOW byte first; anchor "123456789" -> 0x4B37.
|
||||
Header CRC covers [0, 30 + 12*ch_count); data CRC covers the data area
|
||||
and sits at end of file; 8ch/320pts whole segment = 5250 B.
|
||||
- Channel table entry (12 B): sem 1 + src 1 + bits 1 + availability 1
|
||||
Header CRC covers [0, 30 + 16*ch_count); data CRC covers the data area
|
||||
and sits at end of file; 8ch/320pts whole segment = 5282 B.
|
||||
- Channel table entry (16 B, v2): sem 1 + src 1 + bits 1 + availability 1
|
||||
(= adc_cal_flags(sem) low 8 bits, raw, no invented bit meanings) +
|
||||
k_q16 i32 (uV/code * 65536) + b_codes i32 (code domain). There is NO
|
||||
k_q16 i32 (uV/code * 65536) + b_codes i32 (code domain) +
|
||||
r_q16 i32 (ratio * 65536, 0 = no scaling on this channel). There is NO
|
||||
separate LSB field: LSB == k_q16, uV = k_q16/65536, never re-scaled.
|
||||
mV = (code + b_codes) * k_q16/65536/1000 -- exactly the
|
||||
adc_cal_apply() formula. Header eng only LABELS the unit (0 =
|
||||
pin-domain, 1 secondary, 2 primary); it never changes arithmetic.
|
||||
mV = (code + b_codes) * k_q16/65536/1000, then * r_q16/65536 when
|
||||
r_q16 != 0 -- exactly the adc_cal_apply() formula, computed in the same
|
||||
TWO integer steps as the C side (one expression overflows int64).
|
||||
Header eng only LABELS the unit (0 = pin-domain, 1 = secondary,
|
||||
2 = primary); it never changes arithmetic.
|
||||
- flags_seq bit s = ADC_CAL_F_VALID of sem s; clip_mask bit s = sem s
|
||||
(same mapping as App/algo/meas.h; V0.1 constant 0xE6 = UB UC IB IC IN).
|
||||
avail is the RAW pass-through of adc_cal_flags(): VALID and CLIPPED are
|
||||
@@ -91,17 +94,19 @@ report it, do not silently re-rule from one side):
|
||||
VALID/CLIPPED relation nor on avail-vs-clip_mask drift -- those get a
|
||||
soft note only ("VALID+CLIPPED(both)" in dump/fft, a WARN line in dump),
|
||||
no exit-code change, nothing refused.
|
||||
eng likewise derives from flags_seq on the board (feed_eng(): all 8
|
||||
electrical channels VALID -> secondary(1), else pin-domain(0)); a
|
||||
pairing mismatch here is also a SOFT WARN only, never a rejection.
|
||||
- v1 pins ch_count = 8 (TEMP not recorded; TEMP needs a version bump
|
||||
together with the TEMP-in-flags question); PC must verify table entry
|
||||
eng derives from the per-channel RATIO on the board (feed_eng(): every
|
||||
electrical channel either has r configured or is clipped-and-not-shown);
|
||||
a pairing mismatch here is also a SOFT WARN only, never a rejection.
|
||||
- v1/v2 pins ch_count = 8 (TEMP not recorded; TEMP needs a version bump
|
||||
together with the TEMP-in-flags question); v2 = v1 + i32 r_q16 per
|
||||
channel-table entry (12 -> 16 B, head 128 -> 160, segment 5250 -> 5282).
|
||||
PC must verify table entry
|
||||
i carries sem == i (ascending UA..IN), mismatch voids the whole
|
||||
segment; data column i == table entry i; samples int16 LE, TPAFE two's
|
||||
complement, STM32 0..4095 stored positive with NO midpoint bias
|
||||
(biasing it would fabricate bipolarity).
|
||||
- t0_ms / wall / frame_seq are u32 little-endian. ver must equal magic
|
||||
byte 2 and both must equal 1, else refuse.
|
||||
byte 2 and both must equal FMT_VER, else refuse.
|
||||
"""
|
||||
import argparse
|
||||
import math
|
||||
@@ -114,7 +119,7 @@ import tempfile
|
||||
# ---- format constants (design section 6.2, little-endian) -------------------
|
||||
MAGIC_W = 0x57 # 'W'
|
||||
MAGIC_R = 0x52 # 'R'
|
||||
FMT_VER = 1 # byte 2 of magic AND byte 3 (ver) both == 1
|
||||
FMT_VER = 2 # byte 2 of magic AND byte 3 (ver) both == 2
|
||||
SEG_LABEL_SAMPLE = "WAVE%02d" % 0 # sample segment label, produced by format so
|
||||
# this file never carries a hardcoded letter+digit identifier token
|
||||
|
||||
@@ -133,10 +138,11 @@ OFF_FLAGS = 23 # 1 validity bitmap (see Q5)
|
||||
OFF_CLIP = 24 # 2 clip bitmap, bit s = sem s (see Q6)
|
||||
OFF_FSEQ = 26 # 4 frame_seq at segment head
|
||||
HDR_FIXED = 30 # bytes before channel table
|
||||
ENT_SIZE = 12 # bytes per channel-table entry
|
||||
ENT_SIZE = 16 # bytes per channel-table entry (v2: 12 -> 16)
|
||||
ENT_SEM, ENT_SRC, ENT_BITS, ENT_AVAIL = 0, 1, 2, 3
|
||||
ENT_KQ = 4 # i32 Q16 uV/code == the LSB (sections 6.2/6.3)
|
||||
ENT_B = 8 # i32 code-domain zero offset
|
||||
ENT_RQ16 = 12 # i32 Q16 ratio (terminal/primary ratio); 0 = no scaling
|
||||
|
||||
SRC_TPAFE, SRC_STM32 = 0, 1
|
||||
SRC_NAMES = {SRC_TPAFE: "TPAFE5160", SRC_STM32: "STM32-ADC1"}
|
||||
@@ -198,17 +204,55 @@ def put_crc16(value):
|
||||
# ---- decode helpers ----------------------------------------------------------
|
||||
def lsb_uv(ent):
|
||||
"""LSB in micro-volts per code = k_q16 / 65536 (section 6.3: k_q16 IS
|
||||
the LSB; never re-multiplied)."""
|
||||
the LSB; never re-multiplied). This is the PIN-side slope only -- the
|
||||
terminal ratio (r_q16) is a separate factor, see code_to_mv()."""
|
||||
return ent["k_q16"] / 65536.0
|
||||
|
||||
|
||||
def ent_scale_mv(ent):
|
||||
"""Floating-point mV per code in the RECORDING DOMAIN (test expectation
|
||||
helper): k slope via lsb_uv(), times the Q16 ratio when r_q16 != 0.
|
||||
Analytic expectations (e.g. sine RMS) use this so they stay in step with
|
||||
code_to_mv() without duplicating the two-step integer rounding."""
|
||||
base = lsb_uv(ent) / 1000.0
|
||||
if ent["r_q16"] == 0:
|
||||
return base
|
||||
return base * ent["r_q16"] / 65536.0
|
||||
|
||||
|
||||
def code_to_mv(ent, code):
|
||||
"""Code -> mV via exactly the adc_cal_apply() formula:
|
||||
value = (code + b) * k[us/code] * 1e-6 [V] -> mV = that * 1000.
|
||||
The header eng field only labels the unit (0 = pin-domain); per
|
||||
section 6.3 it must never change this arithmetic, and the PC must not
|
||||
re-guess units from the current firmware state."""
|
||||
return (code + ent["b_codes"]) * lsb_uv(ent) / 1000.0
|
||||
"""Code -> engineering millivolt, mathematically identical to the C side
|
||||
(App/algo/wave_store.c wave_store_code_to_milli); a divergent rounding
|
||||
rule would make the two independent implementations disagree on the last
|
||||
digit of every sample, so the rule is mirrored on purpose.
|
||||
|
||||
t = (code + b_codes) * k_q16 uV * 65536, up to ~2.2e12
|
||||
r_q16 == 0 -> mv = round_half_away(t / 65536000) (= 1000 * 2**16)
|
||||
r_q16 != 0 -> mv = round_half_away(t * r_q16 / (1000 * 2**32))
|
||||
|
||||
One rounding, at the end, in the recording DOMAIN: secondary mV/mA when
|
||||
r_q16 != 0, pin mV when r_q16 == 0. The header eng field only labels
|
||||
which of those it is; it never changes this arithmetic.
|
||||
|
||||
Why the C side cannot write that one expression: t * r_q16 reaches
|
||||
4.6e21, past int64 (9.2e18), so it splits t = 1000*q + s and recombines
|
||||
the remainders -- algebraically the same single rounding. Python has
|
||||
arbitrary-precision ints, so this reader evaluates the exact value and
|
||||
keeps the two implementations in step.
|
||||
"""
|
||||
t = (code + ent["b_codes"]) * ent["k_q16"]
|
||||
r = ent["r_q16"]
|
||||
if r == 0:
|
||||
return _div_round(t, 65536000)
|
||||
return _div_round(t * r, 1000 * (1 << 32))
|
||||
|
||||
|
||||
def _div_round(num, den):
|
||||
"""Integer division rounding half AWAY from zero -- mirrors
|
||||
wave_store.c wave_st_div_round() including the negative branch."""
|
||||
if num >= 0:
|
||||
return (num + den // 2) // den
|
||||
return -((-num + den // 2) // den)
|
||||
|
||||
|
||||
def rail_thresholds(ent):
|
||||
@@ -303,6 +347,7 @@ def parse_segment(buf, name="segment"):
|
||||
"avail": buf[o + ENT_AVAIL],
|
||||
"k_q16": struct.unpack_from("<i", buf, o + ENT_KQ)[0],
|
||||
"b_codes": struct.unpack_from("<i", buf, o + ENT_B)[0],
|
||||
"r_q16": struct.unpack_from("<i", buf, o + ENT_RQ16)[0],
|
||||
})
|
||||
|
||||
samples = []
|
||||
@@ -472,20 +517,20 @@ def fmt_dump(seg):
|
||||
% (seg["crc_data"], seg["pts"] * seg["chc"] * 2,
|
||||
"low" if CRC_LO_FIRST else "high"))
|
||||
add("-- channel table (%d entries x %d B) --" % (seg["chc"], ENT_SIZE))
|
||||
add("%3s %3s %-5s %-10s %4s %5s %11s %9s %12s %10s"
|
||||
add("%3s %3s %-5s %-10s %4s %5s %11s %9s %11s %12s %10s"
|
||||
% ("IDX", "SEM", "NAME", "SRC", "BITS", "AVAIL", "K_Q16",
|
||||
"B_CODES", "LSB_uV", "FLAG"))
|
||||
"B_CODES", "R_Q16", "LSB_uV", "FLAG"))
|
||||
for ent in seg["entries"]:
|
||||
fl = []
|
||||
if ent["avail"] & ADC_CAL_F_VALID:
|
||||
fl.append("VALID")
|
||||
if ent["avail"] & ADC_CAL_F_CLIPPED:
|
||||
fl.append("CLIPPED")
|
||||
add("%3d %3d %-5s %-10s %4d 0x%02X %11d %9d %12.6f %10s"
|
||||
add("%3d %3d %-5s %-10s %4d 0x%02X %11d %9d %11d %12.6f %10s"
|
||||
% (ent["idx"], ent["sem"], sem_name(ent["sem"]),
|
||||
SRC_NAMES.get(ent["src"], "SRC%d" % ent["src"]),
|
||||
ent["bits"], ent["avail"], ent["k_q16"], ent["b_codes"],
|
||||
lsb_uv(ent), "|".join(fl) if fl else "."))
|
||||
ent["r_q16"], lsb_uv(ent), "|".join(fl) if fl else "."))
|
||||
# lead ruling 2026-10-29: soft notes, never a reject, never an exit code
|
||||
both = [sem_name(e["sem"]) for e in seg["entries"]
|
||||
if (e["avail"] & (ADC_CAL_F_VALID | ADC_CAL_F_CLIPPED))
|
||||
@@ -583,9 +628,11 @@ def fmt_fft(seg, cycle, all_cycles, only_ch):
|
||||
SRC_NAMES.get(ent["src"], "?"), "ALL",
|
||||
sum(rmss) / len(rmss), min(rmss), max(rmss),
|
||||
len(rmss)))
|
||||
lines.append("note: mV = (code + b_codes) * k_q16/65536/1000 "
|
||||
"(adc_cal_apply formula, unit labeled by header eng, "
|
||||
"never re-guessed); full-cycle window, coefficients = "
|
||||
lines.append("note: mV = (code + b_codes) * k_q16/65536/1000, then "
|
||||
"* r_q16/65536 when r_q16 != 0 (adc_cal_apply formula, same "
|
||||
"two integer steps as the C side; result is in the "
|
||||
"recording domain, labeled by header eng, never "
|
||||
"re-guessed); full-cycle window, coefficients = "
|
||||
"sin/cos(2*pi*k/N)*sqrt(2)/N as in App/algo/phasor.c; "
|
||||
"phase = atan2(im,re) deg")
|
||||
return lines
|
||||
@@ -795,7 +842,7 @@ def cmd_plot(args):
|
||||
|
||||
|
||||
# ---- synthetic segment builders (selftest) ------------------------------------
|
||||
def syn_entries(chc=8, avail_mode="v01"):
|
||||
def syn_entries(chc=8, avail_mode="v01", ratio_mode="pin"):
|
||||
"""avail = raw pass-through of adc_cal_flags(). Modes (lead ruling
|
||||
2026-10-29 asks for the first two as interop shapes):
|
||||
v01 = V0.1 board truth, pre-calibration: TPAFE trio 0x00, STM32 five
|
||||
@@ -803,15 +850,34 @@ def syn_entries(chc=8, avail_mode="v01"):
|
||||
both = deliberately VALID+CLIPPED on the five: 0x01 TPAFE / 0x03 STM32
|
||||
(legal: soft note only, must still parse clean)
|
||||
none = all-zero avail under clip_mask 0xE6 (avail/header drift; soft
|
||||
WARN only, must still parse clean)"""
|
||||
WARN only, must still parse clean)
|
||||
|
||||
ratio_mode picks the v2 r_q16 snapshot (ROM defaults from adc_cal.h):
|
||||
rom = board defaults: voltage family 65536000 (1000.0), current family
|
||||
1927532 (29.4118 A/V) -- sem 0/3 voltage (UA/UN), 4 current (IA)
|
||||
pin = all zero r_q16 (no scaling; output stays pin-domain mV)
|
||||
one = all 65536 (ratio exactly 1.0: must equal the old pin-domain value)
|
||||
"""
|
||||
pa_t, pa_s = {"v01": (0x00, 0x02), "both": (0x01, 0x03),
|
||||
"none": (0x00, 0x00)}[avail_mode]
|
||||
rom = {0: 65536000, 3: 65536000, 4: 1927532}
|
||||
ents = []
|
||||
for sem in range(chc):
|
||||
if sem in (0, 3, 4):
|
||||
ents.append((sem, SRC_TPAFE, 16, pa_t, 10000000, 0))
|
||||
kq16 = 10000000
|
||||
src, bits, avail = SRC_TPAFE, 16, pa_t
|
||||
else:
|
||||
ents.append((sem, SRC_STM32, 12, pa_s, 52800000, 0))
|
||||
kq16 = 52800000
|
||||
src, bits, avail = SRC_STM32, 12, pa_s
|
||||
if ratio_mode == "rom":
|
||||
rq16 = rom.get(sem, 0)
|
||||
elif ratio_mode == "pin":
|
||||
rq16 = 0
|
||||
elif ratio_mode == "one":
|
||||
rq16 = 65536
|
||||
else:
|
||||
raise ValueError("unknown ratio_mode %r" % (ratio_mode,))
|
||||
ents.append((sem, src, bits, avail, kq16, 0, rq16))
|
||||
return ents
|
||||
|
||||
|
||||
@@ -842,7 +908,7 @@ def syn_samples(pts=320, pps=20, chc=8, rail=True):
|
||||
|
||||
def build_synthetic(chc=8, pts=320, pps=20, trig_src=1, clip=0x00E6,
|
||||
flags=0x00, wall=0, t0=123456, fseq=987654321,
|
||||
avail_mode="v01", eng=0, rail=True):
|
||||
avail_mode="v01", eng=0, rail=True, ratio_mode="pin"):
|
||||
hdr = bytearray()
|
||||
hdr += bytes([MAGIC_W, MAGIC_R, FMT_VER, FMT_VER])
|
||||
hdr += struct.pack("<H", 1000)
|
||||
@@ -856,10 +922,10 @@ def build_synthetic(chc=8, pts=320, pps=20, trig_src=1, clip=0x00E6,
|
||||
hdr += struct.pack("<H", clip)
|
||||
hdr += struct.pack("<I", fseq)
|
||||
assert len(hdr) == HDR_FIXED
|
||||
for sem, src, bits, avail, kq16, b_codes in syn_entries(chc,
|
||||
avail_mode)[:chc]:
|
||||
for sem, src, bits, avail, kq16, b_codes, rq16 in syn_entries(
|
||||
chc, avail_mode, ratio_mode)[:chc]:
|
||||
hdr += bytes([sem, src, bits, avail])
|
||||
hdr += struct.pack("<ii", kq16, b_codes)
|
||||
hdr += struct.pack("<iii", kq16, b_codes, rq16)
|
||||
hdr += put_crc16(crc16_modbus(hdr))
|
||||
data = b"".join(struct.pack("<%dh" % pts, *col)
|
||||
for col in syn_samples(pts, pps, chc, rail))
|
||||
@@ -915,7 +981,7 @@ def selftest():
|
||||
# parser (both CRC gates passing inside parse == implicit PASS)
|
||||
img = build_synthetic()
|
||||
seg = parse_segment(img, name="synth8")
|
||||
ck.check("size 8ch/320pts == 5250", seg["size"] == 5250,
|
||||
ck.check("size 8ch/320pts == 5282", seg["size"] == 5282,
|
||||
"got %d" % seg["size"])
|
||||
fields = dict(fs_hz=1000, pps=20, chc=8, pts=320, pre=160, trig_src=1,
|
||||
trig_elm=0, t0_ms=123456, wall=0, eng=0, flags_seq=0,
|
||||
@@ -923,14 +989,18 @@ def selftest():
|
||||
for k in sorted(fields):
|
||||
ck.check("field %s round-trip" % k, seg[k] == fields[k],
|
||||
"got %r" % (seg[k],))
|
||||
ck.check("table entry 0 == sem0/src0/bits16/avail0x00/k_q16 10000000",
|
||||
ck.check("table entry 0 == sem0/src0/bits16/avail0x00/k_q16 10000000/"
|
||||
"r_q16 0 (pin mode)",
|
||||
(seg["entries"][0]["sem"], seg["entries"][0]["src"],
|
||||
seg["entries"][0]["bits"], seg["entries"][0]["avail"],
|
||||
seg["entries"][0]["k_q16"]) == (0, 0, 16, 0x00, 10000000))
|
||||
ck.check("table entry 1 == sem1/src1/bits12/avail0x02/k_q16 52800000",
|
||||
seg["entries"][0]["k_q16"],
|
||||
seg["entries"][0]["r_q16"]) == (0, 0, 16, 0x00, 10000000, 0))
|
||||
ck.check("table entry 1 == sem1/src1/bits12/avail0x02/k_q16 52800000/"
|
||||
"r_q16 0 (pin mode)",
|
||||
(seg["entries"][1]["sem"], seg["entries"][1]["src"],
|
||||
seg["entries"][1]["bits"], seg["entries"][1]["avail"],
|
||||
seg["entries"][1]["k_q16"]) == (1, 1, 12, 0x02, 52800000))
|
||||
seg["entries"][1]["k_q16"],
|
||||
seg["entries"][1]["r_q16"]) == (1, 1, 12, 0x02, 52800000, 0))
|
||||
# per-entry availability (low 8 bits of adc_cal_flags) vs header
|
||||
# clip_mask: the CLIPPED bits must reconcile, bit s = sem s both sides
|
||||
clip_from_avail = 0
|
||||
@@ -947,7 +1017,7 @@ def selftest():
|
||||
img_b = build_synthetic(avail_mode="both", flags=0xFF, fseq=71)
|
||||
segb = parse_segment(img_b, name="synth-both") # raises if wrongly voided
|
||||
ck.check("both-form (flags 0xFF, avail 0x01/0x03) parses, not rejected",
|
||||
segb["size"] == 5250)
|
||||
segb["size"] == 5282)
|
||||
ck.check("dump both-form carries VALID+CLIPPED(both) note",
|
||||
"VALID+CLIPPED(both)" in "\n".join(fmt_dump(segb)))
|
||||
fftb = "\n".join(fmt_fft(segb, None, False, range(segb["chc"])))
|
||||
@@ -1051,7 +1121,7 @@ def selftest():
|
||||
mv = channel_mv(seg, ci)
|
||||
win = mv[-pps:]
|
||||
rms1, ang1 = phasor_rms_ang(win, 1, pps)
|
||||
want_rms = amp_codes * lsb_uv(ent) / 1000.0 / math.sqrt(2.0)
|
||||
want_rms = amp_codes * ent_scale_mv(ent) / math.sqrt(2.0)
|
||||
ck.near("fft ch%d RMS" % ci, rms1, want_rms, 0.01)
|
||||
ck.near("fft ch%d phase (atan2(im,re) convention)" % ci, ang1,
|
||||
phi_deg, 0.01, abs_tol=2.0)
|
||||
|
||||
Reference in New Issue
Block a user