docs(采集可信): 数据可信文档台阶化重构并立项故障录波

两份工作互锁(互相引用),故同一笔提交。

数据可信:全文按「五级台阶」重排(§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/ 的生成器产出,脚本与产物同笔提交。
代码一行未动:本波只交付文档与设计。
This commit is contained in:
2026-10-29 13:18:28 +08:00
parent 49cddac0dd
commit b46e44d92c
10 changed files with 1840 additions and 142 deletions
@@ -0,0 +1,164 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1060" height="818" viewBox="0 0 1060 818" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
<rect x="0" y="0" width="1060" height="818" fill="#ffffff"/>
<text x="46.0" y="42.0" font-size="21" font-weight="700" fill="#0f172a">同一个针脚,两种证法 —— 直流只证「高度」,正弦才证「形状与节拍」</text>
<text x="46.0" y="68.0" font-size="13" fill="#475569">上排已做完(2026-10-27 静态注入六项判据全 PASS);下排一次也没做过:正弦波从没进过这条链,所以后面的相量、测频、削顶、谐波对不对,现在无人能答</text>
<text x="46.0" y="87.0" font-size="13" fill="#475569">两排共用同一个注入点、同一套前端、同一份折算代码 —— 差别只在信号源是「平的」还是「会走 20 点/周的正弦」</text>
<rect x="34.0" y="108.0" width="992.0" height="160.0" rx="10" fill="#f0fdf4" stroke="#bbf7d0" stroke-width="1.2"/>
<text x="46.0" y="128.0" font-size="12.5" font-weight="700" fill="#16a34a">上排 · 直流静态标定(已做)</text>
<rect x="922.0" y="116.0" width="88.0" height="34.0" rx="6" fill="#ffffff" stroke="#16a34a" stroke-width="1.2"/>
<line x1="928.0" y1="125.0" x2="1004.0" y2="125.0" stroke="#16a34a" stroke-width="2"/>
<circle cx="934.0" cy="125.0" r="2.8" fill="#16a34a"/>
<circle cx="966.0" cy="125.0" r="2.8" fill="#16a34a"/>
<circle cx="998.0" cy="125.0" r="2.8" fill="#16a34a"/>
<rect x="46.0" y="168.0" width="136.0" height="88.0" rx="8" fill="#ffffff" stroke="#16a34a" stroke-width="1.6"/>
<text x="53.0" y="186.0" font-size="12.5" font-weight="700" fill="#0f172a">直流源</text>
<text x="53.0" y="204.0" font-size="10.5" fill="#475569">电池或台式电源</text>
<text x="53.0" y="218.0" font-size="10.5" fill="#475569">+ 电位器分压</text>
<path d="M 192.2 208.0 L 201.2 212.0 L 192.2 216.0 Z" fill="#334155"/>
<line x1="184.0" y1="212.0" x2="194.2" y2="212.0" stroke="#334155" stroke-width="1.6"/>
<rect x="212.4" y="168.0" width="136.0" height="88.0" rx="8" fill="#ffffff" stroke="#16a34a" stroke-width="1.6"/>
<text x="219.4" y="186.0" font-size="12.5" font-weight="700" fill="#0f172a">万用表</text>
<text x="219.4" y="204.0" font-size="10.5" fill="#475569">同时接在注入点</text>
<text x="219.4" y="218.0" font-size="10.5" fill="#475569">当唯一基准读数</text>
<path d="M 358.6 208.0 L 367.6 212.0 L 358.6 216.0 Z" fill="#334155"/>
<line x1="350.4" y1="212.0" x2="360.6" y2="212.0" stroke="#334155" stroke-width="1.6"/>
<rect x="378.8" y="168.0" width="136.0" height="88.0" rx="8" fill="#ffffff" stroke="#16a34a" stroke-width="1.6"/>
<text x="385.8" y="186.0" font-size="12.5" font-weight="700" fill="#0f172a">注入点 R31</text>
<text x="385.8" y="204.0" font-size="10.5" fill="#475569">网名 AIN1 = U5.A3</text>
<text x="385.8" y="218.0" font-size="10.5" fill="#475569">= ADC7 引脚(1:1)</text>
<path d="M 525.0 208.0 L 534.0 212.0 L 525.0 216.0 Z" fill="#334155"/>
<line x1="516.8" y1="212.0" x2="527.0" y2="212.0" stroke="#334155" stroke-width="1.6"/>
<rect x="545.2" y="168.0" width="136.0" height="88.0" rx="8" fill="#ffffff" stroke="#16a34a" stroke-width="1.6"/>
<text x="552.2" y="186.0" font-size="12.5" font-weight="700" fill="#0f172a">前端 + 转换器</text>
<text x="552.2" y="204.0" font-size="10.5" fill="#475569">同一条电阻网络</text>
<text x="552.2" y="218.0" font-size="10.5" fill="#475569">TPAFE 16b、ADC1 12b</text>
<path d="M 691.4 208.0 L 700.4 212.0 L 691.4 216.0 Z" fill="#334155"/>
<line x1="683.2" y1="212.0" x2="693.4" y2="212.0" stroke="#334155" stroke-width="1.6"/>
<rect x="711.6" y="168.0" width="136.0" height="88.0" rx="8" fill="#ffffff" stroke="#16a34a" stroke-width="1.6"/>
<text x="718.6" y="186.0" font-size="12.5" font-weight="700" fill="#0f172a">折算</text>
<text x="718.6" y="204.0" font-size="10.5" fill="#475569">adc_cal_apply()</text>
<text x="718.6" y="218.0" font-size="10.5" fill="#475569">单位仍是引脚 V</text>
<path d="M 857.8 208.0 L 866.8 212.0 L 857.8 216.0 Z" fill="#334155"/>
<line x1="849.6" y1="212.0" x2="859.8" y2="212.0" stroke="#334155" stroke-width="1.6"/>
<rect x="878.0" y="168.0" width="136.0" height="88.0" rx="8" fill="#ffffff" stroke="#16a34a" stroke-width="1.6"/>
<text x="885.0" y="186.0" font-size="12.5" font-weight="700" fill="#0f172a">三点线性拟合</text>
<text x="885.0" y="204.0" font-size="10.5" fill="#475569">0 / 0.5 / 1.0 V</text>
<text x="885.0" y="218.0" font-size="10.5" fill="#475569">R² = 0.9999997,斜率</text>
<text x="885.0" y="232.0" font-size="10.5" fill="#475569">差 −0.14 %</text>
<rect x="34.0" y="286.0" width="992.0" height="160.0" rx="10" fill="#fff1f2" stroke="#fecdd3" stroke-width="1.2"/>
<text x="46.0" y="306.0" font-size="12.5" font-weight="700" fill="#e11d48">下排 · 交流正弦验证(本文新增 · 待做)</text>
<rect x="922.0" y="294.0" width="88.0" height="34.0" rx="6" fill="#ffffff" stroke="#e11d48" stroke-width="1.2"/>
<line x1="926.0" y1="311.0" x2="1006.0" y2="311.0" stroke="#cbd5e1" stroke-width="1" stroke-dasharray="3 3"/>
<polyline points="928.0,311.0 929.3,310.1 930.5,309.1 931.8,308.2 933.1,307.3 934.3,306.5 935.6,305.7 936.9,305.0 938.1,304.3 939.4,303.7 940.7,303.2 941.9,302.8 943.2,302.4 944.5,302.2 945.7,302.0 947.0,302.0 948.3,302.0 949.5,302.2 950.8,302.4 952.1,302.8 953.3,303.2 954.6,303.7 955.9,304.3 957.1,305.0 958.4,305.7 959.7,306.5 960.9,307.3 962.2,308.2 963.5,309.1 964.7,310.1 966.0,311.0 967.3,311.9 968.5,312.9 969.8,313.8 971.1,314.7 972.3,315.5 973.6,316.3 974.9,317.0 976.1,317.7 977.4,318.3 978.7,318.8 979.9,319.2 981.2,319.6 982.5,319.8 983.7,320.0 985.0,320.0 986.3,320.0 987.5,319.8 988.8,319.6 990.1,319.2 991.3,318.8 992.6,318.3 993.9,317.7 995.1,317.0 996.4,316.3 997.7,315.5 998.9,314.7 1000.2,313.8 1001.5,312.9 1002.7,311.9 1004.0,311.0" fill="none" stroke="#e11d48" stroke-width="1.9" stroke-linejoin="round"/>
<circle cx="928.0" cy="311.0" r="2.0" fill="#e11d48"/>
<circle cx="931.8" cy="308.2" r="2.0" fill="#e11d48"/>
<circle cx="935.6" cy="305.7" r="2.0" fill="#e11d48"/>
<circle cx="939.4" cy="303.7" r="2.0" fill="#e11d48"/>
<circle cx="943.2" cy="302.4" r="2.0" fill="#e11d48"/>
<circle cx="947.0" cy="302.0" r="2.0" fill="#e11d48"/>
<circle cx="950.8" cy="302.4" r="2.0" fill="#e11d48"/>
<circle cx="954.6" cy="303.7" r="2.0" fill="#e11d48"/>
<circle cx="958.4" cy="305.7" r="2.0" fill="#e11d48"/>
<circle cx="962.2" cy="308.2" r="2.0" fill="#e11d48"/>
<circle cx="966.0" cy="311.0" r="2.0" fill="#e11d48"/>
<circle cx="969.8" cy="313.8" r="2.0" fill="#e11d48"/>
<circle cx="973.6" cy="316.3" r="2.0" fill="#e11d48"/>
<circle cx="977.4" cy="318.3" r="2.0" fill="#e11d48"/>
<circle cx="981.2" cy="319.6" r="2.0" fill="#e11d48"/>
<circle cx="985.0" cy="320.0" r="2.0" fill="#e11d48"/>
<circle cx="988.8" cy="319.6" r="2.0" fill="#e11d48"/>
<circle cx="992.6" cy="318.3" r="2.0" fill="#e11d48"/>
<circle cx="996.4" cy="316.3" r="2.0" fill="#e11d48"/>
<circle cx="1000.2" cy="313.8" r="2.0" fill="#e11d48"/>
<rect x="46.0" y="346.0" width="136.0" height="88.0" rx="8" fill="#ffffff" stroke="#e11d48" stroke-width="1.6"/>
<text x="53.0" y="364.0" font-size="12.5" font-weight="700" fill="#0f172a">信号发生器</text>
<text x="53.0" y="382.0" font-size="10.5" fill="#475569">50 Hz 正弦</text>
<text x="53.0" y="396.0" font-size="10.5" fill="#475569">幅度按峰值算,见底部红</text>
<text x="53.0" y="410.0" font-size="10.5" fill="#475569">线</text>
<path d="M 192.2 386.0 L 201.2 390.0 L 192.2 394.0 Z" fill="#334155"/>
<line x1="184.0" y1="390.0" x2="194.2" y2="390.0" stroke="#334155" stroke-width="1.6"/>
<rect x="212.4" y="346.0" width="136.0" height="88.0" rx="8" fill="#ffffff" stroke="#e11d48" stroke-width="1.6"/>
<text x="219.4" y="364.0" font-size="12.5" font-weight="700" fill="#0f172a">注入点 R31</text>
<text x="219.4" y="382.0" font-size="10.5" fill="#475569">同上排同一个点</text>
<text x="219.4" y="396.0" font-size="10.5" fill="#475569">外串 10 k、与板共地</text>
<path d="M 358.6 386.0 L 367.6 390.0 L 358.6 394.0 Z" fill="#334155"/>
<line x1="350.4" y1="390.0" x2="360.6" y2="390.0" stroke="#334155" stroke-width="1.6"/>
<rect x="378.8" y="346.0" width="136.0" height="88.0" rx="8" fill="#ffffff" stroke="#e11d48" stroke-width="1.6"/>
<text x="385.8" y="364.0" font-size="12.5" font-weight="700" fill="#0f172a">前端 + 转换器</text>
<text x="385.8" y="382.0" font-size="10.5" fill="#475569">同上排同一条链</text>
<text x="385.8" y="396.0" font-size="10.5" fill="#475569">STM32 五路负半周必削顶</text>
<path d="M 525.0 386.0 L 534.0 390.0 L 525.0 394.0 Z" fill="#334155"/>
<line x1="516.8" y1="390.0" x2="527.0" y2="390.0" stroke="#334155" stroke-width="1.6"/>
<rect x="545.2" y="346.0" width="136.0" height="88.0" rx="8" fill="#ffffff" stroke="#e11d48" stroke-width="1.6"/>
<text x="552.2" y="364.0" font-size="12.5" font-weight="700" fill="#0f172a">1 kHz 采样点</text>
<text x="552.2" y="382.0" font-size="10.5" fill="#475569">TIM3 TRGO 同时触发</text>
<text x="552.2" y="396.0" font-size="10.5" fill="#475569">50 Hz → 20 点/整周</text>
<path d="M 691.4 386.0 L 700.4 390.0 L 691.4 394.0 Z" fill="#334155"/>
<line x1="683.2" y1="390.0" x2="693.4" y2="390.0" stroke="#334155" stroke-width="1.6"/>
<rect x="711.6" y="346.0" width="136.0" height="88.0" rx="8" fill="#ffffff" stroke="#e11d48" stroke-width="1.6"/>
<text x="718.6" y="364.0" font-size="12.5" font-weight="700" fill="#0f172a">全周傅氏</text>
<text x="718.6" y="382.0" font-size="10.5" fill="#475569">phasor_run():√2/N </text>
<text x="718.6" y="396.0" font-size="10.5" fill="#475569">预乘</text>
<text x="718.6" y="410.0" font-size="10.5" fill="#475569">直接得有效值与相位</text>
<path d="M 857.8 386.0 L 866.8 390.0 L 857.8 394.0 Z" fill="#334155"/>
<line x1="849.6" y1="390.0" x2="859.8" y2="390.0" stroke="#334155" stroke-width="1.6"/>
<rect x="878.0" y="346.0" width="136.0" height="88.0" rx="8" fill="#ffffff" stroke="#e11d48" stroke-width="1.6"/>
<text x="885.0" y="364.0" font-size="12.5" font-weight="700" fill="#0f172a">读数出口</text>
<text x="885.0" y="382.0" font-size="10.5" fill="#475569">meas 200 ms → 104 YC</text>
<text x="885.0" y="396.0" font-size="10.5" fill="#475569"> 0x4011</text>
<text x="885.0" y="410.0" font-size="10.5" fill="#475569">幅值 + 品质 + 频率(或</text>
<text x="885.0" y="424.0" font-size="10.5" fill="#475569"> UI)</text>
<text x="46.0" y="456.0" font-size="12.5" font-weight="700" fill="#2563eb">只有正弦能证的六件事(直流在构造上就证不到)</text>
<rect x="46.0" y="466.0" width="151.3" height="74.0" rx="8" fill="#eff6ff"/>
<line x1="46.0" y1="466.0" x2="46.0" y2="540.0" stroke="#2563eb" stroke-width="3"/>
<text x="56.0" y="486.0" font-size="12" font-weight="700" fill="#0f172a">整周窗对不对</text>
<text x="56.0" y="504.0" font-size="10" fill="#475569">窗长差 1 点,相位误差就跟</text>
<text x="56.0" y="518.0" font-size="10" fill="#475569">着来;直流恒 0 相位,测不</text>
<text x="56.0" y="532.0" font-size="10" fill="#475569">出</text>
<rect x="209.3" y="466.0" width="151.3" height="74.0" rx="8" fill="#eff6ff"/>
<line x1="209.3" y1="466.0" x2="209.3" y2="540.0" stroke="#2563eb" stroke-width="3"/>
<text x="219.3" y="486.0" font-size="12" font-weight="700" fill="#0f172a">有效值是否依赖波形</text>
<text x="219.3" y="504.0" font-size="10" fill="#475569">直流只有一个点:方波 / 三</text>
<text x="219.3" y="518.0" font-size="10" fill="#475569">角 / 正弦算出的都是同一个</text>
<text x="219.3" y="532.0" font-size="10" fill="#475569">数</text>
<rect x="372.7" y="466.0" width="151.3" height="74.0" rx="8" fill="#eff6ff"/>
<line x1="372.7" y1="466.0" x2="372.7" y2="540.0" stroke="#2563eb" stroke-width="3"/>
<text x="382.7" y="486.0" font-size="12" font-weight="700" fill="#0f172a">相位</text>
<text x="382.7" y="504.0" font-size="10" fill="#475569">φ = atan2(fIm, fRe) 全</text>
<text x="382.7" y="518.0" font-size="10" fill="#475569">靠正弦才有意义</text>
<rect x="536.0" y="466.0" width="151.3" height="74.0" rx="8" fill="#eff6ff"/>
<line x1="536.0" y1="466.0" x2="536.0" y2="540.0" stroke="#2563eb" stroke-width="3"/>
<text x="546.0" y="486.0" font-size="12" font-weight="700" fill="#0f172a">测频 freq_run</text>
<text x="546.0" y="504.0" font-size="10" fill="#475569">共轭乘积取相位增量;直流相</text>
<text x="546.0" y="518.0" font-size="10" fill="#475569">位不前进 → 恒无效</text>
<rect x="699.3" y="466.0" width="151.3" height="74.0" rx="8" fill="#eff6ff"/>
<line x1="699.3" y1="466.0" x2="699.3" y2="540.0" stroke="#2563eb" stroke-width="3"/>
<text x="709.3" y="486.0" font-size="12" font-weight="700" fill="#0f172a">削顶路由 CLIPPED</text>
<text x="709.3" y="504.0" font-size="10" fill="#475569">UB/UC/IB/IC/IN 单极性 0</text>
<text x="709.3" y="518.0" font-size="10" fill="#475569">~3.3 V,负半周必削顶 → 该</text>
<text x="709.3" y="532.0" font-size="10" fill="#475569">报 IV 就得报 IV</text>
<rect x="862.7" y="466.0" width="151.3" height="74.0" rx="8" fill="#eff6ff"/>
<line x1="862.7" y1="466.0" x2="862.7" y2="540.0" stroke="#2563eb" stroke-width="3"/>
<text x="872.7" y="486.0" font-size="12" font-weight="700" fill="#0f172a">谐波实例 C04 / C05</text>
<text x="872.7" y="504.0" font-size="10" fill="#475569">二次 / 五次相量已建但无消</text>
<text x="872.7" y="518.0" font-size="10" fill="#475569">费者:发生器给含谐波信号才</text>
<text x="872.7" y="532.0" font-size="10" fill="#475569">能验</text>
<rect x="46.0" y="558.0" width="476.0" height="128.0" rx="10" fill="#fff1f2" stroke="#e11d48" stroke-width="1.6"/>
<text x="60.0" y="582.0" font-size="12.5" font-weight="700" fill="#e11d48">安全红线(照抄 ADC通道映射说明 §7)</text>
<text x="60.0" y="604.0" font-size="11" fill="#0f172a">TPAFE 侧只到 ±2 V:正弦看峰值不只看有效值 —— 1 Vrms 的峰值已是 1.41 V,再高</text>
<text x="60.0" y="619.0" font-size="11" fill="#475569">就顶到 ±5 V 档边缘;</text>
<text x="60.0" y="638.0" font-size="11" fill="#0f172a">STM32 那五路严禁超过 3.3 V(单极性,超了是硬件级风险,不是削顶那一路好笑的验证</text>
<text x="60.0" y="653.0" font-size="11" fill="#475569">);</text>
<text x="60.0" y="672.0" font-size="11" fill="#0f172a">发生器与板必须共地(不然量到的是地环差);引脚侧注入一律外串 10 k;不许拿端子侧</text>
<text x="60.0" y="687.0" font-size="11" fill="#475569">上额定电压走捷径。</text>
<rect x="538.0" y="558.0" width="476.0" height="128.0" rx="10" fill="#fffbeb" stroke="#d97706" stroke-width="1.6"/>
<text x="552.0" y="582.0" font-size="12.5" font-weight="700" fill="#d97706">这台发生器证不到的三件事</text>
<text x="552.0" y="604.0" font-size="11" fill="#0f172a">④ 端子变比:注入点在子板之前,算出来的还是引脚域 —— 变比要靠原理图或万用表实测(</text>
<text x="552.0" y="619.0" font-size="11" fill="#475569">另一张卡);</text>
<text x="552.0" y="638.0" font-size="11" fill="#0f172a">精度等级验收:发生器不是标准源,它的幅值误差本身就要被校,不能拿它冒充送检依据;</text>
<text x="552.0" y="657.0" font-size="11" fill="#0f172a">跨转换器相位:UB/UC 与 UA 分属两片转换器、无共同采样触发(V0.1 硬件缺陷),任何</text>
<text x="552.0" y="672.0" font-size="11" fill="#475569">相位比较都不作数。</text>
<line x1="46.0" y1="700.0" x2="1014.0" y2="700.0" stroke="#cbd5e1" stroke-width="1"/>
<text x="46.0" y="722.0" font-size="13" fill="#475569">读数出口现在就有:104 YC(test/host/iec104_master.py,浮点无量化)+ 串口每 10 s 的 phasor(hb) / freq(hb) / meas: 行;但板上没有「把窗里 2</text>
<text x="46.0" y="741.0" font-size="13" fill="#475569">0 个点导出来」的出口,正弦只能靠派生标量证,而 phasor 行是 ×0.1 定点(注 1 V 只剩 10 格)。</text>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

+64
View File
@@ -0,0 +1,64 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1060" height="712" viewBox="0 0 1060 712" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
<rect x="0" y="0" width="1060" height="712" fill="#ffffff"/>
<text x="46.0" y="42.0" font-size="21" font-weight="700" fill="#0f172a">「数据可信」是五级台阶 —— 现在踩在第 2 级</text>
<text x="46.0" y="68.0" font-size="13" fill="#64748b">每升一级只多答一个问题:通路活 → 静态读数对 → 动态波形对 → 换算到端子 → 精度达标;台阶的高度差 = 下游能多信多少</text>
<text x="46.0" y="88.0" font-size="13" fill="#64748b">第 3 级不要标准源、也不要往端子加压:一台信号发生器 + 已有的注入点就能做(判据见 docs/任务规划/数据可信.md §4.6)</text>
<rect x="34.0" y="106.0" width="600.0" height="532.0" rx="12" fill="#eff6ff"/>
<rect x="46.0" y="370.0" width="196.0" height="96.0" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2"/>
<circle cx="70.0" cy="396.0" r="13" fill="#16a34a"/>
<text x="70.0" y="401.0" font-size="13" text-anchor="middle" font-weight="700" fill="#ffffff">1</text>
<text x="90.0" y="402.0" font-size="14.5" font-weight="700" fill="#0f172a">通路活</text>
<text x="57.0" y="428.0" font-size="11" font-weight="700" fill="#16a34a">已证 · 2026-10-27</text>
<rect x="248.0" y="306.0" width="196.0" height="160.0" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="2"/>
<circle cx="272.0" cy="332.0" r="13" fill="#16a34a"/>
<text x="272.0" y="337.0" font-size="13" text-anchor="middle" font-weight="700" fill="#ffffff">2</text>
<text x="292.0" y="338.0" font-size="14.5" font-weight="700" fill="#0f172a">静态引脚电压</text>
<text x="259.0" y="364.0" font-size="11" font-weight="700" fill="#16a34a">已证 · 2026-10-27</text>
<rect x="450.0" y="242.0" width="196.0" height="224.0" rx="8" fill="#fff1f2" stroke="#e11d48" stroke-width="2"/>
<circle cx="474.0" cy="268.0" r="13" fill="#e11d48"/>
<text x="474.0" y="273.0" font-size="13" text-anchor="middle" font-weight="700" fill="#ffffff">3</text>
<text x="494.0" y="274.0" font-size="14.5" font-weight="700" fill="#0f172a">动态引脚波形</text>
<text x="461.0" y="300.0" font-size="11" font-weight="700" fill="#e11d48">待做 · 本文新增</text>
<rect x="652.0" y="178.0" width="196.0" height="288.0" rx="8" fill="#fffbeb" stroke="#d97706" stroke-width="2"/>
<circle cx="676.0" cy="204.0" r="13" fill="#d97706"/>
<text x="676.0" y="209.0" font-size="13" text-anchor="middle" font-weight="700" fill="#ffffff">4</text>
<text x="696.0" y="210.0" font-size="14.5" font-weight="700" fill="#0f172a">端子二次值</text>
<text x="663.0" y="236.0" font-size="11" font-weight="700" fill="#d97706">等外部资源</text>
<rect x="854.0" y="114.0" width="196.0" height="352.0" rx="8" fill="#fffbeb" stroke="#d97706" stroke-width="2"/>
<circle cx="878.0" cy="140.0" r="13" fill="#d97706"/>
<text x="878.0" y="145.0" font-size="13" text-anchor="middle" font-weight="700" fill="#ffffff">5</text>
<text x="898.0" y="146.0" font-size="14.5" font-weight="700" fill="#0f172a">精度等级验收</text>
<text x="865.0" y="172.0" font-size="11" font-weight="700" fill="#d97706">等外部资源</text>
<line x1="46.0" y1="466.0" x2="1014.0" y2="466.0" stroke="#334155" stroke-width="2.2"/>
<text x="56.0" y="490.0" font-size="12.5" font-weight="700" fill="#0f172a">9 路都在转换?没串道?极性</text>
<text x="56.0" y="507.0" font-size="12.5" font-weight="700" fill="#0f172a">没反?</text>
<text x="56.0" y="527.0" font-size="11.5" fill="#64748b">开机自检 + 帧计数仍 1000 Hz</text>
<text x="56.0" y="542.0" font-size="11.5" fill="#64748b">;逐路注入只动本路</text>
<text x="258.0" y="490.0" font-size="12.5" font-weight="700" fill="#0f172a">注 1 V 直流,读数是不是 1 </text>
<text x="258.0" y="507.0" font-size="12.5" font-weight="700" fill="#0f172a">V?</text>
<text x="258.0" y="527.0" font-size="11.5" fill="#64748b">万用表 + 电位器三点线性:斜率</text>
<text x="258.0" y="542.0" font-size="11.5" fill="#64748b"> 6544.2 vs 理论 6553.6 码/V</text>
<text x="258.0" y="557.0" font-size="11.5" fill="#64748b">,差 −0.14 %</text>
<text x="460.0" y="490.0" font-size="12.5" font-weight="700" fill="#0f172a">注 1 V 正弦,算出来还是不是</text>
<text x="460.0" y="507.0" font-size="12.5" font-weight="700" fill="#0f172a"> 1 V?</text>
<text x="460.0" y="527.0" font-size="11.5" fill="#64748b">注正弦验全周傅氏幅值 / 相位 /</text>
<text x="460.0" y="542.0" font-size="11.5" fill="#64748b"> 测频 / 削顶 / 谐波抑制。挡路</text>
<text x="460.0" y="557.0" font-size="11.5" fill="#64748b">:FREQ_AMP_MIN = 50 V 在引</text>
<text x="460.0" y="572.0" font-size="11.5" fill="#64748b">脚域恒不过 → 测频只能读到 IV</text>
<text x="662.0" y="490.0" font-size="12.5" font-weight="700" fill="#0f172a">引脚上这 1 V 对应端子侧几 V</text>
<text x="662.0" y="507.0" font-size="12.5" font-weight="700" fill="#0f172a">?</text>
<text x="662.0" y="527.0" font-size="11.5" fill="#64748b">④ 子板 / 端子板变比:仓内无原</text>
<text x="662.0" y="542.0" font-size="11.5" fill="#64748b">理图,须拿图纸或万用表实测</text>
<text x="864.0" y="490.0" font-size="12.5" font-weight="700" fill="#0f172a">额定工况下整条链误差够几级?</text>
<text x="864.0" y="510.0" font-size="11.5" fill="#64748b">标准源两点标定写 [adc_cal] </text>
<text x="864.0" y="525.0" font-size="11.5" fill="#64748b">置 VALID → 保护自动解卡 → 送</text>
<text x="864.0" y="540.0" font-size="11.5" fill="#64748b">检</text>
<line x1="643.0" y1="100.0" x2="643.0" y2="638.0" stroke="#e11d48" stroke-width="2" stroke-dasharray="7 5"/>
<text x="651.0" y="118.0" font-size="11" font-weight="700" fill="#e11d48">eng 升档门:④ 变比到手 + 电气 </text>
<text x="651.0" y="133.0" font-size="11" font-weight="700" fill="#e11d48">8 路 VALID 全置位 → eng 才升 </text>
<text x="651.0" y="148.0" font-size="11" font-weight="700" fill="#e11d48">1</text>
<text x="46.0" y="132.0" font-size="11.5" font-weight="700" fill="#2563eb">M4 承诺边界(§2.6):蓝底这三列都还在「ADC 引脚」这一侧,eng = 0、单位是引脚电压 V</text>
<line x1="46.0" y1="660.0" x2="1014.0" y2="660.0" stroke="#cbd5e1" stroke-width="1"/>
<text x="46.0" y="684.0" font-size="13" fill="#64748b">第 2、3 级共用同一个注入点与同一套读数出口,但证的是两件事:直流证「一条水平线的高度」,正弦证「整条时间轴的形状与节拍」——第 3 级不能由第 2 级代证</text>
<text x="46.0" y="703.0" font-size="13" fill="#64748b">。</text>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

+79
View File
@@ -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` | 「录波采样率」行是本波偏离的对照基准;「立项未排」行与「文件传输(录波)」行按本波状态更新 |
+321 -137
View File
@@ -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[等待:发生器到位<br/>不动端子、不烧高压]
W1 --> Q1
Q1 -- 是 --> DYN[信号发生器动态波形验证<br/>D1 幅值 · D2 波形 · D5 削顶 先做]
DYN --> WB[回写:测试说明加「交流」一节<br/>通道映射补动态实测节]
WB --> Q2{变比到手?}
Q2 -- 否 --> W2[等待:子板实物或原理图]
W2 --> Q2
Q2 -- 是 --> CAL[两点标定与精度验收<br/>标准源 / 继保测试仪]
CAL --> V[电气 8 路 VALID 全置位]
V --> R[保护自动解卡<br/>闭锁 → 解锁台架验证]
R --> E([标准源送检 + 第三方 104 主站验收])
W1 -. 并行不阻塞 .-> ENG[eng 保持 0<br/>本波只承诺「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 绑定零改动 |
@@ -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` | 通道归属与注入点清单的属主;回放页「削」徽标的判据出处 |
@@ -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。
@@ -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 ('<text x="%.1f" y="%.1f" font-size="%g"%s%s fill="%s">%s</text>'
% (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 ('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%g"%s/>'
% (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 '<polyline points="%s" fill="none" stroke="%s" stroke-width="%g"%s stroke-linejoin="round"/>' % (
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 '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%g" fill="%s"%s/>' % (
x, y, w, h, rx, fill, s)
def arrow(x, y, color):
"""向右的短箭头,画在两个环节之间。"""
return ('<path d="M %.1f %.1f L %.1f %.1f L %.1f %.1f Z" fill="%s"/>'
% (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('<circle cx="%.1f" cy="%.1f" r="2.8" fill="%s"/>' % (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('<circle cx="%.1f" cy="%.1f" r="2.0" fill="%s"/>' % (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('<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (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 = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
'viewBox="0 0 %d %d" font-family="%s">\n%s\n</svg>\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()
@@ -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 ('<text x="%.1f" y="%.1f" font-size="%g"%s%s fill="%s">%s</text>'
% (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 ('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%g"%s/>'
% (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 '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%g" fill="%s"%s/>' % (
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('<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (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('<circle cx="%.1f" cy="%.1f" r="13" fill="%s"/>' % (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 = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
'viewBox="0 0 %d %d" font-family="%s">\n%s\n</svg>\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()
@@ -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('<text x="%s" y="%s" font-size="%s" font-weight="%s" text-anchor="%s" fill="%s">%s</text>'
% (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('<rect x="%s" y="%s" width="%s" height="%s" rx="%s" %s/>' % (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('<polyline points="%s" %s/>' % (" ".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('<rect x="0" y="0" width="%s" height="%s" fill="%s"/>' % (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 = ('<svg xmlns="http://www.w3.org/2000/svg" width="%s" height="%s" viewBox="0 0 %s %s" '
'font-family="%s">%s%s</svg>') % (
W, H, W, H, FONT,
'<defs><marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" '
'orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#334155"/></marker></defs>',
"".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))