From b46e44d92c836742e16656dcd4104b62c696305d Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 13:18:28 +0800 Subject: [PATCH] =?UTF-8?q?docs(=E9=87=87=E9=9B=86=E5=8F=AF=E4=BF=A1):=20?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=8F=AF=E4=BF=A1=E6=96=87=E6=A1=A3=E5=8F=B0?= =?UTF-8?q?=E9=98=B6=E5=8C=96=E9=87=8D=E6=9E=84=E5=B9=B6=E7=AB=8B=E9=A1=B9?= =?UTF-8?q?=E6=95=85=E9=9A=9C=E5=BD=95=E6=B3=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两份工作互锁(互相引用),故同一笔提交。 数据可信:全文按「五级台阶」重排(§1 全局 → §2 信度现状 → §3 M4 退出判据逐条 → §4 设计约束 → §5 任务卡 → §6 顺序 → §7 决策 → §8 风险 → §9 边界 → §10 关系)。 恢复 §2.2 量纲错配(该节此前被瘦身删掉,test/host/prot_dim 的两处引用一直悬空); 新增 §2.7 与 §4.6:ADC 只验过直流静态标定,正弦动态波形一次也没采过——用信号发生 器在既有注入点注 ±2 V 以内的正弦即可验全周傅氏、测频、快照与削顶路由,不必往端子 加压、也不需要标准源。§6 手绘框图改 Mermaid。章节编号未重排:§2 / §4 / §7 / §10 被 App 与 test 的头注释当规范引用。 故障录波:用户裁定 2026-10-29 录波先行,三条口径——1 kHz(每周波 20 点)、8 路 电气量全录、一屏只画一路共 8 路可切换。新增设计说明(与产品需求 80 点/周波口径的 偏离显式登记;存原始码值而非工程量,绕开未标定闭锁与测频幅值门;四个所有者的跨 任务边界与三条并发陷阱;段文件字节布局;单通道回放显示契约;约 5.9 KB 静态增量 触发 R6)与卡片登记簿(数据面最小闭环 → 手动与保护启动触发 → 现场单通道回放页, 二期的 4 kHz、突变量启动、COMTRADE、104 文件传输不返工的结构约束写进设计文档)。 两张旧配图与一张新配图改由 tools/arch_diagram/ 的生成器产出,脚本与产物同笔提交。 代码一行未动:本波只交付文档与设计。 --- docs/images/ADC直流标定与交流验证.svg | 164 +++++++ docs/images/故障录波数据流.svg | 1 + docs/images/数据可信台阶.svg | 64 +++ docs/任务规划/故障录波.md | 79 +++ docs/任务规划/数据可信.md | 458 ++++++++++++------ docs/子系统设计/故障录波设计说明.md | 335 +++++++++++++ docs/测试说明/ADC前端注入验证流程.md | 18 +- .../arch_diagram/gen_data_trust_dc_ac_svg.py | 324 +++++++++++++ .../arch_diagram/gen_data_trust_ladder_svg.py | 250 ++++++++++ .../arch_diagram/gen_wave_record_flow_svg.py | 289 +++++++++++ 10 files changed, 1840 insertions(+), 142 deletions(-) create mode 100644 docs/images/ADC直流标定与交流验证.svg create mode 100644 docs/images/故障录波数据流.svg create mode 100644 docs/images/数据可信台阶.svg create mode 100644 docs/任务规划/故障录波.md create mode 100644 docs/子系统设计/故障录波设计说明.md create mode 100644 tools/arch_diagram/gen_data_trust_dc_ac_svg.py create mode 100644 tools/arch_diagram/gen_data_trust_ladder_svg.py create mode 100644 tools/arch_diagram/gen_wave_record_flow_svg.py diff --git a/docs/images/ADC直流标定与交流验证.svg b/docs/images/ADC直流标定与交流验证.svg new file mode 100644 index 00000000..551f8ea4 --- /dev/null +++ b/docs/images/ADC直流标定与交流验证.svg @@ -0,0 +1,164 @@ + + +同一个针脚,两种证法 —— 直流只证「高度」,正弦才证「形状与节拍」 +上排已做完(2026-10-27 静态注入六项判据全 PASS);下排一次也没做过:正弦波从没进过这条链,所以后面的相量、测频、削顶、谐波对不对,现在无人能答 +两排共用同一个注入点、同一套前端、同一份折算代码 —— 差别只在信号源是「平的」还是「会走 20 点/周的正弦」 + +上排 · 直流静态标定(已做) + + + + + + +直流源 +电池或台式电源 ++ 电位器分压 + + + +万用表 +同时接在注入点 +当唯一基准读数 + + + +注入点 R31 +网名 AIN1 = U5.A3 += ADC7 引脚(1:1) + + + +前端 + 转换器 +同一条电阻网络 +TPAFE 16b、ADC1 12b + + + +折算 +adc_cal_apply() +单位仍是引脚 V + + + +三点线性拟合 +0 / 0.5 / 1.0 V +R² = 0.9999997,斜率 +差 −0.14 % + +下排 · 交流正弦验证(本文新增 · 待做) + + + + + + + + + + + + + + + + + + + + + + + + +信号发生器 +50 Hz 正弦 +幅度按峰值算,见底部红 +线 + + + +注入点 R31 +同上排同一个点 +外串 10 k、与板共地 + + + +前端 + 转换器 +同上排同一条链 +STM32 五路负半周必削顶 + + + +1 kHz 采样点 +TIM3 TRGO 同时触发 +50 Hz → 20 点/整周 + + + +全周傅氏 +phasor_run():√2/N +预乘 +直接得有效值与相位 + + + +读数出口 +meas 200 ms → 104 YC + 0x4011 +幅值 + 品质 + 频率(或 + UI) +只有正弦能证的六件事(直流在构造上就证不到) + + +整周窗对不对 +窗长差 1 点,相位误差就跟 +着来;直流恒 0 相位,测不 +出 + + +有效值是否依赖波形 +直流只有一个点:方波 / 三 +角 / 正弦算出的都是同一个 +数 + + +相位 +φ = atan2(fIm, fRe) 全 +靠正弦才有意义 + + +测频 freq_run +共轭乘积取相位增量;直流相 +位不前进 → 恒无效 + + +削顶路由 CLIPPED +UB/UC/IB/IC/IN 单极性 0 +~3.3 V,负半周必削顶 → 该 +报 IV 就得报 IV + + +谐波实例 C04 / C05 +二次 / 五次相量已建但无消 +费者:发生器给含谐波信号才 +能验 + +安全红线(照抄 ADC通道映射说明 §7) +TPAFE 侧只到 ±2 V:正弦看峰值不只看有效值 —— 1 Vrms 的峰值已是 1.41 V,再高 +就顶到 ±5 V 档边缘; +STM32 那五路严禁超过 3.3 V(单极性,超了是硬件级风险,不是削顶那一路好笑的验证 +); +发生器与板必须共地(不然量到的是地环差);引脚侧注入一律外串 10 k;不许拿端子侧 +上额定电压走捷径。 + +这台发生器证不到的三件事 +④ 端子变比:注入点在子板之前,算出来的还是引脚域 —— 变比要靠原理图或万用表实测( +另一张卡); +精度等级验收:发生器不是标准源,它的幅值误差本身就要被校,不能拿它冒充送检依据; +跨转换器相位:UB/UC 与 UA 分属两片转换器、无共同采样触发(V0.1 硬件缺陷),任何 +相位比较都不作数。 + +读数出口现在就有:104 YC(test/host/iec104_master.py,浮点无量化)+ 串口每 10 s 的 phasor(hb) / freq(hb) / meas: 行;但板上没有「把窗里 2 +0 个点导出来」的出口,正弦只能靠派生标量证,而 phasor 行是 ×0.1 定点(注 1 V 只剩 10 格)。 + diff --git a/docs/images/故障录波数据流.svg b/docs/images/故障录波数据流.svg new file mode 100644 index 00000000..c90a33b2 --- /dev/null +++ b/docs/images/故障录波数据流.svg @@ -0,0 +1 @@ +录波段数据的四个所有者 —— 谁独占写、谁只能读、标志怎么过界写侧只有一条路:adcTask 压环 → defaultTask 落盘 → /wave 段文件;读者一律只读已封存的段。「已封存」是唯一的跨任务接口:没有任何人读别人的活缓冲——这是本图唯一要记住的一件事。触发来路:只置标志UI 手动录波操作页prot_elm 的 picked 上升沿bench 构建定周期自抓谁都只置一个原子标志adcTask:唯一写环的人每拍压 8 路 int16 原始码值专用录波环 320 点 × 8 路查标志 → 冻结 → 计满封段= 5,120 B,落 SRAM1defaultTask:文件唯一写者排空已封存区 → f_write写回字节数不符即算失败分次写 256 B,不整段驻留更新段账与最新段号/wave 段文件文件名 WAVE 加两位十进制头存 eng、系数与 clip_mask数据区按通道分列存放与 /log 平级,FTP 根下可见只置标志已封存标志字节数不符即失败采集主线照常推进(本波一行不改它)sample_ring → phasor → freq → meas → 104 / UI,全链同源冻结窗口丢的是录波样本,不是采集:相量、测频、判据、上送一律照常三个读者:一律只读已封存的段uiTask 回放页 —— 只读窗口取一段,自己画(不写文件)FTP(netTask / lftpd)—— 只搬文件,不碰采集上下文PC 脚本 —— 重算全周傅氏,与板上 meas: 与 104 浮点对表陷阱一 · 取窗竞态sample_ring_window() 读 head 后逐点拷,没有临界区现在只有 adcTask 同任务调用才安全处置:录波不复用它,另立专用环;跨任务只传已封存标志陷阱二 · FatFs 追加写该移植 FA_OPEN_APPEND 不含 FA_WRITE簇分配失败还返 FR_OK 加短写,无错误码处置:打开带 FA_WRITE,判成功必须比对写回字节数陷阱三 · 只有 8.3 短名FF_USE_LFN=0:长名与点开头一律拒处置:段名 = WAVE 加两位十进制再加 .BIN,轮转覆盖目录 /wave 与 /log 平级,FTP 鉴权现成,零新代码四条「不许」——上面那张图上没有箭头经过的地方不许跨任务读活的录波环或 sample_ring——没有临界区,读出来是两帧混合数据不许 protectTask 写环或落盘——它只置一个请求标志,同 prot_elm_request_reset 的模式不许 netTask 与 lftpd 读环——文件服务只搬已封存的段,不碰采集上下文不许把 U 与 I 两段叠在同一张图上比相位——跨转换器无共同采样触发,V0.1 的硬件事实字段级布局见 docs/子系统设计/故障录波设计说明.md §6.2;预算与 R6 前置见 §7;触发源与段长冻结语义见 §4。本图由 tools/arch_diagram/gen_wave_record_flow_svg.py 生成,改图请改脚本重跑,勿手改 SVG。 diff --git a/docs/images/数据可信台阶.svg b/docs/images/数据可信台阶.svg new file mode 100644 index 00000000..41721214 --- /dev/null +++ b/docs/images/数据可信台阶.svg @@ -0,0 +1,64 @@ + + +「数据可信」是五级台阶 —— 现在踩在第 2 级 +每升一级只多答一个问题:通路活 → 静态读数对 → 动态波形对 → 换算到端子 → 精度达标;台阶的高度差 = 下游能多信多少 +第 3 级不要标准源、也不要往端子加压:一台信号发生器 + 已有的注入点就能做(判据见 docs/任务规划/数据可信.md §4.6) + + + +1 +通路活 +已证 · 2026-10-27 + + +2 +静态引脚电压 +已证 · 2026-10-27 + + +3 +动态引脚波形 +待做 · 本文新增 + + +4 +端子二次值 +等外部资源 + + +5 +精度等级验收 +等外部资源 + +9 路都在转换?没串道?极性 +没反? +开机自检 + 帧计数仍 1000 Hz +;逐路注入只动本路 +注 1 V 直流,读数是不是 1 +V? +万用表 + 电位器三点线性:斜率 + 6544.2 vs 理论 6553.6 码/V +,差 −0.14 % +注 1 V 正弦,算出来还是不是 + 1 V? +注正弦验全周傅氏幅值 / 相位 / + 测频 / 削顶 / 谐波抑制。挡路 +:FREQ_AMP_MIN = 50 V 在引 +脚域恒不过 → 测频只能读到 IV +引脚上这 1 V 对应端子侧几 V +? +④ 子板 / 端子板变比:仓内无原 +理图,须拿图纸或万用表实测 +额定工况下整条链误差够几级? +标准源两点标定写 [adc_cal] +置 VALID → 保护自动解卡 → 送 +检 + +eng 升档门:④ 变比到手 + 电气 +8 路 VALID 全置位 → eng 才升 +1 +M4 承诺边界(§2.6):蓝底这三列都还在「ADC 引脚」这一侧,eng = 0、单位是引脚电压 V + +第 2、3 级共用同一个注入点与同一套读数出口,但证的是两件事:直流证「一条水平线的高度」,正弦证「整条时间轴的形状与节拍」——第 3 级不能由第 2 级代证 +。 + diff --git a/docs/任务规划/故障录波.md b/docs/任务规划/故障录波.md new file mode 100644 index 00000000..caea3902 --- /dev/null +++ b/docs/任务规划/故障录波.md @@ -0,0 +1,79 @@ +# 故障录波(立项 · 卡片登记簿) + +> **本文管排期与判据**:卡片表、顺序、资源占用、边界。**设计与口径的属主是 `docs/子系统设计/故障录波设计说明.md`**——为什么存原始码值、为什么先做 1 kHz、段文件格式、回放页画什么,都写在那份里,本文不重抄。 +> **本文的待办只认下面的卡片表**;设计层面的开放项去设计文档找。 + +--- + +## 1. 现状一句话 + +全仓**没有任何录波代码**:`App/` 下 grep 无实现,`Drivers/BSP/UI/ui_menu_tab.c:41` 那条「手动录波」叶子(菜单第 26 叶)**至今未绑内容页**;产品需求把「录波文件存储」「文件传输(录波)」两行标为未实现/未开始;旧口径把录波挂在「工程量口径实测闭环之后」,本波用**原始码值**解掉这个依赖(`docs/子系统设计/故障录波设计说明.md` §3.2)。 + +三条已裁定口径(用户裁定 2026-10-29,理由见`docs/子系统设计/故障录波设计说明.md` §1.2 与 §2): + +1. **采样率 1 kHz(每周波 20 点)**——复用现有 TIM3 节拍,与产品需求「80 点/周波」的偏离已在`docs/子系统设计/故障录波设计说明.md` §2 显式登记; +2. **8 路电气量全录**,含 V0.1 下必削顶的 STM32 五路,数据不筛选、用位图标记可信性; +3. **一屏只画一路、8 路可切换**——160×160 单色屏画不出多路,且跨转换器本就没有同一时刻性。 + +--- + +## 2. 卡片表 + +资源图例:**🔴 标准源**(需送检机构/0.05 级标准表)· **🟡 台架可做**(信号发生器、注入、FTP、串口)· **🟢 纯软件**(主机单测、构建、模拟器)。 + +| 卡 | 内容 | 退出判据 | 资源 | 依赖 | +|---|---|---|---|---| +| **录波数据面最小闭环** | 新增专用录波环与段格式编解码(`docs/子系统设计/故障录波设计说明.md` §5 到 §7);bench 构建定周期自抓;FTP 拉回;仓内新增一个纯标准库 PC 脚本读段、画图、重算全周傅氏 | 注入 1 V / 50 Hz 正弦 → 拉回段文件 → PC 脚本量出周波数、幅值、极性与注入一致;削顶路数与头里 `clip_mask` 一致;**PC 重算的基波有效值与板上 `meas:` 行、104 浮点 YC 逐个对表通过**;段文件 CRC 不符时脚本必须拒绝而不是画出东西 | 🟡+🟢(需信号发生器与板,不需标准源) | 无(可与固件其他道并行;独占构建锁) | +| **手动触发与保护启动接线** | 触发状态机接两路真实来路:UI「手动录波」操作页(第 26 叶绑页,形态沿用已规划好的操作页确认模式)+ `App/algo/prot_elm.c` 的 `picked` 上升沿 | 面板按一次出一段;造一次过流,段号与该时刻 `/log/start.log` 事件能对齐;冷却期内二次触发只加 `ovf` 计数、不重录;冻结窗口内 104 上送与保护判据不受影响(**这条是"丢录波样本不丢采集"的验收**) | 🟡+🟢+UI 道(菜单树与字库是整文件生成物,须单写者) | 上一张卡 | +| **现场单通道回放页** | 新增「录波回放」叶子与内容页:单通道 2 周波 40 点、阶梯线、0 线与峰值标注、单位徽标、削顶标记、空态文案(`docs/子系统设计/故障录波设计说明.md` §8) | `ui-verify` 五步全过;回放页入 `ui-golden` 黄金参考;远端看图能拍到此页(只作留证,不当波形通路);**空态与 CRC 失败态绝不允许画成一条直线**;菜单叶数与字库生成物同步重跑且 `generated-fresh` 一致 | 🟢+UI 道(模拟器与黄金参考) | 上一张卡 | +| 二期(不在本波) | 4 kHz 全速与 80 点/周波、突变量启动、COMTRADE、104 文件传输、分段与参考机形态(录波 65 通道、多类型多段) | 不在本波验收面内;重开条件见 §4 | 🔴/独立立项 | — | + +**为什么录波排在信号发生器动态验证之前**(用户裁定):动态验证真正缺的不是信号源,是**样本出口**——录波段文件一旦有了,动态验证就从「比对几个派生标量」升级成「PC 独立重算 vs 板上读数」的对表,同一套台架接线两次都用得上,不必再造一条串口通路。`docs/子系统设计/故障录波设计说明.md` §11 记了因此而变的三处口径。 + +--- + +## 3. 顺序与并行度 + +```mermaid +flowchart TD + DSG[设计定稿: 设计文档 + 本卡片表] --> RING[专用录波环与段格式编解码] + RING --> PC[PC 侧读段脚本与对表] + PC --> BENCH{bench 台架对表通过?} + BENCH -- 否: 回头改环或格式 --> RING + BENCH -- 是 --> TRIG[两路真实触发来路: 手动 + 保护启动] + TRIG --> COOL{冻结窗口内采集与上送无恙?} + COOL -- 否 --> TRIG + COOL -- 是 --> PAGE[单通道回放页与叶子上线] + PAGE --> DONE[录波本波收口: 台账回填 + 文档现状化] + PAGE -. 二期另立项 .-> NEXT[4 kHz 全速 / 突变量启动 / COMTRADE / 104 文件传输]; +``` + +图内不写步骤号,卡名与 §2 卡片表逐字对应。并行度规则见 `docs/环境与部署/并行任务工作树方案.md` §7.3;**UI 道是单写者**——菜单树与中文字库都是整文件生成物,动叶子的那张卡不许与别的改 UI 的道并行。 + +--- + +## 4. 落地时的硬前置(每张卡收尾都要过) + +| 前置 | 为什么 | +|---|---| +| 跑 `scripts/ram_report.py` 与 `scripts/stack_report.py` 前后各一次,回填 `scripts/ram_baseline.py --update` | 录波环与落盘缓冲约 5.9 KB 静态增量,**大于 4 KB 触发 R6 规则**(`docs/内存管理/内存内容分配策略.md` §6 台账与 R6);CCM 只剩七千余字节 → 落位必须进 SRAM1(`docs/子系统设计/故障录波设计说明.md` §7) | +| 新 .c 双端注册 | `new-source-file`:`MDK-ARM/STM32F407-Demo.uvprojx` 与 `simulator/CMakeLists.txt` 都是显式文件列表 | +| `@build` 0 错误 0 警告 | 任何代码改动后的固定动作 | +| 主机侧单测新增三套并挂总闸 | 环、段格式、触发状态机都是纯逻辑,能被 `test-coverage` 真跑;别把该测的部分留给台架 | +| 菜单与字库生成物重跑 | 新增叶子改叶数 → 同步 `docs/UI设计/页面补齐规划.md` 与 `docs/任务规划/UI补齐.md` 的口径闭合,重跑 `tools/menu_gen/gen_menu.py` 与中文字库链,过 `generated-fresh` 与 `font-charset` | +| 台账回填与文档现状化 | `docs/任务规划/README.md` 登记簿销项时同步清理引用;`docs/子系统设计/故障录波设计说明.md` §11 的三处口径要回写 `docs/任务规划/数据可信.md` | + +**一期不做**(重开条件):4 kHz 全速要等采集节拍改造与算法层抽取结构一并评审(结构约束已写进`docs/子系统设计/故障录波设计说明.md` §2.1,二期只加不改);突变量启动跟 `docs/算法/继电保护功能复现规划.md` 的「故障启动(波形启动)」行;COMTRADE 与 104 文件传输等送检或第三方交接需求。 + +--- + +## 5. 与其它文档的关系 + +| 文档 | 关系 | +|---|---| +| `docs/子系统设计/故障录波设计说明.md` | **设计与口径属主**;本文只登记卡片与判据 | +| `docs/任务规划/数据可信.md` | 上游信度口径;动态波形验证卡因录波而改排在其后,读数出口升级见`docs/子系统设计/故障录波设计说明.md` §11 | +| `docs/任务规划/登记簿.md` | 原「故障录波与 104 文件传输」暂缓行已随本波立项销项(104 文件传输仍留二期) | +| `docs/任务规划/UI补齐.md`、`docs/UI设计/页面补齐规划.md` | 「手动录波」叶子旧状态是「不做(录波模块不存在)」,重开条件正是本波;上线后两处的叶数闭合要一起改 | +| `docs/问题记录/采集与数据处理短板评测.md` | 记录链缺口的登记处;本波落地后该行改指设计文档 | +| `docs/产品需求/产品需求设计文档.md` | 「录波采样率」行是本波偏离的对照基准;「立项未排」行与「文件传输(录波)」行按本波状态更新 | diff --git a/docs/任务规划/数据可信.md b/docs/任务规划/数据可信.md index 02ac0603..384f7959 100644 --- a/docs/任务规划/数据可信.md +++ b/docs/任务规划/数据可信.md @@ -1,27 +1,65 @@ # 数据可信(等待台架与外部资源) -> **本文同时是设计约束的归属文件,不是纯任务卡。** 排序只看 -> `docs/任务规划/README.md` §2;后续事项统一索引在 `登记簿.md` §7。 -> ⚠ §2.1 / §2.3 / §2.6 / §4.1~§4.5 / §7 / §8 / §9 / §10 都被**代码头注释**引用为口径出处 -> (`adc_cal.*` / `meas.*` / `cfg_file.*` / `prot_elm` / `protect_task.c` / `ui_data*`)—— -> 它们是**现行规范、不是"已完成记录"**,不要当历史删掉;已完成的事项按"完成即销项"只删 §5 任务卡表里的行。 -> ⚠ **保护现处于未标定闭锁态**:判据在 `App/algo/adc_cal.c` 的 `adc_cal_prot_blocked()`——UA/UN/IA 的 -> `ADC_CAL_F_VALID` **全置位**才放行,`protect_task` 拿它的返回值停走判据(判据已从 `protect_task.c` -> 搬进 `adc_cal.c`,旧行号引用作废;`DTU_BENCH` 构建才旁路)。`VALID` 只在 `adc_cal_set()` 通过 -> 「`k` 在 ROM 系数 ×[0.5, 2.0] 带内 + `|b| ≤ ADC_CAL_B_MAX`」校验后置位(`s_kcfg.valid`,行号 -> `adc_cal.c:378` 为 2026-10-28 核对快照)。用户裁定(2026-10-25)**保持闭锁到标准源到位**, -> 不缩短解锁路径(不用万用表走捷径)。 +> **这篇是什么**:把"采集出来的数到底能不能信"一次讲完——已经证到哪一步、按构造还证不到什么、 +> 下一跳怎么跨、哪些必须等外部资源。 +> **它同时是设计约束的归属文件**:§2.1 / §2.3 / §2.6 / §4.1~§4.5 / §7 / §8 / §9 / §10 被**代码与测试头注释** +> 当口径出处引用(`adc_cal.*` / `meas.*` / `cfg_file.*` / `prot_elm` / `protect_task.c` / `ui_data*` / +> `test/host/prot_dim/*`),它们是**现行规范,不是"已完成记录"**。已完成的事项按"完成即销项"只删 §5 表里的行。 +> 排序权威只有一处:`docs/任务规划/README.md` §2;后续事项索引在 `docs/任务规划/登记簿.md` §7。 + +> **最该先知道的一条**:保护判据现在处于**未标定闭锁**态。判据 = `App/algo/adc_cal.c` 的 +> `adc_cal_prot_blocked()`——UA / UN / IA 的 `ADC_CAL_F_VALID` **全置位**才放行,`protect_task` 拿它的返回值 +> 停走判据(判据已从 `protect_task.c` 搬进 `adc_cal.c`,旧行号引用作废;只有 `DTU_BENCH` 构建旁路,**且只旁路这一处**)。 +> 用户裁定(2026-10-25):**保持闭锁到标准源到位**,不缩短解锁路径(不用万用表走捷径)。 --- -## 2. 事实基线(2026-10-23 代码级核对) +## 1. 三十秒看清全局 -> 本节所有结论都可在代码里逐条复查,行号为核对时快照。**不依赖任何文档转述**(§2.6 的实测数值出自台架记录,属主在该节)。 -> (编号不连续是故意的:§2.1 / §2.3 / §2.6 被代码头注释引用为口径出处,编号不重排。) +图由 `tools/arch_diagram/gen_data_trust_ladder_svg.py` 生成(改图改脚本重跑,勿手改 SVG)。 -### 2.1 通道与量纲 +![数据可信是五级台阶,现在踩在第 2 级](../images/数据可信台阶.svg) -| 语义通道 | 转换器 | 码值格式 | LSB(@ 引脚) | 前端 | 板级系数(输入侧 = 引脚侧) | 交流可用性 | +每升一级只多答一个问题,台阶之间的高度差就是"下游能多信多少": + +| 台阶 | 多答的问题 | 状态 | +|---|---|---| +| 1 通路活 | 9 路都在转换、没串道、极性没反? | ✅ 2026-10-27 已证 | +| 2 静态引脚电压 | 注 1 V 直流,读数是不是 1 V? | ✅ 2026-10-27 已证(六项判据全 PASS,§2.6) | +| **3 动态引脚波形** | **注 1 V 正弦,算出来还是不是 1 V?** | **⬜ 一次也没做过(§2.7 → 方案见 §4.6)** | +| 4 端子二次值 | 引脚上这 1 V 对应端子侧几 V? | 🟡 等子板原理图或实物 | +| 5 精度等级验收 | 额定工况下整条链误差够几级? | 🟡 等标准源 | + +**三条结论**: + +1. **第 1、2 级已闭环**,证据与判据的属主是 `docs/硬件平台/ADC通道映射说明.md` §8.1,执行步骤属主是 + `docs/测试说明/ADC前端注入验证流程.md`——本文不重抄,只写"证到哪、为什么不能再往外推"。 +2. **第 3 级是当前唯一的空白,而且不需要任何外部资源**:一台能出 50 Hz 正弦的交流信号发生器, + 接在**已经验证过的那个注入点**上就行。它比第 4、5 级安全一个量级——只需 ±2 V 以内的正弦, + 而端子级验证要上 100 V / 220 V 二次值。**不用烧高压、不用标准源,这是把它排最前的理由**(§4.6)。 +3. **第 4、5 级在等外部**:④ 子板/端子板变比(仓库里没有原理图)与标准源两点标定, + 两者完成前 `eng` 保持 0,本波只承诺"ADC 引脚电压"。 + +**本文怎么读**: + +| 你想做什么 | 直接看 | +|---|---| +| 判断现在这些数能信到什么程度 | §1 这张图 + §2.6(静态证到哪)+ §2.7(动态没证什么) | +| 改采集 / 算法 / 品质位相关代码 | §2.1 §2.2 §2.3(口径)+ §4.1~§4.5(设计依据) | +| 上台架做正弦验证 | §4.6(接法、判据 D1~D6、读数出口、坑)+ §5 的「信号发生器动态波形验证」卡 | +| 看还差谁、按什么顺序做 | §5 任务卡 + §6 顺序图 | +| 确认某件事是**故意不做**而不是漏了 | §9;有没有踩坑风险看 §8 | + +--- + +## 2. 事实基线(2026-10-29 代码级核对;行号是核对时快照) + +### 2.1 通道与量纲(锚点:`adc_cal.*` / `meas.*` / `ui_data*`) + +**已证的**:9 路语义 ↔ 物理通道 ↔ 转换器这三者一一对应(`k_adc_map` 与 CubeMX 标签逐条对齐 + 开机自检)。 +**没证的**:这张表的系数只到**引脚**,不到端子。 + +| 语义通道 | 转换器 | 码值格式 | LSB(@ 引脚) | 前端 | ROM 默认系数(@ 引脚) | 交流可用性 | |---|---|---|---|---|---|---| | UA | TPAFE5160 | 16 位双极性 ±5 V | 152.59 µV | 1k 串 + 滤波 C(③ = 1:1,无分压) | **152.59 µV/码** | ✅ 可用 | | UN | TPAFE5160 | 同上 | 152.59 µV | 同上 | **152.59 µV/码** | ✅ 可用 | @@ -30,25 +68,46 @@ | IB / IC / IN | STM32 ADC1 | 同上 | 805.66 µV | 同上 | **805.66 µV/码** | ❌ 削顶 | | TEMP | STM32 ADC1 | 同上 | 805.66 µV | 同上 | 热敏非线性 | — 不纳入线性标定 | -- 通道数:`ADC_SEM_COUNT = 9`(8 电气 + 温度),定义见 `App/algo/adc_input.h`。 -- ⚠ **上表的"板级系数"只到 ADC 引脚,不到端子**:③ 段已定 1:1,但端子到 `AINn` 之间还有一级 - 子板/端子板变比(④ 段),而**仓库里没有任何文档给出该变比** - (`docs/硬件平台/` 只有 `.tel` 网表与分析报告,无子板原理图)。故 - "端子二次值 = 码 × 上表系数"**不成立**,详见 §2.6 与 §5"端子/子板变比确认"。 +- 通道数 `ADC_SEM_COUNT = 9`(8 电气 + 温度),枚举定义 `App/algo/adc_input.h:38-49`。 +- 唯一折算点:`App/task/adc_task.c:382` 的 `frame[s] = adc_cal_apply((adc_semantic_t)s, code)`—— + **全仓只有这一处**把码值变成工程量,现单位 = **ADC 引脚电压 V**(`eng = 0`)。 +- 端到端链路分四段(① 语义↔网名↔通道、② 引脚电压↔码值、③ `AINn` ↔ 引脚、④ 端子 ↔ `AINn`), + 属主 `docs/硬件平台/ADC通道映射说明.md` §7.1:①②③ 已定死(③ 变比 1:1),**只有 ④ 是缺口**。 -### 2.3 三个必须同步重定的下游常数 +### 2.2 量纲错配与量纲回归(锚点:`test/host/prot_dim/*`;M4 退出判据⑥) -| # | 位置 | 现值 | 语义 | 标定后必须做什么 | -|---|---|---|---|---| -| 1 | `App/algo/freq.h:55` | `FREQ_AMP_MIN 50.0f` | 现在是"50 个码值"(几乎恒成立,等于没有幅值门) | 改为二次值口径并重新论证(对 100 V PT,50 V 是合理的"无波形"门限——但结论要写清依据,不能靠巧合) | -| 2 | `App/protocol/iec104/iec104_pts.c:23` `k_yc[].deadband` | 9 点全 10.0(freq 0.5) | 注释写"工程量绝对值",实际是码值 | 按各点工程量重定(建议规则:额定值的 0.5%) | -| 3 | `App/algo/prot_set.c` `g_prot_set_meta[]` | 10 A / 120 V / 15 V … | 现在是"跟码值比"的工程单位 | 复核默认值与 `min`/`max` 范围;**标定后它们才名副其实**,此前一直是错的 | +**这一节为什么必须在**:台架此前"通过",是因为**没有一条判据检查"不该动作时不动作"**。 +"码值当工程量"的错配曾让 OC 拿 ~11000 码值去比 10 A 定值 → **恒假动作**。这类错的可怕之处不是 +"测不准",而是**每次都跳**,而且三处消费者(判据、104、UI)一起错、一处都不报错。 -### 2.6 验证现状:已知电压注入已于 2026-10-27 台架闭环 +固化它的回归 = `test/host/prot_dim/`:从 **ADC 码值**出发、**显式写出码值↔二次值换算**,走真实的 +折算 → 采样环 → 全周傅氏 → 元件判据链;任何一环把量纲弄丢(例如折算点被旁路),断言立刻翻转 +(TC-PROTDIM-002 就是反错配对照,专门复现那次历史事故)。它用的是**合成系数 10 mV/码**, +不是板上 152.59 / 805.66 µV/码——**它证的是"链条别丢量纲",不证"变比数值对不对"**(后者归 §2.6 与 §4.6)。 -判据与实测记录的属主 = `docs/硬件平台/ADC通道映射说明.md` §8.1,执行步骤 = `docs/测试说明/ADC前端注入验证流程.md`。 -一块万用表 + 一个电位器 + 串口 `adc:` 心跳行就够,**不需要**示波器、信号源、标准源;注入点在串联电阻的 -端子侧 `U5` 排针(不碰 LQFP 引脚)。 +| 量纲档 | 怎么进入这个档 | 现在的处置 | +|---|---|---| +| `eng = 0`(ADC 引脚电压 V) | `VALID` 未置位——**当前产品构建恒此态** | §4.3 闭锁停走整条判据链;UI 第 7 行显示"未标定"徽标 | +| `eng = 1`(二次值 V / A) | ④ 变比到手 **且** 电气 8 路 `adc_cal_flags()` 的 VALID 全置位(`App/app/ui_data_feed.c` 的 `feed_eng()`) | 定值与工程量同域,判据才名副其实 | +| 混档(最坏情况) | 只改折算系数、不改下游常数 | §2.3 那三处必须跟着动,这正是 §2.3 存在的理由 | + +### 2.3 三个必须同步重定的下游常数(锚点:`freq.h` / `iec104_pts.*` / `prot_set.c`) + +> 这节的意义是**"动一处量纲要同步三处"**,不是待办清单——三行现在两行已落地。 + +| # | 位置 | 现值与语义 | 状态 | +|---|---|---|---| +| 1 | `App/algo/freq.h:62` | `FREQ_AMP_MIN 50.0f`——测频幅值门,已按**二次值 V 口径**重定(50 V = 0.5×Un,与 `prot_set.c` 头部量纲基准同源) | ✅ 已重定;⚠ **副作用**:`eng=0` 期间帧值 ≤5 V < 50 V → 本门**恒不过** → `freq_valid()` 恒 0(`freq.h:58-60` 自陈)。这条直接决定 §4.6 里测频腿怎么读、为什么 D4 的"过判据"是"如实报不可测" | +| 2 | `App/protocol/iec104/iec104_pts.c:69-80` | `k_yc[].deadband` 已按点重定:电压 0.5 V、电流 0.005 A、频率 0.5 Hz(真源在 `run_par`,UI 可改) | ✅ 已重定;④ 变比到手后需再复核一次量纲 | +| 3 | `App/algo/prot_set.c:25-70` | `g_prot_set_meta[]` 定值 = 二次值 A / V(基准 100 V / 1 A):OC1 10 A、OV 120 V、UV 20 V、3U0 15 V… | 🟡 口径对,但**标定前无法在板上验真**;§7 #1 定了精度目标后连同 `min`/`max` 复核 | + +### 2.4 / 2.5 —— 2026-10-25 瘦身时删除,编号不复用(防止被重新取走) + +### 2.6 静态验证:已知电压注入已于 2026-10-27 台架闭环(锚点:`adc_cal.*` / `meas.*` / `ui_data*`) + +判据与实测记录的属主 = `docs/硬件平台/ADC通道映射说明.md` §8.1,执行步骤属主 = +`docs/测试说明/ADC前端注入验证流程.md`。一块万用表 + 一个电位器 + 串口 `adc:` 心跳行就够, +**不需要**示波器、信号源、标准源;注入点在串联电阻的端子侧 `U5` 排针(不碰 LQFP 引脚)。 | 项 | 实测 | 判据 | |---|---|---| @@ -59,17 +118,54 @@ | 极性(TPAFE 三路 −1.0 V) | −6566 / −6568 / −6566,对称性 ≤0.34 % | 落在 −6554 附近且 ≤2 % | | 节奏 | Δ9998 / 10 s ≈ 1000 Hz,`idle=0`、`adc1_err=0` | 注入期间仍 1000 Hz | -六项全 PASS ⇒ ③ 段(`AINn` ↔ 转换器引脚)**变比 = 1:1**(2:1 分压会把斜率打到约 3277)、`k_adc_map[]` 逐路对应、 -TPAFE 补码符号位解释正确、§2.1 的 ROM 默认系数被实测支持。空载浮空读数(TPAFE ~11450 / STM32 ~1000)是 -**上电瞬态 + 输入悬空**,不是零点,别拿首帧当零点用。 +六项全 PASS ⇒ ③ 段(`AINn` ↔ 转换器引脚)**变比 = 1:1**(若存在 2:1 分压,斜率应约 3277)、 +`k_adc_map[]` 逐路对应、TPAFE 补码符号位解释正确、§2.1 的 ROM 默认系数被实测支持。 +空载浮空读数(TPAFE ~11450 / STM32 ~1000)是**上电瞬态 + 输入悬空**,不是零点,别拿首帧当零点用。 -**仍推不出的只有 ④ 端子 → `AINn` 的子板/端子板变比**:仓库里没有子板原理图,注入又是在 `AINn` 侧做的, -反推不出 ④。故"端子二次值 = 码 × §2.1 系数"**不成立**,见 §5「端子/子板变比确认」。 +**这一轮证到的是"一条水平线的高度"**:给定直流电压 → 码值 → 折算,斜率、零点、线性度、极性、 +通道对位、节拍全在 ±2 % 内。**直流信号在时间轴上没有形状**,所以相位、频率、波形相关性、真实削顶、谐波 +这五类**按构造**不在本节证据范围内——逐条列在 §2.7。另有两项**本轮未覆盖**: +串扰(零点步骤时其余七路浮空本身在漂)与"注入进行中"的节奏(只测了注入前后),口径同 +`docs/硬件平台/ADC通道映射说明.md` §8.1 尾注。 -> **M4 承诺边界(本节就是这条边界的出处)**:只到 **ADC 引脚电压**(`eng = 0`)。"端子二次值"要等「端子/子板 -> 变比确认」拿到变比、「两点标定与精度验收」用标准源写入 `[adc_cal]` 并置 `VALID`;`eng` 升 1 的条件是电气 8 路 -> `adc_cal_flags()` 的 VALID **全置位**(`App/app/ui_data_feed.c` 的 `feed_eng()`)。**未证的两项**:串扰、注入进行中 -> 的节奏。**本节编号与承诺边界口径勿删**——`adc_cal.*` / `meas.*` / `ui_data*` / `simulator/main.c` 的头注释都指到这里。 +**一个必须记住的区分**:`CLIPPED` 位是**来源派生**(`App/algo/adc_cal.c:291-293`:系数来自 STM32 就置位, +V0.1 恒命中 UB/UC/IB/IC/IN,位图 `0xE6`),它表示"这路不能测交流",**不等于**"这路此刻正在削顶"。真削顶的签名是 +`cal:` 行的 `rail=` 位图与 `rail: edge=…` 那行 `[ERR]`——这条路径至今只在直流饱和里起过(§4.6 D5)。 + +> **M4 承诺边界**:只到 **ADC 引脚电压**(`eng = 0`)。"端子二次值"要等 +> §5「端子/子板变比确认」拿到变比、「两点标定与精度验收」用标准源写入 `[adc_cal]` 并置 `VALID`; +> `eng` 升 1 的条件是电气 8 路 VALID **全置位**(`App/app/ui_data_feed.c` 的 `feed_eng()`)。 +> **本节编号与承诺边界口径勿删**——`adc_cal.*` / `meas.*` / `ui_data*` / `simulator/main.c` 的头注释都指到这里。 + +### 2.7 动态验证:正弦波一次也没采过 + +**一句话**:整条计算链现在只在**PC 合成数据**上验过,从没在**真实交流波形**上验过。 +不是"精度差一点",是"这一档完全空白"。 + +证据(逐条可查,全部是各套件自己的自陈,不是推测): + +| 出处 | 原话要点 | +|---|---| +| `test/host/phasor/run_unit_test.py` | 「板端**无信号源**,无法注入已知幅值/相位/频率的标准波形,因而无法验证"读数 = 设定有效值"」 | +| `test/host/meas/run_unit_test.py` | 「读数与注入源比对类判据仍归台架(实测采样类,本套件不覆盖)」 | +| `test/host/prot_dim/prot_dim_test.c` | 系数是合成覆写的 10 mV/码,非板上 152.59 / 805.66 µV/码 | +| `App/algo/adc_cal.h` | `adc_cal_two_point()`:「实测注入未做(台架待跑),本函数即"公式齐备"的交付物」 | +| `docs/测试说明/ADC前端注入验证流程.md` | 「**只验直流**:零点 / 线性 / 通道映射 / 极性。不验交流有效值——台架无信号源」 | +| `App/task/adc_task.c` 心跳块 | `adc:` 是当拍瞬时码值、`meas:` 是 200 ms 快照——**没有一行能给出波形形状** | + +按下游列一遍"现在能不能信": + +| 下游 | 现在 | 为什么 | +|---|---|---| +| 基波有效值(RMS) | 🟡 算法在 PC 上证过(幅值 ≤1 %、49/51 Hz 下 ≤1.5 %),**经真 ADC 后一次没测** | 1 V 正弦进 ADC 后 `fAmSQ` 是否 = 1 V,无实测 | +| 相位 | ❌ 从未在真信号上比过;且代码里**没有跨通道相位符号约定** | 只许比同片内通道(§9 的硬件限制) | +| 频率 | ❌ **恒不启动** | 幅值门 50 V 是二次值口径,引脚域 ≤5 V 恒不过(§2.3 #1)→ UI `Hz` 行恒 `--`、104 `0x401D` 恒 IV | +| 削顶 / 品质 | 🟡 位图逻辑对,真削顶路径没跑过 | `rail:` 那条 `[ERR]` 至今只在直流饱和里起过 | +| 谐波 C04 / C05 | ❌ 实例建了,**无消费者、无出口** | `prot_elm.c` 无引用,`meas` 只算 `harm=1` | +| 保护判据 | 🔒 闭锁中 | §4.3 | + +→ **下一跳就是 §4.6**:把一根真正的 50 Hz 正弦从**已经验证过的那个注入点**灌进去, +一次把上表里"手边设备能改"的几行改掉。 --- @@ -77,17 +173,22 @@ TPAFE 补码符号位解释正确、§2.1 的 ROM 默认系数被实测支持。 **波次目标**:把"数"从原始码值推进到**有物理含义、有品质、有死区、能上送**的工程量。 -| 里程碑 | 退出判据(全部满足才算完成) | 服务分类 | 状态(2026-10-25) | -|---|---|---|---| -| **M4 系数与量纲** | ① **至少 1 路电压通道经已知电压注入验证**,斜率与理论 LSB 偏差 ≤2 %;② `adc_cal` 折算落地,采样环里是**引脚电压 V**而非码值(`eng=0`;若变比确认已给变比则直接到二次值 `eng=1`);③ 8 通道系数/零点可落盘、上电加载失败回退 ROM 默认;④ 三处下游常数按工程量重定并注明依据;⑤ "未标定 → 保护闭锁"生效;⑥ 量纲回归用例(不该动作时不动作)通过 | Standard | 🔶 **未收口**:①②③④⑥ 已达成(① 于 2026-10-27 台架实测闭环,UA 斜率偏差 −0.14 %,见 `docs/硬件平台/ADC通道映射说明.md` §8.1),**只剩 ⑤ 的台架面待两点标定**(需标准源置 `VALID` 才能验闭锁→解锁) | +M4 的六条退出判据(逐条对齐现状,别整格打勾): + +| 退出判据 | 状态 | 证据 / 缺口 | +|---|---|---| +| ① 至少 1 路电压通道经已知电压注入验证,斜率与理论 LSB 偏差 ≤2 % | ✅ 2026-10-27 | UA 6544.2 vs 6553.6 码/V(−0.14 %),`docs/硬件平台/ADC通道映射说明.md` §8.1 | +| ② `adc_cal` 折算落地,采样环里是**引脚电压 V** 而非码值 | ✅ | `App/task/adc_task.c:382`(全仓唯一折算点) | +| ③ 8 通道系数 / 零点可落盘,加载失败回退 ROM 默认 | ✅ | `[adc_cal]` / `[adc_cal_ofs]`,见 §4.4 | +| ④ 三处下游常数按工程量重定并注明依据 | ✅(有尾巴) | §2.3;#1 在 `eng=0` 恒失效已记因于 `freq.h:58-60` | +| ⑤ "未标定 → 保护闭锁"生效 | 🟡 软件道 ✅;**"解锁"这半程要等 `VALID` 置位**才能上台架验 | `adc_cal_prot_blocked()` + `App/task/protect_task.c:245` | +| ⑥ 量纲回归用例(不该动作时不动作)通过 | ✅ | `test/host/prot_dim/`,见 §2.2 | + +**所以 M4 差的是"台架面",而且它分两半**: +**动态这半(§4.6)手边就能做、不要外部资源**;端子与精度那半要等子板与标准源(§5 后两张卡)。 **M5 遥测链 / M6 运行参数与点表**:均已达成,口径沉淀在 `App/algo/meas.h`、`App/algo/run_par.h` -头注释与 §4.5;M5 的"104 上送值与**工程量**一致"仍挂"端子/子板变比确认"与"两点标定"(注入验证只证到"引脚电压"口径, -二次值要等变比确认拿到变比)。 - -> **不进本波退出判据的两项增强**:谐波**方和根真有效值**(挂 -> `docs/算法/继电保护功能复现规划.md` 「遥测谐波轨(可选)」)与快照**4 点平均平滑**——随谐波轨立项一起做; -> 快照保持「基波有效值 + 直取相量」口径(§4.5 原文)。 +头注释与 §4.5;其中"104 上送值与**工程量**一致"仍挂在变比确认与两点标定上(注入验证只证到引脚电压口径)。 --- @@ -102,26 +203,21 @@ TPAFE 补码符号位解释正确、§2.1 的 ROM 默认系数被实测支持。 | 符号 | 含义 | 存哪 | 定标 | |---|---|---|---| -| `k` | µV/码(通道灵敏度) | cfg 段,int32 **Q16**(µV/码 × 65536) | 默认 = 板级理论系数(见 §2.1 表) | +| `k` | µV/码(通道灵敏度) | cfg 段,int32 **Q16**(µV/码 × 65536) | 默认 = 板级理论系数(§2.1 表) | | `b` | 零点偏移(码值) | cfg 段,int32 | 默认 0 | | `k_f` | V/码(运行时) | RAM,`adc_cal_init/load` 时由 `k` 派生 | 不落盘 | -**为什么存 µV 而不是直接存 V/码**:`k` 在 V/码 量级是 3×10⁻⁴,Q16 只有 20 个刻度(5 % 分辨率); -换成 µV/码 后是 305,Q16 有 2000 万个刻度。**单位选择本身就是精度决策**,不是记账方式。 - -**为什么 Q16 而不是参考项目的 Q10**:TPAFE 通道 `k = 305.18 µV/码`,Q10 只能表达 -312(+0.16 %);Q16 表达 ≈2.0×10⁷(误差 < 0.001 %)。两者都占 4 字节, -Q16 把"系数定点舍入"这一项从误差预算里直接抹掉——没有理由不选它。 -(参考项目用 2^10 是因为定点 DSP 乘法成本;本机有 FPU,运行时本来就是 float。) -int32 上限 2.15×10⁹,`k` 最大可表达 32767 µV/码,远大于实际需要。 - -**运行时不走定点**:`adc_cal_init/load` 时把 `k` 预乘成 `float k_f = k_uv × 1e-6f`, -逐点只做 `frame[s] = (float)(code + b) * k_f`。1 kHz × 9 通道 = 9k 次浮点乘/秒,可忽略。 +- **为什么存 µV 而不是 V/码**:`k` 在 V/码 量级是 3×10⁻⁴,Q16 只有 20 个刻度(5 % 分辨率); + 换成 µV/码是 305,Q16 有 2000 万个刻度。 +- **为什么 Q16 而不是参考项目的 Q10**:TPAFE 通道 Q10 只能表达 312(+0.16 %),Q16 误差 < 0.001 %; + 两者都占 4 字节,Q16 把"系数定点舍入"这一项从误差预算里直接抹掉。参考项目用 2^10 是因为定点 DSP + 乘法成本,本机有 FPU、运行时本来就是 float。int32 上限可表达 32767 µV/码,远够。 +- **运行时不走定点**:`k` 预乘成 `float k_f`,逐点只做一次浮点乘——1 kHz × 9 通道 = 9k 次/秒,可忽略。 ### 4.2 三态可用性:标定 ≠ 可测 -`k` 默认值是**板级理论系数**,此时工程量单位是"**ADC 引脚电压**",不是二次值——因为还缺 -互感器/子板变比。所以通道状态必须能区分三档,不能让消费者把引脚电压当二次值: +`k` 的默认值是**板级理论系数**,此时工程量单位是"ADC 引脚电压",不是二次值——还缺互感器/子板变比。 +所以通道状态必须让消费者区分三档,不能把引脚电压当二次值用: ```c #define ADC_CAL_F_VALID 0x01u /* 该通道 k 已按变比标定 → 工程量为二次值 */ @@ -131,48 +227,44 @@ int32 上限 2.15×10⁹,`k` 最大可表达 32767 µV/码,远大于实际 | 通道 | `VALID` | `CLIPPED` | 消费者应如何对待 | |---|---|---|---| | UA / UN / IA | 标定后置位 | 0 | 可用于保护、计量、上送 | -| UB / UC / IB / IC / IN | 永不置位(V0.2 硬件改版前) | 1 | 只作码值监视;104 恒 IV(维持 `k_yc[]` 现有 `-1` 绑定) | +| UB / UC / IB / IC / IN | 永不置位(V0.2 硬件改版前) | **1,且是来源派生**(不是实测削顶,见 §2.6 表) | 只作码值监视;点表**已绑定**,靠品质位把 104 侧驱动成 IV、值清零 | | TEMP | 0 | 0 | 不参与线性标定,页面显示 `--` | -`adc_cal_flags(sem)` 供 UI / 104 / 保护三处共用,映射到各自的品质位(104 的 `IV`/`OV`、 -UI 快照的 `qds`)。 +`adc_cal_flags(sem)` 是三处(UI / 104 / 保护)唯一的可用性查询入口;`adc_cal_apply()` **不返回**可用性, +所以"只看系数不看 flags"这种用法在接口层就被堵掉(对应 §8 的"削顶通道被当成可测"一行)。 ### 4.3 未标定即闭锁(本波唯一的行为变更) -保护判据拿"ADC 引脚电压"跟"二次值定值"比,必然误动作。所以 M4 必须让 -`protect_task` 多一个闭锁源:**UA/UN/IA 的 `VALID` 位未全置位 → 判据停走**。 +保护判据拿"ADC 引脚电压"跟"二次值定值"比必然误动作,所以 `protect_task` 多一个闭锁源: +**UA / UN / IA 的 `VALID` 未全置位 → 判据停走**(实现已搬进 `App/algo/adc_cal.c` 的 +`adc_cal_prot_blocked()`,`App/task/protect_task.c:245` 消费并打 INFO + 记 SOE)。 -- 与「自检失败闭锁」采样故障闭锁同构(`PROT_HF_LOCK_MASK` 那一套),不新增机制。 -- 现有台架 `@test` 的期望会因此变化——**这是有意的**,需要在任务卡里同步更新判据。 -- 台架调试需要旁路时,走编译期开关(仅 `DTU_BENCH` 构建),不进 cfg、不进 104。 +- 与"自检失败 / 采样故障闭锁"同构(`PROT_HF_LOCK_MASK` 那一套),**不新增机制**。 +- 现有台架 `@test` 的期望会因此变化——**这是有意的**,任务卡里同步更新判据。 +- 台架调试要旁路只能走编译期开关 `DTU_BENCH`,不进 cfg、不进 104。 + ⚠ **`DTU_BENCH` 只旁路这一处**:它**不**旁路 §2.3 #1 的测频幅值门,也不产生任何读数出口。 ### 4.4 落盘:拆两段,不碰引擎 -`CFG_FILE_MAX_KEYS = 16`,8 通道 × (k + b) = 16 正好顶格,将来加一个键就爆。故拆成两段: +`CFG_FILE_MAX_KEYS = 16`,8 通道 ×(k + b)= 16 正好顶格,将来加一个键就爆。故拆两段: | 段名 | 键 | 字节 | 回调 | |---|---|---|---| -| `[adc_cal]` | `k_ua` `k_un` `k_ia` `k_ub` `k_uc` `k_ib` `k_ic` `k_in` + `valid`(uint8 位图) | 8×4 + 1 = 33 | 不挂(RAM 副本即生效值,无需"立即应用") | +| `[adc_cal]` | `k_ua` `k_un` `k_ia` `k_ub` `k_uc` `k_ib` `k_ic` `k_in` + `valid`(uint8 位图) | 8×4 + 1 = 33 | 不挂(RAM 副本即生效值) | | `[adc_cal_ofs]` | `b_ua` … `b_in` | 8×4 = 32 | 不挂 | -段名 7 / 11 字符,键名 4 字符,均在 15 字符上限内。段数 2 → 总计 4/8,未逼近上限。 +段名 7 / 11 字符、键名 4 字符,均在 15 字符上限内;段数总计 5/8(含 `[run_par]` 9 键死区),未逼近上限。 -> **现状口径**:`CFG_VAL_INT` 已就位;运行参数段 = `[run_par]` 9 键死区,总计 5/8 段。 -> 系数唯一真源仍是 `[adc_cal]/[adc_cal_ofs]`——104 写 k/b 经 `adc_cal_set` 代理,run_par 不存 -> 第二份;落盘链走 `SYS_EVT_RUN_PAR_SAVE`(iec104Task peek 投递 → defaultTask 整档 save), -> `p_cb` 回调依旧未用,本节「不挂回调」口径不变。 - -⚠ **负零点**:引擎原本只有 `CFG_VAL_UINT`(`s_decode_uint()` 用 `strtoul` + 上界校验), -负的零点偏移会落成 `4294967291` 这种现场看不懂的数——**这就是补 `CFG_VAL_INT` 的动因** -(见 §7 已定案行)。备选方案"零点存码值 + 32768 偏置的 uint16"已否决:绕开引擎但要每个读点 -记住减偏置,属于给未来埋坑。 +- 系数唯一真源仍是这两段——104 写 k/b 经 `adc_cal_set()` 代理,`run_par` **不存第二份**; + 落盘链走 `SYS_EVT_RUN_PAR_SAVE`(iec104Task peek 投递 → defaultTask 整档 save),`p_cb` 依旧未用。 +- ⚠ **负零点**:引擎原本只有 `CFG_VAL_UINT`(`strtoul` + 上界校验),负的零点偏移会落成 + `4294967291` 这种现场看不懂的数——**这就是补 `CFG_VAL_INT` 的动因**(§7 #3)。 + 备选"零点存码值 + 32768 偏置的 uint16"已否决:绕开引擎却要每个读点记住减偏置,属于给未来埋坑。 ### 4.5 遥测快照:让 104 与 UI 共用一份上游 -现在 104 的 `s_src_yc_read()` 自己遍历相量,UI 将来也要一份。两处各取 = 两处各自决定 -"什么时候算新值""品质怎么判",迟早不一致。 - -本波引入 `App/algo/meas.c`:每 **200 ms** 从相量使用区取一次,产出 +104 的 `s_src_yc_read()` 自己遍历相量、UI 将来也要一份 → 两处各自决定"什么时候算新值""品质怎么判", +迟早打架。本波引入 `App/algo/meas.c`:每 **200 ms** 从相量使用区取一次,产出 ```c typedef struct { @@ -184,67 +276,145 @@ typedef struct { uint16_t qds[9]; /* 每点品质:IV / NT / OV / CLIPPED */ uint8_t freq_valid; float freq_hz; -} meas_snap_t; +} meas_snap_t; /* 72 B,双区共 ≈144 B;改字段 = 改协议,须同步 104 与 UI */ ``` -- **发布者 = `adc_task`**:在 `phasor_run()` / `freq_run()` 之后按 200 ms 分频发布。生产者在 - 自己的上下文发布,与 `ui_data_feed` 同理——消费者不组装、不判断新旧。 -- **跨任务读取 → 沿用 `phasor` 双区原子交换**:写侧填计算区、末尾翻转单字节活跃索引; - 读侧先读索引再拷该区。`phasor` 已经是"adcTask 写 / protectTask 读"的同一形态, - **不是新机制**,不引入锁。 -- 104 的 `s_src_yc_read()` 改为读快照(不再自己遍历相量)。 -- 「UI 扩展」的 `ui_data_feed` 也读快照 → **同一份数据,两个消费者**,这是"高内聚"的具体落点。 -- 200 ms 是**上送节拍**,不是平均窗:相量本身已经是 1 ms 刷新的有效值,再平均会引入滞后。 +- **发布者 = `adc_task`**,在 `phasor_run()` / `freq_run()` 之后按 200 ms 分频发布;生产者在自己上下文里 + 发布,消费者不组装、不判断新旧。全仓 `meas_tick()` **调用点数 = 1** 是评审判据(§8"快照双写")。 +- **跨任务读沿用 `phasor` 的双区原子交换**(写侧填非使用区、末尾翻单字节索引;读侧先取索引再整体拷), + **不是新机制**、不引入锁。 +- 104 与 UI 都读这份快照 → **同一份数据、两个消费者**;104 侧值与品质整体透传,不做本地判断。 +- **200 ms 是上送节拍,不是平均窗**:相量本身已是 1 ms 刷新的基波有效值,再平均只会引入滞后。 + ⚠ 这条在动态验证里有个可读的后果:注入非整 50 Hz 的正弦时,快照轨会呈现拍频式跳动,**属预期**(§8)。 + +### 4.6 动态波形验证:用信号发生器把整条计算链跑一遍 + +图由 `tools/arch_diagram/gen_data_trust_dc_ac_svg.py` 生成(改图改脚本重跑,勿手改 SVG)。 + +![上排直流静态标定、下排交流正弦验证:同一个注入点、同一套前端,两种证法](../images/ADC直流标定与交流验证.svg) + +#### 4.6.1 为什么这一跳既不用标准源、也不用烧高压 + +| 理由 | 依据 | +|---|---| +| 注入点已经验证过,且就在那儿 | `docs/测试说明/ADC前端注入验证流程.md` §3 的注入点清单(串联电阻端子侧 = `U5` 排针);③ 段 1:1 已拿到台架证据(§2.6) | +| **不知道 ④ 变比也能验算法** | 注入点在 ④ **之前**,发生器给的电压就是引脚电压,算法链要证的正是"引脚域里算得对不对" | +| 幅度小两个数量级 | 只需 ±2 V 以内的正弦(TPAFE 侧判据上限);端子二次值验证要上 100 V / 220 V,风险与设备都在那一级 | +| 现有静态验证的读数出口可直接复用 | 串口心跳 + 104 YC,零固件改动(§4.6.3) | + +**这一跳证的是功能性**(算得对不对、品质报得实不实、节拍稳不稳),**不是计量**(误差几级)—— +发生器不当"标准"的界线见 §4.6.5 与 §7 #9。 + +#### 4.6.2 判据 D1~D6 + +| # | 验什么 | 接法 | 读哪 | 过判据 | +|---|---|---|---|---| +| **D1** | 基波幅值计量 | UA(R31 / `AIN1`)注 1.000 Vrms、50.000 Hz 正弦 | `meas:` 的 `ua_mv`;或 104 `0x4011` | 读数 = 万用表 AC 档读数,偏差 ≤2 %(与静态同档)。⚠ 别用 `phasor(hb)` 行比幅值——那是 ×0.1 定点,注 1 V 只剩 10 格(§4.6.3) | +| **D2** | 读数是否只跟基波走 | 同幅值改方波、三角 | 同上 | 期望值 = **基波分量有效值**(方波基波 = 4/π × 峰值/√2 ≈ 0.90×峰值;三角 = 峰值/√6),**不是**真有效值;偏差 ≤2 %。顺带把"这台仪表读的是基波"这条口径钉进文档——用户拿真有效值表去对会以为差了 10 % | +| **D3** | 相位 | UA 与 UN 两路**同片**(都走 TPAFE)注同幅、同相 | `phasor(hb)` 各行 `Re=` / `Im=` | 两路 `atan2(fIm, fRe)` 之差 ≤2°;改变发生器相位 90°,差值跟着走。**只许比同片内**——跨转换器无同刻性(§9),STM32 那五路还有 1 拍乒乓滞后 | +| **D4** | 测频 | 同 D1 的 1 V/50 Hz | `freq(hb):` 与 104 `0x401D` | **预期就是"不出数"**(幅值门 50 V 在引脚域恒不过,§2.3 #1)→ 这条的判据是"如实报不可测"(`freq_valid=0`、UI `Hz` 显示 `--`、104 上送 IV),**不是**报出一个错误的 50.00。想真验 45~55 Hz 精度(PC 侧判据 0.02 Hz)须临时把 `FREQ_AMP_MIN` 降到引脚域量级(如 0.5 V)单跑一轮 → §7 #8 + §8 风险行 | +| **D5** | 削顶与品质路由(**负向用例**) | 给 UB(R52 / `AIN2`)注同幅正弦 | `cal:` 行的 `rail=` / `edge=`;`rail: edge=…` 的 `[ERR]` 行;104 `0x4012` | 负半周钉下轨 → **连续 20 拍(正好一个 50 Hz 周波)后该路贴轨位锁存**、`edge` 起沿、104 上该路 IV 且值清零。⚠ 这条会**故意造出 `[ERR]`**,而 `@test` 的 forbidden 标记含 `[ERR]` → 本轮要么人工判读、要么把这五路排在最后单独一轮(§8 第一行) | +| **D6** | 谐波轨 | 基波 + 20 % 二次 + 20 % 五次(IA 上注) | `phasor(hb) C04-IA-F2` / `C05-IA-F5` 两行(`log_phasors()` 逐实例打印,实例表加行即自动覆盖) | 谐波轨读数 = 注入分量(±2 %)。⚠ 这两根轨**目前没有任何下游消费者**(`prot_elm.c` 无引用、`meas` 只算 `harm=1`),所以 D6 只证"算得对",**不证"有人用"**——出口归 §9 的谐波轨立项 | + +**先做 D1 / D2 / D5**(纯读数、无跨通道约定),D3 / D6 次之,D4 视 §7 #8 的裁定。 + +#### 4.6.3 读数出口(零固件改动) + +| 出口 | 有什么 | 坑 | +|---|---|---| +| 串口每 10 s 心跳(`App/task/adc_task.c` 心跳块) | `adc:`(9 路码值)、`cal:`(三路 mV + `vl`/`cl`/`rail`/`edge` 位图)、`phasor(hb)`(逐实例 `Re` / `Im` / `rms`)、`freq(hb)`、`meas:`(快照轨) | MicroLIB 无 `%f`,全是定点:`phasor` ×0.1(**1 V 注入只剩 10 格**)、`freq` ×0.01 Hz、`cal:` / `meas:` ×1000(1 mV)。10 s 一行的单点瞬时值对 50 Hz 等价于随机相位抽样 | +| 104 YC(现成 PC 主站 `test/host/iec104_master.py`) | `0x4011`~`0x4018` 八个电气量 + `0x401D` 频率,**浮点无量化**,还带品质 | UB/UC/IB/IC/IN 恒 IV、值清零 → 那五路只能回读串口码值域 | +| UI 交流量页 | `Ua` / `U0` / `Ia` / `Ia2` / `Ia5` / `Hz` / 标定徽标 共 7 行 | 160×160 单色;`--` **同时**表示"没数"和"有数但不可信" → 不能当判据,只能当旁证 | +| **板上没有的(本波现状)** | 把窗里 20 / 40 个点导出来的任何出口:无命令控制台、无 dump、当前还不落文件 | 所以现在只能靠**派生标量**证;要看形状就拿示波器直接看注入点(只对照发生器,别拿它当判据)。**这条限制是临时的**:故障录波已立项,段文件就是现成的样本出口,届时判据升级为「PC 独立重算 vs 板上读数」对表(见 `docs/子系统设计/故障录波设计说明.md` §11) | + +#### 4.6.4 接法与红线(照抄既有红线,不新发明) + +- 注入点:串联电阻的**端子侧**焊盘(`U5` 排针),逐路清单见 `docs/测试说明/ADC前端注入验证流程.md` §3; + 引脚侧没有串阻保护,必须外串 **10 k** 限流。 +- 幅度按**峰值**算,别看 Vrms 数字:1.000 Vrms 正弦峰值已 1.414 V;TPAFE 侧按判据只到 **±2 V**(满量程 ±5 V), + STM32 那五路是 **0~3.3 V 单极性**——给它们注交流只能是 D5 那种"故意削顶"的用例,峰值同样不得超 3.3 V。 +- 发生器与板**必须共地**(否则量到的是两地之间的差),且不形成地环:优先单点共地、必要时发生器浮地。 +- **不许**因为"反正发生器便宜"就往端子侧上额定电压——那是 §5 第二张卡(要变比)与 §9(要标准源)的范围。 + +#### 4.6.5 这台发生器证不到什么(别让读数长得像证据) + +| 证不到 | 为什么 | 归属 | +|---|---|---| +| 端子二次值 | 注入点在 ④ 之前,算出来还是引脚域 | §5「端子/子板变比确认」 | +| 精度等级(0.5 级 / 1 级) | 发生器自己的幅值误差本身就是要被校的量,不能冒充标准源 | §5「两点标定与精度验收」+ §7 #1 | +| 跨转换器相位 / 功率 / 对称分量 | 两片转换器无共同采样触发(V0.1 硬件缺陷)+ STM32 侧 1 拍乒乓滞后。量纲链路口径见 `docs/硬件平台/ADC通道映射说明.md` §7;处置见 §9 | +| THD / 谐波方和根 | 固件里只有两根谐波轨,没有合成出口 | §9(挂谐波轨立项) | +| 温升、EMC、长时间漂移 | 单轮台架不覆盖 | 送检与型式试验 | --- ## 5. 任务卡(完成即销项删行;标识列用语义短语,不配编号) -> 资源图例:🔴 台架(COM6/ST-LINK,`bench.lock` 独占)· 🟡 外部(标准源 / 子板原理图)· 🟢 纯软件。 +> 资源图例:🔴 台架(COM6 / ST-LINK,`bench.lock` 独占)· 🟡 外部(标准源 / 子板原理图)· 🟢 纯软件。 > 状态:⬜ 未开始 · 🟦 进行中。**已闭环 = 直接删行**。 -> **新条目一律用语义标识,不新增字母+数字编号**(规则见 `docs/任务规划/README.md` §0)。 -> **顺序:端子/子板变比确认 → 两点标定**;**下一步做变比确认**(等子板实物或原理图)。 -> 台架注入验证的执行步骤在 `docs/测试说明/ADC前端注入验证流程.md`、实测结论在 `docs/硬件平台/ADC通道映射说明.md` §8.1; -> 本表只写判据。 +> 本表**只写判据**;执行步骤在 `docs/测试说明/ADC前端注入验证流程.md`,实测结论在 +> `docs/硬件平台/ADC通道映射说明.md` §8.1(动态实测结论开在其后)。 | 标识(任务名)| DoD(可验证验收判据) | 硬依赖 | 资源 | 写范围 | 状态 | |---|---|---|---|---|---| -| **端子/子板变比确认** | 给出"端子电压 → ADC 引脚电压"的比值(万用表同时量端子与引脚,或查子板原理图后实测核对),写入 `docs/硬件平台/ADC通道映射说明.md`;据此判定 `eng` 能否置 1;数据缺失则本波只交付引脚电压并登记缺口 | 已知电压注入验证(✅) | 🟡 子板实物或原理图 | `docs/硬件平台/` | ⬜ | -| **两点标定与精度验收** | 电压 4 路(UA/UB/UC/UN)+ 电流 4 路(IA/IB/IC/IN)各做两点标定(零点 + 满度,温度不纳入),写入 `[adc_cal]` 并置 `VALID`;误差 ≤ 目标精度(待定:0.5 级 / 1 级);证据归档 | 端子/子板变比确认 | 🟡 标准源 / 继保测试仪 | `docs/算法/`、`docs/测试说明/boot_log_data/` | ⬜ | +| **信号发生器动态波形验证** | 按 §4.6.2 跑完 D1~D6(D4 允许以"如实报不可测"通过),逐条记实测 / 判据 / 结论;执行步骤与判据表**新增到** `docs/测试说明/ADC前端注入验证流程.md`(直流节之后开一节"交流"),实测结论在 `docs/硬件平台/ADC通道映射说明.md` §8.1 之后新开一节;证据(串口日志 + 104 落库)归档 `docs/测试说明/boot_log_data/` | 静态注入闭环(✅ 2026-10-27)+ 一台能出 50 Hz 正弦、幅度可调到 1 Vrms 的交流信号发生器(**不需要标准源、不需要端子加压**) | 🔴 台架 + 手边发生器 | `docs/测试说明/`、`docs/硬件平台/` | ⬜ | +| **端子/子板变比确认** | 给出"端子电压 → ADC 引脚电压"的比值(万用表同时量端子与引脚,或查子板原理图后实测核对),写入 `docs/硬件平台/ADC通道映射说明.md`;据此判定 `eng` 能否置 1;数据缺失则本波只交付引脚电压并登记缺口 | 已知电压注入验证(✅);**与动态验证无依赖关系**,可并行 | 🟡 子板实物或原理图 | `docs/硬件平台/` | ⬜ | +| **两点标定与精度验收** | 电压 4 路(UA/UB/UC/UN)+ 电流 4 路(IA/IB/IC/IN)各做两点标定(零点 + 满度,温度不纳入),写入 `[adc_cal]` 并置 `VALID`;误差 ≤ 目标精度(待定:0.5 级 / 1 级 → §7 #1);证据归档 | 端子/子板变比确认;削顶五路的处置结论(D5) | 🟡 标准源 / 继保测试仪 | `docs/算法/`、`docs/测试说明/boot_log_data/` | ⬜ | -> 两张卡的分工:**变比确认与两点标定都是外部依赖**(子板 / 标准源),软件道已无待做项。M4 用板级 -> LSB 常数即可收口(`eng=0`),但"二次值"真正成立要靠变比确认拿到的变比、`VALID` 置位要靠两点标定。 +**为什么把动态验证排在第一**(不是因为它最重要,而是因为它把后面两张卡踩实): + +1. 它**不依赖任何外部资源**,而另两张卡都在等东西;等待期正好做它。 +2. 它复用刚闭环的注入点与读数出口,**边际成本几乎为零**。 +3. 它会先把 D5 那类"预期内的 `[ERR]`"暴露在台架规程里并写清判读,避免第二、三张卡跑到一半 + 才发现"自动化测试把这轮读成 FAIL"。 +4. 它顺手把"发生器能证 / 不能证"的界线钉住,两点标定开跑时判据不会串档。 --- ## 6. 并行与顺序 -``` -变比确认(子板)→ 两点标定(标准源) -两点标定(VALID 置位后保护才走判据)─→ 闭锁验证 → B10 送检 +主干一条道,两处等外部资源(自环 = 等待),动态那一步不阻塞后面的卡: + +```mermaid +flowchart TD + S([静态注入 2026-10-27 已闭环]) --> Q1{发生器在手?} + Q1 -- 否 --> W1[等待:发生器到位
不动端子、不烧高压] + W1 --> Q1 + Q1 -- 是 --> DYN[信号发生器动态波形验证
D1 幅值 · D2 波形 · D5 削顶 先做] + DYN --> WB[回写:测试说明加「交流」一节
通道映射补动态实测节] + WB --> Q2{变比到手?} + Q2 -- 否 --> W2[等待:子板实物或原理图] + W2 --> Q2 + Q2 -- 是 --> CAL[两点标定与精度验收
标准源 / 继保测试仪] + CAL --> V[电气 8 路 VALID 全置位] + V --> R[保护自动解卡
闭锁 → 解锁台架验证] + R --> E([标准源送检 + 第三方 104 主站验收]) + W1 -. 并行不阻塞 .-> ENG[eng 保持 0
本波只承诺「ADC 引脚电压」] + ENG -.-> E ``` -- **实测斜率偏差规则**:实测斜率与理论 LSB 偏差 >2 % 时**先查前端(分压/变比/焊接)再改系数**—— - 用系数凑斜率会把硬件的错固化成"标定值"。 -- **WIP**:本波已收口,**当前软件道为 0**;剩两张卡全是外部窗口,串行做即可。 - 并行度规则见 `docs/环境与部署/并行任务工作树方案.md` §7.3。 +- **实测斜率偏差规则**:实测斜率与理论 LSB 偏差 >2 % 时**先查前端(分压 / 变比 / 焊接)再改系数**—— + 用系数凑斜率会把硬件的错固化成"标定值"。这条对动态同样成立(D1 偏差 >2 % 先查接线与共地)。 +- 并行度规则见 `docs/环境与部署/并行任务工作树方案.md` §7.3。 --- ## 7. 前置决策 -> 只剩 **2 条真开放**,都在等外部资源。已定案的收敛成下面那一行——**它是口径锚点,勿删**: -> `App/cfg/cfg_file.c` 头注释引用的"§7 决策 3"就指向其中的 `CFG_VAL_INT` 项。 +> 一张表带状态列:🟡 是还没拍的(拍板前相应任务卡无法判定 DoD),✅ 是**口径锚点,勿删**—— +> **第 3 行是 `CFG_VAL_INT`,`App/cfg/cfg_file.c:31` 引用的"§7 决策 3"就指它,勿改序。** -| # | 问题 | 备选 | 建议 / 现状 | -|---|---|---|---| -| 1 | **精度目标** | 0.5 级 / 1 级 | 决定两点标定的判据。**仍未拍板**——保护判据本身只需 3~5 %,0.5 级是计量口径;**两点标定开跑前必须定**,否则"误差 ≤ 目标精度"这条 DoD 无法判定 | -| 2 | **端子 / 子板变比从哪来** | ① 有子板原理图(直接算)② 只有子板实物(万用表同时量端子与引脚)③ 都没有 | 决定 `eng` 能否置 1。②同样可行且够用;③则本波只交付"ADC 引脚电压",二次值登记为缺口。**未拍板**(取决于子板是否到手) | - -**已定案(不再讨论)**:未标定即闭锁(2026-10-25 用户裁定**保持闭锁到标准源到位**, -不缩短解锁路径)· `CFG_VAL_INT` 补引擎类型 · 遥测快照由 `adc_task` 分频发布 · -温度不纳入线性标定(`adc_cal_flags(TEMP)` 恒 0)· 首次注入在**串联电阻子板侧(`AINn` / `U5` 排针, -即 ③ 段的端子侧)**,≤2 V 直流——`U5` 排针才是可安全接触的最低一级(引脚侧要拆子板)。 +| # | 问题 / 决策 | 备选 | 现状 / 建议 | 状态 | +|---|---|---|---|---| +| 1 | **精度目标** | 0.5 级 / 1 级 | 决定两点标定的判据。**两点标定开跑前必须定**,否则"误差 ≤ 目标精度"这条 DoD 无法判定。保护判据本身只需 3~5 %,0.5 级是计量口径 | 🟡 开放 | +| 2 | **端子 / 子板变比从哪来** | ① 有子板原理图(直接算)② 只有子板实物(万用表同时量端子与引脚)③ 都没有 | 决定 `eng` 能否置 1。② 同样可行且够用;③ 则本波只交付"ADC 引脚电压",二次值登记为缺口 | 🟡 开放 | +| 3 | **`CFG_VAL_INT`(有符号零点)** | 补引擎值类型 / 零点改 uint16 + 32768 偏置 | 已定案:补值类型(`strtol` + `ERANGE` + 宽度上下界,`s_encode()` 带负号)。偏置方案否决理由见 §4.4 尾注 | ✅ 已定案 | +| 4 | 未标定即闭锁 | 保持闭锁 / 用万用表走捷径提前解锁 | 已定案:用户裁定 2026-10-25 **保持闭锁到标准源到位**,不缩短解锁路径 | ✅ 已定案 | +| 5 | 遥测快照发布权 | `adc_task` 分频发布 / 消费者各自计时组装 | 已定案:`adc_task` 唯一发布者(§4.5),调用点数 = 1 由评审守 | ✅ 已定案 | +| 6 | 温度是否纳入线性标定 | 纳入 / 不纳入 | 已定案:不纳入,`adc_cal_flags(TEMP)` 恒 0,页面 `--` | ✅ 已定案 | +| 7 | 注入层级与上限 | 引脚侧 / `U5` 排针端子侧 | 已定案:端子侧(③ 段的"端子侧"),≤2 V 直流——`U5` 排针是可安全接触的最低一级,引脚侧要拆子板 | ✅ 已定案 | +| 8 | **`eng = 0` 期间的测频幅值门怎么处置** | A 保持恒失效(现状,UI 恒 `--`)/ B 台架临时把 `FREQ_AMP_MIN` 降到引脚域量级跑一轮 D4 再还原 / C 换成"相对量"门限(如帧内最大值的一定时比例) | 建议 **A 为常态 + B 为一次性台架**,C 需要重新论证别在过渡期改。**跑之前先定**,还原由 §8 风险行盯 | 🟡 开放 | +| 9 | **动态验证的基准与发生器要求** | 万用表 AC 档 / 发生器自报刻度 / 借标准源 | 建议:以**万用表 AC 档为基准**,发生器只要求"波形干净、幅度稳定、频率能锁 50.000 Hz",**不拿它的刻度当精度**;判据档沿用静态 ±2 % | 🟡 开放 | --- @@ -252,11 +422,17 @@ typedef struct { | 风险 | 触发条件 | 缓解 | |---|---|---| +| **削顶负向用例被当成固件 bug** | 做 D5 时 STM32 那五路必然钉下轨 → `rail: edge=…` 是 `[ERR]` 行,而 `@test` 的 forbidden 标记含 `[ERR]` → 整轮被判 FAIL | 判读规则写进流程文档:"做 D5 的这一轮 `[ERR] rail:` 属预期,只人工判读、不计入 `@test` 结论",或把这五路排在最后单独一轮并事先声明。同时别忘 `cl=0xE6` 是**来源派生**(§2.6),不是本轮新削出来的 | +| **拿发生器的精度当标准用** | 用发生器自报 Vrms 当基准值去比 | 基准 = 万用表 AC 档实测(§7 #9);发生器只当"波形干净、频率可锁"的信号源,它自己的幅值误差正是要被校的量 | +| **临时改了 `FREQ_AMP_MIN` 忘了还原** | 走 §7 #8 的 B 方案 | 改动落在产品头文件里、会编进固件。单列一条收尾判据:还原后 `@build` 0 错误 0 警告 + `git diff` 里 `App/algo/freq.h` 必须为空;并在 §7 #8 那行留一句"本轮台架已还原" | +| **峰值当成有效值,顶到量程外** | 只看 Vrms 数字:1.000 Vrms 峰值已 1.414 V | 接线前按**峰值**核一遍:TPAFE 侧 ≤±2 V、STM32 侧峰值必须 <3.3 V(且负半周本就钉轨,只作 D5 用) | +| **发生器与板不共地 / 地环** | 两台设备各自接电网地 | 单点共地,必要时发生器浮地;读数漂或出现与幅度无关的偏置先查这里 | +| **非整 50 Hz 注入被当成 `meas` 抖动 bug** | 注 49.5 / 50.5 Hz | `meas` 是**瞬时基波有效值透传、200 ms 是上送节拍不是平均窗**(§4.5)→ 快照轨会有拍频式周期间跳动,属预期。测频腿要扫频时读 `freq(hb)`,别读 `meas:` | | **`cfg` 键数顶格** | 后续给 `[adc_cal]` 加键 | 已按 8+1 / 8 拆段;加键前先看 `CFG_FILE_MAX_KEYS`,必要时再拆 | -| **标定值把保护"调坏"** | 两点标定写入错误系数 | `adc_cal_set()` 校验范围(`k` 必须在板级系数 × [0.5, 2.0] 内)+ 写入后回读校验;越界拒绝并留 `[ERR]` | +| **标定值把保护"调坏"** | 两点标定写入错误系数 | `adc_cal_set()` 校验范围(`k` 必须在板级系数 ×[0.5, 2.0] 内)+ 写入后回读校验;越界拒绝并留 `[ERR]` | | **削顶通道被当成可测** | 消费者只看 `k` 不看 `flags` | `adc_cal_flags()` 是唯一查询入口,`adc_cal_apply()` 不返回可用性;单测覆盖"CLIPPED 通道 104 恒 IV" | -| **快照双写** | 104 或 UI 侧也加一个 200 ms 计时器 | 设计约定"唯一发布者 = `adc_task`";其余侧只读,评审时看 `meas_tick()` 的调用点数量 = 1 | -| **加压烧板** | 变比未确认就往端子加额定电压 | **端子级上额定值必须在变比确认之后**,且先量"端子对地通断"确认无直流通路 | +| **快照双写** | 104 或 UI 侧也加一个 200 ms 计时器 | 设计约定"唯一发布者 = `adc_task`";评审时数 `meas_tick()` 调用点 = 1 | +| **加压烧板** | 变比未确认就往端子加额定电压 | **端子级上额定值必须在变比确认之后**,且先量"端子对地通断"确认无直流通路。动态验证只在 `U5` 排针侧 ≤±2 V 做,正就是为了绕开这条风险 | --- @@ -264,22 +440,30 @@ typedef struct { | 不做 | 理由 / 归属 | |---|---| -| 录波 + 104 文件传输 | 独立立项;且录波通道必须先有工程量,排在本波之后 | -| 等周波调频(`docs/算法/继电保护功能复现规划.md` §3.1 的「等周波采样(软件测频 + 调频闭环)」) | 与标定正交;无标准源时判据不可达 | -| 功率 / 相位 / 功率因数 | **V0.1 硬件禁止**(`docs/硬件平台/ADC通道映射说明.md` §7 通道归属行:跨转换器无同一时刻性) | -| PT 断线判据 | 需要 `flags` 与遥信配合,而 V0.1 **无开入硬件** → 与下一行同归开入波次 | +| THD / 谐波方和根真有效值、快照 4 点平均 | 挂 `docs/算法/继电保护功能复现规划.md` 的「遥测谐波轨(可选)」一起做;快照保持"基波有效值 + 直取相量"口径(§4.5) | +| **谐波轨的消费出口**(C04 / C05 目前无人消费) | D6 只证"算得对";接线到判据或上送属谐波轨立项范围,本波不动 | +| 端子级额定电压、绝缘耐压、温升 | 要标准源 / 型式试验条件;本波所有注入都在 `U5` 排针侧 ±2 V 以内 | +| 录波(原始码值段文件) | **已独立立项**(2026-10-29):`docs/任务规划/故障录波.md`;本卡「动态波形验证」的样本出口将改由录波段文件承担,排序也随之调整(`docs/子系统设计/故障录波设计说明.md` §11) | +| 104 文件传输(规约侧读文件) | 仍独立待排;现场与台架取回一律走现成 FTP(21 端口),规约侧文件服务等送检或第三方交接需求再议 | +| 等周波调频 | 与标定正交、无标准源时判据不可达,见 `docs/算法/继电保护功能复现规划.md` §3.1 | +| 功率 / 相位跨相比较 / 功率因数 / 对称分量 | **V0.1 硬件禁止**:跨转换器无同一时刻性,`docs/硬件平台/ADC通道映射说明.md` §7 通道归属行 | +| PT 断线判据 | 需要 `flags` 与遥信配合,而 V0.1 **无开入硬件** → 归开入波次 | | 开入 / 开出固件 | 固件里**完全没有**这两类代码(`docs/问题记录/采集与数据处理短板评测.md` 登记),是另一个波次的量级 | -| 温度非线性折算 | 见 §7 已定案行"温度不纳入线性标定" | +| 温度非线性折算 | §7 已定案行"温度不纳入线性标定" | | 端子二次值(一次/二次变比换算) | **条件项**:变比确认拿到变比才做;拿不到则本波只到"ADC 引脚电压",`eng` 保持 0,缺口登记进 `docs/任务规划/登记簿.md` | -| UI 页面(含交流量回炉) | 归「UI 扩展」;本波只保证 `meas_snap_t` 可供其消费 | +| UI 页面(含交流量回炉) | 归「UI 补齐」;本波只保证 `meas_snap_t` 与 `adc_cal_flags()` 可被其消费 | --- ## 10. 与其它工作的关系 -| 工作 | 关系 | +| 工作 / 文档 | 关系 | |---|---| -| **送检验收** | 标准源送检的标定复核依赖本波两点标定;验收索引见 `登记簿.md` §7 | -| **UI 补齐** | 已交付的 `meas_snap_t` + `adc_cal_flags()` 供 `ui_data_rt_t` 填 `eng`:现为 0 = 引脚电压,变比确认拿到变比后才升 1 = 二次值,交流量页"标定状态"才有真值。当前 UI 任务见 `UI补齐.md`,不必等变比确认 | -| **结构债** | 无交集 | -| **双转换器无共同采样触发(V0.2 硬件改版)** | 本波的 `CLIPPED` 位就是「双转换器无共同采样触发」在软件里的可见形态;V0.2 改版后该位恒 0,页面与 104 绑定零改动 | +| **`docs/测试说明/ADC前端注入验证流程.md`** | **执行步骤的属主**。本文只写"证到哪、判据是什么、为什么这么排";怎么接表笔、读哪一行、填哪张表一律在那篇里。动态验证的**步骤**同样落那篇(新开"交流"节),本文 §4.6 只留判据 | +| **通道映射说明** | `docs/硬件平台/ADC通道映射说明.md` §7.1(①②③④ 链路分段)与 §8.1(静态六项实测)是**量纲与实测记录的属主**;本文 §2.1 / §2.6 是它的**消费者**,不另立口径,动态实测结论也回写该文件 | +| **`docs/子系统设计/故障录波设计说明.md`** | 录波段文件是本卡「动态波形验证」的**样本出口**:录波落地后判据从「比对派生标量」升级为「PC 独立重算 vs 板上读数」对表,动态验证的排序也因此让位给录波数据面(用户裁定 2026-10-29,理由与三处口径变更见该文 §11) | +| **`docs/算法/采集与算法层设计说明.md`** | 算法与判据正文(全周傅氏、测频、保护元件);本文只规定"这些算法的信度到什么程度" | +| **`docs/子系统设计/IEC104上行设计说明.md`** | 动态验证的**读数出口**(`0x4011`~`0x4018` + `0x401D`);送检验收索引在 `docs/任务规划/登记簿.md` §7 | +| **送检验收** | 标准源送检依赖本文「两点标定与精度验收」;顺序不可倒(先变比 → 再标定 → 才解卡 → 才送检,§6) | +| **UI 补齐** | 已交付的 `meas_snap_t` + `adc_cal_flags()` 供 `ui_data_rt_t` 填 `eng`:现为 0 = 引脚电压,变比确认拿到变比后才升 1 = 二次值。**UI 不必等本文任何一张卡** | +| **双转换器无共同采样触发(V0.2 硬件改版)** | 本波的 `CLIPPED` 位就是这条硬件缺陷在软件里的可见形态;V0.2 改版后该位恒 0,页面与 104 绑定零改动 | diff --git a/docs/子系统设计/故障录波设计说明.md b/docs/子系统设计/故障录波设计说明.md new file mode 100644 index 00000000..2d1917a1 --- /dev/null +++ b/docs/子系统设计/故障录波设计说明.md @@ -0,0 +1,335 @@ +# 故障录波设计说明(1 kHz · 8 路原始码值 · 现场单通道回放) + +> **模块名称**:故障录波——采样源沿用 TIM3 1 kHz 节拍,专用录波环(拟新增 `App/algo/wave_ring.c`)、触发、落盘(defaultTask 是文件系统唯一写者)、取回(FTP 21 端口)、现场回放(UI 单通道页)。 +> **文档定位**:本波的**设计属主**——口径、通道集、数据形态、触发模型、文件格式、预算与所有权边界、显示契约、二期不返工的结构约束在此定稿。**排期与卡片判据在 `docs/任务规划/故障录波.md`**,本文不写排期。 +> **状态**:P0 设计定稿(2026-10-29),**代码一行未动**。三条已裁定口径见 §1.2。 +> **相关文档**: +> `docs/子系统设计/ADC采样节拍设计说明.md`(节拍与时序账的属主;本文 §3 的数据源就是它) +> `docs/硬件平台/ADC通道映射说明.md`(网名 `AINn` 不等于板级 `ADCn`,也不等于转换器通道号;链路分段与削顶归属) +> `docs/任务规划/数据可信.md` §4.6(动态波形验证判据;录波落地后它的读数出口升级,见 §11) +> `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1(FatFs 两条实证陷阱与 8.3 短名限制) +> `docs/内存管理/内存内容分配策略.md` §6 与 R6 规则(新增静态缓冲大于 4 KB 的前置动作) +> `docs/UI设计/内容页架构.md`、`docs/UI设计/RemoDispBus协议与接收设计说明.md`(回放页与远端看图的能力边界) + +## 目录 + +- `§1` 三条已裁定口径:1 kHz、8 路全录、一屏一路 +- `§2` 与产品需求 80 点/周波口径的偏离,与二期不返工的四条结构约束 +- `§3` 录什么:通道归属、削顶类标记、为什么存原始码值 +- `§4` 触发模型与段长冻结语义 +- `§5` 四个所有者与三条并发陷阱 +- `§6` 段文件字节布局 +- `§7` 预算(R6 是硬前置) +- `§8` 现场回放显示契约 +- `§9` 观测:只加一条周期行 +- `§10` 测试与门禁,含 bench 自抓红线 +- `§11` 与数据可信任务卡的三处口径变更 +- `§12` 风险 `§13` 范围边界 `§14` 与其它文档的关系 +--- + +## 1. 这一波要解决什么 + +### 1.1 两个动机,一个数据面 + +| 动机 | 谁提的 | 要求 | +|---|---|---| +| **现场能大概看一眼波形** | 产品(用户裁定 2026-10-29) | 装置上按几下就能看「这一路现在是不是正弦、削没削顶」,不必插串口、不必带笔记本 | +| **台架能把样本拿出来对表** | `docs/任务规划/数据可信.md` §4.6 | 把板上采到的原始样本导出,在 PC 上独立重算全周傅氏,与板上派生读数逐个对——这才是「验证我们后面的各种计算」唯一硬的做法 | + +两条共用同一个数据面:**录波环 + 段文件 + 取回通路**。区别只在「谁来触发、给谁看」。分开做会变成两套导出机制,所以并成一件。 + +### 1.2 三条已裁定口径(本文其余章节都以此为前提) + +| # | 口径 | 裁定 | 代价与换来什么 | +|---|---|---|---| +| 1 | **采样率 = 1 kHz(20 点/周波)** | 复用现有 TIM3 节拍,不改采集 | 与产品需求的「80 点/周波(4 kHz)」口径存在**已登记偏离**(§2);换来的是采集主线零改动——不动 `Src/tim.c`、不动 CubeMX、不动相量与测频 | +| 2 | **8 路电气量全录** | 五路削顶也照录 | 数据不筛选,现场看见的是真实形状;代价是文件必须自带「这一路可信吗」的标记(§3.1 与 §6.2),否则削顶会被读成「系统里真有零序」 | +| 3 | **一屏只画一路,支持 8 路切换** | 160×160 单色屏画不出多路 | 不需要图例(本来也没有颜色);8 路切换用方向键在同一页内换通道,**不新增菜单叶子**(§8.2) | + +--- + +## 2. 与产品需求口径的偏离(显式登记,不许静默降级) + +产品需求 `docs/产品需求/产品需求设计文档.md` 的「录波采样率」行写着:录波 = 20 × 4 = **80 点/周波(50 Hz 下 4 kHz)**,理由也写在那一行——「保护计算用 20 点/周波,录波要 4 倍过采样才能看清波形」。本波只做 20 点/周波,是**明知故犯的偏离**,把理由与代价写在明处: + +| 项 | 内容 | +|---|---| +| 为什么先做 1 kHz | 抬到 4 kHz 的第一块骨头不在录波,而在采集主线:一拍时序账要重算(现在 TPAFE 转换约 23 µs、EXTI 读 8 通道约 0.5 µs、ADC1 第 9 点约 67 µs、读窗与下次 CONVST 相隔约 977 µs,见 `docs/子系统设计/ADC采样节拍设计说明.md` 的时序账),半区语义与「恒定小偏移」都要重推;算法层还有两处按 1 kHz 写死的假设(`App/algo/phasor.h` 的 `PHASOR_N 20U`、`App/algo/freq.h` 的 `FREQ_FS_HZ 1000.0f`)会被一起牵动 | +| 芯片支持吗 | **支持**:TPAFE5160 是 16 bit / 350 kSPS 全通道同时采样(`docs/芯片手册/TPAFE5160.md` 首行)→ 4 kHz 是纯固件与算法的工程账,不是硬件上限 | +| 1 kHz 能看到什么 | 基波形状、削顶、直流偏置、明显畸变、频率(数过零)。**看不到**:2/5 次谐波的形状细节(每周期只剩 10 / 4 个点)、暂态前沿 | +| 偏离登记 | 落码时在产品需求那一行补一句指向本文;「看清波形」这一目标在 1 kHz 下**只算部分达成**,不许拿 1 kHz 的图当满足 80 点口径的证据 | + +### 2.1 二期不返工的结构约束(本波就写进代码注释) + +将来抬到 4 kHz 时**只允许加、不允许改**的四条规则,本波先立着: + +1. **录波环按「全速原始序列」建模**:「每周波点数」(现 20)与「采样率」(现 1000)是**写进文件头的数据**,不是编译期假设——回放端与 PC 脚本一律从头里读; +2. **算法层永远看 1 kHz 视图**:抬频时在 ADC 侧加 4:1 抽取喂 `sample_ring` 与 `freq`,`PHASOR_N` 与 `FREQ_FS_HZ` 不许被改; +3. **录波环与 `sample_ring` 解耦**(各有独立缓冲与指针,见 §5.1),二期全速只扩录波环,不牵动相量输入; +4. 文件格式的版本号字段本波置 1,二期换格式时递增,PC 侧按版本分派。 + +--- + +## 3. 数据源:录什么、从哪一帧录 + +### 3.1 语义通道顺序与转换器归属(不可混为一谈的两类) + +语义枚举与归属见 `App/algo/adc_input.h`(`ADC_SEM_UA` 到 `ADC_SEM_IN`,再加 `ADC_SEM_TEMP` 与 `ADC_SEM_COUNT`): + +| 通道 | 转换器 | 位宽与量程 | 在录波里的含义 | +|---|---|---|---| +| UA / UN / IA | TPAFE5160 | 16 bit,±5 V **双极性** | **可信类**:能录出真实的双向工频波形 | +| UB / UC / IB / IC / IN | STM32 ADC1 | 12 bit,0 到 3.3 V **单极性、无中点偏置** | **形状受限类**:交流必然把负半周钉在下轨。录它是为了如实呈现 V0.1 现状,不是为了当工频量用 | +| TEMP | STM32 ADC1 | 12 bit | 可选附带单点(非线性未折算,`docs/任务规划/数据可信.md` §7 已定案) | + +两条硬件事实决定回放能做到什么,**必须进文件头,也必须进页面**: + +- **跨转换器无共同采样触发**(V0.1 硬件缺陷):8 路分属两片转换器,没有同一时刻性 → **禁止把 U 与 I 叠在同一张图上比相位**。文件头按通道写「来源转换器 + 采样时刻偏移」;回放只画单通道,这不是妥协,是这条硬件事实逼出来的形态; +- **STM32 侧另有 1 拍乒乓滞后**(`App/algo/adc_input.h` 的取数口径注释)→ 同一转换器内也不是严格同刻。 + +### 3.2 存原始码值,不存折算工程量(本波最重要的一条数据形态决策) + +| 理由 | 说明 | +|---|---| +| 现在根本没有工程量 | 两点标定未做,`adc_cal_flags()` 的 `ADC_CAL_F_VALID` 未置位 → `adc_cal_prot_blocked()` 返 1,帧值仍是引脚域电压(`docs/任务规划/数据可信.md` §4.3)。「录波必须先有工程量」这条旧前提被本条推翻 | +| 绕开测频幅值门 | `App/algo/freq.h` 的 `FREQ_AMP_MIN 50.0f` 以二次值 V 为口径,`eng=0` 期间帧值远小于它恒不过门 → `freq_valid()=0`。**录波取的是这道门之前的原始帧**,所以不受影响,动态验证才有样本可对 | +| 最省且可追溯 | int16 码值 2 B/点;标定完成后不必重录,PC 端拿头里的 `k` / `b` 就能把老文件重算成工程量 | +| 诚实 | 文件里装的是「板子看见的东西」,不是「我们希望它看见的东西」 | + +**文件头因此必须带**:每路的来源转换器、位宽、LSV 用 µV 计的定点值(TPAFE 152.59 µV、STM32 805.66 µV)、封存瞬间的 `eng` 档位、折算 `k` / `b` 快照、有效性位图与削顶位图。缺任何一项,老文件在新固件上就会被解释错。 + +--- + +## 4. 触发模型 + +### 4.1 四种触发,本波做前三种 + +| 触发 | 来源 | 本波 | +|---|---|---| +| **手动** | UI「手动录波」操作页(该叶子早已在菜单里:`ui_menu_tree.txt` 装置维护组,现未绑页) | 做 | +| **保护启动** | `App/algo/prot_elm.c` 的 `picked` 上升沿(元件启动),与 `/log/start.log` 同源,避免两处各自判定 | 做(复用事件点,不新增判据) | +| 定周期自抓 | `DTU_BENCH` 构建下每 N 秒强制一段,供信号发生器对表 | 做,**只在 bench 构建**,产品构建无此路径 | +| **突变量启动** | 需要增量化 `Δi` 类算法与定值 | 不做,归二期(`docs/算法/继电保护功能复现规划.md` 的「故障启动(波形启动)」行) | + +### 4.2 段长与冻结语义(RAM 就靠这一条省下来) + +- 一段 = **预触发 8 周波 + 后触发 8 周波 = 320 点/路**(1 kHz 下 320 ms); +- **触发即冻结环**:停止推进录波写指针,defaultTask 直接从已冻结的环落盘,落完解冻; +- **冻结期间丢的是录波样本,不是采集**——`sample_ring`、相量、保护判据照常跑(这就是 §2.1 第 3 条要求解耦的原因),冻结不拖慢保护动作,也不让 104 断数; +- 代价:一次触发的落盘窗口(约几十 ms)内再来触发只记第一次(冷却期),**不做多段排队**——那是二期「6 段 / RAM 16 个」的形态。 + +```mermaid +flowchart TD + IDLE[空闲: 录波环照常滚动写入] --> ARM{触发源有效?} + ARM -- 无 --> IDLE + ARM -- 手动 / 保护启动 / bench 定时 --> FRZ[冻结环: 停止推进写指针] + FRZ --> POST[后触发计满 8 周波] + POST --> PACK[封段: 写文件头与通道表] + PACK --> W[defaultTask 落盘到 /wave 8.3 短名] + W --> OK{写成功且字节数相符?} + OK -- 否 --> ERR[记 wave: 失败日志, 丢弃本段] + OK -- 是 --> MARK[段计数与最新段号入快照] + ERR --> REL + MARK --> REL[解冻: 恢复滚动写入] + REL --> COOL[冷却期: 期间新触发只计数不重录] + COOL -. 计满 .-> IDLE + COOL -. 期间又来触发 .-> SKIP[丢弃: 溢出计数加一] + SKIP --> COOL +``` + +图内不写步骤号;冻结 → 后触发 → 封段 → 落盘 的四段动作在 §4.2 与 §5 正文里。 +## 5. 所有权与并发边界(这块最容易做错) + +### 5.1 谁写谁读 + +| 角色 | 职责 | 不许做 | +|---|---|---| +| **adcTask**(生产者) | 每拍把 8 路原始码值(可带 TEMP)压进录波环;查触发标志;触发时冻结 | 不碰 FatFs、不碰网络 | +| **protectTask** | 在 `picked` 上升沿置一个**请求标志**(原子置位,与 `prot_elm_request_reset()` 同一模式) | 不写环、不落盘 | +| **defaultTask**(文件系统唯一写者) | 排空已封存的段 → `f_write` → 校验字节数 → 更新段账 | 不读活缓冲,只读**已封存**区 | +| **uiTask**(回放页) | 只读窗口取段数据 → 自绘(与 `App/log/soe_store.c` 读侧同构) | 不写文件 | +| **netTask / lftpd** | 把 /wave 暴露给 FTP 取回 | 不在 netTask 里读录波环 | + +### 5.2 三条已知陷阱,本波必须绕开 + +| 陷阱 | 事实出处 | 本波处置 | +|---|---|---| +| **取窗竞态** | `App/algo/sample_ring.c` 的 `sample_ring_window()` 没有临界区(读 `s_head` 后逐点拷),现在只有 adcTask 同任务调用才安全 | 录波**不复用它**:adcTask 在自己的节拍里取「本拍帧」压专用环;跨任务只传递「已封存」标志,杜绝跨任务读活缓冲 | +| **FatFs 追加写** | 该移植 `FA_OPEN_APPEND` 不含 `FA_WRITE`,且 `f_write` 簇分配失败时返回 `FR_OK` 加短写、无错误码(`docs/架构设计/STM32F4-Base存储架构说明.md` §10.1) | 打开一律 `FA_WRITE|FA_OPEN_APPEND`;**判成功必须比对写回字节数**,短写按失败处理(丢段 + 日志),绝不当成功记账 | +| **只能 8.3 短名** | 出处同一张存储架构说明的短名限制:`FF_USE_LFN=0`,不得以点开头、不得长名 | 文件名 = WAVE 加两位十进制再加 .BIN,共 100 个名字轮转覆盖;目录 /wave(与 /log 平级,FTP 根下可见) | + +![录波数据的四个所有者与跨任务边界:adcTask 只写环,defaultTask 独占落盘,uiTask 与 PC 只读](../images/故障录波数据流.svg) + +图由 `tools/arch_diagram/gen_wave_record_flow_svg.py` 生成(改图改脚本重跑,勿手改 SVG)。 + +--- + +## 6. 段文件格式 + +### 6.1 本波为什么不用 COMTRADE + +| 判据 | 结论 | +|---|---| +| 谁要用 | 本波消费者只有两个:装置自己的回放页、仓内一个 PC 脚本——**没有任何第三方要求 COMTRADE** | +| 代价 | COMTRADE 要「配置 + 数据」两份文件(扩展名 CFG 与 DAT)、ASCII 通道表、时标与采样率段;FatFs 只有 8.3 短名,两套文件名要各占一个号,段数一多就不够分;多段与时标在 Flash 余量里换不来可读性收益 | +| 归属 | 送检或第三方交接时才需要 → 二期(`docs/问题记录/采集与数据处理短板评测.md` 的记录链缺口行已登记 COMTRADE) | + +### 6.2 段文件布局(小端;文件名 WAVE 加两位十进制) + +| 偏移 | 字段 | 宽度 | 说明 | +|---|---|---|---| +| 0 | magic | 3 | 前两字节固定 WR,第三字节是版本号(本波写 1);版本号并进 magic,换格式即变(§2.1 第 4 条) | +| 3 | ver | 1 | 恒 1 | +| 4 | fs_hz | 2 | 采样率,本波恒 1000 | +| 6 | pts_per_cycle | 1 | 每周波点数,本波 20 | +| 7 | ch_count | 1 | 8(带 TEMP 则 9) | +| 8 | pts_per_ch | 2 | 320 | +| 10 | pre_pts | 2 | 160(预触发点数,回放画触发光标用) | +| 12 | trig_src | 1 | 1 是手动,2 是保护启动,3 是 bench 定时 | +| 13 | trig_elm | 1 | 触发元件序号(`prot_elm` 实例下标;手动与定时为 0) | +| 14 | t0_ms | 4 | 段首相对上电时刻;配合墙钟还原绝对时刻 | +| 18 | wall | 4 | 对时后的 epoch 秒(未对时为 0) | +| 22 | eng | 1 | 封存瞬间的折算档位:0 引脚域、1 二次值、2 一次值 | +| 23 | flags_seq | 1 | 有效性位图(引 `adc_cal_flags()` 口径) | +| 24 | clip_mask | 2 | 削顶归属(V0.1 恒 0xE6,见 `docs/任务规划/数据可信.md` §2.7) | +| 26 | frame_seq | 4 | 段首帧序号,可与串口周期行的 frames 对账 | +| 30 | 通道表 | 每路 12 | 语义号 1 + 来源转换器 1 + 位宽 1 + 有效位 1 + LSB(µV) 4 + k 2(定点)+ b 2(定点) | +| — | CRC16(头) | 2 | 与屏板链路同一张 CRC-16/MODBUS 表,不新造 | +| — | 数据区 | pts_per_ch × ch_count × 2 | **按通道分列**:一路连续 320 点,单通道回放只读那一路 | +| — | CRC16(数据区) | 2 | 长度为 0 或 CRC 不符 → 整段作废,页面必须显示「该段不可读」 | + +**为什么按通道分列而不是按帧交错**:回放页只画一路 → 只需连续读那一路;PC 重算全周傅氏也是逐路独立。交错布局两种用法都要跨步长取数,白付代价。 + +--- + +## 7. 预算(先算后写,R6 是硬前置) + +| 项 | 算式 | 结果 | +|---|---|---| +| 录波环 | int16 × 320 点 × 8 路 | **5,120 B** | +| 环索引与状态 | 头尾指针、封存标志、段计数、冷却计数 | 约 32 B | +| 落盘搬运缓冲 | 256 B × 2(分次写,不做整段驻留) | 512 B | +| 回放页读窗 | 256 B(与 `App/log/soe_store.c` 读窗同量级) | 256 B | +| **合计静态增量** | — | **约 5.9 KB** | + +- **大于 4 KB ⇒ 触发 R6**:`docs/内存管理/内存内容分配策略.md` 的 R6 要求「任何新增静态缓冲 >4 KB 必须先核 §6 台账余量,改后同步 §6 数字、.sct 头注释与 AGENTS 栈表」;加大缓冲本身属 **Ask-first**; +- **落位 = SRAM1,不进 CCM**:CCM 受管区已 87.9 %、只剩 7,904 B,SRAM1 为 62.2 %(数字出自 `docs/内存管理/RAM预算台账.md` §1 受管区表);录波环不是 DMA 可见对象,没有「CCM 不可达」的约束,但 CCM 也**没有余量**给它; +- **UI 子系统配额早已超**(同一张表的子系统行)→ 回放页的描述符与名表仍要在台账记账,别拿「反正已超」当免检; +- 前后各跑一次 `scripts/ram_report.py` 与 `scripts/stack_report.py`(defaultTask 多了落盘链、uiTask 多了读窗与自绘);`mem-budget` 钩子是总量红线,`ram_baseline.py --update` 回填趋势; +- **Flash**:段格式编解码 + 状态机 + 回放页 + 一条日志行,粗估 3 到 5 KB(Flash 基线在 25 % 量级,不是瓶颈); +- **NAND**:一段约 5.2 KB 数据加头。轮转 100 段约 0.5 MB,对 256 MB 单分区是零头——**瓶颈不是容量,是写次数**(§10 红线)。 + +## 8. 现场回放显示契约 + +### 8.1 一屏画什么 + +单通道,8 路切换(上下键切通道,左右键切段),页面元素: + +| 元素 | 内容 | 为什么这么定 | +|---|---|---| +| 顶部 | 通道名 + 段号 + 触发源缩写(M 手动 / P 保护 / T 定时) | 一眼知道「哪一路、哪一段、怎么来的」 | +| 波形区 | 约 120×90 px,画 **2 个周波(40 点)**,**阶梯连线**(每点占 3 px 宽) | 1 bpp 下平滑折线的平缓段会被阈值吃掉(`Drivers/BSP/UI/ui_deco.h` 的实测口径:斜线边缘易被吃、须加粗);正弦的过零段正是最平缓的部分。阶梯线每段非水平即垂直,恰好绕开这条,而且更贴近「这是离散采样」的事实 | +| 参考线 | 0 线(虚线)+ 峰值标注(定点 mV) | 没有 0 线就看不出直流偏置与削顶 | +| 底行 | 单位徽标:引脚域 / 二次值(取头里 `eng`) | 与 UI 交流量页第 7 行同一诚实性口径(`docs/UI设计/页面扩充详细方案.md` §3.1.1 的落地注) | +| 徽标 | 该路属「形状受限」类时右上打「削」 | 让钉下轨不被读成系统故障 | +| 空态 | 「无录波段」/「该段不可读」 | CRC 不符或零长度必须显式说;**画成一条直线等于伪造证据** | + +### 8.2 控件与菜单:两条省事且省 RAM 的裁定 + +- **不开 `LV_USE_CHART`**:它在 `Lib/lv_conf.h` 与 `simulator/lv_conf.h` 两份里都是 0,开它要过 `lv-conf-sync` 同步门禁、吃 Flash 与 RAM,而 UI 子系统配额已超;内容页框架本身是「一次性自绘、无子对象」,绘制链尾就是页面的 `draw_cb`(`docs/UI设计/内容页架构.md` 的「什么是内容页」一节),自绘线条有先例(`ui_deco`); +- **显示页要新增一条菜单叶子**(「录波回放」,放记录查询组),触发入口仍用装置维护组那条既有的「手动录波」叶子。新增叶子会改菜单树叶数与页面绑定口径:落地时必须同步 `docs/UI设计/页面补齐规划.md` 与 `docs/任务规划/UI补齐.md` 的叶数闭合,重跑 `tools/menu_gen/gen_menu.py`(`generated-fresh` 逐字节),新增汉字走中文字库链(`font-charset`)。菜单树与字库都是整文件生成物 → **UI 道单写者,同刻只允许一条道写**(`docs/任务规划/README.md` §1 的执行纪律)。 + +### 8.3 触发入口与回放页的分工 + +| 页 | 干什么 | 归哪张卡 | +|---|---|---| +| 「手动录波」操作页 | 确认即触发一段,形态沿用已规划好的操作页确认模式(`docs/UI设计/页面补齐规划.md` §二 第 26 叶) | 录波 P2 | +| 「录波回放」页 | 列段 → 选通道 → 画单通道 | 录波 P3 | + +### 8.4 现场之外,PC 侧必须同时具备 + +- **取回**:FTP 拉 /wave/WAVE 段文件(lftpd 与 config.ini 的 ftp 段鉴权现成,无需新代码); +- **PC 脚本**:仓内新增一个纯标准库脚本(与 `test/host/iec104_master.py` 同族风格)——读段、画图、重算全周傅氏,输出「板上报数 vs PC 重算」对照表。**这才是 `docs/任务规划/数据可信.md` §4.6 那套判据想要的东西**; +- **远端看图不是波形通路**:RemoDispBus 一帧 3204 B、最坏约 4 s(`Drivers/BSP/UI/ui_net_sink.h` 的阻塞口径),只适合拍一张回放页的屏做留证。 + +--- + +## 9. 观测(日志不是免费的) + +新增**一条**周期行,不铺开: + + wave: seg=NN armed=1 src=P pts=320 err=N ovf=N drop=N + +- `err` 是落盘失败或 CRC 自弃次数(**字节数不符就算失败**,见 §5.2);`ovf` 是冷却期丢弃的触发数;`drop` 是冻结窗口丢掉的录波点数; +- 只在原先静默的黑洞处补锚点,别重复已有步骤——每条 DBG 日志约 90 B Flash 加 216 B 栈(`AGENTS.md` 的日志与工具链节); +- 该行落地时同步进 `docs/测试说明/ADC前端注入验证流程.md` 的读数出口表。 + +--- + +## 10. 测试与门禁 + +| 层 | 内容 | +|---|---| +| 主机侧纯逻辑单测(新增,进 `test/host/`) | ① 环:预触发与后触发点数、冻结与解冻、冷却丢弃计数;② 段格式编解码往返,含 CRC 不符、零长度、字节数不符三条失败路径;③ 触发状态机三条来路(手动 / 保护 / bench)。**环与格式必须是纯逻辑**(不碰寄存器、不碰 RTOS)才能这样测——这是 `App/algo/` 既有分层纪律 | +| 板级 | 信号发生器注 1 V / 50 Hz → 手动一段 → FTP 拉回 → PC 脚本量出周波数、幅值、极性、削顶路数与 `clip_mask` 一致;再把 PC 重算的基波有效值与板上 `meas:` 与 104 浮点 YC 对表 | +| UI | `ui-verify` 五步;回放页入 `ui-golden` 黄金参考(**参考图一旦入库只能改代码对齐,禁止刷参考掩盖**);新叶走 `gen_menu.py` 重生成 + 字库链 | +| 构建 | `@build` 要求 0 错误 0 警告;新 .c 双端注册走 `new-source-file`(`MDK-ARM/STM32F407-Demo.uvprojx` 与 `simulator/CMakeLists.txt` 两处显式列表) | +| 台账 | R6 三处同步 + `scripts/ram_baseline.py --update` + `mem-numbers` 与 `mem-budget` 钩子 | +| 图与生成物 | 本设计文档的配图生成器受 `generated-fresh` 逐字节幂等约束;脚本与产物同一笔提交 | + +**bench 定时自抓的红线**:定周期自抓在 1 kHz / 8 路下每 2 s 一段,每 5 分钟就是一轮 NAND 写。**长时间挂着它白烧写次数**,而 dhara 的坏块记账是持久代价、普通恢复清不掉(`AGENTS.md` 测试构建节,`docs/测试说明/存储维护与容量恢复.md`)→ 定时模式必须**默认关闭 + 有时长上限 + 启动时打一次告警**,用完立刻回产品构建。 + +--- + +## 11. 与 `docs/任务规划/数据可信.md` 的口径变更(三处) + +| 旧口径 | 新口径 | 原因 | +|---|---|---| +| 「录波通道必须先有工程量,排在本波之后」 | 录波录**原始码值**,不等工程量;解释靠头里的系数快照 | §3.2:现在根本没有工程量,等下去等于永远不动 | +| 动态波形验证的读数出口 = 派生标量(`meas:` / 104 / `phasor(hb)`) | 派生标量仍是**判据**,但样本改从录波段导出 → 判据升级为「PC 独立重算 vs 板上读数」对表 | 录波段就是现成的样本出口,不必再造一条串口通路 | +| 动态波形验证「手边可做、排在最前」 | 排在录波 P1 之后(用户裁定 2026-10-29:录波先行) | 先有样本通路,验证才不留遗憾 | + +卡片表与顺序图在 `docs/任务规划/故障录波.md`;本文只定设计,不写排期。 + +--- + +## 12. 风险 + +| 风险 | 后果 | 处置 | +|---|---|---| +| 削顶五路被读成「系统有零序 / 有谐波」 | 现场误判 | 头里 `clip_mask` + 页面「削」徽标 + 空态不许画直线(§8.1) | +| 有人把 U 与 I 两段叠图比相位 | 得出**没有物理意义**的相位差 | 回放不做多路叠加;头里写明来源转换器;与 `docs/任务规划/数据可信.md` §9 的 V0.1 禁止行同源 | +| 落盘把 defaultTask 拖住 | 日志与配置引擎变慢(defaultTask 是两者唯一写者) | 分次写(256 B 双缓冲)、一次心跳最多一段、`f_write` 前后有耗时观测;不做整段驻留再写 | +| 触发风暴 + 轮转覆盖 | 现场唯一一段有效波形被后到的抖动冲掉 | 冷却期 + `ovf` 计数;「最新段号」进快照;分段优先级属二期 | +| 定时自抓忘关 | 白烧 NAND 写次数(持久代价) | 默认关 + 上限 + 告警(§10 红线) | +| 标定落地后老文件被解释错 | 同一个系数字段值变了含义漂移 | 头里存**封存瞬间**的 `eng` 与系数快照;PC 脚本按头解释,绝不按当前固件口径解释 | +| 环写成 float 或按帧交错 | RAM 翻倍 / 单通道回放跨步取数 | §6.2 与 §7 把形态钉死为 int16 分列 | +| 拿 1 kHz 的图宣称「看清波形」 | 掩盖与产品 80 点口径的偏离 | §2 偏离登记 + §2.1 结构约束写进代码注释 | + +--- + +## 13. 范围边界(本波不做) + +| 不做 | 归属 | +|---|---| +| 4 kHz 全速、每周波 80 点 | 二期;触发条件 = 采集节拍改造与算法层抽取结构一并评审(§2.1) | +| 突变量启动 | `docs/算法/继电保护功能复现规划.md` 的「故障启动(波形启动)」行 | +| COMTRADE 导出、104 文件传输 | 送检与第三方交接时才需要(产品需求「文件传输(录波)」行至今标未开始) | +| 参考机形态(录波 65 通道;3 类型 / 6 段 / RAM 16 个) | 二期对标;本波只有 8 路单段 | +| 电度与暂态量录波 | 不属本波 | +| 60 Hz 工况自适应 | `pts_per_cycle` 已留字段,但不做实现 | + +--- + +## 14. 与其它文档的关系 + +| 文档 / 工作 | 关系 | +|---|---| +| `docs/任务规划/故障录波.md` | **排期与卡片判据的属主**;本文是设计属主,两份不互相抄 | +| `docs/任务规划/数据可信.md` | 上游:静态与动态信度、削顶归属由它定义;本文 §11 记录因录波而变的三处口径 | +| `docs/子系统设计/ADC采样节拍设计说明.md` | 数据源与节拍属主;本波**不改**它,二期改节拍时必须回头改它 | +| `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 | FatFs 陷阱与 8.3 短名的属主;本文 §5.2 只是消费者 | +| `docs/UI设计/页面补齐规划.md`、`docs/任务规划/UI补齐.md` | 「手动录波」叶子旧状态是「不做(无录波模块)」;P2 与 P3 落地时两处的叶数闭合与重开条件要同步 | +| `docs/问题记录/采集与数据处理短板评测.md` | 记录链缺口(波形数据面 / COMTRADE / 104 文件服务)的登记处;P1 落地后该行改指本文 | +| `docs/产品需求/产品需求设计文档.md` | 「录波采样率」行是本文 §2 偏离的对照基准 | +| `docs/硬件平台/ADC通道映射说明.md` | 通道归属与注入点清单的属主;回放页「削」徽标的判据出处 | diff --git a/docs/测试说明/ADC前端注入验证流程.md b/docs/测试说明/ADC前端注入验证流程.md index 855ba08b..e57088ea 100644 --- a/docs/测试说明/ADC前端注入验证流程.md +++ b/docs/测试说明/ADC前端注入验证流程.md @@ -3,8 +3,11 @@ > **这篇是什么**:已知电压注入验证的**执行侧**——判据与实测记录在 `docs/硬件平台/ADC通道映射说明.md` §8.1, > 这边写怎么接表笔、读哪一行日志、填哪张表、结果往哪回写。 > -> **只验直流**:零点 / 线性 / 通道映射 / 极性。不验交流有效值——台架无信号源,且 V0.1 有五路 -> 单极性削顶(`UB/UC/IB/IC/IN`),交流精度要等标准源到位后的两点标定(`docs/任务规划/数据可信.md` §5)。 +> **本文只验直流**:零点 / 线性 / 通道映射 / 极性——它证的是"一条水平线的高度"。 +> **交流(正弦)动态验证不用等标准源**:一台信号发生器接在本文 §3 的同一批注入点上就能跑, +> 判据与接法见 `docs/任务规划/数据可信.md` §4.6;等标准源的只是**精度等级**那一步 +> (`docs/任务规划/数据可信.md` §5 的「两点标定与精度验收」)。V0.1 有五路单极性削顶 +> (`UB/UC/IB/IC/IN`),它们在交流轮次里只能作"预期削顶"的负向用例。 > > **不需要**示波器、信号源、标准源。**只需要**一块万用表 + 一个电位器 + 串口日志。 @@ -62,7 +65,9 @@ > 引脚侧没有串阻保护,注入前**必须外串 10 k 限流**。除非端子侧结果对不上、需要分离 > "转换器的错"和"前端的错",否则不要用引脚侧。 -## 4. 读数出口(固件只给这四行,没有命令控制台) +## 4. 读数出口(固件只有下面这些周期行,没有命令控制台) + +> **样本出口将变**:故障录波已立项(2026-10-29,docs/任务规划/故障录波.md)。落地后段文件会直接给出原始码值样本,动态验证的判据随之从「比对下表这些派生标量」升级为「PC 独立重算 vs 板上读数」对表,口径变更见 docs/子系统设计/故障录波设计说明.md §11。下表仍是**当前**唯一可读的东西。 | 日志行 | 频率 | 内容 | 用途 | | ----------------------------------------------------------- | ------ | ------------------------ | ------------------- | @@ -70,6 +75,11 @@ | `cal: ua_mv=.. un_mv=.. ia_mv=.. vl=.. cl=..` | 每 10 s | UA/UN/IA 折算后毫伏 + 标定/削顶位图 | 旁证:码值 → 电压是否自洽 | | `first: UA=.. … T=..` | 每次开机一次 | 9 路原始码值 | 与 `adc:` 行互校(同序同格式) | | `frames=.. idle=.. adc1_err=.. tgap=..` | 每 10 s | 采集健康计数 | 步骤 4 的节奏判据 | +| `phasor(hb) C0x-…: Re=.. Im=.. rms=..`(逐实例)、`freq(hb): f=.. valid=..` | 每 10 s | 基波相量分量与测频读数(×0.1 / ×0.01 定点) | **交流轮次的主读数**(直流轮次只作旁证) | +| `meas: seq=.. v=.. ua_mv=.. un_mv=.. ia_mv=.. f=.. fv=..` | 每 10 s | 200 ms 遥测快照轨(基波有效值 mV) | 交流轮次的幅值比对(1 mV 分辨率,比 phasor 行细) | + +> ⚠ 直流轮次用不到后两行,但**它们一直都在**。交流轮次(判据见 `docs/任务规划/数据可信.md` §4.6) +> 必须用 `meas:` 或 104 YC 比幅值,别用 `phasor(hb)` 行——那行是 ×0.1 定点,注 1 V 只剩 10 格。 `adc:` 行是 2026-10-27 新增的(`App/task/adc_task.c` 心跳块),此前只有 `cal:` 的三路, 验 8 个通道要复位二十余次。现在**全程不必复位**。 @@ -235,7 +245,6 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, 3. `docs/任务规划/数据可信.md` 的已知电压注入验证行按"完成即删"销项;判据与实测证据的属主是 `docs/硬件平台/ADC通道映射说明.md` §8.1。 - ## 6. 判据汇总 | 项 | 判据 | 2026-10-27 实测 | 结论 | 不通过意味着 | @@ -256,7 +265,6 @@ TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性, ⚠ **两项残留口径**(不影响本轮 PASS,但别当成已验证):**串扰**未单独证(见上表"映射"行); **节奏**只测了注入前后、没测注入进行中。两项若日后怀疑,各自的补做方法已写在对应小节。 - ## 7. 安全红线 1. **STM32 侧严禁超过 3.3 V**(VDDA + 0.3 V 即击穿),电位器上限就接 3.3 V 轨,不要接 5 V。 diff --git a/tools/arch_diagram/gen_data_trust_dc_ac_svg.py b/tools/arch_diagram/gen_data_trust_dc_ac_svg.py new file mode 100644 index 00000000..ab0c9e4c --- /dev/null +++ b/tools/arch_diagram/gen_data_trust_dc_ac_svg.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""生成 docs/images/ADC直流标定与交流验证.svg —— 上下两条泳道讲清"同一个针脚、两种证法": +上排 = 已做完的直流静态标定(一条水平线的高度),下排 = 本文新增的交流正弦验证 +(整条时间轴的形状与节拍),底部 = 只有正弦能证的六件事 + 安全红线(纯标准库)。 + +为什么不用 Mermaid:本图要的是"两条流水线一一对齐 + 波形示意",Mermaid 画不出 +20 点/周的正弦采样点,也无法把上下一一对齐。谁挡谁的顺序拓扑仍是 Mermaid, +见 docs/任务规划/数据可信.md §6。 + +用法(仓库根目录,任意 python3): + python tools/arch_diagram/gen_data_trust_dc_ac_svg.py +输出: + docs/images/ADC直流标定与交流验证.svg +改内容:链路环节 / 六件事 / 红线变了才重画;调坐标编辑下方常量后重跑。 +""" +import html +import math +import os +import sys +import xml.etree.ElementTree as ET + + +def emit(s): + """控制台打印按目标编码替换,绝不因 GBK 终端遇上 ²/− 之类字符而崩 + (generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。""" + enc = getattr(sys.stdout, "encoding", None) or "ascii" + sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n") + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +OUT = os.path.join(ROOT, "docs", "images", "ADC直流标定与交流验证.svg") + +# ----------------------------- 画布 ----------------------------- +W = 1060 +H = 818 +PAD = 46 +RIGHT = W - PAD +INNER = RIGHT - PAD + +FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" +TITLE_FONT = 21 +SUB_FONT = 13 +BOX_TITLE = 12.5 +BOX_DESC = 10.5 +CHIP_TITLE = 12.0 +CHIP_DESC = 10.0 +SMALL = 11.0 +TINY = 10.5 + +INK = "#0f172a" +LINE = "#334155" +MUTED = "#475569" +FAINT = "#94a3b8" +RULE = "#cbd5e1" +WHITE = "#ffffff" + +C_GREEN = "#16a34a" +BG_GREEN = "#f0fdf4" +BD_GREEN = "#bbf7d0" +C_ROSE = "#e11d48" +BG_ROSE = "#fff1f2" +BD_ROSE = "#fecdd3" +C_BLUE = "#2563eb" +BG_BLUE = "#eff6ff" +C_AMBER = "#d97706" +BG_AMBER = "#fffbeb" + +# ----------------------------- 数据(改这里)----------------------------- +TITLE = "同一个针脚,两种证法 —— 直流只证「高度」,正弦才证「形状与节拍」" +SUB1 = ("上排已做完(2026-10-27 静态注入六项判据全 PASS);下排一次也没做过:正弦波从没进过这条链," + "所以后面的相量、测频、削顶、谐波对不对,现在无人能答") +SUB2 = ("两排共用同一个注入点、同一套前端、同一份折算代码 —— 差别只在信号源是「平的」还是「会走 20 点/周的正弦」") + +LANE_A_LABEL = "上排 · 直流静态标定(已做)" +LANE_A_GLYPH = "flat" +LANE_B_LABEL = "下排 · 交流正弦验证(本文新增 · 待做)" +LANE_B_GLYPH = "sine" + +# 每条泳道 6 个环节:(标题, 说明 1, 说明 2) +BOXES_A = [ + ("直流源", "电池或台式电源", "+ 电位器分压"), + ("万用表", "同时接在注入点", "当唯一基准读数"), + ("注入点 R31", "网名 AIN1 = U5.A3", "= ADC7 引脚(1:1)"), + ("前端 + 转换器", "同一条电阻网络", "TPAFE 16b、ADC1 12b"), + ("折算", "adc_cal_apply()", "单位仍是引脚 V"), + ("三点线性拟合", "0 / 0.5 / 1.0 V", "R² = 0.9999997,斜率差 −0.14 %"), +] +BOXES_B = [ + ("信号发生器", "50 Hz 正弦", "幅度按峰值算,见底部红线"), + ("注入点 R31", "同上排同一个点", "外串 10 k、与板共地"), + ("前端 + 转换器", "同上排同一条链", "STM32 五路负半周必削顶"), + ("1 kHz 采样点", "TIM3 TRGO 同时触发", "50 Hz → 20 点/整周"), + ("全周傅氏", "phasor_run():√2/N 预乘", "直接得有效值与相位"), + ("读数出口", "meas 200 ms → 104 YC 0x4011", "幅值 + 品质 + 频率(或 UI)"), +] + +CHIPS_TITLE = "只有正弦能证的六件事(直流在构造上就证不到)" +CHIPS = [ + ("整周窗对不对", "窗长差 1 点,相位误差就跟着来;直流恒 0 相位,测不出"), + ("有效值是否依赖波形", "直流只有一个点:方波 / 三角 / 正弦算出的都是同一个数"), + ("相位", "φ = atan2(fIm, fRe) 全靠正弦才有意义"), + ("测频 freq_run", "共轭乘积取相位增量;直流相位不前进 → 恒无效"), + ("削顶路由 CLIPPED", "UB/UC/IB/IC/IN 单极性 0~3.3 V,负半周必削顶 → 该报 IV 就得报 IV"), + ("谐波实例 C04 / C05", "二次 / 五次相量已建但无消费者:发生器给含谐波信号才能验"), +] + +NOTES = [ + ("安全红线(照抄 ADC通道映射说明 §7)", C_ROSE, BG_ROSE, [ + "TPAFE 侧只到 ±2 V:正弦看峰值不只看有效值 —— 1 Vrms 的峰值已是 1.41 V,再高就顶到 ±5 V 档边缘;", + "STM32 那五路严禁超过 3.3 V(单极性,超了是硬件级风险,不是削顶那一路好笑的验证);", + "发生器与板必须共地(不然量到的是地环差);引脚侧注入一律外串 10 k;不许拿端子侧上额定电压走捷径。"]), + ("这台发生器证不到的三件事", C_AMBER, BG_AMBER, [ + "④ 端子变比:注入点在子板之前,算出来的还是引脚域 —— 变比要靠原理图或万用表实测(另一张卡);", + "精度等级验收:发生器不是标准源,它的幅值误差本身就要被校,不能拿它冒充送检依据;", + "跨转换器相位:UB/UC 与 UA 分属两片转换器、无共同采样触发(V0.1 硬件缺陷),任何相位比较都不作数。"]), +] + +FOOTNOTE = ("读数出口现在就有:104 YC(test/host/iec104_master.py,浮点无量化)+ 串口每 10 s 的 phasor(hb) / freq(hb) / meas: 行;" + "但板上没有「把窗里 20 个点导出来」的出口,正弦只能靠派生标量证,而 phasor 行是 ×0.1 定点(注 1 V 只剩 10 格)。") + +# ----------------------------- 布局 ----------------------------- +LANE_TOP_A = 108 +LANE_H = 160 +LANE_TOP_B = LANE_TOP_A + LANE_H + 18 +BOX_W = 136 +BOX_H = 88 +GAP = (INNER - 6 * BOX_W) / 5.0 +LANE_LAB_H = 26 +BOX_Y_OFF = LANE_LAB_H + 34 + +CHIPS_TOP = LANE_TOP_B + LANE_H + 20 +CHIP_W = (INNER - 5 * 12) / 6.0 +CHIP_H = 74 + +NOTE_TOP = CHIPS_TOP + CHIP_H + 18 +NOTE_W = (INNER - 16) / 2.0 +NOTE_H = 128 + +# ----------------------------- 工具 ----------------------------- +OVERFLOW = [] + + +def esc(s): + return html.escape(s, quote=True) + + +def text_w(s, fs): + """粗估像素宽:CJK / 全角约 1.0em,其余约 0.58em(宁可估大)。""" + return sum(fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) for ch in s) + + +def check_fit(s, fs, avail, where): + if text_w(s, fs) > avail: + OVERFLOW.append("%s: %.0f > %.0f [%s]" % (where, text_w(s, fs), avail, s)) + + +def txt(x, y, s, fs, fill, weight=400, anchor=None): + a = "" if anchor is None else ' text-anchor="%s"' % anchor + w = "" if weight == 400 else ' font-weight="%d"' % weight + return ('%s' + % (x, y, fs, a, w, fill, esc(s))) + + +def line(x1, y1, x2, y2, color=LINE, width=2.0, dash=None): + d = "" if dash is None else ' stroke-dasharray="%s"' % dash + return ('' + % (x1, y1, x2, y2, color, width, d)) + + +def polyline(pts, color, width=1.8, dash=None): + d = "" if dash is None else ' stroke-dasharray="%s"' % dash + p = " ".join("%.1f,%.1f" % (a, b) for a, b in pts) + return '' % ( + p, color, width, d) + + +def rect(x, y, w, h, fill, stroke=None, rx=8, sw=1.4): + s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw) + return '' % ( + x, y, w, h, rx, fill, s) + + +def arrow(x, y, color): + """向右的短箭头,画在两个环节之间。""" + return ('' + % (x, y - 4, x + 9, y, x, y + 4, color)) + + +def glyph(x, y, w, h, kind, color): + """泳道左侧的波形示意:flat = 一条水平线加三个取样点;sine = 正弦加 20 个采样点。""" + out = [rect(x, y, w, h, WHITE, color, rx=6, sw=1.2)] + mid = y + h / 2.0 + if kind == "flat": + out.append(line(x + 6, mid - 8, x + w - 6, mid - 8, color, 2.0)) + for i in range(3): + cx = x + 12 + i * (w - 24) / 2.0 + out.append('' % (cx, mid - 8, color)) + else: + pts = [] + n = 60 + for i in range(n + 1): + t = i / float(n) + px = x + 6 + t * (w - 12) + py = mid - (h / 2.0 - 8) * math.sin(t * 2.0 * math.pi) + pts.append((px, py)) + out.append(line(x + 4, mid, x + w - 4, mid, RULE, 1.0, dash="3 3")) + out.append(polyline(pts, color, 1.9)) + for i in range(20): + t = i / 20.0 + px = x + 6 + t * (w - 12) + py = mid - (h / 2.0 - 8) * math.sin(t * 2.0 * math.pi) + out.append('' % (px, py, color)) + return "\n".join(out) + + +# ----------------------------- 生成 ----------------------------- +def build(): + out = [] + add = out.append + + for s, fs, where in ((TITLE, TITLE_FONT, "title"), (SUB1, SUB_FONT, "sub1"), + (SUB2, SUB_FONT, "sub2"), (CHIPS_TITLE, BOX_TITLE, "chips title")): + check_fit(s, fs, INNER, where) + + add('' % (W, H, WHITE)) + + # 标题 + add(txt(PAD, 42, TITLE, TITLE_FONT, INK, weight=700)) + for i, s in enumerate((SUB1, SUB2)): + add(txt(PAD, 68 + i * 19, s, SUB_FONT, MUTED)) + + # 两条泳道 + for lane_i, (top, label, boxes, color, bg, bd, glyph_kind) in enumerate(( + (LANE_TOP_A, LANE_A_LABEL, BOXES_A, C_GREEN, BG_GREEN, BD_GREEN, LANE_A_GLYPH), + (LANE_TOP_B, LANE_B_LABEL, BOXES_B, C_ROSE, BG_ROSE, BD_ROSE, LANE_B_GLYPH))): + add(rect(PAD - 12, top, INNER + 24, LANE_H, bg, bd, rx=10, sw=1.2)) + add(txt(PAD, top + 20, label, BOX_TITLE, color, weight=700)) + add(glyph(RIGHT - 92, top + 8, 88, 34, glyph_kind, color)) + for i, (bt, d1, d2) in enumerate(boxes): + bx = PAD + i * (BOX_W + GAP) + by = top + BOX_Y_OFF + add(rect(bx, by, BOX_W, BOX_H, WHITE, color, rx=8, sw=1.6)) + check_fit(bt, BOX_TITLE, BOX_W - 14, "lane%d box%d title" % (lane_i, i)) + add(txt(bx + 7, by + 18, bt, BOX_TITLE, INK, weight=700)) + yy = by + 36 + for j, d in enumerate((d1, d2)): + for ln in wrap(d, BOX_DESC, BOX_W - 14, 2, "lane%d box%d desc%d" % (lane_i, i, j)): + add(txt(bx + 7, yy, ln, BOX_DESC, MUTED)) + yy += 14 + if i < 5: + add(arrow(bx + BOX_W + GAP / 2.0 - 5, by + BOX_H / 2.0, LINE)) + add(line(bx + BOX_W + 2, by + BOX_H / 2.0, bx + BOX_W + GAP / 2.0 - 3, + by + BOX_H / 2.0, LINE, 1.6)) + + # 六件事 + add(txt(PAD, CHIPS_TOP - 10, CHIPS_TITLE, BOX_TITLE, C_BLUE, weight=700)) + for i, (t, d) in enumerate(CHIPS): + cx = PAD + i * (CHIP_W + 12) + add(rect(cx, CHIPS_TOP, CHIP_W, CHIP_H, BG_BLUE, None, rx=8)) + add(line(cx, CHIPS_TOP, cx, CHIPS_TOP + CHIP_H, C_BLUE, 3.0)) + check_fit(t, CHIP_TITLE, CHIP_W - 16, "chip%d title" % i) + add(txt(cx + 10, CHIPS_TOP + 20, t, CHIP_TITLE, INK, weight=700)) + for j, ln in enumerate(wrap(d, CHIP_DESC, CHIP_W - 18, 4, "chip%d desc" % i)): + add(txt(cx + 10, CHIPS_TOP + 38 + j * 14, ln, CHIP_DESC, MUTED)) + + # 两个说明块 + for i, (title, color, bg, rows) in enumerate(NOTES): + nx = PAD + i * (NOTE_W + 16) + add(rect(nx, NOTE_TOP, NOTE_W, NOTE_H, bg, color, rx=10, sw=1.6)) + check_fit(title, BOX_TITLE, NOTE_W - 20, "note%d title" % i) + add(txt(nx + 14, NOTE_TOP + 24, title, BOX_TITLE, color, weight=700)) + y = NOTE_TOP + 46 + for r in rows: + for j, ln in enumerate(wrap(r, SMALL, NOTE_W - 28, 2, "note%d row" % i)): + add(txt(nx + 14, y, ln, SMALL, INK if j == 0 else MUTED)) + y += 15 + y += 4 + + # 脚注 + fy = NOTE_TOP + NOTE_H + 26 + add(line(PAD, fy - 12, RIGHT, fy - 12, RULE, 1.0)) + for i, ln in enumerate(wrap(FOOTNOTE, SUB_FONT, INNER, 2, "foot")): + add(txt(PAD, fy + 10 + i * 19, ln, SUB_FONT, MUTED)) + + svg = ('\n%s\n\n' + % (W, H, W, H, FONT, "\n".join(out))) + + d = os.path.dirname(OUT) + if not os.path.isdir(d): + os.makedirs(d) + with open(OUT, "w", encoding="utf-8", newline="\n") as fp: + fp.write(svg) + ET.fromstring(svg) + emit("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, H, OUT)) + if OVERFLOW: + for o in OVERFLOW: + emit("OVERFLOW WARN " + o) + emit("!! %d overflow warnings" % len(OVERFLOW)) + + +def wrap(s, fs, avail, max_lines, where=""): + """按估算宽度折行(CJK 逐字可断);超出 max_lines 用省略号收尾。""" + if text_w(s, fs) <= avail: + return [s] + lines = [] + cur = "" + for ch in s: + if text_w(cur + ch, fs) > avail: + lines.append(cur) + cur = ch + else: + cur += ch + if cur: + lines.append(cur) + if len(lines) > max_lines: + OVERFLOW.append("%s: 折行 %d > %d" % (where, len(lines), max_lines)) + lines = lines[:max_lines] + lines[max_lines - 1] = lines[max_lines - 1][:-1] + "…" + return lines + + +if __name__ == "__main__": + build() diff --git a/tools/arch_diagram/gen_data_trust_ladder_svg.py b/tools/arch_diagram/gen_data_trust_ladder_svg.py new file mode 100644 index 00000000..9ea18ada --- /dev/null +++ b/tools/arch_diagram/gen_data_trust_ladder_svg.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""生成 docs/images/数据可信台阶.svg —— 「数据可信」的五级台阶:现在踩在第 2 级, +第 3 级(动态正弦波形)用手边设备就能跨(纯标准库)。 + +为什么不用 Mermaid:本图要读的是"台阶的高度差 + 每一级踩没踩上去"——Mermaid 会把 +五级压成同一高度,"还差一级"这件事就看不出来了。谁挡谁的顺序拓扑另有一张 Mermaid +图,在 docs/任务规划/数据可信.md §6(本图刻意不重复那张的语义)。 + +用法(仓库根目录,任意 python3): + python tools/arch_diagram/gen_data_trust_ladder_svg.py +输出: + docs/images/数据可信台阶.svg +改内容:台阶事实(状态 / 判据 / 资源)变了才重画;调坐标 / 文案编辑下方常量后重跑。 +""" +import html +import os +import sys +import xml.etree.ElementTree as ET + + +def emit(s): + """控制台打印按目标编码替换,绝不因 GBK 终端遇上 ²/− 之类字符而崩 + (generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。""" + enc = getattr(sys.stdout, "encoding", None) or "ascii" + sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n") + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +OUT = os.path.join(ROOT, "docs", "images", "数据可信台阶.svg") + +# ----------------------------- 画布与尺寸 ----------------------------- +W = 1060 +H = 712 +PAD = 46 +RIGHT = W - PAD + +FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" + +TITLE_FONT = 21 +SUB_FONT = 13 +HEAD_FONT = 14.5 +Q_FONT = 12.5 +HOW_FONT = 11.5 +TINY_FONT = 11 + +INK = "#0f172a" +LINE = "#334155" +MUTED = "#64748b" +FAINT = "#94a3b8" +RULE = "#cbd5e1" +WHITE = "#ffffff" + +C_GREEN = "#16a34a" +BG_GREEN = "#f0fdf4" +C_AMBER = "#d97706" +BG_AMBER = "#fffbeb" +C_ROSE = "#e11d48" +BG_ROSE = "#fff1f2" +C_BLUE = "#2563eb" +BG_BLUE = "#eff6ff" + +# ----------------------------- 数据(改这里) ----------------------------- +TITLE = "「数据可信」是五级台阶 —— 现在踩在第 2 级" +SUB1 = ("每升一级只多答一个问题:通路活 → 静态读数对 → 动态波形对 → 换算到端子 → 精度达标;" + "台阶的高度差 = 下游能多信多少") +SUB2 = ("第 3 级不要标准源、也不要往端子加压:一台信号发生器 + 已有的注入点就能做" + "(判据见 docs/任务规划/数据可信.md §4.6)") + +# 每列:(编号, 台阶名, 状态, 主色, 底色, 这一级多答的问题, 用什么证 / 什么挡路) +STEPS = [ + ("1", "通路活", "已证 · 2026-10-27", C_GREEN, BG_GREEN, + "9 路都在转换?没串道?极性没反?", + "开机自检 + 帧计数仍 1000 Hz;逐路注入只动本路"), + ("2", "静态引脚电压", "已证 · 2026-10-27", C_GREEN, BG_GREEN, + "注 1 V 直流,读数是不是 1 V?", + "万用表 + 电位器三点线性:斜率 6544.2 vs 理论 6553.6 码/V,差 −0.14 %"), + ("3", "动态引脚波形", "待做 · 本文新增", C_ROSE, BG_ROSE, + "注 1 V 正弦,算出来还是不是 1 V?", + "注正弦验全周傅氏幅值 / 相位 / 测频 / 削顶 / 谐波抑制。" + "挡路:FREQ_AMP_MIN = 50 V 在引脚域恒不过 → 测频只能读到 IV"), + ("4", "端子二次值", "等外部资源", C_AMBER, BG_AMBER, + "引脚上这 1 V 对应端子侧几 V?", + "④ 子板 / 端子板变比:仓内无原理图,须拿图纸或万用表实测"), + ("5", "精度等级验收", "等外部资源", C_AMBER, BG_AMBER, + "额定工况下整条链误差够几级?", + "标准源两点标定写 [adc_cal] 置 VALID → 保护自动解卡 → 送检"), +] + +BAND_LABEL_1 = "M4 承诺边界(§2.6):蓝底这三列都还在「ADC 引脚」这一侧,eng = 0、单位是引脚电压 V" +GATE_LABEL = "eng 升档门:④ 变比到手 + 电气 8 路 VALID 全置位 → eng 才升 1" +FOOTNOTE = ("第 2、3 级共用同一个注入点与同一套读数出口,但证的是两件事:" + "直流证「一条水平线的高度」,正弦证「整条时间轴的形状与节拍」——第 3 级不能由第 2 级代证。") + +# ----------------------------- 布局 ----------------------------- +STEP_W = 196 +STEP_GAP = 6 +X0 = PAD +FIRST_H = 96 # 第 1 级台阶高度 +RISE = 64 # 每高一级加这么多 +BASE_Y = 466 # 地面线 +GATE_X = X0 + 3 * (STEP_W + STEP_GAP) - STEP_GAP - 3 # 第 3/4 列之间 +BAND_TOP = 106 +BAND_BOTTOM = BASE_Y + 172 + + +# ----------------------------- 工具函数 ----------------------------- +def esc(s): + return html.escape(s, quote=True) + + +def text_w(s, fs): + """粗略估算文本像素宽:CJK/全角 ≈ 1.0em,其余 ≈ 0.58em(宁可偏大)。""" + return sum(fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) for ch in s) + + +OVERFLOW = [] + + +def check_fit(s, fs, avail, where): + if text_w(s, fs) > avail: + OVERFLOW.append("%s: %.0f > %.0f [%s]" % (where, text_w(s, fs), avail, s)) + + +def txt(x, y, s, fs, fill, weight=400, anchor=None): + a = "" if anchor is None else ' text-anchor="%s"' % anchor + w = "" if weight == 400 else ' font-weight="%d"' % weight + return ('%s' + % (x, y, fs, a, w, fill, esc(s))) + + +def line(x1, y1, x2, y2, color=LINE, width=2.0, dash=None): + d = "" if dash is None else ' stroke-dasharray="%s"' % dash + return ('' + % (x1, y1, x2, y2, color, width, d)) + + +def rect(x, y, w, h, fill, stroke=None, rx=8, sw=1.6): + s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw) + return '' % ( + x, y, w, h, rx, fill, s) + + +def wrap(s, fs, avail, max_lines, where=""): + """按估算宽度折行(CJK 逐字可断)。超出 max_lines 用省略号收尾—— + 宁可提示文案太长,也不让字溢出盒子。""" + if text_w(s, fs) <= avail: + return [s] + lines = [] + cur = "" + for ch in s: + if text_w(cur + ch, fs) > avail: + lines.append(cur) + cur = ch + else: + cur += ch + if cur: + lines.append(cur) + if len(lines) > max_lines: + OVERFLOW.append("%s: 折行 %d > %d" % (where, len(lines), max_lines)) + lines = lines[:max_lines] + lines[max_lines - 1] = lines[max_lines - 1][:-1] + "…" + return lines + + +# ----------------------------- 生成 ----------------------------- +def build(): + out = [] + add = out.append + + avail = RIGHT - PAD + for s, fs, where in ((TITLE, TITLE_FONT, "title"), (SUB1, SUB_FONT, "sub1"), + (SUB2, SUB_FONT, "sub2")): + check_fit(s, fs, avail, where) + if OVERFLOW: + for o in OVERFLOW: + emit("OVERFLOW WARN " + o) + + add('' % (W, H, WHITE)) + + # ---- 标题区 ---- + add(txt(PAD, 42, TITLE, TITLE_FONT, INK, weight=700)) + add(txt(PAD, 68, SUB1, SUB_FONT, MUTED)) + add(txt(PAD, 88, SUB2, SUB_FONT, MUTED)) + + # ---- 「引脚域」底衬(先画,压在台阶下面)---- + add(rect(PAD - 12, BAND_TOP, GATE_X - PAD + 3, BAND_BOTTOM - BAND_TOP, BG_BLUE, None, rx=12)) + + # ---- 台阶 ---- + inner_w = STEP_W - 20 + for i, (no, name, status, c, bg, q, how) in enumerate(STEPS): + x = X0 + i * (STEP_W + STEP_GAP) + h = FIRST_H + i * RISE + top = BASE_Y - h + add(rect(x, top, STEP_W, h, bg, c, rx=8, sw=2.0)) + add('' % (x + 24, top + 26, c)) + add(txt(x + 24, top + 31, no, 13, WHITE, weight=700, anchor="middle")) + check_fit(name, HEAD_FONT, inner_w - 30, "step %s name" % no) + add(txt(x + 44, top + 32, name, HEAD_FONT, INK, weight=700)) + add(txt(x + 11, top + 58, status, TINY_FONT, c, weight=700)) + + # ---- 地面线 ---- + add(line(PAD, BASE_Y, RIGHT, BASE_Y, LINE, 2.2)) + + # ---- 地面下的明细(问题 + 用什么证),逐列一行 ---- + for i, (no, name, status, c, bg, q, how) in enumerate(STEPS): + x = X0 + i * (STEP_W + STEP_GAP) + y = BASE_Y + 24 + for ln in wrap(q, Q_FONT, inner_w, 2, "step %s q" % no): + add(txt(x + 10, y, ln, Q_FONT, INK, weight=700)) + y += 17 + y += 3 + for ln in wrap(how, HOW_FONT, inner_w, 5, "step %s how" % no): + add(txt(x + 10, y, ln, HOW_FONT, MUTED)) + y += 15 + + # ---- eng 升档门 ---- + add(line(GATE_X, BAND_TOP - 6, GATE_X, BAND_BOTTOM, C_ROSE, 2.0, dash="7 5")) + gy = BAND_TOP + 12 + for ln in wrap(GATE_LABEL, TINY_FONT, STEP_W - 16, 4, "gate label"): + add(txt(GATE_X + 8, gy, ln, TINY_FONT, C_ROSE, weight=700)) + gy += 15 + + # ---- 蓝底标签(在台阶上方的空白区)---- + bl = wrap(BAND_LABEL_1, HOW_FONT, GATE_X - PAD - 24, 2, "band label") + for i, ln in enumerate(bl): + add(txt(PAD, BAND_TOP + 26 + i * 17, ln, HOW_FONT, C_BLUE, weight=700)) + + # ---- 底部说明 ---- + fy = BAND_BOTTOM + 22 + add(line(PAD, fy, RIGHT, fy, RULE, 1.0)) + for i, ln in enumerate(wrap(FOOTNOTE, SUB_FONT, avail, 2, "foot wrap")): + add(txt(PAD, fy + 24 + i * 19, ln, SUB_FONT, MUTED)) + + svg = ('\n%s\n\n' + % (W, H, W, H, FONT, "\n".join(out))) + + d = os.path.dirname(OUT) + if not os.path.isdir(d): + os.makedirs(d) + with open(OUT, "w", encoding="utf-8", newline="\n") as fp: + fp.write(svg) + ET.fromstring(svg) + emit("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, H, OUT)) + if OVERFLOW: + emit("!! %d overflow warnings" % len(OVERFLOW)) + + +if __name__ == "__main__": + build() diff --git a/tools/arch_diagram/gen_wave_record_flow_svg.py b/tools/arch_diagram/gen_wave_record_flow_svg.py new file mode 100644 index 00000000..d2e1891b --- /dev/null +++ b/tools/arch_diagram/gen_wave_record_flow_svg.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""生成 docs/images/故障录波数据流.svg —— 录波段数据的四个所有者与四条「不许」。 + +为什么不用 Mermaid:本图要读的是**跨任务边界的所有权**——谁独占写、谁只能读、 +标志怎么过界,并把「不许」画成红线贴在对应位置。Mermaid 的拓扑图表达不了「这块 +内存归谁」;任务先后与阻塞关系另有一张 Mermaid 图,在 docs/任务规划/故障录波.md +§3,本图刻意不重复那张的语义。 + +用法(仓库根目录,任意 python3): + python tools/arch_diagram/gen_wave_record_flow_svg.py +输出: + docs/images/故障录波数据流.svg +改内容:所有权、段形态、预算或红线变了才重画;调坐标与文案改下方常量后重跑。 +""" +import os +import sys +import html +import xml.etree.ElementTree as ET + +def emit(s): + """控制台按目标编码替换输出,绝不因 GBK 终端遇上 − ² 之类字符而崩 + (generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。""" + enc = getattr(sys.stdout, "encoding", None) or "ascii" + sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n") + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +OUT = os.path.join(ROOT, "docs", "images", "故障录波数据流.svg") + +# ----------------------------- 画布与字体 ----------------------------- +W = 1060 +H = 702 +FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" +F_TITLE = 20 +F_SUB = 12 +F_HEAD = 12.5 +F_BODY = 10.5 +F_TINY = 10 +LH = 14.5 + +INK = "#0f172a" +MUTED = "#475569" +FAINT = "#64748b" +LINE = "#334155" +RULE = "#cbd5e1" +WHITE = "#ffffff" +C_BLUE = "#2563eb" +BG_BLUE = "#eff6ff" +C_GREEN = "#15803d" +BG_GREEN = "#f0fdf4" +C_VIOLET = "#6d28d9" +BG_VIOLET = "#f5f3ff" +C_AMBER = "#b45309" +BG_AMBER = "#fffbeb" +C_TEAL = "#0f766e" +BG_TEAL = "#f0fdfa" +C_ROSE = "#e11d48" +BG_ROSE = "#fff1f2" +BG_GRAY = "#f8fafc" + +# ----------------------------- 几何 ----------------------------- +PAD = 40 +RIGHT = W - 40 +A_TOP = 104 +A_H = 118 +BOX_W = 203 +GAP = 56 +LBL_Y = 236 +B_TOP = 252 +B_H = 94 +BL_X = PAD +BL_W = 430 +BR_X = 494 +BR_W = RIGHT - 494 +C_TOP = 356 +C_H = 136 +CHIP_W = 316 +CHIP_GAP = 16 +D_TOP = 514 +D_H = 132 +FOOT_Y = 672 + +# ----------------------------- 数据(改这里) ----------------------------- +TITLE = "录波段数据的四个所有者 —— 谁独占写、谁只能读、标志怎么过界" +SUB1 = "写侧只有一条路:adcTask 压环 → defaultTask 落盘 → /wave 段文件;读者一律只读已封存的段。" +SUB2 = "「已封存」是唯一的跨任务接口:没有任何人读别人的活缓冲——这是本图唯一要记住的一件事。" + +CHAIN = [ + ("触发来路:只置标志", C_AMBER, BG_AMBER, [ + "UI 手动录波操作页", + "prot_elm 的 picked 上升沿", + "bench 构建定周期自抓", + "谁都只置一个原子标志", + ]), + ("adcTask:唯一写环的人", C_BLUE, BG_BLUE, [ + "每拍压 8 路 int16 原始码值", + "专用录波环 320 点 × 8 路", + "查标志 → 冻结 → 计满封段", + "= 5,120 B,落 SRAM1", + ]), + ("defaultTask:文件唯一写者", C_GREEN, BG_GREEN, [ + "排空已封存区 → f_write", + "写回字节数不符即算失败", + "分次写 256 B,不整段驻留", + "更新段账与最新段号", + ]), + ("/wave 段文件", C_VIOLET, BG_VIOLET, [ + "文件名 WAVE 加两位十进制", + "头存 eng、系数与 clip_mask", + "数据区按通道分列存放", + "与 /log 平级,FTP 根下可见", + ]), +] +EDGE = ["只置标志", "已封存标志", "字节数不符即失败"] + +LEFT_NOTE = ("采集主线照常推进(本波一行不改它)", C_TEAL, BG_TEAL, [ + "sample_ring → phasor → freq → meas → 104 / UI,全链同源", + "冻结窗口丢的是录波样本,不是采集:相量、测频、判据、上送一律照常", +]) +READERS = ("三个读者:一律只读已封存的段", LINE, BG_GRAY, [ + "uiTask 回放页 —— 只读窗口取一段,自己画(不写文件)", + "FTP(netTask / lftpd)—— 只搬文件,不碰采集上下文", + "PC 脚本 —— 重算全周傅氏,与板上 meas: 与 104 浮点对表", +]) + +TRAPS = [ + ("陷阱一 · 取窗竞态", [ + "sample_ring_window() 读 head 后逐点拷,没有临界区", + "现在只有 adcTask 同任务调用才安全", + "处置:录波不复用它,另立专用环;跨任务只传已封存标志", + ]), + ("陷阱二 · FatFs 追加写", [ + "该移植 FA_OPEN_APPEND 不含 FA_WRITE", + "簇分配失败还返 FR_OK 加短写,无错误码", + "处置:打开带 FA_WRITE,判成功必须比对写回字节数", + ]), + ("陷阱三 · 只有 8.3 短名", [ + "FF_USE_LFN=0:长名与点开头一律拒", + "处置:段名 = WAVE 加两位十进制再加 .BIN,轮转覆盖", + "目录 /wave 与 /log 平级,FTP 鉴权现成,零新代码", + ]), +] + +FORBID = [ + "不许跨任务读活的录波环或 sample_ring——没有临界区,读出来是两帧混合数据", + "不许 protectTask 写环或落盘——它只置一个请求标志,同 prot_elm_request_reset 的模式", + "不许 netTask 与 lftpd 读环——文件服务只搬已封存的段,不碰采集上下文", + "不许把 U 与 I 两段叠在同一张图上比相位——跨转换器无共同采样触发,V0.1 的硬件事实", +] +FOOT = ("字段级布局见 docs/子系统设计/故障录波设计说明.md §6.2;预算与 R6 前置见 §7;触发源与段长冻结语义见 §4。" + "本图由 tools/arch_diagram/gen_wave_record_flow_svg.py 生成,改图请改脚本重跑,勿手改 SVG。") + +# ----------------------------- 排版与预检 ----------------------------- +OVERRUNS = [] + +def n(v): + return ("%g" % (round(float(v), 2))) + +def text_w(s, size): + w = 0.0 + for ch in s: + w += size * (1.0 if ord(ch) > 0x2E80 else 0.58) + return w + +def fit(tag, s, size, maxw): + got = text_w(s, size) + if got > maxw + 0.5: + OVERRUNS.append("%s: %.0f gt %.0f %s" % (tag, got, maxw, s)) + +def wrap(s, size, maxw): + out = [] + cur = "" + for ch in s: + if cur and text_w(cur + ch, size) > maxw: + out.append(cur) + cur = ch + else: + cur += ch + if cur: + out.append(cur) + return out + +def esc(s): + return html.escape(s, quote=True) + +E = [] + +def text(x, y, s, size, fill, weight="400", anchor="start"): + E.append('%s' + % (n(x), n(y), n(size), weight, anchor, fill, esc(s))) + +def rect(x, y, w, h, fg, bg, dash=None, rx=9): + st = 'fill="%s" stroke="%s" stroke-width="1.2"' % (bg, fg) + if dash: + st += ' stroke-dasharray="%s"' % dash + E.append('' % (n(x), n(y), n(w), n(h), n(rx), st)) + +def box(x, y, w, h, head, lines, fg, bg, dash=None): + rect(x, y, w, h, fg, bg, dash) + fit("head " + head, head, F_HEAD, w - 18) + text(x + 10, y + 21, head, F_HEAD, fg, "700") + yy = y + 21 + 17 + for ln in lines: + for sub in wrap(ln, F_BODY, w - 20): + fit("body " + sub[:14], sub, F_BODY, w - 20) + text(x + 10, yy, sub, F_BODY, INK) + yy += LH + if yy > y + h - 2: + OVERRUNS.append("tall %s: need %.0f have %.0f" % (head, yy - y, h)) + +def line_seg(pts, color, dash=None, head=True): + st = 'fill="none" stroke="%s" stroke-width="1.4"' % color + if dash: + st += ' stroke-dasharray="%s"' % dash + if head: + st += ' marker-end="url(#ah)"' + E.append('' % (" ".join(n(a) + "," + n(b) for a, b in pts), st)) + +def halo_text(cx, y, s, size, fill, weight="600"): + wpx = text_w(s, size) + 8 + rect(cx - wpx / 2, y - size, wpx, size + 6, WHITE, WHITE, rx=3) + text(cx, y, s, size, fill, weight, "middle") + +# ----------------------------- 绘制 ----------------------------- +E.append('' % (W, H, WHITE)) +text(PAD, 42, TITLE, F_TITLE, INK, "700") +text(PAD, 66, SUB1, F_SUB, MUTED) +text(PAD, 86, SUB2, F_SUB, MUTED) +fit("sub1", SUB1, F_SUB, RIGHT - PAD) +fit("sub2", SUB2, F_SUB, RIGHT - PAD) + +centers = [] +for i, (head, fg, bg, lines) in enumerate(CHAIN): + x = PAD + i * (BOX_W + GAP) + box(x, A_TOP, BOX_W, A_H, head, lines, fg, bg) + centers.append((x, x + BOX_W)) + +for i, lbl in enumerate(EDGE): + x1 = centers[i][1] + x2 = centers[i + 1][0] + line_seg([(x1, A_TOP + A_H / 2), (x2, A_TOP + A_H / 2)], LINE) + halo_text((x1 + x2) / 2, LBL_Y, lbl, F_TINY, MUTED) + +bx4 = PAD + 3 * (BOX_W + GAP) + BOX_W / 2 +by = A_TOP + A_H +line_seg([(bx4, by), (bx4, by + 15), (BR_X + 120, by + 15), (BR_X + 120, B_TOP)], LINE) + +bx2 = PAD + 1 * (BOX_W + GAP) + BOX_W / 2 +line_seg([(bx2, by), (bx2, B_TOP)], FAINT, dash="5,4", head=False) + +box(BL_X, B_TOP, BL_W, B_H, LEFT_NOTE[0], LEFT_NOTE[3], LEFT_NOTE[1], LEFT_NOTE[2]) +box(BR_X, B_TOP, BR_W, B_H, READERS[0], READERS[3], READERS[1], READERS[2]) + +for i, (head, lines) in enumerate(TRAPS): + x = PAD + i * (CHIP_W + CHIP_GAP) + box(x, C_TOP, CHIP_W, C_H, head, lines, C_AMBER, BG_AMBER) + +rect(PAD, D_TOP, RIGHT - PAD, D_H, C_ROSE, BG_ROSE, dash="7,5") +text(PAD + 12, D_TOP + 23, "四条「不许」——上面那张图上没有箭头经过的地方", F_HEAD, C_ROSE, "700") +yy = D_TOP + 23 + 19 +for ln in FORBID: + for sub in wrap(ln, F_BODY, RIGHT - PAD - 26): + fit("forbid " + sub[:12], sub, F_BODY, RIGHT - PAD - 26) + text(PAD + 12, yy, sub, F_BODY, INK) + yy += LH +if yy > D_TOP + D_H - 2: + OVERRUNS.append("tall forbid") + +fy = FOOT_Y +for i, sub in enumerate(wrap(FOOT, F_TINY, RIGHT - PAD)): + text(PAD, fy + i * 14, sub, F_TINY, FAINT) + if i > 1: + OVERRUNS.append("foot lines beyond two") + +svg = ('%s%s') % ( + W, H, W, H, FONT, + '', + "".join(E)) +ET.fromstring(svg) +if not os.path.isdir(os.path.dirname(OUT)): + os.makedirs(os.path.dirname(OUT)) +with open(OUT, "w", encoding="utf-8", newline="\n") as fh: + fh.write(svg + "\n") +emit("wrote %s (%d bytes)" % (os.path.relpath(OUT, ROOT).replace(os.sep, "/"), len(svg.encode("utf-8")))) +for o in OVERRUNS: + emit("OVERFLOW " + o) +emit("overruns=%d" % len(OVERRUNS))