docs(algo): 新增 adc_health 单文件深设计与三张配图
- 新建 docs/算法/采样有效性校验设计说明.md(15 章):判据推导
(100 ms 门限的三条独立依据)、电平/锁存/事件三种位语义与
位号对齐的收益代价、闭锁与 IWDG 兜底责任矩阵、观测盲区、
主机侧验证分工(11 组 TC / 31 断言映射)、现场判读手册、
自测问题与改前必读清单
- 配图走 draw-diagram 生成器路线(脚本 + 产物一并提交):
tools/arch_diagram/gen_adc_health_flow_svg.py 等三个脚本 →
docs/images/ 下三张 SVG;文内另有 2 张 Mermaid(判定主干
flowchart、静默位生命周期 stateDiagram-v2,沿用仓内既有方言)
- 登记与指针:docs/README.md §5.2 加一行 + 变更记录;
docs/算法/采集与算法层设计说明.md §4 与 §13 加深读指针;
本文只写「为什么」,接口表仍由该文档属主,不重复
- 读码确认并如实登记三项(本提交不改代码,待裁):
1) adc_task_init() 全库无调用点,adc_health_init() 因此不在
产品路径,判定正确只依赖 .bss 清零 + 首拍建基线
2) tgap 计帧点数(d-1 加权),adc1_gap 计次数——两侧单位
不对称,心跳里不可直接对比
3) TIM3 停摆时判定层随消费者一起停,最坏约 800 ms + 2498 ms
窗口内 protectTask 仍以旧相量判据(细节见 §10 / §14)
- 门禁自查:pre-commit 目标集全绿(doc-links / id-check /
空白 / EOF / 大文件);主机单测 adc_health 31/31 复跑通过;
9 个新增或改动文件 BOM=无、CR=0、U+FFFD=0
This commit is contained in:
@@ -106,6 +106,7 @@ docs/
|
||||
| [`傅氏算法详解.md`](算法/傅氏算法详解.md) | 全周波傅氏(单频 DFT)原理与工程实现:正交性、梳状带通、滑动窗、系数表折算有效值;保护/遥测双轨有效值的算法基础 |
|
||||
| [`采集与算法层设计说明.md`](算法/采集与算法层设计说明.md) | **`App/algo/` 的现状设计说明**:L1 语义取数与有效性判定、L2 采样环/相量/测频、L3 定值模型与元件框架的契约、关键机制(双区原子切换、乒乓半区、自适应测频窗、定值封存-自检-闭锁)、精度判据 J1~J6 与资源足迹 |
|
||||
| [`继电保护功能复现规划.md`](算法/继电保护功能复现规划.md) | **保护功能复现规划**:把参考系统(CLS100)保护链路映射到本项目(STM32F4)的分层功能/性能/参数清单、平台适配(N-18 等)与分阶段路线图 |
|
||||
| [`采样有效性校验设计说明.md`](算法/采样有效性校验设计说明.md) | **`App/algo/adc_health.c` 单文件深设计**:四类判据(静默 / 丢帧 / 序号倒退 / 错误计数增量)的判据推导与 100 ms 门限依据、电平-锁存-事件三种位语义与位号对齐、闭锁与看门狗兜底的责任划分、观测盲区、主机侧验证分工与现场判读手册(含三张配图生成器) |
|
||||
|
||||
---
|
||||
|
||||
@@ -272,6 +273,7 @@ docs/
|
||||
|
||||
| 日期 | 变更 | 说明 |
|
||||
|---|---|---|
|
||||
| 2026-10-21 | 新增算法专题 | 新建 `docs/算法/采样有效性校验设计说明.md`(`App/algo/adc_health.c` 单文件深设计:四类判据推导、位语义与位号对齐、闭锁与兜底责任划分、盲区与验证分工),并在 `tools/arch_diagram/` 新增三个配图生成器与产物 SVG;本索引 §5.2 补登记,`docs/算法/采集与算法层设计说明.md` §4 与 §13 加指针 |
|
||||
| 2026-10-14 | 网络文档合并 | 《网络Socket监听生命周期设计说明.md》(B10 根治,587 行)并入 [`网络/网络自愈架构设计.md`](网络/网络自愈架构设计.md):原理与规范层论证 → §1.1~§1.4、FUN_PARA bit3 契约 → §4.9、代码级事实清点 → §9、决策记录 → §10、`net_listener.c` 模块化边界 → §11,原「引用与归档约定」顺延为 §12;过程类内容(分阶段勾选清单、台架验收记录)不迁、留 git 历史;原文档删除。全库引用同步 13 处(`net_listener.c`/`.h` 注释、Net层阻塞模型、BSD_Socket_API 两篇、陷阱记录 B10、编号规范 §4.2/§5.2/§7.2);本索引 §5.1 补登记 |
|
||||
| 2026-10-14 | E14"丢中断"复核与归因更正 | 48 轮直读 SINT 矩阵复核历史"丢中断"说法:**芯片侧事件丢失 = 0**("丢失"= drain 读清消费 3/48 + 门控竞态 2/48 + 主机同步 1/48,幽灵态 0/48 未复现);根除性修复 = listen/relisten 路径排事件挪至 LISTEN 命令**之前**(吞新会话 CONNECT 的生产侧自伤窗);全局读收口(全局寄存器唯一读者=`net_poll`,I7 护栏);全库 17 文件约 40 处"丢中断"归因更正为"事件未收到/读清消费竞态",`docs/网络/网络自愈架构设计.md` §5.2 新增 E14 锚点;台架保留复核探针(drain-int/SINT 补读×3/超时归类);同日 E15 幽灵态归因复核(官网+手册实锤:0x04+旧端口 = 芯片**如实报告**旧连接拆链进行中——bit3=1 保留现场为自家配置、拆链窗与 §5.17 重传默认值吻合、WCH 官方博客确认断连即重连被拒且官方方案 = 关闭完成后重开,即 settle 等待) |
|
||||
| 2026-10-13 | 网络自愈架构文档瘦身与改名 | 合并文档 `网络/N34-自愈审计.md`(2062 行审计全记录)瘦身为**终版设计描述**(281 行:不变量 I1~I8 / 架构与机制详解 / 参数与 E 锚点速查 / 红线 / 延后项),并经 `rename_ref.py` 改名 [`网络/网络自愈架构设计.md`](网络/网络自愈架构设计.md)(全库 16 文件 30 处引用同步);审计过程(T1~T4、E0~E13)归档 git 历史,考古命令见该文档「引用与归档约定」 |
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1580" height="740" viewBox="0 0 1580 740" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
|
||||
<text x="40" y="44" font-size="21" font-weight="700" fill="#0f172a">采样有效性校验 · 出口位图与消费者掩码(逐位对齐)</text>
|
||||
<text x="40" y="70" font-size="13" fill="#64748b">同一 bit 在三张位图与四个掩码里始终落在同一列:位号对齐 = 消费者可以位图代打表</text>
|
||||
<rect x="0" y="0" width="1580" height="740" fill="#ffffff"/>
|
||||
<text x="40" y="178" font-size="12" font-weight="700" fill="#334155">判定层写出的三张位图(adc_health.c)</text>
|
||||
<text x="40" y="220" font-size="12" font-weight="700" fill="#0f172a">flags 电平位</text>
|
||||
<text x="40" y="238" font-size="10" fill="#64748b">adc_health_flags(),当前活跃</text>
|
||||
<rect x="310" y="200" width="112" height="46" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.55"/>
|
||||
<text x="320" y="219" font-size="11" font-weight="700" fill="#0d9488">bit0</text>
|
||||
<text x="320" y="236" font-size="10" fill="#334155">TPAFE_STALL</text>
|
||||
<rect x="426" y="200" width="112" height="46" rx="8" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.55"/>
|
||||
<text x="436" y="219" font-size="11" font-weight="700" fill="#0d9488">bit1</text>
|
||||
<text x="436" y="236" font-size="10" fill="#334155">STM32_STALL</text>
|
||||
<rect x="542" y="200" width="112" height="46" rx="8" fill="#fff1f2" stroke="#e11d48" stroke-opacity="0.55"/>
|
||||
<text x="552" y="219" font-size="11" font-weight="700" fill="#e11d48">bit2</text>
|
||||
<text x="552" y="236" font-size="10" fill="#334155">SEQ_BACKWARD</text>
|
||||
<rect x="658" y="200" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="668" y="219" font-size="11" font-weight="700" fill="#94a3b8">bit3</text>
|
||||
<text x="668" y="236" font-size="10" fill="#334155">—</text>
|
||||
<rect x="774" y="200" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="784" y="219" font-size="11" font-weight="700" fill="#94a3b8">bit4</text>
|
||||
<text x="784" y="236" font-size="10" fill="#334155">—</text>
|
||||
<rect x="890" y="200" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="900" y="219" font-size="11" font-weight="700" fill="#94a3b8">bit5</text>
|
||||
<text x="900" y="236" font-size="10" fill="#334155">—</text>
|
||||
<rect x="1006" y="200" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="1016" y="219" font-size="11" font-weight="700" fill="#94a3b8">bit6</text>
|
||||
<text x="1016" y="236" font-size="10" fill="#334155">—</text>
|
||||
<rect x="1122" y="200" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="1132" y="219" font-size="11" font-weight="700" fill="#94a3b8">bit7</text>
|
||||
<text x="1132" y="236" font-size="10" fill="#334155">—</text>
|
||||
<rect x="1250" y="200" width="340" height="46" rx="8" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.25"/>
|
||||
<text x="1262" y="225" font-size="11" fill="#475569">protectTask 每 20 ms 读它做 L5-4 闭锁;不看事件位</text>
|
||||
<text x="40" y="276" font-size="12" font-weight="700" fill="#0f172a">sticky 锁存位</text>
|
||||
<text x="40" y="294" font-size="10" fill="#64748b">adc_health_sticky(),直到 init 才清</text>
|
||||
<rect x="310" y="256" width="112" height="46" rx="8" fill="#fff1f2" stroke="#e11d48" stroke-opacity="0.55"/>
|
||||
<text x="320" y="275" font-size="11" font-weight="700" fill="#e11d48">bit0</text>
|
||||
<text x="320" y="292" font-size="10" fill="#334155">TPAFE_STALL</text>
|
||||
<rect x="426" y="256" width="112" height="46" rx="8" fill="#fff1f2" stroke="#e11d48" stroke-opacity="0.55"/>
|
||||
<text x="436" y="275" font-size="11" font-weight="700" fill="#e11d48">bit1</text>
|
||||
<text x="436" y="292" font-size="10" fill="#334155">STM32_STALL</text>
|
||||
<rect x="542" y="256" width="112" height="46" rx="8" fill="#fff1f2" stroke="#e11d48" stroke-opacity="0.55"/>
|
||||
<text x="552" y="275" font-size="11" font-weight="700" fill="#e11d48">bit2</text>
|
||||
<text x="552" y="292" font-size="10" fill="#334155">SEQ_BACKWARD</text>
|
||||
<rect x="658" y="256" width="112" height="46" rx="8" fill="#fff1f2" stroke="#e11d48" stroke-opacity="0.55"/>
|
||||
<text x="668" y="275" font-size="11" font-weight="700" fill="#e11d48">bit3</text>
|
||||
<text x="668" y="292" font-size="10" fill="#334155">FRSTDATA</text>
|
||||
<rect x="774" y="256" width="112" height="46" rx="8" fill="#fff1f2" stroke="#e11d48" stroke-opacity="0.55"/>
|
||||
<text x="784" y="275" font-size="11" font-weight="700" fill="#e11d48">bit4</text>
|
||||
<text x="784" y="292" font-size="10" fill="#334155">STM32_ERR</text>
|
||||
<rect x="890" y="256" width="112" height="46" rx="8" fill="#fff1f2" stroke="#e11d48" stroke-opacity="0.55"/>
|
||||
<text x="900" y="275" font-size="11" font-weight="700" fill="#e11d48">bit5</text>
|
||||
<text x="900" y="292" font-size="10" fill="#334155">HW_ERR</text>
|
||||
<rect x="1006" y="256" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="1016" y="275" font-size="11" font-weight="700" fill="#94a3b8">bit6</text>
|
||||
<text x="1016" y="292" font-size="10" fill="#334155">不入锁存</text>
|
||||
<rect x="1122" y="256" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="1132" y="275" font-size="11" font-weight="700" fill="#94a3b8">bit7</text>
|
||||
<text x="1132" y="292" font-size="10" fill="#334155">不入锁存</text>
|
||||
<rect x="1250" y="256" width="340" height="46" rx="8" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.25"/>
|
||||
<text x="1262" y="281" font-size="11" fill="#475569">只留痕不闭锁:曾停摆/曾出错必须可追溯</text>
|
||||
<text x="40" y="332" font-size="12" font-weight="700" fill="#0f172a">events 事件位</text>
|
||||
<text x="40" y="350" font-size="10" fill="#64748b">adc_health_check() 出参,仅当拍有效</text>
|
||||
<rect x="310" y="312" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="320" y="331" font-size="11" font-weight="700" fill="#94a3b8">bit0</text>
|
||||
<text x="320" y="348" font-size="10" fill="#334155">—</text>
|
||||
<rect x="426" y="312" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="436" y="331" font-size="11" font-weight="700" fill="#94a3b8">bit1</text>
|
||||
<text x="436" y="348" font-size="10" fill="#334155">—</text>
|
||||
<rect x="542" y="312" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="552" y="331" font-size="11" font-weight="700" fill="#94a3b8">bit2</text>
|
||||
<text x="552" y="348" font-size="10" fill="#334155">—</text>
|
||||
<rect x="658" y="312" width="112" height="46" rx="8" fill="#fffbeb" stroke="#d97706" stroke-opacity="0.55"/>
|
||||
<text x="668" y="331" font-size="11" font-weight="700" fill="#d97706">bit3</text>
|
||||
<text x="668" y="348" font-size="10" fill="#334155">FRSTDATA</text>
|
||||
<rect x="774" y="312" width="112" height="46" rx="8" fill="#fffbeb" stroke="#d97706" stroke-opacity="0.55"/>
|
||||
<text x="784" y="331" font-size="11" font-weight="700" fill="#d97706">bit4</text>
|
||||
<text x="784" y="348" font-size="10" fill="#334155">STM32_ERR</text>
|
||||
<rect x="890" y="312" width="112" height="46" rx="8" fill="#fffbeb" stroke="#d97706" stroke-opacity="0.55"/>
|
||||
<text x="900" y="331" font-size="11" font-weight="700" fill="#d97706">bit5</text>
|
||||
<text x="900" y="348" font-size="10" fill="#334155">HW_ERR</text>
|
||||
<rect x="1006" y="312" width="112" height="46" rx="8" fill="#fffbeb" stroke="#d97706" stroke-opacity="0.55"/>
|
||||
<text x="1016" y="331" font-size="11" font-weight="700" fill="#d97706">bit6</text>
|
||||
<text x="1016" y="348" font-size="10" fill="#334155">TPAFE_GAP</text>
|
||||
<rect x="1122" y="312" width="112" height="46" rx="8" fill="#fffbeb" stroke="#d97706" stroke-opacity="0.55"/>
|
||||
<text x="1132" y="331" font-size="11" font-weight="700" fill="#d97706">bit7</text>
|
||||
<text x="1132" y="348" font-size="10" fill="#334155">STM32_GAP</text>
|
||||
<rect x="1250" y="312" width="340" height="46" rx="8" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.25"/>
|
||||
<text x="1262" y="337" font-size="11" fill="#475569">adc_health_report() 的唯一输入:按位分三级处置</text>
|
||||
<text x="40" y="394" font-size="12" font-weight="700" fill="#334155">消费者侧的掩码与记忆位图</text>
|
||||
<text x="40" y="436" font-size="12" font-weight="700" fill="#0f172a">PROT_HF_LOCK_MASK</text>
|
||||
<text x="40" y="454" font-size="10" fill="#64748b">protect_task.c 编译期常量</text>
|
||||
<rect x="310" y="416" width="112" height="46" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
|
||||
<text x="320" y="435" font-size="11" font-weight="700" fill="#7c3aed">bit0</text>
|
||||
<text x="320" y="452" font-size="10" fill="#334155">闭锁</text>
|
||||
<rect x="426" y="416" width="112" height="46" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
|
||||
<text x="436" y="435" font-size="11" font-weight="700" fill="#7c3aed">bit1</text>
|
||||
<text x="436" y="452" font-size="10" fill="#334155">闭锁</text>
|
||||
<rect x="542" y="416" width="112" height="46" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
|
||||
<text x="552" y="435" font-size="11" font-weight="700" fill="#7c3aed">bit2</text>
|
||||
<text x="552" y="452" font-size="10" fill="#334155">闭锁</text>
|
||||
<rect x="658" y="416" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="668" y="435" font-size="11" font-weight="700" fill="#94a3b8">bit3</text>
|
||||
<text x="668" y="452" font-size="10" fill="#334155">不闭锁</text>
|
||||
<rect x="774" y="416" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="784" y="435" font-size="11" font-weight="700" fill="#94a3b8">bit4</text>
|
||||
<text x="784" y="452" font-size="10" fill="#334155">不闭锁</text>
|
||||
<rect x="890" y="416" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="900" y="435" font-size="11" font-weight="700" fill="#94a3b8">bit5</text>
|
||||
<text x="900" y="452" font-size="10" fill="#334155">不闭锁</text>
|
||||
<rect x="1006" y="416" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="1016" y="435" font-size="11" font-weight="700" fill="#94a3b8">bit6</text>
|
||||
<text x="1016" y="452" font-size="10" fill="#334155">—</text>
|
||||
<rect x="1122" y="416" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="1132" y="435" font-size="11" font-weight="700" fill="#94a3b8">bit7</text>
|
||||
<text x="1132" y="452" font-size="10" fill="#334155">—</text>
|
||||
<rect x="1250" y="416" width="340" height="46" rx="8" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.25"/>
|
||||
<text x="1262" y="433" font-size="11" fill="#475569">命中即 prot_elm_init() 复位元件 + 本拍不 eval;SOE</text>
|
||||
<text x="1262" y="448" font-size="11" fill="#475569"> 类型 5/6</text>
|
||||
<text x="40" y="492" font-size="12" font-weight="700" fill="#0f172a">serious 严重事件</text>
|
||||
<text x="40" y="510" font-size="10" fill="#64748b">adc_task.c 的 adc_health_report()</text>
|
||||
<rect x="310" y="472" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="320" y="491" font-size="11" font-weight="700" fill="#94a3b8">bit0</text>
|
||||
<text x="320" y="508" font-size="10" fill="#334155">—</text>
|
||||
<rect x="426" y="472" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="436" y="491" font-size="11" font-weight="700" fill="#94a3b8">bit1</text>
|
||||
<text x="436" y="508" font-size="10" fill="#334155">—</text>
|
||||
<rect x="542" y="472" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="552" y="491" font-size="11" font-weight="700" fill="#94a3b8">bit2</text>
|
||||
<text x="552" y="508" font-size="10" fill="#334155">—</text>
|
||||
<rect x="658" y="472" width="112" height="46" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
|
||||
<text x="668" y="491" font-size="11" font-weight="700" fill="#7c3aed">bit3</text>
|
||||
<text x="668" y="508" font-size="10" fill="#334155">FRSTDATA</text>
|
||||
<rect x="774" y="472" width="112" height="46" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
|
||||
<text x="784" y="491" font-size="11" font-weight="700" fill="#7c3aed">bit4</text>
|
||||
<text x="784" y="508" font-size="10" fill="#334155">STM32_ERR</text>
|
||||
<rect x="890" y="472" width="112" height="46" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
|
||||
<text x="900" y="491" font-size="11" font-weight="700" fill="#7c3aed">bit5</text>
|
||||
<text x="900" y="508" font-size="10" fill="#334155">HW_ERR</text>
|
||||
<rect x="1006" y="472" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="1016" y="491" font-size="11" font-weight="700" fill="#94a3b8">bit6</text>
|
||||
<text x="1016" y="508" font-size="10" fill="#334155">—</text>
|
||||
<rect x="1122" y="472" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="1132" y="491" font-size="11" font-weight="700" fill="#94a3b8">bit7</text>
|
||||
<text x="1132" y="508" font-size="10" fill="#334155">—</text>
|
||||
<rect x="1250" y="472" width="340" height="46" rx="8" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.25"/>
|
||||
<text x="1262" y="497" font-size="11" fill="#475569">三类计数增量恒应为 0 → 每次 DBG_ERROR + 自检留痕</text>
|
||||
<text x="40" y="548" font-size="12" font-weight="700" fill="#0f172a">gap 丢帧事件</text>
|
||||
<text x="40" y="566" font-size="10" fill="#64748b">同一函数里的丢帧分支</text>
|
||||
<rect x="310" y="528" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="320" y="547" font-size="11" font-weight="700" fill="#94a3b8">bit0</text>
|
||||
<text x="320" y="564" font-size="10" fill="#334155">—</text>
|
||||
<rect x="426" y="528" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="436" y="547" font-size="11" font-weight="700" fill="#94a3b8">bit1</text>
|
||||
<text x="436" y="564" font-size="10" fill="#334155">—</text>
|
||||
<rect x="542" y="528" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="552" y="547" font-size="11" font-weight="700" fill="#94a3b8">bit2</text>
|
||||
<text x="552" y="564" font-size="10" fill="#334155">—</text>
|
||||
<rect x="658" y="528" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="668" y="547" font-size="11" font-weight="700" fill="#94a3b8">bit3</text>
|
||||
<text x="668" y="564" font-size="10" fill="#334155">—</text>
|
||||
<rect x="774" y="528" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="784" y="547" font-size="11" font-weight="700" fill="#94a3b8">bit4</text>
|
||||
<text x="784" y="564" font-size="10" fill="#334155">—</text>
|
||||
<rect x="890" y="528" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="900" y="547" font-size="11" font-weight="700" fill="#94a3b8">bit5</text>
|
||||
<text x="900" y="564" font-size="10" fill="#334155">—</text>
|
||||
<rect x="1006" y="528" width="112" height="46" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
|
||||
<text x="1016" y="547" font-size="11" font-weight="700" fill="#7c3aed">bit6</text>
|
||||
<text x="1016" y="564" font-size="10" fill="#334155">TPAFE_GAP</text>
|
||||
<rect x="1122" y="528" width="112" height="46" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
|
||||
<text x="1132" y="547" font-size="11" font-weight="700" fill="#7c3aed">bit7</text>
|
||||
<text x="1132" y="564" font-size="10" fill="#334155">STM32_GAP</text>
|
||||
<rect x="1250" y="528" width="340" height="46" rx="8" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.25"/>
|
||||
<text x="1262" y="545" font-size="11" fill="#475569">刻意不告警:超拍是常态(启动突发期即累计 23 次),只累</text>
|
||||
<text x="1262" y="560" font-size="11" fill="#475569">计</text>
|
||||
<text x="40" y="604" font-size="12" font-weight="700" fill="#0f172a">s_health_logged</text>
|
||||
<text x="40" y="622" font-size="10" fill="#64748b">编排层记忆位图(每类一次留痕)</text>
|
||||
<rect x="310" y="584" width="112" height="46" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
|
||||
<text x="320" y="603" font-size="11" font-weight="700" fill="#7c3aed">bit0</text>
|
||||
<text x="320" y="620" font-size="10" fill="#334155">记过</text>
|
||||
<rect x="426" y="584" width="112" height="46" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
|
||||
<text x="436" y="603" font-size="11" font-weight="700" fill="#7c3aed">bit1</text>
|
||||
<text x="436" y="620" font-size="10" fill="#334155">记过</text>
|
||||
<rect x="542" y="584" width="112" height="46" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
|
||||
<text x="552" y="603" font-size="11" font-weight="700" fill="#7c3aed">bit2</text>
|
||||
<text x="552" y="620" font-size="10" fill="#334155">记过</text>
|
||||
<rect x="658" y="584" width="112" height="46" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
|
||||
<text x="668" y="603" font-size="11" font-weight="700" fill="#7c3aed">bit3</text>
|
||||
<text x="668" y="620" font-size="10" fill="#334155">记过</text>
|
||||
<rect x="774" y="584" width="112" height="46" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
|
||||
<text x="784" y="603" font-size="11" font-weight="700" fill="#7c3aed">bit4</text>
|
||||
<text x="784" y="620" font-size="10" fill="#334155">记过</text>
|
||||
<rect x="890" y="584" width="112" height="46" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
|
||||
<text x="900" y="603" font-size="11" font-weight="700" fill="#7c3aed">bit5</text>
|
||||
<text x="900" y="620" font-size="10" fill="#334155">记过</text>
|
||||
<rect x="1006" y="584" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="1016" y="603" font-size="11" font-weight="700" fill="#94a3b8">bit6</text>
|
||||
<text x="1016" y="620" font-size="10" fill="#334155">不记</text>
|
||||
<rect x="1122" y="584" width="112" height="46" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-opacity="0.35"/>
|
||||
<text x="1132" y="603" font-size="11" font-weight="700" fill="#94a3b8">bit7</text>
|
||||
<text x="1132" y="620" font-size="10" fill="#334155">不记</text>
|
||||
<rect x="1250" y="584" width="340" height="46" rx="8" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.25"/>
|
||||
<text x="1262" y="601" font-size="11" fill="#475569"> |= (serious | rise):用同一位号把 SYSLOG 压成每类</text>
|
||||
<text x="1262" y="616" font-size="11" fill="#475569">每次开机一条</text>
|
||||
<rect x="310" y="106" width="16" height="16" rx="4" fill="#f0fdfa" stroke="#0d9488" stroke-width="1.6"/>
|
||||
<text x="332" y="118" font-size="12" fill="#475569">电平位(可自愈)</text>
|
||||
<rect x="458" y="106" width="16" height="16" rx="4" fill="#fff1f2" stroke="#e11d48" stroke-width="1.6"/>
|
||||
<text x="480" y="118" font-size="12" fill="#475569">锁存位(不自愈)</text>
|
||||
<rect x="606" y="106" width="16" height="16" rx="4" fill="#fffbeb" stroke="#d97706" stroke-width="1.6"/>
|
||||
<text x="628" y="118" font-size="12" fill="#475569">边沿事件位(仅当拍)</text>
|
||||
<rect x="778" y="106" width="16" height="16" rx="4" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.6"/>
|
||||
<text x="800" y="118" font-size="12" fill="#475569">消费者掩码命中 / 记忆位</text>
|
||||
<rect x="970" y="106" width="16" height="16" rx="4" fill="#f8fafc" stroke="#94a3b8" stroke-width="1.6"/>
|
||||
<text x="992" y="118" font-size="12" fill="#475569">不适用</text>
|
||||
<rect x="40" y="682" width="1500" height="44" rx="10" fill="#fffbeb" stroke="#d97706" stroke-opacity="0.45"/>
|
||||
<text x="56" y="709" font-size="11" fill="#7c2d12">bit3 / bit4 / bit5 在 events 与 sticky 中刻意用同一位号 → 消费者能把事件位直接套在锁存位图上做每类一次留痕;bit8 以上全 32 位保留未用。详见 docs/算法/采样有效性校验设计说明.md</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,193 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1580" height="1038" viewBox="0 0 1580 1038" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
|
||||
<rect x="0" y="0" width="1580" height="1038" fill="#ffffff"/>
|
||||
<text x="40" y="44" font-size="21" font-weight="700" fill="#0f172a">采样有效性判定时间轴 · 四个工况的 elapsed 与位存续区间</text>
|
||||
<text x="40" y="70" font-size="13" fill="#64748b">elapsed = (int32_t)(now_ms - 最后一次序号前进的时刻);条带 = 位为 1 的存续区间</text>
|
||||
<rect x="430" y="106" width="16" height="12" rx="3" fill="#0d9488"/>
|
||||
<text x="452" y="116" font-size="12" fill="#475569">电平位 flags(可自愈)</text>
|
||||
<rect x="619" y="106" width="16" height="12" rx="3" fill="#e11d48"/>
|
||||
<text x="641" y="116" font-size="12" fill="#475569">锁存位 sticky / 不自愈</text>
|
||||
<rect x="813" y="106" width="16" height="12" rx="3" fill="#d97706"/>
|
||||
<text x="835" y="116" font-size="12" fill="#475569">边沿事件 events(仅当拍)</text>
|
||||
<rect x="1022" y="106" width="16" height="12" rx="3" fill="#e11d48"/>
|
||||
<text x="1044" y="116" font-size="12" fill="#475569">静默门限 100 ms</text>
|
||||
<text x="40" y="178" font-size="13" font-weight="700" fill="#334155">工况与计数</text>
|
||||
<text x="430" y="178" font-size="13" font-weight="700" fill="#334155">静默计时 elapsed(ms)与位存续</text>
|
||||
<text x="1150" y="178" font-size="13" font-weight="700" fill="#334155">关键时刻与判读</text>
|
||||
<rect x="40" y="220" width="370" height="142" rx="10" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.25"/>
|
||||
<text x="54" y="246" font-size="13" font-weight="700" fill="#0f172a">A 对照:健康 1 kHz(d 恒为 1)</text>
|
||||
<text x="54" y="270" font-size="11" fill="#475569">seq: 1000 1001 1002 ... 每拍恰好 +1</text>
|
||||
<text x="54" y="287" font-size="11" fill="#475569">每拍都刷新最后前进时刻 adv_ms</text>
|
||||
<text x="54" y="310" font-size="11" font-weight="700" fill="#0d9488">elapsed = now - adv_ms 恒为 0 → 永不进门限带</text>
|
||||
<rect x="430" y="224" width="690" height="96" fill="#ffffff" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<line x1="430" y1="316.0" x2="1120" y2="316.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="320.0" font-size="10" fill="#64748b" text-anchor="end">0</text>
|
||||
<line x1="430" y1="284.0" x2="1120" y2="284.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="288.0" font-size="10" fill="#64748b" text-anchor="end">50</text>
|
||||
<line x1="430" y1="252.0" x2="1120" y2="252.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="256.0" font-size="10" fill="#64748b" text-anchor="end">100</text>
|
||||
<line x1="430" y1="220.0" x2="1120" y2="220.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="224.0" font-size="10" fill="#64748b" text-anchor="end">150</text>
|
||||
<line x1="430.0" y1="316" x2="430.0" y2="320" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="430.0" y="332" font-size="10" fill="#64748b" text-anchor="middle">0</text>
|
||||
<line x1="602.5" y1="316" x2="602.5" y2="320" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="602.5" y="332" font-size="10" fill="#64748b" text-anchor="middle">50</text>
|
||||
<line x1="775.0" y1="316" x2="775.0" y2="320" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="775.0" y="332" font-size="10" fill="#64748b" text-anchor="middle">100</text>
|
||||
<line x1="947.5" y1="316" x2="947.5" y2="320" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="947.5" y="332" font-size="10" fill="#64748b" text-anchor="middle">150</text>
|
||||
<line x1="1120.0" y1="316" x2="1120.0" y2="320" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="1120.0" y="332" font-size="10" fill="#64748b" text-anchor="middle">200</text>
|
||||
<line x1="430" y1="252.0" x2="1120" y2="252.0" stroke="#e11d48" stroke-width="2" stroke-dasharray="8 5"/>
|
||||
<text x="944" y="246.0" font-size="10" fill="#e11d48">ADC_HEALTH_STALL_MS = 100</text>
|
||||
<polyline points="430.0,316.0 1120.0,316.0" fill="none" stroke="#0d9488" stroke-width="2.6" stroke-linejoin="round"/>
|
||||
<rect x="430" y="328" width="690" height="15" rx="4" fill="#f1f5f9" stroke-opacity="0"/>
|
||||
<rect x="430" y="347" width="690" height="15" rx="4" fill="#f1f5f9" stroke-opacity="0"/>
|
||||
<text x="424" y="339" font-size="10" fill="#64748b" text-anchor="end">flags</text>
|
||||
<text x="424" y="358" font-size="10" fill="#64748b" text-anchor="end">sticky/events</text>
|
||||
<text x="1150" y="238" font-size="11" fill="#334155">判定层对健康态**零输出**:events=0、flags=0、</text>
|
||||
<text x="1150" y="255" font-size="11" fill="#334155">sticky=0。主机单测 TC-ADCHEALTH-002 用 2000 拍</text>
|
||||
<text x="1150" y="272" font-size="11" fill="#334155">扫描把这个结论钉死(健康不误报)。</text>
|
||||
<rect x="40" y="406" width="370" height="142" rx="10" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.25"/>
|
||||
<text x="54" y="432" font-size="13" font-weight="700" fill="#0f172a">B TPAFE 静默:100 ms 检出,恢复只清电平位</text>
|
||||
<text x="54" y="456" font-size="11" fill="#475569">seq: 1000 1001 ... 冻结在 1050</text>
|
||||
<text x="54" y="473" font-size="11" fill="#475569">STM32 车道照常前进(分车道独立判定)</text>
|
||||
<text x="54" y="496" font-size="11" font-weight="700" fill="#0d9488">elapsed 每拍 +1,达到 100 ms 那拍即置位</text>
|
||||
<rect x="430" y="410" width="690" height="96" fill="#ffffff" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<line x1="430" y1="502.0" x2="1120" y2="502.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="506.0" font-size="10" fill="#64748b" text-anchor="end">0</text>
|
||||
<line x1="430" y1="470.0" x2="1120" y2="470.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="474.0" font-size="10" fill="#64748b" text-anchor="end">50</text>
|
||||
<line x1="430" y1="438.0" x2="1120" y2="438.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="442.0" font-size="10" fill="#64748b" text-anchor="end">100</text>
|
||||
<line x1="430" y1="406.0" x2="1120" y2="406.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="410.0" font-size="10" fill="#64748b" text-anchor="end">150</text>
|
||||
<line x1="430.0" y1="502" x2="430.0" y2="506" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="430.0" y="518" font-size="10" fill="#64748b" text-anchor="middle">0</text>
|
||||
<line x1="602.5" y1="502" x2="602.5" y2="506" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="602.5" y="518" font-size="10" fill="#64748b" text-anchor="middle">50</text>
|
||||
<line x1="775.0" y1="502" x2="775.0" y2="506" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="775.0" y="518" font-size="10" fill="#64748b" text-anchor="middle">100</text>
|
||||
<line x1="947.5" y1="502" x2="947.5" y2="506" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="947.5" y="518" font-size="10" fill="#64748b" text-anchor="middle">150</text>
|
||||
<line x1="1120.0" y1="502" x2="1120.0" y2="506" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="1120.0" y="518" font-size="10" fill="#64748b" text-anchor="middle">200</text>
|
||||
<line x1="430" y1="438.0" x2="1120" y2="438.0" stroke="#e11d48" stroke-width="2" stroke-dasharray="8 5"/>
|
||||
<text x="944" y="432.0" font-size="10" fill="#e11d48">ADC_HEALTH_STALL_MS = 100</text>
|
||||
<polyline points="430.0,502.0 775.0,438.0 947.5,406.0" fill="none" stroke="#0d9488" stroke-width="2.6" stroke-linejoin="round"/>
|
||||
<polyline points="947.5,502.0 1120.0,502.0" fill="none" stroke="#0d9488" stroke-width="2.6" stroke-linejoin="round"/>
|
||||
<line x1="775.0" y1="438.0" x2="775.0" y2="506" stroke="#64748b" stroke-width="1" stroke-dasharray="3 3"/>
|
||||
<circle cx="775.0" cy="438.0" r="9" fill="#0f172a"/>
|
||||
<text x="775.0" y="441.6" font-size="10.5" font-weight="700" fill="#ffffff" text-anchor="middle">1</text>
|
||||
<line x1="947.5" y1="406.0" x2="947.5" y2="506" stroke="#64748b" stroke-width="1" stroke-dasharray="3 3"/>
|
||||
<circle cx="947.5" cy="406.0" r="9" fill="#0f172a"/>
|
||||
<text x="947.5" y="409.6" font-size="10.5" font-weight="700" fill="#ffffff" text-anchor="middle">2</text>
|
||||
<rect x="430" y="514" width="690" height="15" rx="4" fill="#f1f5f9" stroke-opacity="0"/>
|
||||
<rect x="430" y="533" width="690" height="15" rx="4" fill="#f1f5f9" stroke-opacity="0"/>
|
||||
<text x="424" y="525" font-size="10" fill="#64748b" text-anchor="end">flags</text>
|
||||
<text x="424" y="544" font-size="10" fill="#64748b" text-anchor="end">sticky/events</text>
|
||||
<rect x="775.0" y="514.0" width="172.5" height="15" rx="4" fill="#0d9488" fill-opacity="0.85"/>
|
||||
<text x="955.5" y="525.0" font-size="10" fill="#0d9488">TPAFE_STALL 电平</text>
|
||||
<rect x="775.0" y="533.0" width="345.0" height="15" rx="4" fill="#e11d48" fill-opacity="0.85"/>
|
||||
<text x="651.9" y="544.0" font-size="10" fill="#e11d48">TPAFE_STALL 锁存留痕</text>
|
||||
<text x="1150" y="424" font-size="11" fill="#334155">1 t=100 ms:ADC_HF_TPAFE_STALL 置位 + 进 sticky;</text>
|
||||
<text x="1150" y="441" font-size="11" fill="#334155"> 闭锁路径 = protectTask 下一拍(间隔 <= 20 ms)</text>
|
||||
<text x="1150" y="458" font-size="11" fill="#334155"> 命中掩码 → prot_elm_init() + 本拍不 eval</text>
|
||||
<text x="1150" y="475" font-size="11" fill="#334155">2 t=150 ms:序号重新前进 → 电平位清、sticky 仍留痕</text>
|
||||
<rect x="40" y="592" width="370" height="142" rx="10" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.25"/>
|
||||
<text x="54" y="618" font-size="13" font-weight="700" fill="#0f172a">C 消费者超拍:d=3 只算丢帧,永不判静默</text>
|
||||
<text x="54" y="642" font-size="11" fill="#475569">seq: 1000 1003 1006 ... 每拍 +3</text>
|
||||
<text x="54" y="659" font-size="11" fill="#475569">生产者活着,消费者没跟上</text>
|
||||
<text x="54" y="682" font-size="11" font-weight="700" fill="#0d9488">d >= 1 → 刷新 adv_ms,elapsed 恒 0;tgap += d-1</text>
|
||||
<rect x="430" y="596" width="690" height="96" fill="#ffffff" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<line x1="430" y1="688.0" x2="1120" y2="688.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="692.0" font-size="10" fill="#64748b" text-anchor="end">0</text>
|
||||
<line x1="430" y1="656.0" x2="1120" y2="656.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="660.0" font-size="10" fill="#64748b" text-anchor="end">50</text>
|
||||
<line x1="430" y1="624.0" x2="1120" y2="624.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="628.0" font-size="10" fill="#64748b" text-anchor="end">100</text>
|
||||
<line x1="430" y1="592.0" x2="1120" y2="592.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="596.0" font-size="10" fill="#64748b" text-anchor="end">150</text>
|
||||
<line x1="430.0" y1="688" x2="430.0" y2="692" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="430.0" y="704" font-size="10" fill="#64748b" text-anchor="middle">0</text>
|
||||
<line x1="602.5" y1="688" x2="602.5" y2="692" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="602.5" y="704" font-size="10" fill="#64748b" text-anchor="middle">50</text>
|
||||
<line x1="775.0" y1="688" x2="775.0" y2="692" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="775.0" y="704" font-size="10" fill="#64748b" text-anchor="middle">100</text>
|
||||
<line x1="947.5" y1="688" x2="947.5" y2="692" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="947.5" y="704" font-size="10" fill="#64748b" text-anchor="middle">150</text>
|
||||
<line x1="1120.0" y1="688" x2="1120.0" y2="692" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="1120.0" y="704" font-size="10" fill="#64748b" text-anchor="middle">200</text>
|
||||
<line x1="430" y1="624.0" x2="1120" y2="624.0" stroke="#e11d48" stroke-width="2" stroke-dasharray="8 5"/>
|
||||
<text x="944" y="618.0" font-size="10" fill="#e11d48">ADC_HEALTH_STALL_MS = 100</text>
|
||||
<polyline points="430.0,688.0 1120.0,688.0" fill="none" stroke="#0d9488" stroke-width="2.6" stroke-linejoin="round"/>
|
||||
<polyline points="430.0,688.0 499.0,688.0 499.0,660.2 637.0,660.2 637.0,632.3 775.0,632.3 775.0,604.5 1120.0,604.5" fill="none" stroke="#d97706" stroke-width="2.4" stroke-linejoin="round"/>
|
||||
<text x="438" y="612" font-size="10" fill="#d97706">tpafe_gaps 累计点数(右轴 0~8)</text>
|
||||
<line x1="499.0" y1="682.2" x2="499.0" y2="692" stroke="#64748b" stroke-width="1" stroke-dasharray="3 3"/>
|
||||
<circle cx="499.0" cy="682.2" r="9" fill="#0f172a"/>
|
||||
<text x="499.0" y="685.8" font-size="10.5" font-weight="700" fill="#ffffff" text-anchor="middle">1</text>
|
||||
<line x1="637.0" y1="682.2" x2="637.0" y2="692" stroke="#64748b" stroke-width="1" stroke-dasharray="3 3"/>
|
||||
<circle cx="637.0" cy="682.2" r="9" fill="#0f172a"/>
|
||||
<text x="637.0" y="685.8" font-size="10.5" font-weight="700" fill="#ffffff" text-anchor="middle">2</text>
|
||||
<rect x="430" y="700" width="690" height="15" rx="4" fill="#f1f5f9" stroke-opacity="0"/>
|
||||
<rect x="430" y="719" width="690" height="15" rx="4" fill="#f1f5f9" stroke-opacity="0"/>
|
||||
<text x="424" y="711" font-size="10" fill="#64748b" text-anchor="end">flags</text>
|
||||
<text x="424" y="730" font-size="10" fill="#64748b" text-anchor="end">sticky/events</text>
|
||||
<rect x="499.0" y="719.0" width="6.9" height="15" rx="4" fill="#d97706" fill-opacity="0.85"/>
|
||||
<text x="513.9" y="730.0" font-size="10" fill="#d97706">ADC_HE_TPAFE_GAP 仅当拍</text>
|
||||
<rect x="637.0" y="719.0" width="6.9" height="15" rx="4" fill="#d97706" fill-opacity="0.85"/>
|
||||
<text x="651.9" y="730.0" font-size="10" fill="#d97706">同上</text>
|
||||
<rect x="775.0" y="719.0" width="6.9" height="15" rx="4" fill="#d97706" fill-opacity="0.85"/>
|
||||
<text x="789.9" y="730.0" font-size="10" fill="#d97706">同上</text>
|
||||
<text x="1150" y="610" font-size="11" fill="#334155">1 t=20 ms:d=3 → tgap += 2 + 当拍事件,下一拍事件消失</text>
|
||||
<text x="1150" y="627" font-size="11" fill="#334155"> (边沿语义,不是电平)</text>
|
||||
<text x="1150" y="644" font-size="11" fill="#334155">2 丢帧不入锁存、不告警、不闭锁;两者口径也不同:tgap</text>
|
||||
<text x="1150" y="661" font-size="11" fill="#334155"> 计帧点数(d-1 加权),adc1_gap 计次数(每次 +1)。</text>
|
||||
<text x="1150" y="678" font-size="11" fill="#334155"> 台架日志:启动突发期累计 23 次,稳态每 10 s +1~2 次</text>
|
||||
<rect x="40" y="778" width="370" height="142" rx="10" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.25"/>
|
||||
<text x="54" y="804" font-size="13" font-weight="700" fill="#0f172a">D 序号倒退:锁存不可自愈,代价是永久闭锁</text>
|
||||
<text x="54" y="828" font-size="11" fill="#475569">seq: 1050 1051 ... 跳到 995</text>
|
||||
<text x="54" y="845" font-size="11" fill="#475569">d = 995 - 1051 < 0(有符号差)</text>
|
||||
<text x="54" y="868" font-size="11" font-weight="700" fill="#0d9488">置位后重新对齐基线,不重复触发;位永不自动清</text>
|
||||
<rect x="430" y="782" width="690" height="96" fill="#ffffff" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<line x1="430" y1="874.0" x2="1120" y2="874.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="878.0" font-size="10" fill="#64748b" text-anchor="end">0</text>
|
||||
<line x1="430" y1="842.0" x2="1120" y2="842.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="846.0" font-size="10" fill="#64748b" text-anchor="end">50</text>
|
||||
<line x1="430" y1="810.0" x2="1120" y2="810.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="814.0" font-size="10" fill="#64748b" text-anchor="end">100</text>
|
||||
<line x1="430" y1="778.0" x2="1120" y2="778.0" stroke="#cbd5e1" stroke-width="0.8" stroke-dasharray="3 4"/>
|
||||
<text x="424" y="782.0" font-size="10" fill="#64748b" text-anchor="end">150</text>
|
||||
<line x1="430.0" y1="874" x2="430.0" y2="878" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="430.0" y="890" font-size="10" fill="#64748b" text-anchor="middle">0</text>
|
||||
<line x1="602.5" y1="874" x2="602.5" y2="878" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="602.5" y="890" font-size="10" fill="#64748b" text-anchor="middle">50</text>
|
||||
<line x1="775.0" y1="874" x2="775.0" y2="878" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="775.0" y="890" font-size="10" fill="#64748b" text-anchor="middle">100</text>
|
||||
<line x1="947.5" y1="874" x2="947.5" y2="878" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="947.5" y="890" font-size="10" fill="#64748b" text-anchor="middle">150</text>
|
||||
<line x1="1120.0" y1="874" x2="1120.0" y2="878" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="1120.0" y="890" font-size="10" fill="#64748b" text-anchor="middle">200</text>
|
||||
<line x1="430" y1="810.0" x2="1120" y2="810.0" stroke="#e11d48" stroke-width="2" stroke-dasharray="8 5"/>
|
||||
<text x="944" y="804.0" font-size="10" fill="#e11d48">ADC_HEALTH_STALL_MS = 100</text>
|
||||
<polyline points="430.0,874.0 1120.0,874.0" fill="none" stroke="#0d9488" stroke-width="2.6" stroke-linejoin="round"/>
|
||||
<line x1="637.0" y1="868.2" x2="637.0" y2="878" stroke="#64748b" stroke-width="1" stroke-dasharray="3 3"/>
|
||||
<circle cx="637.0" cy="868.2" r="9" fill="#0f172a"/>
|
||||
<text x="637.0" y="871.8" font-size="10.5" font-weight="700" fill="#ffffff" text-anchor="middle">1</text>
|
||||
<line x1="1099.3" y1="868.2" x2="1099.3" y2="878" stroke="#64748b" stroke-width="1" stroke-dasharray="3 3"/>
|
||||
<circle cx="1099.3" cy="868.2" r="9" fill="#0f172a"/>
|
||||
<text x="1099.3" y="871.8" font-size="10.5" font-weight="700" fill="#ffffff" text-anchor="middle">2</text>
|
||||
<rect x="430" y="886" width="690" height="15" rx="4" fill="#f1f5f9" stroke-opacity="0"/>
|
||||
<rect x="430" y="905" width="690" height="15" rx="4" fill="#f1f5f9" stroke-opacity="0"/>
|
||||
<text x="424" y="897" font-size="10" fill="#64748b" text-anchor="end">flags</text>
|
||||
<text x="424" y="916" font-size="10" fill="#64748b" text-anchor="end">sticky/events</text>
|
||||
<rect x="637.0" y="886.0" width="483.0" height="15" rx="4" fill="#e11d48" fill-opacity="0.85"/>
|
||||
<text x="507.8" y="897.0" font-size="10" fill="#e11d48">SEQ_BACKWARD 不自动清</text>
|
||||
<rect x="637.0" y="905.0" width="483.0" height="15" rx="4" fill="#e11d48" fill-opacity="0.85"/>
|
||||
<text x="528.8" y="916.0" font-size="10" fill="#e11d48">SEQ_BACKWARD 锁存</text>
|
||||
<text x="1150" y="796" font-size="11" fill="#334155">1 t=60 ms:倒退被检出 → 重新对齐避免每拍重复置位</text>
|
||||
<text x="1150" y="813" font-size="11" fill="#334155">2 电平位与锁存位同时置位 → 保护永久退出,直到</text>
|
||||
<text x="1150" y="830" font-size="11" fill="#334155"> 复位或显式 adc_health_init();这是刻意取舍:</text>
|
||||
<text x="1150" y="847" font-size="11" fill="#334155"> 计数被改写时相量不可信,宁可拒动不误动</text>
|
||||
<rect x="40" y="982" width="1500" height="42" rx="10" fill="#fffbeb" stroke="#d97706" stroke-opacity="0.45"/>
|
||||
<text x="56" y="1008" font-size="11" fill="#7c2d12">横轴 = 距最后一次序号前进的时间(ms,tick 回绕由有符号差值吸收);红线 = ADC_HEALTH_STALL_MS = 100 ms。详见 docs/算法/采样有效性校验设计说明.md</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,120 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1580" height="818" viewBox="0 0 1580 818" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
|
||||
<rect x="0" y="0" width="1580" height="818" fill="#ffffff"/>
|
||||
<text x="40" y="44" font-size="21" font-weight="700" fill="#0f172a">采样有效性校验(adc_health)· 数据流与出口</text>
|
||||
<text x="40" y="70" font-size="13" fill="#64748b">三条量车道平行流过五个阶段;判定层每拍一次、常数次整数运算,输入只是计数快照</text>
|
||||
<rect x="136" y="150" width="268" height="34" rx="8" fill="#f1f5f9" stroke="#475569" stroke-opacity="0.35"/>
|
||||
<text x="148" y="172" font-size="13" font-weight="700" fill="#334155">1. 观测计数(ISR / 驱动侧)</text>
|
||||
<rect x="424" y="150" width="236" height="34" rx="8" fill="#f1f5f9" stroke="#475569" stroke-opacity="0.35"/>
|
||||
<text x="436" y="172" font-size="13" font-weight="700" fill="#334155">2. 本拍计数快照</text>
|
||||
<rect x="680" y="150" width="276" height="34" rx="8" fill="#f1f5f9" stroke="#475569" stroke-opacity="0.35"/>
|
||||
<text x="692" y="172" font-size="13" font-weight="700" fill="#334155">3. 判定(adc_health.c 纯逻辑)</text>
|
||||
<rect x="976" y="150" width="264" height="34" rx="8" fill="#f1f5f9" stroke="#475569" stroke-opacity="0.35"/>
|
||||
<text x="988" y="172" font-size="13" font-weight="700" fill="#334155">4. 出口位图(32 位)</text>
|
||||
<rect x="1260" y="150" width="250" height="34" rx="8" fill="#f1f5f9" stroke="#475569" stroke-opacity="0.35"/>
|
||||
<text x="1272" y="172" font-size="13" font-weight="700" fill="#334155">5. 消费者与处置</text>
|
||||
<rect x="40" y="214" width="82" height="148" rx="10" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.5"/>
|
||||
<text x="81" y="293" font-size="12" font-weight="700" fill="#0d9488" text-anchor="middle">TPAFE</text>
|
||||
<rect x="136" y="214" width="268" height="148" rx="10" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.45"/>
|
||||
<text x="149" y="244" font-size="12" font-weight="700" fill="#0d9488">tpafe5160.c 的 BUSY EXTI</text>
|
||||
<text x="149" y="270" font-size="11" fill="#334155">s_frame_seq++:每读完一帧 +1</text>
|
||||
<text x="149" y="292" font-size="11" fill="#334155">s_frstdata_err++:无跳变帧累计(E-44)</text>
|
||||
<text x="149" y="314" font-size="11" fill="#334155">ISR 内只加计数,不打日志</text>
|
||||
<line x1="404" y1="288.0" x2="415" y2="288.0" stroke="#0d9488" stroke-width="2.2"/>
|
||||
<path d="M 424.0 288.0 L 413.0 282.5 L 413.0 293.5 z" fill="#0d9488"/>
|
||||
<rect x="424" y="214" width="236" height="148" rx="10" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.45"/>
|
||||
<text x="437" y="244" font-size="12" font-weight="700" fill="#0d9488">adcTask 每拍 poll 之后取值</text>
|
||||
<text x="437" y="270" font-size="11" fill="#334155">tpafe_seq</text>
|
||||
<text x="437" y="292" font-size="11" fill="#334155">tpafe_frst_err(uint16 放宽)</text>
|
||||
<text x="437" y="314" font-size="11" fill="#334155">⚠ 必须取本拍 poll 之后的值</text>
|
||||
<line x1="660" y1="288.0" x2="671" y2="288.0" stroke="#0d9488" stroke-width="2.2"/>
|
||||
<path d="M 680.0 288.0 L 669.0 282.5 L 669.0 293.5 z" fill="#0d9488"/>
|
||||
<rect x="680" y="214" width="276" height="148" rx="10" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.45"/>
|
||||
<text x="693" y="244" font-size="12" font-weight="700" fill="#0d9488">序号差值 d = seq - 上次</text>
|
||||
<text x="693" y="270" font-size="11" fill="#334155">d < 0 → 置倒退位并重新对齐</text>
|
||||
<text x="693" y="292" font-size="11" fill="#334155">d > 1 → tgap += d-1 + 当拍事件</text>
|
||||
<text x="693" y="314" font-size="11" fill="#334155">d >= 1 → 刷新「最后前进时刻」</text>
|
||||
<line x1="956" y1="288.0" x2="967" y2="288.0" stroke="#0d9488" stroke-width="2.2"/>
|
||||
<path d="M 976.0 288.0 L 965.0 282.5 L 965.0 293.5 z" fill="#0d9488"/>
|
||||
<rect x="976" y="214" width="264" height="148" rx="10" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.45"/>
|
||||
<text x="989" y="244" font-size="12" font-weight="700" fill="#0d9488">本车道出口</text>
|
||||
<text x="989" y="270" font-size="11" fill="#334155">bit0 ADC_HF_TPAFE_STALL(自愈)</text>
|
||||
<text x="989" y="292" font-size="11" fill="#334155">bit2 ADC_HF_SEQ_BACKWARD(锁存)</text>
|
||||
<text x="989" y="314" font-size="11" fill="#334155">bit6/bit3 ADC_HE_TPAFE_GAP / FRSTDATA</text>
|
||||
<line x1="1240" y1="288.0" x2="1251" y2="288.0" stroke="#0d9488" stroke-width="2.2"/>
|
||||
<path d="M 1260.0 288.0 L 1249.0 282.5 L 1249.0 293.5 z" fill="#0d9488"/>
|
||||
<rect x="1260" y="214" width="250" height="148" rx="10" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.45"/>
|
||||
<text x="1273" y="244" font-size="12" font-weight="700" fill="#0d9488">闭锁 + 留痕 + 计数</text>
|
||||
<text x="1273" y="270" font-size="11" fill="#334155">protectTask 每 20 ms 读 bit0/bit2</text>
|
||||
<text x="1273" y="292" font-size="11" fill="#334155">gap / FRSTDATA 事件 → 编排层日志</text>
|
||||
<text x="1273" y="314" font-size="11" fill="#334155">tgap → 10 s 心跳字段</text>
|
||||
<rect x="40" y="396" width="82" height="148" rx="10" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.5"/>
|
||||
<text x="81" y="475" font-size="12" font-weight="700" fill="#2563eb" text-anchor="middle">STM32</text>
|
||||
<rect x="136" y="396" width="268" height="148" rx="10" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.45"/>
|
||||
<text x="149" y="426" font-size="12" font-weight="700" fill="#2563eb">adc_input.c(DMA2 Stream4)</text>
|
||||
<text x="149" y="452" font-size="11" fill="#334155">s_dma_seq++:半区/整区完成各一次</text>
|
||||
<text x="149" y="474" font-size="11" fill="#334155">s_stm32_gaps++:完成计数一次前进 >1</text>
|
||||
<text x="149" y="496" font-size="11" fill="#334155">s_stm32_errs++:连续 >=2 拍无新帧</text>
|
||||
<text x="149" y="518" font-size="11" fill="#334155">s_stm32_hw_errs++:HAL 错误回调</text>
|
||||
<line x1="404" y1="470.0" x2="415" y2="470.0" stroke="#2563eb" stroke-width="2.2"/>
|
||||
<path d="M 424.0 470.0 L 413.0 464.5 L 413.0 475.5 z" fill="#2563eb"/>
|
||||
<rect x="424" y="396" width="236" height="148" rx="10" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.45"/>
|
||||
<text x="437" y="426" font-size="12" font-weight="700" fill="#2563eb">同一拍的四个 STM32 计数</text>
|
||||
<text x="437" y="452" font-size="11" fill="#334155">stm32_seq / stm32_errs</text>
|
||||
<text x="437" y="474" font-size="11" fill="#334155">stm32_gaps / stm32_hw_errs</text>
|
||||
<text x="437" y="496" font-size="11" fill="#334155">gap 细账不重复取(L1 已算)</text>
|
||||
<line x1="660" y1="470.0" x2="671" y2="470.0" stroke="#2563eb" stroke-width="2.2"/>
|
||||
<path d="M 680.0 470.0 L 669.0 464.5 L 669.0 475.5 z" fill="#2563eb"/>
|
||||
<rect x="680" y="396" width="276" height="148" rx="10" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.45"/>
|
||||
<text x="693" y="426" font-size="12" font-weight="700" fill="#2563eb">序号差值 d + 三处计数增量</text>
|
||||
<text x="693" y="452" font-size="11" fill="#334155">d < 0 → 同一个 SEQ_BACKWARD 位</text>
|
||||
<text x="693" y="474" font-size="11" fill="#334155">d >= 1 → 刷新最后前进时刻</text>
|
||||
<text x="693" y="496" font-size="11" fill="#334155">增量 != 0 → 事件 + 锁存</text>
|
||||
<line x1="956" y1="470.0" x2="967" y2="470.0" stroke="#2563eb" stroke-width="2.2"/>
|
||||
<path d="M 976.0 470.0 L 965.0 464.5 L 965.0 475.5 z" fill="#2563eb"/>
|
||||
<rect x="976" y="396" width="264" height="148" rx="10" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.45"/>
|
||||
<text x="989" y="426" font-size="12" font-weight="700" fill="#2563eb">本车道出口</text>
|
||||
<text x="989" y="452" font-size="11" fill="#334155">bit1 ADC_HF_STM32_STALL(自愈)</text>
|
||||
<text x="989" y="474" font-size="11" fill="#334155">bit2 共用(两车道同一位)</text>
|
||||
<text x="989" y="496" font-size="11" fill="#334155">bit7/bit4/bit5 GAP / ERR / HW_ERR</text>
|
||||
<line x1="1240" y1="470.0" x2="1251" y2="470.0" stroke="#2563eb" stroke-width="2.2"/>
|
||||
<path d="M 1260.0 470.0 L 1249.0 464.5 L 1249.0 475.5 z" fill="#2563eb"/>
|
||||
<rect x="1260" y="396" width="250" height="148" rx="10" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.45"/>
|
||||
<text x="1273" y="426" font-size="12" font-weight="700" fill="#2563eb">闭锁 + 严重事件</text>
|
||||
<text x="1273" y="452" font-size="11" fill="#334155">bit1/bit2 → 同左闭锁路径</text>
|
||||
<text x="1273" y="474" font-size="11" fill="#334155">STM32_ERR / HW_ERR:恒 0 增量</text>
|
||||
<text x="1273" y="496" font-size="11" fill="#334155">出现即 DBG_ERROR + 自检留痕</text>
|
||||
<rect x="40" y="578" width="82" height="148" rx="10" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.5"/>
|
||||
<text x="81" y="657" font-size="12" font-weight="700" fill="#7c3aed" text-anchor="middle">时基</text>
|
||||
<rect x="136" y="578" width="268" height="148" rx="10" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.45"/>
|
||||
<text x="149" y="608" font-size="12" font-weight="700" fill="#7c3aed">osKernelGetTickCount()</text>
|
||||
<text x="149" y="634" font-size="11" fill="#334155">HAL 时基 TIM7,tick = 1 ms</text>
|
||||
<text x="149" y="656" font-size="11" fill="#334155">2^32 ms 后自然回绕</text>
|
||||
<line x1="404" y1="652.0" x2="415" y2="652.0" stroke="#7c3aed" stroke-width="2.2"/>
|
||||
<path d="M 424.0 652.0 L 413.0 646.5 L 413.0 657.5 z" fill="#7c3aed"/>
|
||||
<rect x="424" y="578" width="236" height="148" rx="10" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.45"/>
|
||||
<text x="437" y="608" font-size="12" font-weight="700" fill="#7c3aed">now_ms:本拍唯一一份</text>
|
||||
<text x="437" y="634" font-size="11" fill="#334155">健康判定与心跳上报共用</text>
|
||||
<text x="437" y="656" font-size="11" fill="#334155">不在判定层内再取一次</text>
|
||||
<line x1="660" y1="652.0" x2="671" y2="652.0" stroke="#7c3aed" stroke-width="2.2"/>
|
||||
<path d="M 680.0 652.0 L 669.0 646.5 L 669.0 657.5 z" fill="#7c3aed"/>
|
||||
<rect x="680" y="578" width="276" height="148" rx="10" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.45"/>
|
||||
<text x="693" y="608" font-size="12" font-weight="700" fill="#7c3aed">差值法 (int32_t)(now - adv)</text>
|
||||
<text x="693" y="634" font-size="11" fill="#334155">回绕安全(取有符号差)</text>
|
||||
<text x="693" y="656" font-size="11" fill="#334155">>= ADC_HEALTH_STALL_MS(100)</text>
|
||||
<text x="693" y="678" font-size="11" fill="#334155">可判定窗 ±2^31 ≈ ±24.85 天</text>
|
||||
<line x1="956" y1="652.0" x2="967" y2="652.0" stroke="#7c3aed" stroke-width="2.2"/>
|
||||
<path d="M 976.0 652.0 L 965.0 646.5 L 965.0 657.5 z" fill="#7c3aed"/>
|
||||
<rect x="976" y="578" width="264" height="148" rx="10" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.45"/>
|
||||
<text x="989" y="608" font-size="12" font-weight="700" fill="#7c3aed">tpafe_stall_ms</text>
|
||||
<text x="989" y="634" font-size="11" fill="#334155">静默期内给出停滞时长</text>
|
||||
<text x="989" y="656" font-size="11" fill="#334155">正常态恒为 0</text>
|
||||
<text x="989" y="678" font-size="11" fill="#334155">STM32 侧未上报(观测盲区)</text>
|
||||
<line x1="1240" y1="652.0" x2="1251" y2="652.0" stroke="#7c3aed" stroke-width="2.2"/>
|
||||
<path d="M 1260.0 652.0 L 1249.0 646.5 L 1249.0 657.5 z" fill="#7c3aed"/>
|
||||
<rect x="1260" y="578" width="250" height="148" rx="10" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.45"/>
|
||||
<text x="1273" y="608" font-size="12" font-weight="700" fill="#7c3aed">上升沿日志可读性</text>
|
||||
<text x="1273" y="634" font-size="11" fill="#334155">health: flags=… stall_ms=…</text>
|
||||
<text x="1273" y="656" font-size="11" fill="#334155">回答到底停了多少毫秒</text>
|
||||
<rect x="40" y="756" width="1500" height="46" rx="10" fill="#fffbeb" stroke="#d97706" stroke-opacity="0.45"/>
|
||||
<text x="56" y="784" font-size="12" fill="#7c2d12">判定层零平台依赖(只 include <stdint.h>、不碰寄存器 / 不取数 / 不打日志)→ 主机侧以 #include 原样纳入、免桩全覆盖单测。详见 docs/算法/采样有效性校验设计说明.md</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,582 @@
|
||||
# 采样有效性校验设计说明
|
||||
|
||||
> **本文回答的问题**:`App/algo/adc_health.c` 这 200 行为什么长成现在这样——判据是怎么定下来的、
|
||||
> 每个设计选择换到了什么、代价与盲区在哪里、改它之前必须一起动什么。
|
||||
> **阅读前提**:采样节拍与乒乓双缓冲 → `docs/子系统设计/ADC采样节拍设计说明.md`;
|
||||
> 8 路电气量为何分属两片转换器 → `docs/硬件平台/ADC通道映射说明.md`。
|
||||
> **不在本文范围**:接口签名与全层约定速览在 `docs/算法/采集与算法层设计说明.md`,本文不重复其接口表;
|
||||
> 需求与验收口径登记在 `docs/算法/继电保护功能复现规划.md` §3.1(功能项 L1-6)与同文档 §3.5(L5 自检与闭锁)。
|
||||
> **配图**:三张 SVG 的生成器在 `tools/arch_diagram/`(改图 = 改脚本顶部数据后重跑,勿手改 `.svg`)。
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 三分钟建立心智模型](#1-三分钟建立心智模型)
|
||||
- [2. 它要解决的问题](#2-它要解决的问题)
|
||||
- [3. 在采集链路里的位置](#3-在采集链路里的位置)
|
||||
- [4. 四条设计思想](#4-四条设计思想)
|
||||
- [4.1 判「前进量」,不判「数据合理性」](#41-判前进量不判数据合理性)
|
||||
- [4.2 差值法:把回绕问题一次解决](#42-差值法把回绕问题一次解决)
|
||||
- [4.3 三种位语义:电平 / 锁存 / 事件](#43-三种位语义电平--锁存--事件)
|
||||
- [4.4 纯逻辑换取免桩单测](#44-纯逻辑换取免桩单测)
|
||||
- [5. 判定主干走读](#5-判定主干走读)
|
||||
- [6. 四类判据逐个说透](#6-四类判据逐个说透)
|
||||
- [6.1 静默 stall:门限 100 ms 是怎么定的](#61-静默-stall门限-100-ms-是怎么定的)
|
||||
- [6.2 丢帧 gap:只计数,不告警](#62-丢帧-gap只计数不告警)
|
||||
- [6.3 序号倒退 backward:锁存不可自愈,代价是永久闭锁](#63-序号倒退-backward锁存不可自愈代价是永久闭锁)
|
||||
- [6.4 错误计数增量:事件 + 锁存,但都不闭锁](#64-错误计数增量事件--锁存但都不闭锁)
|
||||
- [7. 位图、掩码与告警编排](#7-位图掩码与告警编排)
|
||||
- [8. 闭锁链路与兜底责任划分](#8-闭锁链路与兜底责任划分)
|
||||
- [9. 状态机视角:静默位的生命周期](#9-状态机视角静默位的生命周期)
|
||||
- [10. 代价与观测盲区](#10-代价与观测盲区)
|
||||
- [11. 怎么验证](#11-怎么验证)
|
||||
- [11.1 最容易读错的一处:「未消费的那次 +1」](#111-最容易读错的一处未消费的那次-1)
|
||||
- [11.2 台架证据](#112-台架证据)
|
||||
- [12. 现场判读手册](#12-现场判读手册)
|
||||
- [13. 自测问题(读完能不能真的懂)](#13-自测问题读完能不能真的懂)
|
||||
- [14. 改它之前必读](#14-改它之前必读)
|
||||
- [15. 相关文档](#15-相关文档)
|
||||
- [变更记录](#变更记录)
|
||||
|
||||
---
|
||||
|
||||
## 1. 三分钟建立心智模型
|
||||
|
||||
**一句话**:本模块不看数据、不看寄存器,只回答一个问题——**两片转换器还在不在往前吐帧**。
|
||||
|
||||
| 维度 | 内容 |
|
||||
|---|---|
|
||||
| 输入 | 7 个 `uint32_t` 计数快照 `adc_health_in_t`(两条车道的序号 + 四类错误计数 + 一份 `now_ms`),由调用方在**本拍 `adc_input_poll()` 之后**取好 |
|
||||
| 输出 | `adc_health_status_t`:`flags` 电平位 / `sticky` 锁存位 / `events` 当拍事件位 / `tpafe_gaps` 累计丢帧点数 / `tpafe_stall_ms` 当前静默时长 |
|
||||
| 内部状态 | 12 个 `static`(1 个基线标志 + 11 个计数、时刻、位图)= **48 B** `.bss`,无堆、无栈上缓冲 |
|
||||
| 依赖 | 只有 `<stdint.h>`:不碰 HAL、不取数、不打日志、不建任何内核对象 |
|
||||
| 调用节奏 | 每拍一次(与 `adc_input_poll()` 同频,1 kHz),常数次整数运算 |
|
||||
|
||||
**它不是**(先把边界钉住,后面每节都从这里出发):
|
||||
|
||||
- **不是采集层**——取数是 `App/algo/adc_input.c` 的事,见 `docs/算法/采集与算法层设计说明.md` §3。
|
||||
- **不是告警器**——日志与留痕的分级编排在 `App/task/adc_task.c` 的 `adc_health_report()`(§7)。
|
||||
- **不是看门狗**——它判不了「消费者自己卡死」,那由 IWDG 兜,见 §8 的责任划分表。
|
||||
- **不是通道自检**——码值越界不在本层(V0.1 无标定,判据不成立,见 §4.1)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 它要解决的问题
|
||||
|
||||
| 故障 | 现场表现 | 没有本层时谁来发现 | 有本层后 |
|
||||
|---|---|---|---|
|
||||
| 节拍源/转换器静默 | 相量停在最后一帧的数值上,看着完全合理 | 只能靠人盯 10 s 一条的心跳日志 | 100 ms 内置电平位 → 保护直接闭锁 |
|
||||
| 消费者超拍丢帧 | 相量窗出现时间空洞,幅值/相位带系统性偏差 | 同上(`adc1_gap` / `tgap` 在心跳里) | 计数 + 当拍事件,**不告警**(§6.2) |
|
||||
| 帧序号倒退 | 序号与数据的对应关系失效,快照可能是两帧混合 | 无 | 锁存位 + 永久闭锁(§6.3) |
|
||||
| 错误计数增量 | 个别帧不可信(FRSTDATA 无跳变、HAL 报错、L1 停摆) | 无(驱动只累计不上报) | 事件 + 锁存,供事后归因(§6.4) |
|
||||
|
||||
最关键的是第一行。**节拍停而任务照跑时,上层拿到的是旧数据而不是空数据**:`App/task/adc_task.c`
|
||||
只在 `adc_input_poll()` 返回 1 时才压环重算,于是 L2 相量**保持最后一帧**;`App/task/protect_task.c`
|
||||
每 20 ms 读到的 rms 数值合理、`phasor_get()` 返回 0(成功),定时限判据却在这条冻住的 rms 上继续
|
||||
累计延时——这比「没有数据」危险得多。**本层的全部价值就是把「数据是否新鲜」变成一个可以被
|
||||
保护的消费者直接消费的电平量**。
|
||||
|
||||
---
|
||||
|
||||
## 3. 在采集链路里的位置
|
||||
|
||||

|
||||
|
||||
| 层 | 落点 | 在本机制里只负责什么 |
|
||||
|---|---|---|
|
||||
| 驱动 ISR | `Drivers/BSP/TPAFE5160/tpafe5160.c` | BUSY EXTI 每读完一帧 `s_frame_seq++`;无跳变帧只累计 `s_frstdata_err` |
|
||||
| L1 采集 | `App/algo/adc_input.c` | DMA 半区/整区完成计数 + 自算丢帧(次数)/停摆(连续 >=2 拍无新帧)/HAL 错误四类计数 |
|
||||
| 快照装配 | `App/task/adc_task.c` | **本拍 poll 之后**把 6 个计数 + 一份 `now_ms` 填进 `adc_health_in_t` |
|
||||
| 判定 | `App/algo/adc_health.c` | 只做判定与位图维护;不知道寄存器、不知道日志 |
|
||||
| 编排 | `App/task/adc_task.c` | 按位分级:`DBG_ERROR` / `SYSLOG_SELFTEST` / `DBG_INFO`(恢复) |
|
||||
| 闭锁 | `App/task/protect_task.c` | 每 20 ms 读 `adc_health_flags()`,命中掩码即复位元件并停判据(L5-4) |
|
||||
|
||||
两条边界是刻意的,破坏它们的代价写在右边:
|
||||
|
||||
1. **ISR 侧只加计数,不在中断里打日志**——FRSTDATA 异常只累计(E-44),帧序号只自增(E-45)。
|
||||
中断里做重活会直接推后下一拍采样,见 `docs/子系统设计/ADC采样节拍设计说明.md` §5。
|
||||
2. **判定层不许出现 HAL / 日志 / 取数**——`test/host/adc_health/adc_health_test.c` 用 `#include`
|
||||
原样纳入生产源文件、**零桩**跑全套。判定层里多一个 `DBG_ERROR`,主机侧就要为它写一份桩,
|
||||
「离线穷举故障」这条唯一的验证路径随之失效(§11)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 四条设计思想
|
||||
|
||||
### 4.1 判「前进量」,不判「数据合理性」
|
||||
|
||||
| 候选判据 | 为什么不用 |
|
||||
|---|---|
|
||||
| 码值越界 / 超量程 | L1-5 通道标定未实现(`docs/算法/继电保护功能复现规划.md` §3.1),码值还没有工程量含义;且两片转换器增益不同(TPAFE 16 位双极性 / STM32 12 位单极性),不存在统一阈值 |
|
||||
| 波形合理性(方差、过零、单调) | 这是「信号对不对」,属 L5-2 的另一半;空载与零值都是合法工况,判据只能靠猜阈值 |
|
||||
| **序号是否前进** | 唯一与信号内容无关的新鲜度证据:不管输入是正弦、零值还是断线,正常态就是每拍 +1 |
|
||||
|
||||
于是本层只看 5 个计数器加 1 个时基。这也是它能保持纯逻辑、能被穷举测试的根因——**判据越朴素,
|
||||
可测面越完整**。
|
||||
|
||||
### 4.2 差值法:把回绕问题一次解决
|
||||
|
||||
两侧序号都是 `uint32_t` 自由 running 计数,判定只看差值:
|
||||
|
||||
```c
|
||||
d = in->tpafe_seq - s_tpafe_seq; /* 无符号减,自然吸收 2^32 回绕 */
|
||||
if ((int32_t)d < 0) { /* 倒退 */ } /* 借符号位区分「倒退」与「前进很多」 */
|
||||
```
|
||||
|
||||
| 无符号差值 d | 按 `(int32_t)` 读 | 判定动作 |
|
||||
|---|---|---|
|
||||
| 0 | 0 | 没前进:静默计时继续长大(§6.1) |
|
||||
| 1 | 1 | 正常:刷新「最后前进时刻」 |
|
||||
| 2 .. 2^31-1 | 正 | 丢帧:生产者活着、消费者没跟上(§6.2) |
|
||||
| 2^31 .. 0xFFFFFFFF | 负 | 单调性破坏,锁存(§6.3) |
|
||||
|
||||
任何「先判大小、再判是否跨过 0」的写法都要为「这次跳变是不是回绕」写特例;取有符号差值后,
|
||||
跨 0 的正常推进天然落在 `d = 1` 那一格,**特例消失**。代价是一个对称的放弃面:可判定窗为
|
||||
±2^31(1 kHz 下约 ±24.85 天),超过该跨度的倒退会被读成前进。这个场景被有意放弃——它要求
|
||||
计数器一次性被改写 24 天以上的量,真到那一步,问题已经不在这条判据的职责范围内。
|
||||
|
||||
`now_ms` 同理用 `(int32_t)` 差值,因此 1 ms tick 的 49.7 天回绕不需要任何特殊分支;
|
||||
`TC-ADCHEALTH-009` 把「跨 0xFFFFFFFF 扫 512 拍不误报 + 回绕后冻结仍要报」钉成回归。
|
||||
|
||||
### 4.3 三种位语义:电平 / 锁存 / 事件
|
||||
|
||||
| 类别 | 语义 | 谁清 | 典型消费者 | 如果只留这一种 |
|
||||
|---|---|---|---|---|
|
||||
| 电平位 `flags` | 当前是否正在故障 | 条件消失自动清(stall);倒退位不清 | `protectTask` 每 20 ms 闭锁判定 | 现场偶发一次 30 ms 停摆后自愈 → 事后查不到任何痕迹 |
|
||||
| 锁存位 `sticky` | 本次开机内是否出现过 | 只有 `adc_health_init()` | 自检留痕、UI、事后归因 | 一次瞬态就会把保护永久闭锁 |
|
||||
| 事件位 `events` | 仅本次调用发生 | 出参,不落内部状态 | 编排层打日志、去重 | 20 ms 轮询的消费者大概率错过只活 1 ms 的那一拍 |
|
||||
|
||||
三者不是冗余,而是**三种时间尺度**:ms 级(事件)/ 拍级持续(电平)/ 开机级(锁存)。
|
||||
把三种语义塞进一个 `uint32_t` 的代价是调用方要背注释;分开三张位图后,注释只剩一句
|
||||
「取哪张位图对应哪种时间尺度」——§7 的掩码表就是照这张表写的。
|
||||
|
||||
### 4.4 纯逻辑换取免桩单测
|
||||
|
||||
入参是「值」而不是「指针 + 接口」,因此测试可以把时间与计数**任意摆**:
|
||||
|
||||
```c
|
||||
in.now_ms = 0xFFFFFF00u; /* 直接站在 tick 回绕边界上 */
|
||||
in.tpafe_seq = 995u; /* 人为制造倒退 */
|
||||
```
|
||||
|
||||
这些工况在台架上要靠「拔芯片 / 改驱动」才造得出来,而且造出来也说不清落在哪一拍、
|
||||
是否恰好跨过门限。**故障注入的可复现性 + 边界的可精确性**是这一层被单独拆成纯逻辑文件的
|
||||
直接动因(完整理由见 `test/host/adc_health/adc_health_test.c` 文件头)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 判定主干走读
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
IN["本拍 poll 之后的计数快照"] --> NUL{"in 为空 ?"}
|
||||
NUL -- "是:本拍整段跳过,out 一个字段都不写" --> NEXT
|
||||
NUL -- 否 --> PRIME{"基线已建立 ?"}
|
||||
PRIME -- "否:只记基线与两侧最后前进时刻" --> OUT
|
||||
PRIME -- 是 --> D1["算 TPAFE 序号差值 d"]
|
||||
D1 --> B1{"d < 0 ?"}
|
||||
B1 -- "是:置 SEQ_BACKWARD 电平+锁存,并用新值重新对齐基线" --> D2
|
||||
B1 -- 否 --> G1{"d > 1 ?"}
|
||||
G1 -- "是:tgap += d-1,置当拍丢帧事件" --> A1
|
||||
G1 -- 否 --> A1{"d >= 1 ?"}
|
||||
A1 -- "是:刷新 TPAFE 最后前进时刻" --> D2["算 STM32 序号差值 d"]
|
||||
A1 -- "否:不改任何状态,静默计时继续长大" --> D2
|
||||
D2 --> A2{"STM32 侧 d < 0 或 d >= 1 ?"}
|
||||
A2 -- "d < 0:同一个 SEQ_BACKWARD 位(两车道共用)" --> ST
|
||||
A2 -- "d >= 1:刷新 STM32 最后前进时刻" --> ST
|
||||
A2 -- "d = 0:本拍什么都不改" --> ST{"两侧 elapsed >= 100 ms ?"}
|
||||
ST -- "是:置 stall 电平位并进 sticky" --> ERR["四类错误计数做增量比较"]
|
||||
ST -- "否:清 stall 电平位(自愈路径)" --> ERR
|
||||
ERR --> OUT["写 out 四字段与 tpafe_stall_ms"]
|
||||
OUT --> RPT["adc_health_report() 编排层分级处置"]
|
||||
RPT --> NEXT["等下一拍 TIM3 唤醒"] --> IN
|
||||
```
|
||||
|
||||
| 步 | 条件 | 动作 | 关键:不动什么 |
|
||||
|---|---|---|---|
|
||||
| 1 建基线 | `s_primed == 0` | 记 6 个计数与 `now_ms`,两侧前进时刻都设为本拍 | **不判定、不产生事件**——开机首拍没有参照系(此刻 `tpafe5160_frame_seq()` 已非 0,因为 TIM3 比调度器早约 100 ms 就在生产帧) |
|
||||
| 2 TPAFE 差值 | `d < 0` / `d > 1` / `d >= 1` | 见图中三个分支 | 倒退分支**只重新对齐序号**,不动错误计数基线,避免一次倒退顺手抹掉别的证据 |
|
||||
| 3 STM32 差值 | 同一口径,但不在此累计丢帧 | 前进/倒退两态 | **不重算 gap**:L1 已有细账 `adc_input_stm32_gaps()`,两处各算一套就会打架(§6.2) |
|
||||
| 4 静默判定 | `elapsed >= 100` | 置电平位并进 sticky;否则**清**电平位 | 判定排在前进更新之后 → 同一拍内「前进」与「静默」互斥,不会出现刚推进又报静默 |
|
||||
| 5 错误增量 | 四个计数各自 `!=` 基线 | 事件位 + 锁存位,并更新基线 | 用 `!=` 不用 `>`:见 §6.4 |
|
||||
| 6 出参 | `out != 0` | 写 4 个字段 + `tpafe_stall_ms`(非静默态给 0) | 事件位只在本次出参里出现,绝不写进 `flags`/`sticky` |
|
||||
|
||||
三条容易被漏掉的行为:
|
||||
|
||||
1. `in == NULL` **提前返回**,`out` 保持原值(不是写全 0)——调用方复用同一个结构体时看不到被抹平的历史。
|
||||
2. `out == NULL` 仍照常更新内部状态——支持「只喂状态、不取结果」的调用法(`TC-ADCHEALTH-010` 钉住)。
|
||||
3. **首拍也会写 `out`**(全 0):`events` 在首拍恒为 0,`flags` 反映当前电平(首拍必为 0)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 四类判据逐个说透
|
||||
|
||||

|
||||
|
||||
四块面板分别是:A 健康对照、B 静默检出与自愈、C 消费者超拍、D 序号倒退。红线 =
|
||||
`ADC_HEALTH_STALL_MS` = 100 ms;下方两条条带 = 位为 1 的存续区间(上 = `flags`,
|
||||
下 = `sticky` / `events`)。
|
||||
|
||||
### 6.1 静默 stall:门限 100 ms 是怎么定的
|
||||
|
||||
```c
|
||||
if ((uint32_t)(int32_t)(in->now_ms - s_tpafe_adv_ms) >= ADC_HEALTH_STALL_MS) { 置位并留痕 }
|
||||
else { 清电平位 } /* 只有 stall 类有 else 分支,SEQ_BACKWARD 没有 */
|
||||
```
|
||||
|
||||
门限的三条独立依据(缺一条就不成立):
|
||||
|
||||
| 依据 | 内容 |
|
||||
|---|---|
|
||||
| 判据本身不需要为抖动留余量 | `d = 0` 表示生产者这一拍**没有产出新帧**。消费者慢只会让 `d > 1`,永远不会让 `d = 0`——所以门限不是为了躲抖动,而是为了把「还能解释成别的」的空间压到 0 |
|
||||
| 与工频窗对齐 | 1 kHz 下 100 拍 = 100 ms = 5 个 50 Hz 周波 = 连续 5 个全周波 DFT 窗没有任何新点 → 此刻相量必然是 5 周波之前的旧值,任何电气量判据都不可信 |
|
||||
| 与兜底层级成阶梯 | 本层 100 ms → `protectTask` 20 ms 一拍 → 最坏 120 ms 完成闭锁;再往下是看门狗层级(100 ms 抽样、连续 800 ms 缺席起停喂、IWDG 标称 2498 ms / LSI 偏快约 1701 ms 溢出)。100 ms 既不去抢看门狗的活,也不让保护拿着旧值走进动作时限 |
|
||||
|
||||
- **边界含等号**:恰好 100 ms 那一拍即报,检测延迟 = 门限 + 最多 1 拍采样相位;
|
||||
`TC-ADCHEALTH-003` 把 99 ms 不报 / 100 ms 必报钉成回归。
|
||||
- **恢复语义**:只清电平位,`sticky` 永久留痕——「曾经停摆过」是自检要的东西。
|
||||
- **两车道独立判定**:一片转换器停而另一片照跑,只报对应那一位(`TC-ADCHEALTH-004`)。
|
||||
位号分开正是为了把故障定位到「哪一侧」,合并成一位就只剩「采集坏了」。
|
||||
- 门限是**策略参数不是物理常数**:改它只需同步 §11 的三条 TC 与本文,不必动判据结构。
|
||||
|
||||
### 6.2 丢帧 gap:只计数,不告警
|
||||
|
||||
`d > 1` 的语义是「生产者活着,消费者没跟上」:中间那些帧已被双缓冲覆盖。判定层
|
||||
`s_tpafe_gaps += (d - 1)` 并发一个**当拍**事件位。
|
||||
|
||||
| 车道 | 丢帧证据来源 | 单位 | 为什么这样分 |
|
||||
|---|---|---|---|
|
||||
| TPAFE | 判定层自算(驱动只暴露 `tpafe5160_frame_seq()`) | **帧点数**:按 `d - 1` 加权 | 驱动侧除帧序号外没有别的观测点,判定层手里的 seq 是唯一线索 |
|
||||
| STM32 | L1 的 `adc_input_stm32_gaps()`,判定层只看它的增量 | **次数**:每次事件 +1 | L1 在消费点上直接看得到 DMA 完成计数,细账归它;判定层再算一套就是两处判据打架 |
|
||||
|
||||
⚠ 读心跳日志时**别把 `tgap` 与 `adc1_gap` 当同一单位**(一个是点、一个是次),
|
||||
字段口径见 `docs/子系统设计/ADC采样节拍设计说明.md` §8。
|
||||
|
||||
为什么刻意不告警、不锁存、不闭锁:消费者超拍是**常态**。同一批台架日志里,
|
||||
启动日志突发期 `adc1_gap` 已累计 23 次,稳态每 10 s 心跳 +1~2 次(`tgap` 1 → 2)。
|
||||
这类事件若进 `DBG_ERROR` + `SYSLOG_SELFTEST`,开机噪声会把真故障完全盖掉,还会持续吃掉
|
||||
32 KB 的 `selftest.log` 配额。真正要紧张的信号是组合而非事件本身:`tgap` 持续快速上涨
|
||||
**且** `[PROF]` 行的整拍耗时超过 1 ms → 相量窗里出现时间空洞(探针字段与基线见
|
||||
`docs/子系统设计/看门狗设计说明.md` §11.4)。
|
||||
|
||||
### 6.3 序号倒退 backward:锁存不可自愈,代价是永久闭锁
|
||||
|
||||
`d < 0` 时同时置 `flags` 与 `sticky` 的 `ADC_HF_SEQ_BACKWARD`,然后**用新序号重新对齐基线**,
|
||||
且**没有清除分支**。三个动作各有各的理由:
|
||||
|
||||
| 动作 | 理由 |
|
||||
|---|---|
|
||||
| 同时置电平位 | 让 `protectTask` 立刻闭锁——单调性破坏意味着帧序号与缓冲内容的对应关系不再可信,快照可能是两帧混合,此时任何电气量判据都不可信 |
|
||||
| 置位后重新对齐 | 若不重新对齐,基线永远大于当前序号,之后的正常推进会长期被读成异常,`tgap` 与静默计时同时被污染 |
|
||||
| 不做自愈清除 | 「后面恢复正常」不能证明计数器已经可信;能证明它可信的只有重启或显式 `adc_health_init()`。取舍方向与保护一致:**宁拒动不误动** |
|
||||
|
||||
现实里造成倒退的三条路径:驱动改了自增时机;计数器被别的代码写坏;**运行期重跑了会清零
|
||||
计数的 `*_init()` 而没有同时 `adc_health_init()`**——`App/algo/adc_input.c` 的 init 会把
|
||||
`s_dma_seq` 归零,判定层看到的就是从大值跳回小值,**第一拍即判倒退并永久闭锁**。当前启动
|
||||
序列里两个 init 都只执行一次(驱动在内核前、判定在任务入口),因此撞不上;改这条链时它是
|
||||
头号陷阱(§14)。
|
||||
|
||||
倒退还有一条必须知道的**可判定窗**边界:`(int32_t)` 解读只在 ±2^31 内单调,1 kHz 下约
|
||||
±24.85 天;倒退幅度超过这个跨度会被读成前进(§4.2 已说明这是有意放弃的场景)。
|
||||
|
||||
### 6.4 错误计数增量:事件 + 锁存,但都不闭锁
|
||||
|
||||
| 计数(快照字段) | 生产者 | 正常增量 | 出现增量意味着 | 事件位 | 锁存位 | 闭锁 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `tpafe_frst_err` | TPAFE BUSY EXTI | 0 | 该帧 8 个通道不可信(FRSTDATA 无跳变,E-44) | `ADC_HE_FRSTDATA` | `ADC_HF_FRSTDATA` | 否 |
|
||||
| `stm32_errs` | L1 消费侧 | 0 | 连续 >=2 拍无新帧,或 DMA 挂载失败 | `ADC_HE_STM32_ERR` | `ADC_HF_STM32_ERR` | 否 |
|
||||
| `stm32_gaps` | L1 消费侧 | 少量 | 超拍丢帧(§6.2) | `ADC_HE_STM32_GAP` | 不入锁存 | 否 |
|
||||
| `stm32_hw_errs` | HAL ADC/DMA 错误回调 | 0 | 硬件层面报过错 | `ADC_HE_HW_ERR` | `ADC_HF_HW_ERR` | 否 |
|
||||
|
||||
- **用 `!=` 而不是 `>`**:① 驱动侧一次 `+N` 的跳变不会因为「已经大于基线」被吞掉;
|
||||
② 计数器被归零(例如重跑 init)也能立刻看到,而不是等它慢慢爬回原值。
|
||||
- **`uint16` → `uint32` 的放宽**:`tpafe5160_frstdata_err()` 是 `uint16_t` 且在 0xFFFF 饱和,
|
||||
快照字段放宽到 `uint32_t` 是为了不让「截断」变成一条假增量。
|
||||
- **为什么不闭锁**:一次性事件痕迹不等于「当前测量不可信」;闭锁权只交给电平类
|
||||
(判据见 `App/task/protect_task.c` 的 `PROT_HF_LOCK_MASK`)。
|
||||
- **两层如何互补**:HAL 报错本身不足以停保护,但它之后大概率紧跟 `STM32_STALL` ——
|
||||
锁存位负责事后把「先是 hw 报错、随后静默」这条因果链复原出来。
|
||||
|
||||
---
|
||||
|
||||
## 7. 位图、掩码与告警编排
|
||||
|
||||

|
||||
|
||||
| 位 | 名称 | 类别 | 置位条件 | 清除条件 |
|
||||
|---|---|---|---|---|
|
||||
| 0 | `ADC_HF_TPAFE_STALL` | 电平 | TPAFE 车道 elapsed >= 100 ms | elapsed < 100 ms |
|
||||
| 1 | `ADC_HF_STM32_STALL` | 电平 | STM32 车道 elapsed >= 100 ms | elapsed < 100 ms |
|
||||
| 2 | `ADC_HF_SEQ_BACKWARD` | 电平 + 锁存 | 任一侧 `(int32_t)d < 0` | 仅 `adc_health_init()` |
|
||||
| 3 | `ADC_HF_FRSTDATA` / `ADC_HE_FRSTDATA` | 锁存 + 事件 | `tpafe_frst_err` 有变化 | 仅 `adc_health_init()` |
|
||||
| 4 | `ADC_HF_STM32_ERR` / `ADC_HE_STM32_ERR` | 锁存 + 事件 | `stm32_errs` 有变化 | 仅 `adc_health_init()` |
|
||||
| 5 | `ADC_HF_HW_ERR` / `ADC_HE_HW_ERR` | 锁存 + 事件 | `stm32_hw_errs` 有变化 | 仅 `adc_health_init()` |
|
||||
| 6 | `ADC_HE_TPAFE_GAP` | 仅事件 | 本次 TPAFE `d > 1` | 出参,不落状态 |
|
||||
| 7 | `ADC_HE_STM32_GAP` | 仅事件 | `stm32_gaps` 有变化 | 出参,不落状态 |
|
||||
| 8..31 | 保留 | — | 未使用 | — |
|
||||
|
||||
**位号对齐是刻意设计**:bit3 / bit4 / bit5 在 `events` 与 `sticky` 里编号相同,编排层因此可以
|
||||
拿事件位直接去操作「已留痕位图」,一行搞定每类每次开机只留痕一次:
|
||||
|
||||
```c
|
||||
if (((serious | rise) & ~s_health_logged) != 0u) { /* 位图当去重键 */
|
||||
(void)syslog_post(SYSLOG_SELFTEST, "adc sampling fault");
|
||||
s_health_logged |= (serious | rise);
|
||||
}
|
||||
```
|
||||
|
||||
代价也随之而来:新增一个故障位必须一次想清楚**三张位图 + 编排层记忆位图**的归属,
|
||||
漏一格就可能出现「事件报了但永远不去重」或「锁存位与事件位串位」。规则写在 §14。
|
||||
|
||||
编排层的三级策略(`adc_health_report()`,`App/task/adc_task.c`):
|
||||
|
||||
| 级别 | 触发 | 动作 | 为什么这样分 |
|
||||
|---|---|---|---|
|
||||
| 严重事件 | `events` 命中 bit3/bit4/bit5 | 每次都 `DBG_ERROR`(带 `events` 与 `sticky`) | 这三类计数增量恒应为 0,出现即故障,且不会连续刷屏 |
|
||||
| 电平上升沿 | `flags & ~prev` | `DBG_ERROR`(带 `stall_ms` 与 `tgap`) | 只报一次沿,持续 5 s 的停摆也只有一条错误行;`stall_ms` 直接回答「停多久」 |
|
||||
| 电平下降沿 | `prev & ~flags` | `DBG_INFO`(恢复) | 「恢复」必须可见,否则只看到一次错误、无法判断现在还在不在故障 |
|
||||
| 自检留痕 | 上述两类首次出现 | `SYSLOG_SELFTEST` 一条,按位去重 | 持续故障若逐条写会把 32 KB `selftest.log` 写满,并淹没别的模块 |
|
||||
| 丢帧 | bit6 / bit7 | **不处置** | §6.2 的实测理由 |
|
||||
|
||||
两个容易忽略的细节:
|
||||
|
||||
- 控制台日志受 `dbg_console_writer_ready()` 门控(写者注册前直发会与 `defaultTask` 的存储初始化
|
||||
日志字节级交错),但 **`SYSLOG_SELFTEST` 不受门控**——内核前有 `s_preboot` 缓冲,
|
||||
开机窗口里的故障痕迹不能丢。
|
||||
- 每条 `DBG_*` 约 90 B Flash + 216 B 栈(每个调用点内联行缓冲与时间戳缓冲,见根 `AGENTS.md`
|
||||
平台约束)。**「判定层不打日志」不只为了可测性,也因为日志在 1 kHz 判定路径上是奢侈品**:
|
||||
编排把成本集中到少数几个沿上。
|
||||
|
||||
---
|
||||
|
||||
## 8. 闭锁链路与兜底责任划分
|
||||
|
||||
`App/task/protect_task.c` 每 20 ms 读一次电平位:
|
||||
|
||||
```c
|
||||
#define PROT_HF_LOCK_MASK (ADC_HF_TPAFE_STALL | ADC_HF_STM32_STALL | ADC_HF_SEQ_BACKWARD)
|
||||
```
|
||||
|
||||
| 时刻 | 事件 |
|
||||
|---|---|
|
||||
| t = 0 | 生产者最后一次让序号前进(此后无新帧) |
|
||||
| t = 100 ms | 判定层置 stall 电平位并进 `sticky`;编排层打一条 `DBG_ERROR` + 一条自检留痕 |
|
||||
| t <= 120 ms | `protectTask` 下一拍命中掩码 → `prot_elm_init()` 复位全部元件 + 本拍不跑判据;`SOE_T_SAMPLE_BLOCK`(value = `hf & PROT_HF_LOCK_MASK`,能反查是哪一位) |
|
||||
| 恢复时 | 序号重新前进 → 电平位清 → 下一拍自动复归,`SOE_T_SAMPLE_RESUME` + `DBG_INFO` |
|
||||
|
||||
**谁兜住什么**——这张表是本层与看门狗的分界,也是最常见的误读点:
|
||||
|
||||
| 故障位置 | 谁先发现 | 多久 | 结果 |
|
||||
|---|---|---|---|
|
||||
| TIM3 节拍源停 | **不是本层**:`adcTask` 永久阻塞在 `osThreadFlagsWait`,没人跑判定 | 100 ms 抽样 + 连续 800 ms 缺席起停喂 | IWDG 溢出复位(标称 2498 ms,LSI 偏快约 1701 ms) |
|
||||
| TPAFE BUSY EXTI 停 | 本层 `ADC_HF_TPAFE_STALL` | 100 ms | 闭锁保护(任务仍活着,不复位) |
|
||||
| ADC1 / DMA 停 | 本层 `ADC_HF_STM32_STALL`,L1 的 `stm32_errs` 增量同时给出事件 | 100 ms | 闭锁 + 留痕 |
|
||||
| HAL 报错 | L1 计数 → 本层 `HE_HW_ERR` | 下一拍 | 事件 + 锁存,不闭锁 |
|
||||
| `adcTask` 超拍 | 本层 `HE_TPAFE_GAP` + L1 `adc1_gap` | 当拍 | 只计数 |
|
||||
| 帧序号倒退 | 本层 `SEQ_BACKWARD` | 当拍 | 闭锁且不自愈,直到复位 |
|
||||
|
||||
一句话概括本层的**能力边界**:它判的是「生产者相对消费者是否前进」,因此**判不了消费者自己**
|
||||
是否还活着。后者必须换一个独立机制(IWDG)来实现——两层不能互相替代,也不要指望把门限调小
|
||||
就能替代它。
|
||||
|
||||
**如实登记的已知窗口**:TIM3 停摆到 IWDG 复位之间(最坏约 800 ms + 2498 ms ≈ 3.3 s,标称值),
|
||||
`protectTask` 仍在 20 ms 轮询、电平位却还没人置(判定随消费者一起停了),相量保持旧值参与判据。
|
||||
V0.1 没有出口硬件,后果限于遥信与日志;真要压这个窗口,正解是给相量侧加「帧年龄」判据
|
||||
(让 `prot_elm` 自己看数据新鲜度),而不是把 stall 门限调小。
|
||||
|
||||
---
|
||||
|
||||
## 9. 状态机视角:静默位的生命周期
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
ADVANCING : elapsed = 0,flags 为 0
|
||||
TIMING : 0 < elapsed < 100 ms,仍不报
|
||||
STALLED : TPAFE_STALL = 1(同时进 sticky)
|
||||
[*] --> ADVANCING : 首拍建基线
|
||||
ADVANCING --> TIMING : 本拍 d = 0
|
||||
TIMING --> ADVANCING : 序号重新前进
|
||||
TIMING --> STALLED : elapsed 达 100 ms(含等号)
|
||||
STALLED --> ADVANCING : 序号重新前进<br/>电平位清,sticky 永久留痕
|
||||
STALLED --> STALLED : 继续无前进,elapsed 每拍 +1
|
||||
note right of STALLED
|
||||
SEQ_BACKWARD 不在这台状态机里:
|
||||
它是前进 -> 已破坏的终态,无自愈路径
|
||||
end note
|
||||
```
|
||||
|
||||
| 状态 | `flags` | `sticky` | 编排层可见动作 |
|
||||
|---|---|---|---|
|
||||
| 前进中 | 0 | 不变 | 若上一拍为 1 → 打一条恢复 `DBG_INFO` |
|
||||
| 计时中 | 0 | 不变 | 无 |
|
||||
| 停摆 | 1 | 1(首次置位时留痕一次) | 上升沿 `DBG_ERROR` + 自检一条 |
|
||||
|
||||
倒退位是**另一台状态机**:前进 → 已破坏(终态),没有任何路径回到前进——这正是 §6.3 的取舍。
|
||||
把两台机器画在一张图上是常见的误读来源(会让人以为 `SEQ_BACKWARD` 也会自愈),
|
||||
所以 §14 的清单要求:任何新位都必须先写明它属于哪一类。
|
||||
|
||||
---
|
||||
|
||||
## 10. 代价与观测盲区
|
||||
|
||||
| 项 | 实测/核算 | 说明 |
|
||||
|---|---|---|
|
||||
| 静态 RAM | 48 B(11 个 `uint32_t` = 44 B + 1 个 `uint8_t` 与对齐 4 B) | 判定层的全部驻留成本;无堆分配(自研代码禁 `malloc`,见根 `AGENTS.md` 平台约束) |
|
||||
| 每拍 CPU | 约 20 条以内的整数加减与比较;无乘除、无浮点、无数组访问 | 与 1 kHz 节拍同频,量级远小于一次 `adc_input_poll()`(实测 8 us) |
|
||||
| 栈 | 判定本体只有 3 个局部量;最坏链在编排层的日志调用尾 | 判定层的日志尾是 `adcTask` 这条链加深的起点,曾直接触发抬栈一档;当前值与余量台账见 `docs/内存管理/RAM使用分析与优化方案.md` §1.4 |
|
||||
| Flash | 主要花在编排层的日志调用点,判定层本身很小 | 每条 `DBG_*` 约 90 B Flash,故新增位**不要**顺手加日志 |
|
||||
|
||||
**观测盲区**(如实登记,别在别处再假设它们存在):
|
||||
|
||||
1. `adc_health_status_t` 只给 `tpafe_stall_ms`,**STM32 侧没有对应的停滞时长字段**——
|
||||
只知道「STM32 静默了」,不知道「静默多久」。
|
||||
2. `tpafe_stall_ms` 只在电平位置位时给值,正常态恒为 0,因此它不能当「距上次前进的时间」趋势量用。
|
||||
3. **观察者死亡不可自证**(§8 的责任矩阵),判定层的存活依赖 `adcTask`。
|
||||
4. `tgap` 与 `adc1_gap` 单位不同(点 / 次,§6.2),跨字段做除法或对比会得出错误结论。
|
||||
5. 倒退的可判定窗是 ±2^31 拍(约 ±24.85 天),窗口外的倒退读作前进(§6.3)。
|
||||
|
||||
---
|
||||
|
||||
## 11. 怎么验证
|
||||
|
||||
**验证分工先说清**:台架 `@test` 只能证明「健康时不误报」(要求零 `[ERR]`),
|
||||
**证不了「故障时必报」**——静默与倒退在健康台架上不可复现(复现 = 拔芯片或改驱动)。
|
||||
「必报」这一半完全由主机侧单测承担,这就是本层必须是纯逻辑的原因(§4.4)。
|
||||
|
||||
| 判据 | 用例 | 钉住的具体边界 |
|
||||
|---|---|---|
|
||||
| 首拍只建基线 | `TC-ADCHEALTH-001` | 开机时序号已非 0 也不误报;随后 1 ms 无前进不算静默 |
|
||||
| 健康不误报 | `TC-ADCHEALTH-002` | 1 kHz 连扫 2000 拍,`flags` / `events` / `sticky` 全程为 0 |
|
||||
| TPAFE 静默 | `TC-ADCHEALTH-003` | 99 ms 不报 / 100 ms 必报(门限含等号);`sticky` 留痕;恢复只清电平位 |
|
||||
| STM32 静默 | `TC-ADCHEALTH-004` | 两车道独立:TPAFE 正常时只报 STM32 位 |
|
||||
| TPAFE 丢帧 | `TC-ADCHEALTH-005` | `d = 3` → `tpafe_gaps` 恰好 +2;事件只活一拍;计数是累计量 |
|
||||
| STM32 丢帧 | `TC-ADCHEALTH-006` | 计数增量 → 事件;**不进锁存**(超拍不是故障) |
|
||||
| 序号倒退 | `TC-ADCHEALTH-007` | 电平 + 锁存同时置位;随后 50 拍正常推进仍不清 |
|
||||
| 三类错误增量 | `TC-ADCHEALTH-008` | 事件位与锁存位**位号一致**;计数不变则不重复触发 |
|
||||
| tick 回绕 | `TC-ADCHEALTH-009` | 跨 0xFFFFFFFF 扫 512 拍不误报;回绕后冻结仍能检出 |
|
||||
| 空指针守卫 | `TC-ADCHEALTH-010` | `in == NULL` 不动 `out`;`out == NULL` 仍更新内部状态 |
|
||||
| 幂等复位 | `TC-ADCHEALTH-011` | 故障后 `adc_health_init()` 全清,且重新建基线干净 |
|
||||
|
||||
合计 **11 组用例 / 31 条断言**(用例清单与被测源文件同一处:
|
||||
`test/host/adc_health/adc_health_test.c` 文件头)。跑法:
|
||||
|
||||
```powershell
|
||||
# 直接跑(MSVC 离线编译真实源文件,毫秒级)
|
||||
& '.venv\Scripts\python.exe' test/host/adc_health/run_unit_test.py
|
||||
# 走门禁(改动被测源、头、编排文件或测试用例时自动触发)
|
||||
& '.venv\Scripts\python.exe' -m pre_commit run adc-health-unit-test --all-files
|
||||
```
|
||||
|
||||
门禁的触发文件集是 `App/algo/adc_health.*` + `App/task/adc_task.c` + `test/host/adc_health/`,
|
||||
**即改判定层或改编排层都会跑这套单测**——编排层的位图语义也是它的被保护对象之一。
|
||||
|
||||
### 11.1 最容易读错的一处:「未消费的那次 +1」
|
||||
|
||||
测试里普遍写成 `for (...) { check(&in); seq++; now++; }`:**递增发生在 check 之后**,
|
||||
于是本轮循环末尾那次 `+1` 还没被任何一次 check 看过。下一段(冻结场景)的第一拍会先
|
||||
消费掉它,看见 `d = 1` → 顺带把静默计时起点重置为该拍的 `now_ms`。结果:
|
||||
|
||||
| 用例 | 表面写法 | 实际含义 |
|
||||
|---|---|---|
|
||||
| `TC-ADCHEALTH-003` | 100 拍不报,再一拍报 | 冻结后 elapsed 依次是 0..99(不报),下一拍正好 100(报) |
|
||||
| `TC-ADCHEALTH-004` / `009` | 需要 101 拍才报 | 同样先吃掉那 1 拍,elapsed 从 0 起算,第 101 次 check 才是 100 ms |
|
||||
|
||||
读这些 TC **不要按循环次数理解门限**,要按「本次 check 传入的 `now_ms` 与当时的
|
||||
`*_adv_ms` 之差」理解。真实系统里消费者每拍一次、生产者每拍前进一帧 → elapsed 恒为 0;
|
||||
生产者停摆后 elapsed 从 1 开始每拍 +1,第 100 拍置位。
|
||||
|
||||
### 11.2 台架证据
|
||||
|
||||
`docs/测试说明/boot_log_data/20261011_prod_adc_health_boot_log.txt`:两条心跳行
|
||||
(`adc1_err=0 adc1_hw=0 adc1_gap=23 → 25`、`tgap=1 → 2`、`frames` 每 10 s +10000)
|
||||
加上全程零 `[ERR]` —— 即「健康不误报」+「gap 常态被正确压住」。
|
||||
|
||||
---
|
||||
|
||||
## 12. 现场判读手册
|
||||
|
||||
| 日志行(原样) | 含义 | 下一步查什么 |
|
||||
|---|---|---|
|
||||
| `[ADC_TASK] health: flags=0x1 stall_ms=137 tgap=2` | TPAFE 静默上升沿,本拍已停 137 ms | 先看 `seq` 是否还在涨(10 s 心跳):不涨 = 生产者侧;再确认 TIM3 与 CONVST |
|
||||
| `[ADC_TASK] health: flags=0x2 ...` | STM32 静默上升沿 | 看 `adc1_seq` / `adc1_hw`:`adc1_hw` 有增量 → DMA/ADC 报错在先 |
|
||||
| `[ADC_TASK] health: flags=0x4 ...` | 序号倒退 | 直接找谁改了计数器(§6.3 三条路径);此位不会自愈,只能复位 |
|
||||
| `[ADC_TASK] sampling fault: events=0x18 sticky=0x18` | 错误计数增量(这里 = bit4 + bit3) | `sticky` 与 `events` 同值 = 首次出现;`sticky` 更大 = 之前还出过别的类 |
|
||||
| `[ADC_TASK] health: recovered flags=0x0 sticky=0x3` | 静默自愈 | 别把「恢复」读成「没问题」:`sticky=0x3` 说明两侧都停过 |
|
||||
| `[PROT] sampling FAULT -> protection blocked (hf=0x00000001)` | 闭锁真的生效了 | 括号里就是命中位;此后保护不判据,元件状态全复位 |
|
||||
| `[PROT] sampling recovered -> protection resumed` | 闭锁解除 | 与上一条配对看时长 |
|
||||
| 心跳 `tgap=` 持续上涨 | 消费者超拍 | 看 `[PROF]` 的 `adcm` 是否 > 1 ms(`docs/子系统设计/看门狗设计说明.md` §11.4) |
|
||||
|
||||
两个必须记住的读法:
|
||||
|
||||
- **`sticky` 是「曾经」,`flags` 是「现在」**。只看 `sticky` 会把历史故障读成当前故障;
|
||||
只看 `flags` 会漏掉「已经自愈但其实停过」。SOE 反查同理:类型 5 的 value 是触发瞬间的
|
||||
`hf & PROT_HF_LOCK_MASK`,解码用 `test/host/soe/decode_soe.py`。
|
||||
- **看到 IWDG 复位就不要再看 `sticky`**:复位后全部状态清零,`sticky` 只覆盖本次开机。
|
||||
|
||||
三个典型剧本:
|
||||
|
||||
| 剧本 | 判定路径 | 结论 |
|
||||
|---|---|---|
|
||||
| 现场报「保护退出」 | 查 `[PROT] sampling FAULT` 的 `hf` → 回到 `flags` 上升沿行看 `stall_ms` | `hf=0x4` → 计数器被动过,查软件;`hf=0x1/0x2` → 采样侧真停,查节拍与转换器供电 |
|
||||
| 心跳里 `idle` / `skip` 在涨但无 `health` 行 | 那不是本层的判据(本层只看序号) | 属 L1 相位伪影或通道首次未就绪,见 `docs/子系统设计/ADC采样节拍设计说明.md` §8 |
|
||||
| 偶发一条 `health` + 一条 `recovered` | 一次瞬态静默(几十 ms 到几百 ms) | 查同窗口的日志突发、NAND 擦写、FTP 传输等长耗时窗口(`docs/子系统设计/看门狗设计说明.md` §4.3) |
|
||||
|
||||
---
|
||||
|
||||
## 13. 自测问题(读完能不能真的懂)
|
||||
|
||||
| 问题 | 答案要点 |
|
||||
|---|---|
|
||||
| 为什么 `d > 1` 绝不能算静默? | 它证明生产者**还在**产出,只是被覆盖了;算静默就会把「自己慢」误判成「硬件坏」并闭锁保护 |
|
||||
| 为什么 stall 闭锁而 FRSTDATA 增量不闭锁? | 前者是「当前测量不可信」的电平条件,后者是一次性事件痕迹;闭锁权只给电平类 |
|
||||
| 为什么 `SEQ_BACKWARD` 不自愈,代价是什么? | 恢复正常不能证明计数器可信;代价是永久闭锁直到复位,IWDG 也不会救(任务都活着) |
|
||||
| tick 回绕要不要特殊处理? | 不要,`(int32_t)` 差值天然吸收;被放弃的只有 ±2^31 之外的倒退 |
|
||||
| 判定层能不能顺手加一条日志? | 不能,见 §4.4:免桩单测立刻失效,且每条约 90 B Flash + 216 B 栈 |
|
||||
| 消费者自己卡死谁能发现? | 不是本层;`adcTask` 缺席 → 看门狗 800 ms 起停喂 → IWDG 复位 |
|
||||
| 首拍为什么不判定? | 此刻序号已非 0(TIM3 早于调度器约 100 ms 启动),没有「上一拍」就没有参照系 |
|
||||
|
||||
---
|
||||
|
||||
## 14. 改它之前必读
|
||||
|
||||
| 想改什么 | 必须一起动 |
|
||||
|---|---|
|
||||
| `ADC_HEALTH_STALL_MS` | `TC-ADCHEALTH-003` / `004` / `009` 的拍数假设、本文 §6.1 的推导与 §8 的闭锁时间预算、`docs/算法/采集与算法层设计说明.md` §4 的门限描述 |
|
||||
| 新增一个故障位 | 一次定清四件事:① 电平还是锁存还是仅事件;② 是否与事件位**位号对齐**;③ 进不进 `PROT_HF_LOCK_MASK`;④ 编排层 `s_health_logged` 去重位段够不够(bit0..bit5 已被占用) |
|
||||
| 改判定顺序 | §5 的走读表与流程图;尤其「前进更新必须在静默判定之前」,反过来会出现同一拍既刷新又报静默 |
|
||||
| 动驱动侧计数器(自增时机、清零点、位宽) | 必须同时复核 §6.3 的倒退路径与 §6.4 的 `!=` 增量口径;位宽一变就可能造出假增量 |
|
||||
| 把任何 `*_init()` 挪到运行期重跑 | **必须同时 `adc_health_init()`**——`App/algo/adc_input.c` 的 init 会把 `s_dma_seq` 归零,不配套就第一拍报 `SEQ_BACKWARD` 并永久闭锁 |
|
||||
| 在判定层引入 HAL / 日志 / 内核 API | 先回答「主机单测怎么免桩跑」;答不上来就别改 |
|
||||
|
||||
三条禁令(都对应已写死的机制,不是风格问题):
|
||||
|
||||
1. 判定层**不取数、不打日志、不碰寄存器**——否则 §11 的整条验证路径作废。
|
||||
2. 判定层**不用浮点、不做除法**——它在 1 kHz 路径上,且主机侧要能逐拍精确复现每个分支。
|
||||
3. 位图**不复用已冻结位号**:bit0..bit7 已分配,新位从 bit8 起;`events` 与 `sticky` 若要共享
|
||||
语义就必须共享位号(§7 的去重写法依赖这点)。
|
||||
|
||||
**当前事实登记**(改这段链时会撞上):产品启动路径上 `adc_health_init()` 并未被调用——
|
||||
`adc_task_init()` 目前在仓库里没有调用点(只有 `adc_task_func` 被 `Src/freertos.c` 建为任务)。
|
||||
判定仍然正确,因为判定层状态全在 `.bss`(复位即 0),而**首拍只建基线**这条设计让 init 变成
|
||||
可选动作。但「init 可选」不等于「init 无用」:运行期任何需要重建参照系的场合都必须显式调用。
|
||||
|
||||
---
|
||||
|
||||
## 15. 相关文档
|
||||
|
||||
- `docs/算法/采集与算法层设计说明.md` §4 —— 本模块在全层中的位置与接口速览(本文不重复)
|
||||
- `docs/子系统设计/ADC采样节拍设计说明.md` §8 —— 心跳字段全表与判读(`tgap` 所在行)
|
||||
- `docs/子系统设计/看门狗设计说明.md` §4.3 —— 长耗时窗口的三类处置(瞬态静默的候选来源)
|
||||
- `docs/子系统设计/看门狗设计说明.md` §11.4 —— `[PROF]` 探针字段与耗时基线(判「超拍」要看它)
|
||||
- `docs/硬件平台/ADC通道映射说明.md` —— 语义映射唯一权威口径(与「哪一侧静默」直接相关)
|
||||
- `docs/算法/继电保护功能复现规划.md` §3.1 —— L1 功能项与验收口径(本层 = L1-6 行)
|
||||
- `docs/算法/继电保护功能复现规划.md` §3.5 —— L5 自检与闭锁(本层供数的 L5-4 在此登记)
|
||||
- `docs/内存管理/RAM使用分析与优化方案.md` §1.4 —— `adcTask` 栈最坏链台账
|
||||
- `App/algo/README.md` —— 目录级文件清单与构建测试入口
|
||||
|
||||
---
|
||||
|
||||
## 变更记录
|
||||
|
||||
| 日期 | 变更 | 说明 |
|
||||
|---|---|---|
|
||||
| 2026-10-21 | 新建 | 首版单文件深设计:判据推导、四种位语义、闭锁与兜底责任矩阵、验证分工与现场判读手册;新增三张配图生成器(`tools/arch_diagram/gen_adc_health_flow_svg.py`、`gen_adc_health_bitmap_svg.py`、`gen_adc_health_timeline_svg.py`)与产物 SVG |
|
||||
@@ -138,6 +138,9 @@ flowchart TD
|
||||
`SYSLOG_SELFTEST`(每类每次开机一次,防刷屏写满 `selftest.log`);下降沿 `DBG_INFO`
|
||||
("恢复"必须可见,否则只看到一次错误无从判断是否还在)。
|
||||
|
||||
**深读**:本模块的判据推导(100 ms 门限的三条依据)、位号对齐的收益与代价、闭锁与兜底的责任
|
||||
划分、观测盲区、验证分工与现场判读手册 → `docs/算法/采样有效性校验设计说明.md`。
|
||||
|
||||
---
|
||||
|
||||
## 5. L2 输入 `sample_ring.c`
|
||||
@@ -406,6 +409,7 @@ adcTask 静态最坏链与余量的**台账在** `docs/内存管理/RAM使用分
|
||||
## 13. 相关文档
|
||||
|
||||
- 目录索引与构建 / 测试入口:`App/algo/README.md`
|
||||
- L1-6 判定层深设计(判据推导 / 盲区 / 验证分工):`docs/算法/采样有效性校验设计说明.md`
|
||||
- 分层与功能项清单(L1~L5 的属主登记处):`docs/算法/继电保护功能复现规划.md`
|
||||
- 通道映射权威口径:`docs/硬件平台/ADC通道映射说明.md`
|
||||
- 采样节拍与 DMA 搬运:`docs/子系统设计/ADC采样节拍设计说明.md`
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/采样有效性位图与掩码.svg —— L1-6 出口位图与消费者掩码对照(纯标准库)。
|
||||
|
||||
为什么不用 Mermaid:本图的核心信息是**位号对齐**(同一 bit 在三张位图与四个消费者掩码
|
||||
里落在同一列),必须等宽网格逐列对齐;Mermaid 无法控制列位。
|
||||
|
||||
用法(仓库根目录,任意 python3):
|
||||
python tools/arch_diagram/gen_adc_health_bitmap_svg.py
|
||||
输出:
|
||||
docs/images/采样有效性位图与掩码.svg
|
||||
改内容时:编辑本文件顶部的 GROUPS 数据后重跑。
|
||||
"""
|
||||
import html
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
OUT = os.path.join(ROOT, "docs", "images", "采样有效性位图与掩码.svg")
|
||||
|
||||
W = 1580
|
||||
PAD = 40
|
||||
TITLE_FONT = 21
|
||||
SUB_FONT = 13
|
||||
ROW_LABEL_FONT = 12.5
|
||||
BIT_FONT = 11
|
||||
TAG_FONT = 10.5
|
||||
NOTE_FONT = 11.5
|
||||
HEAD_FONT = 12.5
|
||||
|
||||
LABEL_X, LABEL_W = PAD, 262
|
||||
GRID_X = PAD + LABEL_W + 8
|
||||
BIT_W, BIT_H, BIT_GAP = 112, 46, 4
|
||||
NOTE_W = 340
|
||||
GROUP_GAP = 26
|
||||
|
||||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||||
INK, MUTED = "#0f172a", "#64748b"
|
||||
C_LEVEL = ("#0d9488", "#f0fdfa") # 电平(可自愈)
|
||||
C_LATCH = ("#e11d48", "#fff1f2") # 锁存(不自愈)
|
||||
C_EVENT = ("#d97706", "#fffbeb") # 仅边沿事件
|
||||
C_MASK = ("#7c3aed", "#f5f3ff") # 消费者掩码命中
|
||||
C_NONE = ("#94a3b8", "#f8fafc") # 未使用 / 不适用
|
||||
|
||||
# ----------------------------- 数据(改这里) -----------------------------
|
||||
# kind: level / latch / event / mask / none
|
||||
GROUPS = [
|
||||
dict(head="判定层写出的三张位图(adc_health.c)", rows=[
|
||||
dict(label="flags 电平位", sub="adc_health_flags(),当前活跃",
|
||||
note="protectTask 每 20 ms 读它做 L5-4 闭锁;不看事件位",
|
||||
bits=[("bit0", "TPAFE_STALL", "level"), ("bit1", "STM32_STALL", "level"),
|
||||
("bit2", "SEQ_BACKWARD", "latch"), ("bit3", "—", "none"),
|
||||
("bit4", "—", "none"), ("bit5", "—", "none"),
|
||||
("bit6", "—", "none"), ("bit7", "—", "none")]),
|
||||
dict(label="sticky 锁存位", sub="adc_health_sticky(),直到 init 才清",
|
||||
note="只留痕不闭锁:曾停摆/曾出错必须可追溯",
|
||||
bits=[("bit0", "TPAFE_STALL", "latch"), ("bit1", "STM32_STALL", "latch"),
|
||||
("bit2", "SEQ_BACKWARD", "latch"), ("bit3", "FRSTDATA", "latch"),
|
||||
("bit4", "STM32_ERR", "latch"), ("bit5", "HW_ERR", "latch"),
|
||||
("bit6", "不入锁存", "none"), ("bit7", "不入锁存", "none")]),
|
||||
dict(label="events 事件位", sub="adc_health_check() 出参,仅当拍有效",
|
||||
note="adc_health_report() 的唯一输入:按位分三级处置",
|
||||
bits=[("bit0", "—", "none"), ("bit1", "—", "none"), ("bit2", "—", "none"),
|
||||
("bit3", "FRSTDATA", "event"), ("bit4", "STM32_ERR", "event"),
|
||||
("bit5", "HW_ERR", "event"), ("bit6", "TPAFE_GAP", "event"),
|
||||
("bit7", "STM32_GAP", "event")]),
|
||||
]),
|
||||
dict(head="消费者侧的掩码与记忆位图", rows=[
|
||||
dict(label="PROT_HF_LOCK_MASK", sub="protect_task.c 编译期常量",
|
||||
note="命中即 prot_elm_init() 复位元件 + 本拍不 eval;SOE 类型 5/6",
|
||||
bits=[("bit0", "闭锁", "mask"), ("bit1", "闭锁", "mask"),
|
||||
("bit2", "闭锁", "mask"), ("bit3", "不闭锁", "none"),
|
||||
("bit4", "不闭锁", "none"), ("bit5", "不闭锁", "none"),
|
||||
("bit6", "—", "none"), ("bit7", "—", "none")]),
|
||||
dict(label="serious 严重事件", sub="adc_task.c 的 adc_health_report()",
|
||||
note="三类计数增量恒应为 0 → 每次 DBG_ERROR + 自检留痕",
|
||||
bits=[("bit0", "—", "none"), ("bit1", "—", "none"), ("bit2", "—", "none"),
|
||||
("bit3", "FRSTDATA", "mask"), ("bit4", "STM32_ERR", "mask"),
|
||||
("bit5", "HW_ERR", "mask"), ("bit6", "—", "none"),
|
||||
("bit7", "—", "none")]),
|
||||
dict(label="gap 丢帧事件", sub="同一函数里的丢帧分支",
|
||||
note="刻意不告警:超拍是常态(启动突发期即累计 23 次),只累计",
|
||||
bits=[("bit0", "—", "none"), ("bit1", "—", "none"), ("bit2", "—", "none"),
|
||||
("bit3", "—", "none"), ("bit4", "—", "none"), ("bit5", "—", "none"),
|
||||
("bit6", "TPAFE_GAP", "mask"), ("bit7", "STM32_GAP", "mask")]),
|
||||
dict(label="s_health_logged", sub="编排层记忆位图(每类一次留痕)",
|
||||
note=" |= (serious | rise):用同一位号把 SYSLOG 压成每类每次开机一条",
|
||||
bits=[("bit0", "记过", "mask"), ("bit1", "记过", "mask"),
|
||||
("bit2", "记过", "mask"), ("bit3", "记过", "mask"),
|
||||
("bit4", "记过", "mask"), ("bit5", "记过", "mask"),
|
||||
("bit6", "不记", "none"), ("bit7", "不记", "none")]),
|
||||
]),
|
||||
]
|
||||
|
||||
FOOT = ("bit3 / bit4 / bit5 在 events 与 sticky 中刻意用同一位号 → 消费者能把事件位直接"
|
||||
"套在锁存位图上做每类一次留痕;bit8 以上全 32 位保留未用。"
|
||||
"详见 docs/算法/采样有效性校验设计说明.md")
|
||||
|
||||
KIND_COLOR = dict(level=C_LEVEL, latch=C_LATCH, event=C_EVENT, mask=C_MASK, none=C_NONE)
|
||||
|
||||
|
||||
# ----------------------------- 工具函数 -----------------------------
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True)
|
||||
|
||||
|
||||
def text_w(s, fs):
|
||||
return sum(fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) for ch in s)
|
||||
|
||||
|
||||
WARN = []
|
||||
|
||||
|
||||
def fit(s, fs, avail, where):
|
||||
if text_w(s, fs) > avail:
|
||||
WARN.append("%s: %.0f > %.0f [%s]" % (where, text_w(s, fs), avail, s))
|
||||
|
||||
|
||||
# ----------------------------- 生成 -----------------------------
|
||||
def build():
|
||||
body = []
|
||||
addb = body.append
|
||||
|
||||
for g in GROUPS:
|
||||
fit(g["head"], HEAD_FONT, W - 2 * PAD, "group head")
|
||||
for r in g["rows"]:
|
||||
fit(r["label"], ROW_LABEL_FONT, LABEL_W - 16, "row label")
|
||||
fit(r["sub"], TAG_FONT, LABEL_W - 16, "row sub")
|
||||
for ln in wrap(r["note"], NOTE_FONT, NOTE_W - 24):
|
||||
fit(ln, NOTE_FONT, NOTE_W - 24, "row note")
|
||||
for b, tag, kind in r["bits"]:
|
||||
fit(tag, TAG_FONT, BIT_W - 12, "bit tag " + b)
|
||||
fit(FOOT, NOTE_FONT, W - 2 * PAD - 32, "foot")
|
||||
for w in WARN:
|
||||
print("OVERFLOW WARN " + w)
|
||||
|
||||
row_h = BIT_H + 10
|
||||
y = 178
|
||||
|
||||
for g in GROUPS:
|
||||
addb('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#334155">%s</text>'
|
||||
% (PAD, y, HEAD_FONT, esc(g["head"])))
|
||||
y += 22
|
||||
for r in g["rows"]:
|
||||
cy = y
|
||||
addb('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s">%s</text>'
|
||||
% (PAD, cy + 20, ROW_LABEL_FONT, INK, esc(r["label"])))
|
||||
addb('<text x="%d" y="%d" font-size="%d" fill="%s">%s</text>'
|
||||
% (PAD, cy + 38, TAG_FONT, MUTED, esc(r["sub"])))
|
||||
for i, (b, tag, kind) in enumerate(r["bits"]):
|
||||
fg, bg = KIND_COLOR[kind]
|
||||
x = GRID_X + i * (BIT_W + BIT_GAP)
|
||||
addb('<rect x="%d" y="%d" width="%d" height="%d" rx="8" fill="%s" '
|
||||
'stroke="%s" stroke-opacity="%s"/>'
|
||||
% (x, cy, BIT_W, BIT_H, bg, fg, "0.55" if kind != "none" else "0.35"))
|
||||
addb('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s">'
|
||||
'%s</text>' % (x + 10, cy + 19, BIT_FONT, fg, esc(b)))
|
||||
addb('<text x="%d" y="%d" font-size="%d" fill="#334155">%s</text>'
|
||||
% (x + 10, cy + 36, TAG_FONT, esc(tag)))
|
||||
nx = GRID_X + 8 * (BIT_W + BIT_GAP) + 12
|
||||
addb('<rect x="%d" y="%d" width="%d" height="%d" rx="8" fill="#f8fafc" '
|
||||
'stroke="#64748b" stroke-opacity="0.25"/>' % (nx, cy, NOTE_W, BIT_H))
|
||||
lines = wrap(r["note"], NOTE_FONT, NOTE_W - 24)
|
||||
ty = cy + (BIT_H - (len(lines) - 1) * 15 + 4) / 2.0
|
||||
for ln in lines:
|
||||
addb('<text x="%d" y="%d" font-size="%d" fill="#475569">%s</text>'
|
||||
% (nx + 12, ty, NOTE_FONT, esc(ln)))
|
||||
ty += 15
|
||||
y += row_h
|
||||
y += GROUP_GAP
|
||||
|
||||
# 图例
|
||||
lx, ly = GRID_X, 118
|
||||
for label, kind in [("电平位(可自愈)", "level"), ("锁存位(不自愈)", "latch"),
|
||||
("边沿事件位(仅当拍)", "event"), ("消费者掩码命中 / 记忆位", "mask"),
|
||||
("不适用", "none")]:
|
||||
fg, bg = KIND_COLOR[kind]
|
||||
addb('<rect x="%d" y="%d" width="16" height="16" rx="4" fill="%s" stroke="%s" '
|
||||
'stroke-width="1.6"/>' % (lx, ly - 12, bg, fg))
|
||||
lx += 22
|
||||
addb('<text x="%d" y="%d" font-size="12" fill="#475569">%s</text>'
|
||||
% (lx, ly, esc(label)))
|
||||
lx += text_w(label, 12) + 30
|
||||
|
||||
total_h = y + 74
|
||||
foot_y = total_h - 58
|
||||
body.insert(0, '<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, total_h))
|
||||
head = ['<text x="%d" y="44" font-size="%d" font-weight="700" fill="%s">%s</text>'
|
||||
% (PAD, TITLE_FONT, INK, esc("采样有效性校验 · 出口位图与消费者掩码(逐位对齐)")),
|
||||
'<text x="%d" y="70" font-size="%d" fill="%s">%s</text>'
|
||||
% (PAD, SUB_FONT, MUTED,
|
||||
esc("同一 bit 在三张位图与四个掩码里始终落在同一列:位号对齐 = 消费者可以位图代打表"))]
|
||||
|
||||
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
|
||||
'viewBox="0 0 %d %d" font-family="%s">\n%s\n%s\n'
|
||||
'<rect x="%d" y="%d" width="%d" height="44" rx="10" fill="#fffbeb" '
|
||||
'stroke="#d97706" stroke-opacity="0.45"/>\n'
|
||||
'<text x="%d" y="%d" font-size="%d" fill="#7c2d12">%s</text>\n</svg>\n'
|
||||
% (W, total_h, W, total_h, FONT, "\n".join(head), "\n".join(body),
|
||||
PAD, foot_y, W - 2 * PAD, PAD + 16, foot_y + 27, NOTE_FONT, esc(FOOT)))
|
||||
|
||||
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)
|
||||
print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, total_h, OUT))
|
||||
if len(WARN):
|
||||
print("!! %d 处文字可能溢出" % len(WARN))
|
||||
|
||||
|
||||
def wrap(s, fs, avail):
|
||||
"""按估算宽度做中文换行(无空格语言:逐字累计)。"""
|
||||
lines, cur = [], ""
|
||||
for ch in s:
|
||||
if text_w(cur + ch, fs) > avail and cur:
|
||||
lines.append(cur)
|
||||
cur = ""
|
||||
cur += ch
|
||||
if cur:
|
||||
lines.append(cur)
|
||||
return lines
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/采样有效性校验数据流.svg —— L1-6「观测计数 → 快照 → 判定 → 位图 → 消费者」(纯标准库)。
|
||||
|
||||
为什么不用 Mermaid:本图要表达**三条量车道(TPAFE / STM32 / 时基)平行流过五个阶段**,
|
||||
Mermaid 自动布局会把三条车道挤成交叉线、也保不住"同一拍取数"的横向对齐。
|
||||
这里按车道手工排布:每车道一行,箭头全部水平,行内可直接横向读一遍判据。
|
||||
|
||||
用法(仓库根目录,任意 python3):
|
||||
python tools/arch_diagram/gen_adc_health_flow_svg.py
|
||||
输出:
|
||||
docs/images/采样有效性校验数据流.svg
|
||||
改内容时:只编辑本文件顶部的 STAGES / LANES 数据后重跑。
|
||||
"""
|
||||
import html
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
OUT = os.path.join(ROOT, "docs", "images", "采样有效性校验数据流.svg")
|
||||
|
||||
# ----------------------------- 画布与尺寸 -----------------------------
|
||||
W = 1580
|
||||
PAD = 40
|
||||
TITLE_FONT = 21
|
||||
SUB_FONT = 13
|
||||
HEAD_FONT = 13
|
||||
CELL_TITLE_FONT = 12.5
|
||||
CELL_NOTE_FONT = 11
|
||||
LANE_FONT = 12
|
||||
FOOT_FONT = 12
|
||||
|
||||
CELL_H = 148
|
||||
LANE_GAP = 34
|
||||
MAIN_Y = 214
|
||||
LANE_LABEL_W = 96
|
||||
COLS = [
|
||||
(PAD + LANE_LABEL_W, 268), # 阶段 1:观测计数(ISR / 消费侧)
|
||||
(PAD + LANE_LABEL_W + 288, 236), # 阶段 2:本拍快照
|
||||
(PAD + LANE_LABEL_W + 544, 276), # 阶段 3:判定(纯逻辑)
|
||||
(PAD + LANE_LABEL_W + 840, 264), # 阶段 4:出口位图
|
||||
(PAD + LANE_LABEL_W + 1124, 250), # 阶段 5:消费者
|
||||
]
|
||||
|
||||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||||
C_LANE = ["#0d9488", "#2563eb", "#7c3aed"]
|
||||
BG_LANE = ["#f0fdfa", "#eff6ff", "#f5f3ff"]
|
||||
C_HEAD, BG_HEAD = "#475569", "#f1f5f9"
|
||||
INK, MUTED = "#0f172a", "#64748b"
|
||||
|
||||
# ----------------------------- 数据(改这里) -----------------------------
|
||||
STAGES = [
|
||||
"1. 观测计数(ISR / 驱动侧)",
|
||||
"2. 本拍计数快照",
|
||||
"3. 判定(adc_health.c 纯逻辑)",
|
||||
"4. 出口位图(32 位)",
|
||||
"5. 消费者与处置",
|
||||
]
|
||||
|
||||
# 每条车道 = 一行;cells 与 STAGES 一一对应
|
||||
LANES = [
|
||||
dict(
|
||||
name="TPAFE",
|
||||
color=0,
|
||||
cells=[
|
||||
dict(title="tpafe5160.c 的 BUSY EXTI",
|
||||
lines=["s_frame_seq++:每读完一帧 +1",
|
||||
"s_frstdata_err++:无跳变帧累计(E-44)",
|
||||
"ISR 内只加计数,不打日志"] ),
|
||||
dict(title="adcTask 每拍 poll 之后取值",
|
||||
lines=["tpafe_seq",
|
||||
"tpafe_frst_err(uint16 放宽)",
|
||||
"⚠ 必须取本拍 poll 之后的值"] ),
|
||||
dict(title="序号差值 d = seq - 上次",
|
||||
lines=["d < 0 → 置倒退位并重新对齐",
|
||||
"d > 1 → tgap += d-1 + 当拍事件",
|
||||
"d >= 1 → 刷新「最后前进时刻」"] ),
|
||||
dict(title="本车道出口",
|
||||
lines=["bit0 ADC_HF_TPAFE_STALL(自愈)",
|
||||
"bit2 ADC_HF_SEQ_BACKWARD(锁存)",
|
||||
"bit6/bit3 ADC_HE_TPAFE_GAP / FRSTDATA"] ),
|
||||
dict(title="闭锁 + 留痕 + 计数",
|
||||
lines=["protectTask 每 20 ms 读 bit0/bit2",
|
||||
"gap / FRSTDATA 事件 → 编排层日志",
|
||||
"tgap → 10 s 心跳字段"] ),
|
||||
],
|
||||
),
|
||||
dict(
|
||||
name="STM32",
|
||||
color=1,
|
||||
cells=[
|
||||
dict(title="adc_input.c(DMA2 Stream4)",
|
||||
lines=["s_dma_seq++:半区/整区完成各一次",
|
||||
"s_stm32_gaps++:完成计数一次前进 >1",
|
||||
"s_stm32_errs++:连续 >=2 拍无新帧",
|
||||
"s_stm32_hw_errs++:HAL 错误回调"] ),
|
||||
dict(title="同一拍的四个 STM32 计数",
|
||||
lines=["stm32_seq / stm32_errs",
|
||||
"stm32_gaps / stm32_hw_errs",
|
||||
"gap 细账不重复取(L1 已算)"] ),
|
||||
dict(title="序号差值 d + 三处计数增量",
|
||||
lines=["d < 0 → 同一个 SEQ_BACKWARD 位",
|
||||
"d >= 1 → 刷新最后前进时刻",
|
||||
"增量 != 0 → 事件 + 锁存"] ),
|
||||
dict(title="本车道出口",
|
||||
lines=["bit1 ADC_HF_STM32_STALL(自愈)",
|
||||
"bit2 共用(两车道同一位)",
|
||||
"bit7/bit4/bit5 GAP / ERR / HW_ERR"] ),
|
||||
dict(title="闭锁 + 严重事件",
|
||||
lines=["bit1/bit2 → 同左闭锁路径",
|
||||
"STM32_ERR / HW_ERR:恒 0 增量",
|
||||
"出现即 DBG_ERROR + 自检留痕"] ),
|
||||
],
|
||||
),
|
||||
dict(
|
||||
name="时基",
|
||||
color=2,
|
||||
cells=[
|
||||
dict(title="osKernelGetTickCount()",
|
||||
lines=["HAL 时基 TIM7,tick = 1 ms",
|
||||
"2^32 ms 后自然回绕"] ),
|
||||
dict(title="now_ms:本拍唯一一份",
|
||||
lines=["健康判定与心跳上报共用",
|
||||
"不在判定层内再取一次"] ),
|
||||
dict(title="差值法 (int32_t)(now - adv)",
|
||||
lines=["回绕安全(取有符号差)",
|
||||
">= ADC_HEALTH_STALL_MS(100)",
|
||||
"可判定窗 ±2^31 ≈ ±24.85 天"] ),
|
||||
dict(title="tpafe_stall_ms",
|
||||
lines=["静默期内给出停滞时长",
|
||||
"正常态恒为 0",
|
||||
"STM32 侧未上报(观测盲区)"] ),
|
||||
dict(title="上升沿日志可读性",
|
||||
lines=["health: flags=… stall_ms=…",
|
||||
"回答到底停了多少毫秒"] ),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
FOOT = ("判定层零平台依赖(只 include <stdint.h>、不碰寄存器 / 不取数 / 不打日志)→ 主机侧以 #include 原样纳入、免桩全覆盖单测。"
|
||||
"详见 docs/算法/采样有效性校验设计说明.md")
|
||||
|
||||
|
||||
# ----------------------------- 工具函数 -----------------------------
|
||||
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 build():
|
||||
out = []
|
||||
add = out.append
|
||||
|
||||
n_cells = max(len(l["cells"]) for l in LANES)
|
||||
for li, lane in enumerate(LANES):
|
||||
for ci, cell in enumerate(lane["cells"]):
|
||||
_, cw = COLS[ci]
|
||||
avail = cw - 26
|
||||
check_fit(cell["title"], CELL_TITLE_FONT, avail,
|
||||
"lane %s col %d title" % (lane["name"], ci + 1))
|
||||
for ln in cell["lines"]:
|
||||
check_fit(ln, CELL_NOTE_FONT, avail,
|
||||
"lane %s col %d line" % (lane["name"], ci + 1))
|
||||
for ci, st in enumerate(STAGES):
|
||||
check_fit(st, HEAD_FONT, COLS[ci][1] - 20, "stage head %d" % (ci + 1))
|
||||
if len(OVERFLOW):
|
||||
for o in OVERFLOW:
|
||||
print("OVERFLOW WARN " + o)
|
||||
|
||||
lane_h = CELL_H + LANE_GAP
|
||||
bottom = MAIN_Y + len(LANES) * lane_h - LANE_GAP
|
||||
total_h = bottom + 92
|
||||
|
||||
add('<rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, total_h))
|
||||
add('<text x="%d" y="44" font-size="%d" font-weight="700" fill="%s">%s</text>'
|
||||
% (PAD, TITLE_FONT, INK, esc("采样有效性校验(adc_health)· 数据流与出口")))
|
||||
add('<text x="%d" y="70" font-size="%d" fill="%s">%s</text>'
|
||||
% (PAD, SUB_FONT, MUTED,
|
||||
esc("三条量车道平行流过五个阶段;判定层每拍一次、常数次整数运算,输入只是计数快照")))
|
||||
|
||||
# ---- 阶段标题 ----
|
||||
for ci, st in enumerate(STAGES):
|
||||
x, cw = COLS[ci]
|
||||
add('<rect x="%d" y="150" width="%d" height="34" rx="8" fill="%s" stroke="%s" '
|
||||
'stroke-opacity="0.35"/>' % (x, cw, BG_HEAD, C_HEAD))
|
||||
add('<text x="%d" y="172" font-size="%d" font-weight="700" fill="#334155">%s</text>'
|
||||
% (x + 12, HEAD_FONT, esc(st)))
|
||||
|
||||
# ---- 车道 ----
|
||||
for li, lane in enumerate(LANES):
|
||||
color, bg = C_LANE[lane["color"]], BG_LANE[lane["color"]]
|
||||
y0 = MAIN_Y + li * lane_h
|
||||
# 车道标签
|
||||
add('<rect x="%d" y="%d" width="%d" height="%d" rx="10" fill="%s" stroke="%s" '
|
||||
'stroke-opacity="0.5"/>' % (PAD, y0, LANE_LABEL_W - 14, CELL_H, bg, color))
|
||||
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s" '
|
||||
'text-anchor="middle">%s</text>'
|
||||
% (PAD + (LANE_LABEL_W - 14) / 2.0, y0 + CELL_H / 2.0 + 5, LANE_FONT,
|
||||
color, esc(lane["name"])))
|
||||
|
||||
for ci, cell in enumerate(lane["cells"]):
|
||||
x, cw = COLS[ci]
|
||||
add('<rect x="%d" y="%d" width="%d" height="%d" rx="10" fill="%s" stroke="%s" '
|
||||
'stroke-opacity="0.45"/>' % (x, y0, cw, CELL_H, bg, color))
|
||||
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s">%s</text>'
|
||||
% (x + 13, y0 + 30, CELL_TITLE_FONT, color, esc(cell["title"])))
|
||||
yy = y0 + 56
|
||||
for ln in cell["lines"]:
|
||||
add('<text x="%d" y="%d" font-size="%d" fill="#334155">%s</text>'
|
||||
% (x + 13, yy, CELL_NOTE_FONT, esc(ln)))
|
||||
yy += 22
|
||||
# 水平箭头到下一列
|
||||
if ci + 1 < len(lane["cells"]):
|
||||
nx = COLS[ci + 1][0]
|
||||
cy = y0 + CELL_H / 2.0
|
||||
add('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" '
|
||||
'stroke-width="2.2"/>' % (x + cw, cy, nx - 9, cy, color))
|
||||
add('<path d="M %.1f %.1f L %.1f %.1f L %.1f %.1f z" fill="%s"/>'
|
||||
% (nx, cy, nx - 11, cy - 5.5, nx - 11, cy + 5.5, color))
|
||||
|
||||
# ---- 底部说明 ----
|
||||
add('<rect x="%d" y="%d" width="%d" height="46" rx="10" fill="#fffbeb" '
|
||||
'stroke="#d97706" stroke-opacity="0.45"/>' % (PAD, bottom + 30, W - 2 * PAD))
|
||||
add('<text x="%d" y="%d" font-size="%d" fill="#7c2d12">%s</text>'
|
||||
% (PAD + 16, bottom + 58, FOOT_FONT, esc(FOOT)))
|
||||
|
||||
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, total_h, W, total_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)
|
||||
print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, total_h, OUT))
|
||||
if len(OVERFLOW):
|
||||
print("!! %d 处文字可能溢出,见上方 WARN" % len(OVERFLOW))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/采样有效性判定时间轴.svg —— L1-6 四个工况的时序与门限(纯标准库)。
|
||||
|
||||
为什么不用 Mermaid:本图要表达**时间与门限的比例关系**(100 ms 门限线、静默计时线、
|
||||
电平位/锁存位的存续区间),必须按比例画坐标与条形;Mermaid 只画拓扑、不保比例。
|
||||
|
||||
用法(仓库根目录,任意 python3):
|
||||
python tools/arch_diagram/gen_adc_health_timeline_svg.py
|
||||
输出:
|
||||
docs/images/采样有效性判定时间轴.svg
|
||||
改内容时:编辑本文件顶部的 PANELS 数据后重跑。
|
||||
"""
|
||||
import html
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
OUT = os.path.join(ROOT, "docs", "images", "采样有效性判定时间轴.svg")
|
||||
|
||||
W = 1580
|
||||
PAD = 40
|
||||
TITLE_FONT = 21
|
||||
SUB_FONT = 13
|
||||
AXIS_FONT = 10.5
|
||||
TXT_FONT = 11.5
|
||||
HEAD_FONT = 13.5
|
||||
|
||||
PLOT_X0, PLOT_X1 = 430, 1120
|
||||
NOTE_X, NOTE_W = 1150, 390
|
||||
T_MAX = 200.0
|
||||
V_MAX = 150.0 # 纵轴:静默计时 ms(面板 C 的第二序列另按自身标度)
|
||||
PLOT_H = 96
|
||||
STRIP_H = 15
|
||||
PANEL_H = 186
|
||||
FIRST_Y = 220
|
||||
PLOT_W = PLOT_X1 - PLOT_X0
|
||||
|
||||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||||
INK, MUTED = "#0f172a", "#64748b"
|
||||
C_OK = "#16a34a"
|
||||
C_LEVEL = "#0d9488"
|
||||
C_LATCH = "#e11d48"
|
||||
C_EVENT = "#d97706"
|
||||
C_AXIS = "#cbd5e1"
|
||||
C_TH = "#e11d48"
|
||||
BG_STRIP = "#f1f5f9"
|
||||
|
||||
# ----------------------------- 数据(改这里) -----------------------------
|
||||
THRESHOLD = 100.0
|
||||
|
||||
PANELS = [
|
||||
dict(
|
||||
title="A 对照:健康 1 kHz(d 恒为 1)",
|
||||
seq=["seq: 1000 1001 1002 ... 每拍恰好 +1",
|
||||
"每拍都刷新最后前进时刻 adv_ms"],
|
||||
verdict="elapsed = now - adv_ms 恒为 0 → 永不进门限带",
|
||||
elapsed=[[(0, 0), (200, 0)]],
|
||||
strips=[],
|
||||
notes=["判定层对健康态**零输出**:events=0、flags=0、",
|
||||
"sticky=0。主机单测 TC-ADCHEALTH-002 用 2000 拍",
|
||||
"扫描把这个结论钉死(健康不误报)。"],
|
||||
),
|
||||
dict(
|
||||
title="B TPAFE 静默:100 ms 检出,恢复只清电平位",
|
||||
seq=["seq: 1000 1001 ... 冻结在 1050",
|
||||
"STM32 车道照常前进(分车道独立判定)"],
|
||||
verdict="elapsed 每拍 +1,达到 100 ms 那拍即置位",
|
||||
elapsed=[[(0, 0), (100, 100), (150, 150)], [(150, 0), (200, 0)]],
|
||||
strips=[("flags", "TPAFE_STALL 电平", C_LEVEL, 100, 150),
|
||||
("sticky", "TPAFE_STALL 锁存留痕", C_LATCH, 100, 200)],
|
||||
marks=[(100, 100, "1"), (150, 150, "2")],
|
||||
notes=["1 t=100 ms:ADC_HF_TPAFE_STALL 置位 + 进 sticky;",
|
||||
" 闭锁路径 = protectTask 下一拍(间隔 <= 20 ms)",
|
||||
" 命中掩码 → prot_elm_init() + 本拍不 eval",
|
||||
"2 t=150 ms:序号重新前进 → 电平位清、sticky 仍留痕"],
|
||||
),
|
||||
dict(
|
||||
title="C 消费者超拍:d=3 只算丢帧,永不判静默",
|
||||
seq=["seq: 1000 1003 1006 ... 每拍 +3",
|
||||
"生产者活着,消费者没跟上"],
|
||||
verdict="d >= 1 → 刷新 adv_ms,elapsed 恒 0;tgap += d-1",
|
||||
elapsed=[[(0, 0), (200, 0)]],
|
||||
series=dict(label="tpafe_gaps 累计点数(右轴 0~8)", color=C_EVENT,
|
||||
points=[(0, 0), (20, 0), (20, 2), (60, 2), (60, 4),
|
||||
(100, 4), (100, 6), (200, 6)]),
|
||||
strips=[("events", "ADC_HE_TPAFE_GAP 仅当拍", C_EVENT, 20, 22),
|
||||
("events", "同上", C_EVENT, 60, 62),
|
||||
("events", "同上", C_EVENT, 100, 102)],
|
||||
marks=[(20, 9, "1"), (60, 9, "2")],
|
||||
notes=["1 t=20 ms:d=3 → tgap += 2 + 当拍事件,下一拍事件消失",
|
||||
" (边沿语义,不是电平)",
|
||||
"2 丢帧不入锁存、不告警、不闭锁;两者口径也不同:tgap",
|
||||
" 计帧点数(d-1 加权),adc1_gap 计次数(每次 +1)。",
|
||||
" 台架日志:启动突发期累计 23 次,稳态每 10 s +1~2 次"],
|
||||
),
|
||||
dict(
|
||||
title="D 序号倒退:锁存不可自愈,代价是永久闭锁",
|
||||
seq=["seq: 1050 1051 ... 跳到 995",
|
||||
"d = 995 - 1051 < 0(有符号差)"],
|
||||
verdict="置位后重新对齐基线,不重复触发;位永不自动清",
|
||||
elapsed=[[(0, 0), (200, 0)]],
|
||||
strips=[("flags", "SEQ_BACKWARD 不自动清", C_LATCH, 60, 200),
|
||||
("sticky", "SEQ_BACKWARD 锁存", C_LATCH, 60, 200)],
|
||||
marks=[(60, 9, "1"), (194, 9, "2")],
|
||||
notes=["1 t=60 ms:倒退被检出 → 重新对齐避免每拍重复置位",
|
||||
"2 电平位与锁存位同时置位 → 保护永久退出,直到",
|
||||
" 复位或显式 adc_health_init();这是刻意取舍:",
|
||||
" 计数被改写时相量不可信,宁可拒动不误动"],
|
||||
),
|
||||
]
|
||||
|
||||
FOOT = ("横轴 = 距最后一次序号前进的时间(ms,tick 回绕由有符号差值吸收);红线 = ADC_HEALTH_STALL_MS = 100 ms。"
|
||||
"详见 docs/算法/采样有效性校验设计说明.md")
|
||||
|
||||
|
||||
# ----------------------------- 工具函数 -----------------------------
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True)
|
||||
|
||||
|
||||
def text_w(s, fs):
|
||||
return sum(fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) for ch in s)
|
||||
|
||||
|
||||
WARN = []
|
||||
|
||||
|
||||
def fit(s, fs, avail, where):
|
||||
if text_w(s, fs) > avail:
|
||||
WARN.append("%s: %.0f > %.0f [%s]" % (where, text_w(s, fs), avail, s))
|
||||
|
||||
|
||||
def X(t):
|
||||
return PLOT_X0 + PLOT_W * (t / T_MAX)
|
||||
|
||||
|
||||
def Y(v):
|
||||
return v
|
||||
|
||||
|
||||
# ----------------------------- 生成 -----------------------------
|
||||
def build():
|
||||
b = []
|
||||
add = b.append
|
||||
|
||||
for pn in PANELS:
|
||||
fit(pn["title"], HEAD_FONT, PLOT_W - 40, "panel title")
|
||||
for s in pn["seq"] + [pn["verdict"]]:
|
||||
fit(s, TXT_FONT, 360, "panel seq/verdict")
|
||||
for s in pn["notes"]:
|
||||
fit(s, TXT_FONT, NOTE_W - 24, "panel note")
|
||||
for w in WARN:
|
||||
print("OVERFLOW WARN " + w)
|
||||
|
||||
total_h = FIRST_Y + len(PANELS) * PANEL_H + 74
|
||||
|
||||
add('<text x="%d" y="44" font-size="%d" font-weight="700" fill="%s">%s</text>'
|
||||
% (PAD, TITLE_FONT, INK, esc("采样有效性判定时间轴 · 四个工况的 elapsed 与位存续区间")))
|
||||
add('<text x="%d" y="70" font-size="%d" fill="%s">%s</text>'
|
||||
% (PAD, SUB_FONT, MUTED,
|
||||
esc("elapsed = (int32_t)(now_ms - 最后一次序号前进的时刻);条带 = 位为 1 的存续区间")))
|
||||
|
||||
# 图例
|
||||
lx, ly = PLOT_X0, 116
|
||||
for label, color in [("电平位 flags(可自愈)", C_LEVEL), ("锁存位 sticky / 不自愈", C_LATCH),
|
||||
("边沿事件 events(仅当拍)", C_EVENT), ("静默门限 100 ms", C_TH)]:
|
||||
add('<rect x="%d" y="%d" width="16" height="12" rx="3" fill="%s"/>' % (lx, ly - 10, color))
|
||||
lx += 22
|
||||
add('<text x="%d" y="%d" font-size="12" fill="#475569">%s</text>' % (lx, ly, esc(label)))
|
||||
lx += text_w(label, 12) + 30
|
||||
|
||||
# 列头
|
||||
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#334155">%s</text>'
|
||||
% (PAD, 178, HEAD_FONT, esc("工况与计数")))
|
||||
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#334155">%s</text>'
|
||||
% (PLOT_X0, 178, HEAD_FONT, esc("静默计时 elapsed(ms)与位存续")))
|
||||
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#334155">%s</text>'
|
||||
% (NOTE_X, 178, HEAD_FONT, esc("关键时刻与判读")))
|
||||
|
||||
for pi, pn in enumerate(PANELS):
|
||||
ptop = FIRST_Y + pi * PANEL_H
|
||||
base = ptop + PLOT_H
|
||||
strip_y0 = base + 12
|
||||
|
||||
# 左侧说明块
|
||||
add('<rect x="%d" y="%d" width="%d" height="%d" rx="10" fill="#f8fafc" '
|
||||
'stroke="#64748b" stroke-opacity="0.25"/>' % (PAD, ptop, 370, PLOT_H + 46))
|
||||
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="%s">%s</text>'
|
||||
% (PAD + 14, ptop + 26, HEAD_FONT, INK, esc(pn["title"])))
|
||||
yy = ptop + 50
|
||||
for s in pn["seq"]:
|
||||
add('<text x="%d" y="%d" font-size="%d" fill="#475569">%s</text>'
|
||||
% (PAD + 14, yy, TXT_FONT, esc(s)))
|
||||
yy += 17
|
||||
add('<text x="%d" y="%d" font-size="%d" font-weight="700" fill="#0d9488">%s</text>'
|
||||
% (PAD + 14, yy + 6, TXT_FONT, esc(pn["verdict"])))
|
||||
|
||||
# 坐标框 + 网格
|
||||
add('<rect x="%d" y="%d" width="%d" height="%d" fill="#ffffff" stroke="%s" '
|
||||
'stroke-width="1"/>' % (PLOT_X0, ptop + 4, PLOT_W, PLOT_H, C_AXIS))
|
||||
for gv in (0, 50, 100, 150):
|
||||
gy = base - PLOT_H * (gv / V_MAX)
|
||||
add('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" stroke-width="0.8" '
|
||||
'stroke-dasharray="3 4"/>' % (PLOT_X0, gy, PLOT_X1, gy, C_AXIS))
|
||||
add('<text x="%d" y="%.1f" font-size="%d" fill="%s" text-anchor="end">%s</text>'
|
||||
% (PLOT_X0 - 6, gy + 4, AXIS_FONT, MUTED, str(gv)))
|
||||
for tv in (0, 50, 100, 150, 200):
|
||||
tx = X(tv)
|
||||
add('<line x1="%.1f" y1="%d" x2="%.1f" y2="%d" stroke="%s" stroke-width="1"/>'
|
||||
% (tx, base, tx, base + 4, C_AXIS))
|
||||
add('<text x="%.1f" y="%d" font-size="%d" fill="%s" text-anchor="middle">%s</text>'
|
||||
% (tx, base + 16, AXIS_FONT, MUTED, str(tv)))
|
||||
|
||||
# 门限线
|
||||
ty = base - PLOT_H * (THRESHOLD / V_MAX)
|
||||
add('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" stroke-width="2" '
|
||||
'stroke-dasharray="8 5"/>' % (PLOT_X0, ty, PLOT_X1, ty, C_TH))
|
||||
add('<text x="%d" y="%.1f" font-size="%d" fill="%s">%s</text>'
|
||||
% (PLOT_X1 - 176, ty - 6, AXIS_FONT, C_TH, esc("ADC_HEALTH_STALL_MS = 100")))
|
||||
|
||||
# elapsed 折线
|
||||
for seg in pn["elapsed"]:
|
||||
pts = " ".join("%.1f,%.1f" % (X(t), base - PLOT_H * (v / V_MAX)) for t, v in seg)
|
||||
add('<polyline points="%s" fill="none" stroke="%s" stroke-width="2.6" '
|
||||
'stroke-linejoin="round"/>' % (pts, C_LEVEL))
|
||||
|
||||
# 第二序列(丢帧累计,右轴标度)
|
||||
sd = pn.get("series")
|
||||
if sd:
|
||||
vmax = max(v for _, v in sd["points"]) or 1.0
|
||||
pts = " ".join("%.1f,%.1f" % (X(t), base - PLOT_H * (v / (vmax * 1.15)))
|
||||
for t, v in sd["points"])
|
||||
add('<polyline points="%s" fill="none" stroke="%s" stroke-width="2.4" '
|
||||
'stroke-linejoin="round"/>' % (pts, sd["color"]))
|
||||
add('<text x="%d" y="%d" font-size="%d" fill="%s">%s</text>'
|
||||
% (PLOT_X0 + 8, ptop + 20, AXIS_FONT, sd["color"], esc(sd["label"])))
|
||||
|
||||
# 标记点
|
||||
for t, v, tag in pn.get("marks", []):
|
||||
mx, my = X(t), base - PLOT_H * (v / V_MAX)
|
||||
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%d" stroke="%s" stroke-width="1" '
|
||||
'stroke-dasharray="3 3"/>' % (mx, my, mx, base + 4, MUTED))
|
||||
add('<circle cx="%.1f" cy="%.1f" r="9" fill="%s"/>' % (mx, my, INK))
|
||||
add('<text x="%.1f" y="%.1f" font-size="10.5" font-weight="700" fill="#ffffff" '
|
||||
'text-anchor="middle">%s</text>' % (mx, my + 3.6, esc(tag)))
|
||||
|
||||
# 位存续条带
|
||||
for i in range(2):
|
||||
add('<rect x="%d" y="%d" width="%d" height="%d" rx="4" fill="%s" '
|
||||
'stroke-opacity="0"/>' % (PLOT_X0, strip_y0 + i * (STRIP_H + 4), PLOT_W,
|
||||
STRIP_H, BG_STRIP))
|
||||
add('<text x="%d" y="%d" font-size="%d" fill="%s" text-anchor="end">%s</text>'
|
||||
% (PLOT_X0 - 6, strip_y0 + 11, AXIS_FONT, MUTED, esc("flags")))
|
||||
add('<text x="%d" y="%d" font-size="%d" fill="%s" text-anchor="end">%s</text>'
|
||||
% (PLOT_X0 - 6, strip_y0 + STRIP_H + 15, AXIS_FONT, MUTED, esc("sticky/events")))
|
||||
for name, label, color, t0, t1 in pn["strips"]:
|
||||
row = 0 if name == "flags" else 1
|
||||
sx = X(t0)
|
||||
sw = max(4.0, X(t1) - X(t0))
|
||||
sy = strip_y0 + row * (STRIP_H + 4)
|
||||
add('<rect x="%.1f" y="%.1f" width="%.1f" height="%d" rx="4" fill="%s" '
|
||||
'fill-opacity="0.85"/>' % (sx, sy, sw, STRIP_H, color))
|
||||
lab_x = sx + sw + 8
|
||||
if lab_x + text_w(label, AXIS_FONT) > PLOT_X1 + 4:
|
||||
lab_x = max(PLOT_X0 + 6, sx - text_w(label, AXIS_FONT) - 8)
|
||||
add('<text x="%.1f" y="%.1f" font-size="%d" fill="%s">%s</text>'
|
||||
% (lab_x, sy + 11, AXIS_FONT, color, esc(label)))
|
||||
|
||||
# 右侧判读
|
||||
ny = ptop + 18
|
||||
for s in pn["notes"]:
|
||||
add('<text x="%d" y="%d" font-size="%d" fill="#334155">%s</text>'
|
||||
% (NOTE_X, ny, TXT_FONT, esc(s)))
|
||||
ny += 17
|
||||
|
||||
fy = total_h - 56
|
||||
add('<rect x="%d" y="%d" width="%d" height="42" rx="10" fill="#fffbeb" '
|
||||
'stroke="#d97706" stroke-opacity="0.45"/>' % (PAD, fy, W - 2 * PAD))
|
||||
add('<text x="%d" y="%d" font-size="%d" fill="#7c2d12">%s</text>'
|
||||
% (PAD + 16, fy + 26, TXT_FONT, esc(FOOT)))
|
||||
|
||||
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
|
||||
'viewBox="0 0 %d %d" font-family="%s">\n<rect x="0" y="0" width="%d" height="%d" '
|
||||
'fill="#ffffff"/>\n%s\n</svg>\n'
|
||||
% (W, total_h, W, total_h, FONT, W, total_h, "\n".join(b)))
|
||||
|
||||
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)
|
||||
print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, total_h, OUT))
|
||||
if len(WARN):
|
||||
print("!! %d 处文字可能溢出" % len(WARN))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
Reference in New Issue
Block a user