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.c、stm32h7rsxx_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 地址(通常无需修改) |
| 烧录脚本 | 通过 cpp 从 memory_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 更新烧写脚本
烧写脚本通过 cpp 从 memory_map.h 自动获取地址,避免硬编码。
6.5 更新 C 源码引用
将 #include "memory_map.h" 添加到需要地址常量的头文件中,删除原有的硬编码宏定义。
7. 注意事项
7.1 memory_map.h 的限制
由于该文件被 C 源码和链接脚本模板共用,必须遵守:
- 不能 使用
UL、U等 C 语言后缀 - 不能 包含
<stdint.h>等 C 库头文件 - 不能 使用 C 语言类型(如
uint32_t)
7.2 固件加密
如果后续需要加密固件:
- 只加密 Slot 中的固件
.bin(代码和数据) resources.bin保持不加密(CPU 需要直接 XIP 读取位图数据,加密后无法直接读取)
7.3 非 TouchGFX 框架
如果使用其他 GUI 框架(如 LVGL):
- 需要确认框架是否支持
ExtFlashSection类似的段标记 - 如果不支持,可以手动将资源数组用
__attribute__((section("ExtFlashSection")))标记 - 或者框架直接从文件系统/地址指针加载资源,则不需要本方案