2、硬件抽象层(HAL)基础:HAL层的作用、显示驱动接口定义、触摸驱动接口定义、定时器与心跳配置。

好,咱们进入第二章。这一章要聊的是整个移植工作的地基——硬件抽象层,也就是 HAL 层。

很多刚入行的朋友,一上来就对着芯片手册写驱动,恨不得把每个寄存器都摸透。我个人习惯是,先搭好 HAL 层这堵“防火墙”。为什么?因为 GUI 框架不应该直接跟硬件寄存器打交道。你想想看,今天你用 STM32,明天项目换成了 GD32,后天又换成瑞萨,如果代码里到处都是寄存器操作,那移植起来简直是一场噩梦。

2.1 HAL 层的作用:给 GUI 一个“标准插座”

HAL 层说白了,就是一套标准接口。它把底层的 LCD 控制器、触摸控制器、定时器这些硬件,包装成 GUI 框架能理解的函数。

它的核心价值就三点:

  • 隔离变化:硬件换了,只改 HAL 层,上层 GUI 代码纹丝不动。
  • 简化开发:写 GUI 应用的人,不需要知道你的像素数据是通过 SPI 还是 RGB 接口刷上去的。
  • 方便测试:你可以写一个模拟的 HAL 层,在 PC 上就跑起来,不用等硬件板子。

记住这个原则:HAL 层是 GUI 对硬件的唯一依赖点。你的目标是把所有硬件相关的代码,都关在这个“笼子”里。

我曾经在一个项目里,接手别人的代码,发现 GUI 的绘图函数里直接操作了 LCD 控制器的寄存器。结果换了个屏幕型号,整个绘图模块都得重写。嗯,从那以后,我定了个规矩:任何硬件寄存器操作,都不允许出现在 HAL 层之外。

2.2 显示驱动接口定义:把像素“推”到屏幕上

显示驱动是 HAL 层里最核心的部分。GUI 框架需要知道怎么把一块内存里的颜色数据,送到屏幕上。

通常,我们需要定义以下几个接口:

接口函数 功能描述 参数说明
LCD_Init() 初始化 LCD 控制器和屏幕 无参数,或传入配置结构体指针
LCD_DrawPoint() 在指定坐标画一个像素点 x, y 坐标,颜色值
LCD_Fill() 填充一个矩形区域 起始/结束坐标,颜色值
LCD_Flush() 将显存数据刷新到屏幕 区域坐标,数据指针

这里我特别想强调一下 LCD_Flush()。很多轻量级 GUI 用的是“帧缓冲”机制,也就是在内存里开一块显存,画图都在显存里操作,最后一次性刷到屏幕。这个接口的效率,直接决定了 GUI 的流畅度。

来看一个典型的接口定义示例:

/* 显示驱动接口结构体 */
typedef struct {
    void (*Init)(void);
    void (*DrawPoint)(uint16_t x, uint16_t y, uint32_t color);
    void (*Fill)(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint32_t color);
    void (*Flush)(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint32_t *pixels);
} DisplayDriver_t;

我的小技巧:接口参数尽量用 uint16_t 和 uint32_t,别用 int。因为嵌入式平台 int 的长度可能不一样,用标准长度类型更安全。

2.3 触摸驱动接口定义:捕捉用户的每一次触碰

触摸驱动比显示驱动稍微复杂一点,因为它涉及到事件处理。你不能只报告“现在手指在哪”,还得告诉 GUI “手指是按下了、移动了、还是抬起来了”。

我建议把触摸事件抽象成三种状态:

  • 按下(Press):手指接触屏幕
  • 移动(Move):手指在屏幕上滑动
  • 释放(Release):手指离开屏幕

接口定义可以这样:

/* 触摸事件类型 */
typedef enum {
    TOUCH_PRESS,
    TOUCH_MOVE,
    TOUCH_RELEASE
} TouchEvent_t;

/* 触摸点数据结构 */
typedef struct {
    TouchEvent_t event;
    uint16_t x;
    uint16_t y;
    uint8_t  pressure;  /* 压力值,可选 */
} TouchPoint_t;

/* 触摸驱动接口 */
typedef struct {
    void (*Init)(void);
    uint8_t (*GetPoint)(TouchPoint_t *point);  /* 返回 1 表示有触摸事件 */
} TouchDriver_t;

这里有个坑,我曾经踩过。有些触摸芯片在手指离开时,会报告一个无效坐标。如果你不做过滤,GUI 可能会在手指抬起时,误以为点击到了某个奇怪的位置。所以,GetPoint 函数内部一定要做有效性检查。

注意:触摸坐标通常需要做“校准”。因为触摸屏的物理坐标和 LCD 的显示坐标,不一定是一一对应的。你需要在 HAL 层里做一次线性变换,把触摸芯片的原始值,映射到屏幕分辨率上。

2.4 定时器与心跳配置:让 GUI “活”起来

GUI 不是静态的。它需要定时刷新、处理动画、检测触摸超时。这一切,都依赖一个稳定的“心跳”。

这个心跳通常是一个周期性的定时器中断,频率一般在 10ms 到 100ms 之间。我习惯用 20ms,也就是 50Hz。这个频率对人眼来说足够平滑,又不会给 CPU 带来太大负担。

HAL 层需要提供两个基础服务:

  1. 获取系统滴答:返回一个递增的毫秒计数器。
  2. 注册心跳回调:让 GUI 框架能在每个定时周期里执行自己的处理函数。

代码实现通常是这样:

/* 全局系统滴答计数器 */
static volatile uint32_t sys_tick_ms = 0;

/* 定时器中断服务函数(假设 1ms 触发一次) */
void SysTick_Handler(void) {
    sys_tick_ms++;
}

/* HAL 接口:获取当前毫秒数 */
uint32_t HAL_GetTick(void) {
    return sys_tick_ms;
}

/* HAL 接口:毫秒级延时 */
void HAL_Delay(uint32_t ms) {
    uint32_t start = sys_tick_ms;
    while ((sys_tick_ms - start) < ms);
}

关键点:HAL_GetTick() 必须是一个原子操作。如果你的平台是 32 位的,读取 uint32_t 通常是原子的。但如果是 8 位单片机,读取 32 位变量可能需要多条指令,这时候要关中断保护一下。

GUI 框架的“心跳”函数,一般会在一个高优先级的定时器中断里调用,或者放在主循环里轮询。我个人建议放在定时器中断里,这样响应更及时,动画也更流畅。

2.5 本章知识体系总览

说了这么多,咱们用一张图把 HAL 层的结构理清楚。下面这张 SVG 图,展示了 GUI、HAL 和硬件三者之间的关系。

HAL 层架构与数据流 GUI 框架(上层应用) 硬件抽象层(HAL) 显示驱动接口 触摸驱动接口 定时器/心跳 LCD 控制器 / 屏幕 触摸芯片 / 控制器 硬件定时器 调用标准接口 操作寄存器 I2C/SPI 读取 配置中断 说明:HAL 层是 GUI 与硬件之间的“翻译官”。 更换硬件平台时,只需重写 HAL 层的三个模块,GUI 代码无需改动。

从这张图可以看得很清楚:GUI 框架只跟 HAL 层打交道,HAL 层再跟具体的硬件打交道。这种分层设计,就是移植性的核心。

好了,这一章的内容就到这里。HAL 层虽然看起来简单,但它是整个移植工作的基石。你花在 HAL 层设计上的每一分钟,都会在后续的移植工作中得到回报。