2. TouchGFX低内存平台移植:硬件抽象层(HAL)基础与初始化流程

好,我们正式开始移植实战的第一关——HAL层。

很多初学者拿到TouchGFX,第一反应是去改框架代码。其实不用。TouchGFX把硬件相关的部分都抽象出来了,你只需要填充一个结构体,实现几个接口函数。说白了,HAL层就是TouchGFX和你的MCU硬件之间的翻译官。

我个人习惯把HAL初始化拆成三步走:时钟与显示接口准备 → HAL对象构造与配置 → 帧缓冲与刷新机制绑定。这三步走通了,你的屏幕就能亮起来,TouchGFX就能跑起来。

核心认知:HAL层不负责UI逻辑,它只负责两件事——把像素数据送到屏幕,把触摸/按键事件送给框架。其他事情,TouchGFX内部自己搞定。

2.1 HAL层在TouchGFX中的位置

先看一张整体架构图,帮你建立全局视角。

TouchGFX 软件栈与HAL层位置 应用层(Screen / Presenter / Model) TouchGFX 框架(Drawable / Container / Widget) 硬件抽象层(HAL / LCD / TouchController) 板级支持包(BSP / 驱动 / 外设初始化) 移植关注区 HAL + BSP 约 80% 工作量 框架层不动 应用层不动 关键接口 HAL::init() LCD::draw() TouchController FrameBuffer

看到红色那块了吗?那就是我们今天要啃的骨头。上面两层你不用动,下面那层BSP你本来就有。HAL层就是连接它们的桥梁。

我的经验:刚开始移植时,别急着把HAL写得完美。先让屏幕能显示一个纯色,再逐步加功能。我见过太多人一上来就想把DMA、双缓冲、局部刷新全搞定,结果卡在一个像素格式上三天出不来。

2.2 HAL初始化核心流程

TouchGFX的HAL初始化,本质上就是填充一个HAL::DMA、一个LCD、一个TouchController,然后调用HAL::init()。就这么简单。

但细节藏在流程里。我们拆开看:

  1. 硬件外设初始化——时钟、GPIO、SPI/FSMC/LTDC、DMA等。这部分属于BSP,但HAL依赖它。
  2. 帧缓冲地址配置——告诉TouchGFX你的显存放在哪里。低内存平台通常用内部SRAM或外部PSRAM。
  3. LCD控制器实例化——根据你的显示接口(RGB并行、SPI串行、MIPI DSI等)选择对应的LCD类。
  4. 触摸控制器实例化——实现sampleTouch()接口,返回触摸坐标。
  5. HAL对象构造——把上面三个对象传给HAL构造函数。
  6. 调用HAL::init()——框架内部完成定时器、刷新策略、帧缓冲管理等初始化。

嗯,这里要注意:第1步和第2步的顺序不能乱。你得先确保内存和显示接口能工作,再去构造HAL对象。

2.3 关键数据结构与接口

我们来看一个典型的HAL初始化代码。这是我在STM32G070上做的一个低内存移植示例,帧缓冲只有32KB。

// 1. 硬件初始化(BSP层)
void BSP_Init()
{
    HAL_Init();
    SystemClock_Config();    // 配置系统时钟
    MX_GPIO_Init();          // 初始化GPIO
    MX_SPI1_Init();          // 初始化SPI(接LCD)
    MX_DMA_Init();           // 初始化DMA(用于刷屏)
}

// 2. 帧缓冲地址定义
// 低内存平台:使用内部SRAM,地址固定
#define FRAME_BUFFER_ADDR   0x20000000  // STM32G070 SRAM起始
#define FRAME_BUFFER_SIZE   (128 * 128 * 2)  // 128x128 RGB565 = 32KB

// 3. LCD控制器实例化
// 使用SPI接口的ST7735,128x128分辨率
static ST7735_LCD lcd;
static ST7735_Touch touch;

// 4. HAL对象构造与初始化
void TouchGFX_HAL_Init()
{
    // 构造HAL对象
    HAL hal(
        dma,            // DMA接口(用于快速刷屏)
        lcd,            // LCD显示驱动
        touch,          // 触摸驱动
        FRAME_BUFFER_ADDR,  // 帧缓冲地址
        128,            // 显示宽度
        128,            // 显示高度
        0,              // 虚拟缓冲区(低内存不用)
        0               // 预渲染缓冲区(低内存不用)
    );

    // 初始化HAL
    hal.init();

    // 设置帧缓冲策略
    // 低内存平台建议使用单缓冲 + 局部刷新
    hal.setFrameBufferStrategy(FrameBufferStrategy::SINGLE_BUFFER);
    hal.setRefreshStrategy(RefreshStrategy::PARTIAL_REFRESH);
}

踩坑警告:我曾经在一个项目里把帧缓冲地址写错了,指向了代码区。结果屏幕死活不亮,调试器一看,HAL初始化直接HardFault。检查地址是否可写、是否对齐、是否被其他模块占用,这是第一步。

2.4 低内存平台的HAL配置要点

低内存平台和普通平台最大的区别在哪?说白了就三个字:省着用。

我整理了一个对比表,帮你快速理解差异:

配置项 普通平台(>256KB RAM) 低内存平台(≤64KB RAM)
帧缓冲策略 双缓冲或三缓冲 单缓冲
刷新策略 全屏刷新 局部刷新
帧缓冲位置 外部SDRAM 内部SRAM或PSRAM
像素格式 RGB888或RGB565 RGB565或L8
DMA使用 必须,用于快速刷屏 可选,CPU直接刷也可
虚拟缓冲区 需要,用于动画平滑 关闭

你看,低内存平台几乎每一项都在做减法。但减法不等于阉割功能,而是用更精巧的方式实现同样的效果。

我的建议:如果你用的是内部SRAM,帧缓冲大小要精打细算。128x128的RGB565刚好32KB,很多MCU的SRAM也就64KB,剩下32KB给堆栈和全局变量。再大就得上外部PSRAM了。

2.5 初始化后的验证方法

代码写完了,怎么知道HAL初始化成功了?别急着跑TouchGFX的Demo,先做两个简单测试:

测试一:纯色填充

直接往帧缓冲地址写一个颜色值,看屏幕是否显示对应颜色。这是最基础的硬件通路验证。

// 测试:将帧缓冲全部填充为红色(RGB565: 0xF800)
uint16_t* fb = (uint16_t*)FRAME_BUFFER_ADDR;
for (int i = 0; i < 128 * 128; i++) {
    fb[i] = 0xF800;
}
// 触发一次刷屏
lcd.flushDisplayBuffer(FRAME_BUFFER_ADDR, 0, 0, 128, 128);

测试二:触摸坐标回显

在屏幕上显示触摸点的坐标值。如果触摸没反应,检查sampleTouch()的SPI读取时序。

// 测试:轮询触摸并打印坐标
while (1) {
    int x, y;
    if (touch.sampleTouch(x, y)) {
        printf("Touch: (%d, %d)\r\n", x, y);
    }
    HAL_Delay(10);
}

这两个测试过了,你的HAL层就基本稳了。接下来就可以放心地跑TouchGFX的Demo了。

记住:HAL初始化不是一次性的工作。随着你后续添加DMA、双缓冲、局部刷新等功能,HAL的配置会不断调整。但基础框架——时钟、帧缓冲、LCD、触摸——这四根柱子必须一开始就立稳。

嗯,这一章的内容就到这。HAL初始化看起来代码不多,但每一行背后都有硬件细节。你动手做一遍,遇到问题再回来看,理解会更深刻。