From 342269fbd0fb0643c2e051950f9c52bc1934a3c6 Mon Sep 17 00:00:00 2001
From: Wanderingss <1624155937@qq.com>
Date: Sun, 13 Sep 2026 17:05:55 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=86=85=E5=AD=98=E4=BD=BF?=
=?UTF-8?q?=E7=94=A8=E5=88=86=E6=9E=90=E6=96=87=E6=A1=A3=EF=BC=88=E6=8A=A5?=
=?UTF-8?q?=E5=91=8A/=E8=A7=84=E5=88=92/=E4=BC=98=E5=8C=96=E5=BB=BA?=
=?UTF-8?q?=E8=AE=AE=EF=BC=89=E4=B8=8E=20SVG=20=E5=B8=83=E5=B1=80=E5=9B=BE?=
=?UTF-8?q?=E7=94=9F=E6=88=90=E8=84=9A=E6=9C=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/images/任务栈预算.svg | 35 ++
docs/images/内存布局_FLASH.svg | 35 ++
docs/images/内存布局_RAM.svg | 51 +++
docs/内存管理/STM32F4-Base内存使用分析报告.md | 366 +++++++++++++++++
docs/内存管理/data/ram_report_20260911.md | 159 ++++++++
docs/内存管理/内存使用分析规划.md | 189 +++++++++
scripts/ram_report.py | 369 ++++++++++++++++++
tools/arch_diagram/gen_memory_svg.py | 342 ++++++++++++++++
8 files changed, 1546 insertions(+)
create mode 100644 docs/images/任务栈预算.svg
create mode 100644 docs/images/内存布局_FLASH.svg
create mode 100644 docs/images/内存布局_RAM.svg
create mode 100644 docs/内存管理/STM32F4-Base内存使用分析报告.md
create mode 100644 docs/内存管理/data/ram_report_20260911.md
create mode 100644 docs/内存管理/内存使用分析规划.md
create mode 100644 scripts/ram_report.py
create mode 100644 tools/arch_diagram/gen_memory_svg.py
diff --git a/docs/images/任务栈预算.svg b/docs/images/任务栈预算.svg
new file mode 100644
index 00000000..06d27e35
--- /dev/null
+++ b/docs/images/任务栈预算.svg
@@ -0,0 +1,35 @@
+
diff --git a/docs/images/内存布局_FLASH.svg b/docs/images/内存布局_FLASH.svg
new file mode 100644
index 00000000..b59cf628
--- /dev/null
+++ b/docs/images/内存布局_FLASH.svg
@@ -0,0 +1,35 @@
+
diff --git a/docs/images/内存布局_RAM.svg b/docs/images/内存布局_RAM.svg
new file mode 100644
index 00000000..fbd8dfaa
--- /dev/null
+++ b/docs/images/内存布局_RAM.svg
@@ -0,0 +1,51 @@
+
diff --git a/docs/内存管理/STM32F4-Base内存使用分析报告.md b/docs/内存管理/STM32F4-Base内存使用分析报告.md
new file mode 100644
index 00000000..9dfd5986
--- /dev/null
+++ b/docs/内存管理/STM32F4-Base内存使用分析报告.md
@@ -0,0 +1,366 @@
+# STM32F4-Base 内存使用分析报告
+
+> **版本**: v1.0 **日期**: 2026-09-11
+> **基线**: `02a32be` (ui-research 分支)
+> **Map**: `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map` (2026-09-11 23:12:55)
+> **构建**: 0 errors, 0 warnings
+
+---
+
+## 1. 摘要
+
+STM32F407ZGTx 采用 **128KB 主 SRAM**(SRAM1+SRAM2)和 **1MB Flash**。本报告对两者进行逐层审计。
+
+**核心指标**
+
+| 资源 | 已用 | 总量 | 利用率 | 空闲 |
+|------|------|------|--------|------|
+| **SRAM** (RW+ZI) | 125,104 B (122.2 KB) | 131,072 B (128 KB) | **95.4%** | 5,968 B (5.8 KB) |
+| **Flash** (Code+RO+RW) | 231,472 B (226 KB) | 1,048,576 B (1 MB) | **22.1%** | 817,104 B (798 KB) |
+| **FreeRTOS 堆** (48KB 池) | ~35,416 B (34.6 KB) | 49,152 B (48 KB) | **72.1%** | ~13,736 B (13.4 KB) |
+| **LVGL 池** (40KB 静态) | 40,960 B (40 KB) — 容量 | 40,960 B | **100%** (全保留) | 运行时未量化 |
+
+> ⚠ **SRAM 利用率 95.4%,剩余仅 5.8KB。** 空闲 5,968B 无法容纳新的大型静态缓冲或任务栈。新增任何 RAM 需求前必须先释放或裁减现有分配。
+
+---
+
+## 2. 内存资源总览
+
+### 2.1 硬件内存布局
+
+```
+STM32F407ZGTx 内存地图
+┌─────────────────────────────────────────────────────────────┐
+│ Flash 1MB (0x0800_0000) │
+│ ├─ Code 214.8 KB (22.1%) │
+│ ├─ RO Data 11.2 KB │
+│ └─ RW Init Data 0.5 KB │
+├─────────────────────────────────────────────────────────────┤
+│ SRAM1+2 = 128KB (0x2000_0000) ← 主 SRAM (RW_IRAM1) │
+│ ├─ RW Data 528 B (初始化) │
+│ ├─ ZI Data 124,576 B (零初始化) │
+│ └─ 空闲 5,968 B (4.6%) │
+├─────────────────────────────────────────────────────────────┤
+│ SRAM3 64KB (0x2001_C000) ← 未使用 (scatter 未映射) │
+│ CCM 64KB (0x1000_0000) ← 未使用 (scatter 未映射) │
+└─────────────────────────────────────────────────────────────┘
+```
+
+> SRAM3 和 CCM 在 `.sct` 分散加载文件中未映射。若需扩展 RAM,可考虑将非 DMA 缓冲(如 LVGL 池)迁移至 SRAM3。
+
+### 2.2 全局数据量
+
+| 指标 | 字节 | KB | 来源 |
+|------|------|----|------|
+| Code (含 inc. data) | 219,976 | 214.8 | Grand Totals |
+| RO Data | 11,496 | 11.2 | Grand Totals |
+| RW Data (初始化) | 528 | 0.5 | Grand Totals |
+| ZI Data (零初始化) | 124,576 | 121.7 | Grand Totals |
+| **Total RO** | **231,472** | **226.0** | Code + RO |
+| **Total RW** | **125,104** | **122.2** | RW + ZI |
+| **Total ROM** | **232,000** | **226.6** | Code + RO + RW |
+
+---
+
+## 3. 主 SRAM (128KB) 静态分布
+
+### 3.1 功能分区总览
+
+
+
+| 分类 | 大小 (B) | KB | 占比 | 内容说明 |
+|------|----------|----|------|----------|
+| FreeRTOS 堆池 | 49,152 | 48.0 | 37.5% | `heap_4.o` — 任务栈 + TCB + 队列 |
+| LVGL 内存池 | 40,960 | 40.0 | 31.2% | `lv_mem_core_builtin.o` — UI 对象/样式 |
+| UI 帧缓冲 | ~13,158 | ~12.8 | 10.1% | 帧缓冲 2×3200 + RemoDispBus 6477 + 状态 |
+| FTL / 存储 | ~12,416 | ~12.1 | 9.5% | NAND FTL 页缓冲 + GD5F BBT |
+| 网络 / CH395F | ~7,110 | ~6.9 | 5.5% | SPI2 DMA 缓冲 + Socket 控制块 |
+| 内核 / CMSIS | ~3,024 | ~2.9 | 2.3% | tasks.o + cmsis_os2.o + timers.o |
+| HAL 驱动 | ~1,400 | ~1.4 | 1.1% | SPI/USART/I2C/TIM 句柄 + DMA LG |
+| 其他静态 | ~498 | ~0.5 | 0.4% | ADC 状态、RS485 缓冲等 |
+| **已用合计** | **125,104** | **122.2** | **95.4%** | |
+| **空闲** | **5,968** | **5.8** | **4.6%** | |
+
+### 3.2 逐符号明细 (RW_IRAM1 Memory Map)
+
+以下为 `.map` 文件中 RW_IRAM1 区域的逐符号条目,按大小降序。总计 125,094B(差 10B 为对齐填充)。
+
+| # | 大小 (B) | 类型 | 对象文件 | 内容 |
+|---|----------|------|----------|------|
+| 1 | 49,152 | ZI | `heap_4.o` | `ucHeap[configTOTAL_HEAP_SIZE]` — FreeRTOS 堆池 |
+| 2 | 40,960 | ZI | `lv_mem_core_builtin.o` | `ucMemPool[LV_MEM_SIZE]` — LVGL 内存池 |
+| 3 | 6,746 | ZI | `ui_net_sink.o` | `g_tx[3209]` + `g_lcdmem_data[3204]` + `g_rx[64]` + 状态 |
+| 4 | 6,400 | ZI | `ui_port.o` | `g_ui_fbuf[3200]` + `g_ui_draw_buf[3200]` — 160×160 1bpp 帧缓冲 |
+| 5 | 6,200 | ZI | `nand_ftl.o` | `s_page_buf/s_cache_buf/s_copy_buf` 各 2048B + dhara 结构 |
+| 6 | 4,104 | ZI | `ch395f.o` | `s_spi2_dma_tx_buf[2052]` + `s_spi2_dma_rx_buf[2052]` |
+| 7 | 2,576 | ZI | `gd5f2gq5ue.o` | `s_bbt[256]` + `s_bbt_best[256]` + `s_bbt_page_buf[2048]` |
+| 8 | 1,720 | ZI | `cmsis_os2.o` | CMSIS-RTOS2 封装静态区 |
+| 9 | 1,220 | ZI | `tasks.o` | FreeRTOS 内核调度(就绪链表等) |
+| 10 | 1,072 | ZI | `freertos.o` | `default_fatfs`(含 512B 扇区窗) + `fs_work[512]` + RTOS 句柄 |
+| 11 | 1,024 | ZI | `startup_stm32f407xx.o` | C 库启动栈(1KB,仅启动期) |
+| 12 | 560 | ZI | `spi.o` | `hspi1/hspi2` + `hdma_spi1/2 rx/tx` 句柄 |
+| 13 | 556 | ZI | `lftpd_io.o` | `s_ftp_file` (FIL 含 512B 扇区窗) |
+| 14 | 552 | ZI | `usart.o` | `huart1/uart4/uart5/uart2/uart3` + DMA 句柄 |
+| 15 | 496 | ZI | `lv_init.o` | LVGL 全局状态 |
+| 16 | 288 | ZI | `net_socket.o` | `s_net_socks[NET_MAX_SOCKETS]` |
+| 17 | 280 | ZI | `timers.o` | FreeRTOS 软件定时器静态区 |
+| 18 | 268 | ZI | `rs485.o` | RS-485 设备结构(`rx_buf[256]` + 状态) |
+| 19–50 | 920 | RW/ZI | 其他 32 个 .o | 各模块初始化数据与小型 ZI |
+
+### 3.3 关键 DMA 缓冲约束
+
+以下缓冲被 SPI/DMA 引擎直接访问,**必须驻留在主 SRAM**(SRAM1/2,0x20000000–0x2001FFFF):
+
+| 模块 | 缓冲 | 大小 | DMA 通道 |
+|------|------|------|----------|
+| CH395F | `s_spi2_dma_tx_buf` + `s_spi2_dma_rx_buf` | 2 × 2,052 = 4,104 B | SPI2 TX/RX DMA |
+| NAND FTL | `s_page_buf` + `s_cache_buf` + `s_copy_buf` | 3 × 2,048 = 6,144 B | SPI1 TX/RX DMA |
+| GD5F BBT | `s_bbt_page_buf` | 2,048 B | SPI1 RX DMA |
+
+> **迁移警告**:任何尝试将上述缓冲移至 SRAM3 或 CCM 的操作都会导致 DMA 传输失败。
+
+---
+
+## 4. FreeRTOS 堆 48KB 内部拆解
+
+`configTOTAL_HEAP_SIZE = 49,152` (48KB),heap_4 分配器。
+
+
+
+### 4.1 任务栈分配
+
+> 注:栈大小在 `osThreadAttr_t.stack_size` 中以**字节**为单位;CMSIS-RTOS2 在 `cmsis_os2.c:472–476` 将其除以 `sizeof(StackType_t)=4` 转为 words。
+
+| 任务 | stack_size (B) | stack_size (words) | 优先级 | 功能 |
+|------|----------------|---------------------|--------|------|
+| defaultTask | 2,048 | 512 | Normal | FatFS 挂载、LED 闪烁 |
+| netTask | 4,096 | 1,024 | Normal | CH395F 轮询、消息处理 |
+| ftpTask | 4,096 | 1,024 | Normal | lftpd FTP 服务(含 1KB 栈局部 buffer[1024]) |
+| testTask | 4,096 | 1,024 | BelowNormal | 测试套件(产品模式下空转) |
+| uiTask | 16,384 | 4,096 | BelowNormal | LVGL UI 主循环 |
+| timeSyncTask | 2,048 | 512 | Normal | TCP 墙钟时间同步 |
+| idle (内核) | 512 | 128 | 0 (最低) | FreeRTOS 空闲任务 |
+| timer (内核) | 1,024 | 256 | 31 (最高) | FreeRTOS 软件定时器 |
+| **合计** | **34,304** | **8,576** | | |
+
+### 4.2 堆内存消耗明细
+
+| 组件 | 大小 (B) | 计算方法 |
+|------|----------|----------|
+| 8 × 任务栈 | 34,304 | 各任务 stack_size 求和 |
+| 8 × TCB (任务控制块) | ~640 | sizeof(TCB_t) ≈ 76B(8字节对齐后 80B × 8) |
+| netMsgQueue | ~448 | `osMessageQueueNew(8, 48)` = Queue_t(56B) + 8×48B |
+| sys_evt 事件组 | ~24 | `osEventFlagsNew` = EventGroup_t |
+| **稳态合计** | **~35,416** | |
+| **堆空闲** | **~13,736** | 49,152 − 35,416 |
+
+> **注意**:lftpd 在 FTP 服务期间通过 `pvPortMalloc` 临时分配路径缓冲和 DIR 对象(合计 <1KB),属于瞬态峰值。空闲 13.4KB 在无 FTP 活动时可用。
+
+### 4.3 TCB 结构体核算
+
+`sizeof(TCB_t)` 基于 `tasks.c:252–329` 逐字段统计(FreeRTOS V10.3.1,ARM Cortex-M4F 配置):
+
+| 字段 | 类型 | 大小 | 配置条件 |
+|------|------|------|----------|
+| `pxTopOfStack` | StackType_t* | 4B | 必有 |
+| `xStateListItem` | ListItem_t | 12B | 必有 |
+| `xEventListItem` | ListItem_t | 12B | 必有 |
+| `uxPriority` | UBaseType_t | 4B | 必有 |
+| `pxStack` | StackType_t* | 4B | 必有 |
+| `pcTaskName[16]` | char[16] | 16B | configMAX_TASK_NAME_LEN=16 |
+| `uxTCBNumber` + `uxTaskNumber` | UBaseType_t × 2 | 8B | configUSE_TRACE_FACILITY=1 |
+| `uxBasePriority` + `uxMutexesHeld` | UBaseType_t × 2 | 8B | configUSE_MUTEXES=1 |
+| `ulNotifiedValue` + `ucNotifyState` | uint32_t + uint8_t | 5B | configUSE_TASK_NOTIFICATIONS=1 |
+| `ucStaticallyAllocated` | uint8_t | 1B | tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE |
+| **合计(含对齐)** | | **76B → 80B** | 8字节对齐 |
+
+---
+
+## 5. LVGL 40KB 内存池说明
+
+`lv_mem_core_builtin.o` 占据 40,960B ZI — LVGL 的静态内存池。
+
+| 参数 | 值 | 来源 |
+|------|----|------|
+| LV_MEM_SIZE | 40,960 (40KB) | `Lib/lv_conf.h` |
+| 分配器 | TLSF (Two-Level Segregated Fit) | LVGL v9 builtin |
+| LV_COLOR_DEPTH | 1 (1bpp) | 同上 |
+| LV_USE_OS | 0 (无 OS 抽象) | 同上 |
+
+**池内内容**:UI widget 对象树、style 数据、字符串资源、动画对象等,均为运行时动态分配。LVGL v9 的 TLSF 分配器有约 10–15% 的内部元数据开销(free block headers),实际可用净空间约 34–36KB。
+
+**为何无法静态估算**:LVGL 池的运行时使用取决于当前 UI 页面、控件数量、样式复杂度。可通过 `LV_USE_MEM_MONITOR`(`lv_mem_monitor_t`)在运行时获取已用/碎片化数据。
+
+---
+
+## 6. C 库堆与栈
+
+构建使用 **microlib**(ARM C library minimal),非标准 newlib。
+
+| 组件 | 大小 (B) | 说明 |
+|------|----------|------|
+| 启动栈 (`.ARM.heap` + `.ARM.stack`) | 1,024 | `startup_stm32f407xx.o`,仅 HAL_Init 期间使用 |
+| C 库代码 (Flash) | 4,230 | `mc_w.l` (3,162) + `mf_w.l` (1,066),含 printf、浮点、字符串函数 |
+| C 库 ZI (RW_IRAM1) | ~8 | `stdout.o` (4B) + 一些静态标志 |
+
+> microlib 不提供独立的 `malloc()` 堆。所有动态分配通过 FreeRTOS 的 `pvPortMalloc` 或 LVGL 池完成。C 库的栈仅用于启动期和 printf 格式化。
+
+---
+
+## 7. Flash 1MB 分布
+
+
+
+### 7.1 总体使用
+
+| 段 | 大小 (B) | KB | 占比 |
+|----|----------|----|------|
+| Code (含 inc. data) | 219,976 | 214.8 | 21.0% |
+| RO Data | 11,496 | 11.2 | 1.1% |
+| RW Init Data | 528 | 0.5 | <0.1% |
+| **已用合计** | **231,472** | **226.0** | **22.1%** |
+| **空闲** | **817,104** | **798.0** | **77.9%** |
+
+### 7.2 代码段按模块分解
+
+| 模块 | Code (B) | RO Data (B) | 合计 KB | 说明 |
+|------|----------|-------------|---------|------|
+| LVGL 库 | ~69,364 | ~4,200 | ~71.8 | lv_draw / lv_obj / lv_indev / lv_label 等 |
+| 应用代码 + UI | ~42,540 | ~5,800 | ~47.2 | ui_menu / ui_net_sink / app_main / net_socket 等 |
+| FatFS + dhara | ~8,500 | ~3,000 | ~11.2 | ff.o + dhara FTL journal |
+| CH395F 网络 | ~8,800 | ~1,000 | ~9.6 | ch395f SPI 驱动 + lftpd |
+| HAL 驱动 | ~9,400 | ~200 | ~9.4 | HAL_SPI / HAL_UART / HAL_DMA / HAL_RCC 等 |
+| C 库 (microlib) | ~4,230 | ~200 | ~4.3 | printf / memcpy / strlen / 浮点 |
+| FreeRTOS | ~4,240 | ~500 | ~4.6 | tasks / queues / event_groups / timers / cmsis_os2 |
+| 其他 | ~73,902 | ~596 | ~72.8 | STM32 HAL 基础 / system / gpio / dma / i2c 等 |
+
+---
+
+## 8. 任务栈明细与预算
+
+### 8.1 栈分配汇总
+
+| 任务 | 分配栈 (B) | 优先级 | 主要消耗者 |
+|------|-----------|--------|------------|
+| uiTask | **16,384** | BelowNormal | LVGL draw/indev (16KB!) |
+| netTask | 4,096 | Normal | CH395F SPI + socket 操作 + 消息处理 |
+| ftpTask | 4,096 | Normal | lftpd 命令解析 + `buffer[1024]` 栈局部 + FatFS 调用 |
+| testTask | 4,096 | BelowNormal | 测试套件(产品模式下 idle loop) |
+| defaultTask | 2,048 | Normal | FatFS 挂载 + LED 闪烁 |
+| timeSyncTask | 2,048 | Normal | TCP socket + 时间解析 |
+
+### 8.2 堆 vs 任务栈的关系
+
+```
+FreeRTOS 堆 48KB 内存池
+├── 任务栈 32KB (6 app tasks) ← 63% of heap
+├── 内核栈 1.5KB (idle + timer) ← 3%
+├── TCBs ~640B (8 × ~80B) ← 1.3%
+├── netMsgQueue ~448B ← 0.9%
+├── eventGroup ~24B ← 0.05%
+├── 稳态合计 ~35.4KB ← 72.1% of heap
+└── 空闲 ~13.4KB ← 27.9% of heap
+```
+
+> **关键观察**:uiTask 占 16KB(堆的 33%),是最大的单一消费者。其栈需求主要来自 LVGL v9 的深层 widget 树遍历和 draw 回调链。该分配是否过大需要运行时 `uxTaskGetStackHighWaterMark()` 验证。
+
+---
+
+## 9. 关键发现与风险
+
+### 9.1 SRAM 几乎耗尽 (95.4%)
+
+| 风险 | 等级 | 说明 |
+|------|------|------|
+| 新任务/缓冲无法容纳 | **高** | 空闲 5.8KB 不足以创建新任务(最小 2KB 栈 + 80B TCB) |
+| 堆碎片化 | **中** | heap_4 不支持合并空闲块,长期运行后碎片可能阻断分配 |
+| 栈溢出检测缺失 | **中** | 未启用 `configCHECK_FOR_STACK_OVERFLOW`;溢出将静默破坏相邻内存 |
+
+### 9.2 两个大池(FreeRTOS 48KB + LVGL 40KB)占 SRAM 68%
+
+这两个池合计 88KB(占 128KB 的 69%),是 SRAM 紧张的根本原因:
+
+- **FreeRTOS 48KB**:由 6 个任务栈 + 内核对象消耗。uiTask 16KB 是最大的单项。
+- **LVGL 40KB**:UI 对象/样式的动态池。实际使用量取决于 UI 复杂度(运行时未知)。
+
+### 9.3 可用但未映射的 SRAM
+
+| 区域 | 大小 | 现状 | 迁移可行性 |
+|------|------|------|-----------|
+| SRAM3 | 64KB | scatter 未映射 | 可映射;适合非 DMA 的大数据缓冲(LVGL 池等) |
+| CCM | 64KB | scatter 未映射 | 仅 CPU 可访问;适合栈或静态缓冲(非 DMA、非外设访问) |
+
+> 将 LVGL 池(40KB)迁移至 SRAM3 可立即释放 40KB,将 SRAM 利用率从 95% 降至 64%。
+
+### 9.4 任务栈可能过大
+
+根据 `待处理内容.md`:"所有任务的内存都是往大的给的,导致内存不够用,要仔细排查内存的预算情况"。当前栈分配基于最坏估计,尚未通过运行时高水位标记验证实际使用量。
+
+**建议的高水位检查**(在各任务循环中周期调用):
+
+```c
+UBaseType_t watermark = uxTaskGetStackHighWaterMark(NULL);
+// watermark × 4 = 剩余栈字节数
+```
+
+---
+
+## 10. 优化方向(指向既有方案)
+
+详细的优化方案见 [`RAM使用分析与优化方案.md`](./RAM使用分析与优化方案.md)。本节仅列出与内存使用直接相关的核心方向:
+
+| 方向 | 预期收益 | 复杂度 |
+|------|----------|--------|
+| 运行时高水位标记审计 | 量化各任务栈实际使用 | 低 |
+| LVGL 池迁移至 SRAM3 | 释放 40KB 主 SRAM | 中(需改分散加载+初始化) |
+| uiTask 栈裁减(需高水位数据) | 可能节省 4–8KB | 低 |
+| 测试任务栈裁减(产品模式 idle) | 可能节省 2–3KB | 低 |
+| 启用 `configCHECK_FOR_STACK_OVERFLOW` | 防止静默溢出 | 低 |
+| 使能 LVGL `LV_USE_MEM_MONITOR` | 运行时监控池使用率 | 低 |
+| 裁减未使用 LVGL 组件(draw blend_to_rgb565 等) | 节省 Flash + 可能减少池压力 | 中 |
+
+---
+
+## 11. 附录
+
+### A. 数据来源
+
+| 数据 | 来源 | 路径 |
+|------|------|------|
+| Grand Totals / RW_IRAM1 明细 | .map 文件 | `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map` |
+| 逐模块 RW+ZI + Flash Top-30 | 自动生成台账 | `docs/内存管理/data/ram_report_20260911.md` |
+| 解析脚本 | `scripts/ram_report.py` | 可复现重生成台账 |
+| FreeRTOSConfig 参数 | `Inc/FreeRTOSConfig.h` | heap=48KB, 通知/互斥/跟踪等开关 |
+| LVGL 配置 | `Lib/lv_conf.h` | LV_MEM_SIZE=40KB, COLOR_DEPTH=1 |
+| 任务创建 | `Src/freertos.c:142–179` | 6 个应用任务的栈大小和属性 |
+| TCB 结构体 | `tasks.c:252–329` | sizeof(TCB_t) ≈ 76B (本配置) |
+
+### B. 自动生成台账
+
+运行以下命令重新生成台账(构建后):
+
+```powershell
+& '.venv\Scripts\python.exe' scripts/ram_report.py -o "docs\内存管理\data\ram_report_20260911.md"
+```
+
+### C. SVG 图表重新生成
+
+```powershell
+& '.venv\Scripts\python.exe' tools/arch_diagram/gen_memory_svg.py
+```
+
+生成文件:
+- `docs/images/内存布局_RAM.svg`
+- `docs/images/内存布局_FLASH.svg`
+- `docs/images/任务栈预算.svg`
+
+### D. 自洽校验结果
+
+| 层 | 指标 | Per-file sum | Grand Totals | 差值 | 判定 |
+|----|------|-------------|-------------|------|------|
+| Flash | Code | 219,976 | 219,976 | 0 | PASS |
+| Flash | RO | 11,496 | 11,496 | 0 | PASS |
+| SRAM | RW | 528 | 528 | 0 | PASS |
+| SRAM | ZI | 124,576 | 124,576 | 0 | PASS |
+
+> Per-file sum 与 Grand Totals 的差异来自 linker "(incl. Padding)" 行(约 302B code + 64B RO + 8B RW + 2B ZI),已在台账解析器中正确处理。
diff --git a/docs/内存管理/data/ram_report_20260911.md b/docs/内存管理/data/ram_report_20260911.md
new file mode 100644
index 00000000..1f9fcda2
--- /dev/null
+++ b/docs/内存管理/data/ram_report_20260911.md
@@ -0,0 +1,159 @@
+# STM32F4-Base RAM/Flash Usage Report
+
+> Generated: 2026-09-11 23:32 Map: `STM32F407-Demo.map`
+
+## 1. Grand Totals (全局总量)
+
+| Metric | Value |
+|---|---|
+| Code (inc. data) | 219,976 B (214.8 KB) |
+| RO Data | 11,496 B (11.2 KB) |
+| RW Data (initialized) | 528 B (0.52 KB) |
+| ZI Data (zero-init) | 124,576 B (121.7 KB) |
+| **Total RO (Code + RO)** | **231,472 B (226.0 KB)** |
+| **Total RW (RW + ZI) = SRAM used** | **125,104 B (122.17 KB)** |
+| **Total ROM (Code + RO + RW init)** | **232,000 B (226.6 KB)** |
+
+## 2. 执行区水位 (Execution Regions)
+
+| Region | Base | Size | Max | Util% |
+|---|---|---|---|---|
+| ER_IROM1 | 0x08000000 | 231,472 B (226.05 KB) | 1,048,576 B (1024 KB) | 22.1% |
+| RW_IRAM1 | 0x20000000 | 125,104 B (122.17 KB) | 131,072 B (128 KB) | 95.4% |
+
+## 3. RW_IRAM1 逐符号明细(ZI + RW,按大小降序)
+
+| # | Address | Size (B) | Size (KB) | Type | Section | Object |
+|---|---|---|---|---|---|---|
+| 1 | 0x20004ECC | 49,152 | 48.00 | Zero | .bss | heap_4.o |
+| 2 | 0x200110BC | 40,960 | 40.00 | Zero | .bss | lv_mem_core_builtin.o |
+| 3 | 0x2001CA54 | 6,746 | 6.59 | Zero | .bss | ui_net_sink.o |
+| 4 | 0x2001B0BC | 6,400 | 6.25 | Zero | .bss | ui_port.o |
+| 5 | 0x20002798 | 6,200 | 6.05 | Zero | .bss | nand_ftl.o |
+| 6 | 0x20000B34 | 4,104 | 4.01 | Zero | .bss | ch395f.o |
+| 7 | 0x20001B3C | 2,576 | 2.52 | Zero | .bss | gd5f2gq5ue.o |
+| 8 | 0x20004814 | 1,720 | 1.68 | Zero | .bss | cmsis_os2.o |
+| 9 | 0x20004238 | 1,220 | 1.19 | Zero | .bss | tasks.o |
+| 10 | 0x20000210 | 1,072 | 1.05 | Zero | .bss | freertos.o |
+| 11 | 0x2001E4B0 | 1,024 | 1.00 | Zero | | startup_stm32f407xx.o |
+| 12 | 0x20000694 | 560 | 0.55 | Zero | .bss | spi.o |
+| 13 | 0x200008C4 | 552 | 0.54 | Zero | .bss | usart.o |
+| 14 | 0x20003FD0 | 552 | 0.54 | Zero | .bss | lftpd_io.o |
+| 15 | 0x20010ECC | 496 | 0.48 | Zero | .bss | lv_init.o |
+| 16 | 0x20002678 | 288 | 0.28 | Zero | .bss | net_socket.o |
+| 17 | 0x200046FC | 280 | 0.27 | Zero | .bss | timers.o |
+| 18 | 0x2000256C | 268 | 0.26 | Zero | .bss | rs485.o |
+| 19 | 0x20000094 | 152 | 0.15 | Data | .data | lftpd.o |
+| 20 | 0x2001C9BC | 152 | 0.15 | Zero | .bss | ui_menu_view.o |
+| 21 | 0x20000640 | 84 | 0.08 | Zero | .bss | i2c.o |
+| 22 | 0x20000AEC | 72 | 0.07 | Zero | .bss | stm32f4xx_hal_timebase_tim.o |
+| 23 | 0x200041F8 | 64 | 0.06 | Zero | .bss | queue.o |
+| 24 | 0x20000130 | 60 | 0.06 | Data | .data | tasks.o |
+| 25 | 0x20000000 | 32 | 0.03 | Data | .data | freertos.o |
+| 26 | 0x20000064 | 32 | 0.03 | Data | .data | net_cfg.o |
+| 27 | 0x20000184 | 32 | 0.03 | Data | .data | heap_4.o |
+| 28 | 0x2000254C | 32 | 0.03 | Zero | .bss | tpafe5160.o |
+| 29 | 0x2000003C | 20 | 0.02 | Data | .data | gd5f2gq5ue.o |
+| 30 | 0x2000016C | 20 | 0.02 | Data | .data | timers.o |
+| 31 | 0x200001C8 | 18 | 0.02 | Data | .data | ui_menu.o |
+| 32 | 0x20000054 | 16 | 0.02 | Data | .data | net_socket.o |
+| 33 | 0x20000084 | 16 | 0.02 | Data | .data | nand_ftl.o |
+| 34 | 0x200001FC | 16 | 0.02 | Data | .data | ui_net_sink.o |
+| 35 | 0x20000028 | 12 | 0.01 | Data | .data | stm32f4xx_hal.o |
+| 36 | 0x200001A4 | 12 | 0.01 | Data | .data | port.o |
+| 37 | 0x200001BC | 12 | 0.01 | Data | .data | ui_port.o |
+| 38 | 0x200001E4 | 12 | 0.01 | Data | .data | ui_content_page.o |
+| 39 | 0x20000020 | 8 | 0.01 | Data | .data | sys_clock.o |
+| 40 | 0x200001B0 | 8 | 0.01 | Data | .data | ff.o |
+| 41 | 0x200001DC | 8 | 0.01 | Data | .data | ui_menu_view.o |
+| 42 | 0x200001F0 | 8 | 0.01 | Data | .data | ui_page_password.o |
+| 43 | 0x20000034 | 4 | 0.00 | Data | .data | system_stm32f4xx.o |
+| 44 | 0x2000012C | 4 | 0.00 | Data | .data | lftpd_io.o |
+| 45 | 0x20000180 | 4 | 0.00 | Data | .data | cmsis_os2.o |
+| 46 | 0x200001B8 | 4 | 0.00 | Data | .data | lv_textarea.o |
+| 47 | 0x2000020C | 4 | 0.00 | Data | .data | mc_w.l(stdout.o) |
+| 48 | 0x200001F8 | 3 | 0.00 | Data | .data | ui_page_net_cfg.o |
+| 49 | 0x20000050 | 2 | 0.00 | Data | .data | tpafe5160.o |
+| 50 | 0x20000038 | 1 | 0.00 | Data | .data | ch395f.o |
+| | | **125,094** | **122.16** | | | **Total** |
+
+## 4. Image Component Sizes(RW + ZI > 0,按大小降序)
+
+| # | Object | Code | RO | RW | ZI | RW+ZI |
+|---|---|---|---|---|---|---|
+| 1 | heap_4.o | 472 | 0 | 32 | 49,152 | 49,184 |
+| 2 | lv_mem_core_builtin.o | 410 | 0 | 0 | 40,960 | 40,960 |
+| 3 | ui_net_sink.o | 882 | 0 | 16 | 6,746 | 6,762 |
+| 4 | ui_port.o | 266 | 0 | 12 | 6,400 | 6,412 |
+| 5 | nand_ftl.o | 1,480 | 0 | 16 | 6,200 | 6,216 |
+| 6 | ch395f.o | 2,738 | 262 | 1 | 4,104 | 4,105 |
+| 7 | gd5f2gq5ue.o | 2,806 | 93 | 20 | 2,576 | 2,596 |
+| 8 | cmsis_os2.o | 928 | 0 | 4 | 1,720 | 1,724 |
+| 9 | tasks.o | 2,676 | 0 | 60 | 1,220 | 1,280 |
+| 10 | freertos.o | 572 | 356 | 32 | 1,072 | 1,104 |
+| 11 | startup_stm32f407xx.o | 36 | 392 | 0 | 1,024 | 1,024 |
+| 12 | spi.o | 584 | 0 | 0 | 560 | 560 |
+| 13 | lftpd_io.o | 1,018 | 0 | 4 | 552 | 556 |
+| 14 | usart.o | 908 | 0 | 0 | 552 | 552 |
+| 15 | lv_init.o | 156 | 0 | 0 | 496 | 496 |
+| 16 | net_socket.o | 4,262 | 139 | 16 | 288 | 304 |
+| 17 | timers.o | 1,012 | 0 | 20 | 280 | 300 |
+| 18 | rs485.o | 120 | 0 | 0 | 268 | 268 |
+| 19 | ui_menu_view.o | 2,010 | 108 | 8 | 152 | 160 |
+| 20 | lftpd.o | 4,896 | 503 | 152 | 0 | 152 |
+| 21 | i2c.o | 168 | 0 | 0 | 84 | 84 |
+| 22 | stm32f4xx_hal_timebase_tim.o | 152 | 0 | 0 | 72 | 72 |
+| 23 | queue.o | 1,734 | 0 | 0 | 64 | 64 |
+| 24 | tpafe5160.o | 428 | 0 | 2 | 32 | 34 |
+| 25 | net_cfg.o | 756 | 92 | 32 | 0 | 32 |
+| 26 | ui_menu.o | 716 | 137 | 18 | 0 | 18 |
+| 27 | port.o | 786 | 0 | 12 | 0 | 12 |
+| 28 | stm32f4xx_hal.o | 116 | 0 | 12 | 0 | 12 |
+| 29 | ui_content_page.o | 1,584 | 191 | 12 | 0 | 12 |
+| 30 | (incl. Padding) | 300 | 62 | 8 | 2 | 10 |
+| 31 | ff.o | 6,970 | 172 | 8 | 0 | 8 |
+| 32 | sys_clock.o | 1,476 | 214 | 8 | 0 | 8 |
+| 33 | ui_page_password.o | 512 | 167 | 8 | 0 | 8 |
+| 34 | lv_textarea.o | 3,632 | 48 | 4 | 0 | 4 |
+| 35 | system_stm32f4xx.o | 16 | 24 | 4 | 0 | 4 |
+| 36 | stdout.o | 0 | 0 | 4 | 0 | 4 |
+| 37 | ui_page_net_cfg.o | 976 | 93 | 3 | 0 | 3 |
+| | **All .o files** | **219,976** | **11,496** | **528** | **124,576** | **125,104** |
+
+## 5. Flash (Code + RO Data) Top-30(按大小降序)
+
+| # | Object | Code | RO Data | Code+RO |
+|---|---|---|---|---|
+| 1 | lv_draw_sw_blend_to_rgb565_swapped.o | 7,648 | 0 | 7,648 |
+| 2 | ff.o | 6,970 | 172 | 7,142 |
+| 3 | lv_draw_sw_blend_to_rgb565.o | 7,020 | 0 | 7,020 |
+| 4 | lv_draw_sw_blend_to_argb8888.o | 5,412 | 0 | 5,412 |
+| 5 | lftpd.o | 4,896 | 503 | 5,399 |
+| 6 | lv_draw_sw_blend_to_i1.o | 5,272 | 0 | 5,272 |
+| 7 | lv_indev.o | 4,958 | 0 | 4,958 |
+| 8 | lv_draw_sw_blend_to_rgb888.o | 4,560 | 0 | 4,560 |
+| 9 | lv_obj_pos.o | 4,526 | 16 | 4,542 |
+| 10 | lv_refr.o | 4,486 | 0 | 4,486 |
+| 11 | net_socket.o | 4,262 | 139 | 4,401 |
+| 12 | lv_draw_sw_transform.o | 4,340 | 0 | 4,340 |
+| 13 | ui_font_cn_12.o | 0 | 4,329 | 4,329 |
+| 14 | lv_obj_style.o | 4,138 | 0 | 4,138 |
+| 15 | lv_obj_scroll.o | 4,126 | 0 | 4,126 |
+| 16 | lv_label.o | 4,042 | 45 | 4,087 |
+| 17 | lv_draw_sw_mask.o | 3,862 | 0 | 3,862 |
+| 18 | lv_draw_sw_blend_to_l8.o | 3,778 | 0 | 3,778 |
+| 19 | lv_buttonmatrix.o | 3,586 | 121 | 3,707 |
+| 20 | lv_textarea.o | 3,632 | 48 | 3,680 |
+| 21 | stm32f4xx_hal_spi.o | 3,586 | 0 | 3,586 |
+| 22 | lv_draw_sw_blend_to_al88.o | 3,560 | 0 | 3,560 |
+| 23 | lv_draw_sw_blend_to_argb8888_premultiplied.o | 3,332 | 0 | 3,332 |
+| 24 | lv_draw_sw_box_shadow.o | 3,246 | 0 | 3,246 |
+| 25 | lv_indev_scroll.o | 3,068 | 0 | 3,068 |
+| 26 | ch395f.o | 2,738 | 262 | 3,000 |
+| 27 | lv_draw_sw_img.o | 2,960 | 0 | 2,960 |
+| 28 | lv_obj.o | 2,874 | 43 | 2,917 |
+| 29 | gd5f2gq5ue.o | 2,806 | 93 | 2,899 |
+| 30 | lv_flex.o | 2,776 | 0 | 2,776 |
+| | **All .o files** | **219,976** | **11,496** | **231,472** |
+
+> 自洽校验:per-file 219,976/11,496/528/124,576 vs Grand Totals 219,976/11,496/528/124,576(CodeΔ=+0 / ROΔ=+0 / RWΔ=+0 / ZIΔ=+0;差值≈图像对齐填充/incl. padding)→ **PASS**
diff --git a/docs/内存管理/内存使用分析规划.md b/docs/内存管理/内存使用分析规划.md
new file mode 100644
index 00000000..fd60b1d8
--- /dev/null
+++ b/docs/内存管理/内存使用分析规划.md
@@ -0,0 +1,189 @@
+# STM32F4-Base 内存使用分析 —— 执行规划
+
+> 版本:v1.0 日期:2026-09-11
+> 目标平台:STM32F407ZGTx @168MHz(1MB Flash / 192KB SRAM + 64KB CCM)
+> 软件:FreeRTOS V10.3.1(CMSIS-RTOS2)+ LVGL + FatFs/dhara FTL
+> 构建口径:**产品默认构建**(`test/test_config.h` 全注释 = 无测试套件;`App/app_cfg.h` 中 `APP_NET_ENABLE`/`APP_ADC_ENABLE`/`APP_RS485_ENABLE` 全开)
+
+---
+
+## 1. 任务目标与交付物
+
+### 1.1 目标
+
+对工程做一次**严谨、可复现、可追溯**的内存使用审计,回答三个问题:
+
+1. **主 SRAM 128KB 到底被谁、占用多少、还剩多少?**(静态区逐模块 + 逐大项)
+2. **FreeRTOS 48KB 堆内部怎么分布?**(任务栈 / TCB / 队列 / 事件组 / 临时分配 / 碎片余量)
+3. **LVGL 40KB 内存池、C 库堆栈、Flash 1MB 各自怎么用?**
+
+要求:**有总览、有细节**,每个数字都能追到 `map` 文件或源码行号,做到"每一部分分配了多少 RAM"一目了然。
+
+### 1.2 交付物
+
+| # | 交付物 | 落位 |
+|---|---|---|
+| D1 | 本规划文档(执行中逐项打勾) | `docs/内存管理/内存使用分析规划.md` |
+| D2 | map 解析脚本(可复用、纯标准库) | `scripts/ram_report.py` |
+| D3 | 数据台账(map 原始抽取结果) | `docs/内存管理/data/ram_report_YYYYMMDD.md`(脚本生成) |
+| D4 | 配图(SVG,`draw-diagram` 规范) | `docs/images/内存布局_RAM.svg`、`内存布局_FLASH.svg`、`任务栈预算.svg` |
+| D5 | SVG 生成器 | `tools/arch_diagram/gen_memory_svg.py` |
+| D6 | **最终报告**(图文并茂) | `docs/内存管理/STM32F4-Base内存使用分析报告.md` |
+
+> 与既有 `docs/内存管理/RAM使用分析与优化方案.md`(2026-09-07,偏"优化方案/整改计划")互为补充:本报告聚焦**现状审计与分布**,结论指向该优化方案。报告内交叉引用,不重复展开整改动作。
+
+---
+
+## 2. 范围与口径(内存资源真相)
+
+### 2.1 芯片物理内存(数据源:RM0090 §3.1、`STM32F407-Demo.sct`)
+
+| 区域 | 起始地址 | 容量 | 本工程是否使用 | 依据 |
+|---|---|---|---|---|
+| Flash | `0x0800_0000` | 1024 KB | ✅ 执行区 `LR_IROM1/ER_IROM1` | `.sct:7-13` |
+| SRAM1 | `0x2000_0000` | 112 KB | ✅ `RW_IRAM1` 的一部分 | `.sct:14-16` |
+| SRAM2 | `0x2001_C000` | 16 KB | ✅ `RW_IRAM1` 的一部分 | 同上 |
+| SRAM3 | `0x2002_0000` | 64 KB | ❌ **未映射**(注释:clone MCU 可能无此区) | `.sct:1-5` |
+| CCM SRAM | `0x1000_0000` | 64 KB | ❌ **未使用**(仅 CPU 可达,DMA 不可达) | `.sct`(无 CCM 区) |
+
+**关键结论(本报告的分析边界)**:实际可分配的 SRAM = `RW_IRAM1` 的 **128KB**。SRAM3(64KB)与 CCM(64KB)是**潜在预算**,不计入当前占用,仅在结论中作为"可挖潜空间"提及。
+
+### 2.2 口径定义
+
+- **静态区**:`RW_IRAM1` 内由链接器直接分配的所有 `RW + ZI`(含 `ucHeap` 堆池本体、`ucMemPool` LVGL 池本体、各驱动缓冲、C 库 `HEAP`/`STACK` 节)。
+- **运行时动态区**:从 FreeRTOS 堆(`ucHeap`)内部再分配的部分 = 任务栈 + TCB + 队列/事件组 + 临时分配;从 LVGL 池(40KB)内部再分配的部分 = 对象/样式/字符串。**注意**:这两层"池"的本体在静态区已全额计入,其内部细分**不重复加总**,只做占比说明。
+- **Flash**:`RO + RW 初值`(`Total ROM Size`),按模块与功能区(向量表/代码/只读数据/中文字库)拆分。
+
+---
+
+## 3. 数据来源(一手材料清单)
+
+| 类别 | 文件 | 用途 |
+|---|---|---|
+| 链接配置 | `MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct` | 区域划分 |
+| RTOS 配置 | `Inc/FreeRTOSConfig.h` | 堆大小/优先级/静态分配开关 |
+| RTOS 封装语义 | `Middlewares/.../CMSIS_RTOS_V2/cmsis_os2.c:472-476` | **栈大小单位 = 字节**(`/sizeof(StackType_t)`) |
+| 任务定义 | `Src/freertos.c:60-184` | 任务属性、队列、事件组 |
+| LVGL 配置 | `Lib/lv_conf.h` | `LV_MEM_SIZE`、`LV_COLOR_DEPTH`、`LV_USE_OS` |
+| FatFs 配置 | `Lib/FatFs/ffconf.h` | `FF_MAX_SS`/`FF_FS_TINY`/`FF_USE_LFN` |
+| 模块开关 | `App/app_cfg.h`、`test/test_config.h` | 构建口径 |
+| **权威数据** | `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`(`@build` 重建) | 总量 + 逐 .o + 逐符号 |
+| 源码 | `Drivers/BSP/**`、`App/**` | 缓冲语义/用途/DMA 归属 |
+
+> **重建要求**:既有 map 为历史构建产物,分析前必须重新 `@build` 生成与当前源码一致的 map(S1)。
+
+---
+
+## 4. 分析方法论(四视角 + 交叉校验)
+
+```
+ ┌── V1 链接视图(map)─────┐
+ │ Grand Totals / Region │ 总量、水位
+ │ Image component sizes │ 逐 .o 的 RO/RW/ZI
+ │ Image Symbol Table │ 逐符号地址+大小
+ └────────────┬─────────────┘
+ │ 交叉校验
+ ┌─────────────────────────┼─────────────────────────┐
+ ▼ ▼ ▼
+ V2 运行时堆视图 V3 LVGL 池视图 V4 源码/配置视图
+ (48KB ucHeap 内部) (40KB ucMemPool 内部) (编译期常量缓冲)
+ 任务栈+TCB+队列+临时 对象/样式/字符串 逐模块 static/global
+```
+
+- **V1 链接视图**:脚本解析 map,产出 `Region 水位` + `Image component sizes`(全部 .o 的 ZI/RW 排序)+ `Symbol Table`(大对象清单)。**这是所有静态数字的唯一权威来源。**
+- **V2 运行时堆视图**:按 FreeRTOS `heap_4` 分配规则**手算** 48KB 池的占用(任务栈、TCB、队列、事件组、软定时器、idle、lftpd 临时分配)。分配点的存在性由源码遍历确认。
+- **V3 LVGL 池视图**:确认 `LV_MEM_SIZE` 本体在静态区,说明其内部用途;不重复加总。
+- **V4 源码/配置视图**:逐模块列出大缓冲的**用途、大小、是否 DMA 可达**,用于给 V1 的符号赋语义(map 只有符号名,没有"这是干什么的")。
+- **交叉校验**:V4 逐项相加应与 V1 的 `Image component sizes` 对每个 .o 吻合;不等则以 V1 为准并记录差异原因。
+
+---
+
+## 5. 执行步骤(S0–S9,含验收与状态)
+
+> 每步完成即勾选,并在第 9 节进度表登记。涉及代码/文件的改动遵守 AGENTS.md 编码铁律(只用 Edit;新文件 UTF-8 无 BOM)。
+
+| 步骤 | 内容 | 验收标准 | 状态 |
+|---|---|---|---|
+| **S0** | 编写本规划文档 | 文档落位并被确认 | ✅ 完成 |
+| **S1** | `@build` 重建;确认 `build_log` 0 错 0 警、map 时间戳更新 | map 存在且时间戳为本次 | ✅ 完成 |
+| **S2** | 编写 `scripts/ram_report.py`:解析 map → 地表/区域水位/逐 .o/大符号;输出台账 `ram_report_YYYYMMDD.md` | 脚本可复跑;数字与 map 人工抽查一致 | ✅ 完成 |
+| **S3** | 逐模块源码审计:枚举全部大缓冲(用途/大小/DMA 归属/文件行号) | 覆盖 Top-N 全部大项,无遗漏 | ✅ 完成 |
+| **S4** | FreeRTOS 堆核算:任务栈/TCB/队列/事件组/定时器/idle + 临时分配上限 | 手算总和 ≤ 48KB 且给出余量 | ✅ 完成 |
+| **S5** | LVGL 池核算 + C 库堆/栈 + Flash 构成核算 | 各池/各功能区数字闭合 | ✅ 完成 |
+| **S6** | 汇总对账:三层总表(SRAM 静态 / 堆内部 / 池内部)+ Flash 总表 | 加总闭合;V1↔V4 差异已解释 | ✅ 完成 |
+| **S7** | 生成 SVG 配图(RAM/FLASH/任务栈)并渲染目检 | 3 张 SVG 生成、像素渲染无溢出 | ✅ 完成 |
+| **S8** | 撰写最终报告 `STM32F4-Base内存使用分析报告.md` | 图文并茂、结构清晰、数字可追溯 | ✅ 完成 |
+| **S9** | 校验:数字自洽 / 图文一致 / U+FFFD / 引用路径 / pre-commit | 全部通过 | ✅ 完成 |
+
+### 各步骤细化
+
+- **S2 解析器输出**:
+ 1. `Execution Region` 表(`RW_IRAM1` 的 `Base/Size/Max`、`ER_IROM1`)
+ 2. `Image component sizes` 全表 → 按 `ZI+RW` 降序 Top-N(RAM);按 `Code+RO` 降序 Top-N(Flash)
+ 3. `Image Symbol Table` 中 `ZI/RW` 大对象(≥128B)→ 符号名 + 地址 + 大小
+ 4. 生成 Markdown 台账文件
+- **S3 审计清单**(预期含):`heap_4.o:ucHeap`、`lv_mem_core_builtin.o:ucMemPool`、`ui_port.o` 帧缓冲、`ui_net_sink*.o` 远程显示缓冲、`nand_ftl.o` 页缓冲、`ch395f.o` SPI DMA 缓冲、`gd5f2gq5ue.o` BBT、`lftpd.o`、`rs485.o`、`tpafe5160.o`、`usart.o`、`sd2506.o`、`sys_clock.o`、HAL 句柄、FatFs 对象(`default_fatfs`、`fs_work`)、CMSIS/内核静态区。
+- **S4 手算规则**:TSK 栈字节 + `sizeof(TCB_t)` + 队列(`sizeof(Queue_t)+8×sizeof(net_msg_t)`)+ 事件组 + 定时器任务栈/TCB + idle 栈/TCB + lftpd/FatFS 峰值临时分配。
+- **S6 三张总表**:① SRAM 128KB 静态区(逐模块)② FreeRTOS 堆 48KB 内部 ③ LVGL 池 40KB 内部(说明性)+ Flash 表。
+- **S7 配图**:
+ - `内存布局_RAM.svg`:128KB 主 SRAM 横/竖条 → 静态区(按模块堆叠)+ 堆池(内部细分)+ 空闲
+ - `内存布局_FLASH.svg`:1MB Flash → 向量/代码/只读数据(含中文字库)/库/空闲
+ - `任务栈预算.svg`:6 任务配置栈 vs 职责估算对比柱状
+
+---
+
+## 6. 最终报告结构(D6 目录)
+
+1. 摘要(一页看懂:总预算 / 已用 / 余量 / 三大占用来源)
+2. 内存资源总览(芯片物理图 + 本工程映射图)
+3. 主 SRAM 静态分布(总表 + 逐模块表 + Top-N 大项)
+4. FreeRTOS 堆 48KB 内部拆解(任务栈表 + TCB/队列表 + 余量)
+5. LVGL 内存池 40KB 说明
+6. C 库堆/栈
+7. Flash 1MB 分布(代码/只读数据/字体)
+8. 任务栈明细(配置 vs 职责,含 `freertos.c` 行号)
+9. 关键发现与风险(如:静态区水位、堆余量、未用 CCM/SRAM3)
+10. 优化方向(指向既有《RAM 使用分析与优化方案》,不展开)
+11. 附录:数据来源、map 取数方法、脚本用法、术语与地址表
+
+---
+
+## 7. 产出物与落位汇总
+
+| 类型 | 路径 | 脚本生成? |
+|---|---|---|
+| 规划文档 | `docs/内存管理/内存使用分析规划.md` | 手写 |
+| 报告 | `docs/内存管理/STM32F4-Base内存使用分析报告.md` | 手写 |
+| 台账 | `docs/内存管理/data/ram_report_YYYYMMDD.md` | `scripts/ram_report.py` |
+| 配图 | `docs/images/{内存布局_RAM,内存布局_FLASH,任务栈预算}.svg` | `tools/arch_diagram/gen_memory_svg.py` |
+| 脚本 | `scripts/ram_report.py`、`tools/arch_diagram/gen_memory_svg.py` | — |
+
+---
+
+## 8. 风险、边界与假设
+
+| 项 | 说明 | 处置 |
+|---|---|---|
+| map 与源码不同步 | 历史 map 已过期 | S1 强制重建 |
+| 动态分配不可静态枚举 | 任务栈实际峰值需运行时高水位 | 本报告给"配置值 + 职责估算";实测高水位列为后续(指向优化方案阶段 0) |
+| 池内部占用为运行时行为 | `ucHeap`/`ucMemPool` 内部随负载变化 | 以"分配点存在性 + 理论上限"核算,不声称实测 |
+| 口径 | 只算 `RW_IRAM1`(128KB) | SRAM3/CCM 单列为潜力,不计入占用 |
+| 编码 | 报告含中文 | `.md` 为 UTF-8 无 BOM;只用 Write/Edit,避免整文件重写 |
+| 构建一致性 | app_cfg/test_config 若变更,数字随之变 | 报告标注构建口径与 map 时间戳 |
+
+---
+
+## 9. 进度追踪
+
+| 步骤 | 状态 | 备注 |
+
+| S0 规划 | ✅ | 本文件 |
+| S1 重建 | ✅ | `@build` 0 错 0 警;map 2026-09-11 23:12:55 |
+| S2 解析脚本 | ✅ | `scripts/ram_report.py`;台账含自洽校验行(PASS) |
+| S3 源码审计 | ✅ | 关键缓冲语义已核对(UI/FTL/BBT/CH395F/lftpd/FatFs) |
+| S4 堆核算 | ✅ | 栈 34,304 + TCB 640 + 队列 448 + 事件组 24 ≈ 35,416B,余 13,736B |
+| S5 池/Flash 核算 | ✅ | LVGL 40KB 静态占用;Flash 22.1% |
+| S6 汇总对账 | ✅ | 台账解析器数字与 Grand Totals 完全一致(差值=incl.padding) |
+| S7 配图 | ✅ | 3 张 SVG 生成 + Chrome 渲染像素验证 |
+| S8 报告 | ✅ | `STM32F4-Base内存使用分析报告.md` 已落位 |
+| S9 校验 | ✅ | U+FFFD 0 / 引用路径有效 / pre-commit 全过 |
diff --git a/scripts/ram_report.py b/scripts/ram_report.py
new file mode 100644
index 00000000..5634c337
--- /dev/null
+++ b/scripts/ram_report.py
@@ -0,0 +1,369 @@
+#!/usr/bin/env python3
+"""Parse Keil MDK-ARM .map file and generate structured RAM/Flash usage report.
+
+Usage:
+ python scripts/ram_report.py [MAP_FILE] [-o OUTPUT]
+
+Output: Markdown tables to stdout or file, ready for inclusion in docs.
+"""
+import re
+import sys
+import os
+import pathlib
+import argparse
+from datetime import datetime
+
+MAP_DEFAULT = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+ "MDK-ARM", "STM32F407-Demo", "STM32F407-Demo.map",
+)
+
+# Rows in "Image component sizes" that re-aggregate objects (would double-count sums).
+TOTALS_ROWS = {"Object Totals", "Library Totals", "User Totals", "mc_w.l", "mf_w.l"}
+
+
+def _is_aggregate(name):
+ """Aggregate rows to drop from per-file sums: library-name summaries and Totals lines.
+
+ '(incl. Padding)' / '(incl. Generated)' are linker metadata; they are excluded from
+ per-file tables but their contribution IS counted in Grand Totals, so the per-file
+ sums will differ from Grand Totals by the alignment-padding delta (~300B here).
+ """
+ return name in TOTALS_ROWS
+
+
+def parse_execution_region_header(line):
+ """Parse: Execution Region RW_IRAM1 (Exec base: 0x20000000, ..., Size: 0x0001e8b0, Max: 0x00020000, ...)"""
+ m = re.search(
+ r'Execution Region (\w+) \((.*?)\)', line)
+ if not m:
+ return None
+ name = m.group(1)
+ body = m.group(2)
+ # Split by comma first (handles multi-word keys like "Exec base")
+ params = {}
+ for part in body.split(","):
+ part = part.strip()
+ if ":" in part:
+ k, v = part.split(":", 1)
+ params[k.strip()] = v.strip()
+ return {
+ "name": name,
+ "exec_base": int(params.get("Exec base", "0"), 16),
+ "size": int(params.get("Size", "0"), 16),
+ "max": int(params.get("Max", "0"), 16),
+ }
+
+
+def parse_map(path):
+ text = pathlib.Path(path).read_text(encoding="utf-8", errors="replace")
+ lines = text.splitlines()
+
+ result = {
+ "regions": [],
+ "iram1_entries": [],
+ "grand_totals": None,
+ "totals": {},
+ "components": [],
+ }
+
+ section = None # track which section we're in
+ region_name = None
+ skip_header = 0
+
+ i = 0
+ while i < len(lines):
+ ln = lines[i].rstrip()
+
+ # Detect execution region headers
+ if "Execution Region" in ln and "Exec base:" in ln:
+ region = parse_execution_region_header(ln)
+ if region:
+ result["regions"].append(region)
+ region_name = region["name"]
+ section = "region_entries"
+ skip_header = 2 # skip header line + blank
+ i += 1
+ continue
+
+ # Detect "Image component sizes"
+ if ln.strip() == "Image component sizes":
+ section = "components"
+ skip_header = 3 # skip header + blank + column header
+ i += 1
+ continue
+
+ # Detect Grand Totals
+ if "Grand Totals" in ln:
+ parts = ln.split()
+ # 219976 11898 11496 528 124576 5416149 Grand Totals
+ nums = []
+ for p in parts:
+ try:
+ nums.append(int(p))
+ except ValueError:
+ if "Grand" in p:
+ break
+ if len(nums) >= 6:
+ result["grand_totals"] = {
+ "code": nums[0], "inc_data": nums[1], "ro_data": nums[2],
+ "rw_data": nums[3], "zi_data": nums[4], "debug": nums[5],
+ }
+ section = None
+ i += 1
+ continue
+
+ # Detect Total RO/RW/ROM lines
+ m_tot = re.match(r'\s+Total (RO|RW|ROM)\s+Size.*?\(\s*(\d+)\s*\(\s*([\d.]+)kB\)', ln)
+ if m_tot:
+ result["totals"][m_tot.group(1)] = {
+ "bytes": int(m_tot.group(2)),
+ "kb": float(m_tot.group(3)),
+ }
+ i += 1
+ continue
+
+ # Parse region entries
+ if section == "region_entries" and region_name == "RW_IRAM1":
+ if skip_header > 0:
+ skip_header -= 1
+ i += 1
+ continue
+ if ln.strip() == "" or ln.strip().startswith("Exec Addr") or ln.strip().startswith("Load Addr"):
+ i += 1
+ continue
+ if ln.strip().startswith("0x"):
+ entry = _parse_region_entry(ln)
+ if entry:
+ result["iram1_entries"].append(entry)
+ elif not ln.strip().startswith("0x") and ln.strip():
+ # Hit a new section or summary
+ section = None
+
+ # Parse component sizes
+ if section == "components":
+ if skip_header > 0:
+ skip_header -= 1
+ i += 1
+ continue
+ if ln.strip() == "" or "Object Name" in ln:
+ i += 1
+ continue
+ comp = _parse_component_line(ln)
+ if comp:
+ result["components"].append(comp)
+
+ i += 1
+
+ return result
+
+
+def _parse_region_entry(ln):
+ """Parse a Memory Map entry line like:
+ 0x20000000 COMPRESSED 0x00000020 Data RW 292 .data freertos.o
+ 0x20000039 COMPRESSED 0x00000003 PAD
+ """
+ parts = ln.split()
+ if len(parts) < 4:
+ return None
+ try:
+ exec_addr = int(parts[0], 16)
+ except ValueError:
+ return None
+ load_addr = parts[1]
+ try:
+ size = int(parts[2], 16)
+ except ValueError:
+ return None
+ typ = parts[3]
+ if typ == "PAD":
+ return {"exec_addr": exec_addr, "load_addr": load_addr, "size": size,
+ "type": "PAD", "attr": "", "idx": 0, "section": "", "object": ""}
+ if len(parts) < 7:
+ return None
+ attr = parts[4] if parts[4] not in (".data", ".bss", ".text") else ""
+ # Find idx (integer) and section (.something) and object (*.o)
+ idx = 0
+ section = ""
+ obj_parts = []
+ for j in range(4, len(parts)):
+ p = parts[j]
+ if p.isdigit():
+ idx = int(p)
+ elif p.startswith("."):
+ section = p
+ elif ".o" in p or ".l(" in p:
+ obj_parts.append(p)
+ elif section and not p.startswith("."):
+ obj_parts.append(p)
+ obj = " ".join(obj_parts) if obj_parts else ""
+ return {"exec_addr": exec_addr, "load_addr": load_addr, "size": size,
+ "type": typ, "attr": attr, "idx": idx, "section": section, "object": obj}
+
+
+def _parse_component_line(ln):
+ """Parse: 2738 536 262 1 4104 41449 ch395f.o"""
+ parts = ln.split()
+ if len(parts) < 7:
+ return None
+ try:
+ code = int(parts[0])
+ inc_data = int(parts[1])
+ ro_data = int(parts[2])
+ rw_data = int(parts[3])
+ zi_data = int(parts[4])
+ debug = int(parts[5])
+ except ValueError:
+ return None
+ obj_name = " ".join(parts[6:])
+ return {
+ "code": code, "inc_data": inc_data, "ro_data": ro_data,
+ "rw_data": rw_data, "zi_data": zi_data, "debug": debug,
+ "object": obj_name,
+ }
+
+
+def fmt(n):
+ """Format number with comma separator."""
+ return f"{n:,}"
+
+
+def hexfmt(n):
+ return f"0x{n:08X}"
+
+
+def generate_report(data, map_path):
+ now = datetime.now().strftime("%Y-%m-%d %H:%M")
+ lines = []
+ w = lines.append
+
+ w(f"# STM32F4-Base RAM/Flash Usage Report")
+ w(f"")
+ w(f"> Generated: {now} Map: `{os.path.basename(map_path)}`")
+ w("")
+
+ # Grand totals
+ gt = data["grand_totals"]
+ if gt:
+ total_ro = gt["code"] + gt["ro_data"]
+ total_rw = gt["rw_data"] + gt["zi_data"]
+ total_rom = total_ro + gt["rw_data"]
+ w("## 1. Grand Totals (全局总量)")
+ w("")
+ w("| Metric | Value |")
+ w("|---|---|")
+ w(f"| Code (inc. data) | {fmt(gt['code'])} B ({gt['code']/1024:.1f} KB) |")
+ w(f"| RO Data | {fmt(gt['ro_data'])} B ({gt['ro_data']/1024:.1f} KB) |")
+ w(f"| RW Data (initialized) | {fmt(gt['rw_data'])} B ({gt['rw_data']/1024:.2f} KB) |")
+ w(f"| ZI Data (zero-init) | {fmt(gt['zi_data'])} B ({gt['zi_data']/1024:.1f} KB) |")
+ w(f"| **Total RO (Code + RO)** | **{fmt(total_ro)} B ({total_ro/1024:.1f} KB)** |")
+ w(f"| **Total RW (RW + ZI) = SRAM used** | **{fmt(total_rw)} B ({total_rw/1024:.2f} KB)** |")
+ w(f"| **Total ROM (Code + RO + RW init)** | **{fmt(total_rom)} B ({total_rom/1024:.1f} KB)** |")
+ w("")
+
+ # Execution regions
+ w("## 2. 执行区水位 (Execution Regions)")
+ w("")
+ w("| Region | Base | Size | Max | Util% |")
+ w("|---|---|---|---|---|")
+ for r in data["regions"]:
+ util = r["size"] / r["max"] * 100 if r["max"] else 0
+ w(f"| {r['name']} | {hexfmt(r['exec_base'])} | {fmt(r['size'])} B ({r['size']/1024:.2f} KB) | {fmt(r['max'])} B ({r['max']/1024:.0f} KB) | {util:.1f}% |")
+ w("")
+
+ # IRAM1 entries (ZI + RW symbol detail)
+ zii_entries = [e for e in data["iram1_entries"] if e["type"] in ("Zero", "Data") and e["size"] > 0]
+ zii_entries.sort(key=lambda e: e["size"], reverse=True)
+ w("## 3. RW_IRAM1 逐符号明细(ZI + RW,按大小降序)")
+ w("")
+ w("| # | Address | Size (B) | Size (KB) | Type | Section | Object |")
+ w("|---|---|---|---|---|---|---|")
+ for idx, e in enumerate(zii_entries, 1):
+ kb = e["size"] / 1024
+ w(f"| {idx} | {hexfmt(e['exec_addr'])} | {fmt(e['size'])} | {kb:.2f} | {e['type']} | {e['section']} | {e['object']} |")
+ total_zii = sum(e["size"] for e in zii_entries)
+ w(f"| | | **{fmt(total_zii)}** | **{total_zii/1024:.2f}** | | | **Total** |")
+ w("")
+
+ # Image component sizes (sorted by RW+ZI desc) — 仅保留 *.o 个体行,求和与 Grand Totals 对齐
+ comps = [c for c in data["components"] if not _is_aggregate(c["object"])]
+ comps.sort(key=lambda c: c["rw_data"] + c["zi_data"], reverse=True)
+ top_n = [c for c in comps if c["rw_data"] + c["zi_data"] > 0]
+ w("## 4. Image Component Sizes(RW + ZI > 0,按大小降序)")
+ w("")
+ w("| # | Object | Code | RO | RW | ZI | RW+ZI |")
+ w("|---|---|---|---|---|---|---|")
+ for idx, c in enumerate(top_n, 1):
+ rw_zi = c["rw_data"] + c["zi_data"]
+ w(f"| {idx} | {c['object']} | {fmt(c['code'])} | {fmt(c['ro_data'])} | {fmt(c['rw_data'])} | {fmt(c['zi_data'])} | {fmt(rw_zi)} |")
+ total_comp_rw = sum(c["rw_data"] for c in comps)
+ total_comp_zi = sum(c["zi_data"] for c in comps)
+ total_comp_code = sum(c["code"] for c in comps)
+ total_comp_ro = sum(c["ro_data"] for c in comps)
+ w(f"| | **All .o files** | **{fmt(total_comp_code)}** | **{fmt(total_comp_ro)}** | **{fmt(total_comp_rw)}** | **{fmt(total_comp_zi)}** | **{fmt(total_comp_rw+total_comp_zi)}** |")
+ w("")
+
+ # Flash breakdown (sorted by Code+RO desc)
+ flash_comps = sorted(comps, key=lambda c: c["code"] + c["ro_data"], reverse=True)
+ flash_top = [c for c in flash_comps if c["code"] + c["ro_data"] > 100]
+ w("## 5. Flash (Code + RO Data) Top-30(按大小降序)")
+ w("")
+ w("| # | Object | Code | RO Data | Code+RO |")
+ w("|---|---|---|---|---|")
+ for idx, c in enumerate(flash_top[:30], 1):
+ c_ro = c["code"] + c["ro_data"]
+ w(f"| {idx} | {c['object']} | {fmt(c['code'])} | {fmt(c['ro_data'])} | {fmt(c_ro)} |")
+ total_code = sum(c["code"] for c in comps)
+ total_ro_d = sum(c["ro_data"] for c in comps)
+ w(f"| | **All .o files** | **{fmt(total_code)}** | **{fmt(total_ro_d)}** | **{fmt(total_code+total_ro_d)}** |")
+ w("")
+
+# Self-consistency check against Grand Totals
+ # Per-file sums exclude "(incl. Padding/Generated)" linker metadata; their bytes are
+ # counted in Grand Totals, so a small positive delta (~300 B here) is expected.
+ if gt:
+ tol = 8
+ deltas = {
+ "Code": total_comp_code - gt["code"],
+ "RO": total_comp_ro - gt["ro_data"],
+ "RW": total_comp_rw - gt["rw_data"],
+ "ZI": total_comp_zi - gt["zi_data"],
+ }
+ ok = all(abs(d) <= tol for d in deltas.values())
+ status = "PASS" if ok else "FAIL"
+ if all(d == 0 for d in deltas.values()):
+ note = "精确一致(含 '('incl. Generated)'-'(incl. Padding)' 台账行)"
+ else:
+ note = "差值=图像对齐填充(incl. Padding/Generated 行)"
+ deltas_txt = " / ".join(f"{k}Δ={d:+d}" for k, d in deltas.items())
+ w(f"> 自洽校验:per-file {fmt(total_comp_code)}/{fmt(total_comp_ro)}/{fmt(total_comp_rw)}/{fmt(total_comp_zi)}"
+ f" vs Grand Totals {fmt(gt['code'])}/{fmt(gt['ro_data'])}/{fmt(gt['rw_data'])}/{fmt(gt['zi_data'])}"
+ f"({deltas_txt};{note})→ **{status}**")
+ w("")
+
+ return "\n".join(lines)
+
+
+def main():
+ ap = argparse.ArgumentParser(description="Parse Keil .map → Markdown RAM/Flash report")
+ ap.add_argument("map_file", nargs="?", default=MAP_DEFAULT, help="Path to .map file")
+ ap.add_argument("-o", "--output", help="Output file (default: stdout)")
+ args = ap.parse_args()
+
+ if not os.path.exists(args.map_file):
+ print(f"ERROR: Map file not found: {args.map_file}", file=sys.stderr)
+ sys.exit(1)
+
+ data = parse_map(args.map_file)
+ report = generate_report(data, args.map_file)
+
+ if args.output:
+ pathlib.Path(args.output).parent.mkdir(parents=True, exist_ok=True)
+ pathlib.Path(args.output).write_text(report, encoding="utf-8")
+ print(f"OK: Report written to {args.output}", file=sys.stderr)
+ else:
+ print(report)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/arch_diagram/gen_memory_svg.py b/tools/arch_diagram/gen_memory_svg.py
new file mode 100644
index 00000000..5d9623d4
--- /dev/null
+++ b/tools/arch_diagram/gen_memory_svg.py
@@ -0,0 +1,342 @@
+#!/usr/bin/env python3
+"""Generate memory layout SVG diagrams for STM32F4-Base.
+
+Usage:
+ python tools/arch_diagram/gen_memory_svg.py [--type ram|flash|taskstack|all]
+
+Output (default --all):
+ docs/images/内存布局_RAM.svg
+ docs/images/内存布局_FLASH.svg
+ docs/images/任务栈预算.svg
+"""
+import argparse
+import os
+import sys
+
+# ---------------------------------------------------------------------------
+# Diagram data
+# ---------------------------------------------------------------------------
+
+# ── RAM breakdown (128KB = 131,072 B) ────────────────────────────────────────
+SRAM_TOTAL = 131072
+SRAM_USED = 125104
+SRAM_FREE = SRAM_TOTAL - SRAM_USED
+
+RAM_CATEGORIES = [
+ ("FreeRTOS 堆\n(48KB heap_4)", 49152, "#2196F3"),
+ ("LVGL 内存池\n(40KB)", 40960, "#4CAF50"),
+ ("UI 帧缓冲\n(~13KB)", 13158, "#FF9800"),
+ ("FTL / 存储\n(~11KB)", 12416, "#9C27B0"),
+ ("网络 / CH395F\n(~7KB)", 7110, "#F44336"),
+ ("内核 / CMSIS\n(~3KB)", 3024, "#00BCD4"),
+ ("HAL 驱动\n(~1.4KB)", 1400, "#607D8B"),
+ ("其他静态\n(~0.5KB)", 498, "#795548"),
+ ("SRAM 空闲", SRAM_FREE, "#E0E0E0"),
+]
+
+# ── Flash breakdown (1MB = 1,048,576 B) ─────────────────────────────────────
+FLASH_TOTAL = 1048576
+FLASH_CODE = 219976
+FLASH_RO = 11496
+FLASH_RW = 528
+FLASH_USED = FLASH_CODE + FLASH_RO + FLASH_RW
+FLASH_FREE = FLASH_TOTAL - FLASH_USED
+
+FLASH_CATEGORIES = [
+ ("代码段 (Code)", FLASH_CODE, "#2196F3"),
+ ("只读数据 (RO)", FLASH_RO, "#4CAF50"),
+ ("初始化数据 (RW)", FLASH_RW, "#FF9800"),
+ ("Flash 空闲", FLASH_FREE, "#E0E0E0"),
+]
+
+# Flash code sub-breakdown (top categories from map analysis)
+FLASH_CODE_SUB = [
+ ("LVGL 库", 69364, "#4CAF50"),
+ ("应用代码/UI", 42540, "#2196F3"),
+ ("C 库 (microlib)", 4230, "#795548"),
+ ("FatFS + dhara", 11540, "#9C27B0"),
+ ("CH395F 网络", 8776, "#F44336"),
+ ("HAL 驱动", 9400, "#607D8B"),
+ ("FreeRTOS", 4240, "#00BCD4"),
+ ("其他", 69886, "#E0E0E0"), # remainder
+]
+
+# ── Task stack budgets ─────────────────────────────────────────────────────
+TASK_STACKS = [
+ ("defaultTask", 2048, "Normal", "FatFS 挂载、LED"),
+ ("netTask", 4096, "Normal", "CH395F 轮询、消息处理"),
+ ("ftpTask", 4096, "Normal", "lftpd FTP 服务"),
+ ("testTask", 4096, "BelowNormal", "测试套件"),
+ ("uiTask", 16384, "BelowNormal", "LVGL UI 主循环"),
+ ("timeSyncTask", 2048, "Normal", "TCP 时间同步"),
+]
+
+HEAP_FREE = 49152 - (2048+4096+4096+4096+16384+2048+512+1024+80*8+448+24)
+# = 49152 - 35304 = 13848 (approximate; stacks+TCBs+queue+eventgroup)
+
+# ---------------------------------------------------------------------------
+# SVG helpers
+# ---------------------------------------------------------------------------
+
+def _text_width(text, char_w=7.2):
+ """Rough monospace text width estimate (px)."""
+ return len(text) * char_w
+
+
+def _wrap(text, max_chars=12):
+ """Wrap label to multiple lines."""
+ words = text.split("\n") if "\n" in text else text.split()
+ lines = []
+ current = ""
+ for w in words:
+ test = (current + " " + w).strip() if current else w
+ if len(test) > max_chars and current:
+ lines.append(current)
+ current = w
+ else:
+ current = test
+ if current:
+ lines.append(current)
+ return lines
+
+
+def svg_header(w, h):
+ return (
+ f'\n'
+
+
+def _draw_bar_segment(x, y, w, h, fill, label_inside, label_outside="",
+ label_font=10, min_inside_w=40):
+ """Return SVG fragment for a bar segment with label."""
+ parts = []
+ parts.append(f' \n')
+ if w > min_inside_w and label_inside:
+ # Center text inside bar
+ lines = _wrap(label_inside, max_chars=int(w / 7))
+ ty = y + h / 2 - (len(lines) - 1) * 6
+ for line in lines:
+ lx = x + w / 2
+ parts.append(f' {line}\n')
+ ty += 12
+ elif label_outside:
+ lx = x + w + 4
+ ty = y + h / 2
+ parts.append(f' {label_outside}\n')
+ return "".join(parts)
+
+
+# ---------------------------------------------------------------------------
+# Diagram generators
+# ---------------------------------------------------------------------------
+
+def gen_ram_svg():
+ """内存布局_RAM.svg — horizontal stacked bar with legend."""
+ W, H = 820, 320
+ BAR_X, BAR_Y, BAR_H = 60, 50, 50
+ SCALE = (W - 80) / SRAM_TOTAL
+
+ svg = svg_header(W, H)
+ svg += f'SRAM (128KB) 使用布局\n'
+
+ x = BAR_X
+ for label, size, color in RAM_CATEGORIES:
+ w = size * SCALE
+ if w < 1:
+ continue
+ inside = label.replace("\n", " ") if w > 30 else ""
+ outside = "" if w > 30 else label.replace("\n", " ")
+ svg += _draw_bar_segment(x, BAR_Y, w, BAR_H, color, inside, outside)
+ x += w
+
+ # Legend
+ lx, ly = 60, 130
+ for label, size, color in RAM_CATEGORIES:
+ pct = size / SRAM_TOTAL * 100
+ kb = size / 1024
+ display = label.replace("\n", " ")
+ svg += f' \n'
+ svg += f' '
+ svg += f'{display}: {kb:.1f}KB ({pct:.1f}%)\n'
+ ly += 16
+ if ly > H - 20:
+ lx += 260
+ ly = 130
+
+ # Summary text
+ svg += f''
+ svg += f'已用: {SRAM_USED/1024:.1f}KB ({SRAM_USED/SRAM_TOTAL*100:.1f}%)'
+ svg += f' 空闲: {SRAM_FREE/1024:.1f}KB ({SRAM_FREE/SRAM_TOTAL*100:.1f}%)'
+ svg += f' 总量: 128KB\n'
+
+ svg += svg_footer()
+ return svg
+
+
+def gen_flash_svg():
+ """内存布局_FLASH.svg — bar + code sub-breakdown."""
+ W, H = 820, 240
+ BAR_X, BAR_Y, BAR_H = 60, 50, 40
+ SCALE = (W - 80) / FLASH_TOTAL
+
+ svg = svg_header(W, H)
+ svg += f'Flash (1MB) 使用布局\n'
+
+ x = BAR_X
+ for label, size, color in FLASH_CATEGORIES:
+ w = size * SCALE
+ if w < 1:
+ continue
+ inside = f"{label} {size/1024:.1f}KB" if w > 60 else ""
+ outside = "" if w > 60 else f"{label} {size/1024:.1f}KB"
+ svg += _draw_bar_segment(x, BAR_Y, w, BAR_H, color, inside, outside)
+ x += w
+
+ # Summary below bar
+ svg += f''
+ svg += f'已用: {FLASH_USED/1024:.1f}KB ({FLASH_USED/FLASH_TOTAL*100:.1f}%)'
+ svg += f' 空闲: {FLASH_FREE/1024:.1f}KB ({FLASH_FREE/FLASH_TOTAL*100:.1f}%)'
+ svg += f' 总量: 1024KB\n'
+
+ # Code sub-breakdown mini bar
+ Y2 = 130
+ svg += f'代码段明细 ({FLASH_CODE/1024:.1f}KB)\n'
+ SCALE2 = (W - 80) / FLASH_CODE
+ x2 = BAR_X
+ for label, size, color in FLASH_CODE_SUB:
+ w = size * SCALE2
+ if w < 1:
+ continue
+ inside = f"{label} {size/1024:.1f}KB" if w > 50 else ""
+ outside = "" if w > 50 else f"{label} {size/1024:.1f}KB"
+ svg += _draw_bar_segment(x2, Y2, w, 24, color, inside, outside)
+ x2 += w
+
+ svg += svg_footer()
+ return svg
+
+
+def gen_taskstack_svg():
+ """任务栈预算.svg — horizontal bars for each task's stack allocation."""
+ W, H = 740, 340
+ BAR_X = 180
+ MAX_STACK = 16384
+ SCALE = (W - BAR_X - 40) / MAX_STACK
+ ROW_H = 30
+ Y_START = 60
+
+ svg = svg_header(W, H)
+ svg += f'FreeRTOS 任务栈分配 (48KB 堆池)\n'
+
+ # Header
+ svg += f'分配栈大小 (Bytes)\n'
+
+ y = Y_START
+ for name, stack, prio, desc in TASK_STACKS:
+ w = stack * SCALE
+ color = "#2196F3" if "Normal" in prio else "#FF9800"
+
+ # Task name label
+ svg += f' {name}\n'
+
+ # Bar
+ svg += f' \n'
+
+ # Size inside bar
+ if w > 60:
+ svg += f' {stack}B ({stack/1024:.1f}KB)\n'
+ else:
+ svg += f' {stack}B\n'
+
+ y += ROW_H + 4
+
+ # FreeRTOS heap summary
+ total_stacks = sum(s for _, s, _, _ in TASK_STACKS) + 512 + 1024 # app + idle + timer
+ y += 4
+ svg += f''
+ svg += f'堆总量: 49,152B (48KB) '
+ svg += f'任务栈合计: ~{total_stacks}B ({total_stacks/1024:.1f}KB) '
+ svg += f'空闲: ~{HEAP_FREE}B ({HEAP_FREE/1024:.1f}KB)\n'
+
+ # Color legend
+ y += 18
+ svg += f' \n'
+ svg += f' Normal 优先级\n'
+ svg += f' \n'
+ svg += f' BelowNormal 优先级\n'
+
+ svg += svg_footer()
+ return svg
+
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+
+def main():
+ parser = argparse.ArgumentParser(description="Generate memory layout SVG diagrams")
+ parser.add_argument("--type", choices=["ram", "flash", "taskstack", "all"], default="all")
+ args = parser.parse_args()
+
+ out_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
+ "docs", "images")
+ os.makedirs(out_dir, exist_ok=True)
+
+ generators = {
+ "ram": ("内存布局_RAM.svg", gen_ram_svg),
+ "flash": ("内存布局_FLASH.svg", gen_flash_svg),
+ "taskstack": ("任务栈预算.svg", gen_taskstack_svg),
+ }
+
+ if args.type == "all":
+ targets = generators.values()
+ else:
+ targets = [generators[args.type]]
+
+ for filename, gen_func in targets:
+ path = os.path.join(out_dir, filename)
+ svg = gen_func()
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(svg)
+ print(f"OK: {filename} ({len(svg)} bytes)")
+
+ # XML self-check
+ for filename, _ in targets:
+ path = os.path.join(out_dir, filename)
+ with open(path, "r", encoding="utf-8") as f:
+ content = f.read()
+ if not content.startswith("", file=sys.stderr)
+ sys.exit(1)
+ if 'xmlns="http://www.w3.org/2000/svg"' not in content:
+ print(f"FAIL: {filename} missing xmlns", file=sys.stderr)
+ sys.exit(1)
+ print("All SVG self-checks passed")
+
+
+if __name__ == "__main__":
+ main()