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 @@ + + + + +FreeRTOS 任务栈分配 (48KB 堆池) +分配栈大小 (Bytes) + defaultTask + + 2048B (2.0KB) + netTask + + 4096B (4.0KB) + ftpTask + + 4096B (4.0KB) + testTask + + 4096B (4.0KB) + uiTask + + 16384B (16.0KB) + timeSyncTask + + 2048B (2.0KB) +堆总量: 49,152B (48KB) 任务栈合计: ~34304B (33.5KB) 空闲: ~13736B (13.4KB) + + Normal 优先级 + + BelowNormal 优先级 + 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 @@ + + + + +Flash (1MB) 使用布局 + + 代码段 (Code) 214.8KB + + 只读数据 (RO) 11.2KB + + Flash 空闲 797.4KB +已用: 226.6KB (22.1%) 空闲: 797.4KB (77.9%) 总量: 1024KB +代码段明细 (214.8KB) + + LVGL 库 67.7KB + + 应用代码/UI 41.5KB + + C 库 (microlib) 4.1KB + + FatFS + dhara 11.3KB + + CH395F 网络 8.6KB + + HAL 驱动 9.2KB + + FreeRTOS 4.1KB + + 其他 68.2KB + 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 @@ + + + + +SRAM (128KB) 使用布局 + + FreeRTOS 堆 (48KB heap_4) + + LVGL 内存池 (40KB) + + UI 帧缓冲 + (~13KB) + + FTL / 存储 + (~11KB) + + 网络 / + CH395F + (~7KB) + + 内核 / CMSIS (~3KB) + + HAL 驱动 (~1.4KB) + + 其他静态 (~0.5KB) + + + FreeRTOS 堆 (48KB heap_4): 48.0KB (37.5%) + + LVGL 内存池 (40KB): 40.0KB (31.2%) + + UI 帧缓冲 (~13KB): 12.8KB (10.0%) + + FTL / 存储 (~11KB): 12.1KB (9.5%) + + 网络 / CH395F (~7KB): 6.9KB (5.4%) + + 内核 / CMSIS (~3KB): 3.0KB (2.3%) + + HAL 驱动 (~1.4KB): 1.4KB (1.1%) + + 其他静态 (~0.5KB): 0.5KB (0.4%) + + SRAM 空闲: 5.8KB (4.6%) +已用: 122.2KB (95.4%) 空闲: 5.8KB (4.6%) 总量: 128KB + 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 功能分区总览 + +![SRAM 布局图](../images/内存布局_RAM.svg) + +| 分类 | 大小 (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 分配器。 + +![任务栈预算](../images/任务栈预算.svg) + +### 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 分布 + +![Flash 布局图](../images/内存布局_FLASH.svg) + +### 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' + '\n' + '\n' + '\n' + ) + + +def svg_footer(): + return '\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("" not in content: + print(f"FAIL: {filename} missing ", 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()