MCU GUI资源分离方案(TOUCHGFX/LVGL/其他)

MCU GUI资源分离方案(TOUCHGFX/LVGL/其他)

0. 环境说明

0.1 芯片平台

项目 说明
MCU STM32H7R7xx, Cortex-M7 @ 600MHz
内部 Flash 64KB(仅放 Boot)
内部 RAM AXI SRAM 456KB @ 0x24000000 + DTCM 64KB + ITCM 64KB
外部 NOR Flash MX25UM25645G, 32MB, XSPI1, memory-mapped @ 0x90000000
外部 HyperRAM 8MB, XSPI2, memory-mapped @ 0x70000000
显示 RGB565 800×480 LTDC

0.2 工程结构

工程由 CubeMX 生成基础 HAL 配置,采用 Makefile 构建系统,不依赖 IDE。

复制代码
工程根目录/
├── makefile                    # 顶层 Makefile,统一调度各子工程
├── Makefile/
│   ├── Appli/Makefile          # 应用固件构建(本方案主要修改对象)
│   ├── Boot/Makefile           # Bootloader 构建
│   └── ExtMemLoader/Makefile   # 外部存储器加载器
├── Common/Inc/                 # 共用头文件
├── Drivers/                    # HAL 驱动 + BSP
├── Appli/                      # 应用代码(含 TouchGFX / LVGL)
├── Boot/                       # Bootloader 代码
└── Tools/                      # 工具脚本(烧录、OTA 打包等)

子工程均可独立构建,顶层 make 依次编译全部:

bash 复制代码
make              # 编译全部
make Appli        # 仅编译应用
make Boot         # 仅编译 Bootloader
make ExtMemLoader # 仅编译外部存储器加载器

0.3 CubeMX 生成策略

工程由 STM32CubeMX 生成基础 HAL 代码,之后手动添加业务逻辑。CubeMX 会覆盖 Core/ 目录下的部分文件(如 main.cstm32h7rsxx_it.c),但不会覆盖

  • Makefile/Appli/Makefile(用户自行维护的构建脚本)
  • Common/ 目录(共用头文件和源码)
  • Appli/TouchGFX/ 目录(TouchGFX 应用层)
  • Appli/task/Appli/UI/Appli/Protocol/ 等业务代码

链接脚本由 CubeMX 生成初始版本(.ld),用户将其转换为模板文件(.ld.in)后通过预处理自动生成。

0.4 LRUN 模式(Load to RAM and Run)

本工程采用 LRUN 模式:代码存储在 NOR Flash 中,启动时由 Bootloader 拷贝到 HyperRAM 中执行。

复制代码
启动流程:
Boot (内部Flash) → 读取 Slot A/B → 拷贝到 HyperRAM → 跳转执行
                    ↓                    ↓
              NOR Flash 0x90010000   HyperRAM 0x70000000

为什么要用 LRUN 而非直接 XIP?

方式 优点 缺点
XIP(原地执行) 启动快,无需拷贝 NOR Flash 读取速度慢(~80MB/s),影响性能
LRUN(拷贝到 RAM) 执行速度快(HyperRAM ~200MB/s),性能稳定 启动需拷贝耗时

LRUN 模式下,代码段(.text.rodata)的 VMA 在 HyperRAM ,LMA 在 NOR Flash。链接脚本中通过 AT> 指定 LMA:

ld 复制代码
.text : { *(.text*) } > EXTRAM    /* VMA = HyperRAM */
.text : { *(.text*) } AT> FLASH   /* LMA = NOR Flash */

Bootloader 在启动时完成代码从 NOR Flash 到 HyperRAM 的拷贝。

资源分离与 LRUN 的关系:

资源分离后,位图/字体数据存放在 NOR Flash 的资源分区,不拷贝到 HyperRAM 。TouchGFX 在渲染时通过 blockCopy() 直接从 NOR Flash 的 XIP 地址读取。因为位图数据量大但访问模式是顺序的流式读取,NOR Flash 的 XIP 性能足以满足显示需求,不需要占用宝贵的 HyperRAM 空间。


1. 目的

1.1 问题

带图形用户界面(GUI)的嵌入式固件中,图片、字体等资源文件往往占据大量空间。以 TouchGFX 为例,位图数据以 C 语言数组的形式编译到固件中,导致固件体积远超出 Flash 分区的可用空间。

1.2 根因

通过分析 TouchGFX 生成的资源文件可以发现,固件体积中占大头的是位图数据

资源类型 源码体积 说明
TouchGFX 图片 数 MB ~ 数十 MB 编译后仍占大量空间
TouchGFX 字体 约 100 KB 较小
TouchGFX 文本 约 20 KB 可忽略
实际代码 + 运行时数据 约 1 MB 纯逻辑

1.3 解决思路

将 GUI 的位图和字体数据(统称「资源」)从固件中剥离 出来,单独存放在 Flash 的资源分区中。固件本身只包含代码和运行时数据,缩小到各槽位容量以内。

NOR Flash 通常是 memory-mapped 的(XIP,eXecute In Place),CPU 可以直接从 NOR Flash 地址读取数据。GUI 框架在渲染时通过 blockCopy() 或直接 memcpy 从 NOR Flash 读取位图数据,不需要先拷贝到 RAM 再使用。

1.4 适用范围

  • 使用 TouchGFX 的项目(资源自动带有 ExtFlashSection / FontFlashSection 段标记)
  • 使用 LVGL 的项目(需要另行设计资源加载方式)
  • 其他将资源以数组形式编译到固件的 GUI 框架

2. Flash 分区布局

2.1 示例布局

复制代码
NOR Flash (XSPI1, 32MB, memory-mapped @ 0x90000000)
┌─────────────────┬──────────────┬──────────────┬──────────────────┬──────────┐
│   Boot Info     │   Slot A     │   Slot B     │  Resource 分区   │ Data 区  │
│   (主/备各512B) │   (固件)     │   (固件)     │  (图片/字体)     │          │
│   64KB 对齐     │   3 MB       │   3 MB       │  6 MB            │ 剩余     │
├─────────────────┼──────────────┼──────────────┼──────────────────┼──────────┤
│ 0x90000000      │ 0x90010000   │ 0x90310000   │ 0x90610000       │ ...      │
└─────────────────┴──────────────┴──────────────┴──────────────────┴──────────┘

各分区由 memory_map.h 中的宏自动计算得出:

c 复制代码
#define SLOT_SIZE               0x00300000          /* 每槽 3MB              */

#define SLOT_A_OFFSET           0x00010000          /* 64KB 对齐             */
#define SLOT_A_ADDR             (NOR_FLASH_BASE + SLOT_A_OFFSET)

/* Slot B 在 Slot A 之后 */
#define SLOT_B_ADDR             (SLOT_A_ADDR + SLOT_SIZE)

/* Resource 分区在 Slot B 之后 */
#define RESOURCE_ADDR           (SLOT_B_ADDR + SLOT_SIZE)
#define RESOURCE_SIZE           0x00600000          /* 6MB                   */

/* Data 区在 Resource 之后 */
#define DATA_START_OFFSET       (RESOURCE_ADDR + RESOURCE_SIZE)

2.2 分区大小设计原则

分区 大小确定依据
Slot A/B 剥离资源后的固件大小 × 余量(建议 1.5~2 倍)
Resource 资源总量(位图 + 字体 + 其他)
Data 总 Flash 容量减去前面所有分区

各槽位和资源分区的大小应根据实际固件和资源体积 灵活调整,只需在 memory_map.h 中修改对应宏即可。


3. 文件清单与修改说明

3.1 新增文件

文件 用途
memory_map.h 所有内存地址常量的单点定义(需放在共用 include 目录)
linker_script.ld.in 链接脚本模板(#include "memory_map.h",通过 cpp -P 预处理)
flash.sh 统一烧录脚本

3.2 需要修改的文件

文件 改动内容
原有地址宏定义的头文件 删除重复的地址宏,改为 #include "memory_map.h"
所有硬编码地址常量的源文件 改为引用 memory_map.h 中的宏
Makefile 链接脚本改为从 .ld.in 预处理生成;.bin--remove-section 排除资源段;新增 resources.bin 提取规则
GUI HAL 层 确认 blockCopy() 可正确处理 memory-mapped 地址(通常无需修改)
烧录脚本 通过 cppmemory_map.h 自动获取地址

3.3 保持兼容(不需要修改)

  • MPU / Cache 配置中的地址(通常是 CubeMX 生成区)
  • 原有烧录脚本(作为参考保留)

4. 核心机制说明

4.1 资源分离原理

TouchGFX 的位图数据在生成时带有 LOCATION_ATTRIBUTE("ExtFlashSection") 标记:

c 复制代码
// 在 generated/images/src/image_background_day_full.cpp 中
LOCATION_PRAGMA("ExtFlashSection")
KEEP extern const unsigned char image_background_day_full[] LOCATION_ATTRIBUTE("ExtFlashSection") = { ... };

链接脚本将 ExtFlashSection(和 FontFlashSection)分配到 EXTFLASH 内存区域(即 NOR Flash 的资源分区地址 0x90410000)。

构建时,通过两条 objcopy 命令分别处理:

复制代码
1. 生成固件 .bin:   --remove-section=.ExtFlashSection --remove-section=.FontFlashSection
   → 只包含代码 + 运行时数据,约 1.2MB

2. 提取资源 .bin:   --only-section=.ExtFlashSection --only-section=.FontFlashSection
   → 只包含位图 + 字体数据,约 2.5MB(压缩后)

4.2 链接脚本模板预处理

链接脚本 .ld.in 使用 #include "memory_map.h" 引入地址宏,通过 cpp -P 预处理生成最终的 .ld 文件:

makefile 复制代码
# Makefile 中的规则
$(LDSCRIPT): $(LDSCRIPT_IN) ../../Common/Inc/memory_map.h | $(BUILD_DIR)
    cpp -P -I../../Common/Inc $< $@

注意: memory_map.h 中不能使用 UL 后缀,也不能包含 <stdint.h> 等 C 库头文件,因为 cpp 会将其展开为大量类型定义,污染链接脚本。

4.3 blockCopy 不需要重写

因为 NOR Flash 是 memory-mapped 的(映射到 CPU 地址空间 0x90000000),底层的 memcpy 或 DMA2D 可以直接从该地址读取数据。TouchGFX 默认的 blockCopy 实现已经可以正确处理,因此不需要重写。

cpp 复制代码
// TouchGFXHAL.cpp --- 恢复为默认实现
bool TouchGFXHAL::blockCopy(void* RESTRICT dest, const void* RESTRICT src, uint32_t numBytes)
{
    return TouchGFXGeneratedHAL::blockCopy(dest, src, numBytes);
}

4.4 烧写脚本地址自动获取

Tools/flash.sh 使用 cpp 在运行时从 memory_map.h 提取地址值:

bash 复制代码
get_addr() {
    local macro="$1"
    local val
    val=$(echo "#include \"memory_map.h\"
${macro}" | cpp -P -I"${SCRIPT_DIR}/Common/Inc" -x c - 2>/dev/null)
    echo $((val))
}

SLOT_A_ADDR=$(get_addr SLOT_A_ADDR)       # → 2415984640 (= 0x90010000)
SLOT_A_ADDR_HEX=$(printf '0x%X' $SLOT_A_ADDR)  # → 0x90010000

这样修改地址只需编辑 memory_map.h,烧写脚本自动同步。


5. 日常使用指南

5.1 构建

bash 复制代码
# 构建全部
make

构建产物:

复制代码
build/
├── firmware.elf         # 完整 ELF(含资源段)
├── firmware.bin         # 固件(已排除资源段)
├── resources.bin        # 资源(位图+字体)
└── linker.ld            # 预处理生成的链接脚本

5.2 修改内存地址

只需编辑 memory_map.h,重新构建即可------链接脚本、C 源码、烧写脚本全部自动同步。

5.3 资源更新

如果只更换了资源(没有修改代码),只需重新生成 resources.bin 并烧录资源分区:

bash 复制代码
make              # 重新编译,生成新的 resources.bin

6. 实施步骤

6.1 创建 memory_map.h

将项目中所有分散的地址常量收集到一个统一的头文件中,不要使用 UL 后缀,不要包含 <stdint.h> (该文件会被 cpp 预处理用于链接脚本)。

6.2 创建链接脚本模板

复制现有的 .ld 文件为 .ld.in,在文件开头添加 #include "memory_map.h",将 MEMORY 区域中的地址替换为对应的宏。

添加资源段:

ld 复制代码
.ExtFlashSection : { *(ExtFlashSection) *(ExtFlashSection.*) } > EXTFLASH
.FontFlashSection : { *(FontFlashSection) *(FontFlashSection.*) } > EXTFLASH

6.3 修改 Makefile

makefile 复制代码
CP = $(CROSS_COMPILE)objcopy
LDSCRIPT = $(BUILD_DIR)/linker.ld
LDSCRIPT_IN = linker.ld.in

$(LDSCRIPT): $(LDSCRIPT_IN) memory_map.h | $(BUILD_DIR)
    cpp -P -Ipath/to/include $< $@

# 固件 .bin 排除资源段
$(BUILD_DIR)/$(TARGET).bin: $(BUILD_DIR)/$(TARGET).elf | $(BUILD_DIR)
    $(CP) -O binary --remove-section=.ExtFlashSection --remove-section=.FontFlashSection $< $@

# 提取资源
$(BUILD_DIR)/resources.bin: $(BUILD_DIR)/$(TARGET).elf | $(BUILD_DIR)
    $(CP) -O binary --only-section=.ExtFlashSection --only-section=.FontFlashSection $< $@

6.4 更新烧写脚本

烧写脚本通过 cppmemory_map.h 自动获取地址,避免硬编码。

6.5 更新 C 源码引用

#include "memory_map.h" 添加到需要地址常量的头文件中,删除原有的硬编码宏定义。


7. 注意事项

7.1 memory_map.h 的限制

由于该文件被 C 源码和链接脚本模板共用,必须遵守:

  • 不能 使用 ULU 等 C 语言后缀
  • 不能 包含 <stdint.h> 等 C 库头文件
  • 不能 使用 C 语言类型(如 uint32_t

7.2 固件加密

如果后续需要加密固件:

  • 只加密 Slot 中的固件 .bin(代码和数据)
  • resources.bin 保持不加密(CPU 需要直接 XIP 读取位图数据,加密后无法直接读取)

7.3 非 TouchGFX 框架

如果使用其他 GUI 框架(如 LVGL):

  • 需要确认框架是否支持 ExtFlashSection 类似的段标记
  • 如果不支持,可以手动将资源数组用 __attribute__((section("ExtFlashSection"))) 标记
  • 或者框架直接从文件系统/地址指针加载资源,则不需要本方案

10 参考文档