2. LVGL基础回顾

好,咱们直接进入正题。上一章聊了触摸校准到底解决什么问题,这一章我们得把 LVGL 的输入设备驱动框架彻底捋一遍。说白了,你要让触摸屏正常工作,就得先搞清楚 LVGL 是怎么“接”输入设备的。

我个人习惯把 LVGL 的输入设备驱动想象成一个“适配层”。硬件那边可能是 SPI、I2C 或者 USB 接口的触摸芯片,LVGL 这边只认一个东西——lv_indev_drv_t 结构体。你只要把这个结构体填对,注册好,LVGL 就能从你的硬件拿到数据。

2.1 lv_indev_drv_t 结构体:驱动的心脏

这个结构体定义在 lv_indev.h 里。我刚开始用 LVGL 的时候,第一件事就是去翻这个结构体的成员。你想想看,它决定了你的触摸屏、鼠标、键盘甚至编码器怎么跟 LVGL 交互。

核心成员我列在下面这张表里,建议你收藏一下,写驱动时对照着看:

成员名 类型 说明
type lv_indev_type_t 设备类型:触摸屏、鼠标、键盘、编码器、按钮
read_cb void (*)(lv_indev_drv_t *, lv_indev_data_t *) 读取数据的回调函数,LVGL 会周期性调用它
feedback_cb void (*)(lv_indev_drv_t *, uint8_t) 反馈回调,比如按键按下时震动或发声(可选)
user_data void * 用户自定义数据指针,想传什么传什么
disp lv_disp_t * 关联的显示器,触摸屏必须绑定到某个显示器
read_timer lv_timer_t * 内部使用的定时器,不用你管

重点提醒:type 和 read_cb 是必填项,其他成员可以保持默认。我见过不少新手把 disp 忘了赋值,结果触摸坐标死活不对——因为 LVGL 不知道你的触摸屏对应哪个屏幕。

2.2 设备类型配置:选对类型,事半功倍

lv_indev_type_t 枚举定义了五种设备类型。我按使用频率排个序:

  1. LV_INDEV_TYPE_POINTER —— 指针设备,触摸屏、鼠标都归它。坐标是绝对定位。
  2. LV_INDEV_TYPE_KEYPAD —— 键盘或矩阵按键。用 LV_KEY_* 键值。
  3. LV_INDEV_TYPE_ENCODER —— 旋转编码器。可以左右旋转和按下确认。
  4. LV_INDEV_TYPE_BUTTON —— 外部物理按键。每个按键绑定屏幕上的一个区域。

嗯,这里要注意:触摸屏必须选 LV_INDEV_TYPE_POINTER。我遇到过有人把触摸屏配成 KEYPAD 类型,结果点击事件死活触发不了——因为 LVGL 对 POINTER 类型的事件处理逻辑完全不同。

2.3 read_cb 回调函数:数据从硬件到 LVGL 的桥梁

这个回调是驱动里最核心的部分。LVGL 内部会用一个定时器(默认每 5 毫秒)调用你的 read_cb,你需要在里面完成两件事:

  1. 从硬件读取原始数据(比如触摸坐标、按键状态)
  2. 把数据填充到 lv_indev_data_t 结构体中

lv_indev_data_t 的结构也很简单:

typedef struct {
  lv_point_t point;    // 坐标点,触摸屏用
  uint32_t key;        // 按键值,键盘/编码器用
  uint32_t btn_id;     // 按钮 ID,按钮设备用
  uint8_t enc_diff;    // 编码器差值
  lv_indev_state_t state; // 状态:LV_INDEV_STATE_PRESSED 或 RELEASED
  bool continue_reading;  // 是否继续读取(高级用法)
} lv_indev_data_t;

写回调的时候,我个人习惯先清空结构体,再逐字段赋值。来看一个实际的触摸屏驱动示例:

static void touchpad_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
  // 清零,避免残留数据
  lv_memset_00(data, sizeof(lv_indev_data_t));

  // 读取触摸芯片(以 FT6x36 为例)
  uint8_t touch_points = 0;
  uint16_t x = 0, y = 0;

  // 假设这是你的硬件读取函数
  if (ft6x36_read_touch(&touch_points, &x, &y) == 0) {
    if (touch_points > 0) {
      data->point.x = x;
      data->point.y = y;
      data->state = LV_INDEV_STATE_PRESSED;
    } else {
      data->state = LV_INDEV_STATE_RELEASED;
    }
  } else {
    // 读取失败,释放状态
    data->state = LV_INDEV_STATE_RELEASED;
  }
}

小技巧:如果你的触摸芯片支持多点触控,continue_reading 字段可以设为 true,LVGL 会立即再次调用你的回调,直到你把它设为 false。这样能实现多点触摸的轮询。

2.4 完整注册流程:三步走

把上面这些串起来,注册一个触摸屏驱动只需要三步。我每次写新项目都是这个套路:

// 第一步:定义并初始化驱动结构体
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);

// 第二步:配置类型和回调
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = touchpad_read_cb;
indev_drv.disp = lv_disp_get_default();  // 绑定默认显示器

// 第三步:注册驱动,返回设备句柄
lv_indev_t *my_indev = lv_indev_drv_register(&indev_drv);

你看,就这么简单。但简单归简单,坑也不少。我曾经在一个项目里忘了调用 lv_indev_drv_init(),直接给结构体成员赋值。结果 read_timer 指针是野的,系统跑几分钟就死机。嗯,从那以后我再也不敢跳过初始化函数了。

2.5 知识体系总览

为了让你对整个框架有个直观印象,我画了一张图。它把 lv_indev_drv_t、lv_indev_data_t、回调函数和硬件之间的关系串起来了:

LVGL 输入设备驱动框架 硬件设备 触摸芯片 / 鼠标 / 键盘 驱动回调 read_cb() LVGL 核心 事件分发 / 对象处理 读取 填充 data lv_indev_drv_t • type:设备类型枚举 • read_cb:数据读取回调 • disp:关联显示器 • user_data:用户自定义数据 lv_indev_data_t • point:触摸坐标 (x, y) • state:按下 / 释放 • key / btn_id / enc_diff • continue_reading:多点支持 注册流程(三步) ① lv_indev_drv_init() → ② 配置 type / read_cb / disp ③ lv_indev_drv_register() → 返回 lv_indev_t 句柄 提示:read_cb 由 LVGL 内部定时器周期性调用,无需手动轮询

避坑指南:我曾经在一个项目里同时注册了两个 POINTER 类型的输入设备,结果 LVGL 把两个设备的坐标混在一起处理,触摸完全乱套。如果你确实需要多个触摸屏,记得用 lv_indev_set_group() 把它们分到不同的组里。