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 枚举定义了五种设备类型。我按使用频率排个序:
- LV_INDEV_TYPE_POINTER —— 指针设备,触摸屏、鼠标都归它。坐标是绝对定位。
- LV_INDEV_TYPE_KEYPAD —— 键盘或矩阵按键。用
LV_KEY_*键值。 - LV_INDEV_TYPE_ENCODER —— 旋转编码器。可以左右旋转和按下确认。
- LV_INDEV_TYPE_BUTTON —— 外部物理按键。每个按键绑定屏幕上的一个区域。
嗯,这里要注意:触摸屏必须选 LV_INDEV_TYPE_POINTER。我遇到过有人把触摸屏配成 KEYPAD 类型,结果点击事件死活触发不了——因为 LVGL 对 POINTER 类型的事件处理逻辑完全不同。
2.3 read_cb 回调函数:数据从硬件到 LVGL 的桥梁
这个回调是驱动里最核心的部分。LVGL 内部会用一个定时器(默认每 5 毫秒)调用你的 read_cb,你需要在里面完成两件事:
- 从硬件读取原始数据(比如触摸坐标、按键状态)
- 把数据填充到
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、回调函数和硬件之间的关系串起来了:
避坑指南:我曾经在一个项目里同时注册了两个 POINTER 类型的输入设备,结果 LVGL 把两个设备的坐标混在一起处理,触摸完全乱套。如果你确实需要多个触摸屏,记得用 lv_indev_set_group() 把它们分到不同的组里。