2. 开发环境搭建
上一章我们搭好了开发环境,也跑通了第一个Demo。但那个Demo是在模拟器上跑的,说白了就是“假硬件”。真正要把TouchGFX跑在你的板子上,必须过HAL这一关。
HAL的全称是Hardware Abstraction Layer,硬件抽象层。它就像一座桥——桥的一头是TouchGFX的图形引擎,另一头是你的LCD、触摸屏、帧缓冲。桥搭不好,车就过不去。
核心观点:HAL类的本质,就是把“画点、读触摸、刷屏幕”这些硬件操作,封装成TouchGFX能调用的标准接口。你不需要改TouchGFX内核,只需要重写HAL里的几个方法。
2.1 HAL类到底长什么样?
打开TouchGFX生成的工程,找到TouchGFXHAL.cpp。你会看到类似这样的结构:
class TouchGFXHAL : public touchgfx::HAL
{
public:
TouchGFXHAL(touchgfx::DMA_Interface& dma,
touchgfx::LCD& lcd,
touchgfx::TouchController& tc,
uint16_t width,
uint16_t height)
: touchgfx::HAL(dma, lcd, tc, width, height)
{
}
virtual void initialize();
virtual void flushFrameBuffer();
virtual uint16_t* getTFTFrameBuffer();
virtual void setTFTFrameBuffer(uint16_t* addr);
virtual void displayDriverTransmitBlock(uint8_t* pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h);
};
嗯,这里要注意:你真正需要重写的方法,其实只有三四个。其他的TouchGFX已经帮你兜底了。
2.2 必须重写的核心方法
我个人习惯把HAL的方法分成三类:帧缓冲管理、显示刷新、触摸输入。下面这张图能帮你快速建立整体认知:
2.3 逐个击破:方法详解与实战
2.3.1 initialize() —— 一上来先把家底摸清
这个方法在系统启动时只调用一次。你想想看,TouchGFX刚启动,它得知道:屏幕多大?颜色深度多少?帧缓冲在哪儿?
void TouchGFXHAL::initialize()
{
// 调用父类初始化
touchgfx::HAL::initialize();
// 设置帧缓冲地址 —— 我习惯放在SDRAM的固定区域
setFrameBufferStartAddress((uint16_t*)0xC0000000);
// 告诉引擎:我支持16位RGB565
setDisplayOrientation(touchgfx::ORIENTATION_LANDSCAPE);
// 如果你用双缓冲,这里要分配两个缓冲区
// 我曾经踩过坑:只配了一个缓冲区,结果刷新时画面撕裂
}
我的经验:帧缓冲地址一定要对齐到4字节边界。有些MCU的DMA对地址对齐有要求,不对齐直接死机。我遇到过一回,排查了整整两天才发现是地址没对齐。
2.3.2 flushFrameBuffer() —— 把画好的图推出去
TouchGFX每画完一帧,就会调用这个方法。它的任务很简单:把帧缓冲里的数据刷到LCD上。
void TouchGFXHAL::flushFrameBuffer()
{
// 如果你用的是MCU+LCD直连(比如FSMC接口)
// 其实什么都不用做,因为帧缓冲和LCD是共享内存的
// 但如果你用的是SPI接口的LCD,这里就要启动DMA传输
// 举个例子:通过SPI DMA发送整个帧缓冲
// HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)getTFTFrameBuffer(), FRAME_BUFFER_SIZE);
// 注意:一定要等DMA传输完成再返回
// 否则TouchGFX会以为刷完了,开始画下一帧,造成撕裂
}
避坑指南:我曾经在STM32F429上做项目,flushFrameBuffer里忘了等DMA传输完成标志位。结果画面一直闪烁,查了三天示波器才发现是DMA还没传完,TouchGFX已经把帧缓冲改了。解决方案:加一个DMA传输完成中断信号量,在flushFrameBuffer里等待。
2.3.3 getTFTFrameBuffer() 与 setTFTFrameBuffer() —— 帧缓冲的管家
这两个方法成对出现。TouchGFX通过getTFTFrameBuffer拿到当前要画的缓冲区地址,画完之后通过setTFTFrameBuffer告诉HAL:画完了,你可以刷了。
uint16_t* TouchGFXHAL::getTFTFrameBuffer()
{
// 返回当前正在使用的帧缓冲地址
// 如果是双缓冲,这里要返回“后台缓冲区”的地址
return (uint16_t*)0xC0000000;
}
void TouchGFXHAL::setTFTFrameBuffer(uint16_t* addr)
{
// 一般不需要额外操作
// 但如果你用双缓冲,这里要切换前台/后台缓冲区指针
// 我习惯在这里加一个断点,调试时能清楚看到帧切换时机
}
2.3.4 displayDriverTransmitBlock() —— 局部刷新的利器
这个方法不是必须重写的,但如果你想让性能更好,我建议你实现它。它的作用是:只刷新屏幕上变化的那一小块区域,而不是整屏重刷。
void TouchGFXHAL::displayDriverTransmitBlock(uint8_t* pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
// 设置LCD的窗口坐标
// 比如ILI9341的窗口设置命令
// LCD_SetWindow(x, y, x + w - 1, y + h - 1);
// 然后通过SPI或并行接口发送像素数据
// LCD_WriteData(pixels, w * h * 2); // 16位色深,每个像素2字节
}
性能对比:整屏刷新320×240的屏幕,RGB565格式,需要320×240×2=153.6KB数据。如果只刷新一个100×100的按钮,只需要100×100×2=20KB。局部刷新能省下将近87%的数据量!
2.4 触摸控制器的适配
HAL本身不直接处理触摸,它通过TouchController接口来获取触摸数据。你需要实现一个TouchController的子类:
class MyTouchController : public touchgfx::TouchController
{
public:
virtual void init()
{
// 初始化触摸芯片的I2C/SPI接口
// 我习惯在这里做一次触摸芯片的ID校验
uint8_t id = readTouchID();
if (id != EXPECTED_ID) {
// 打印错误,但不要死机
// 有些板子触摸芯片没焊好,但LCD还能显示
}
}
virtual bool sampleTouch(int32_t& x, int32_t& y)
{
// 读取触摸坐标
// 注意:坐标可能需要做旋转/镜像变换
// 因为LCD的物理方向和TouchGFX的逻辑方向可能不一致
uint16_t rawX, rawY;
if (readTouchPoint(&rawX, &rawY)) {
// 坐标变换:我的LCD是竖屏,但TouchGFX配的是横屏
x = rawY;
y = 319 - rawX; // 举个例子,具体看你的屏
return true;
}
return false;
}
};
小技巧:在sampleTouch里加一个静态变量做滤波。比如连续采样3次,取中间值。我遇到过触摸芯片偶尔跳点的情况,加了这个滤波之后稳得很。
2.5 常见问题与排查思路
| 现象 | 大概率原因 | 排查方法 |
|---|---|---|
| 屏幕全黑 | 帧缓冲地址不对,或LCD初始化失败 | 用调试器读帧缓冲内存,看是否有数据 |
| 画面闪烁 | flushFrameBuffer没等DMA完成 | 加信号量同步,或改用阻塞传输 |
| 触摸没反应 | 坐标变换错误,或I2C通信异常 | 先打印原始坐标,确认触摸芯片能读到数据 |
| 颜色显示不对 | 颜色深度配置错误(RGB565 vs RGB888) | 检查HAL初始化时的颜色格式设置 |
| 画面撕裂 | 双缓冲没配好,或刷新和渲染冲突 | 开启双缓冲,并在VSync中断里切换缓冲区 |
2.6 我的HAL适配清单
每次做新板子的HAL适配,我都会按这个清单来检查,你可以直接拿去用:
- 帧缓冲:分配了足够的内存吗?地址对齐了吗?
- LCD初始化:时序对吗?复位时序够长吗?
- flushFrameBuffer:是整屏刷还是局部刷?DMA同步了吗?
- 触摸:坐标变换对吗?滤波加了吗?
- 性能:局部刷新实现了吗?帧率能到多少?
最后提醒一句:HAL适配最怕“我以为没问题”。我每次都会在initialize()里加一个彩条测试——画几条红绿蓝的竖条,确认颜色和位置都对,再跑TouchGFX的Demo。这一步能帮你过滤掉80%的硬件适配问题。