From 7c505b2457d977adf7750abbb841b0453fd6bfb9 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 13 Nov 2026 08:09:09 +0800 Subject: [PATCH] =?UTF-8?q?refactor(adc):=20=E6=9C=80=E5=B0=8F=E4=BA=8C?= =?UTF-8?q?=E4=B9=98=E6=8B=9F=E5=90=88=E7=A7=BB=E5=87=BA=E5=9B=BA=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E5=8F=AA=E7=95=99=20PC=20=E4=BE=A7=E5=94=AF=E4=B8=80?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户裁定(2026-11-13):设备本体只需要校正后的参数,不需要自己拟合 标定曲线,故固件侧最小二乘拟合整体移出,拟合的唯一实现留在 PC 侧 test/host/cal_fit.py。 删除的死代码(生产零调用、上一轮 map 里已被 linker 标 Removing): - adc_cal.h:adc_cal_fit() 声明、8 个拒绝码、5 个常量 - adc_cal.c:s_div_round() / s_fit_accum() / adc_cal_fit() 共 224 行 - adc_cal_test.c:tc_00b_fit 226 行(184 -> 150 条) 原样保留的设备侧安全闸: - adc_cal_apply() 折算、adc_cal_set() / adc_cal_set_ratio() 参数合法 带拦截(防工具写坏参数)、adc_cal_flags() / adc_cal_prot_blocked() / adc_cal_rail_ths()。设备不「算」,但必须「用」和「验」。 测试面: - cal_fit.py 转为唯一实现,新增 --selftest:承接原 C 侧金标向量 (r_q16=39322000 / b=12)与 4 条拒绝路径,共 11 条断言;已做变异 测试证明它会红(改坏圆整口径 -> 2 条 FAIL) - cal_two_point.py 删除,两点工具由多点拟合取代 文档口径统一为「求参在 PC、设备只做折算与合法带校验」,6 篇 docs 同步; 执行卡改名 ADC多点标定执行流程.md(正文同步 5 点最小二乘口径)。 cfg 热重载吞段缺陷的解析器契约与 TC-CONFIG-063 机器钉一并入库。 Program Size 不变(Code=256196):删掉的是从未链接进镜像的死代码。 验证:Keil 0 errors 0 warnings;adc_cal 150/150;selftest 11/11; ci_local.py RC=0(static 48/48、manual 三根、size-gate、host-unit 4341/4341);doc-links / mermaid / mojibake 全 0。 --- App/algo/README.md | 14 +- App/algo/adc_cal.c | 74 +-- App/algo/adc_cal.h | 64 ++- docs/README.md | 6 +- docs/UI设计/UI_数据供给设计说明.md | 2 +- docs/产品需求/产品需求设计文档.md | 14 +- docs/产品需求/参考产品复刻明细.md | 4 +- docs/任务规划/README.md | 6 +- docs/子系统设计/IEC104上行设计说明.md | 4 +- docs/子系统设计/故障录波设计说明.md | 6 +- docs/架构设计/硬件规格与能力边界.md | 2 +- docs/架构设计/项目架构总览.md | 2 +- docs/测试说明/专项验证/ADC两点标定执行流程.md | 134 ----- docs/测试说明/专项验证/ADC前端注入验证流程.md | 2 +- docs/测试说明/专项验证/ADC多点标定执行流程.md | 183 ++++++ .../专项验证/故障录波板级到场轮执行手册.md | 8 +- docs/硬件平台/ADC通道映射说明.md | 113 +++- docs/算法/继电保护功能复现规划.md | 2 +- docs/算法/采样数据校正全流程.md | 25 +- docs/算法/采集与算法层设计说明.md | 58 +- scripts/check_mermaid.py | 5 + scripts/size_attestation.json | 8 +- test/host/README.md | 4 +- test/host/adc_cal/adc_cal_test.c | 64 +-- test/host/cal_fit.py | 523 ++++++++++++++++++ test/host/cal_two_point.py | 224 -------- test/host/cal_write.py | 2 +- 27 files changed, 945 insertions(+), 608 deletions(-) delete mode 100644 docs/测试说明/专项验证/ADC两点标定执行流程.md create mode 100644 docs/测试说明/专项验证/ADC多点标定执行流程.md create mode 100644 test/host/cal_fit.py delete mode 100644 test/host/cal_two_point.py diff --git a/App/algo/README.md b/App/algo/README.md index 38236049..e4237a76 100644 --- a/App/algo/README.md +++ b/App/algo/README.md @@ -11,7 +11,7 @@ |---|---|---|---|---| | `adc_input.h` / `.c` | 语义取数与有效性 | 统一语义取数:两片转换器的码值按语义(`UA…IN` / 温度)暴露,并消费 STM32 ADC1 的 DMA 乒乓缓冲 | `adc_input_init()` `adc_input_poll()` `adc_input_raw()` | TPAFE 快照消费 / STM32 ADC1 启动与扫描 / 通道语义映射 | | `adc_health.h` / `.c` | 语义取数与有效性 | 采样有效性判定:吃计数快照,产出静默 / 丢帧 / 单调性 / 错误增量故障位(判定与告警编排分离);「采样通道自检」残余的码值贴轨观察器同层 | `adc_health_check()` `adc_health_flags()` `adc_health_rail_watch()` | 「采样有效性校验」/「采样通道自检」(残余) | -| `adc_cal.h` / `.c` | 标定折算 | 通道标定折算(「数据可信」标定折算与落盘整定):工程量 =(code+b)×k×r,k(前端斜率)/ b(零点)/ r(端子变比)三维、三段 cfg 落盘 + 三态 flags + 合法带哨兵 + 两点公式(标的是 r) | `adc_cal_apply()` `adc_cal_flags()` `adc_cal_set()` `adc_cal_set_ratio()` `adc_cal_ratio_configured()` | 「通道标定(一次系数 + 零漂)」(M4 半边) | +| `adc_cal.h` / `.c` | 标定折算 | 通道标定折算(「数据可信」标定折算与落盘整定):工程量 =(code+b)×k×r,k(前端斜率)/ b(零点)/ r(端子变比)三维、三段 cfg 落盘 + 三态 flags + 合法带哨兵 + 标定参数写入与合法带校验(拟合在 PC 侧 Python,本层不算曲线) | `adc_cal_apply()` `adc_cal_flags()` `adc_cal_set()` `adc_cal_set_ratio()` `adc_cal_ratio_configured()` | 「通道标定(一次系数 + 零漂)」(M4 半边) | | `meas.h` / `.c` | 全周傅氏相量发布 | 遥测快照(「数据可信」遥测快照发布,M5 地基):200 ms 基波有效值 + 品质快照,双区无锁读,全仓唯一发布者 = adcTask | `meas_tick()` `meas_get()` `meas_seq()` | M5(104 改读快照与频率点绑定、UI 是读者) | | `sample_ring.h` / `.c` | 全周傅氏相量输入 | 按语义通道存最近 2 周波(40 点),供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | 全周傅氏相量输入(为「每 1 ms 滑动窗刷新」供数) | | `phasor.h` / `.c` | 全周傅氏相量 | 全周傅氏:一周波 20 点 DFT → 基波 / 谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | 全周傅氏系数表 / 基波相量实例 / 相量有效值输出 / 每 1 ms 滑动窗刷新 / 双区指针原子交换 | @@ -36,8 +36,8 @@ adcTask 现取现填后交棒,本层不复制第二个事实源)。两份录 ## 2. 一拍的数据流(一眼版) -`adc_input_poll()` → `adc_input_raw()` ×9 → `adc_cal_apply()` ×9(标定折算折算,现口径 = -ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」,码值域)→ `sample_ring_push_frame()` +`adc_input_poll()` → `adc_input_raw()` ×9 → `adc_cal_apply()` ×9(标定折算,现口径 = +端子二次值 V/A,r=0 的路退回引脚电压)+ `adc_health_rail_watch()` ×9(「采样通道自检」,码值域)→ `sample_ring_push_frame()` → `phasor_run()` → `freq_run()` → 消费者(`adcTask` 心跳与 语义取数与有效性健康判定;`protectTask` 每 20 ms 取相量 / 频率跑 `prot_elm_run()`;**每 200 ms** `meas_tick()` 发布遥测快照供 iec104Task / uiTask 读,即「数据可信」遥测快照发布)。节拍 = TIM3 1 kHz 更新事件,两片转换器同源触发。 完整数据流图与初始化落点 → `docs/算法/采集与算法层设计说明.md` §2。 @@ -106,15 +106,17 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」 - **ROM 默认 = 量程口径冻结定死的引脚侧 LSB**(TPAFE 152.59 / STM32 805.66 µV/码, `docs/硬件平台/ADC通道映射说明.md` §7.1);表下标 = 语义枚举(UA0…IN7), V0.2 改版与本目录 `adc_input` 映射表同步改。 -- 三态可用性 `adc_cal_flags()`:VALID(已两点标定确认:k/b 在带内 **且** 变比 r 已配置) +- 三态可用性 `adc_cal_flags()`:VALID(已标定确认:k/b 在带内 **且** 变比 r 已配置) / CLIPPED(运行期按来源派生,V0.2 后自动清零)。**消费者判「能不能信」只许查它**, `adc_cal_apply()` 不返回可用性(docs/算法/采集与算法层设计说明.md §13.3 三态可用性表)。 - 合法带哨兵:k ∉ ROM×[0.5,2.0] 或 |b| > 8192 → 折算回退 ROM + 每通道每开机一条 `[ERR]` + VALID 被压制(**即便文件位图说已标定**);这层兜底同时挡坏文件与坏标定。 变比 r 是另一条独立哨兵:不在 `0` 或 `≥ 1.0` 的合法带里 → 该路**按不折算处理** (停在引脚电压,不回退),同样不报 VALID。 -- `adc_cal_set()` / `adc_cal_two_point()` / `adc_cal_set_ratio()`:整定、两点公式(标 r) - 与变比写入三个入口齐备(单测还原合成 k/b/r);**台架两点标定待实测**,本层不含注入。 +- `adc_cal_set()` / `adc_cal_set_ratio()`:整定与变比写入两个入口(写前做 k/r/b 合法带拦截, + 防工具写坏参数);**拟合不在本层**——N 点最小二乘是台架离线动作,唯一实现在 + `test/host/cal_fit.py`(`--selftest` 是它的回归锚点)。 + **台架多点标定待实测**,本层不含注入(读数来源与判读口径见该脚本)。 - cfg 三段 `[adc_cal]` / `[adc_cal_ofs]` / `[adc_cal_sec]` **不挂回调**(RAM 副本即生效值, 引擎 load/poll 原地改写,逐点现读);注册在 `app_main_init()` 内核前完成, 必须先于 `cfg_file_load`。 diff --git a/App/algo/adc_cal.c b/App/algo/adc_cal.c index 15e8edd7..e6342a21 100644 --- a/App/algo/adc_cal.c +++ b/App/algo/adc_cal.c @@ -28,7 +28,8 @@ * v1.1 2026-10-25 DTU 项目组 运行参数段(道D):新增 adc_cal_b_codes() 生效零点 * 读取口;set() 授权写者扩至 iec104Task 直通路径 * (run_par 0x8040~/0x8048~,契约同 §4.4) - * + 合法带哨兵回退 + 两点标定公式(台架注入待跑) + * + 合法带哨兵回退 + 标定公式(v2.1 起改在 PC 侧做 N 点 + * 最小二乘,固件不再持有拟合算术) * v1.2 2026-10-26 DTU 项目组 道F task-16:新增 adc_cal_prot_blocked()——未标定 * 闭锁(UA/UN/IA 三路 VALID 全齐才放行)收成本层单点, * DTU_BENCH 旁路同时收成全仓唯一一处;此前该判据在 @@ -41,6 +42,24 @@ * 折算 (code+b)×k×1e-6×r;+adc_cal_r_uv/r_q16/ * ratio_configured/set_ratio;two_point 改标 r; * VALID 追加「r 已配置」条件 + * v2.1 2026-11-13 DTU 项目组 两点 → N 点最小二乘:删两点公式(全仓 + * 生产代码零调用,只有单测调用,故对运行中的固件无影响), + * 新增固件侧拟合函数 + 配套拒绝码: + * N 组 (读数, 注入) 做普通最小二乘 y=a·x+c,再一次项 + * r_new=a·r_old、常数项 b_new=b_old+c/(k·1e-3·r_new) + * 折回 r/b。拒绝判据 n<3 / den<=0 / 单调性反转 / a<=0 / + * r_new 越带 / |b_new|>B_MAX / 最大残差 > 满量程 2%。 + * 全程 int64 定点,与 test/host/cal_fit.py 逐位对拍 + * (金标向量钉在 test/host/adc_cal/adc_cal_test.c) + * v2.2 2026-11-13 DTU 项目组 拟合移出固件:删 v2.1 新增的固件侧拟合函数及其算术核 + * (圆整除法 + 累加器)、5 个域上界常量与 8 个拒绝码宏。 + * 理由:标定是台架一次性离线动作(注入已知量 → 读屏上 + * meas: → PC 解 r/b → 经 104 写入),设备只需接受校正后的 + * 参数;固件版生产零调用者(被 linker 移除),留着就是 + * 第二个真相源。设备侧安全闸(set/set_ratio 的 k/r/b + * 合法带拦截)**原样保留**——那是防工具写坏参数的闸。 + * 拟合唯一实现移到 test/host/cal_fit.py,原金标向量随之 + * 搬进其 --selftest。 */ /* 头文件包含区 */ @@ -634,59 +653,6 @@ void adc_cal_note_persisted(void) { s_st_persist_water = s_st_set_ok; /* 水位只会追平,不会越过 */ } -/* - * 函数功能:两点标定公式(标定项的算术核,纯函数无状态;v2.0 起标 r 不标 k) - * 入口参数:c_lo/c_hi、v_lo_uv/v_hi_uv、k_q16、p_r_q16/p_b 见 adc_cal.h 同名声明 - * 返 回 值:0 成功 / -1 参数、斜率或变比越带 / -2 零点越 ±B_MAX int - * 限定条件:无(int64 中间量,主机可测) - * 函数说明:公式与**为什么这样分步**(直接一次乘除会顶穿 int64): - * v_uv = (code + b) × k_q16 × r / 65536 ← 折算式的 µV 形态 - * ⇒ 差分里零点抵消:dv = dc × k_q16 × r / 65536 - * ⇒ t = dv × 65536 / k_q16 = dc × r ← code·ratio 的混合量,量级 ≤ 2^31 - * ⇒ r_q16 = t × 65536 / dc ← 还原成 Q16 比值 - * ⇒ b = t_lo × 65536 / r_q16 − c_lo,其中 t_lo = v_lo × 65536 / k_q16 - * 若把 r_q16 = dv×2^32/(dc×k_q16) 一次算完,5 kV 端子注入时分子到 1e19 - * 已经越过 long long 上界(9.22e18)——分两步走,中间量 largest 约 1.4e14 - */ -int adc_cal_two_point(int32_t c_lo, int32_t c_hi, int32_t v_lo_uv, int32_t v_hi_uv, - int32_t k_q16, int32_t *p_r_q16, int32_t *p_b) { - long long dc; - long long dv; - long long t; - long long rq; - long long tl; - long long bb; - - if ((p_r_q16 == NULL) || (p_b == NULL)) { - return -1; - } - if ((k_q16 <= 0L) || (k_q16 > 100000000L)) { - return -1; /* 系数不在合理域(1526 µV/码 上界)= 调用方取错了 */ - } - if ((v_lo_uv <= -ADC_CAL_V_MAX) || (v_lo_uv >= ADC_CAL_V_MAX) || - (v_hi_uv <= -ADC_CAL_V_MAX) || (v_hi_uv >= ADC_CAL_V_MAX)) { - return -1; /* ±1000 V:超了几乎必然是把 mV 或码值当 µV 填了 */ - } - dc = (long long)c_hi - (long long)c_lo; - dv = (long long)v_hi_uv - (long long)v_lo_uv; - if ((dc <= 0LL) || (dv <= 0LL)) { - return -1; /* 斜率必须为正:端子值与码值同向,反向 = 映射/接线错 */ - } - t = ((dv << 16) + ((long long)k_q16 >> 1)) / (long long)k_q16; /* = dc × r */ - rq = ((t << 16) + (dc >> 1)) / dc; /* = r × 65536 */ - if ((rq < (long long)ADC_CAL_R_MIN) || (rq > (long long)ADC_CAL_R_MAX)) { - return -1; /* 变比越带:<1.0 = 把 mV 当 V,>32767 = 单位/量级错 */ - } - tl = (((long long)v_lo_uv << 16) + ((long long)k_q16 >> 1)) / (long long)k_q16; /* (c_lo+b) × r */ - bb = ((tl << 16) / rq) - (long long)c_lo; - if ((bb > (long long)ADC_CAL_B_MAX) || (bb < -(long long)ADC_CAL_B_MAX)) { - return -2; /* 零点异常 → 先查前端,别用变比凑(§8 风险表) */ - } - *p_r_q16 = (int32_t)rq; - *p_b = (int32_t)bb; - return 0; -} - /* * 函数功能:通道码值域贴轨带门限(采样通道自检 判定的门限来源,按 FS 百分比) *(保护功能清单见 docs/算法/继电保护功能复现规划.md) diff --git a/App/algo/adc_cal.h b/App/algo/adc_cal.h index 4757b3d7..1caf43f8 100644 --- a/App/algo/adc_cal.h +++ b/App/algo/adc_cal.h @@ -10,7 +10,7 @@ * b 管零点偏移(码值,±ADC_CAL_B_MAX); * r 管「端子二次值 ↔ 引脚电压」——由端子/子板分压与 CT 负担电阻决定, * 出厂默认按台架实测反算(电压 1000 V/V、电流 29.4118 A/V), - * 两点标定今后标的就是它。 + * 台架标定标的就是它(多点最小二乘在 PC 侧算,见 v2.2)。 * 为什么必须分开:把变比折进 k 会溢出存储口径——UA 端子域 k = 152 587.9 µV/码, * Q16 = 1.0e10 超出 int32 上限 2.147e9;而 r 单独存 Q16 完全够(1000 → 65 536 000)。 * 合并还会把「硬件分压比」伪装成「前端斜率」,正是 @@ -19,7 +19,7 @@ * [adc_cal_sec] 三段,布局见 docs/算法/采集与算法层设计说明.md §13.8); * **不挂回调**——RAM 副本即生效值,逐点折算现取现用(1 kHz × 9 通道 = 9k 次/秒 * 浮点乘,可忽略)。 - * 三态可用性:ADC_CAL_F_VALID(该路已两点标定确认)与 + * 三态可用性:ADC_CAL_F_VALID(该路已标定确认)与 * ADC_CAL_F_CLIPPED(V0.1 硬件削顶)由 adc_cal_flags() 单点查询, * 104 / UI / 保护闭锁(未标定即闭锁)共用(docs/算法/采集与算法层设计说明.md §13.3)。 * **未标定闭锁的判定本身也归本层**(`adc_cal_prot_blocked()`):它讲的是 @@ -27,7 +27,7 @@ * 组装器都只是它的消费者(道F task-16 把此前的两份实现收成一份)。 * ⚠ 两个**互不派生**的事实,别互相反推: * · 数值量纲由 r 决定——r ≠ 0 的通道,adc_cal_apply() 输出的就是二次值(V 或 A); - * · VALID 由两点标定置位——它回答"这个数准不准",回答不了"这个数是什么单位"。 + * · VALID 由标定位图置位——它回答"这个数准不准",回答不了"这个数是什么单位"。 * 屏面第 7 行的量纲标签(eng)只看前者,保护闭锁只看后者。 * 一次值(eng=2)仍留预留:其变比的家是 prot_set 已在册的 * PS_PT_PRI/PS_PT_SEC 与 PS_CT_PRI/PS_CT_SEC 四项(**不再新开 cfg 段**, @@ -56,9 +56,29 @@ * 理由 = 端子域 k 的 Q16 溢出 int32(UA 152587.9 µV/码 * → 1.0e10),变比并进 k 这条路不通;且变比是硬件 * 事实、k 是前端斜率,混一起就是「用系数凑硬件」。 - * 同步:adc_cal_two_point() 改标 r(k 由 ROM 固化)、 + * 同步:两点公式改标 r(k 由 ROM 固化;v2.1 起标定 + * 算法换成 N 点最小二乘,落在 PC 侧,见 v2.2)、 * +adc_cal_ratio_q16()/+adc_cal_ratio()/+adc_cal_ratio_configured() * +adc_cal_set_ratio() + * v2.1 2026-11-13 DTU 项目组 两点 → N 点最小二乘:删两点公式(生产代码 + * 零调用,只有单测调用),新增固件侧拟合函数——把 N 组 + * (读数, 注入) 做普通最小二乘 y=a·x+c,再由一次项/常数项 + * 折回 r/b(读数在哪个 r 下采的必须入参说明)。 + * 拒绝判据:n<3 / den<=0 / 单调性反转 / a<=0 / + * r_new 越带 / |b_new|>B_MAX / 最大残差 > 满量程跨度 2%。 + * 全程 int64 定点(不用 double 累加):结果确定、可跨平台 + * 复现,并与 test/host/cal_fit.py 逐位对拍(金标向量在 + * test/host/adc_cal/adc_cal_test.c)。 + * v2.2 2026-11-13 DTU 项目组 拟合移出固件:删 v2.1 新增的固件侧拟合函数及其算术核 + * (圆整除法 + 累加器)、配套的 5 个域上界常量与 8 个拒绝码宏。 + * 理由:标定是台架一次性离线动作(注入已知量 → 读屏上 + * meas: → PC 上解 r/b → 经 104 写入),设备只需接受 + * 校正后的参数,不需要自己拟合曲线;固件版生产零调用者、 + * 仅为给 Python 提供镜像对象而存在,留着就是第二个真相源。 + * **保留**设备侧安全闸:adc_cal_set()/adc_cal_set_ratio() + * 的 k/r/b 合法带拦截(防工具写坏参数)。拟合唯一实现 + * 移到 test/host/cal_fit.py,原金标向量随之搬进其 + * --selftest(否则"解出的 r 对不对"再无机器验证)。 */ #ifndef ADC_CAL_H #define ADC_CAL_H @@ -72,7 +92,7 @@ extern "C" { #endif /* 通道可用性标志(adc_cal_flags() 返回值位) */ -#define ADC_CAL_F_VALID (0x01u) /* 该路已两点标定确认(k/b/r 合法带内 → 读数可信) */ +#define ADC_CAL_F_VALID (0x01u) /* 该路已标定确认(k/b/r 合法带内 → 读数可信) */ #define ADC_CAL_F_CLIPPED (0x02u) /* 硬件削顶(V0.1 STM32 侧单极性无偏置)→ 交流量不可信 */ /* @@ -96,8 +116,8 @@ extern "C" { * 200 kΩ/330 Ω 假定值,而台架读数差 60 %(0.005 vs 0.008),远超屏面分辨率, * 故以实测为准;§7.2 已按本次结论改写(子板原理图仍不在仓库,见该节「复核边界」)。 * ⚠ 这两个数是**单点粗测**(5 V 注入只有 33 码,1 LSB = 3 %), - * 变比真值区间约 909~1111(电压族);送检前必须用标准源两点标定重写 r(见 - * adc_cal_two_point() 的语义变更)。 + * 变比真值区间约 909~1111(电压族);送检前必须用标准源 N 点最小二乘重写 r + * (PC 侧工具 test/host/cal_fit.py,见 v2.2)。 * r = 0 是合法值,语义 = 「本路不折算,输出停在 ADC 引脚电压」——TEMP 恒此态, * 也是现场关掉某路折算的显式手段(不是 1.0)。 */ @@ -111,14 +131,14 @@ extern "C" { * 上界 = int32 上限(Q16 下最大比值 32767.99),1000 与 29.41 都远在带内。 * ⚠ 工程实现上**只在下界用 ADC_CAL_R_MIN 比较**:r 是 int32,`r > ADC_CAL_R_MAX` * 是恒假分支(cppcheck 报 knownConditionTrueFalse)。ADC_CAL_R_MAX 留给 - * int64 中间量(两点标定算出的 kq / 除法前的分子)做上界闸。 */ + * int64 中间量(最小二乘算出的 r_new / 除法前的分子)做上界闸。 */ #define ADC_CAL_R_MIN (65536L) /* 1.0 */ #define ADC_CAL_R_MAX (2147483647L) /* 零点偏移幅值上限(码值):超此即判文件损坏/异常标定,回退 ROM 并留痕 */ #define ADC_CAL_B_MAX (8192L) -/* 两点标定的端子输入域上界(µV,含电流按 µA 同域):±1000 V / 1000 A。 +/* 标定的端子输入域上界(µV,含电流按 µA 同域):±1000 V / 1000 A。 * 它只拦「把 mV 或码值当 µV 填」这一类量级错——本平台 V0.1 端子量程远低于此, * 定值本身不是量程声明,别拿它当"支持 1000 V"的依据 */ #define ADC_CAL_V_MAX (1000000000L) @@ -134,7 +154,7 @@ extern "C" { * 文件值由引擎 load/poll 原地写入 RAM 副本,本层逐点折算现读。 * ⚠ 变比段 [adc_cal_sec] 的 ROM 默认**不是 0**(电压 1000 / 电流 29.4118), * 因此烧进新固件后屏上立刻显二次值——这是刻意的:变比是硬件事实, - * 出厂就该带着;而 VALID(准不准)仍然要求两点标定,两者互不代替。 + * 出厂就该带着;而 VALID(准不准)仍然要求标定,两者互不代替。 */ void adc_cal_init(void); @@ -239,7 +259,7 @@ int32_t adc_cal_k_q16(adc_semantic_t sem); * 别拿它反推 VALID——VALID 是"标定过没有",r 是"折算比是多少", * 两者由两个字段各记各的(见 adc_cal_flags 的注释)。 * ⚠ 本值是**整体折算比**(端子二次值 ÷ 引脚电压),不是纯硬件分压比: - * k 由 ROM 固化成板级名义斜率,其残差被 r 吸收(见 adc_cal_two_point 说明)。 + * k 由 ROM 固化成板级名义斜率,其残差被 r 吸收(台架标定时一并吸收进 r)。 */ float adc_cal_ratio(adc_semantic_t sem); @@ -300,28 +320,6 @@ int adc_cal_set(adc_semantic_t sem, int32_t k_q16, int32_t b_codes); */ int adc_cal_set_ratio(adc_semantic_t sem, int32_t r_q16); -/* - * 函数功能:两点标定公式(「数据可信」标定项的算术核,纯函数无状态) - * 入口参数:c_lo/c_hi - 两点码值(c_hi > c_lo) int32_t - * v_lo_uv/v_hi_uv - 两点**端子侧**实测值(µV:电压填 1e6 域,电流填 - * 1e6 域但单位是 µA,即「0.5 A → 500000」) int32_t - * k_q16 - 该路生效系数(走 adc_cal_k_q16() 取,不要自己填) int32_t - * p_r_q16 / p_b - 输出:变比(Q16)与零点(码值) int32_t * - * 返 回 值:0 - 成功;-1 - 参数/斜率非法或算出的变比越出 [ADC_CAL_R_MIN, ADC_CAL_R_MAX]; - * -2 - 零点越 ±ADC_CAL_B_MAX(先查前端,不得用系数凑错斜率, - * docs/算法/采集与算法层设计说明.md §13.2 尾注) int - * 限定条件:无(纯整数运算,主机侧可直接单测) - * 函数说明:**v2.0 起标的是 r,不再标 k**——k 是板级前端斜率(ROM 固化,现场 - * 不该改也改不动),真正未知的是「端子↔引脚」这一层。公式: - * r = (v_hi−v_lo) / [ (c_hi−c_lo) × k ] ← 零点在差分里抵消 - * b = v_lo / (k × r) − c_lo - * 两条都用 int64 中间量;`k × r` 的量级先按 Q16 归一到同一个 2^32 再除, - * 避免先乘后除把 int64 顶到溢出边。v 上限 1e9 µV(1000 V / 1000 A)—— - * 超了几乎必然是把 mV 或码值当 µV 填了,直接判参数错而不是算个假变比出来。 - */ -int adc_cal_two_point(int32_t c_lo, int32_t c_hi, int32_t v_lo_uv, int32_t v_hi_uv, - int32_t k_q16, int32_t *p_r_q16, int32_t *p_b); - /* * 函数功能:通道码值域「贴轨带」门限(采样通道自检 越界检出的门限来源,按 FS 百分比) * 入口参数:sem - 语义通道 adc_semantic_t diff --git a/docs/README.md b/docs/README.md index 01c6c58e..1b009537 100644 --- a/docs/README.md +++ b/docs/README.md @@ -112,7 +112,7 @@ docs/ | [`采集与算法层设计说明.md`](算法/采集与算法层设计说明.md) | **`App/algo/` 的现状设计说明**:语义取数与有效性判定、全周傅氏相量采样环/相量/测频、保护判据定值模型与元件框架的契约、关键机制(双区原子切换、乒乓半区、自适应测频窗、定值封存-自检-闭锁)、精度判据 J1~J6 与资源足迹;**§13 = 标定折算**(码值→工程量、三态可用性、未标定即闭锁、遥测快照、六轮动态验证判据、已定案的前置决策) | | [`继电保护功能复现规划.md`](算法/继电保护功能复现规划.md) | **保护功能复现规划**:把参考系统(CLS100)保护链路映射到本项目(STM32F4)的分层功能/性能/参数清单、平台适配(「双转换器无共同采样触发」等)与分阶段路线图 | | [`采样有效性校验设计说明.md`](算法/采样有效性校验设计说明.md) | **`App/algo/adc_health.c` 单文件深设计**:四类判据(静默 / 丢帧 / 序号倒退 / 错误计数增量)的判据推导与 100 ms 门限依据、电平-锁存-事件三种位语义与位号对齐、闭锁与看门狗兜底的责任划分、观测盲区、主机侧验证分工与现场判读手册(含三张配图生成器) | -| [`采样数据校正全流程.md`](算法/采样数据校正全流程.md) | **读者入口**:按一帧数据的真实流向,把端子→码值→三维折算(k/b/r)→相量/测频/遥测快照→104·UI·保护·录波串成一条线;含整定落盘、两点标定现状(板上无一键标定、台架手工路径见 `docs/测试说明/专项验证/ADC两点标定执行流程.md`)、三个判定(VALID/CLIPPED/eng 互不派生);细节归各属主文档 | +| [`采样数据校正全流程.md`](算法/采样数据校正全流程.md) | **读者入口**:按一帧数据的真实流向,把端子→码值→三维折算(k/b/r)→相量/测频/遥测快照→104·UI·保护·录波串成一条线;含整定落盘、多点标定现状(板上无一键标定、**拟合跑在 PC 侧 `test/host/cal_fit.py`、固件只做折算与参数合法带校验**,台架手工路径见 `docs/测试说明/专项验证/ADC多点标定执行流程.md`)、三个判定(VALID/CLIPPED/eng 互不派生);细节归各属主文档 | --- @@ -157,8 +157,8 @@ docs/ | 文档 | 内容 | |---|---| -| [`ADC前端注入验证流程.md`](测试说明/专项验证/ADC前端注入验证流程.md) | **ADC 前端与两片转换器的台架注入验证**(已知电压注入验证的执行侧,实测证据 → `docs/硬件平台/ADC通道映射说明.md` §8.1):两轮——直流轮(注入点清单 / 期望码值表 / 零点·线性·映射·极性·节奏五步,每步自带现场记录表,**一块万用表即可**)、交流轮(幅值/波形/相位/测频/削顶/谐波六轮执行卡);**两点标定轮已独立成篇** → `docs/测试说明/专项验证/ADC两点标定执行流程.md` | -| [`ADC两点标定执行流程.md`](测试说明/专项验证/ADC两点标定执行流程.md) | **UA/UN/IA 两点标定的执行卡**:开工拍板(保护口径 ≤3~5 %、量级 5 V/50 V 与 0.5 A/2 A 安全裁定、继保仪 ≥0.1 级)→ `meas:` 行三点读数填进文档内 `CALDATA` 标记表 → `test/host/cal_two_point.py --from-doc` 批量求解(`cal_two_point.py` 亦支持单路手填)→ `test/host/cal_write.py` 经 104 定值段 0x8050~/0x8048~ 写入置 VALID 自动落盘 → 三路齐解除保护闭锁(解卡时序红线与回写位置都在卡里);判据属主 `docs/算法/采集与算法层设计说明.md` §13.13 与 `docs/硬件平台/ADC通道映射说明.md` §7.2;**2026-11-12 实跑结果**(UA / IA 已标定、UN 未标 → 保护仍闭锁)登记在 `docs/硬件平台/ADC通道映射说明.md` §7.3 | +| [`ADC前端注入验证流程.md`](测试说明/专项验证/ADC前端注入验证流程.md) | **ADC 前端与两片转换器的台架注入验证**(已知电压注入验证的执行侧,实测证据 → `docs/硬件平台/ADC通道映射说明.md` §8.1):两轮——直流轮(注入点清单 / 期望码值表 / 零点·线性·映射·极性·节奏五步,每步自带现场记录表,**一块万用表即可**)、交流轮(幅值/波形/相位/测频/削顶/谐波六轮执行卡);**标定轮已独立成篇** → `docs/测试说明/专项验证/ADC多点标定执行流程.md` | +| [`ADC多点标定执行流程.md`](测试说明/专项验证/ADC多点标定执行流程.md) | **UA/UN/IA 多点(5 点)最小二乘标定的执行卡**:开工拍板(保护口径 ≤3~5 %、5 点档位 5/10/25/50/100 V 与 0.5/1/1.25/1.5/2 A、抬压逐点确认纪律、继保仪 ≥0.1 级)→ `meas:` 行读数按「每行一个点」填进文档内 `CALDATA` 长表(5 点 × 3 路)→ `test/host/cal_fit.py --from-doc` 在 **PC 侧**批量做最小二乘求解(亦支持 `--sem/--x/--y` 单路手填;**固件里没有拟合代码**,设备只做折算与写入参数合法带校验)→ `test/host/cal_write.py` 经 104 定值段 0x8050~/0x8048~ 写入置 VALID 自动落盘 → 三路齐解除保护闭锁(解卡时序红线与回写位置都在卡里);判据属主 `docs/算法/采集与算法层设计说明.md` §13.13 与 `docs/硬件平台/ADC通道映射说明.md` §7.2;**2026-11-12 实跑结果**(两点算法那一轮:UA / IA 已标定、UN 未标 → 保护仍闭锁)登记在 `docs/硬件平台/ADC通道映射说明.md` §7.3 | | [`启动日志原子性复验流程.md`](测试说明/专项验证/启动日志原子性复验流程.md) | **控制台整行独占 / 崩溃遗言原子性**这类**行级完整性**判据的固定动作与口径(`docs/子系统设计/控制台单写者日志设计说明.md` 的验收侧:那边讲怎么修,这边讲怎么证明修好了、以及为什么"标记全中"不等于"日志没坏") | | [`故障录波板级到场轮执行手册.md`](测试说明/专项验证/故障录波板级到场轮执行手册.md) | **录波剩余台架判据的执行手册**(判据属主是 `docs/子系统设计/故障录波设计说明.md` §10,这里只写"人做什么、说哪句话、会看到什么"):四幕(枚举埋点修复 / 注量对表 / 保护触发对时刻(src=P)含两个用户决定 / bench 自抓占卷)各自标清到场与否,配到场检查清单、现场记录表与收工还原清单 | | [`链路延迟测试说明.md`](测试说明/专项验证/链路延迟测试说明.md) | 网络链路时延分段定义、判定基线、执行流程 | diff --git a/docs/UI设计/UI_数据供给设计说明.md b/docs/UI设计/UI_数据供给设计说明.md index 31f77512..70bdebb5 100644 --- a/docs/UI设计/UI_数据供给设计说明.md +++ b/docs/UI设计/UI_数据供给设计说明.md @@ -179,7 +179,7 @@ typedef struct { **要点**: -- **`eng` 字段是诚实性的关键**(【2026-10-25 落地注】本条原写"0 = 原始码值(未标定)、`val` 是 ADC 码值(千级)",那是标定落地前的旧口径,已按现场改真值语义):标定层 `adc_cal` 的 ROM 默认系数按**引脚侧 LSB**定死(TPAFE 152.59 µV/码、STM32 805.66 µV/码),采样环折算出来的 `val` 现在是 **ADC 引脚电压 V**——既不是千级码值,也不是端子二次值。承诺边界的依据是 `docs/算法/采集与算法层设计说明.md §13.5`:这一波只承诺到"引脚电压","端子二次值"要等变比确认与两点标定。所以页面文案是 `eng=0` → "未标定"、`eng=1` → "二次值",而不是"码值";升档条件写在 `App/app/ui_data_feed.c` 的 `feed_eng()`——**电气 8 路 `adc_cal_flags()` 的 VALID 全置位才升**(整帧共用一个 `eng`,只要还有一路是引脚电压就不能给整帧贴"二次值"标签,宁可整体保守)。TEMP 不参与线性标定 → 该路恒带 `UI_DATA_Q_RAW`,页面据此不挂单位、不与其他路互比。 +- **`eng` 字段是诚实性的关键**(【2026-10-25 落地注】本条原写"0 = 原始码值(未标定)、`val` 是 ADC 码值(千级)",那是标定落地前的旧口径,已按现场改真值语义):标定层 `adc_cal` 的 ROM 默认系数按**引脚侧 LSB**定死(TPAFE 152.59 µV/码、STM32 805.66 µV/码),采样环折算出来的 `val` 现在是 **ADC 引脚电压 V**——既不是千级码值,也不是端子二次值。承诺边界的依据是 `docs/算法/采集与算法层设计说明.md §13.5`:这一波只承诺到"引脚电压","端子二次值"要等变比确认与多点标定。所以页面文案是 `eng=0` → "未标定"、`eng=1` → "二次值",而不是"码值";升档条件写在 `App/app/ui_data_feed.c` 的 `feed_eng()`——**电气 8 路 `adc_cal_flags()` 的 VALID 全置位才升**(整帧共用一个 `eng`,只要还有一路是引脚电压就不能给整帧贴"二次值"标签,宁可整体保守)。TEMP 不参与线性标定 → 该路恒带 `UI_DATA_Q_RAW`,页面据此不挂单位、不与其他路互比。 - **字符串只用定长数组**:`clock[20]` 在结构体内,无指针、无生命周期问题(跨层传指针是经典事故源)。 - **`yx_valid` 与 `yx_bits` 分开**:无效点不参与显示、不参与变位判断(沿用 104 点表的既有做法,避免"IV 通道抖动成变位")。 - **`yx_dp`(2026-10-25 批 4 追加)**:单点还是双点是 **104 点表事实**(`iec104_pts.c` 的 `YX_DP_N` diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index 5cda1b5c..fa54dbb2 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -99,7 +99,7 @@ > **产品现状一句话**:平台能力(网络 / 存储 / 时间 / UI / 日志 / 看门狗 / FTP / 配置) > **已实现并台架验收**;采集主线(语义取数与有效性采集 → 标定折算 → 全周傅氏相量 → 测频与遥测快照 → 保护判据 > → 出口与记录 SOE → IEC 60870-5-104 上送)**软件链路已全线贯通并台架闭环**;与参考产品的差距 -> 集中在三处:① 保护判据处于**未标定闭锁**态(等标准源两点标定)、② **点表规模**远小于参考 +> 集中在三处:① 保护判据处于**未标定闭锁**态(等标准源多点标定)、② **点表规模**远小于参考 > (定值 40 vs 249 项、遥测 9 vs 31 点、开入开出 0 vs 138/31 点)、③ **独立子系统未立项** > (故障录波、电度、多规约、SNTP)。即:**"数"与"判据"已可信到引脚电压口径, > 缺的是标定、点表铺开与一次设备接口**(明细见 复刻明细 §1.2、§9)。 @@ -192,7 +192,7 @@ flowchart LR | 平台能力台架验收 | 网络/存储/时间/UI/日志/看门狗/FTP/配置 全部闭环 | ✅ 已达成(§10.4) | | 采集主线到业务出口 | 采样→相量→测频→遥测快照→判据→104/UI 全链路数据同源 | ✅ 软件链路闭环;判据等标定(§9.1.1) | | 屏板独立带屏 | 串口通道交付并台架验收 | ✅ 主机侧交付(§5.16;尾巴见屏板链路设计说明 §9.2) | -| 送检就绪 | 两点标定 → 保护解卡 → 第三方主站验收 | ⬜ 等标准源与端子变比(§9.2 第 4/5 步) | +| 送检就绪 | 多点标定 → 保护解卡 → 第三方主站验收 | ⬜ 等标准源与端子变比(§9.2 第 4/5 步) | | 复刻完成 | 复刻明细各项达"做完了"口径 | 🔶 见 [`参考产品复刻明细.md`](参考产品复刻明细.md) §1.2 | @@ -255,7 +255,7 @@ flowchart LR | P0 | FR-CFG 配置引擎(§5.4) | ✅ | INI 段键表 + 外科保存 + 免重启生效 | | P0 | FR-LOG 记录与日志(§5.5) | ✅ | 定长记录 + 控制台单写者 | | P0 | FR-WD 看门狗(§5.7) | ✅ | 心跳位图 + 复位原因 + Fault 现场 | -| P0 | FR-ADC 采集与标定(§5.9) | ✅ | 1 kHz 同源节拍;两点标定后解卡 | +| P0 | FR-ADC 采集与标定(§5.9) | ✅ | 1 kHz 同源节拍;多点标定后解卡 | | P0 | FR-MEAS 遥测与相量(§5.11) | ✅ | 全周傅氏 + 测频 + 200 ms 快照单源 | | P0 | FR-IEC104 上行(§5.15) | ✅ | 被控站;无效点入表置 IV | | P0 | FR-PROT 保护(§5.12) | 🔶 | 软件就绪;**未标定闭锁**(用户裁定走标准源) | @@ -557,7 +557,7 @@ flowchart LR | 闭锁源 | 判据 | 行为 | |---|---|---| -| **未标定** | `UA`/`UN`/`IA` 的 `ADC_CAL_F_VALID` 未全部置位 | 整条判据链停走(产品构建默认态,等两点标定) | +| **未标定** | `UA`/`UN`/`IA` 的 `ADC_CAL_F_VALID` 未全部置位 | 整条判据链停走(产品构建默认态,等多点标定) | | **采样故障** | `adc_health_flags()` 电平位(TPAFE/STM32 静默、序号倒退) | 元件全部复位 + 本拍不判据,自愈后自动恢复 | | **定值非法** | 定值校验和(sum32 正/反码)复核失败 | 闭锁相关保护并告警,需重写定值解除 | | **全局软压板退出** | `PS_GLOBAL_EN` = OFF(104 遥控 `YK12`) | 元件复位 + 停判据,压板恢复即重返联 | @@ -1663,7 +1663,7 @@ pre-commit run --all-files | 条目 | 对应 复刻明细 §1.2 | 现状 | 缺的是 | 解除条件 | |---|---|---|---|---| -| **标定闭锁** | 1~3 | 9 个元件、定值、闭锁链、SOE、104 上送全部就绪,但 `ADC_CAL_F_VALID` 未置位 → 判据整体停走 | **外部资源**:子板/端子板变比 + 标准源 | 端子变比确认 → 8 通道两点标定 → VALID 置位(届时自动解卡,无需改代码) | +| **标定闭锁** | 1~3 | 9 个元件、定值、闭锁链、SOE、104 上送全部就绪,但 `ADC_CAL_F_VALID` 未置位 → 判据整体停走 | **外部资源**:子板/端子板变比 + 标准源 | 端子变比确认 → 8 通道多点标定 → VALID 置位(届时自动解卡,无需改代码) | | **出口与开入** | 7、8 | 无硬件;保护动作只体现为遥信 + SOE;遥控开关量否定确认 | **硬件 + 立项**:出口继电器、开入回路、回读自检 | 开入开出作为独立模块立项(不可移植项 X3 解除) | | **三相与同期** | 4、12 | 建接口不使能(B/C 相与 `IN` 削顶、跨转换器无同刻) | **V0.2 硬件** | 8 路电气量全接 TPAFE + 中点偏置 | | **电度计量** | 6 | 未实现(功率积分需跨转换器同刻) | **V0.2 硬件** | 同上(不可移植项 X4 解除) | @@ -1704,7 +1704,7 @@ pre-commit run --all-files | 1 | UI 补齐(记录 5 页 → 运行报告) | 规模型 | **✅ 2026-10-28 落地并绑页;2026-11-06 台架判据复核完成,整体销项** | 六页已落地;余 3 叶属不做项(遥信 3,无开入硬件),重开条件见 `docs/架构设计/硬件规格与能力边界.md` H3 | | 2 | net 层跨上下文收口 | 平台 | **下一顺位(未开工)** | UI 清单已清空,切换条件已满足;**开工待用户点头** | | 3 | 其余结构与可靠性评估 | 平台 | 排队 | net 收口后 | -| 4 | 端子/子板变比确认 → 两点标定 | 阻塞型 | 等外部资源 | 子板实物/原理图、标准源到位可插入 | +| 4 | 端子/子板变比确认 → 多点标定 | 阻塞型 | 等外部资源 | 子板实物/原理图、标准源到位可插入 | | 5 | 标准源送检与第三方 104 主站验收 | 阻塞型 | 等标定 | 保护 VALID 置位;送检窗口前 2 日冻结特性 | | 6 | 定值铺开 / 映射铺开 / 遥测铺开 / 软压板铺开 | 规模型 | 等排期 | 标定后(工程量才有意义) | | 7 | 元件铺开与遥测增强 | 规模型 | 等标准源/硬件 | 先解除未标定闭锁;三相类等 V0.2 | @@ -1712,7 +1712,7 @@ pre-commit run --all-files | 9 | 验证窗口事项(CI 远端、PC peer、负向注入) | 平台 | 逢条件顺带 | 条件到位即执行 | | 10 | 出口与开入 / 三相与同期 / 电度计量 / 对下采集 | 阻塞型 | 不排 | V0.2 硬件 / 用户决策 | -> **不可颠倒的顺序(一)**:变比确认 → 两点标定 → 保护解卡 → 送检。未完成标定就送检, +> **不可颠倒的顺序(一)**:变比确认 → 多点标定 → 保护解卡 → 送检。未完成标定就送检, > 保护表现为**不动作**;保持该闭锁,不用非标准源走捷径(用户裁定,2026-10-25)。 > > **不可颠倒的顺序(二)**:**点表铺开必须晚于标定**——否则铺出去的全是 IV 点, diff --git a/docs/产品需求/参考产品复刻明细.md b/docs/产品需求/参考产品复刻明细.md index 6d4f2fb9..e329a1b3 100644 --- a/docs/产品需求/参考产品复刻明细.md +++ b/docs/产品需求/参考产品复刻明细.md @@ -162,7 +162,7 @@ tTimer_NoUI 无压无流判据(CN_ | 归因 | 涉及条目 | 解除方式 | |---|---|---| -| **外部资源**(标准源、端子变比) | 定值标定、保护判据解卡 | 两点标定(需求档 §9.2 第 4 步) | +| **外部资源**(标准源、端子变比) | 定值标定、保护判据解卡 | 多点标定(需求档 §9.2 第 4 步) | | **硬件缺失**(X3 / X4) | 开入 138、开出 31、电度 8、同期、三相通道 | V0.2 硬件 | | **立项未排** | 多规约、SNTP、定值铺开 | 按需求档 §9.2 顺序。**录波最小闭环已交付代码面**(2026-10-29,设计与板级判据 `docs/子系统设计/故障录波设计说明.md` §10;原卡片 2026-10-30 销项并入后删除;1 kHz 起步与本文 80 点/周波口径的偏离在该设计文档 §2 显式登记);**4 kHz 全速、多段、COMTRADE、104 文件传输仍未排**,属二期范围 | | **算力 / 形态**(X1 / X2) | 双核分工、VxWorks | 不解除。**屏板链路已从范围外项移入范围内**(规格见需求档 §5.16,主机侧实现 `docs/子系统设计/屏板链路设计说明.md`;串口通道已实现,网络通道未做):主机侧链路与参考 1:1 同构,不属"替代方案" | @@ -722,7 +722,7 @@ tTimer_NoUI 无压无流判据(CN_ | 国密 | SM2 签名验签、SM3 摘要与标准测试向量一致 | | 软压板 | 9 面全部可投退且行为可观测(投退后对应功能确实停走/恢复) | | 出口与开入 | 保护动作 → 出口继电器动作 → 开入回读确认,全链路 ≤ 120 ms | -| 标定解卡 | 8 通道两点标定后 `ADC_CAL_F_VALID` 全置位;注入标准源,保护动作值与定值误差 ≤ 3 % | +| 标定解卡 | 8 通道多点标定后 `ADC_CAL_F_VALID` 全置位;注入标准源,保护动作值与定值误差 ≤ 3 % | | 屏板链路 | **串口通道**:屏板 280 ms 召唤、主机 ≤200 ms 内**发完** **4208 B**(整帧无 2 个字符时间以上的间隙),连续 10 min **主机侧**零超时、零 CRC 错帧;显存 3200 B 与 `g_ui_fbuf` 逐字节一致;7 个物理键(`1/2/3/4/7/8/9`)动作正确;**状态灯位掩码逐位一致、LED 0 恒亮**;屏板 ≈5000 s 自复位不误判离线;LOGO 下发出局(不实现);**网络通道**:INIT 应答 **16 B** 逐字段一致且无颜色字段、`LCDMEM` 偏移 0 得 3204 B、重复召唤无应答、偏移 ≥3200 无应答、`KEEPLIVE` 无应答且 1 分钟不掉链、8 键(含 `F1\|ENT` 组合)动作正确(需求档 §5.16.10 十四条,第 15 条 LOGO 出局) | --- diff --git a/docs/任务规划/README.md b/docs/任务规划/README.md index dec93402..e33590ff 100644 --- a/docs/任务规划/README.md +++ b/docs/任务规划/README.md @@ -41,8 +41,8 @@ | 顺序 | 工作 | 当前状态 | 开始或切换条件 | |---|---|---|---| -| **1** | **端子 / 子板变比确认 → 两点标定** | 变比已按参数算定;标定未开工 | 缺标准源。开工前必须先拍板三件事(精度目标、变比来源、`eng=0` 期测频门处置),见 `docs/算法/采集与算法层设计说明.md` §13.13 | -| **2** | 标准源送检与第三方 104 主站验收 | 等待标定 | 两点标定完成且保护 `VALID` 已置位;送检窗口前 2 日冻结特性 | +| **1** | **端子 / 子板变比确认 → 多点标定** | 变比已按参数算定;标定未开工 | 缺标准源。开工前必须先拍板四件事(精度目标、变比来源、`eng=0` 期测频门处置、标定点数与分布),见 `docs/算法/采集与算法层设计说明.md` §13.13 | +| **2** | 标准源送检与第三方 104 主站验收 | 等待标定 | 多点标定完成且保护 `VALID` 已置位;送检窗口前 2 日冻结特性 | | **3** | 定值 / 映射 / 遥测 / 软压板铺开 | 等排期 | 必须晚于标定,否则铺出去的全是 IV 点(两条不可颠倒的顺序见 `docs/产品需求/产品需求设计文档.md` §9.2) | | **4** | 保护元件与采集增强 | 等待标准源或硬件改版 | 先解除未标定闭锁;跨转换器相位 / 功率 / 对称分量类功能等 V0.2 硬件(缺陷见 `docs/任务规划/登记簿.md` §1) | | **5** | 其余结构与可靠性评估 | 排队 | 观察项的归属是各模块文档的「已知限制」节,不为清清单强做 | @@ -52,7 +52,7 @@ ### 2.1 关键依赖 -- **送检链不可颠倒**:变比确认 → 两点标定 → 保护解卡 → 送检。顺序反了保护表现为不动作,不许用非标准源走捷径(红线见 `docs/产品需求/产品需求设计文档.md` §9.2)。 +- **送检链不可颠倒**:变比确认 → 多点标定 → 保护解卡 → 送检。顺序反了保护表现为不动作,不许用非标准源走捷径(红线见 `docs/产品需求/产品需求设计文档.md` §9.2)。 - **保护扩展**:标准源未到位前不新增无法验证的判据;跨转换器相位、功率、对称分量类功能等 V0.2 硬件。 - **观察项不抢道**:内存优化、仓库体积等观察项不抢占当前编码道。 diff --git a/docs/子系统设计/IEC104上行设计说明.md b/docs/子系统设计/IEC104上行设计说明.md index 90d37a0c..2b5e777f 100644 --- a/docs/子系统设计/IEC104上行设计说明.md +++ b/docs/子系统设计/IEC104上行设计说明.md @@ -46,7 +46,7 @@ - **数据源 = 测频与遥测快照**(`App/algo/meas.h`,「数据可信」遥测快照发布与 104 消费):200 ms 基波有效值 + 每点品质,104 与 UI 共用同一份上游;读数单位 = ADC 引脚电压(eng=0,`App/algo/adc_cal` - 折算层 2026-10-25 落地;二次值等两点标定置 VALID)。槽位值/品质整体透传,本层 + 折算层 2026-10-25 落地;二次值等多点标定置 VALID)。槽位值/品质整体透传,本层 **不再自行判新旧**(快照 `seq` 仅供 COV 去重)。 - 品质映射:快照 `MEAS_QDS_*` 低字节与线上 QDS 同值直传;**CLIPPED → IV**(「削顶通道 恒 IV」口径不变、改由品质驱动,`docs/算法/采集与算法层设计说明.md §13.3` 三态可用性表 + @@ -57,7 +57,7 @@ 基准恒为**上次上送值**,非上次读值——防慢漂移把基准吃掉,参考系统同款);品质翻转 当拍直报、不计数。死区数值 = 运行时副本(`iec104_pts_yc_set_deadband`,ROM 默认沿 下游常数按工程量重定的额定 0.5% 档:0.5 V / 0.005 A / 0.5 Hz;运行参数段接 8020H 段后主站可读写); - 标准源注入复核(两点标定)待台架。 + 标准源注入复核(多点标定)待台架。 ### 3.2 遥信 YX(IOA 1~33) diff --git a/docs/子系统设计/故障录波设计说明.md b/docs/子系统设计/故障录波设计说明.md index 5c7376af..665d37cb 100644 --- a/docs/子系统设计/故障录波设计说明.md +++ b/docs/子系统设计/故障录波设计说明.md @@ -91,7 +91,7 @@ | 理由 | 说明 | |---|---| -| 现在根本没有工程量 | 两点标定未做,`adc_cal_flags()` 的 `ADC_CAL_F_VALID` 未置位 → `adc_cal_prot_blocked()` 返 1,帧值仍是引脚域电压(`docs/算法/采集与算法层设计说明.md §13.4`)。「录波必须先有工程量」这条旧前提被本条推翻 | +| 现在根本没有工程量 | 标定未做,`adc_cal_flags()` 的 `ADC_CAL_F_VALID` 未置位 → `adc_cal_prot_blocked()` 返 1,帧值仍是引脚域电压(`docs/算法/采集与算法层设计说明.md §13.4`)。「录波必须先有工程量」这条旧前提被本条推翻 | | 绕开测频幅值门 | `App/algo/freq.h` 的 `FREQ_AMP_MIN 50.0f` 以二次值 V 为口径,`eng=0` 期间帧值远小于它恒不过门 → `freq_valid()=0`。**录波取的是这道门之前的原始帧**,所以不受影响,动态验证才有样本可对 | | 最省且可追溯 | int16 码值 2 B/点;标定完成后不必重录,PC 端拿头里的 `k` / `b` 就能把老文件重算成工程量 | | 诚实 | 文件里装的是「板子看见的东西」,不是「我们希望它看见的东西」 | @@ -220,9 +220,9 @@ flowchart TD | 折算算式与防溢出 | PC 与板端同一条定点算式:`t = (code + b_codes) × k_q16`;`r_q16 = 0` → `m = 圆整(t / 65536000)`(引脚域 mV,与 v1 逐位一致);`r_q16 ≠ 0` → `m = 圆整(t × r_q16 / (65536000 × 65536))`(录波域毫单位)。**只允许在最后圆整一次**:先圆整成整数 mV 再乘变比,会让 `r_q16 = 65536`(比值 1.0)的输出与引脚域差 ±1 LSB,而「r = 1.0 等同引脚域」正是两份实现对齐的锚点。**一次乘完会溢出 int64**(t ≈ 4.3e12、r_q16 ≤ 2.147e9,乘积 9e21 > 9.2e18)→ 板端用余数分解换等价单次圆整(`App/algo/wave_store.c` 的 `wave_store_code_to_milli`,注释里写明分解式与各项上界);PC 侧 Python 用大整数直接算同一个值。输出是**录波域毫单位**:eng=1 时二次 mV/mA、eng=0 时引脚 mV,两端口径一致 | | 变比为什么不并进 `k_q16` | `k_q16` 是**板级前端斜率**(µV/码 × 65536,TPAFE `10000000` / 片内 ADC `52800000`),`r_q16` 是**端子变比**(PT/CT 变比 × 65536)。若把 r 折进 k,电压族 = `10000000 × 65536000 / 65536 = 1.0e10`,已超 int32 上限 2.147e9 → Q16 定标下无法在同一个 int32 里同时表达两个量纲,故必须独立成字段 | | `flags_seq` 位义 | bit s = 语义通道 s 的 `ADC_CAL_F_VALID`;本波 `ch_count` 固定 8 | -| 可用性字段真值表 | 两行,实现期按 `App/algo/adc_cal.c` 280 到 298 行核出(VALID 需文件位图与 k 合法带**同时**成立;CLIPPED 纯由来源派生——该路 ROM 系数属片内 ADC 侧即置位,与样本内容无关):① **未标定**(产品构建,两点标定未做)逐路 availability = UA / UN / IA 取 `0x00`,UB / UC / IB / IC / IN 取 `0x02`,`flags_seq` = `0x00`,`clip_mask` = 0xE6;② **已标定**(电气 8 路系数全部写入且落合法带)逐路 = `0x01`(TPAFE 三路)/ `0x03`(片内 ADC 五路,VALID 与 CLIPPED 同时为真),`flags_seq` = `0xFF`,`clip_mask` 仍是 0xE6。**VALID 与 CLIPPED 是两件独立事实**:写侧如实透传 `adc_cal_flags()` 逐路原值(`App/task/adc_task.c` 548 行),读侧不得据任何品质组合判废,PC 与回放页只给软告警(见下面「判废面与软告警面」一行) | +| 可用性字段真值表 | 两行,实现期按 `App/algo/adc_cal.c` 280 到 298 行核出(VALID 需文件位图与 k 合法带**同时**成立;CLIPPED 纯由来源派生——该路 ROM 系数属片内 ADC 侧即置位,与样本内容无关):① **未标定**(产品构建,标定未做)逐路 availability = UA / UN / IA 取 `0x00`,UB / UC / IB / IC / IN 取 `0x02`,`flags_seq` = `0x00`,`clip_mask` = 0xE6;② **已标定**(电气 8 路系数全部写入且落合法带)逐路 = `0x01`(TPAFE 三路)/ `0x03`(片内 ADC 五路,VALID 与 CLIPPED 同时为真),`flags_seq` = `0xFF`,`clip_mask` 仍是 0xE6。**VALID 与 CLIPPED 是两件独立事实**:写侧如实透传 `adc_cal_flags()` 逐路原值(`App/task/adc_task.c` 548 行),读侧不得据任何品质组合判废,PC 与回放页只给软告警(见下面「判废面与软告警面」一行) | | eng 派生式 | `eng` 不是独立事实,是**逐路变比配置状态**的派生量:**每一路电气量「要么已配变比、要么削顶(页面本来就显 `-`,不参与整帧量纲断言)」,八路全满足 → 二次值(1),缺任意一路 → 引脚域(0)**。判据本体 `App/app/ui_data_feed.c` 的 `feed_eng()`(屏显侧),段头字段填写在 `App/task/adc_task.c` 的封存交棒块里现算。**v2 起判据换成变比配置,不再看 `flags_seq`**:`ADC_CAL_F_VALID` 回答「准不准」、`r_q16` 回答「什么单位」,两件事;V0.1 五路片内 ADC 削顶永远拿不到 VALID,用 VALID 当量纲判据会让档位永不解除。两处分布在**不同任务里现算**,理论上会撞上配置重载的毫秒级沉降窗而不一致——这是允许态,见下一行 | -| 判废面与软告警面 | **完整性判废面自始至终只有这些**:精确尺寸等式、`magic` 与 `ver`(含 `ver` 须等于魔数第三字节)、头 CRC 与数据 CRC、表序 `sem` 等于下标、`ch_count` 上限、`pts_per_cycle` / `pts_per_ch` / `pre_pts` 下限、长度为零、文件短读。**品质组合与单位标签永远不进这道门**。软告警面:`avail` 同时带 VALID 与 CLIPPED 时 PC 在 NOTE 列打「同时为真,两点标定完成后属正常,非故障」;`eng` 与逐路变比配置违反上一条派生式时打 WARN。两者都**不改退出码、不影响出图、不拒渲染**。理由:把「单位标签不一致」当数据损坏,等于让一次改标定文件的沉降窗销毁故障证据 | +| 判废面与软告警面 | **完整性判废面自始至终只有这些**:精确尺寸等式、`magic` 与 `ver`(含 `ver` 须等于魔数第三字节)、头 CRC 与数据 CRC、表序 `sem` 等于下标、`ch_count` 上限、`pts_per_cycle` / `pts_per_ch` / `pre_pts` 下限、长度为零、文件短读。**品质组合与单位标签永远不进这道门**。软告警面:`avail` 同时带 VALID 与 CLIPPED 时 PC 在 NOTE 列打「同时为真,标定完成后属正常,非故障」;`eng` 与逐路变比配置违反上一条派生式时打 WARN。两者都**不改退出码、不影响出图、不拒渲染**。理由:把「单位标签不一致」当数据损坏,等于让一次改标定文件的沉降窗销毁故障证据 | | v1 旧段兼容 | **不做**:`ver` 与 magic 第三字节必须都等于 2,v1 段(`ver` = 1)一律按「该段不可读」处理。理由:v1 没有 `r_q16`,同一份 code 在 v1 里只能解释成引脚域;让读侧按版本分支去猜「这一路到底折没折算」会把「格式已知」降级成「推断已知」,而旧段的有效寿命只到下一次清 `/wave`。升版时全部旧段作废是可接受的代价(`/wave` 是 100 槽轮转,本波不清库) | | 通道表序与列序 | 第 i 个通道表项 ↔ 数据区第 i 列,按语义号**升序**:UA=0 UB=1 UC=2 UN=3 IA=4 IB=5 IC=6 IN=7(`App/algo/adc_input.h` 的 `adc_semantic_t`)。PC 必须校验表内语义号等于下标,不等即整段作废。交叉验证:`clip_mask` 恒 0xE6 = 2+4+32+64+128,恰好是 UB/UC/IB/IC/IN 五个语义号位,这份算术就是这个顺序的证据 | | 样本编码 | int16 小端;TPAFE 侧 16 位补码双极性;STM32 侧 0..4095 原样存正数,**不加中点偏置**——V0.1 单极性无偏置是硬件现状,加偏置等于伪造双极性(`docs/算法/采集与算法层设计说明.md §13.5`) | diff --git a/docs/架构设计/硬件规格与能力边界.md b/docs/架构设计/硬件规格与能力边界.md index 2ba69f0e..1f31d06b 100644 --- a/docs/架构设计/硬件规格与能力边界.md +++ b/docs/架构设计/硬件规格与能力边界.md @@ -76,7 +76,7 @@ | H4 | **无本地屏与按键** | 160×160 单色屏仅存在于远端客户端 | UI 以 TCP :7003 推流 + 远端按键回注(`../产品需求/产品需求设计文档.md` §8) | 产品形态即如此,不解除 | | H5 | **无 SDIO** | SDIO 固定引脚组被 TPAFE/UART 占用 | FatFs 卷只能落 SPI NAND(当前设计不受影响) | 不解除 | -> **量纲口径**(由 H1/H2 派生):在端子/子板变比确认与两点标定完成前,遥测与保护读数的单位是 +> **量纲口径**(由 H1/H2 派生):在端子/子板变比确认与多点标定完成前,遥测与保护读数的单位是 > **ADC 引脚电压**(`eng=0`),不是二次值;此时 `ADC_CAL_F_VALID` 未置位, > **保护判据整体停走**(`../产品需求/产品需求设计文档.md` §5.12)。这是刻意设计,不是"保护不动作"的缺陷。 diff --git a/docs/架构设计/项目架构总览.md b/docs/架构设计/项目架构总览.md index 0d61434a..2d1369b1 100644 --- a/docs/架构设计/项目架构总览.md +++ b/docs/架构设计/项目架构总览.md @@ -75,7 +75,7 @@ | `timeSyncTask` | 2560 | Normal | 等网络 → 200 ms 校时节拍 `time_sntp_run()`(SNTP 客户端,UDP 出向;写 RTC 经 `sys_clock_set_unix`。:8888 推送监听已退役 2026-11-04) | `time_sync_task` | | `adcTask` | 2560 | AboveNormal | 纯消费者 + **唯一发布者**:`osThreadFlagsWait` 等 TIM3 1kHz 唤醒(ISR 内 `adc_tick_irq_notify` 置位)→ `adc_input_poll()`(TPAFE 原子快照 + STM32 ADC1 DMA 双缓冲)→ 标定折算 → 相量 → 测频;录波支路 `wave_ring_push()/tick()`;**每 200ms `meas_tick()` 发布遥测快照**(双区无锁,104 与 UI 共用同一份上游) | `adc_task_func` | | `iec104Task` | 2560 | Normal | 等网络 → IEC 60870-5-104 上行(工厂默认 :2404 监听 + link 状态机;遥测读 meas 快照、遥信读 `prot_elm` 元件态,COV 死区/变位上送;`[comm]`/`[run_par]` 配置;IEC 104 上行/第二轮)。任何构建都创建,纳入 IWDG `required_mask` | `iec104_serv_task` | -| `protectTask` | 2048 | Normal | 保护判据 20ms 节拍:`prot_elm_run()` + 「自检失败闭锁」采样故障闭锁 + 定值校验和巡检 + 动作/复归边沿 SOE 投递(保护任务);⚠ **当前处于「未标定闭锁」态**——`adc_cal_flags()` 无 VALID 时判据停走,两点标定写入 `[adc_cal]` 后自动解卡(用户裁定 2026-10-25:保持闭锁,不用万用表走捷径);纳入 IWDG `required_mask`(产品 `0x1F7`) | `protect_task` | +| `protectTask` | 2048 | Normal | 保护判据 20ms 节拍:`prot_elm_run()` + 「自检失败闭锁」采样故障闭锁 + 定值校验和巡检 + 动作/复归边沿 SOE 投递(保护任务);⚠ **当前处于「未标定闭锁」态**——`adc_cal_flags()` 无 VALID 时判据停走,多点标定写入 `[adc_cal]` 后自动解卡(用户裁定 2026-10-25:保持闭锁,不用万用表走捷径);纳入 IWDG `required_mask`(产品 `0x1F7`) | `protect_task` | > `idle`(128 words)/ `timer`(256 words)由 FreeRTOS 内核创建。所有任务栈从 FreeRTOS `heap_4` 堆分配(**已迁 CCM**,2026-10-12 保护任务 由 48KB 扩容至 56KB,见 `docs/内存管理/RAM使用分析与优化方案.md` §4.1.2/§2.3)。 > **栈台账**:权威逐任务数字在内存方案 §2.3;`stack_report.py` 9/9 PASS(2026-10-22 静态最坏链 + 同日真机 `[STACK]` 水位;defaultTask 最坏 3024B/5120B 余 40%、protectTask 静态 872B/2048B 余 57%)。⚠ 改栈或加大栈内局部变量前后**必须**重跑该脚本——静态分析低估含递归/间接调用的链(优化前 ftpTask:静态 2528B vs 实测 3480B)。 diff --git a/docs/测试说明/专项验证/ADC两点标定执行流程.md b/docs/测试说明/专项验证/ADC两点标定执行流程.md deleted file mode 100644 index 7ecbf363..00000000 --- a/docs/测试说明/专项验证/ADC两点标定执行流程.md +++ /dev/null @@ -1,134 +0,0 @@ -# ADC 两点标定执行流程(台架) - -给 `UA/UN/IA` 各加两个已知量,解出变比 `r` 与零点 `b`,经 104 定值段写入;三路 `VALID` 齐 - -## 1. 开工核对 - -| 项 | 判据 | -|---|---| -| 量级 | 电压族 **5 V / 50 V**、电流族 **0.5 A / 2 A** 误差代价 ≈0.7 % / 0.6 %,在保护 3~5 % 内;0.5 级计量日后另跑高点轮,不混用) | -| 仪器 | 继保仪 **≥0.1 级** | -| 网络 | PC 与板同网段(192.168.31.100),:2404 可达、FTP :21(admin)可用 | -| 接线 | PT 一次侧按下表接;CT 二次**全程不开路**;注入源与板**共地**、先接线后加压 | - -**PT 一次侧接线表**(电压采集板 8 端子 → 继保仪输出): - -| 板端子 | 接继保仪 | 说明 | -|---|---|---| -| `UA` | `UA` | 相 A 电压 | -| `UAN` | `UN` | 中性参考 | -| `UB` | `UB` | 相 B 电压 | -| `UBN` | `UN` | 中性参考 | -| `UC` | `UC` | 相 C 电压 | -| `UCN` | `UN` | 中性参考 | -| `U0` | `UZ` | 零序电压(N-GND) | -| `U0N` | `UN` | 中性参考 | - -要点:每个 `U*N` **各自直接接**继保仪 `UN` 端子,**不要四个连一起**(连一起会经各 200 KΩ 串阻分压,PT 一次侧只拿一半电压)。`U0` 接继保仪 `UZ`(零序电压输出,对应 N-GND),`U0N` 接 `UN`——这样 U0 PT 一次侧跨 `UZ-UN`,台架上能读到零序信号。 - -**CT 一次侧接线表**(电流采集板 8 端子 → 继保仪输出): - -| 板端子 | 接继保仪 | 说明 | -|---|---|---| -| `IA` | `IA` | 相 A 电流 | -| `IAN` | `IBN` | 中性参考 | -| `IB` | `IB` | 相 B 电流 | -| `IBN` | `ICN` | 中性参考 | -| `IC` | `IC` | 相 C 电流 | -| `ICN` | `I0N` | 中性参考 | -| `I0` | `IN` | 零序电流(N) | -| `I0N` | `IAN` | 中性参考 | - -要点:`IA`/`IB`/`IC` 各自接对应相输出;`IAN`→`IBN`→`ICN`→`I0N`→`IAN` 串联链,`I0` 接继保仪 `IN`——I0 CT 跨 `IN` 到链上,能读到 IN 电流。**CT 二次全程不开路**;回路中**不得串额外电阻**(串电阻会分压削顶)。 - -## 2. 读数 - -**读哪行、怎么读**:串口每 10 s 打一行 `meas: seq=… v=… ua_mv=… un_mv=… ia_mv=… f=… fv=…`。电压格单位 mV、电流格单位 mA。每工况稳定后**连读 3 拍取中间值**(第一拍是切换瞬态,最后一拍防漂移;3 拍 ≈ 30 s)。 - -**实际案例**: - -```text -# 未注入(零点)——连续多拍都是 0: -meas: seq=1400 v=1 ua_mv=0 un_mv=0 ia_mv=0 f=0 fv=0 - -# 继保仪出 1.000 V(UA + UN 同注)后稳定,连续多拍: -meas: seq=1450 v=1 ua_mv=995 un_mv=996 ia_mv=0 f=0 fv=0 -meas: seq=1500 v=1 ua_mv=995 un_mv=996 ia_mv=0 f=0 fv=0 -meas: seq=1550 v=1 ua_mv=995 un_mv=996 ia_mv=0 f=0 fv=0 -``` - -解读:注入 1.000 V → `ua_mv` 显示 995 mV(差 5 mV = 零点偏移,正常);`un_mv` 显示 996 mV;`ia_mv` 仍 0(没注电流)。**你填进 CALDATA 表的就是 `995` / `996` 这些数**——脚本内部会换算回码值再解 r/b,你不需要算码值。 - -**两点标定实际操作**(继保仪输出按 §1 拍板的 5 V / 50 V / 0.5 A / 2 A): - -| 步骤 | 继保仪输出 | 串口 `meas:` 行读哪格 | 填入 CALDATA 表 | -|---|---|---|---| -| ① 零点 | 0 V / 0 A | 稳定 3 拍 `ua_mv` / `ia_mv` 取中(预期个位数 mV/mA) | 零点 meas | -| ② 低点 | 5.000 V / 0.5 A | 稳定 3 拍取中(预期 ≈ 5000 mV / 500 mA) | 低点 meas | -| ③ 高点 | 50.000 V / 2.0 A | 稳定 3 拍取中(预期 ≈ 50000 mV / 2000 mA) | 高点 meas | - -**零点越带怎么办**:如果零点 `meas:` 格绝对值 > 1250(电压路 mV、电流路 mA,对应码值 ±8192),说明前端有偏压——先查共地/接线,**不得用系数凑**(`docs/硬件平台/ADC通道映射说明.md` §7.2 禁则)。 - -## 3. 求解:`test/host/cal_two_point.py` - -把每路三个读数填进下表(`CALDATA` 注释标记供 `--from-doc` 提取,勿删改标记;空格=未填会被跳过): - - -| 路 | 零点 meas | 低点 meas | 高点 meas | 已知低点 | 已知高点 | -|---|---|---|---|---|---| -| ua | 50 | 9049.7 | 90176.4 | 5 | 50 | -| un | 70 | | | 5 | 50 | -| ia | 2 | 496.75 | 1985.49 | 0.5 | 2 | - - -> 上表是 **2026-11-12 台架那一轮的实际读数**(留作样例:真实数据长这样、量级对不对可以照此眼校)。 -> 下次重新标定**先把 meas 三列清空**,填你这次读到的数;`un` 那行留空即代表本轮不测该路。 -> 那一轮解出 **UA `r = 554.687914` / `b = 0`、IA `r = 29.634259` / `b = 0`**, -> 已写入设备(结果登记 → `docs/硬件平台/ADC通道映射说明.md` §7.3)。 - -meas 列 = `meas:` 行读数(`ua`/`un` 为 mV、`ia` 为 mA,即 ×1000 那格);已知列 = 继保仪实际输出(伏/安)。填完跑: - -```powershell -& '.venv\Scripts\python.exe' test/host/cal_two_point.py --from-doc docs/测试说明/专项验证/ADC两点标定执行流程.md -``` - -脚本对每路已填行解出新的变比 `r` 与零点偏移 `b`,最后打出一条合并写入命令(`WRITE-ALL:` 开头,直接接给 `cal_write.py`)。 - -**怎么判断结果能不能用**(按顺序看): - -| 看什么 | 合格 | 不合格怎么办 | -|---|---|---| -| 每路 `OK r=…` 行里的 **dev**(新 r 相对旧 r 的偏差) | 在 **±10 %** 内 | 超了 = 读数单位填错或接线有问题,**先排查,不要写** | -| 有没有 `REJECT` 行 | 没有 | 有 `REJECT` = 两点填反了(高点比低点还低)或零点超出 ±8192 码,**修正表里对应格再跑** | -| 填了零点列时的 `ZERO CHECK` 行 | 独立算出的 b 与求解 b **量级一致**(比如都是个位数) | 量级差很多 = 零点读数来源可疑(共地/接线),先排查 | -| 末尾 `WRITE-ALL:` 行 | 存在且包含你填的所有路 | 只包含部分路 = 有路被跳过(看上面的 INCOMPLETE 提示),补齐后重跑 | - -全部路都 OK 才进 §4 写入。 - -## 4. 写入:`test/host/cal_write.py` - -| 路 | `r` | `b` | -|---|---|---| -| `UA` | `0x8050` | `0x8048` | -| `UN` | `0x8053` | `0x804B` | -| `IA` | `0x8054` | `0x804C` | - -```powershell -& '.venv\Scripts\python.exe' test/host/cal_write.py --r 0x8050=995.99 --b 0x8048=-1 --read 0x8050,0x8048 --expect 995.99,-1 -``` - -每路**先 `r` 后 `b`**(`b` 经 `adc_cal_set()` 置 `VALID`;`b` 必须整数码值);`SUMMARY PASS` + C_RD 读回差 <0.1 才进下一路。收到 PN → 看串口 `[ERR] set ch=... rejected`(`r < 1.0` 或 `|b| > 8192`)。 - -## 5. 验收与解卡 - -1. **每路完成**:`cal:` 行 `vl` 位图对位点亮(`UA` bit0、`UN` bit3、`IA` bit4,三路齐 `0x19`);低/高点复注对表;FTP 取 `/config.ini` 核 `[adc_cal]`/`[adc_cal_ofs]`/`[adc_cal_sec]`;断电重启后 `vl` 与读回不变。 -2. **解卡(最后一路 `b` 前)**:先把各通道置 **50 V / 0.5 A 附近**——**别留 0**(零压解锁会被判低电压);跳闸回路若接真断路器先确认不驱动。最后一笔 `b` 后应打 `calibration VALID -> protection unlocked` + SOE。**严禁伪造 `valid` 位**。 -3. **回写**:结果登记进 `docs/硬件平台/ADC通道映射说明.md` §7.3(变比与零点实测表), - §9 台阶第 4 级按已标定路数标 🟡/✅(第 5 级留送检);日志按 `docs/测试说明/测试数据/boot_log_data/` 命名归档。 - -> **2026-11-12 那一轮的结果与遗留**(照抄现状,别当成"全标完了"):UA / IA 两路标定并落盘, -> 断电重启后读回不变;`vl=0x11`(UA bit0 + IA bit4),`cl=0xe6`。**UN 未标**(台架无有效信号), -> 三路不齐 → 保护判据仍打 `[PROT] uncalibrated -> protection blocked` 并停走,**属预期**。 -> 补完 UN 才是"三路齐、自动解卡"的完整验证。**另外**:那一轮暴露出配置引擎热重载吞段的缺陷 -> (写进去的 `r`/`b` 约 8 s 被打回出厂值并覆写文件),已修复并加机器钉, -> 见 `docs/子系统设计/配置文件设计说明.md` §4-(3)。 diff --git a/docs/测试说明/专项验证/ADC前端注入验证流程.md b/docs/测试说明/专项验证/ADC前端注入验证流程.md index c3fbec2f..85686107 100644 --- a/docs/测试说明/专项验证/ADC前端注入验证流程.md +++ b/docs/测试说明/专项验证/ADC前端注入验证流程.md @@ -2,7 +2,7 @@ > **这篇是什么**:已知电压注入验证的**执行侧**——怎么写表笔、读哪行日志、填哪张表、结果往哪回写。判据与实测记录在属主文档:直流轮 `docs/硬件平台/ADC通道映射说明.md` §8.1、交流轮 `docs/算法/采集与算法层设计说明.md §13.10`;**本文只写怎么跑**,分叉时判据从属主。证不到什么见 `docs/硬件平台/ADC通道映射说明.md` 的 §8.1 结论与「边界」段,本文不重抄。 > -> **两个轮次**:§1~§7 = **直流轮次**(2026-10-27 六项 PASS,只需万用表 + 电位器 + 串口日志);§8 = **交流轮次**(幅值 / 波形 / 相位 / 测频 / 削顶 / 谐波),一台信号发生器接 §3 同一批注入点就能跑,等标准源的只有**精度等级**。V0.1 五路单极性削顶(`UB/UC/IB/IC/IN`)在交流轮次只作"预期削顶"负向用例。**两点标定轮已独立成篇**:`docs/测试说明/专项验证/ADC两点标定执行流程.md`(标准源两点 → 求 r/b → 104 定值段写入置 VALID 落盘 → 三路齐了解除保护闭锁;工具 `test/host/cal_two_point.py` / `test/host/cal_write.py`)。 +> **两个轮次**:§1~§7 = **直流轮次**(2026-10-27 六项 PASS,只需万用表 + 电位器 + 串口日志);§8 = **交流轮次**(幅值 / 波形 / 相位 / 测频 / 削顶 / 谐波),一台信号发生器接 §3 同一批注入点就能跑,等标准源的只有**精度等级**。V0.1 五路单极性削顶(`UB/UC/IB/IC/IN`)在交流轮次只作"预期削顶"负向用例。**标定轮已独立成篇**:`docs/测试说明/专项验证/ADC多点标定执行流程.md`(标准源多点(N 点最小二乘)→ 求 r/b → 104 定值段写入置 VALID 落盘 → 三路齐了解除保护闭锁;工具 `test/host/cal_fit.py` / `test/host/cal_write.py`)。 ## 1. 为什么非做不可 diff --git a/docs/测试说明/专项验证/ADC多点标定执行流程.md b/docs/测试说明/专项验证/ADC多点标定执行流程.md new file mode 100644 index 00000000..79f71c75 --- /dev/null +++ b/docs/测试说明/专项验证/ADC多点标定执行流程.md @@ -0,0 +1,183 @@ +# ADC 多点标定执行流程(台架,5 点最小二乘) + +给 `UA/UN/IA` 各加 **5 个**已知量,用普通最小二乘解出变比 `r` 与零点 `b`,经 104 定值段写入;三路 `VALID` 齐 + +> **本文是本轮(5 点最小二乘)的执行卡**;2026-11-12 那轮的实测值(`r = 554.687914` 等)是**两点算法下的结果**,保留作历史对照,见 §3 末与 §5 末——不要与本轮新填的读数混用。 + +## 1. 开工核对 + +| 项 | 判据 | +|---|---| +| **标定点数** | **每路 5 个点**(算法接受 3~8 点,少于 3 点直接拒绝)。为什么从 2 个点改成 5 个:两点时两个读数各自的误差**原封不动**进结果,单点读歪多少、结果就偏多少;点数多了最小二乘会把单点的偏差摊平,个别点抖一下不再决定整条线 | +| **点位分布** | 5 个点**均匀铺开、含两个端点与一个中点**,且彼此拉开(算法会拒绝「点位不拉开」的输入)。具体档位见下一行「量级」;可按继保仪能力调整(算法吃 **3~8** 个点),但必须满足「拉开、覆盖端点」——没有端点就只能外推,区间边缘不可信 | +| 量级 | 电压 **5 / 10 / 25 / 50 / 100 V**,电流 **0.5 / 1 / 1.25 / 1.5 / 2 A**(5 点,含端点与中点) | +| 仪器 | 继保仪 **≥0.1 级** | +| 网络 | PC 与板同网段(192.168.31.100),:2404 可达、FTP :21(admin)可用 | +| 接线 | PT 一次侧按下表接;CT 二次**全程不开路**;注入源与板**共地**、先接线后加压 | + +> ⚠ **抬压必须逐点确认**:2026-11-12 裁定「宁低勿高」仍然有效——每上一个量级先看 `meas:` 行有没有削顶(`rail` / `cl` 位)、确认前端未饱和再继续。100 V 已于 2026-11-13 实测未饱和(见 `docs/硬件平台/ADC通道映射说明.md` §7.4)。0.5 级计量要另跑高点轮、两轮结果不混用,口径见 `docs/硬件平台/ADC通道映射说明.md` §7.2。 + +**PT 一次侧接线表**(电压采集板 8 端子 → 继保仪输出): + +| 板端子 | 接继保仪 | 说明 | +|---|---|---| +| `UA` | `UA` | 相 A 电压 | +| `UAN` | `UN` | 中性参考 | +| `UB` | `UB` | 相 B 电压 | +| `UBN` | `UN` | 中性参考 | +| `UC` | `UC` | 相 C 电压 | +| `UCN` | `UN` | 中性参考 | +| `U0` | `UZ` | 零序电压(N-GND) | +| `U0N` | `UN` | 中性参考 | + +要点:每个 `U*N` **各自直接接**继保仪 `UN` 端子,**不要四个连一起**(连一起会经各 200 KΩ 串阻分压,PT 一次侧只拿一半电压)。`U0` 接继保仪 `UZ`(零序电压输出,对应 N-GND),`U0N` 接 `UN`——这样 U0 PT 一次侧跨 `UZ-UN`,台架上能读到零序信号。 + +**CT 一次侧接线表**(电流采集板 8 端子 → 继保仪输出): + +| 板端子 | 接继保仪 | 说明 | +|---|---|---| +| `IA` | `IA` | 相 A 电流 | +| `IAN` | `IBN` | 中性参考 | +| `IB` | `IB` | 相 B 电流 | +| `IBN` | `ICN` | 中性参考 | +| `IC` | `IC` | 相 C 电流 | +| `ICN` | `I0N` | 中性参考 | +| `I0` | `IN` | 零序电流(N) | +| `I0N` | `IAN` | 中性参考 | + +要点:`IA`/`IB`/`IC` 各自接对应相输出;`IAN`→`IBN`→`ICN`→`I0N`→`IAN` 串联链,`I0` 接继保仪 `IN`——I0 CT 跨 `IN` 到链上,能读到 IN 电流。**CT 二次全程不开路**;回路中**不得串额外电阻**(串电阻会分压削顶)。 + +## 2. 读数 + +**读哪行、怎么读**:串口每 10 s 打一行 `meas: seq=… v=… ua_mv=… un_mv=… ia_mv=… f=… fv=…`。电压格单位 mV、电流格单位 mA。每工况稳定后**连读 3 拍取中间值**(第一拍是切换瞬态,最后一拍防漂移;3 拍 ≈ 30 s)。 + +**实际案例**: + +```text +# 未注入(零点)——连续多拍都是 0: +meas: seq=1400 v=1 ua_mv=0 un_mv=0 ia_mv=0 f=0 fv=0 + +# 继保仪出 1.000 V(UA + UN 同注)后稳定,连续多拍: +meas: seq=1450 v=1 ua_mv=995 un_mv=996 ia_mv=0 f=0 fv=0 +meas: seq=1500 v=1 ua_mv=995 un_mv=996 ia_mv=0 f=0 fv=0 +meas: seq=1550 v=1 ua_mv=995 un_mv=996 ia_mv=0 f=0 fv=0 +``` + +解读:注入 1.000 V → `ua_mv` 显示 995 mV(差 5 mV = 零点偏移,正常);`un_mv` 显示 996 mV;`ia_mv` 仍 0(没注电流)。**你填进 CALDATA 表的就是 `995` / `996` 这些数**——脚本内部按你填的这两列做最小二乘,你不需要算码值、也不需要自己算 `r`。 + +**实际怎么注、注几个点**(按 §1 拍板的 5 点方案;下表是电压路的建议档位,电流路把 V 换成 A、把 5/10/25/50/100 换成 0.5/1/1.25/1.5/2): + +| 步骤 | 继保仪输出(电压路) | 串口 `meas:` 行读哪格 | 填入 CALDATA 表 | +|---|---|---|---| +| ① 第 1 点(下限端点) | 5.000 V | 稳定 3 拍 `ua_mv` 取中(预期 ≈ 5000 mV) | 读数列第 1 行 | +| ② 第 2 点 | 10.000 V | 同上(预期 ≈ 10000 mV) | 读数列第 2 行 | +| ③ 第 3 点(中点) | 25.000 V | 同上(预期 ≈ 25000 mV) | 读数列第 3 行 | +| ④ 第 4 点 | 50.000 V | 同上(预期 ≈ 50000 mV) | 读数列第 4 行 | +| ⑤ 第 5 点(上限端点) | 100.000 V | 同上(预期 ≈ 100000 mV) | 读数列第 5 行 | + +**零点不再单独作为标定点填表**:多点最小二乘的截距项本身就把零点解出来了,零点靠「5 个点的读数与注入量的整体位置」定,比单读一次 0 V 更稳。但**开工仍建议先看一眼零压读数**:`meas:` 行的绝对值若远超零点带(电压路 mV、电流路 mA,对应码值 ±8192),说明前端有偏压——先查共地/接线,**不得用系数凑**(`docs/硬件平台/ADC通道映射说明.md` §7.2 禁则)。 + +## 3. 求解:`test/host/cal_fit.py`(最小二乘) + +**这一节要你干什么**:把 §2 读到的每路 5 个点填进下面这张长表,跑一条命令让脚本用最小二乘解出 `r` 与 `b`,再判断解出来的结果能不能写进设备。 + +**边界先说明白(别指望设备自己算)**:拟合**只在 PC 上跑**(`test/host/cal_fit.py` 是唯一实现,固件里没有拟合代码)。设备侧只做两件事——用 `r`/`b` 折算(`adc_cal_apply()`),以及对你要写进去的参数做合法带校验(`adc_cal_set()` / `adc_cal_set_ratio()`)。所以这条命令跑不通时,问题一定在 PC 侧的输入(表填错、量纲错),不在板子上。 + +**开工前先自检工具本身**(一次就够,10 秒):跑 `& '.venv\Scripts\python.exe' test/host/cal_fit.py --selftest`,期望末行 `=== cal_fit selftest: 11/11 PASSED (failed=0) ===`、exit 0(有任何失败 exit 1)。它跑的是金标向量回归(已知一组点,核对解出的 `r_q16` 与 `b` 是否逐位相等)加 4 条拒绝路径(点数不足、点位没拉开、单调反转、`r` 越带)——**固件里没有拟合实现之后,这是"解出来的 `r` 对不对"唯一的机器验证**,别跳过。 + +表在 `CALDATA` 注释标记之间(`--from-doc` 靠它提取,**勿删改标记**)。**每行一个点**:15 行 = `ua`/`un`/`ia` 三路 × 5 点。已知量列已按 §1 的建议档位填好,**读数格留空等你填**——两格都空的整行 = 本轮不测该点(某路有至少 3 个实点仍可解);只填一格 = 半填错误,脚本报 `INCOMPLETE` 并拒绝。**所以这张表刚拿到手时直接跑 `--from-doc` 必定 exit 2**(注入量已预填、读数为空 = 15 行全是半填)——那不是脚本坏了,是在提示你「读数还没填」。 + + +| 路 | 序号 | 屏上读数 mV/mA | 注入已知量 V/A | +|---|---|---|---| +| ua | 1 | | 5 | +| ua | 2 | | 10 | +| ua | 3 | | 25 | +| ua | 4 | | 50 | +| ua | 5 | | 100 | +| un | 1 | | 5 | +| un | 2 | | 10 | +| un | 3 | | 25 | +| un | 4 | | 50 | +| un | 5 | | 100 | +| ia | 1 | | 0.5 | +| ia | 2 | | 1 | +| ia | 3 | | 1.25 | +| ia | 4 | | 1.5 | +| ia | 5 | | 2 | + + +前两列只是身份(路 + 序号,序号仅作行标识、不参与计算),后两列才是数据:**屏上读数**照抄 `meas:` 行那格(`ua`/`un` 是 mV、`ia` 是 mA,即 ×1000 那格,**别自己换算**);**注入已知量**填继保仪实际输出的伏/安。换量级时两列都要跟着改,别只改一边。 + +> ⚠ **同一路的 N 个点必须在同一个 `r` 下采集**(这是最容易踩、也最难一眼看出来的坑):屏上读数是**按下发到设备的变比 `r` 折算后**的显示值,所以只要中途改过 `r`,前后的读数就不是同一个标尺,混在一起拟合等于拿两把尺子量一条线。做法:**重新标定前先把该路读数清零重来**——不要拿"上一轮 `r=1000` 时读的 5 V / 50 V"和"这一轮 `r=554.69` 时读的 10 V / 100 V"拼成一路。这种混采的典型症状正是残差判据拦住你(`residual > 2% of y span`)——**看到这条拒绝先想"是不是跨了两个 r 时代采的",别急着怀疑前端非线性**。 + +> ⚠ **本轮不测的那一路,两列都要清空**(不只是清读数):只清读数、留注入量 = 半填行,脚本会为那一路打 5 行 `INCOMPLETE` 并以 **exit 2** 结束——即使另外两路全 OK,你也拿不到可用的 `WRITE-ALL`。整路两格都空才会被安静跳过(`SKIP: no filled points`)。台架现状是 **UN 无有效信号**,所以要跑 UA / IA 两路时,**先把 `un` 那 5 行的后两格全清掉**。 + +> **2026-11-12 台架那一轮的历史读数**——**那是两点算法下的结果**,留作样例(真实数据长这样、量级对不对可以照此眼校)。点位数与列结构已不适用新表,**别照抄**: +> +> | 路 | 零点 meas | 低点 meas | 高点 meas | 已知低点 | 已知高点 | +> |---|---|---|---|---|---| +> | ua | 50 | 9049.7 | 90176.4 | 5 | 50 | +> | un | 70 | | | 5 | 50 | +> | ia | 2 | 496.75 | 1985.49 | 0.5 | 2 | +> +> 那一轮解出 **UA `r = 554.687914` / `b = 0`、IA `r = 29.634259` / `b = 0`**,已写入设备 +> (结果登记 → `docs/硬件平台/ADC通道映射说明.md` §7.3)。**新一轮 5 点最小二乘会解出不同的值**, +> 两组数不要混着用。 + +```powershell +& '.venv\Scripts\python.exe' test/host/cal_fit.py --from-doc docs/测试说明/专项验证/ADC多点标定执行流程.md +``` + +只想单解一路、不填文档时也可手填(`--x` = 读数序列 mV/mA,`--y` = 注入序列 V/A,逗号分隔、点数 3~8): + +```powershell +& '.venv\Scripts\python.exe' test/host/cal_fit.py --sem ua --x 5021.5,10041.8,25024.1,50024.1,99933.0 --y 5,10,25,50,100 +``` + +脚本对每路已填点做普通最小二乘,解出新的变比 `r` 与零点 `b`,最后打出一条合并写入命令(`WRITE-ALL:` 开头,**先全部 `--r` 再全部 `--b`**,直接接给 `cal_write.py`)。默认旧值 `ua`/`un` 取 1000.0、`ia` 取 29.4118、`b` 取 0;要覆盖时用 `--r-old` / `--b-old`。 + +**怎么判断结果能不能用**(按顺序看): + +| 看什么 | 合格 | 不合格怎么办 | +|---|---|---| +| 每路 `OK` 行里的 **`res%`**(最大残差占已知量跨度的百分比) | **≤ 2 %** | 超了 = 有读数脱离直线(接线松动、读错格、该点工况没稳)。把 5 点读数与注入量对一眼,哪个点明显偏就复测那个点,**不要直接写** | +| 有没有 `REJECT` 行 | 没有 | 原因字符串就在 `REJECT` 后面,照字面读。**最先该看的是 `bad argument (|x|,|y| < 200000, k_q16 in (0,1e8], r_old in band)`**——它在拟合之前就拦,覆盖三类:① x/y 超出 ±200 V / ±200 A(成因是把 V 当 mV **多乘**了 1000,如 y 填 10000 而非 10);② `--r-old` 越带(填成比值如 0.5547 而非 554.69,或填 0);③ `k_q16` 非法(走文档表时基本不可能)。
**反向的单位错——x 列漏乘 1000(把 mV 填成 V,如填 10 而非 10000)不会在这里报**:漏乘后的 x 仍在合法值域内,只是一路走到拟合后把 `a` 放大 1000 倍,最后报 **`r_new out of band [1.0, 32767.99]`**。所以看到 `r_new out of band` **先排一下「x 列是不是少乘了 1000」**,别只往「变比真的越带」或「前端非线性」上想。
其余拟合类:`n outside [3,8]` 点数不在 3~8、`x span collapsed` 点位没拉开、`non-monotonic` 读数与注入量方向相反、`slope <= 0` 斜率非正、`\|b_new\| beyond 8192 codes` 零点越带、`residual > 2% of y span` 残差超限(非线性或削顶)。除 `bad argument` 外上面这些都是原文前缀,脚本还在半句后补具体说明(如 `x span collapsed (den<=0): readings not spread`)。另有 `BAD-TABLE` = 表还是旧的 6 列形态、`INCOMPLETE` = 半填行。按原因改表里对应格再跑 | +| 每路 `OK` 行里的 **`R2`**(决定系数) | 越接近 **1** 越好(0.9999 以上说明 5 个点几乎在一条直线上) | 明显偏低 = 点与点之间不成线性(前端非线性,或混进了不同工况的读数)。先查接线与工况稳定,**别用系数凑** | +| 末尾 `WRITE-ALL:` 行 | 存在且**含你填的全部路** | 只含部分路 = 有路被 `REJECT` 或实点不足 3 个(往上看那一路的提示),补齐后重跑 | + +**不再看「新 r 相对旧 r 的偏差」**:那项是两点算法时代拿两个读数直接比出来的;多点方案里数值本身就会变,改用上面的 `res%` / `R2` 判拟合质量,写入后再按 §5 复注对表。 + +**「`OK` 行全绿、`WRITE-ALL` 也在,但 exit 2」几乎总是同一个原因**:某一路有半填行(只填了读数、或只填了注入量)。那是**错误**不是"跳过"——脚本用 exit 2 拦住你,正因为半填的点无法判断是"漏填"还是"本次不测"。本轮不测的整路,两格都要空。 + +脚本 **exit 0 = 可写、exit 2 = 有拒绝或半填行**(`REJECT` 一定大写,每行以 `OK ` 或 `REJECT ` 开头)。全部路都 `OK` 才进 §4 写入,别在 exit 2 的时候往下走。 + +## 4. 写入:`test/host/cal_write.py` + +| 路 | `r` | `b` | +|---|---|---| +| `UA` | `0x8050` | `0x8048` | +| `UN` | `0x8053` | `0x804B` | +| `IA` | `0x8054` | `0x804C` | + +```powershell +& '.venv\Scripts\python.exe' test/host/cal_write.py --r 0x8050=995.99 --b 0x8048=-1 --read 0x8050,0x8048 --expect 995.99,-1 +``` + +每路**先 `r` 后 `b`**(`b` 经 `adc_cal_set()` 置 `VALID`;`b` 必须整数码值);`SUMMARY PASS` + C_RD 读回差 <0.1 才进下一路。收到 PN → 看串口 `[ERR] set ch=... rejected`(`r < 1.0` 或 `|b| > 8192`)。 + +**为什么 `WRITE-ALL` 坚持「先全部 `r` 再全部 `b`」而不是一路一路成对写**:`VALID` 是写 `b` 时置的,而置 `VALID` 的前提是**该路的 `r` 已经配置好**(`r = 0` 时 `VALID` 必为 0)。所以顺序必须是 `r` 先落地、`b` 再落——反过来会出现「`VALID` 亮了但变比还是出厂值」的中间态,读数当场是错的量级,而保护恰好在那一刻解卡。`cal_write.py` 也是按这个顺序收参数的:全 `r` → 全 `b`。 + +## 5. 验收与解卡 + +1. **每路完成**:`cal:` 行 `vl` 位图对位点亮(`UA` bit0、`UN` bit3、`IA` bit4,三路齐 `0x19`);**复注 2~3 个点对表**(首末两点必查,中间挑一个);FTP 取 `/config.ini` 核 `[adc_cal]`/`[adc_cal_ofs]`/`[adc_cal_sec]`;断电重启后 `vl` 与读回不变。 +2. **解卡(最后一路 `b` 前)**:先把各通道置 **50 V / 0.5 A 附近**——**别留 0**(零压解锁会被判低电压);跳闸回路若接真断路器先确认不驱动。最后一笔 `b` 后应打 `calibration VALID -> protection unlocked` + SOE。**严禁伪造 `valid` 位**。 +3. **回写**:结果登记进 `docs/硬件平台/ADC通道映射说明.md` §7.3(变比与零点实测表,注明本轮是 5 点最小二乘), + §9 台阶第 4 级按已标定路数标 🟡/✅(第 5 级留送检);日志按 `docs/测试说明/测试数据/boot_log_data/` 命名归档。 + +> **2026-11-12 那一轮的结果与遗留**(照抄现状,别当成"全标完了";**那一轮用的是两点算法**):UA / IA 两路标定并落盘, +> 断电重启后读回不变;`vl=0x11`(UA bit0 + IA bit4),`cl=0xe6`。**UN 未标**(台架无有效信号), +> 三路不齐 → 保护判据仍打 `[PROT] uncalibrated -> protection blocked` 并停走,**属预期**。 +> 补完 UN 才是"三路齐、自动解卡"的完整验证。**另外**:那一轮暴露出配置引擎热重载吞段的缺陷 +> (写进去的 `r`/`b` 约 8 s 被打回出厂值并覆写文件),已修复并加机器钉, +> 见 `docs/子系统设计/配置文件设计说明.md` §4-(3)。 diff --git a/docs/测试说明/专项验证/故障录波板级到场轮执行手册.md b/docs/测试说明/专项验证/故障录波板级到场轮执行手册.md index 9ae7a066..f87ef2fb 100644 --- a/docs/测试说明/专项验证/故障录波板级到场轮执行手册.md +++ b/docs/测试说明/专项验证/故障录波板级到场轮执行手册.md @@ -13,7 +13,7 @@ |---|---|---|---|---|---| | 第一幕 | 枚举埋点与修复上板 | **已完成(2026-11-02,远程)** | 无 | 改码加埋点 → 构建 → 烧录回读 → 远程开回放页 → 读 FR 码定性 → 修复重烧 | `docs/子系统设计/故障录波设计说明.md` §10「回放页枚举恒空缺陷修复记录」 | | 第二幕 | 注量对表 | **已闭环(2026-11-06)**——注量对表 2026-11-04 由 #40 交流轮跑完;坏段/空态画面 2026-11-06 台架远程轮拍毕,本幕现场无需重做 | ~~接 UA 发生器线 1 次、拔 1 次;面板按 2 次键(或让 agent 远程注键)~~ 已由远程轮与 #40 消化 | 记心跳基准 → FTP 拉段 → `wave_read` 量四样 → `meas:`/104 YC 对表 → 削顶注大一遍 → 坏段上板看回放页绝不平线 | `docs/子系统设计/故障录波设计说明.md` §10「板级」行 +「板级·回放页」行后半 +「回放页坏段/空态画面闭环记录」 | -| 第三幕 | 保护触发对时刻(src=P) | **要到场**,还含**两个由你拍板的决定** | IA 两点标定各接一次线 + 过流加大到 UI 见数值过 2 A | 读码算 k/b → 写配置生效 → 验证解锁 → 触发后对 start.log 时刻/段号/src=P | `docs/子系统设计/故障录波设计说明.md` §10「板级·触发来路」行 | +| 第三幕 | 保护触发对时刻(src=P) | **要到场**,还含**两个由你拍板的决定** | IA 多点标定各接一次线 + 过流加大到 UI 见数值过 2 A | 读码算 k/b → 写配置生效 → 验证解锁 → 触发后对 start.log 时刻/段号/src=P | `docs/子系统设计/故障录波设计说明.md` §10「板级·触发来路」行 | | 第四幕 | bench 自抓与卷占用 | **已完成(2026-11-07,远程)**——三判据逐条对上,且顺手修掉 bench 定时器判据缺陷一枚(F0,见闭环记录);无需重做 | ~~烧 bench 构建 → 造占卷 ≥2 s → 看 `err`+1、**一条** `ring abandoned`、同号重写 → 还原产品构建~~ 已由远程轮消化 | 同左(已全部执行:长占卷 72.6 s 静默自弃 err 1→15 零留痕 + 临界命中唯一留痕 err 恰 +1 同号重写 + 104/UI 占卷中照常) | `docs/子系统设计/故障录波设计说明.md` §10「bench 定时自抓与占卷弃段观察闭环记录」 | 幕间依赖:**第二幕的"坏段上板看回放页"一步已不再被阻塞**(第一幕 2026-11-02 已修好并板级闭环);其余各幕互相独立,可以先做不用等的。 @@ -103,13 +103,13 @@ flowchart LR **先讲人话**:过流启动录波的代码路已接在 `protect_task.c` 的启动边沿上(判据一 picked,录波同拍请求),但**保护判据现在整机闭锁**——`adc_cal_flags()` 的 VALID 位对 UA/UN/IA 三路都没置位,`protect_task` 直接停走判据。要让保护真出口,**必须先把这三路标定**;这正是 2026-10-25 你裁定"保持闭锁、不用万用表走捷径"守着的那道门。 -- **决定一(解锁)**:这一幕要不要用**发生器为参考**给 UA/UN/IA 做两点标定换保护触发路(src=P)验证?它**不是送检级标定**(端子变比是按参数算定的标称值,见 `docs/硬件平台/ADC通道映射说明.md` §7.2),做出来的工程量是"发生器刻度下的近似值"。**点头 → 往下走;摇头 → 第三幕整体顺延到标准源轮,本文其余各幕照做。** +- **决定一(解锁)**:这一幕要不要用**发生器为参考**给 UA/UN/IA 做多点标定换保护触发路(src=P)验证?它**不是送检级标定**(端子变比是按参数算定的标称值,见 `docs/硬件平台/ADC通道映射说明.md` §7.2),做出来的工程量是"发生器刻度下的近似值"。**点头 → 往下走;摇头 → 第三幕整体顺延到标准源轮,本文其余各幕照做。** - **决定二(还锁)**:这一幕验完,把标定清回闭锁(还原出厂系数)还是**留着 valid** 让保护继续在线(此后 UI 显示与 104 遥测的二次值都按这份近似标定出数——**留着就要在台账注明非送检级**)。 步骤(决定一通过后): -1. **到你接线**:UA 两点——近零(发生器 0 V 或最低档)与满度附近(如 8 V)各录一次;UN、IA 同法(IA 的注入端子与换算照 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §8.3 同款接线,通道与量程归属看 `docs/硬件平台/ADC通道映射说明.md`)。每次换点说一句"点了",agent 抓该点码值。 -2. **[agent]** 两点公式(算术核 `App/algo/adc_cal.c` 的 `adc_cal_two_point()`)算各路 k/b → 经 **[run_par] 写者链**落配置(`App/algo/run_par.c` 会同步回写 `[adc_cal]` 并置 valid)→ 上传 `/config.ini` → 复位生效。 +1. **到你接线**:UA 多点(**≥3 个,建议 5 个**:近零一档、满度附近一档、中间再铺 1~3 档)各录一次;UN、IA 同法(IA 的注入端子与换算照 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §8.3 同款接线,通道与量程归属看 `docs/硬件平台/ADC通道映射说明.md`)。**只录近零 + 满度两个点解不出来**——多点最小二乘要求 ≥3 点,少于 3 点直接被拒。每次换点说一句"点了",agent 抓该点码值。 +2. **[agent]** 多点最小二乘拟合(算术核 PC 侧的 `test/host/cal_fit.py`——**固件里没有拟合代码**)算各路 r/b → 经 **[run_par] 写者链**落配置(`App/algo/run_par.c` 会同步回写 `[adc_cal]` 并置 valid)→ 上传 `/config.ini` → 复位生效。 3. **[agent]** 验证解锁:串口 `protect` 心跳恢复判据走数、UI 实时数据"未标定"徽标消失、104 YC 开始出浮点量。 4. **到你加注**:给 **IA** 慢慢加(定值基准 1 A:OC3 段 = 2×Ir = **2 A 二次**最灵敏;OC1/OC2 是 10/5 A——**加到 UI 遥测 IA 显示刚过 2 A 即可**,不必真到一次侧大电流)。 5. **[agent]** 30 s 内收证据四连:`wave:` 心跳 **src=P** 且 seg+1;`/log/start.log` 新增一条过流启动;两者**时刻对齐**(段头 t0 与 start.log 时刻差在秒级内,同时看 `armed`——只盯 ovf/计数有已知容差,`docs/子系统设计/故障录波设计说明.md` §9);104 遥信该元件变位上送。 diff --git a/docs/硬件平台/ADC通道映射说明.md b/docs/硬件平台/ADC通道映射说明.md index 7028f79c..52b495d3 100644 --- a/docs/硬件平台/ADC通道映射说明.md +++ b/docs/硬件平台/ADC通道映射说明.md @@ -125,14 +125,14 @@ STM32(U17)为 LQFP144 脚号。 ### 7.1 从端子到码值:链路四段口径 -要把「端子上的电压」换算成「屏上的读数」,得先知道这条链路上**哪一段会改数值**。这里把链路切成四段(一段 = 信号经过的一节),逐段给口径与状态;**下表是本节主表**,后面的内容按同样的段名展开。③ 段已有台架实测——变比 **1:1**(斜率实测见 §8.1);④ 段(子板/端子板变比)出厂按子板参数登记为电压族 606.06 : 1、电流族 29.4118 A/V,**2026-11-12 已由两点标定在 UA / IA 上实测覆盖**(§7.3);子板原理图仍不在仓库内。 +要把「端子上的电压」换算成「屏上的读数」,得先知道这条链路上**哪一段会改数值**。这里把链路切成四段(一段 = 信号经过的一节),逐段给口径与状态;**下表是本节主表**,后面的内容按同样的段名展开。③ 段已有台架实测——变比 **1:1**(斜率实测见 §8.1);④ 段(子板/端子板变比)出厂按子板参数登记为电压族 606.06 : 1、电流族 29.4118 A/V,**2026-11-12 已由标定实测覆盖 UA / IA**(那一轮是两点算法,§7.3);子板原理图仍不在仓库内。 | 段 | 这一段在问什么 | 关系 | 状态 | |---|---|---|---| | ① 语义 ↔ 网名 ↔ 板级网名 ↔ 转换器代码通道 | “UA 到底是哪个通道号” | §2/§3/§4/§5 的 17 路映射表 | **定死**(本文件为唯一权威) | | ② 转换器引脚电压 ↔ 码值 | “脚上是 1 V,读出来几个码” | TPAFE `U_pin = code × 152.59 µV`(code 带符号);STM32 `U_pin = code × 805.66 µV` | **定死**(纯转换器事实,见下表) | | ③ `AINn` ↔ `ADCn` / 转换器引脚(板内串联电阻那一节) | “串在中间的电阻有没有把电压分掉” | **变比 = 1:1**:`ADCn` 节点只有 {串阻.2, 滤波 C, 转换器引脚},无对地元件 | **定死**(网表 + 台架实测,见 §8.1:端子侧注入斜率 6544.2 码/V vs 引脚侧理论 6553.6,差 0.14 %,不存在分压) | -| ④ 语义端子(二次端子)↔ `AINn`(子板/端子板那一节) | “端子上的电压,进到板内还是同一个数值吗” | 子板/端子板变比:出厂登记 电压族 UA/UB/UC/UN **606.06 : 1(V/V)**、电流族 IA/IB/IC/IN **29.4118 A/V**(按子板参数算定,算式见 §7.2);**UA 实测 554.69、IA 实测 29.634**(两点标定,见 §7.3) | **UA / IA 已实测定死**(2026-11-12 台架)+ **UN 待标**(台架无有效信号,用户裁定本轮不测)——屏上已显端子二次值(`eng = 1`),保护判据因 UN 缺 VALID 仍停走 | +| ④ 语义端子(二次端子)↔ `AINn`(子板/端子板那一节) | “端子上的电压,进到板内还是同一个数值吗” | 子板/端子板变比:出厂登记 电压族 UA/UB/UC/UN **606.06 : 1(V/V)**、电流族 IA/IB/IC/IN **29.4118 A/V**(按子板参数算定,算式见 §7.2);**UA 实测 554.69、IA 实测 29.634**(2026-11-12 两点算法那一轮,见 §7.3) | **UA / IA 已实测定死**(2026-11-12 台架)+ **UN 待标**(台架无有效信号,用户裁定本轮不测)——屏上已显端子二次值(`eng = 1`),保护判据因 UN 缺 VALID 仍停走 | **芯片级口径**(② 段,与前端网络无关): @@ -154,7 +154,7 @@ STM32(U17)为 LQFP144 脚号。 1. **ROM 默认系数按 ② 段(引脚电压 ↔ 码值)冻结**:TPAFE 通道 `k = 152.59 µV/码`、STM32 通道 `k = 805.66 µV/码`;没有配变比的通道,折算输出 = **ADC 引脚电压 V**(`eng = 0`)。该口径与 ③④ 变比何时定死无关,冻结当日即成立且此后不因 ④ 到手而改动——这是 M4 承诺边界止于"引脚电压"的依据(`docs/算法/采集与算法层设计说明.md §13.5`)。 2. **板级系数 = 引脚侧系数**(152.59 / 805.66)。305.18 / 1611.3 µV/码 只属 ±10 V 档的 LSB(芯片手册口径),**不是本板系数**,不得当板级系数用。 3. **满量程必须写明参考点**:TPAFE 通道满量程 = ±5 V **@ 引脚**(跨度 10 V);③④ 均已定死(④ 值见 §7.2 / §7.3),端子侧换算允许写"引脚侧数值 × ④ 段变比";但某一路 `VALID` 未置位之前**严禁**把"码 × 152.59 µV × 变比"当**已验收的**端子二次值写进精度承诺——`VALID` 回答的是"准不准",变比回答的是"什么单位",两者互不派生(`docs/算法/采集与算法层设计说明.md` §13.3)。 -4. **仍待实测的只有 UN 一路的两点标定与精度等级送检**(前者决定保护能否解卡、后者决定能报几级)。实测斜率与 ② 段理论值偏差 >2 % 时**先查前端**(分压/变比/焊接),**不得用系数凑错斜率**——那会把硬件的错固化成"标定值"。 +4. **仍待实测的只有 UN 一路的多点标定与精度等级送检**(前者决定保护能否解卡、后者决定能报几级)。实测斜率与 ② 段理论值偏差 >2 % 时**先查前端**(分压/变比/焊接),**不得用系数凑错斜率**——那会把硬件的错固化成"标定值"。 ### 7.2 ④ 段变比登记(2026-11-11 台架实测修正) @@ -168,7 +168,7 @@ STM32(U17)为 LQFP144 脚号。 > 供后来者判断"是不是又算错了一遍")。子板原理图仍不在仓库内,所以这条比值目前只有实测证据、 > 没有图纸旁证。 > -> ⚠ **本节 1000.0 也已被推翻**:2026-11-12 两点标定实测 UA = **554.69 V/V** +> ⚠ **本节 1000.0 也已被推翻**:2026-11-12 那一轮标定(两点算法)实测 UA = **554.69 V/V** > (见 §7.3)。那次单点粗测的接线让 UA 的 PT 一次侧只拿到一半电压,反算比值正好翻倍。 > 本节保留原样是为了留证"单点粗测为什么不可信",**当前有效值一律以 §7.3 为准**。 @@ -183,7 +183,7 @@ STM32(U17)为 LQFP144 脚号。 (参数算定 + 实测反算)互证,可信度最高。 - **电压族 1000.0 V/V 只有单点粗测**:5 V 注入时引脚侧只有约 5 mV ≈ 33 码,1 LSB = 3 % 读数, 所以 3 位小数给出的比值区间是 **909 ~ 1111**。这个数是"量级对、精度未定", - **不是**可以写进铭牌的精度指标——送检前必须用标准源两点标定重写(见本节末「精度从哪来」)。 + **不是**可以写进铭牌的精度指标——送检前必须用标准源多点标定重写(见本节末「精度从哪来」)。 - 若真值是 606.06,5 V 注入应显 0.008 V(差 60 %),而屏面实际给 0.005——分辨率完全够分辨这两个值, 所以这不是"测量误差",是原参数算定假设与实物不符(最可能是分压电阻实际值≠报参数, 或子板型号与报参不是同一款)。 @@ -198,13 +198,19 @@ STM32(U17)为 LQFP144 脚号。 | 100 V | 100 mV | 655 | 0.15 % | | 120 V | 120 mV | 786 | 0.13 % | -故两点标定**不要用 5 V 这种小注入**:建议取 20 V 与 120 V 两点(都在 TPAFE ±5 V 引脚量程内, -且码值足够大)。这也是本节的 1000.0 只能算"粗测"的原因。 +故标定**不要用 5 V 这种小注入单点定斜率**:建议在 20 V 以上铺点(都在 TPAFE ±5 V 引脚量程内,且码值足够大)。这也是本节的 1000.0 只能算"粗测"的原因。 + +> **但 5 V 作为多点里的一个端点是可以用的**:上表的 3.05 % 是"**只**拿 5 V 一个点当整条线的斜率"时的账;放进 5 点最小二乘里,5 V 那点只是五个约束之一,它的量化误差被权重摊薄(端点权重最高的是"位置",不是"单个读数")。真正要避开的是"用 5 V 单点反算整条变比"——那正是 2026-11-11 得到 1000.0 的做法。 > **实际执行时降了量级**(用户裁定 2026-11-12,安全优先):台架改注 **5 V / 50 V**、 > **0.5 A / 2 A**。跨度缩小后每点 1 LSB 摊到结果里约 0.7 %(电压族)/ 0.6 %(电流族), > 仍在保护判据 3~5 % 的口径内,够用;但**够不上 0.5 级计量**——那要日后另跑高点轮, -> 两轮结果不混用。执行卡 → `docs/测试说明/专项验证/ADC两点标定执行流程.md`。 +> 两轮结果不混用。执行卡 → `docs/测试说明/专项验证/ADC多点标定执行流程.md`。 +> **标定点数与算法在那之后又改了一版**(2026-11-13):从"两个点直接连线"改为 +> **N 点普通最小二乘(3 ≤ N ≤ 8,台架按 5 点执行)**,点位铺开在 5–100 V / 0.5–2 A。 +> 为什么改:两点时单个读数的误差**原封不动**变成结果误差;点数多了最小二乘把单点偏差摊平, +> 且能顺手给出残差与 `R²` 两个"这条线到底直不直"的判据——两点连线在数学上永远完美, +> 你没有任何独立证据说它对。上面 0.7 % / 0.6 % 是**两点跨度**的账,多点方案的实测数字等台架跑完再填。 **变比进固件的形式(为什么是独立一维,不并进 `k`)**:`r` 存 int32 Q16 比值, 电压族 `ADC_CAL_R_Q16_VOLT = 65 536 000`(= 1000.0 × 65536,无舍入)、 @@ -213,27 +219,31 @@ STM32(U17)为 LQFP144 脚号。 Q16 后是 1.0e10,而 int32 上限只有 2.147e9;UB/UC 走 STM32 更到 5.28e10。折算模型见 `docs/算法/采集与算法层设计说明.md` §13.2。 -**精度从哪来**:出厂默认值只保证量纲与量级;**精度**必须靠标准源两点标定: -在端子上加两个已知量、读两点的示值(串口 `meas:` 行,二次值域),代进 -`adc_cal_two_point()` 算出该路的 `r` 与 `b`,经 `adc_cal_set_ratio()` / `adc_cal_set()` 写入 -并落盘。这一步做完才有 `ADC_CAL_F_VALID`,保护判据才会解卡 -(`docs/算法/采集与算法层设计说明.md` §13.3 / §13.4)。台架把整条流程做完了两路, -求解与写入都已工具化(`test/host/cal_two_point.py` / `test/host/cal_write.py`), -执行步骤属主 `docs/测试说明/专项验证/ADC两点标定执行流程.md`,实测结果见 §7.3。 +**精度从哪来**:出厂默认值只保证量纲与量级;**精度**必须靠标准源多点标定: +在端子上加 N 个(3 ≤ N ≤ 8,台架按 5 个)已知量、读每点的示值(串口 `meas:` 行,二次值域), +把这组点喂给 `test/host/cal_fit.py`(**PC 侧,拟合的唯一实现**——固件里没有拟合代码)做普通最小二乘,解出该路的 `r` 与 `b`,经 +`adc_cal_set_ratio()` / `adc_cal_set()` 写入并落盘。这一步做完才有 `ADC_CAL_F_VALID`, +保护判据才会解卡(`docs/算法/采集与算法层设计说明.md` §13.3 / §13.4)。 +台架把整条流程做完了两路(**那一轮是两点算法**), +求解与写入都已工具化(`test/host/cal_fit.py` / `test/host/cal_write.py`), +执行步骤属主 `docs/测试说明/专项验证/ADC多点标定执行流程.md`,实测结果见 §7.3。 **历史值与作废依据**(别照着旧数算):原按"子板 PT 2 mA/2 mA、输入侧 200 kΩ、输出侧 330 Ω"算得 变比 = 200000 ÷ 330 = 606.0606…。算术没问题(复核过两遍),**错在前提**:这组参数与实物不符。 同一组参数推出的"额定点端子 ≈400 V → `AINn` 侧 0.66 V"也因此不再成立。若将来拿到子板原理图, 正确的做法是**用图纸复核实测量级**、而不是用图纸替换实测——两者冲突时以实测为准。 -### 7.3 两点标定实测结果(2026-11-12 台架) +### 7.3 标定实测结果(2026-11-12 台架,两点算法那一轮) > **这一节要你干什么**:给出**当前这台设备实际在用的**变比与零点,以及 2026-11-11 那个 1000.0 为什么错。 > 这是 `[adc_cal_sec]` / `[adc_cal_ofs]` 段里现在值的来源;执行步骤属主是 -> `docs/测试说明/专项验证/ADC两点标定执行流程.md`。 +> `docs/测试说明/专项验证/ADC多点标定执行流程.md`。 +> +> ⚠ **本节整表是两点算法(两个点直接连线)下的历史实测**,保留原值不动。标定算法已改为 +> **N 点(≥3,台架按 5 点)普通最小二乘**,新一轮重标的结果待填(见本节末段)。 -按执行卡流程在端子上注标准量、读串口 `meas:` 行、用 `test/host/cal_two_point.py` 求解、 -经 104 定值段写入并落盘,实测结果: +按执行卡流程在端子上注标准量、读串口 `meas:` 行、用**当时的两点求解脚本**求解、经 104 定值段写入并落盘, +实测结果(此后这条链已换成多点求解器 `test/host/cal_fit.py`): | 通道 | 状态 | `r`(变比) | `b`(零点,码) | 相对出厂值 | |---|---|---|---|---| @@ -242,7 +252,7 @@ Q16 后是 1.0e10,而 int32 上限只有 2.147e9;UB/UC 走 STM32 更到 5.28 | UN | ⬜ 未标定 | 出厂 1000.0 | 0 | 台架 UN 通道无有效信号,用户裁定本轮不测 | - **IA 与出厂值只差 0.76 %**:电流族出厂值本来就准,标定只是把它压到小数级——这也验证了 - "两点标定链路本身没算错"。 + "那一轮的两点标定链路本身没算错"(链路可信,与算法该用几点无关)。 - **UA 554.69 与按子板参数算定的 606.06 同量级(差 8.5 %)**,而 2026-11-11 单点粗测给的 1000.0 偏大 1.8 倍。原因已定位:那次测量的接线把 `UAN`/`UBN`/`UCN` 并在一起、各串 200 kΩ 后回到 继保仪 `UN`,形成一个分压网络,**UA 的 PT 一次侧只拿到一半电压** → 引脚侧读数减半 → @@ -255,6 +265,57 @@ Q16 后是 1.0e10,而 int32 上限只有 2.147e9;UB/UC 走 STM32 更到 5.28 保护判据保持停走(`cal:` 行 `vl=0x11`、`cl=0xe6`;`[PROT] uncalibrated -> protection blocked`)。 这是预期行为,不是缺陷;UN 标定完成前**不得**绕过。 +**那一轮用的是两点算法;新一轮改用 5 点最小二乘重标,结果待填**(2026-11-13): + +| 项 | 2026-11-12 那一轮 | 新一轮 | +|---|---|---| +| 算法 | 两点连线,`r`/`b` 由两个读数直接解出 | N 点(≥3)普通最小二乘,台架按 **5 点** 执行 | +| 点数 / 跨度 | 2 点(电压 5 V / 50 V、电流 0.5 A / 2 A) | 5 点,铺在 **5–100 V / 0.5–2 A**,含端点与中点 | +| 凭什么说这条线对 | 说不出来——两点连线在数学上永远完美,没有任何独立证据 | 每次求解额外给出**最大残差占跨度的百分比**与 **`R²`**,可直接看出点是否偏离直线 | +| 本轮 `r` / `b` | UA `554.687914` / 0、IA `29.634259` / 0(上表,历史值) | **待填**(台架跑完后登记在此) | + +**为什么改**:两点标定时单点读数误差 100 % 传进结果——5 V 点抖 0.5 %,整条变比就跟着偏 0.5 %,且没有任何 +办法发现它抖了;点数多了最小二乘把单点偏差摊平,个别点抖一下不再决定整条线。更实际的理由是**可判读**: +两点解出的 `r`/`b` 无论接线对不对都"算得出来",只能靠外部旁证(比如与参数算定值比)判断好坏; +多点拟合自带残差与 `R²`,一条不直的线当场暴露。判据与执行步骤见 +`docs/测试说明/专项验证/ADC多点标定执行流程.md` §3。 + +### 7.4 标定区间内外的独立核验(2026-11-13) + +> **这一节要你干什么**:回答"标定完之后,这套折算在**标定点之外**还准不准"。 +> 做法是注几个**压根没参与求 `r` 的**电压/电流,看屏上读数偏多少——这是标定结果唯一的外部检验, +> 与"拟合残差好不好看"是两回事(残差只说明点与自身拟合线的贴合度,说明不了真值)。 + +**核验读数**(继保仪注入,串口 `meas:` 行读数,多拍取中位数): + +| 通道 | 注入已知量 | 屏上读数 | 偏差 | 拍数 / 抖动 | +|---|---|---|---|---| +| UA | **10.000 V**(非标定点) | 10 041.77 mV | **+0.42 %** | 11 拍中位数,抖动 0.17 % | +| UA | **100.000 V**(非标定点,区间上限) | 99 933.02 mV | **−0.067 %** | 9 拍,抖动 0.02 % | +| IA | **0.500 A**(非标定点,区间下限) | 501.58 mA | **+0.32 %** | 11 拍中位数,抖动 0.27 % | +| UN | 全程注入 | 无有效信号 | — | 噪声底 86~93 mV,保护闭锁 | + +- **UN 仍然测不出来**:注了量也只有噪声底(86~93 mV),与 §7.3 那条"台架 UN 通道无有效信号"是同一个现象, + 所以 UN 依旧未标、保护依旧闭锁。**这不是新缺陷**,是同一件事的再次确认。 +- **10 V 与 100 V 两个点都落在 ±0.5 % 内**,100 V 端点更准(−0.067 %)——标定区间的边缘没有翘边。 + +**四点统一折算回引脚电压后的斜率**(同一路不同注量,斜率应当是一个常数;它抖多少就是这条链的非线性有多少): + +| 端子注入 | 折算回引脚电压的斜率 | +|---|---| +| 5 V | 1.8106 mV/V | +| 10 V | 1.8103 mV/V | +| 50 V | 1.8037 mV/V | +| 100 V | 1.8016 mV/V | + +极差 **0.497 %**。结论:**100 V 未饱和、前端在线性区**——若前端在高压段开始压缩,100 V 那点的斜率会明显小于 +5 V 那点(分压器/运放输出被削),而实测四点斜率单调微降 0.5 %,属于分压电阻容差与读数抖动同量级, +不是饱和特征。 + +⚠ **边界口径(别误读这张表)**:这两点(以及上表的 5 V / 50 V)**都不是标定点、不参与求 `r`**, +它们只作"线性证据"——证明已有的 `r`/`b` 在标定点之外仍然成立。要改 `r`/`b` 必须回到 +`docs/测试说明/专项验证/ADC多点标定执行流程.md` 的 N 点流程重跑,**不许拿核验点反推系数**。 + --- ## 8. 逐通道验证方法(上板自检口径) @@ -262,7 +323,7 @@ Q16 后是 1.0e10,而 int32 上限只有 2.147e9;UB/UC 走 STM32 更到 5.28 1. **TPAFE 侧**:对目标 `AIN` 注入 1 V DC,观察对应 `buf[i]` 变化(UA→`buf[6]`、UN→`buf[5]`、IA→`buf[2]`);**语义与下标不符即映射错**。判据不需要示波器:① 用固件帧计数替代"`BUSY` 有脉冲"——注入前后 `tpafe5160_frame_seq()` 仍严格 1000 Hz 递增即等价;② 幅值判据用万用表读注入点实际电压 + 同时读 `adc_input_raw()` 码值,三点线性拟合(R² ≥ 0.999)。 2. **STM32 侧**:对目标引脚(如 PC4=UB)注入 0.5 V,读 `ADC_CHANNEL_14` 应变化;若不变而相邻通道变化 → 通道宏写错(见 §5 警告)。映射的**代码侧**一致性有两道保证:① `App/algo/adc_input.c` 的 `k_adc_map[]` 与 CubeMX 标签(`ADC17_UB`=PC4 等)逐条对齐;② 开机自检 `adc_input_init()` 校验每个 STM32 语义通道都落在 `k_stm32_rank_ch[]` 扫描序列内(挡"映射到不扫描的通道 → 恒读陈旧值")。 3. **备用通道**:RESERVED 路不接入任何语义,UI 与上报里不得出现;若实测有读数漂移,只记录不消费。 -4. **端子侧变比**:上面 1/2 两条都只到 **ADC 引脚**。③ 段(`AINn` ↔ 转换器引脚)已由网表追线定死为 **1:1**(`ADCn` 节点无对地元件,拓扑图见 §7.1),**板内没有未知比值**;④ 子板/端子板变比出厂按子板参数登记(电压族 606.06 : 1、电流族 29.4118 A/V),**已由 2026-11-12 两点标定在 UA / IA 上实测覆盖**(554.69 V/V、29.634 A/V,见 §7.3;UN 未标)。故**不得**用"2:1 分压"换算出二次值;端子口径一律按 §7.1 分段表与 §7.2 / §7.3 登记写。 +4. **端子侧变比**:上面 1/2 两条都只到 **ADC 引脚**。③ 段(`AINn` ↔ 转换器引脚)已由网表追线定死为 **1:1**(`ADCn` 节点无对地元件,拓扑图见 §7.1),**板内没有未知比值**;④ 子板/端子板变比出厂按子板参数登记(电压族 606.06 : 1、电流族 29.4118 A/V),**已由 2026-11-12 标定实测覆盖 UA / IA**(那一轮是两点算法;554.69 V/V、29.634 A/V,见 §7.3;UN 未标)。故**不得**用"2:1 分压"换算出二次值;端子口径一律按 §7.1 分段表与 §7.2 / §7.3 登记写。 ### 8.1 静态注入实测记录 @@ -298,7 +359,7 @@ Q16 后是 1.0e10,而 int32 上限只有 2.147e9;UB/UC 走 STM32 更到 5.28 **IA 前端差分滤波 = 低通 fc ≈ 796 Hz**:C35 = 100 nF 跨接差分对,与 R19/R21 各 1 k([`V0.1.tel`](V0.1.tel):49)构成一阶低通;拓扑为 1 k 串联 + 1 k 接差分负端、不分压(`docs/产品需求/产品需求设计文档.md` FR-ADC-5)。**250 Hz 的 −4.4 % 就是这颗电容在该频点的真实衰减,板端读数忠实**,工频 50 Hz 仅 −0.2 % 不受影响。谐波测量不够用——同拓扑外推:5 次 ≈ −4.6 %(出 ±2 % 判据)、7 次 −8.5 %、9 次 −13.0 %、11 次 −17.7 %、13 次 −22.6 %、15 次 −27.2 %、21 次(1050 Hz)−39.6 %;C35 改 10 nF 则 fc ≈ 8 kHz、21 次仅 −0.9 %。**谐波功能产品化的硬件前置 = 板改小 C35 + 清点其余各路差分滤波电容容值**(登记 `docs/任务规划/登记簿.md` §1)。**为什么改板而不是软件逐阶修正**:修正系数依赖这颗电容的实际容值,而它有容差、随温度漂,每块板、每个温度都不一样,修正表跟不上;二次谐波制动这类“拿某次谐波幅值与基波比值”的判据,比值被前端畸变后判定就不可信。换 10 nF 后测量带内是平的,板端读数直接可信。证伪条件(有示波器再复核):该低通预测 250 Hz 差分分量额外滞后 ≈17°;模型与三点拟合 `docs/测试说明/测试数据/boot_log_data/20261104_ac_ia_rc_model.txt`。 -**边界**:本节不证端子二次值(④ 段变比已算定、两点标定未做,`eng` 仍为 0,见 §7.2)/ 精度等级(要标准源)/ 跨转换器相位(V0.1 硬件缺陷)/ THD。 +**边界**:本节不证端子二次值(④ 段变比已算定、标定未做,`eng` 仍为 0,见 §7.2)/ 精度等级(要标准源)/ 跨转换器相位(V0.1 硬件缺陷)/ THD。 --- @@ -320,7 +381,7 @@ Q16 后是 1.0e10,而 int32 上限只有 2.147e9;UB/UC 走 STM32 更到 5.28 | 1 通路活 | 9 路都在转换、没串道、极性没反? | ✅ 2026-10-27 已证 | | 2 静态引脚电压 | 注 1 V 直流,读数是不是 1 V? | ✅ 2026-10-27 已证(六项判据全 PASS,§8.1) | | **3 动态引脚波形** | **注 Vpp 2.828 V 正弦(板上读数 ≈1 V),算出来还是不是 1 V?** | **✅ 2026-11-02 / 11-04 已跑两轮、六轮全部闭合**(削顶 2 Hz 档与复位复核经用户裁定 2026-11-04 关闭;谐波 B 行超差已归因板端前端 RC fc≈796 Hz → §8.2;实测结论 §8.2) | -| 4 端子二次值 | 引脚上这 1 V 对应端子侧几 V? | 🟡 **UA / IA 已由两点标定落地**(2026-11-12,见 §7.3;屏上已显二次值、`eng = 1`);UN 台架无有效信号未标,保护仍闭锁 | +| 4 端子二次值 | 引脚上这 1 V 对应端子侧几 V? | 🟡 **UA / IA 已由标定落地**(2026-11-12 两点算法那一轮,见 §7.3;屏上已显二次值、`eng = 1`);UN 台架无有效信号未标,保护仍闭锁 | | 5 精度等级验收 | 额定工况下整条链误差够几级? | 🟡 等标准源(本轮继保仪标定只到保护口径 3~5 %,够不上 0.5 级计量) | **三条结论**: @@ -330,7 +391,7 @@ Q16 后是 1.0e10,而 int32 上限只有 2.147e9;UB/UC 走 STM32 更到 5.28 2. **第 3 级已于 2026-11-02 / 11-04 台架闭环,全程没用到任何外部资源**:一台能出 50 Hz 正弦的交流信号发生器, 接在**已经验证过的那个注入点**上就行。它比第 4、5 级安全一个量级——只需 ±2 V 以内的正弦, 而端子级验证要上 100 V / 220 V 二次值。 -3. **第 4 级已部分落地、第 5 级在等外部**:④ 端子变比已由 2026-11-12 两点标定**实测得出** +3. **第 4 级已部分落地、第 5 级在等外部**:④ 端子变比已由 2026-11-12 标定**实测得出**(两点算法那一轮) (UA / IA,见 §7.3),屏上显的已是二次值、`eng = 1`;UN 台架无有效信号未标, 三路齐不了 → 保护判据仍停走。⑤ 精度等级要等标准源送检。开工前置见 `docs/算法/采集与算法层设计说明.md` §13.13。 @@ -349,9 +410,9 @@ Q16 后是 1.0e10,而 int32 上限只有 2.147e9;UB/UC 走 STM32 更到 5.28 | ⑥ 量纲回归用例(不该动作时不动作)通过 | ✅ | `test/host/prot_dim/`,口径 `docs/算法/采集与算法层设计说明.md` §13.6 | **所以差的是"台架面",它分两半**:动态这半已于 2026-11-02 / 11-04 台架闭环(§8.2); -端子这半 2026-11-12 已由两点标定拿到 UA / IA 的实测变比(§7.3),剩 UN 一路与精度等级送检 +端子这半 2026-11-12 已由标定拿到 UA / IA 的实测变比(§7.3,那一轮是两点算法),剩 UN 一路与精度等级送检 (开工前置见 `docs/算法/采集与算法层设计说明.md` §13.13)。 遥测链(快照发布)与运行参数/点表均已达成,口径沉淀在 `App/algo/meas.h`、`App/algo/run_par.h` 头注释与 `docs/算法/采集与算法层设计说明.md` §13.9;其中"104 上送值与**工程量**一致"仍挂在 -变比确认与两点标定上(注入验证只证到引脚电压口径)。 +变比确认与多点标定上(注入验证只证到引脚电压口径)。 diff --git a/docs/算法/继电保护功能复现规划.md b/docs/算法/继电保护功能复现规划.md index 64aa5fd0..d282bcdb 100644 --- a/docs/算法/继电保护功能复现规划.md +++ b/docs/算法/继电保护功能复现规划.md @@ -240,7 +240,7 @@ | --- | --- | --- | --- | | 周期 CONVST 重触发 | 1000 Hz(与保护节拍同源) | 示波器见 U3 `BUSY` 连续脉冲;`tpafe5160_frame_seq()` 持续递增 | ✅ 2026-10-10:**TIM3 更新事件(1 kHz)硬件触发**(ISR 内 `tpafe5160_start_conv_irq()`;方案 A,零引脚/零驱动改动。⚠ 节拍源**不能**是 TIM6/TIM7——F4 上 ADC1 收不到它们的 TRGO,见 `docs/子系统设计/ADC采样节拍设计说明.md` §3.2);台架 `/serial-boot-test` PASS——`seq` 每 10 s 精确 +10000 = 1000.0 Hz、`frst_err=0`(早期证据 `docs/测试说明/测试数据/boot_log_data/20261010_prod_adc_tim6_boot_log.txt`,节拍源当时为 TIM6) | | TPAFE 快照消费 | 8 通道 / 帧 | `adc_task` 每拍调 `tpafe5160_snapshot()`,返回 1/0/-1 有处理 | ✅ ADC 采集链路 已实现并 `/serial-boot-test` 闭环(`adc_task.c`;台架 `idle=0`、`frst_err=0`) | -| STM32 ADC1 启动与扫描 | 9 通道、144 cycles、**TIM3 TRGO 硬件触发 + DMA** | 全库出现 `HAL_ADC_Start_DMA`;9 通道码值随注入变化 | ✅ 2026-10-10(v2.0):`App/algo/adc_input.c` 的 `stm32_take()`——ADC1 由 TIM3 更新事件经 TRGO 触发一轮 9 通道,`HAL_ADC_Start_DMA(&hadc1, buf, 18)` + DMA2_Stream4 循环模式把码值自动搬进**乒乓双缓冲**,任务侧只消费刚完成的半区(**零忙等**,实测单拍 `adcp` 122 µs → **8 µs**);台架 `/serial-boot-test` PASS——`adc1_seq` 每拍 +1、`adc1_err=0`、`adc1_hw=0`。⚠ 「9 通道码值随注入变化」已由 2026-10-27 台架已知电压注入直接证实(六项判据全 PASS,实测记录 `docs/硬件平台/ADC通道映射说明.md` §8.1);两点标定与端子/子板变比确认仍待做 | +| STM32 ADC1 启动与扫描 | 9 通道、144 cycles、**TIM3 TRGO 硬件触发 + DMA** | 全库出现 `HAL_ADC_Start_DMA`;9 通道码值随注入变化 | ✅ 2026-10-10(v2.0):`App/algo/adc_input.c` 的 `stm32_take()`——ADC1 由 TIM3 更新事件经 TRGO 触发一轮 9 通道,`HAL_ADC_Start_DMA(&hadc1, buf, 18)` + DMA2_Stream4 循环模式把码值自动搬进**乒乓双缓冲**,任务侧只消费刚完成的半区(**零忙等**,实测单拍 `adcp` 122 µs → **8 µs**);台架 `/serial-boot-test` PASS——`adc1_seq` 每拍 +1、`adc1_err=0`、`adc1_hw=0`。⚠ 「9 通道码值随注入变化」已由 2026-10-27 台架已知电压注入直接证实(六项判据全 PASS,实测记录 `docs/硬件平台/ADC通道映射说明.md` §8.1);多点标定与端子/子板变比确认仍待做 | | 通道语义映射 | 见 `docs/硬件平台/ADC通道映射说明.md`(**按语义取数,不写死下标**) | `UA→buf[6]`、`UN→buf[5]`、`IA→buf[2]`;STM32 侧 `UB→ADC_CHANNEL_14` | ✅ 2026-10-10:`App/algo/adc_input.c` 的 `k_adc_map[]` 语义表(`UA/UN/IA`→TPAFE `buf[6]/[5]/[2]`,`UB/UC/IB/IC/IN/TEMP`→ADC1 `CH14/3/6/1/2/5`)+ `adc_input_raw()` 按语义取数;开机**映射自检**(`adc_input_init()` 校验 STM32 映射通道必落在扫描序列内 → 挡住"映射到不扫描的通道"静默错)通过(`ADC input layer init: OK`)。台架首帧日志 8 路 + 温度全部读出:`UA=11429 UN=11454 IA=11452`(与 TIM3 轮次一致)、`UB=1018 UC=1044 IB=943 IC=1069 IN=1052 T=850`(互异、非饱和;**两次开机读数不同** → 确为实时转换而非常量) | | 通道标定(一次系数 + 零漂) | 每通道 `k`、`b`;系数定点 `2^10` | 注入标准源,读数误差 ≤ 目标精度 | ⬜ | | 采样有效性校验 | FRSTDATA 跳变、帧序号单调、丢帧计数 | 芯片静默/丢帧被检出并置告警 | ✅ 2026-10-11:新增 `App/algo/adc_health.c/h`(纯逻辑判定层:静默 100 ms 门限 / 丢帧边沿 / 序号倒退锁存 / FRSTDATA·HAL 错误·STM32 停摆计数增量)+ `adc_task.c` 告警编排(严重事件与故障上升沿 `DBG_ERROR` + 每类每次开机一次 `SYSLOG_SELFTEST`,恢复 `DBG_INFO`;gap 不告警只计数,心跳增 `tgap`);主机单测 `test/host/adc_health/` **31 用例全绿**(`adc-health-unit-test` 钩子);台架 `/serial-boot-test` PASS 零 `[ERR]`(健康不误报,`docs/测试说明/测试数据/boot_log_data/20261011_prod_adc_health_boot_log.txt`)。⚠ 抬栈:判定链 `adc_health_report`→`syslog_post` 回显尾使 adcTask 静态最坏链 832→**1080 B** → 栈 512→**640 words**(余 1480 B / 57%) | diff --git a/docs/算法/采样数据校正全流程.md b/docs/算法/采样数据校正全流程.md index 62d5bacf..449168ff 100644 --- a/docs/算法/采样数据校正全流程.md +++ b/docs/算法/采样数据校正全流程.md @@ -2,7 +2,7 @@ > **读这篇干什么**:按一帧数据的真实流向,把采样校正串成一条线——从端子码值到 104 上送、UI、保护,说清每步在哪个文件、用哪些系数、谁决定这数能不能信。本文是读者入口,推导、判据与实测表归各属主文档,这里只给「去哪查」。 > -> **一句话现状**:三维折算已上线、出厂变比已由 2026-11-11 台架实测写进固件(屏上直接显二次值);但两点标定尚未接到生产流程,`VALID` 未置位、保护判据仍闭锁、精度未验收。**「能显示二次值」≠「读数已达精度」**。 +> **一句话现状**:三维折算已上线、出厂变比已由 2026-11-11 台架实测写进固件(屏上直接显二次值);但**多点标定(N 点最小二乘)尚未接到生产流程**,`VALID` 未置位、保护判据仍闭锁、精度未验收。**「能显示二次值」≠「读数已达精度」**。 > > **术语**:PT/CT = 电压/电流互感器(一次侧 → 端子二次值);104 = IEC 60870-5-104 上行规约;UI = 人机界面;Q16 = 16 位定点(整数 ÷ 65536 才是真值);eng = 本帧量纲标志;ROM / RAM = 固件出厂值 / 运行时内存副本。 @@ -68,7 +68,7 @@ flowchart TD | 系数 | 管什么 | 出厂值 | 合法带 | |---|---|---|---| | `k` | 引脚电压 ↔ 码值(µV/码,Q16) | TPAFE `10000000L`(152.59)、STM32 `52800000L`(805.66) | ROM ×[0.5, 2.0] | -| `b` | 零点(码值) | 两点标定得到,出厂 0 | ±8192 | +| `b` | 零点(码值) | 多点标定得到,出厂 0 | ±8192 | | `r` | 端子二次值 ÷ 引脚电压(Q16) | 电压族 `65536000L`(1000.0 V/V)、电流族 `1927532L`(29.4118 A/V) | 0 或 ≥1.0 | 定义在 `App/algo/adc_cal.h`,ROM 出厂表(`k` 与 `r`)在 `App/algo/adc_cal.c`;推导见 `docs/算法/采集与算法层设计说明.md` §13.2。要点: @@ -76,7 +76,7 @@ flowchart TD - 先算引脚电压再乘 `r`(并成一个大系数会溢出 int32、丢 float 精度);`r=0` 合法 = 本路不折算、停在引脚电压;越带不回退 ROM;TEMP 原样透传。 - 出厂变比来自 2026-11-11 继保仪端子注入反算,取代被实测推翻的 606.06;电压族 5 V 注入仅约 33 码(真值区间 909~1111),是单点粗测、**不是铭牌精度**。见 `docs/硬件平台/ADC通道映射说明.md` §7.2。 -`frame[]` 随即 → `sample_ring_push_frame()` → `phasor_run()` → `freq_run()`;每 200 ms `meas_tick()` 取一次遥测快照。精度目前只到引脚层约 ±2%,两点标定写完 `r` 并置 `VALID` 后才谈二次值精度(`docs/算法/采集与算法层设计说明.md` §13.5)。 +`frame[]` 随即 → `sample_ring_push_frame()` → `phasor_run()` → `freq_run()`;每 200 ms `meas_tick()` 取一次遥测快照。精度目前只到引脚层约 ±2%,标定写完 `r` 并置 `VALID` 后才谈二次值精度(`docs/算法/采集与算法层设计说明.md` §13.5)。 --- @@ -109,22 +109,25 @@ flowchart TD --- -## 6. 两点标定:公式就绪、生产整定未接线 +## 6. 多点标定(N 点最小二乘):解法就绪在 PC 侧、生产整定未接线 -**这一节要你干什么**:别误以为两点标定已经在跑——这正是保护还闭锁的原因。 +**这一节要你干什么**:别误以为标定已经在生产流程里跑——这正是保护还闭锁的原因。 -`adc_cal_two_point()`(`App/algo/adc_cal.c`)以 ROM 的 `k` 为入参,解出 `r` 与 `b`: +**求参在 PC 上做,不在设备上做**:`test/host/cal_fit.py` 以 ROM 的 `k` 为入参,对 N 个标定点(**3 ≤ N ≤ 8**)做普通最小二乘,解出 `r` 与 `b`——这是**离线台架动作,固件里没有拟合代码**。设备只需要「用」和「验」:用 `r`/`b` 折算(`adc_cal_apply()`)、对写入参数做合法带校验(`adc_cal_set()` / `adc_cal_set_ratio()`)。设备不需要自己算,所以不值得为它单养一份第二实现。 ``` -r = (v_hi − v_lo) / [(c_hi − c_lo) × k] -b = v_lo / (k × r) − c_lo +先拟合 注入 = a·示值 + c (普通最小二乘;横轴读表、纵轴注入) +再取 r_new = a · r_old +并把截距 c 折回引脚域求 b ``` -标定点要跨度足够大——点太近会按 1/跨度放大读数误差(量化表属主 `docs/硬件平台/ADC通道映射说明.md` §7.2);本轮台架按安全优先裁定为电压族 5 V/50 V、电流族 0.5 A/2 A,误差代价与执行细节见 `docs/测试说明/专项验证/ADC两点标定执行流程.md` §2。 +**为什么不用两点**:两点只吃两个读数,单点误差原封不动变成结果偏差,而且拟合结果没有任何自检面(两点连线在数学上永远完美)。点数多了最小二乘把单点偏差摊平,还顺带给出残差与 `R²` 两个"这条线直不直"的判据。标定点要**拉开、覆盖端点、3~8 个点**(台架推荐 5 点:点位太近会按 1/跨度放大读数误差,量化表属主 `docs/硬件平台/ADC通道映射说明.md` §7.2);本轮台架按安全优先裁定电压族 5–100 V、电流族 0.5–2 A 的区间取点,执行细节见 `docs/测试说明/专项验证/ADC多点标定执行流程.md` §1。 -**⚠ 现状(两件不同的事,别混)**:**板上没有一键标定**——全仓 `adc_cal_two_point(` 只出现在定义、声明与 `test/host/adc_cal/` 单测里,生产代码零调用;**但台架手工路径是通的,且已经跑过**——端子加两个已知量、读 `meas:` 行相量读数、`test/host/cal_two_point.py` 离线解出 `r`/`b`、`test/host/cal_write.py` 经 104 定值段写入(`adc_cal_set()` 落 `b` 时置 `VALID`,脏→落盘→重启保持)。完整执行卡见 `docs/测试说明/专项验证/ADC两点标定执行流程.md`,开工前置与送检顺序见 `docs/算法/采集与算法层设计说明.md` §13.13。 +**⚠ 现状(两件不同的事,别混)**:**板上没有一键标定**——固件**不含**拟合代码(2026-11-13 裁定移出,生产零调用、被 linker 移除),拟合的唯一实现是 PC 侧的 `test/host/cal_fit.py`;设备侧只有折算与合法带校验两个入口。**但台架手工路径是通的,且已经跑过一轮**——端子加一组成比例的已知量、读 `meas:` 行相量读数、`test/host/cal_fit.py` 离线做最小二乘解出 `r`/`b`、`test/host/cal_write.py` 经 104 定值段写入(`adc_cal_set()` 落 `b` 时置 `VALID`,脏→落盘→重启保持)。完整执行卡见 `docs/测试说明/专项验证/ADC多点标定执行流程.md`,开工前置与送检顺序见 `docs/算法/采集与算法层设计说明.md` §13.13。 -**2026-11-12 台架结果**:UA(`r = 554.69`)与 IA(`r = 29.63`)已标定并落盘,`VALID` 位图 `0x11`;**UN 未标**(台架该通道无有效信号)。保护判据要求 UA/UN/IA 三路齐,所以**现在仍然是闭锁态**——只差 UN 一路。实测数据与「2026-11-11 那个 1000.0 为什么错」的归因见 `docs/硬件平台/ADC通道映射说明.md` §7.3。 +**为什么拟合不留在固件里**:整条标定链是「注入 → 读 `meas:` → PC 上解 `r`/`b` → 104 写入」,没有任何一步需要设备参与计算;把拟合留在固件只会多出一份与 PC 工具互为镜像的第二实现(改一处忘一处就是两个口径),而设备真正需要守的是**写入参数合法**与**折算正确**。删掉固件实现后,"解出的 `r` 对不对"由工具自带的金标向量回归守着——`test/host/cal_fit.py --selftest` 用一组已知点核对解出的 `r_q16` 与 `b` 是否逐位相等,外加 4 条拒绝路径(点数不足 / 点位没拉开 / 单调反转 / `r` 越带),11 项全过才 exit 0。**这条自检是替代品不是补充**:固件里没有第二实现了,它是算法正确性唯一的机器验证。 + +**2026-11-12 台架结果**(**那一轮用的是两点算法,不是现在的最小二乘**):UA(`r = 554.69`)与 IA(`r = 29.63`)已标定并落盘,`VALID` 位图 `0x11`;**UN 未标**(台架该通道无有效信号)。保护判据要求 UA/UN/IA 三路齐,所以**现在仍然是闭锁态**——只差 UN 一路。新一轮改用 **5 点最小二乘**重标,结果待填;实测数据与「2026-11-11 那个 1000.0 为什么错」的归因见 `docs/硬件平台/ADC通道映射说明.md` §7.3,标定区间内外的独立核验见 `docs/硬件平台/ADC通道映射说明.md` §7.4。 --- diff --git a/docs/算法/采集与算法层设计说明.md b/docs/算法/采集与算法层设计说明.md index 74274c13..87e9673e 100644 --- a/docs/算法/采集与算法层设计说明.md +++ b/docs/算法/采集与算法层设计说明.md @@ -6,7 +6,7 @@ > 本文不重复其内容;本文不写历史演进、门禁复测记录与修订日志(需要时查 git 历史)。 > **状态**:语义取数与有效性 → 全周傅氏相量 → 保护判据主线已闭环到板端消费者(`protectTask`,20 ms 判据节拍); > 标定折算层 `App/algo/adc_cal` 已于 2026-10-25 落地(标定折算与落盘整定,§13.2, -> 引脚电压量纲 + 系数落盘 + 未标定闭锁);**幅值精度判据**仍待标准源两点注入复核(§13 状态段; +> 引脚电压量纲 + 系数落盘 + 未标定闭锁);**幅值精度判据**仍待标准源多点注入复核(§13 状态段; > 推进顺序与等待的外部资源见 `docs/任务规划/README.md` §2)。 > **相关文档**: > 分层与功能项(语义取数与有效性~动作留痕)的唯一属主登记处:`docs/算法/继电保护功能复现规划.md` §2.1(分层)、§3(功能清单)、§4.2(精度)、§7(路线图) @@ -417,7 +417,7 @@ UA 无幅值落 IA → 0.0020 Hz。 2. **禁止跨转换器运算**(V0.1 硬件缺陷**双转换器无共同采样触发**):`UA/UN/IA` 在 TPAFE、`UB/UC/IB/IC/IN` 在 STM32 ADC1,两片转换器没有共同采样触发 → 无同一时刻性、相位不确定。故本层**不做**跨转换器的 相位 / 功率 / 功率因数 / 负序零序类矢量运算;**8 路全接 TPAFE(V0.2 硬件)后方可解禁**。 -3. **标定折算已落地、精度复核未做**(2026-10-25 `App/algo/adc_cal`;两点标定怎么开工见 §13.13): +3. **标定折算已落地、精度复核未做**(2026-10-25 `App/algo/adc_cal`;多点标定怎么开工见 §13.13): 帧值现 = 引脚侧工程量(ROM 默认系数 = 转换器 LSB 事实值), 前端分压读法判别与端子变比仍待台架——**精度结论前不得把幅值当计量证据**(§13)。 4. **本层无副作用外溢**:不建任务、不等事件、不写日志(日志由调用侧打)、不碰寄存器 @@ -525,10 +525,12 @@ adcTask 静态最坏链与余量的**台账在** `docs/内存管理/RAM使用分 104 与 UI 拿到的数从哪来一份**。它是 `App/algo/adc_cal.*`、`meas.*`、`App/cfg/cfg_file.*`、 `App/task/protect_task.c`、`Drivers/BSP/UI/ui_data*` 这些模块头注释里引用的口径出处。 -**当前状态**(2026-11-12 更新):折算链已落地,量纲由**每路变比 `r`** 决定——出厂默认已带台架 +**当前状态**(2026-11-13 更新):折算链已落地,量纲由**每路变比 `r`** 决定——出厂默认已带台架 实测反算的变比(电压 1000 V/V、电流 29.4118 A/V),所以烧进新固件后屏上直接显**二次值**。 -**两点标定已上板实跑**:UA(`r = 554.69`)与 IA(`r = 29.63`)标定并落盘、`VALID` 置位 +**标定已上板实跑**:UA(`r = 554.69`)与 IA(`r = 29.63`)标定并落盘、`VALID` 置位 (`vl = 0x11`),实测值登记 → `docs/硬件平台/ADC通道映射说明.md` §7.3;UN 台架无有效信号未标。 +⚠ 那两个 `r` 是**两点算法**下的历史实测值;算法已改为 **N 点(3 ≤ N ≤ 8,台架按 5 点)最小二乘**, +新一轮结果待填(`docs/硬件平台/ADC通道映射说明.md` §7.3 末表)。 保护判据仍处于**未标定闭锁**态(§13.3),因为 VALID 要求 **UA/UN/IA 三路齐**,现在只差 UN 一路—— 这是预期行为,不是缺陷。 @@ -585,30 +587,37 @@ adcTask 静态最坏链与余量的**台账在** `docs/内存管理/RAM使用分 窄带没有意义;下界取 1.0(分压与负担电阻只会把端子值变小,比"比值 < 1"更可能的是把 mV 当 V 填错), 上界是 int32 能表达的 32767.99。**`r = 0` 是合法值**,语义 = "本路不折算,输出停在引脚电压", 这也是现场关掉某一路折算的正规手段(改成 1.0 反而会留下一个假变比)。 -- **两点标定今后标的是 `r`,不是 `k`**(`adc_cal_two_point()` 已改签名,`k_q16` 变成入参): - `k` 是板级事实、现场不该改也改不动,真正未知的是"端子 ↔ 引脚"这一层。公式在公式核的注释里, - 要点是分两步算(`t = dc × r` 先出来,再还原 Q16)——一次算 `dv×2^32/(dc×k_q16)` 会在 - 高压注入时把 long long 顶穿。 +- **标定今后标的是 `r`,不是 `k`**(拟合工具的 `k_q16` 是入参):`k` 是板级事实、现场不该改也改不动,真正未知的是"端子 ↔ 引脚"这一层。 +- **怎么从 N 个点解出 `r` 与 `b`**:这一步**在 PC 侧完成,固件只接收结果**——`test/host/cal_fit.py` 对 N 个点(**3 ≤ N ≤ 8**)做**普通最小二乘**,拟合 + `注入 = a · 示值 + c` 这条直线(横轴是 `meas:` 读数、纵轴是注入已知量,`a` 是斜率、`c` 是截距);再由 + `r_new = a · r_old` 得到新变比(注入正比于示值,比例偏差全由 `r` 吸收), + 并把截距折回引脚域求零点 `b`。**定的是口径,不是实现**——数值在什么域里算(浮点还是定点)、 + 累加顺序、各条拒绝判据落在哪一步,都写在 `test/host/cal_fit.py` 里,本节不重复。设备侧只做两件事: + 用 `r`/`b` 折算(`adc_cal_apply()`)、对写入的参数做合法带校验(`adc_cal_set()` / `adc_cal_set_ratio()`)。 +- **为什么不再用两点**:两点解只用到两个读数,**单点误差 100 % 传进结果**且没有任何自检面; + 最小二乘用全部点、把单点偏差摊平,还能顺手算出残差与 `R²`——前者回答"拟合线直不直", + 后者回答"这些点到底像不像一条直线"。四点以上时,某一点读错(接线、读错格)会直接体现为残差超限, + 而不是悄悄变成系数偏差。台架前的判据与执行步骤 → `docs/测试说明/专项验证/ADC多点标定执行流程.md` §3。 ### 13.3 三态可用性:标定 ≠ 可测 `r` 的出厂默认值是**台架实测反算的变比**,所以出厂状态下每路的**量纲**就已经是二次值; -但"这个数准不准"要靠标准源两点标定才能回答。所以通道状态仍必须让消费者区分三档: +但"这个数准不准"要靠标准源多点标定才能回答。所以通道状态仍必须让消费者区分三档: ```c -#define ADC_CAL_F_VALID 0x01u /* 该路已两点标定确认(k/b 在带内 且 r 已配置)→ 读数可信 */ +#define ADC_CAL_F_VALID 0x01u /* 该路已多点标定确认(k/b 在带内 且 r 已配置)→ 读数可信 */ #define ADC_CAL_F_CLIPPED 0x02u /* 硬件削顶(单极性无偏置)→ 交流量不可信 */ ``` **两个事实互不派生、别互相反推**:`VALID` 回答"准不准",`r` 回答"什么单位"。 `r = 0` 时 `VALID` 必为 0(合法带要求 r 已配置),所以"VALID 未置位却有二次值读数" -这种组合不可能出现;反过来说,只配了变比、没做两点标定的通道,输出**确实是**二次值、 +这种组合不可能出现;反过来说,只配了变比、没做过标定的通道,输出**确实是**二次值、 只是精度未定——量纲标签若跟着 VALID 走,屏上就会出现"数值是 5.000(二次)却标未标定 V" 这种自相矛盾。 | 通道 | `VALID` | `CLIPPED` | 消费者应如何对待 | |---|---|---|---| -| UA / UN / IA | 两点标定后置位 | 0 | 可用于保护、计量、上送 | +| UA / UN / IA | 多点标定后置位 | 0 | 可用于保护、计量、上送 | | UB / UC / IB / IC / IN | 永不置位(V0.2 硬件改版前) | **1,且是来源派生**(不是实测削顶,判据见 §13.5) | 只作码值监视;点表**已绑定**,靠品质位把 104 侧驱动成 IV、值清零 | | TEMP | 0 | 0 | 不参与线性标定(`r` 恒 0),页面显示 `--` | @@ -654,12 +663,13 @@ V0.1 恒命中 UB/UC/IB/IC/IN,位图 `0xE6`),它表示"这路不能测交 `cal:` 行的 `rail=` 位图与 `rail: edge=…` 那行 `[ERR]`——这条路径至今只在直流饱和里起过(判据见 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §8.5 削顶卡)。 -> **承诺边界**(2026-11-12 更新):**量纲**已到端子二次值(出厂默认变比已带硬件事实, +> **承诺边界**(2026-11-13 更新):**量纲**已到端子二次值(出厂默认变比已带硬件事实, > `eng` 判据见 §13.3);**精度**分两档说清——① 引脚层 ±2 % 已由动态六轮证过; -> ② 端子二次值这一档,UA / IA 已用继保仪两点标定**实测落盘**(`r = 554.69` / `29.63`, -> 单台、标定点 5 V–50 V 与 0.5 A–2 A,1 LSB 摊入约 0.7 % / 0.6 %,够保护判据 3~5 %、 -> **够不上 0.5 级计量**),UN 未标;**精度等级验收**仍等标准源送检。 -> 两点标定怎么开工见 §13.13,实测值 → `docs/硬件平台/ADC通道映射说明.md` §7.3。 +> ② 端子二次值这一档,UA / IA 已用继保仪标定**实测落盘**(`r = 554.69` / `29.63`, +> **那是两点算法下的结果**;单台、标定点 5 V–50 V 与 0.5 A–2 A,1 LSB 摊入约 0.7 % / 0.6 %, +> 够保护判据 3~5 %、**够不上 0.5 级计量**),UN 未标;**精度等级验收**仍等标准源送检。 +> 算法已改为 **N 点(3 ≤ N ≤ 8,台架按 5 点)最小二乘**,新一轮重标结果待填 → `docs/硬件平台/ADC通道映射说明.md` §7.3 末表。 +> 标定怎么开工见 §13.13,实测值 → `docs/硬件平台/ADC通道映射说明.md` §7.3。 **动态验证已跑两轮(2026-11-02 / 11-04),六轮全部闭合**——幅值 / 波形 / 相位 / 测频 / 谐波 / 削顶直流档都过了;实测数与逐轮结论只详述在 `docs/硬件平台/ADC通道映射说明.md` §8.2,本节不抄第二份。 @@ -667,7 +677,7 @@ V0.1 恒命中 UB/UC/IB/IC/IN,位图 `0xE6`),它表示"这路不能测交 | 下游 | 现在 | 为什么 | |---|---|---| -| 基波有效值 | ✅ 引脚层过(±2 % 内,三档幅值两点复现) | 台架幅值轮 + 波形轮(基波分量口径)实证;**二次值这一档**:UA / IA 已由继保仪两点标定实测落盘(`r = 554.69` / `29.63`),旧结论"出厂变比是单点粗测(5 V 注入只有 33 码、1 LSB = 3 %、真值区间约 909~1111)"仍成立且已在 2026-11-12 被实测定量:实测 554.69 说明那次粗测接线分压把比值放大了一倍(`docs/硬件平台/ADC通道映射说明.md` §7.3);**精度等级**仍待标准源送检 | +| 基波有效值 | ✅ 引脚层过(±2 % 内,三档幅值两点复现) | 台架幅值轮 + 波形轮(基波分量口径)实证;**二次值这一档**:UA / IA 已由继保仪标定实测落盘(`r = 554.69` / `29.63`,**两点算法下的历史值**),旧结论"出厂变比是单点粗测(5 V 注入只有 33 码、1 LSB = 3 %、真值区间约 909~1111)"仍成立且已在 2026-11-12 被实测定量:实测 554.69 说明那次粗测接线分压把比值放大了一倍(`docs/硬件平台/ADC通道映射说明.md` §7.3);2026-11-13 又用**非标定的 10 V / 100 V** 做了区间外核验(+0.42 % / −0.067 %,`docs/硬件平台/ADC通道映射说明.md` §7.4);**精度等级**仍待标准源送检 | | 相位 | ✅ 同片内过(录波档 ≤2° 判据) | 同段 UA/UN 两路实测(0° 与 90° 两工况);**跨转换器仍禁比**——两片转换器无共同采样触发,是 V0.1 硬件边界(`docs/硬件平台/ADC通道映射说明.md` §7 通道归属行),不是待修缺陷 | | 频率 | ✅ 「如实报不可测」成立(这就是现状判据) | 含满幅注入段全不可测、UI 恒 `--`;**45~55 Hz 精度仍未验**——要验须降 `FREQ_AMP_MIN` 单跑一轮 | | 削顶 / 品质 | ✅ DC −1.00 V 档锁存 + sticky 实证;2.0 Hz 低频档与复位复核经用户裁定 2026-11-04 关闭 | `[ERR] rail: edge=0x2 sticky=0x2` 唯一一条、撤注恒持 | @@ -723,7 +733,7 @@ V0.1 恒命中 UB/UC/IB/IC/IN,位图 `0xE6`),它表示"这路不能测交 `run_par` **不存第二份**;落盘链走 `SYS_EVT_RUN_PAR_SAVE`(iec104Task peek 投递 → defaultTask 整档 save),`p_cb` 依旧未用。谁写 RAM / 谁落盘 / 什么时机的完整链条见 §10.1。 - ⚠ **变比段的 ROM 默认不是 0**:烧进新固件后屏上立刻显二次值。这与"没标定过"不矛盾—— - 变比是硬件事实(出厂就该带着),`VALID`(准不准)仍然要求两点标定,两者互不代替(§13.3)。 + 变比是硬件事实(出厂就该带着),`VALID`(准不准)仍然要求实际标定,两者互不代替(§13.3)。 - ⚠ **负零点**:零点偏移是带符号量,引擎只有 `CFG_VAL_UINT`(`strtoul` + 上界校验)时 −5 会落成 `4294967291`。所以 `[adc_cal_ofs]` 走 `CFG_VAL_INT`(定案口径见 §13.12)。 - **一次变比不新开段**:PT/CT 一次额定已在 `prot_set` 里(`PS_PT_PRI` / `PS_PT_SEC` / `PS_CT_PRI` / @@ -816,7 +826,8 @@ typedef struct { | 变比 `r` 存哪 | **新开一段 `[adc_cal_sec]`,int32 Q16 比值**,不并进 `k` | 并进 `k` 直接溢出 int32(UA 端子域 k 的 Q16 = 1.0e10 > 2.147e9);且变比是硬件事实、`k` 是前端斜率,混一起就是"用系数凑硬件" | | `r` 的合法带 | **绝对带**:`0`(不折算)或 `≥ 1.0`,上界 int32 能表达的 32767.99 | `r` 是现场标定量,不同 CT/PT 差几个数量级,用 ROM×[0.5,2.0] 那种窄带没有意义 | | `r` 的出厂默认 | **非 0**:电压族 1000 V/V、电流族 29.4118 A/V | 变比是硬件事实,出厂就该带着;"准不准"由 VALID 回答,两者互不代替(§13.3) | -| 两点标定标谁 | **标 `r`**,`k` 由 ROM 固化 | `k` 是板级事实、现场不该改也改不动;真正未知的是"端子 ↔ 引脚"这一层 | +| 标定标谁 | **标 `r`**,`k` 由 ROM 固化 | `k` 是板级事实、现场不该改也改不动;真正未知的是"端子 ↔ 引脚"这一层 | +| 标定用什么算法 | **N 点(3 ≤ N ≤ 8)普通最小二乘**,台架按 5 点执行,不再用两点连线 | 两点解只吃两个读数、单点误差 100 % 传进结果,且拟合结果没有任何自检面;最小二乘摊平单点偏差,并额外产出残差与 `R²` 两个可判读量。拒绝判据(点数越界 / 点位不拉开 / `r`·`b` 越带 / 非单调 / 残差超跨度 2 %)随拟合一起落在 PC 侧工具 `test/host/cal_fit.py`(固件只做写入参数的合法带校验) | | 整帧量纲标签 `eng` 的判据 | **每路要么已配变比、要么削顶不显示** | 削顶通道页面本来就显 `--`、不进读数;拿它们卡标签会让 V0.1 五路削顶把徽标永久钉在"未标定" | | 一次变比(PT/CT 一次额定)存哪 | **复用 `prot_set` 已有四项**,不新开段 | 已经在册(`PS_PT_PRI` / `PS_PT_SEC` / `PS_CT_PRI` / `PS_CT_SEC`),再开一段就是同一事实两个真源 | | 遥测快照的字段布局 | 由协议侧定、单测按偏移钉死(72 B × 双区) | 布局漂移只能靠"读侧按名字取"掩盖,抓不到真正的错位 | @@ -827,15 +838,16 @@ typedef struct { 新增决策照上表格式往下加:一行一个问题、一行定成了什么、一行为什么。 -### 13.13 两点标定开工前必须先拍板的三件事 +### 13.13 多点标定开工前必须先拍板的四件事 拍板前,相应验收判据无法判定——这是**开跑的前置**,不是排期问题。 | 待定 | 备选 | 为什么必须先定 | |---|---|---| -| **精度目标** | 0.5 级 / 1 级 | 决定两点标定的判据,否则"误差 ≤ 目标精度"这条无法判定。保护判据本身只需 3~5 %,0.5 级是计量口径 | -| **端子 / 子板变比从哪来** | ① 有原理图(直接算)② 只有实物(万用表同时量端子与引脚)③ 都没有 | 决定变比登记的**可信度**(注意:量纲已不依赖它——出厂默认变比已按台架实测反算写进 `[adc_cal_sec]`,`eng` 能升 1;缺的是精度)。2026-11-11 用继保仪单点注入反算得 1000.0 / 29.4118,推翻了原先按子板参数算定的 606.06(详见 `docs/硬件平台/ADC通道映射说明.md` §7.2);**2026-11-12 两点标定又把 1000.0 推翻**——接线分压使 PT 一次侧只拿到一半电压,实测 UA = 554.69,与参数算定值 606.06 只差 8.5 %(`docs/硬件平台/ADC通道映射说明.md` §7.3)| +| **精度目标** | 0.5 级 / 1 级 | 决定标定的判据,否则"误差 ≤ 目标精度"这条无法判定。保护判据本身只需 3~5 %,0.5 级是计量口径 | +| **端子 / 子板变比从哪来** | ① 有原理图(直接算)② 只有实物(万用表同时量端子与引脚)③ 都没有 | 决定变比登记的**可信度**(注意:量纲已不依赖它——出厂默认变比已按台架实测反算写进 `[adc_cal_sec]`,`eng` 能升 1;缺的是精度)。2026-11-11 用继保仪单点注入反算得 1000.0 / 29.4118,推翻了原先按子板参数算定的 606.06(详见 `docs/硬件平台/ADC通道映射说明.md` §7.2);**2026-11-12 那轮两点标定又把 1000.0 推翻**——接线分压使 PT 一次侧只拿到一半电压,实测 UA = 554.69,与参数算定值 606.06 只差 8.5 %(`docs/硬件平台/ADC通道映射说明.md` §7.3)| | **`eng = 0` 期间测频幅值门怎么处置** | 保持恒失效(现状,UI 恒 `--`)/ 台架临时把 `FREQ_AMP_MIN` 降到引脚域量级跑一轮再还原 / 换成相对量门限 | 建议**常态保持恒失效 + 需要时临时降门跑一轮**;换相对量门限要先重新论证,别在过渡期改。临时降门会改产品头文件,收尾必须还原到 `git diff` 为空并重跑构建 | +| **标定点数与分布怎么定** | 3 点(算法下限)~ 8 点(算法上限)/ **5 点(推荐)** | 点数与位置直接决定拟合结果对不对:少于 3 点或超过 8 点都无法解(算法直接拒绝);点挤在一小段里,斜率被 1/跨度放大成噪声;不覆盖端点就只能外推,区间边缘没有锚点。推荐 **5 点铺在量程端点与中点**(电压 5 / 10 / 25 / 50 / 100 V,电流 0.5 / 1 / 1.25 / 1.5 / 2 A),可继保仪能力调整但必须满足「拉开、覆盖端点」。执行细则 → `docs/测试说明/专项验证/ADC多点标定执行流程.md` §1 | **送检顺序不可倒过来**:产品构建当前要求 UA/UN/IA 的标定 `VALID` 全部置位才走保护判据。 未完成标准源标定就送检,保护会表现为不动作。保持该闭锁,不用非标准源标定走捷径。 diff --git a/scripts/check_mermaid.py b/scripts/check_mermaid.py index dc53579c..0874985c 100644 --- a/scripts/check_mermaid.py +++ b/scripts/check_mermaid.py @@ -43,11 +43,16 @@ import sys REPO = pathlib.Path(__file__).resolve().parent.parent # 与钩子 exclude 一致:这些前缀下的 md 不进语料 +# 另含两类**整份仓库副本**的目录(与 check_doc_links.py 的 SKIP_PARTS 同口径): +# DSH 会话/任务插件把工作树建在仓库内,rglob 会把同一篇文档按别人的任务状态 +# 各判一遍——既是假红也是白跑,而它们本来就被 gitignore 挡在钩子语料之外。 EXCLUDE_PREFIXES = ( ".opencode/node_modules/", "Lib/lvgl/", "MDK-ARM/RTE/", "docs/芯片手册/", + ".agents/", + ".dsh-worktrees/", ) DIAGRAM_HEADS = ( diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index d70a29f9..caad3562 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,15 +1,15 @@ { - "built_at": "2026-11-13T01:27:58+08:00", - "fingerprint": "0dec3f6948d069df157395aa3ec6f539f3ef59e9df60242eb0a1fdb517763e16", + "built_at": "2026-11-13T08:03:21+08:00", + "fingerprint": "09cfe12df7414b5c2a91aedf39623942a8be2f36c188e2186f54c245b47e30f2", "fingerprint_files": 1479, "fingerprint_source": "git-index", "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "18c087500a04696933cef6a32aee5dbbcb21dd7d", + "git_head": "1355dbe1b13e9bf43f2a0be0ff290a8a9dd208cd", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1794504398, + "map_mtime": 1794528195, "pct": { "ER_IROM1": 27.46, "RW_CCM": 87.89, diff --git a/test/host/README.md b/test/host/README.md index 6323a177..5ffc8410 100644 --- a/test/host/README.md +++ b/test/host/README.md @@ -31,8 +31,8 @@ | `ch395f_reuse_host.py` | Socket 复用/中止形态 PC 对端(bench 配套) | | `ftp_auth_bench.py` | FTP 鉴权/锁定台架腿 | | `iec104_master.py` | **IEC 60870-5-104 主站客户端(台架复测/保护定值持久化)**:连设备 :2404(工厂默认,`--port` 可指别的)跑 STARTDT → 总召(actcon/四组背景扫描/actterm)→ 经 C_SE 两段写临时压低 OC1 定值确定性触发 YX16 变位(COT=3)→ 恢复 → YK11 两段式 → C_RD 读回;**保护定值持久化模式 `--b7-pre/--b7-post/--b7-mark/--b7-check`**(中间 ST-LINK 复位):YK12 两段投退→保护退出/联动、定值与压板态跨复位保持(含非默认 7.5 往返);纯标准库,输出纯 ASCII 可直接重定向归档为台架证据(判据 exit 0/1/2) | -| `cal_two_point.py` | **两点标定离线计算器**:`--from-doc` 直接读执行卡 §3 `CALDATA` 标记表批量解所有已填路(空行跳过、合并 `WRITE-ALL` 命令);单路 `--sem/--lo-mv/--hi-mv/--known-*` 亦可——镜像 `adc_cal_two_point()` 整数算术(已对拍 `test/host/adc_cal/` 单测 TC-ADCCAL-015 合成参数零偏差);执行卡 `docs/测试说明/专项验证/ADC两点标定执行流程.md` §3;exit 0 可写 / 2 越带拒绝或未填 | -| `cal_write.py` | **两点标定写入器**(库层复用 `iec104_master.py`):C_SE 两段写下发定值段 `0x8050~`(变比 r)/`0x8048~`(零点 b,落笔置 VALID),C_RD 读回复核;每路先 r 后 b,输出纯 ASCII 可归档;执行卡 `docs/测试说明/专项验证/ADC两点标定执行流程.md` §4;exit 0 全过 / 1 有失败 / 2 连接或协议错误 | +| `cal_fit.py` | **N 点最小二乘标定参数求解(全仓唯一实现,v2.2 起固件侧拟合已删除)**(**3~8 点**,推荐 5 点):`--selftest` 跑金标向量回归(原 C 侧 `r_q16=39322000 / b=12` + 4 条拒绝路径,exit 0 全过 / 1 有失败,改动算法后必跑);`--from-doc` 直接读执行卡 §3 `CALDATA` 长表(每行一个点:路 \| 序号 \| 屏上读数 \| 注入已知量)按路分组拟合,输出合并 `WRITE-ALL` 命令;单路 `--sem/--x/--y` 亦可(`--x` = `meas:` 行 mV/mA 原样、`--y` = V/A 内部 ×1000,`--r-old/--b-old` 指读数采集时的设备参数);执行卡 `docs/测试说明/专项验证/ADC多点标定执行流程.md` §3;exit 0 可写 / 2 拒绝(入参越界 `bad argument`、点数越界、点位没拉开、单调性反转、斜率非正、r/b 越带、残差 >2%、表还是旧 6 列)或未填 | +| `cal_write.py` | **标定参数写入器**(库层复用 `iec104_master.py`):C_SE 两段写下发定值段 `0x8050~`(变比 r)/`0x8048~`(零点 b,落笔置 VALID),C_RD 读回复核;每路先 r 后 b,输出纯 ASCII 可归档;执行卡 `docs/测试说明/专项验证/ADC多点标定执行流程.md` §4;exit 0 全过 / 1 有失败 / 2 连接或协议错误 | ### 时间 / 时延 diff --git a/test/host/adc_cal/adc_cal_test.c b/test/host/adc_cal/adc_cal_test.c index 4769e22f..d9ab0bca 100644 --- a/test/host/adc_cal/adc_cal_test.c +++ b/test/host/adc_cal/adc_cal_test.c @@ -22,9 +22,9 @@ * TC-ADCCAL-009 合法带哨兵回退:坏 k/坏 b → ROM 值 + 每通道一次 [ERR] + * flags 不报 VALID(即便位图置位)+ init 重置观测窗 * TC-ADCCAL-014 零点 b 生效(set 路径):(code+b)×k×r 逐项可算 - * TC-ADCCAL-015 两点标定公式:合成 r/b 回算(独立推导、相对误差 1e-6)+ - * 写回往返(set k/b + set_ratio 后两点复现端子值)+ 参数/斜率/ - * k 合法域/v 越 ±1e9/r < 1.0(-1)/零点越界(-2)守卫 + * TC-ADCCAL-015 (已移除)N 点最小二乘拟合:v2.2 起拟合移出固件,算术核唯一实现 + * 在主机侧 test/host/cal_fit.py,其 --selftest 承接原金标向量 + * (r_q16=39322000 / b=12)与拒绝路径断言。本文件不再覆盖。 * TC-ADCCAL-016 rail_ths 门限:TPAFE ±32112 / STM32 4013·81 / TEMP 不监视 * TC-ADCCAL-017 init 幂等:重复调用 RAM 复位默认 + 重新注册 * —— 以下走真实 cfg_file 引擎(标定落盘与整定入口 DoD)—— @@ -430,63 +430,6 @@ static void tc_00a_b_effect(void) { "IA apply = (code-77)*ROMk*r_curr"); } -/* TC-ADCCAL-015:两点标定公式 + 写回往返 */ -static void tc_00b_two_point(void) { - int32_t rq = 0, bb = 0; - const int32_t k0 = 10000000L; /* 该路生效 k(TPAFE 152.587890625 uV/code) */ - const double r0 = 1000.0; /* 待标定的端子 vs 引脚变比(电压族量级) */ - const int32_t b0 = -30L; /* 真零点,码值 */ - const int32_t c1 = 300, c2 = 800; /* 两点码值(合端子 41 V / 117 V 量级) */ - int32_t v1, v2; - int rc; - - printf("[TC-ADCCAL-015] two-point formula recovers synthetic r/b\n"); - /* 合成测量:v = (c + b0) * k0/65536 µV(lround 模拟量化读数) */ - /* 合成测量:端子 uV = (c + b0) x (k0/65536) uV/code x r0(lround 模拟量化读数) */ - v1 = (int32_t)my_lround(((double)c1 + (double)b0) * ((double)k0 / 65536.0) * r0); - v2 = (int32_t)my_lround(((double)c2 + (double)b0) * ((double)k0 / 65536.0) * r0); - rc = adc_cal_two_point(c1, c2, v1, v2, k0, &rq, &bb); - CHECK(rc == 0, "two_point ok"); - CHECK(((double)rq > (r0 * 65536.0 * (1.0 - 1.0e-6))) && - ((double)rq < (r0 * 65536.0 * (1.0 + 1.0e-6))), - "r recovered within 1e-6 relative"); - CHECK(bb > (b0 - 3) && bb < (b0 + 3), "b recovered within +-3 codes"); - /* 写回往返:set(k,b) 置位 + set_ratio 定量纲,两点都应复现端子值 */ - reset_all(); - adc_cal_init(); - CHECK(adc_cal_set(ADC_SEM_UA, k0, bb) == 0, "set recovered k/b"); - CHECK(adc_cal_set_ratio(ADC_SEM_UA, rq) == 0, "set recovered ratio"); - CHECK((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) != 0u, - "VALID needs both writes (k/b band + ratio configured)"); - CHECK(near_d(adc_cal_apply(ADC_SEM_UA, c1), (double)v1 / 1.0e6), - "roundtrip v(c_lo) = measured"); - CHECK(near_d(adc_cal_apply(ADC_SEM_UA, c2), (double)v2 / 1.0e6), - "roundtrip v(c_hi) = measured"); - - printf("[TC-ADCCAL-015] two_point guards\n"); - CHECK(adc_cal_two_point(100, 100, 0L, 100000L, k0, &rq, &bb) == -1, "dc=0 rejected"); - CHECK(adc_cal_two_point(100, 200, 50000L, 50000L, k0, &rq, &bb) == -1, "dv=0 rejected"); - CHECK(adc_cal_two_point(100, 200, 100000L, 50000L, k0, &rq, &bb) == -1, "inverted slope rejected"); - CHECK(adc_cal_two_point(100, 200, 0L, 1000L, 0L, &rq, &bb) == -1, "k_q16 = 0 rejected"); - CHECK(adc_cal_two_point(100, 200, 0L, 1000L, 100000001L, &rq, &bb) == -1, "k_q16 > 1e8 rejected"); - CHECK(adc_cal_two_point(100, 200, 0L, 1000L, k0, NULL, &bb) == -1, "NULL r rejected"); - CHECK(adc_cal_two_point(100, 200, 0L, 1000L, k0, &rq, NULL) == -1, "NULL b rejected"); - /* v 超 +-1e9 uV(= +-1000 V):几乎必然把 mV/码值当 uV 填了 */ - CHECK(adc_cal_two_point(100, 200, 1000000000L, 1000100000L, k0, &rq, &bb) == -1, - "v beyond +1e9 rejected"); - CHECK(adc_cal_two_point(100, 200, -1000100000L, -1000000000L, k0, &rq, &bb) == -1, - "v beyond -1e9 rejected"); - /* 算出的变比 < 1.0(dc=100、k=1e7 时 dv 仅 1000 uV -> r 约 0.065)-> -1 */ - CHECK(adc_cal_two_point(100, 200, 0L, 1000L, k0, &rq, &bb) == -1, "computed r < 1.0 -> -1"); - /* 真零点 +9000 码 > B_MAX:斜率对(r = 1.5)但 b 越界 -> -2(先查前端,不用系数凑) */ - v1 = (int32_t)my_lround((100.0 + 9000.0) * ((double)k0 / 65536.0) * 1.5); - v2 = (int32_t)my_lround((2000.0 + 9000.0) * ((double)k0 / 65536.0) * 1.5); - CHECK(adc_cal_two_point(100, 2000, v1, v2, k0, &rq, &bb) == -2, "b beyond +-8192 -> -2"); - /* 真零点 +9000 码 > B_MAX:斜率对(r = 1.5)但 b 越界 -> -2(先查前端,不用系数凑) */ - v1 = (int32_t)my_lround((100.0 + 9000.0) * ((double)k0 / 65536.0) * 1.5); - v2 = (int32_t)my_lround((2000.0 + 9000.0) * ((double)k0 / 65536.0) * 1.5); - CHECK(adc_cal_two_point(100, 2000, v1, v2, k0, &rq, &bb) == -2, "b beyond +-8192 -> -2"); -} /* TC-ADCCAL-016:贴轨门限表 */ static void tc_00c_rail_ths(void) { @@ -791,7 +734,6 @@ int main(void) { tc_008_set(); tc_009_sanitize(); tc_00a_b_effect(); - tc_00b_two_point(); tc_00c_rail_ths(); tc_00d_idempotent(); tc_010_file_apply(); diff --git a/test/host/cal_fit.py b/test/host/cal_fit.py new file mode 100644 index 00000000..288f7714 --- /dev/null +++ b/test/host/cal_fit.py @@ -0,0 +1,523 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""N 点最小二乘标定求解器:**全仓唯一实现**(v2.2 起固件侧拟合已删除)。 + +为什么是唯一实现:标定是台架一次性离线动作——注入已知量 → 读屏上 `meas:` → +在 PC 上解出 r/b → 经 104 写入设备。设备本体只需要接受校正后的参数,不需要 +自己拟合曲线。固件里曾有过一份 C 实现,生产代码零调用(被 linker 移除), +留着只会变成第二个真相源,v2.2 已整段删除。所以**本脚本的整数算术就是口径**, +`--selftest` 是它的回归锚点(原 C 侧金标向量已搬进来)。 + +把台架注入的一串 (屏上读数, 注入已知量) 点拟合成 y = a*x + c(普通最小二乘), +再折回标定层能落盘的 (r_q16, b): + + r_new_q16 = round(a * r_old_q16) # 一次项 = 端子↔引脚变比 + c_pin_mV = c * 65536 / r_new_q16 # 截距从二次值域折回引脚域 + b_new = b_old + round(c_pin_mV * 65536 * 1000 / k_q16) + +推导(disp 单位 mV):折算模型 disp = (code + b) × (k_q16/65536) × 1e-3 × +(r_q16/65536),把 disp_new = a×disp_old + c 的 code 一次项与常数项分别对齐, +一次项给出 r_new = a·r_old,常数项给出上式 b_new(b_old = 写之前设备里的零点, +必须作为入参传进来,因为读数是在那个零点下读的)。 + +为什么用两点之外的算法:两点没有冗余,单点读数一抖解出的 r 就整条平移;N 点最小二乘 +把残差摊到所有点上,并且能顺手判「非线性/削顶/接线反」——这正是两点的盲区。 + +全程 int64 定点(不用 float 做累加):结果确定、可跨平台复现,同一组输入永远 +解出同一个 r_q16。整数路径的圆整次序是本脚本语义的一部分,改一处就要同步 +--selftest 的期望值。 + +用法(仓库根目录,解释器用 .venv): + + 自检(回归锚点,改动算法后必跑): + .venv/Scripts/python.exe test/host/cal_fit.py --selftest + + 单路(x = `meas:` 行原样读数 mV/mA;y = 继保仪注入值 V/A,内部 ×1000): + .venv/Scripts/python.exe test/host/cal_fit.py --sem ua \\ + --x 5021.5,10041.77,25000,50024.09,99933.02 --y 5,10,25,50,100 + + 批量(读执行卡 之间的长表,每行一个点): + .venv/Scripts/python.exe test/host/cal_fit.py --from-doc \\ + docs/测试说明/专项验证/ADC多点标定执行流程.md + + 公共参数: + --r-old 本次读数采集时设备里的变比(浮点):默认 ua/un=1000.0、ia=29.4118; + 也可 ua=1000,un=1000,ia=29.4118 形式逐路给 + --b-old 本次读数采集时设备里的零点(整数码值,默认 0):裸值或 ua=0,ia=-2 形式 + +⚠ **同一路的 x 必须在同一个 r 下采集**:拿旧 r 时代的读数和新 r 时代的读数混着 +做直线拟合,斜率会被系统性掰弯(表现是解出的 r 差几十个百分点,而合法带仍然放行)。 +|r_new/r_old - 1| > 10% 时打 WARN 就是给这件事提个醒。 + +判据(任一不满足即 REJECT,打印原因,不算可写): + n 不在 [3, 8] 冗余不足或超上限 + den <= 0 示值全同 / 点位没拉开 + x 与 y 相邻增量方向相反 接线反、通道串、削顶的典型征兆 + a <= 0 零斜率/负斜率 + r_new_q16 不在 [1.0, 32767.99] 与 run_par_cal_r_set() 的粗守同带 + |b_new| > 8192 零点越带(先查前端,不用系数凑) + 最大残差 > 满量程跨度 2% 非线性/削顶 + +输出纯 ASCII(中文只在本注释里);exit 0 = 全部解出且合法,2 = 有拒绝/未填行; +--selftest 的 exit 0 = 全部断言通过,1 = 有失败。 +创建日期:2026-11-13(两点 → N 点最小二乘;同日 v2.2 固件侧拟合删除,本脚本转为唯一实现) +""" +import argparse +import re +import sys + +# ---- 判据参数(本脚本是唯一实现,这些值即为口径;改动会使 GOLDEN 期望值失效)---- +# ⚠ 前 5 个是**独立于固件**的算法参数(原先与已删除的 ADC_CAL_FIT_* 同值, +# 现在以本文件为准);后 4 个是设备侧合法带,必须与 App/algo/adc_cal.h / +# run_par.c 保持一致——那两处仍是设备侧安全闸(写参数时的拦截),不随拟合删除。 +K_Q16 = 10000000 # 该路前端斜率(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(int32 上限) +MILLI_MAX = 200000 # 单点值域上界(mV / mA),±200 V / ±200 A +C_MAX = 100000 # 截距 c 上界(mV),±100 V +N_MIN = 3 # 点数下限(少于 3 点无冗余) +N_MAX = 8 # 点数上限 +RES_PCT = 2 # 最大残差 ÷ 满量程跨度上限(百分比) +# 比值上界的两位小数**向下取整**得 32767.99,不能用 R_MAX/65536.0 直接 %.2f: +# 2147483647/65536 = 32767.99998474,四舍五入会印成 32768.00,与 adc_cal.h / +# run_par.c 的"1.0 ~ 32767.99"口径差一个数(提示文案里的带边界必须和真带一致)。 +R_RATIO_MAX = (R_MAX * 100 // 65536) / 100.0 # 32767.99(仅用于提示文案) + +# 语义槽位(App/algo/adc_input.h 枚举序:UA0/UB1/UC2/UN3/IA4/IB5/IC6/IN7) +SEM = { + "ua": dict(slot=0, r_old=1000.0), # 电压族 TPAFE + "un": dict(slot=3, r_old=1000.0), # 电压族 TPAFE + "ia": dict(slot=4, r_old=29.4118), # 电流族 TPAFE +} + +# 拒绝原因码(负数):本脚本的对外契约,--selftest 与 REASON 文案都按这张表 +ERR_ARG = -1 +ERR_POINTS = -2 +ERR_DEN = -3 +ERR_MONO = -4 +ERR_SLOPE = -5 +ERR_R_BAND = -6 +ERR_B_BAND = -7 +ERR_RESID = -8 + +REASON = { + ERR_ARG: "bad argument (|x|,|y| < " + str(MILLI_MAX) + + ", k_q16 in (0,1e8], r_old in band)", + ERR_POINTS: "n outside [%d,%d]: got %%d point(s)" % (N_MIN, N_MAX), + ERR_DEN: "x span collapsed (den<=0): readings not spread", + ERR_MONO: "non-monotonic: adjacent x/y increments oppose", + ERR_SLOPE: "slope <= 0: readings flat or reversed vs injected value", + ERR_R_BAND: "r_new out of band [1.0, %.2f]" % R_RATIO_MAX, + ERR_B_BAND: "|b_new| beyond %d codes" % B_MAX, + ERR_RESID: "residual > %d%% of y span (non-linearity / clipping)" % RES_PCT, +} + + +# ------------------------------------------------------------ 整数算术(唯一实现) +# v2.2 起这里不再是"镜像":App/algo/adc_cal.c 的拟合算术已整段删除,本文件就是口径。 +def div_trunc(num, den): + """整数除法:向零截断(Python // 向负无穷取整,不能直接用)。""" + q = abs(num) // abs(den) + return q if (num < 0) == (den < 0) else -q + + +def div_round(num, den): + """四舍五入(half away from zero);den 恒 > 0。 + + 这是本仓库拟合算术**唯一的圆整口径**,不要换成 `(num + den/2)//den` 那种 + 写法:Python 的 // 对负数是向负无穷取整,负数会在 -0.5 处折向 0, + 与期望的 half-away-from-zero 不同。先判符号再取绝对值,不依赖语言行为。 + """ + if num >= 0: + return (num + den // 2) // den + return -((-num + den // 2) // den) + + +def round_int(v): + """浮点 → 整数:二分之一远离零圆整(入参量化口径,与 to_milli 配对)。""" + return int(v + 0.5) if v >= 0 else -int(-v + 0.5) + + +def to_milli(v): + """读数 → 整数毫单位(mV / mA 域,即 fit() 的直接入参)。""" + return round_int(v) + + +def fit(xs, ys, k_q16=K_Q16, r_old_q16=None, b_old=0): + """N 点最小二乘标定求解(唯一实现)。返回 (rc, info);rc == 0 时 info 是解。 + + xs / ys 已是整数 mV / mA;r_old_q16 = 本轮读数采集时的变比 Q16 原值。 + 整数路径决定 rc 与 r_q16/b;info 里的 a/res%/R2 是报告用浮点量,不参与判定。 + """ + n = len(xs) + if r_old_q16 is None: + return ERR_ARG, None + if (k_q16 <= 0) or (k_q16 > 100000000): + return ERR_ARG, None + if (r_old_q16 < R_MIN) or (r_old_q16 > R_MAX): + return ERR_ARG, None + if (n < N_MIN) or (n > N_MAX): + return ERR_POINTS, None + for v in list(xs) + list(ys): + if abs(v) >= MILLI_MAX: + return ERR_ARG, None + + # 一趟:累加 + 单调性(相邻点方向必须一致) + sx = sy = sxy = sxx = 0 + for i in range(n): + x = xs[i] + y = ys[i] + sx += x + sy += y + sxy += x * y + sxx += x * x + if i > 0: + dx = x - xs[i - 1] + dy = y - ys[i - 1] + if (dx > 0 and dy < 0) or (dx < 0 and dy > 0): + return ERR_MONO, None + + num = n * sxy - sx * sy + den = n * sxx - sx * sx + if den <= 0: + return ERR_DEN, None + + a_q16 = div_round(num * 65536, den) + if a_q16 <= 0: + return ERR_SLOPE, None + + # 先按除法把 a_q16 夹到"r_new 不会越上界"的范围,再做 a_q16 × r_old—— + # 顺序反了会在极端点位下把 int64 顶穿(a_q16 可到 1e17 量级) + if a_q16 > (R_MAX * 65536) // r_old_q16: + return ERR_R_BAND, None + r_new_q16 = (a_q16 * r_old_q16 + 32768) >> 16 + if (r_new_q16 < R_MIN) or (r_new_q16 > R_MAX): + return ERR_R_BAND, None + + # 截距 c:y = a·x + c ⇒ c = (Σy − a·Σx) / n。两遍式(用已算好的 a_q16)。 + # 若改成从 num/den 代数展开的 (Σy·den − num·Σx)/(n·den),分子峰值到 1.2e19, + # 超出 int64 上界——Python 的大整数不会溢出,但这条路径要能直接照抄成 + # 定点实现,所以这里**刻意保持 int64 可容纳的两遍式**,别改成展开式 + c_milli = div_trunc(sy * 65536 - a_q16 * sx, n * 65536) + if c_milli > C_MAX or c_milli < -C_MAX: + return ERR_B_BAND, None # 截距域离谱 = b 必然越带(且护住下一步的 ×2^32) + + # 截距折回引脚域,保留 Q16 亚毫伏精度(截到整 mV 会带来约 6.5 码 / mV 的 b + # 量化,把真正的细分零点修正抹平);两步走,别一次乘完(会顶穿 int64) + c_pin_q16 = div_round(c_milli * 65536 * 65536, r_new_q16) + db = div_round(c_pin_q16 * 1000, k_q16) + b_new = b_old + db + if (b_new > B_MAX) or (b_new < -B_MAX): + return ERR_B_BAND, None + + # 残差:|y_i − (a·x_i + c)| 与满量程跨度比 + y_min = min(ys) + y_max = max(ys) + y_span = y_max - y_min + res_max = 0 + for i in range(n): + pred = div_round(a_q16 * xs[i], 65536) + c_milli + res = ys[i] - pred + if res < 0: + res = -res + if res > res_max: + res_max = res + if (res_max * 100) > (y_span * RES_PCT): + return ERR_RESID, None + + # 报告用浮点量(不参与判定) + a_f = a_q16 / 65536.0 + y_mean = float(sy) / n + ss_res = 0.0 + ss_tot = 0.0 + for i in range(n): + d = ys[i] - (a_f * xs[i] + c_milli) + t = ys[i] - y_mean + ss_res += d * d + ss_tot += t * t + r2 = (1.0 - ss_res / ss_tot) if ss_tot > 0.0 else 0.0 + + return 0, dict(n=n, a_q16=a_q16, a=a_f, c_milli=c_milli, + r_new_q16=r_new_q16, r_new=r_new_q16 / 65536.0, + c_pin_mV=c_pin_q16 / 65536.0, db=db, b=b_new, + res_max=res_max, + res_pct=(100.0 * res_max / y_span) if y_span else 0.0, + r2=r2, y_span=y_span, + dev_pct=100.0 * (r_new_q16 / float(r_old_q16) - 1.0)) + + +# ---------------------------------------------------------------- 输入解析 +def parse_floats(text): + return [float(s) for s in text.split(",") if s.strip() != ""] + + +def parse_map(text, defaults, conv): + """--r-old / --b-old:裸值作用于所有路,或 "ua=…,ia=…" 逐路给。""" + if text is None: + return dict(defaults) + if "=" not in text: + v = conv(text) + return {k: v for k in defaults} + out = dict(defaults) + for part in text.split(","): + part = part.strip() + if not part: + continue + k, v = part.split("=", 1) + k = k.strip().lower() + if k not in defaults: + raise ValueError("unknown sem %r (use ua/un/ia)" % k) + out[k] = conv(v) + return out + + +def parse_cal_table(path): + """提取 ~ 之间的长表。 + + 列序:路 | 序号 | 屏上读数(mV/mA) | 注入已知量(V/A),每行一个点。 + 返回 {sem: [(idx, x_s, y_s), ...]};标记不存在返回 None。 + 两格全空 = 该点本轮没测(调用方跳过);只填一格 = 半填错误(报 INCOMPLETE)。 + """ + with open(path, encoding="utf-8") as f: + text = f.read() + m = re.search(r"(.*?)", + text, re.S) + if m is None: + return None + groups = {} + 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) < 4: + raise ValueError("row for %r has %d columns; need 4 " + "[sem|idx|reading|injected]" % (sem, len(cells))) + if len(cells) > 4: + # 典型来源:没清空的**旧两点表**(6 列)。静默按前四列解会解出垃圾, + # 所以这里明说——比让用户去猜"怎么 REJECT 了"省一轮台架 + raise ValueError("row for %r has %d columns (>4): that is the old " + "two-point table; the N-point table needs exactly " + "4 columns [sem|idx|reading|injected]" + % (sem, len(cells))) + groups.setdefault(sem, []).append((cells[1], cells[2], cells[3])) + return groups + + +def parse_cell(s): + if s in ("", "-", "—"): + return None + return float(s) + + +# ---------------------------------------------------------------- 求解与输出 +def solve_and_print(sem, xs, ys, r_old_q16, b_old): + """解一路并打印判读;成功返回 (sem, slot, r_new, b),拒绝返回 None。""" + ch = SEM[sem] + rc, info = fit(xs, ys, K_Q16, r_old_q16, b_old) + if rc != 0: + reason = REASON[rc] + if rc == ERR_POINTS: + reason = reason % len(xs) + print("sem=%s slot=%d n=%d REJECT %s" % (sem, ch["slot"], len(xs), reason)) + return None + print("sem=%s slot=%d n=%d OK r=%.6f r_q16=%d b=%d a=%.9f res%%=%.3f " + "R2=%.8f" % (sem, ch["slot"], info["n"], info["r_new"], + info["r_new_q16"], info["b"], info["a"], + info["res_pct"], info["r2"])) + if abs(info["dev_pct"]) > 10.0: + print("sem=%s WARN r_new dev %+.2f%% vs r_old=%.6f - all points must be " + "read at the same r, and units are mV vs V" + % (sem, info["dev_pct"], r_old_q16 / 65536.0)) + return (sem, ch["slot"], info["r_new"], info["b"]) + + +def write_all(done): + """末尾合并写入命令:先全部 r,再全部 b(cal_write.py 的两段顺序契约)。""" + 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)) + + +def run_single(a): + missing = [n for n, v in (("--sem", a.sem), ("--x", a.x), ("--y", a.y)) + if v is None] + if missing: + print("ERROR: single-route mode needs %s (or use --from-doc)" + % ",".join(missing)) + return 2 + try: + xv = parse_floats(a.x) + yv = parse_floats(a.y) + 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) + except ValueError as exc: + print("ERROR: %s" % exc) + return 2 + if len(xv) != len(yv): + print("ERROR: --x has %d points, --y has %d" % (len(xv), len(yv))) + return 2 + xs = [to_milli(v) for v in xv] # 屏上读数本来就是 mV/mA + ys = [to_milli(v * 1000.0) for v in yv] # 注入已知量 V/A → mV/mA + r_old_q16 = round_int(r_map[a.sem] * 65536.0) + res = solve_and_print(a.sem, xs, ys, r_old_q16, b_map[a.sem]) + if res is None: + return 2 + write_all([res]) + return 0 + + +def run_from_doc(a): + try: + groups = parse_cal_table(a.from_doc) + except OSError as exc: + print("BAD-DOC: %s" % exc) + return 2 + except ValueError as exc: + print("BAD-TABLE: %s" % exc) + return 2 + if groups is None: + print("NO-MARKER: no table in %s" % a.from_doc) + return 2 + if not groups: + print("EMPTY: CALDATA table has no ua/un/ia rows") + return 2 + 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) + except ValueError as exc: + print("ERROR: %s" % exc) + return 2 + + done = [] + failed = 0 + for sem in sorted(groups): + xs, ys = [], [] + for idx, x_s, y_s in groups[sem]: + xv = parse_cell(x_s) + yv = parse_cell(y_s) + if xv is None and yv is None: + continue # 整行空 = 本轮没测这个点 + if xv is None or yv is None: + print("sem=%s idx=%s INCOMPLETE: need both reading and injected " + "value (or leave both empty); skipped" % (sem, idx)) + failed += 1 + continue + xs.append(to_milli(xv)) + ys.append(to_milli(yv * 1000.0)) + if not xs: + print("sem=%s SKIP: no filled points" % sem) + continue + r_old_q16 = round_int(r_map[sem] * 65536.0) + res = solve_and_print(sem, xs, ys, r_old_q16, b_map[sem]) + if res is None: + failed += 1 + else: + done.append(res) + if done: + write_all(done) + return 0 if (not failed and done) else 2 + + +# ---------------------------------------------------------------- selftest 回归 +# 金标向量:原固件单测(v2.1 的 tc_00b_fit)里的那一组,v2.2 固件侧拟合删除后 +# 搬到这里。输入与期望值**全部写死**,不在这里现算——现算等于自证,锚点就没意义了。 +# +# 数据怎么来的(推导留档,便于将来重构时重新生成): +# code = 500/700/900/1100/1300,真值 r_true=600.0、b_true=+12, +# k_q16=1e7(TPAFE 152.587890625 µV/码),r_old=1000.0,b_old=0 +# 引脚电压 = code × 0.152587890625 mV → x(旧 r 下的屏上读数)= 该值 × 1000/1 +# y(注入已知量)= (code + 12) × 0.152587890625 × 600.0 +GOLDEN = { + "x": [76294, 106812, 137329, 167847, 198364], + "y": [46875, 65186, 83496, 101807, 120117], + "k_q16": K_Q16, + "r_old_q16": 1000 * 65536, + "b_old": 0, + "expect_rc": 0, + "expect_r_q16": 39322000, + "expect_b": 12, +} + +# 拒绝路径断言:(名称, xs, ys, r_old_q16, b_old, 期望 rc, 期望原因前缀) +NG_CASES = [ + ("n<3", [1000, 2000], [1000, 2000], 1000 * 65536, 0, ERR_POINTS, "[3,8]"), + ("x all same", [5000, 5000, 5000], [1000, 2000, 3000], + 1000 * 65536, 0, ERR_DEN, "x span collapsed"), + ("mono reversed", [1000, 2000, 3000, 4000, 5000], + [1000, 2000, 3000, 2000, 1000], 1000 * 65536, 0, ERR_MONO, "non-monotonic"), + ("r out of band", [1000, 101000, 199999], [1000, 1001, 1002], + 1000 * 65536, 0, ERR_R_BAND, "out of band"), +] + + +def run_selftest(): + """跑金标向量 + 拒绝路径断言。exit 0 = 全过,1 = 有失败。""" + fails = 0 + total = 0 + + def chk(cond, label): + nonlocal fails, total + total += 1 + if cond: + print("[PASS] %s" % label) + else: + print("[FAIL] %s" % label) + fails += 1 + + g = GOLDEN + rc, info = fit(g["x"], g["y"], g["k_q16"], g["r_old_q16"], g["b_old"]) + chk(rc == g["expect_rc"], "golden: rc == %d (got %d)" % (g["expect_rc"], rc)) + if info is None: + chk(False, "golden: solver returned no solution") + else: + chk(info["r_new_q16"] == g["expect_r_q16"], + "golden: r_q16 == %d (got %d)" % (g["expect_r_q16"], info["r_new_q16"])) + chk(info["b"] == g["expect_b"], + "golden: b == %d (got %d)" % (g["expect_b"], info["b"])) + + for name, xs, ys, ro, bo, want_rc, want_txt in NG_CASES: + rc, _ = fit(xs, ys, K_Q16, ro, bo) + chk(rc == want_rc, "reject %-14s rc == %d (got %d)" % (name, want_rc, rc)) + chk(want_txt in REASON.get(want_rc, ""), + "reject %-14s message mentions %r" % (name, want_txt)) + + print("=== cal_fit selftest: %d/%d PASSED (failed=%d) ===" % + (total - fails, total, fails)) + return 0 if fails == 0 else 1 + + +def main(): + ap = argparse.ArgumentParser( + description="N-point least-squares calibration solver " + "(sole implementation; firmware no longer fits)") + ap.add_argument("--selftest", action="store_true", + help="run the golden-vector regression (exit 0 = all passed)") + ap.add_argument("--sem", choices=sorted(SEM), default=None) + ap.add_argument("--x", default=None, + help="comma-separated on-screen readings (mV or mA)") + ap.add_argument("--y", default=None, + help="comma-separated injected values (V or A; x1000 inside)") + ap.add_argument("--r-old", default=None, + help="r in the device while readings were taken " + "(default ua/un=1000.0, ia=29.4118; or ua=..,ia=..)") + ap.add_argument("--b-old", default=None, + help="b in the device while readings were taken " + "(codes, default 0; or ua=..,ia=..)") + ap.add_argument("--from-doc", default=None, + help="read the CALDATA-marked long table from a doc") + a = ap.parse_args() + + if a.selftest: + return run_selftest() + if a.from_doc is not None: + return run_from_doc(a) + return run_single(a) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/host/cal_two_point.py b/test/host/cal_two_point.py deleted file mode 100644 index 987ae2ff..00000000 --- a/test/host/cal_two_point.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/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()) diff --git a/test/host/cal_write.py b/test/host/cal_write.py index 5b048d3f..0b11f7e5 100644 --- a/test/host/cal_write.py +++ b/test/host/cal_write.py @@ -5,7 +5,7 @@ 复用台架已验收主站库 test/host/iec104_master.py(不重写协议层)。 地址(App/algo/run_par.c run_par_104_write):r = 0x8050+slot, b = 0x8048+slot;slot UA0/UN3/IA4。执行卡: -docs/测试说明/专项验证/ADC两点标定执行流程.md §4。 +docs/测试说明/专项验证/ADC多点标定执行流程.md §4。 用法: python test/host/cal_write.py --r 0x8050=995.99 --b 0x8048=-1 \