diff --git a/docs/RemoDispBus协议与接收设计说明.md b/docs/RemoDispBus协议与接收设计说明.md index 7b8c15d2..5f5d9594 100644 --- a/docs/RemoDispBus协议与接收设计说明.md +++ b/docs/RemoDispBus协议与接收设计说明.md @@ -52,14 +52,6 @@ RemoDispBus 让远端(PC 上的 DTU-RemoteLCD 客户端 / 浏览器页面) | 4 | `DATA` | `LEN` | 数据段(命令相关,见 §1.5) | | 4+LEN | `CRC` | 1 | 数据段逐字节异或(低 8 位) | -``` -┌───────┬───────┬─────────┬─────────┬───────────┬───────┐ -│ TAG │ CMD │ LEN_H │ LEN_L │ DATA │ CRC │ -├───────┼───────┼─────────┼─────────┼───────────┼───────┤ -│ 1 B │ 1 B │ 1 B │ 1 B │ LEN B │ 1 B │ -└───────┴───────┴─────────┴─────────┴───────────┴───────┘ -``` - - 整帧长度 = `4 + LEN + 1`;`LEN=0` 时最短为 5 字节。 - `LEN` 为 16 位大端,理论最大 65535 数据字节。 diff --git a/docs/STM32F4-Base存储架构说明.md b/docs/STM32F4-Base存储架构说明.md index 8446f203..e42823f3 100644 --- a/docs/STM32F4-Base存储架构说明.md +++ b/docs/STM32F4-Base存储架构说明.md @@ -4,38 +4,13 @@ 本 项目在 GD5F2GQ5UE SPI NAND Flash(256MB)上实现了三层存储软件栈: -``` -┌─────────────────────────────────────────────────────────────┐ -│ 应用程序 (main, freertos) │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ FatFs (文件系统) │ -│ │ -├─────────────────────────────────────────────────────────────┤ -│ dhara FTL │ -│ (地址映射 · 磨损均衡 · 坏块管理 · 垃圾回收) │ -├─────────────────────────────────────────────────────────────┤ -│ GD5F2GQ5UE NAND Flash 驱动 (底层 SPI) │ -│ 硬件 SPI · 页读写 · 块擦除 · BBT · 内部 ECC 使能 │ -├─────────────────────────────────────────────────────────────┤ -│ SPI1 硬件外设 (PB3 SCK, PB4 MISO, PB5 MOSI) │ -│ 42MHz · Mode 0 · MSB First · DMA2 (S0-RX, S3-TX) │ -│ 小包轮询(≤32B) · 页数据 DMA (>32B) │ -└─────────────────────────────────────────────────────────────┘ -``` +![存储软件栈分层(应用 → FatFs → dhara FTL → 驱动 → SPI1)](images/存储栈分层.svg) -``` -┌─────────────────────────────────────────────────────────────┐ -│ 物理分区布局 (256MB) │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ ftl_fatfs (256MB) │ │ -│ │ Block 0~2047 │ │ -│ │ dhara FTL + FatFS(全部 2048 个块) │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ 偏移: 0 256MB│ -└─────────────────────────────────────────────────────────────┘ -``` +> 上图由 `tools/arch_diagram/gen_storage_stack_svg.py` 生成(手绘 SVG,布局确定、层间单向箭头);改分层后编辑脚本顶部 `L_*` / `ROWS` 数据重跑即可。 + +![物理分区布局(256MB 单分区)](images/物理分区布局.svg) + +> 上图由 `tools/arch_diagram/gen_partition_svg.py` 生成(分区宽 ∝ MB 数 + 偏移标尺);改分区后编辑脚本顶部 `FIELDS` 数据重跑即可。 **数据路径:** - **应用** → `FatFS` → `dhara FTL` → `gd5f2gq5ue 驱动`。FTL 提供 LBA 到物理页的映射、磨损均衡、坏块管理和垃圾回收,FatFS 通过标准 `disk_*` 接口访问 FTL 提供的块设备。 diff --git a/docs/images/存储栈分层.svg b/docs/images/存储栈分层.svg new file mode 100644 index 00000000..76dd5e1d --- /dev/null +++ b/docs/images/存储栈分层.svg @@ -0,0 +1,29 @@ + + +STM32F4-Base 存储软件栈 +GD5F2GQ5UE SPI NAND Flash(256MB)· 三层软件栈(FatFs / dhara FTL / 驱动)· 上层只依赖相邻下层 + +应用程序 +main · freertos(CubeMX 生成 + FreeRTOS 任务) + +FatFs +文件系统(标准 disk_* 块设备接口) + +dhara FTL +地址映射 · 磨损均衡 · 坏块管理 · 垃圾回收 + +GD5F2GQ5UE NAND Flash 驱动 +硬件 SPI · 页读写 · 块擦除 · BBT · 内部 ECC 使能 + +SPI1 硬件外设 +PB3 SCK · PB4 MISO · PB5 MOSI · 42MHz · Mode 0 · MSB First · DMA2(S0-RX / S3-TX)· 小包轮询(≤32B)/ 页数据 DMA(>32B) + + + + + + + + +数据路径:应用 → FatFs → dhara FTL → gd5f2gq5ue 驱动 → SPI1 硬件外设(单向依赖)· 详见 docs/STM32F4-Base存储架构说明.md §1 + diff --git a/docs/images/物理分区布局.svg b/docs/images/物理分区布局.svg new file mode 100644 index 00000000..24ec0293 --- /dev/null +++ b/docs/images/物理分区布局.svg @@ -0,0 +1,14 @@ + + +GD5F2GQ5UE 物理分区布局(256MB) +单分区:全片划给 FTL(无静态分区)· 块 = 128KB(64 页 × 2KB)· 共 2048 块 + + +0 + +256 MB + +ftl_fatfs +256 MB · Block 0~2047 +ftl_fatfs:dhara FTL + FatFS 占用全部 2048 块(偏移 0 ~ 256MB)· 详见 docs/STM32F4-Base存储架构说明.md §1 + diff --git a/docs/图形绘制流程.md b/docs/图形绘制流程.md index b77f8ead..708e6184 100644 --- a/docs/图形绘制流程.md +++ b/docs/图形绘制流程.md @@ -12,7 +12,8 @@ | 图形类型 | 工具 | 说明 | |---|---|---| | 协议帧 / 字节布局(精确比例) | **SVG**(`gen_frame_svg.py`) | 字段宽须严格 ∝ 字节数 + 逐字段标注 + 偏移标尺 | -| 分层架构 / 模块依赖 | **SVG**(`gen_arch_svg.py`) | 多节点跨层依赖,Mermaid 会画成"意大利面",手工排布更清晰 | +| 闪存 / 内存分区布局(精确比例) | **SVG**(`gen_partition_svg.py`) | 帧图生成器的 MB 计量副本(分区宽 ∝ MB 数 + 偏移标尺) | +| 分层架构 / 模块依赖 | **SVG**(`gen_arch_svg.py`) | 多节点跨层依赖,Mermaid 会画成"意大利面",手工排布更清晰;单层单职责的栈图用其副本 `gen_storage_stack_svg.py` | | 就绪屏障 / 生产者-消费者 | **SVG**(`gen_barrier_svg.py`) | 三栏对齐 + 正交连线,避免交叉 | | 流程图 / 时序 / 状态机 / ER | **Mermaid**(Markdown 内嵌) | 拓扑关系类图,节点少、强调连接;渲染器原生支持 | | UI 界面 / 像素级样机 | `simulator/` 截图、`tools/ui_mock/` | 非本流程范围 | @@ -41,6 +42,8 @@ | `gen_arch_svg.py` | `docs/images/项目架构总览.svg` | `LAYERS` / `ROWS` / `FOOT` | | `gen_barrier_svg.py` | `docs/images/就绪屏障.svg` | `BITS` / `PRODUCERS` / `CONSUMERS` | | `gen_frame_svg.py` | `docs/images/RemoDispBus帧格式.svg` | `FIELDS` / `TITLE` / `SUB` / `FOOT` | +| `gen_storage_stack_svg.py` | `docs/images/存储栈分层.svg` | `L_*` / `ROWS` / `TITLE` / `SUB` / `FOOT` | +| `gen_partition_svg.py` | `docs/images/物理分区布局.svg` | `FIELDS` / `TITLE` / `SUB` / `FOOT`(MB 计量) | --- @@ -131,3 +134,4 @@ sequenceDiagram |---|---| | 2026-09-11 | 创建:统一图形绘制流程(选型 / SVG 生成器 / Mermaid / 约定 / 检查清单);新增 `tools/arch_diagram/gen_frame_svg.py`(通用协议帧图)+ `docs/RemoDispBus帧格式.svg` | | 2026-09-11 | 新增专门配图目录 `docs/images/`:收拢 `docs/` 根与 `docs/UI 设计/images/` 下 5 张 SVG;同步更新 3 个生成器 OUT 路径、4 处文档图片引用、`AGENTS.md` 与 `.gitignore`(PNG 预览规则);流程沉淀为 `draw-diagram` skill | +| 2026-09-11 | 存量 ASCII 框图清理:`STM32F4-Base存储架构说明.md` 两图(存储栈分层 / 物理分区布局)重绘为 SVG(生成器家族新增 `gen_storage_stack_svg.py` / `gen_partition_svg.py`);`RemoDispBus协议与接收设计说明.md` 删 ASCII 帧图(SVG 帧格式图已在,信息无损失);`测试说明/CH395F_Test_Guide.md` 4 图(2 组件架构 + 2 会话时序)转 Mermaid | diff --git a/docs/测试说明/CH395F_Test_Guide.md b/docs/测试说明/CH395F_Test_Guide.md index ff614034..d2cd3c74 100644 --- a/docs/测试说明/CH395F_Test_Guide.md +++ b/docs/测试说明/CH395F_Test_Guide.md @@ -522,46 +522,35 @@ MCU (TCP Client, ch395f_* 裸命令) PC (tcp_bulk --port 8081) **架构图(组件)**: -``` -┌──────────────┐ TCP 8080 ┌──────────────────────────┐ -│ PC (Python) │ ───────────────────────────────────────────▶ │ CH395F (192.168.1.100) │ -│ tcp_client / │ Ethernet │ Socket 0(单连接复用) │ -│ tcp_server │ (SPI2, 分频=8) └────────────┬─────────────┘ -└──────────────┘ SPI │ - ▼ - ┌──────────────────────────┐ - │ MCU (STM32F407) │ - │ netTask: net_poll() 10ms │ - │ testTask: │ - │ net_layer_test_main() │ - │ └─ phase6_run() 原样回显 │ - └──────────────────────────┘ +```mermaid +flowchart LR + PC["PC(Python)
tcp_client / tcp_server"] -->|"TCP 8080 · 以太网"| CH["CH395F(192.168.1.100)
Socket 0(单连接复用)"] + CH -->|"SPI2(分频=8)"| MCU["MCU(STM32F407)
netTask: net_poll() 10ms
testTask: net_layer_test_main()
└─ phase6_run() 原样回显"] ``` **会话时序图(单连接生命周期)**: -``` - PC (Client) CH395F Socket 0 MCU testTask - │ │ │ - │ SYN ──────────────────▶│ │ - │◀──────────────── SYN+ACK │ │ - │ ACK ──────────────────▶│ ESTABLISHED │ - │ │── 中断/GlobInt ───────────▶│ net_poll() - │ │ │ net_accept()→phase_active - │ [64B 数据] ───────────▶│ recv_len = N │ - │ │── 中断 ───────────────────▶│ net_poll() - │ │ │ phase6_run(): - │ │ │ net_recv(N) → 字节校验 - │ │ │ TEST_CHECK(累加和) - │◀──────────── [64B 回显]─│ net_send() │ - │ (逐字节比对 + 累加和) │ │ - │ (重复 N 轮 / 多包) │ │ - │ │ │ - │ FIN/RST ───────────────▶│ DISCONNECT │ - │ │── 中断 ───────────────────▶│ net_poll() - │ │ │ net_recv EOF → auto_relisten - │ │◀─ 自动 OPEN + TCP_LISTEN │ - │ SYN(新连接) ───────────▶│ LISTEN 自动恢复,重新接受 │ +```mermaid +sequenceDiagram + participant C as PC(Client) + participant S as CH395F Socket 0 + participant T as MCU testTask + C->>S: SYN + S->>C: SYN+ACK + C->>S: ACK(ESTABLISHED) + S->>T: 中断 / GlobInt → net_poll() + Note over T: net_accept() → phase_active + C->>S: [64B 数据](recv_len = N) + S->>T: 中断 → net_poll() + Note over T: phase6_run():net_recv(N) → 字节校验 → TEST_CHECK(累加和) + S->>C: [64B 回显](net_send) + Note over C: 逐字节比对 + 累加和(重复 N 轮 / 多包) + C->>S: FIN/RST(DISCONNECT) + S->>T: 中断 → net_poll() + Note over T: net_recv EOF → auto_relisten + S->>S: 自动 OPEN + TCP_LISTEN + C->>S: SYN(新连接) + Note over C,S: LISTEN 自动恢复,重新接受 ``` - `phase6_run()` 为**纯回显**:非阻塞 `net_recv(sockfd, s_rx_buf, TEST_BUF_SIZE, NET_MSG_DONTWAIT)` 收到 N 字节后,`net_send(sockfd, s_rx_buf, N, 0)` 原样回显;不做固件侧字节自检(字节完整性由 PC 侧 `data == test_data` 校验)。 @@ -728,40 +717,32 @@ net_socket(NET_AF_INET, NET_SOCK_STREAM) // 取空闲 Socket(非监听 So **架构图(组件)**: -``` -┌──────────────┐ TCP 8081 ┌──────────────────────────┐ -│ PC (Python) │ ◀─────────────────────────────────────────── │ CH395F (192.168.1.100) │ -│ tcp_server │ Ethernet │ Socket N(非 0 空闲) │ -│ (回显服务器) │ (SPI2, 分频=8) └────────────┬─────────────┘ -└──────────────┘ SPI │ - ▼ - ┌──────────────────────────┐ - │ MCU (STM32F407) │ - │ netTask: net_poll() 10ms │ - │ testTask: │ - │ net_layer_client_test_main│ - │ └─ net_connect→收发→close│ - └──────────────────────────┘ +```mermaid +flowchart LR + MCU["MCU(STM32F407)
netTask: net_poll() 10ms
testTask: net_layer_client_test_main()
└─ net_connect → 收发 → close"] -->|"SPI2(分频=8)"| CH["CH395F(192.168.1.100)
Socket N(非 0 空闲)"] + CH -->|"TCP 8081 · 以太网"| PC["PC(Python)
tcp_server(回显服务器)"] ``` **会话时序图(Client 主动连接)**: -``` - PC (Server :8081) CH395F Socket N MCU testTask - │ │ │ - │ (Python tcp_server) │ │ net_layer_client_test_main() - │ │◀─ net_socket()+net_connect()│ (异步下发 CONNECT) - │◀────────────── SYN ──────│ │ - │────── SYN+ACK ─────────▶│ ESTABLISHED │ - │ │── 中断/GlobInt ───────────▶│ net_poll() - │ │ │ net_select() 可写 → 建链 - │◀────────────── [64B] ────│ net_send() │ - │ (回显服务器原样返回) │ │ - │ [64B 回显] ───────────▶│ recv_len = N │ - │ │── 中断 ───────────────────▶│ net_poll() - │ │ │ net_recv(N) → 字节校验 - │ │ │ TEST_CHECK(累加和) - │ │◀─ net_close() ─────────────│ 释放 Socket N +```mermaid +sequenceDiagram + participant C as PC(Server :8081) + participant S as CH395F Socket N + participant T as MCU testTask + Note over T: net_layer_client_test_main() + T->>S: net_socket() + net_connect()(异步下发 CONNECT) + S->>C: SYN + C->>S: SYN+ACK(ESTABLISHED) + S->>T: 中断 / GlobInt → net_poll() + Note over T: net_select() 可写 → 建链 + T->>S: [64B](net_send) + S->>C: [64B] + Note over C: 回显服务器原样返回 + C->>S: [64B 回显](recv_len = N) + S->>T: 中断 → net_poll() + Note over T: net_recv(N) → 字节校验 → TEST_CHECK(累加和) + T->>S: net_close()(释放 Socket N) ``` - `net_layer_client_test_main()` 为**独立入口**:使用非 0 空闲 Socket 连接 PC,不在 `net_layer_test_main()` 的监听循环内;由 `StartCh395fTestTask` 的 NET 分支按宏调用。 diff --git a/tools/arch_diagram/gen_partition_svg.py b/tools/arch_diagram/gen_partition_svg.py new file mode 100644 index 00000000..aff3ac13 --- /dev/null +++ b/tools/arch_diagram/gen_partition_svg.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""生成 docs/images/物理分区布局.svg —— GD5F2GQ5UE 全片分区布局图(纯标准库)。 + +由 gen_frame_svg.py 复制而来(build() 与数据解耦,通用):协议帧图按"字节"计量 +(字段宽 ∝ 字节数),本图按"MB"计量(分区宽 ∝ MB 数),其余约定一致—— +分段矩形 + 偏移标尺 + 逐段标注,连续布局无假箭头。 + +用法(仓库根目录,任意 python3): + python tools/arch_diagram/gen_partition_svg.py +输出: + docs/images/物理分区布局.svg +改分区时:编辑本文件顶部的 FIELDS / TITLE / SUB / FOOT 后重跑。 +""" +import html +import os + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +OUT = os.path.join(ROOT, "docs", "images", "物理分区布局.svg") + +# ----------------------------- 画布与尺寸参数 ----------------------------- +PAD = 40 # 左右留白 +PX_PER_MB = 4 # 每 MB 像素宽(分区宽 ∝ MB 数) +BAR_Y = 116 # 分区盒顶 y(标题 / 副标题 / 偏移标尺在其上方) +BAR_H = 74 # 分区盒高 +NAME_DY = 30 # 分区名在盒内的 y 偏移 +SIZE_DY = 54 # 尺寸标注在盒内的 y 偏移 +RULER_DY = 20 # 偏移标尺线在盒顶上方 + +TITLE_FONT = 20 +SUB_FONT = 13 +NAME_FONT = 15 +SIZE_FONT = 12 +RULER_FONT = 12 +FOOT_FONT = 12 + +FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" + +# ----------------------------- 分区数据(改这里) ----------------------------- +# name=分区名 mb=MB 数(None=变长段,按 DATA_UNITS 画) label=尺寸标注 color/bg=配色 +DATA_UNITS = 5 # 变长段的"代表宽度"(按 N MB 画,仅示意) +FIELDS = [ + dict(name="ftl_fatfs", mb=256, label="256 MB · Block 0~2047", color="#2563eb", bg="#eff6ff"), +] + +TITLE = "GD5F2GQ5UE 物理分区布局(256MB)" +SUB = "单分区:全片划给 FTL(无静态分区)· 块 = 128KB(64 页 × 2KB)· 共 2048 块" +FOOT = ("ftl_fatfs:dhara FTL + FatFS 占用全部 2048 块(偏移 0 ~ 256MB)· " + "详见 docs/STM32F4-Base存储架构说明.md §1") + + +# ----------------------------- 工具函数 ----------------------------- +def esc(s): + return html.escape(s, quote=True) + + +def field_w(f): + return (f["mb"] if f["mb"] is not None else DATA_UNITS) * PX_PER_MB + + +def offset_labels(fields): + """按分区 MB 数累计边界偏移;偏移标注统一带 MB 单位。""" + labs, off = ["0"], 0 + for f in fields: + off += f["mb"] if f["mb"] is not None else DATA_UNITS + labs.append("%d MB" % off) + return labs + + +# ----------------------------- 生成 ----------------------------- +def build(): + out = [] + add = out.append + + frame_w = sum(field_w(f) for f in FIELDS) + W = frame_w + 2 * PAD + + # 预计算各分区 x / 宽 + segs, x = [], PAD + for f in FIELDS: + w = field_w(f) + segs.append([f, x, w]) + x += w + labels = offset_labels(FIELDS) + + total_h = BAR_Y + BAR_H + 56 + add('' % (W, total_h)) + + # 标题 + 副标题 + add('%s' + % (PAD, TITLE_FONT, esc(TITLE))) + add('%s' + % (PAD, SUB_FONT, esc(SUB))) + + # 偏移标尺(每个分区边界一个刻度 + 偏移标注) + ruler_y = BAR_Y - RULER_DY + add('' + % (PAD, ruler_y, PAD + frame_w, ruler_y)) + bounds = [PAD] + [s[1] + s[2] for s in segs] + for i, bx in enumerate(bounds): + add('' + % (bx, ruler_y - 4, bx, ruler_y + 4)) + lab = labels[i] if i < len(labels) else "?" + add('%s' + % (bx, ruler_y - 8, RULER_FONT, esc(lab))) + + # 分区盒(名在上、尺寸在下) + for (f, bx, bw) in segs: + add('' + % (bx, BAR_Y, bw, BAR_H, f["bg"], f["color"])) + cx = bx + bw / 2.0 + add('%s' + % (cx, BAR_Y + NAME_DY, NAME_FONT, esc(f["name"]))) + add('%s' + % (cx, BAR_Y + SIZE_DY, SIZE_FONT, f["color"], esc(f["label"]))) + + # 脚注 + add('%s' + % (PAD, total_h - 16, FOOT_FONT, esc(FOOT))) + + svg = ('\n%s\n\n' + % (W, total_h, W, total_h, FONT, "\n".join(out))) + return svg, W, total_h + + +def main(): + svg, W, h = build() + 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() diff --git a/tools/arch_diagram/gen_storage_stack_svg.py b/tools/arch_diagram/gen_storage_stack_svg.py new file mode 100644 index 00000000..185a0245 --- /dev/null +++ b/tools/arch_diagram/gen_storage_stack_svg.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""生成 docs/images/存储栈分层.svg —— GD5F2GQ5UE 存储软件栈分层图(纯标准库)。 + +由 gen_arch_svg.py 复制而来(build() 与数据解耦,通用):每层一个圆角框(标题 + 说明), +层间单向下箭头。本图各层均为"单层单职责",故 chips 留空(只渲染标题 + 说明行)。 + +用法(仓库根目录,任意 python3): + python tools/arch_diagram/gen_storage_stack_svg.py +输出: + docs/images/存储栈分层.svg +改分层时:编辑本文件顶部的 L_* / ROWS / TITLE / SUB / FOOT 数据后重跑。 +""" +import html +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 = 1000 +PAD = 26 +TITLE_FONT = 20 +SUB_FONT = 13 +BAND_TITLE_FONT = 17 +BAND_SUB_FONT = 12 +CHIP_FONT = 13 + +CHIP_H = 38 # 芯片盒高 +CHIP_GAP = 10 # 芯片水平间距 +CHIP_ROW_GAP = 10 # 芯片行间距 +CHIP_PAD = 16 # 芯片内左右留白 +BAND_PAD_X = 18 # 框内左右留白 +BAND_TITLE_H = 38 # 框内标题区高 +BAND_PAD_BOTTOM = 16 +BAND_H_GAP = 26 # 同一行内并列框的间距 +BAND_V_GAP = 44 # 行间距(放箭头) +TITLE_H = 78 # 顶部标题区高 + +FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" + +# ----------------------------- 架构数据(改这里) ----------------------------- +TITLE = "STM32F4-Base 存储软件栈" +SUB = "GD5F2GQ5UE SPI NAND Flash(256MB)· 三层软件栈(FatFs / dhara FTL / 驱动)· 上层只依赖相邻下层" + +L_APP = dict( + title="应用程序", sub="main · freertos(CubeMX 生成 + FreeRTOS 任务)", + color="#2563eb", bg="#eff6ff", chips=[], +) +L_FATFS = dict( + title="FatFs", sub="文件系统(标准 disk_* 块设备接口)", + color="#0d9488", bg="#f0fdfa", chips=[], +) +L_FTL = dict( + title="dhara FTL", sub="地址映射 · 磨损均衡 · 坏块管理 · 垃圾回收", + color="#16a34a", bg="#f0fdf4", chips=[], +) +L_DRV = dict( + title="GD5F2GQ5UE NAND Flash 驱动", sub="硬件 SPI · 页读写 · 块擦除 · BBT · 内部 ECC 使能", + color="#d97706", bg="#fffbeb", chips=[], +) +L_SPI = dict( + title="SPI1 硬件外设", + sub="PB3 SCK · PB4 MISO · PB5 MOSI · 42MHz · Mode 0 · MSB First · DMA2(S0-RX / S3-TX)· 小包轮询(≤32B)/ 页数据 DMA(>32B)", + color="#e11d48", bg="#fff1f2", chips=[], +) + +ROWS = [ + [L_APP], + [L_FATFS], + [L_FTL], + [L_DRV], + [L_SPI], +] + +FOOT = "数据路径:应用 → FatFs → dhara FTL → gd5f2gq5ue 驱动 → SPI1 硬件外设(单向依赖)· 详见 docs/STM32F4-Base存储架构说明.md §1" + + +# ----------------------------- 工具函数 ----------------------------- +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) >= 0x2460 else 0.58) + return w + + +def chip_w(label): + return text_w(label, CHIP_FONT) + 2 * CHIP_PAD + + +def wrap_chips(chips, avail): + rows, cur, cur_w = [], [], 0.0 + for c in chips: + cw = chip_w(c) + need = cw if not cur else cur_w + CHIP_GAP + cw + if cur and need > avail: + rows.append(cur) + cur, cur_w = [c], cw + else: + cur.append(c) + cur_w = need + if cur: + rows.append(cur) + return rows + + +# ----------------------------- 生成 ----------------------------- +def build(): + out = [] + add = out.append + + # 布局:逐行排布(行内可并列多个框,行高取该行最高框) + placements = [] # (layer, chip_rows, x, y, w, h) + y = TITLE_H + for row in ROWS: + n = len(row) + bw = (W - 2 * PAD - BAND_H_GAP * (n - 1)) // n + bands = [] + for i, layer in enumerate(row): + bx = PAD + i * (bw + BAND_H_GAP) + avail = bw - 2 * BAND_PAD_X + rows = wrap_chips(layer["chips"], avail) + bh = BAND_TITLE_H + len(rows) * CHIP_H + (len(rows) - 1) * CHIP_ROW_GAP + BAND_PAD_BOTTOM + bands.append([layer, rows, bx, y, bw, bh]) + row_h = max(b[5] for b in bands) + for b in bands: + b[5] = row_h # 同行等高(视觉"同级") + placements.extend(bands) + y += row_h + BAND_V_GAP + total_h = y - BAND_V_GAP + 54 + + add('' % (W, total_h)) + add('%s' + % (PAD, 40, TITLE_FONT, esc(TITLE))) + add('%s' + % (PAD, 62, SUB_FONT, esc(SUB))) + + for (layer, rows, bx, by, bw, bh) in placements: + # 框背景 + 边框 + add('' % (bx, by, bw, bh, layer["bg"], layer["color"])) + # 标题(层名 + 职责说明) + add('%s' + % (bx + BAND_PAD_X, by + 26, BAND_TITLE_FONT, layer["color"], esc(layer["title"]))) + tw = text_w(layer["title"], BAND_TITLE_FONT) + add('%s' + % (bx + BAND_PAD_X + tw + 12, by + 26, BAND_SUB_FONT, esc(layer["sub"]))) + # 芯片 + cy = by + BAND_TITLE_H + for row in rows: + cx = bx + BAND_PAD_X + for c in row: + cw = chip_w(c) + add('' % (cx, cy, cw, CHIP_H, layer["color"])) + add('%s' + % (cx + cw / 2, cy + CHIP_H / 2 + CHIP_FONT * 0.35, CHIP_FONT, esc(c))) + cx += cw + CHIP_GAP + cy += CHIP_H + CHIP_ROW_GAP + + # 行间箭头:每个上层框向下引一条(顶层两框 → 两条,体现"都依赖下层") + row_first = [] # 每行首个 placement 的下标 + idx = 0 + for row in ROWS: + row_first.append(idx) + idx += len(row) + for r in range(len(ROWS) - 1): + y0 = placements[row_first[r]][3] + placements[row_first[r]][5] + 6 + y1 = placements[row_first[r + 1]][3] - 8 + for i in range(row_first[r], row_first[r] + len(ROWS[r])): + (_, _, bx, _, bw, _) = placements[i] + ax = bx + bw / 2 + add('' % (ax, y0, ax, y1 - 7)) + add('' % (ax, y1)) + + add('%s' + % (PAD, total_h - 18, esc(FOOT))) + + svg = ('\n%s\n\n' + % (W, total_h, W, total_h, FONT, "\n".join(out))) + return svg, total_h + + +def main(): + svg, h = build() + 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()