2. TouchGFX基础回顾
好,咱们今天聊聊 TouchGFX 里一个特别关键的中间层——HAL。说白了,它就是 TouchGFX 和你的硬件之间的那个「翻译官」。没有它,TouchGFX 再漂亮也跑不起来。
我刚开始接触 TouchGFX 的时候,也纳闷过:为什么非要搞个 HAL 出来?直接操作寄存器不香吗?后来踩了几个坑才明白,HAL 的存在,是为了让 TouchGFX 的图形引擎能「无视」底层硬件差异。你换屏幕、换 MCU、换显存类型,只要把 HAL 层适配好,上层代码几乎不用动。
2.1 HAL 到底干了什么?
HAL 的全称是 Hardware Abstraction Layer,硬件抽象层。它的核心任务就三件:
- 屏蔽硬件差异:不管你的 LCD 接口是 RGB565 还是 LTDC,不管你的显存在内部 SRAM 还是外部 SDRAM,HAL 统一给 TouchGFX 提供一套标准接口。
- 提供帧缓冲管理:告诉 TouchGFX 显存在哪、怎么刷、什么时候刷。
- 传递触摸/按键事件:把物理触摸屏的坐标、按键的按下状态,翻译成 TouchGFX 能理解的事件。
一句话总结:HAL 是 TouchGFX 和 MCU 外设之间的「适配器」。你换硬件,只改 HAL;你优化性能,也只动 HAL。
我个人习惯把 HAL 想象成一个「插座」。TouchGFX 是电器,底层硬件是电线。HAL 就是那个插座——它规定了插孔的形状和电压,至于电线那头是火线还是零线,电器不关心。
2.2 HAL 的初始化流程
HAL 的初始化,说白了就是让 TouchGFX 知道「我的硬件长什么样」。流程不复杂,但每一步都容易出问题。我拆开来讲。
2.2.1 初始化流程总览
先看一张流程图,把整体脉络理清楚:
2.2.2 关键步骤详解
第一步:配置时钟
这一步最容易忽略。LCD 控制器需要特定的像素时钟,比如 480x272 的屏幕,60Hz 刷新率,像素时钟大概在 9MHz 左右。时钟配错了,屏幕要么不亮,要么闪烁。我曾经在一个项目里,因为 HSE 分频系数算错了一位,屏幕死活不出图,查了两天才发现是时钟树的问题。
第二步:初始化 LCD 控制器
这里要配置时序参数:HBP、HFP、VBP、VFP、HSYNC、VSYNC。每个屏幕的 datasheet 里都有这些值,直接抄就行。但要注意——有些屏幕的时序单位是像素时钟周期,有些是行周期,别搞混。
第三步:配置帧缓冲地址
显存放在哪?内部 SRAM 还是外部 SDRAM?这直接影响性能。我的建议是:
| 显存位置 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 内部 SRAM | 速度快,无需初始化 | 容量小(通常几十 KB) | 小分辨率、低色彩深度 |
| 外部 SDRAM | 容量大(几 MB 到几十 MB) | 需要初始化时序,速度略慢 | 大分辨率、高色彩深度、多缓冲 |
| 外部 SRAM/PSRAM | 速度较快,引脚少 | 容量中等,成本高 | 中等分辨率,对成本不敏感 |
第四步:初始化触摸/输入
触摸屏的 I2C 或 SPI 接口要提前配好。这里有个坑:触摸芯片的复位时序。有些芯片需要先拉低再拉高,延时至少 10ms。我见过有人没加延时,触摸永远没反应。
第五步:调用 HAL::init()
这一步是 TouchGFX 框架的入口。它会注册几个关键回调:
touchgfxDisplayDriverTransmitBlock:刷屏回调,告诉硬件把显存数据发到 LCD。touchgfxDisplayDriverIsTransmitInProgress:查询刷屏是否完成。touchgfxDisplayDriverShouldTransfer:判断是否需要开始新一帧的传输。
小技巧:如果你用双缓冲或三缓冲,记得在 HAL::init() 之前把多个帧缓冲地址都配好。TouchGFX 会在运行时自动切换。
2.3 HAL 与底层硬件的交互
HAL 和硬件的交互,说白了就是「回调函数 + 中断 + DMA」。TouchGFX 不直接操作寄存器,它通过 HAL 层调用你写的底层函数。
2.3.1 刷屏交互
这是最核心的交互。TouchGFX 渲染完一帧后,会调用 touchgfxDisplayDriverTransmitBlock 把显存数据发出去。这个函数里,你通常要做三件事:
- 启动 DMA 或 LTDC 的数据传输。
- 设置传输完成中断。
- 返回 true 表示传输已启动。
代码大概长这样:
// 伪代码,实际实现因平台而异
bool touchgfxDisplayDriverTransmitBlock(const uint8_t* pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
// 1. 计算目标显存地址
uint32_t destAddr = LCD_FRAME_BUFFER + (y * LCD_WIDTH + x) * 2;
// 2. 启动 DMA 传输
DMA_Start(pixels, destAddr, w * h * 2);
// 3. 使能传输完成中断
DMA_EnableInterrupt();
return true;
}
嗯,这里要注意:DMA 传输完成后,一定要在中断里调用 touchgfxDisplayDriverTransmitEnd(),告诉 TouchGFX 这一帧刷完了。否则 TouchGFX 会一直等,界面就卡住了。
2.3.2 触摸交互
触摸事件通过 HAL 的 setTouchSample 函数传递给 TouchGFX。通常在触摸中断里调用:
// 触摸中断服务函数
void Touch_IRQHandler(void)
{
uint16_t x, y;
uint8_t touched;
// 读取触摸控制器
Touch_Read(&x, &y, &touched);
// 传递给 TouchGFX
HAL::getInstance()->setTouchSample(touched, x, y);
// 触发 TouchGFX 处理
HAL::getInstance()->touchSampleReady();
}
避坑指南:我曾经在一个项目里,触摸中断优先级设得太高,导致刷屏 DMA 中断被抢占,屏幕出现撕裂。后来把触摸中断优先级降到比 DMA 低一级,问题就解决了。中断优先级的设计,一定要想清楚。
2.3.3 帧同步交互
TouchGFX 支持两种帧同步模式:
- VSYNC 同步:利用 LCD 的 VSYNC 信号触发帧切换。画面最流畅,没有撕裂。
- 软件定时同步:用定时器模拟帧率。简单,但可能出现撕裂。
我个人习惯用 VSYNC 同步。虽然要多配一个外部中断引脚,但效果确实好。配置方法很简单:把 LCD 的 VSYNC 引脚接到 MCU 的 EXTI 引脚上,在中断里调用 HAL::getInstance()->vSync() 即可。
2.4 一个完整的 HAL 初始化示例
最后,给一个完整的初始化代码骨架。这是我在 STM32H7 上用的模板,你可以参考:
void TouchGFX_HAL_Init(void)
{
// 1. 配置系统时钟(HAL 库自带函数)
SystemClock_Config();
// 2. 初始化 LCD 控制器
LCD_Init();
// 3. 配置帧缓冲地址
// 使用外部 SDRAM,地址 0xC0000000
uint8_t* frameBuffer = (uint8_t*)0xC0000000;
uint8_t* frameBuffer2 = (uint8_t*)0xC0100000; // 双缓冲
// 4. 初始化触摸
Touch_Init();
// 5. 创建 HAL 实例并初始化
HAL& hal = HAL::getInstance();
hal.initialize<DisplayDriver, TouchController>();
// 6. 设置帧缓冲
hal.setFrameBufferStartAddress(frameBuffer, 480, 272);
hal.setFrameBufferStartAddress(frameBuffer2, 480, 272);
// 7. 注册 VSYNC 回调(如果使用 VSYNC 同步)
// 在 VSYNC 中断中调用 hal.vSync();
}
嗯,这个骨架基本覆盖了所有关键步骤。你实际移植的时候,只需要把 LCD_Init()、Touch_Init() 换成你自己的硬件初始化代码就行。
总结一下:HAL 是 TouchGFX 和硬件之间的桥梁。初始化流程有固定的六步,每一步都有坑,但踩过一次就记住了。刷屏、触摸、帧同步这三个交互点,是 HAL 层最核心的工作。把这三点搞透,TouchGFX 的底层移植就算拿下了。