硬件接口抽象层(HAL):深入理解HAL类的职责

上一章我们搭好了开发环境,也跑通了第一个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的方法分成三类:帧缓冲管理、显示刷新、触摸输入。下面这张图能帮你快速建立整体认知:

HAL 核心职责与数据流 TouchGFX 图形引擎 HAL 硬件抽象层 initialize() | flushFrameBuffer() | getTFTFrameBuffer() 帧缓冲管理 getTFTFrameBuffer() setTFTFrameBuffer() 显示刷新 flushFrameBuffer() displayDriverTransmitBlock() 触摸输入 TouchController sampleTouch() SDRAM / SRAM LCD 驱动器 触摸控制器 说明:HAL 不直接操作寄存器,而是调用你封装的底层驱动函数。 你只需要把“怎么刷屏”“怎么读触摸”告诉 HAL,剩下的交给 TouchGFX。

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适配,我都会按这个清单来检查,你可以直接拿去用:

  1. 帧缓冲:分配了足够的内存吗?地址对齐了吗?
  2. LCD初始化:时序对吗?复位时序够长吗?
  3. flushFrameBuffer:是整屏刷还是局部刷?DMA同步了吗?
  4. 触摸:坐标变换对吗?滤波加了吗?
  5. 性能:局部刷新实现了吗?帧率能到多少?

最后提醒一句:HAL适配最怕“我以为没问题”。我每次都会在initialize()里加一个彩条测试——画几条红绿蓝的竖条,确认颜色和位置都对,再跑TouchGFX的Demo。这一步能帮你过滤掉80%的硬件适配问题。