看门狗 §3.2 流程图重绘为 SVG:任务置位→校验者判定→喂狗 + 时间轴

This commit is contained in:
2026-10-01 06:35:57 +08:00
parent 2d414f8aa9
commit 0ac68605dd
5 changed files with 498 additions and 20 deletions
+126
View File
@@ -0,0 +1,126 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="950" viewBox="0 0 1440 950" 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">IWDG + 单字心跳位图 · 由 defaultTask 独占喂狗权(唯一喂狗者)</text>
<rect x="40" y="98" width="16" height="16" rx="4" fill="#ffffff" stroke="#2563eb" stroke-width="2"/>
<text x="62" y="110" font-size="12" fill="#475569">生产者(任务置位)</text>
<rect x="200" y="98" width="16" height="16" rx="4" fill="#ffffff" stroke="#0d9488" stroke-width="2"/>
<text x="222" y="110" font-size="12" fill="#475569">心跳位图</text>
<rect x="300" y="98" width="16" height="16" rx="4" fill="#ffffff" stroke="#7c3aed" stroke-width="2"/>
<text x="322" y="110" font-size="12" fill="#475569">校验者(喂狗者)</text>
<rect x="448" y="98" width="16" height="16" rx="4" fill="#ffffff" stroke="#16a34a" stroke-width="2"/>
<text x="470" y="110" font-size="12" fill="#475569">喂狗</text>
<rect x="524" y="98" width="16" height="16" rx="4" fill="#ffffff" stroke="#e11d48" stroke-width="2"/>
<text x="546" y="110" font-size="12" fill="#475569">停喂 / 复位</text>
<rect x="40" y="148" width="470" height="584" rx="14" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.28"/>
<text x="60" y="178" font-size="15.0" font-weight="700" fill="#334155">各任务主循环(生产者)—— 每轮只置位,不清零</text>
<text x="60" y="202" font-size="13" font-weight="700" fill="#2563eb">s_hb_bits |= (1u &lt;&lt; MY_ID)</text>
<rect x="62.0" y="220.0" width="426.0" height="62" rx="9" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.5"/>
<rect x="74.0" y="230.0" width="34" height="24" rx="6" fill="#2563eb"/>
<text x="91.0" y="246.5" font-size="12" font-weight="700" fill="#ffffff" text-anchor="middle">b0</text>
<text x="118.0" y="246.0" font-size="14" font-weight="700" fill="#0f172a">defaultTask</text>
<text x="118.0" y="266.0" font-size="11.5" fill="#64748b">配置属主 / syslog 唯一写者 · 50ms 基准</text>
<rect x="62.0" y="300.0" width="426.0" height="62" rx="9" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.5"/>
<rect x="74.0" y="310.0" width="34" height="24" rx="6" fill="#2563eb"/>
<text x="91.0" y="326.5" font-size="12" font-weight="700" fill="#ffffff" text-anchor="middle">b1</text>
<text x="118.0" y="326.0" font-size="14" font-weight="700" fill="#0f172a">netTask</text>
<text x="118.0" y="346.0" font-size="11.5" fill="#64748b">net_poll 循环 · 1ms 节拍</text>
<rect x="62.0" y="380.0" width="426.0" height="62" rx="9" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.5"/>
<rect x="74.0" y="390.0" width="34" height="24" rx="6" fill="#2563eb"/>
<text x="91.0" y="406.5" font-size="12" font-weight="700" fill="#ffffff" text-anchor="middle">b2</text>
<text x="118.0" y="406.0" font-size="14" font-weight="700" fill="#0f172a">ftpTask</text>
<text x="118.0" y="426.0" font-size="11.5" fill="#64748b">lftpd 事件循环(需在 lftpd 侧埋点)</text>
<rect x="62.0" y="460.0" width="426.0" height="62" rx="9" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.5"/>
<rect x="74.0" y="470.0" width="34" height="24" rx="6" fill="#2563eb"/>
<text x="91.0" y="486.5" font-size="12" font-weight="700" fill="#ffffff" text-anchor="middle">b4</text>
<text x="118.0" y="486.0" font-size="14" font-weight="700" fill="#0f172a">uiTask</text>
<text x="118.0" y="506.0" font-size="11.5" fill="#64748b">LVGL 主循环 · 10ms 节拍</text>
<rect x="62.0" y="540.0" width="426.0" height="62" rx="9" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.5"/>
<rect x="74.0" y="550.0" width="34" height="24" rx="6" fill="#2563eb"/>
<text x="91.0" y="566.5" font-size="12" font-weight="700" fill="#ffffff" text-anchor="middle">b5</text>
<text x="118.0" y="566.0" font-size="14" font-weight="700" fill="#0f172a">timeSyncTask</text>
<text x="118.0" y="586.0" font-size="11.5" fill="#64748b">NTP 轮询 · ≤50ms</text>
<rect x="62.0" y="626.0" width="426.0" height="48" rx="9" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.6"/>
<text x="76.0" y="646.0" font-size="12" font-weight="700" fill="#0d9488">心跳位图 s_hb_bits(32 位单字)</text>
<text x="76.0" y="664.0" font-size="11" fill="#64748b">就绪掩码 0x37 = bit0|1|2|4|5(bit3=testTask 默认不在内)</text>
<rect x="76.0" y="682.0" width="34" height="22" rx="4" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.70"/>
<text x="93.0" y="697.0" font-size="11" font-weight="700" fill="#0d9488" text-anchor="middle">0</text>
<rect x="116.0" y="682.0" width="34" height="22" rx="4" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.70"/>
<text x="133.0" y="697.0" font-size="11" font-weight="700" fill="#0d9488" text-anchor="middle">1</text>
<rect x="156.0" y="682.0" width="34" height="22" rx="4" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.70"/>
<text x="173.0" y="697.0" font-size="11" font-weight="700" fill="#0d9488" text-anchor="middle">2</text>
<rect x="196.0" y="682.0" width="34" height="22" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-opacity="0.40"/>
<text x="213.0" y="697.0" font-size="11" font-weight="700" fill="#94a3b8" text-anchor="middle">3</text>
<rect x="236.0" y="682.0" width="34" height="22" rx="4" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.70"/>
<text x="253.0" y="697.0" font-size="11" font-weight="700" fill="#0d9488" text-anchor="middle">4</text>
<rect x="276.0" y="682.0" width="34" height="22" rx="4" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.70"/>
<text x="293.0" y="697.0" font-size="11" font-weight="700" fill="#0d9488" text-anchor="middle">5</text>
<line x1="488.0" y1="251.0" x2="484.0" y2="251.0" stroke="#2563eb" stroke-width="2.2"/>
<line x1="488.0" y1="331.0" x2="484.0" y2="331.0" stroke="#2563eb" stroke-width="2.2"/>
<line x1="488.0" y1="411.0" x2="484.0" y2="411.0" stroke="#2563eb" stroke-width="2.2"/>
<line x1="488.0" y1="491.0" x2="484.0" y2="491.0" stroke="#2563eb" stroke-width="2.2"/>
<line x1="488.0" y1="571.0" x2="484.0" y2="571.0" stroke="#2563eb" stroke-width="2.2"/>
<line x1="484.0" y1="251.0" x2="484.0" y2="650.0" stroke="#2563eb" stroke-width="2.2"/>
<line x1="484.0" y1="650.0" x2="488.0" y2="650.0" stroke="#2563eb" stroke-width="2.2"/>
<path d="M 488.0 650.0 L 477.0 655.5 L 477.0 644.5 z" fill="#2563eb"/>
<rect x="544" y="148" width="856" height="584" rx="14" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.28"/>
<text x="564" y="178" font-size="15.0" font-weight="700" fill="#334155">defaultTask 主循环(唯一喂狗者 · 消费者)</text>
<rect x="566.0" y="204.0" width="812.0" height="62.0" rx="10" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
<text x="580.0" y="228.0" font-size="14" font-weight="700" fill="#0f172a">① 节流:每 100ms 抽样一次</text>
<text x="580.0" y="246.5" font-size="11.5" fill="#64748b">now - last_tick &lt; 100ms 则直接返回(defaultTask 50ms 循环的整数倍)</text>
<line x1="972.0" y1="266.0" x2="972.0" y2="306.0" stroke="#7c3aed" stroke-width="2.2"/>
<path d="M 972.0 306.0 L 966.5 295.0 L 977.5 295.0 z" fill="#7c3aed"/>
<rect x="566.0" y="306.0" width="812.0" height="62.0" rx="10" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.55"/>
<text x="580.0" y="330.0" font-size="14" font-weight="700" fill="#0f172a">② 取走并清零(单字原子)</text>
<text x="580.0" y="348.5" font-size="11.5" fill="#64748b">snapshot = s_hb_bits; s_hb_bits = 0; ← 读-改-写天然原子,免临界区</text>
<line x1="972.0" y1="368.0" x2="972.0" y2="408.0" stroke="#0d9488" stroke-width="2.2"/>
<path d="M 972.0 408.0 L 966.5 397.0 L 977.5 397.0 z" fill="#0d9488"/>
<rect x="566.0" y="408.0" width="812.0" height="62.0" rx="10" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.55"/>
<text x="580.0" y="432.0" font-size="14" font-weight="700" fill="#0f172a">③ 掩码判定</text>
<text x="580.0" y="450.5" font-size="11.5" fill="#64748b">snapshot == 0x37(bit0|1|2|4|5 全中)?</text>
<line x1="488.0" y1="650.0" x2="544.0" y2="650.0" stroke="#0d9488" stroke-width="2.2"/>
<line x1="544.0" y1="650.0" x2="544.0" y2="337.0" stroke="#0d9488" stroke-width="2.2"/>
<line x1="544.0" y1="337.0" x2="566.0" y2="337.0" stroke="#0d9488" stroke-width="2.2"/>
<path d="M 566.0 337.0 L 555.0 342.5 L 555.0 331.5 z" fill="#0d9488"/>
<text x="494.0" y="642.0" font-size="11.5" fill="#0d9488" text-anchor="middle">快照</text>
<line x1="972.0" y1="470.0" x2="972.0" y2="484.0" stroke="#7c3aed" stroke-width="2.2"/>
<line x1="763.0" y1="484.0" x2="763.0" y2="484.0" stroke="#16a34a" stroke-width="2.2"/>
<line x1="972.0" y1="484.0" x2="1181.0" y2="484.0" stroke="#d97706" stroke-width="2.2"/>
<line x1="763.0" y1="484.0" x2="763.0" y2="486.0" stroke="#16a34a" stroke-width="2.2"/>
<path d="M 763.0 486.0 L 757.5 475.0 L 768.5 475.0 z" fill="#16a34a"/>
<line x1="1181.0" y1="484.0" x2="1181.0" y2="486.0" stroke="#d97706" stroke-width="2.2"/>
<path d="M 1181.0 486.0 L 1175.5 475.0 L 1186.5 475.0 z" fill="#d97706"/>
<text x="763.0" y="480.0" font-size="11.5" font-weight="700" fill="#16a34a" text-anchor="middle">是</text>
<text x="1181.0" y="480.0" font-size="11.5" font-weight="700" fill="#d97706" text-anchor="middle">否</text>
<rect x="566.0" y="486.0" width="394.0" height="84.0" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="580.0" y="510.0" font-size="13" font-weight="700" fill="#0f172a">全员到位 → 喂狗</text>
<text x="580.0" y="527.5" font-size="10.5" fill="#64748b">s_miss_streak = 0</text>
<text x="580.0" y="545.0" font-size="10.5" fill="#64748b">IWDG-&gt;KR = 0xAAAA;</text>
<rect x="984.0" y="486.0" width="394.0" height="84.0" rx="10" fill="#fffbeb" stroke="#d97706" stroke-opacity="0.55"/>
<text x="998.0" y="510.0" font-size="13" font-weight="700" fill="#0f172a">有缺席 → 累计,不喂狗</text>
<text x="998.0" y="527.5" font-size="10.5" fill="#64748b">s_miss_total++ / streak++</text>
<text x="998.0" y="545.0" font-size="10.5" fill="#64748b">连续 8 次 → 打 [WD] task stall 后停喂</text>
<rect x="40" y="766" width="1360" height="150" rx="14" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.28"/>
<text x="60" y="794" font-size="15.0" font-weight="700" fill="#334155">停喂之后:时间轴上的三个时刻(0.8s 停喂 → 1.7s 最坏溢出 → 2.5s 标称)</text>
<rect x="100.0" y="828.0" width="396.8" height="14" rx="3" fill="#16a34a" opacity="0.75"/>
<text x="298.4" y="862.0" font-size="11.5" fill="#16a34a" text-anchor="middle">仍照常喂狗</text>
<text x="298.4" y="878.0" font-size="10.5" fill="#94a3b8" text-anchor="middle">miss_streak 1 → 8</text>
<rect x="496.8" y="828.0" width="446.4" height="14" rx="3" fill="#e11d48" opacity="0.75"/>
<text x="720.0" y="862.0" font-size="11.5" fill="#e11d48" text-anchor="middle">已停喂 → 等 IWDG 自然溢出</text>
<text x="720.0" y="878.0" font-size="10.5" fill="#94a3b8" text-anchor="middle">worst case(LSI 47kHz)</text>
<rect x="943.2" y="828.0" width="396.8" height="14" rx="3" fill="#d97706" opacity="0.75"/>
<text x="1141.6" y="862.0" font-size="11.5" fill="#d97706" text-anchor="middle">标称溢出窗口</text>
<text x="1141.6" y="878.0" font-size="10.5" fill="#94a3b8" text-anchor="middle">LSI 32kHz</text>
<line x1="100.0" y1="822.0" x2="100.0" y2="842.0" stroke="#475569" stroke-width="1.6"/>
<text x="100.0" y="816.0" font-size="11" font-weight="700" fill="#475569" text-anchor="middle">0s</text>
<line x1="496.8" y1="822.0" x2="496.8" y2="842.0" stroke="#475569" stroke-width="1.6"/>
<text x="496.8" y="816.0" font-size="11" font-weight="700" fill="#475569" text-anchor="middle">0.8s</text>
<line x1="943.2" y1="822.0" x2="943.2" y2="842.0" stroke="#475569" stroke-width="1.6"/>
<text x="943.2" y="816.0" font-size="11" font-weight="700" fill="#475569" text-anchor="middle">1.7s</text>
<line x1="1340.0" y1="822.0" x2="1340.0" y2="842.0" stroke="#475569" stroke-width="1.6"/>
<text x="1340.0" y="816.0" font-size="11" font-weight="700" fill="#475569" text-anchor="middle">2.5s</text>
<line x1="496.8" y1="824.0" x2="496.8" y2="848.0" stroke="#e11d48" stroke-width="1.6" stroke-dasharray="5 4"/>
<line x1="943.2" y1="824.0" x2="943.2" y2="848.0" stroke="#d97706" stroke-width="1.6" stroke-dasharray="5 4"/>
<line x1="496.8" y1="896.0" x2="943.2" y2="896.0" stroke="#16a34a" stroke-width="1.6" stroke-dasharray="3 3"/>
<text x="720.0" y="912.0" font-size="11" fill="#16a34a" text-anchor="middle">裕度 ≈ 900ms</text>
<text x="40" y="936" font-size="12" fill="#64748b">喂狗 = &quot;所有被监视任务都在 100ms 窗口内动过&quot;;任一任务卡死 → 位图长期残缺 → 停喂 → IWDG 复位(保留 IWDGRSTF 权威归因)。对应 task_watchdog.c / 设计说明 §3.2。</text>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

+16 -20
View File
@@ -204,30 +204,26 @@ IWDG 是**"写键值"式外设**:写 `0x5555` 解锁 → 配 `PR`/`RLR` →
### 3.2 采用方案:任务置位 → 校验者判定 → 校验者喂狗
```
┌──────────────── 各自任务循环 ────────────────┐
│ defaultTask / netTask / ftpTask / uiTask │
│ / timeSyncTask / testTask │
│ 每轮循环:s_hb_bits |= (1u << MY_ID) │ ← 无锁,单字原子写
└───────────────────────┬──────────────────────┘
│ (位图,仅置位,不清零)
▼
┌──────────── defaultTask(唯一喂狗者)─────────┐
│ 每 100 ms 抽样: │
│ snapshot = s_hb_bits; s_hb_bits = 0; │ ← 取走并清零
│ if (snapshot != ALL_REQUIRED_MASK) │
│ → 有任务在 100 ms 内没动过 │
│ → 累计未达标次数 / 记录缺失任务名 │
│ if (连续 N 次未达标) HAL_IWDG_Refresh(); │ ← 只在全员健康时喂
└───────────────────────────────────────────────┘
│
▼
IWDG(超时 2.5 s,PR=4 / RLR=1249)
```
![看门狗心跳模型:任务置位 → 校验者判定 → 校验者喂狗](../images/看门狗心跳模型.svg)
> 图源:`tools/arch_diagram/gen_watchdog_svg.py`(改图改脚本重跑,见
> [`../架构设计/图形绘制流程.md`](../架构设计/图形绘制流程.md))。
**语义**:喂狗 = "**所有被监视的任务都在窗口内动过**"。
任何单个任务卡死 → 位图长期残缺 → 喂狗停止 → 2.5 s 后 IWDG 复位。
具体到代码(`App/util/task_watchdog.c`):
| 阶段 | 代码 |
|---|---|
| ① 任务置位(生产者) | `s_hb_bits \|= (1u << id)`(`task_wd_beat`,仅置位不清零) |
| ② 校验者取走清零 | `snapshot = s_hb_bits; s_hb_bits = 0u;`(`task_wd_poll`,单字原子) |
| ③ 掩码判定 | `snapshot == wd_required_mask()`(默认 `0x37` = bit0\|1\|2\|4\|5) |
| ④ 判定为真 → 喂狗 | `s_miss_streak = 0u; IWDG->KR = 0x0000AAAAu;` |
| ④ 判定为假 → 累计 | `s_miss_total++`;连续 8 次(800 ms)后打 `[WD] task stall` 并**停喂** |
> 图中时间轴的四个时刻(0 / 0.8 s / 1.7 s / 2.5 s)与约 900 ms 裕度的推导见 §4.3。
### 3.3 为什么心跳位图用"取走并清零"
```
+1
View File
@@ -48,6 +48,7 @@
| `gen_cfg_arch_svg.py` | `docs/images/配置文件分层架构.svg` | 配置引擎分层 |
| `gen_net_stack_svg.py` | `docs/images/BSD网络栈分层.svg` | BSD 网络栈分层 |
| `gen_memory_svg.py` | `docs/images/内存布局_*.svg` | RAM / FLASH 布局 |
| `gen_watchdog_svg.py` | `docs/images/看门狗心跳模型.svg` | `TASKS` / `REQ_BITS` / `POLL_STEPS` / `TIMELINE` |
---
+1
View File
@@ -15,6 +15,7 @@
| `gen_frame_svg.py` | 协议帧格式图 | RemoDispBus 帧结构 |
| `gen_cfg_arch_svg.py` | 配置引擎架构图 | 配置分层与持久化 |
| `gen_partition_svg.py` | 分区图 | 存储分区 |
| `gen_watchdog_svg.py` | 看门狗心跳模型图 | IWDG + 任务心跳位图(置位→判定→喂狗 + 时间轴) |
## 使用方式
+354
View File
@@ -0,0 +1,354 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成 docs/images/看门狗心跳模型.svg —— IWDG + 任务心跳位图流程图(纯标准库)。
对应 docs/子系统设计/看门狗设计说明.md §3.2「采用方案:
任务置位 → 校验者判定 → 校验者喂狗」。
结构:左右两栏 + 底部时间轴
左 = 各任务循环(生产者):s_hb_bits |= (1u << MY_ID),仅置位不清零
右 = defaultTask 唯一喂狗者(消费者):100ms 抽样 → 取走清零 → 掩码判定
全中 → 喂狗;有缺 → 累计 miss_streak(容忍 8 次)
下 = 停喂 → IWDG 自然溢出 → 复位(时间轴:0 / 0.8s / 1.7s / 2.5s)
为什么不用 Mermaid:原 ASCII 图是"生产者-消费者 + 判定分支",需要精确对齐
位图行与判定框,且时间轴的"包含关系"必须按比例表达,Mermaid 做不到。
用法(仓库根目录,任意 python3):
python tools/arch_diagram/gen_watchdog_svg.py
输出:
docs/images/看门狗心跳模型.svg
改流程时:编辑本文件顶部的 TASKS / BITS / POLL_STEPS / TIMELINE 数据后重跑。
"""
import html
import math
import os
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
OUT = os.path.join(ROOT, "docs", "images", "看门狗心跳模型.svg")
# ----------------------------- 画布与样式 -----------------------------
W = 1440
PAD = 40
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
TITLE_FONT = 21
SUB_FONT = 13
PANEL_TITLE_FONT = 15
CARD_TITLE_FONT = 14
CARD_NOTE_FONT = 11.5
LABEL_FONT = 11.5
FOOT_FONT = 12
# 调色板(docs/架构设计/图形绘制流程.md §3.1)
C_PROD, BG_PROD = "#2563eb", "#eff6ff" # 蓝:生产者(任务置位)
C_BIT, BG_BIT = "#0d9488", "#f0fdfa" # 青:心跳位图
C_CONS, BG_CONS = "#7c3aed", "#f5f3ff" # 紫:校验者(喂狗者)
C_OK = "#16a34a" # 绿:喂狗
C_WARN, BG_WARN = "#d97706", "#fffbeb" # 琥珀:累计缺失
C_FAIL, BG_FAIL = "#e11d48", "#fff1f2" # 玫红:停喂 / 复位
ARROW_W = 2.2
# ----------------------------- 数据(改这里) -----------------------------
# 任务(生产者):bit = 心跳位序号(task_watchdog.h 的枚举值)
TASKS = [
dict(name="defaultTask", bit=0, note="配置属主 / syslog 唯一写者 · 50ms 基准"),
dict(name="netTask", bit=1, note="net_poll 循环 · 1ms 节拍"),
dict(name="ftpTask", bit=2, note="lftpd 事件循环(需在 lftpd 侧埋点)"),
dict(name="uiTask", bit=4, note="LVGL 主循环 · 10ms 节拍"),
dict(name="timeSyncTask", bit=5, note="NTP 轮询 · ≤50ms"),
]
# 必需掩码(wd_required_mask):5 位全要;bit3 = testTask 不在默认掩码
REQ_BITS = [0, 1, 2, 4, 5]
# 校验者判定步骤(右栏自上而下)
POLL_STEPS = [
dict(title="① 节流:每 100ms 抽样一次",
note="now - last_tick < 100ms 则直接返回(defaultTask 50ms 循环的整数倍)",
color=C_CONS, bg=BG_CONS),
dict(title="② 取走并清零(单字原子)",
note="snapshot = s_hb_bits; s_hb_bits = 0; ← 读-改-写天然原子,免临界区",
color=C_BIT, bg=BG_BIT),
dict(title="③ 掩码判定",
note="snapshot == 0x37(bit0|1|2|4|5 全中)?",
color=C_CONS, bg=BG_CONS),
]
# 判定分支结果
BRANCH_OK = dict(title="全员到位 → 喂狗",
note="s_miss_streak = 0",
note2="IWDG->KR = 0xAAAA;",
color=C_OK, bg="#f0fdf4")
BRANCH_NG = dict(title="有缺席 → 累计,不喂狗",
note="s_miss_total++ / streak++",
note2="连续 8 次 → 打 [WD] task stall 后停喂",
color=C_WARN, bg=BG_WARN)
# 时间轴刻度(秒)与事件
T_AXIS = [0.0, 0.8, 1.7, 2.5]
TIMELINE = [
dict(seg=(0.0, 0.8), label="仍照常喂狗", sub="miss_streak 1 → 8", color=C_OK),
dict(seg=(0.8, 1.7), label="已停喂 → 等 IWDG 自然溢出", sub="worst case(LSI 47kHz)", color=C_FAIL),
dict(seg=(1.7, 2.5), label="标称溢出窗口", sub="LSI 32kHz", color=C_WARN),
]
FOOT = ("喂狗 = \"所有被监视任务都在 100ms 窗口内动过\";任一任务卡死 → 位图长期残缺 → "
"停喂 → IWDG 复位(保留 IWDGRSTF 权威归因)。对应 task_watchdog.c / 设计说明 §3.2。")
# ----------------------------- 工具函数 -----------------------------
def esc(s):
return html.escape(s, quote=True)
def text_w(s, fs):
"""粗略估算文本像素宽:CJK/全角 ≈ 1.0em,其余 ≈ 0.58em(宁可偏大)。"""
w = 0.0
for ch in s:
w += fs * (1.0 if ord(ch) >= 0x2E80 else 0.58)
return w
# ----------------------------- 生成 -----------------------------
def build():
out = []
add = out.append
def line(x1, y1, x2, y2, color, head=True, width=ARROW_W, dash=False):
d = ' stroke-dasharray="7 5"' if dash else ''
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" '
'stroke-width="%.1f"%s/>' % (x1, y1, x2, y2, color, width, d))
if head:
ang = math.atan2(y2 - y1, x2 - x1)
hl, hw = 11.0, 5.5
bx, by = x2 - hl * math.cos(ang), y2 - hl * math.sin(ang)
px, py = -math.sin(ang) * hw, math.cos(ang) * hw
add('<path d="M %.1f %.1f L %.1f %.1f L %.1f %.1f z" fill="%s"/>'
% (x2, y2, bx + px, by + py, bx - px, by - py, color))
def box(x, y, w, h, color, bg, title, note, title_fs=CARD_TITLE_FONT,
note_fs=CARD_NOTE_FONT, stroke_op=0.55, note2=None):
add('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="10" '
'fill="%s" stroke="%s" stroke-opacity="%.2f"/>'
% (x, y, w, h, bg, color, stroke_op))
add('<text x="%.1f" y="%.1f" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
% (x + 14, y + 24, title_fs, esc(title)))
add('<text x="%.1f" y="%.1f" font-size="%.1f" fill="#64748b">%s</text>'
% (x + 14, y + 24 + note_fs + 7, note_fs, esc(note)))
if note2:
add('<text x="%.1f" y="%.1f" font-size="%.1f" fill="#64748b">%s</text>'
% (x + 14, y + 24 + (note_fs + 7) * 2, note_fs, esc(note2)))
# ---- 布局参数 ----
PANEL_Y = 148
PANEL_H = 584 # 由左栏内容(5 任务 + 心跳位图带 + 位格)反推
LB_X, LB_W = PAD, 470 # 左栏:任务(生产者)
RB_X = LB_X + LB_W + 34 # 右栏:校验者(消费者)
RB_W = W - PAD - RB_X
TASK_H, TASK_GAP = 62, 18
sx = LB_X + 22
sw = LB_W - 44
# ---- 标题 ----
add('<text x="%d" y="44" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
% (PAD, TITLE_FONT, esc("看门狗心跳模型 · 任务置位 → 校验者判定 → 校验者喂狗")))
add('<text x="%d" y="70" font-size="%d" fill="#64748b">%s</text>'
% (PAD, SUB_FONT,
esc("IWDG + 单字心跳位图 · 由 defaultTask 独占喂狗权(唯一喂狗者)")))
# ---- 图例 ----
lx, ly = PAD, 110
for color, label in [(C_PROD, "生产者(任务置位)"), (C_BIT, "心跳位图"),
(C_CONS, "校验者(喂狗者)"), (C_OK, "喂狗"),
(C_FAIL, "停喂 / 复位")]:
add('<rect x="%d" y="%d" width="16" height="16" rx="4" fill="#ffffff" '
'stroke="%s" stroke-width="2"/>' % (lx, ly - 12, 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('<rect x="%d" y="%d" width="%d" height="%d" rx="14" fill="#f8fafc" '
'stroke="#64748b" stroke-opacity="0.28"/>' % (LB_X, PANEL_Y, LB_W, PANEL_H))
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#334155">%s</text>'
% (LB_X + 20, PANEL_Y + 30, PANEL_TITLE_FONT,
esc("各任务主循环(生产者)—— 每轮只置位,不清零")))
add('<text x="%d" y="%d" font-size="13" font-weight="700" fill="%s">%s</text>'
% (LB_X + 20, PANEL_Y + 54, C_PROD, esc("s_hb_bits |= (1u << MY_ID)")))
task_y = []
y = PANEL_Y + 72
for t in TASKS:
add('<rect x="%.1f" y="%.1f" width="%.1f" height="%d" rx="9" fill="%s" '
'stroke="%s" stroke-opacity="0.5"/>' % (sx, y, sw, TASK_H, BG_PROD, C_PROD))
# 位号徽标
add('<rect x="%.1f" y="%.1f" width="34" height="24" rx="6" fill="%s"/>'
% (sx + 12, y + 10, C_PROD))
add('<text x="%.1f" y="%.1f" font-size="12" font-weight="700" fill="#ffffff" '
'text-anchor="middle">%s</text>'
% (sx + 29, y + 26.5, esc("b" + str(t["bit"]))))
add('<text x="%.1f" y="%.1f" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
% (sx + 56, y + 26, CARD_TITLE_FONT, esc(t["name"])))
add('<text x="%.1f" y="%.1f" font-size="%.1f" fill="#64748b">%s</text>'
% (sx + 56, y + 46, CARD_NOTE_FONT, esc(t["note"])))
task_y.append(y + TASK_H / 2.0)
y += TASK_H + TASK_GAP
# ---- 心跳位图(左栏底部横带) ----
bm_y = y + 6
bm_h = 48
add('<rect x="%.1f" y="%.1f" width="%.1f" height="%d" rx="9" fill="%s" '
'stroke="%s" stroke-opacity="0.6"/>' % (sx, bm_y, sw, bm_h, BG_BIT, C_BIT))
add('<text x="%.1f" y="%.1f" font-size="12" font-weight="700" fill="%s">%s</text>'
% (sx + 14, bm_y + 20, C_BIT, esc("心跳位图 s_hb_bits(32 位单字)")))
add('<text x="%.1f" y="%.1f" font-size="11" fill="#64748b">%s</text>'
% (sx + 14, bm_y + 38, esc("就绪掩码 0x37 = bit0|1|2|4|5(bit3=testTask 默认不在内)")))
# 各位方格
bx0 = sx + 14
bw, bgap = 34, 6
for i in range(6):
cx = bx0 + i * (bw + bgap)
cy = bm_y + bm_h + 8
on = i in REQ_BITS
add('<rect x="%.1f" y="%.1f" width="%d" height="22" rx="4" fill="%s" '
'stroke="%s" stroke-opacity="%.2f"/>'
% (cx, cy, bw, BG_BIT if on else "#f1f5f9", C_BIT if on else "#94a3b8",
0.7 if on else 0.4))
add('<text x="%.1f" y="%.1f" font-size="11" font-weight="700" fill="%s" '
'text-anchor="middle">%s</text>'
% (cx + bw / 2.0, cy + 15, C_BIT if on else "#94a3b8", esc(str(i))))
# 各任务 → 位图:右侧竖直总线汇入位图带右缘
bus_x = sx + sw - 4
for t in TASKS:
ty = task_y[TASKS.index(t)]
line(sx + sw, ty, bus_x, ty, C_PROD, head=False)
line(bus_x, task_y[0], bus_x, bm_y + bm_h / 2.0, C_PROD, head=False)
line(bus_x, bm_y + bm_h / 2.0, sx + sw, bm_y + bm_h / 2.0, C_PROD)
# ---- 右栏:校验者(defaultTask) ----
add('<rect x="%d" y="%d" width="%d" height="%d" rx="14" fill="#f8fafc" '
'stroke="#64748b" stroke-opacity="0.28"/>' % (RB_X, PANEL_Y, RB_W, PANEL_H))
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#334155">%s</text>'
% (RB_X + 20, PANEL_Y + 30, PANEL_TITLE_FONT,
esc("defaultTask 主循环(唯一喂狗者 · 消费者)")))
st_x = RB_X + 22
st_w = RB_W - 44
STEP_H, STEP_GAP2 = 62, 40
sy = PANEL_Y + 56
step_cy = []
for i, s in enumerate(POLL_STEPS):
box(st_x, sy, st_w, STEP_H, s["color"], s["bg"], s["title"], s["note"])
step_cy.append(sy + STEP_H / 2.0)
if i < len(POLL_STEPS) - 1:
line(st_x + st_w / 2, sy + STEP_H, st_x + st_w / 2, sy + STEP_H + STEP_GAP2,
s["color"])
sy += STEP_H + STEP_GAP2
# 位图 → 校验者 ②(取走清零):正交走线(右出 → 上折 → 入盒)
tap_y = bm_y + bm_h / 2.0
line(sx + sw, tap_y, st_x - 22, tap_y, C_BIT, head=False)
line(st_x - 22, tap_y, st_x - 22, step_cy[1], C_BIT, head=False)
line(st_x - 22, step_cy[1], st_x, step_cy[1], C_BIT)
add('<text x="%.1f" y="%.1f" font-size="%.1f" fill="%s" text-anchor="middle">%s</text>'
% (sx + sw + 6, tap_y - 8, LABEL_FONT, C_BIT, esc("快照")))
# ---- 判定分支:喂狗 / 累计停喂 ----
br_y = sy - STEP_GAP2 + 16
br_w = (st_w - 24) / 2.0
ok_x, ng_x = st_x, st_x + br_w + 24
# 分支引线:从 ③ 下沿分叉(fork 线画在 ③ 之下 8px 处,避免压住盒底)
fork_y = step_cy[2] + STEP_H / 2.0
line(st_x + st_w / 2, fork_y, st_x + st_w / 2, fork_y + 14, C_CONS, head=False)
line(st_x + br_w / 2.0, fork_y + 14, ok_x + br_w / 2.0, fork_y + 14, C_OK, head=False)
line(st_x + st_w / 2, fork_y + 14, ng_x + br_w / 2.0, fork_y + 14, C_WARN, head=False)
line(ok_x + br_w / 2.0, fork_y + 14, ok_x + br_w / 2.0, br_y, C_OK)
line(ng_x + br_w / 2.0, fork_y + 14, ng_x + br_w / 2.0, br_y, C_WARN)
add('<text x="%.1f" y="%.1f" font-size="11.5" font-weight="700" fill="%s" '
'text-anchor="middle">%s</text>'
% (ok_x + br_w / 2.0, br_y - 6, C_OK, esc("是")))
add('<text x="%.1f" y="%.1f" font-size="11.5" font-weight="700" fill="%s" '
'text-anchor="middle">%s</text>'
% (ng_x + br_w / 2.0, br_y - 6, C_WARN, esc("否")))
br_h = 84
box(ok_x, br_y, br_w, br_h, BRANCH_OK["color"], BRANCH_OK["bg"],
BRANCH_OK["title"], BRANCH_OK["note"], title_fs=13, note_fs=10.5,
note2=BRANCH_OK["note2"])
box(ng_x, br_y, br_w, br_h, BRANCH_NG["color"], BRANCH_NG["bg"],
BRANCH_NG["title"], BRANCH_NG["note"], title_fs=13, note_fs=10.5,
note2=BRANCH_NG["note2"])
# ---- 时间轴 ----
tl_y = PANEL_Y + PANEL_H + 34
tl_h = 150
add('<rect x="%d" y="%d" width="%d" height="%d" rx="14" fill="#f8fafc" '
'stroke="#64748b" stroke-opacity="0.28"/>' % (PAD, tl_y, W - 2 * PAD, tl_h))
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#334155">%s</text>'
% (PAD + 20, tl_y + 28, PANEL_TITLE_FONT,
esc("停喂之后:时间轴上的三个时刻(0.8s 停喂 → 1.7s 最坏溢出 → 2.5s 标称)")))
ax_x0, ax_x1 = PAD + 60, W - PAD - 60
ax_y = tl_y + 62
span = T_AXIS[-1]
def ax_pos(t):
return ax_x0 + (ax_x1 - ax_x0) * (t / span)
# 分段色带
for it in TIMELINE:
s0, s1 = it["seg"]
x0, x1 = ax_pos(s0), ax_pos(s1)
add('<rect x="%.1f" y="%.1f" width="%.1f" height="14" rx="3" fill="%s" '
'opacity="0.75"/>' % (x0, ax_y, x1 - x0, it["color"], ))
add('<text x="%.1f" y="%.1f" font-size="11.5" fill="%s" text-anchor="middle">%s</text>'
% ((x0 + x1) / 2.0, ax_y + 34, it["color"], esc(it["label"])))
add('<text x="%.1f" y="%.1f" font-size="10.5" fill="#94a3b8" text-anchor="middle">%s</text>'
% ((x0 + x1) / 2.0, ax_y + 50, esc(it["sub"])))
# 刻度
for t in T_AXIS:
x = ax_pos(t)
line(x, ax_y - 6, x, ax_y + 14, "#475569", head=False, width=1.6)
add('<text x="%.1f" y="%.1f" font-size="11" font-weight="700" fill="#475569" '
'text-anchor="middle">%ss</text>' % (x, ax_y - 12, esc(("%g" % t))))
# 停喂点竖标 + 裕度
x_stop, x_worst = ax_pos(0.8), ax_pos(1.7)
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="1.6" '
'stroke-dasharray="5 4"/>' % (x_stop, ax_y - 4, x_stop, ax_y + 20, C_FAIL))
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="1.6" '
'stroke-dasharray="5 4"/>' % (x_worst, ax_y - 4, x_worst, ax_y + 20, C_WARN))
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="1.6" '
'stroke-dasharray="3 3"/>' % (x_stop, ax_y + 68, x_worst, ax_y + 68, C_OK))
add('<text x="%.1f" y="%.1f" font-size="11" fill="%s" text-anchor="middle">%s</text>'
% ((x_stop + x_worst) / 2.0, ax_y + 84, C_OK, esc("裕度 ≈ 900ms")))
# ---- 页脚 ----
total_h = tl_y + tl_h + 34
add('<text x="%d" y="%d" font-size="%d" fill="#64748b">%s</text>'
% (PAD, total_h - 14, 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)))
return svg, total_h
def main():
svg, h = build()
os.makedirs(os.path.dirname(OUT), exist_ok=True)
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
f.write(svg)
import xml.etree.ElementTree as ET
ET.fromstring(svg)
print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, h, OUT))
if __name__ == "__main__":
main()