3、输入设备移植:触摸屏驱动对接、按键与编码器适配
输入设备移植,说白了就是让 LVGL 能「听懂」你的硬件在说什么。触摸屏、按键、旋转编码器,每种设备的脾气都不一样。我刚开始做移植时,以为只要把坐标传进去就行,结果触摸屏要么乱跳,要么点不动。后来踩了不少坑,才摸清楚这里面的门道。
核心原则:LVGL 不关心你的硬件怎么采集数据,它只关心你通过 indev_read_callback 喂给它的数据是否规范、及时。
3.1 触摸屏驱动对接
触摸屏是最常见的输入设备。对接时,你需要实现一个回调函数,LVGL 会定期调用它来获取触摸状态。
3.1.1 基础对接流程
- 初始化触摸芯片:在
lv_indev_drv_register之前,确保 I2C/SPI 通信正常。 - 实现回调:函数签名固定为
void my_touchpad_read(lv_indev_drv_t *drv, lv_indev_data_t *data)。 - 填充数据:设置
data->point.x、data->point.y和data->state。
static void touchpad_read(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
static lv_coord_t last_x = 0;
static lv_coord_t last_y = 0;
bool touched;
lv_coord_t x, y;
// 调用硬件驱动读取坐标
touched = ft5x06_read_point(&x, &y);
if (touched) {
data->point.x = x;
data->point.y = y;
data->state = LV_INDEV_STATE_PR;
last_x = x;
last_y = y;
} else {
data->point.x = last_x;
data->point.y = last_y;
data->state = LV_INDEV_STATE_REL;
}
}
void touchpad_init(void)
{
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;
lv_indev_drv_register(&indev_drv);
}
我的习惯:在 touched 为 false 时,保留上一次的坐标。这样 LVGL 在释放状态时还能知道手指最后的位置,避免界面元素「跳变」。
3.1.2 坐标校准与旋转
很多触摸屏的物理坐标和屏幕显示方向不一致。我遇到过一款 480x272 的屏,触摸芯片输出的 X 范围是 0~4095,Y 是 0~4095,但实际有效区域只有中间一部分。这时候需要做映射。
// 线性映射示例
data->point.x = (x - x_min) * LV_HOR_RES / (x_max - x_min);
data->point.y = (y - y_min) * LV_VER_RES / (y_max - y_min);
如果屏幕旋转了 90 度,记得交换 X/Y 并反转方向:
// 旋转 90 度
data->point.x = LV_VER_RES - y;
data->point.y = x;
我曾经踩过的坑:坐标映射时用了整数除法,导致触摸点永远到不了屏幕边缘。记得用浮点运算或者先乘后除,避免精度丢失。
3.2 按键与编码器适配
不是所有设备都有触摸屏。按键和编码器在工业设备、家电中非常常见。LVGL 对这两种设备有专门的支持。
3.2.1 按键适配
按键通常映射为 LV_INDEV_TYPE_KEYPAD。你需要告诉 LVGL 哪个按键对应哪个 LVGL 标准键值。
static void keypad_read(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
static uint32_t last_key = 0;
uint32_t key = read_key_gpio(); // 读取按键 GPIO
if (key != KEY_NONE) {
data->key = key;
data->state = LV_INDEV_STATE_PR;
last_key = key;
} else {
data->key = last_key;
data->state = LV_INDEV_STATE_REL;
}
}
常用的键值映射关系:
| 物理按键 | LVGL 键值 | 说明 |
|---|---|---|
| 上 | LV_KEY_UP |
焦点上移 |
| 下 | LV_KEY_DOWN |
焦点下移 |
| 左 | LV_KEY_LEFT |
焦点左移 |
| 右 | LV_KEY_RIGHT |
焦点右移 |
| 确认 | LV_KEY_ENTER |
触发点击 |
| 返回 | LV_KEY_ESC |
返回/取消 |
3.2.2 编码器适配
旋转编码器是旋钮类设备的首选。它有两个信号:旋转方向和按键按下。LVGL 用 LV_INDEV_TYPE_ENCODER 来处理。
static void encoder_read(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
static int16_t last_diff = 0;
int16_t diff = encoder_get_diff(); // 获取旋转差值,正为顺时针,负为逆时针
bool btn = encoder_get_btn(); // 获取按键状态
data->enc_diff = diff;
data->state = btn ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;
last_diff = diff;
}
我个人建议:编码器的 enc_diff 不要累积太大值。LVGL 每次读取后会自动清零,你只需要返回本次周期内的变化量即可。如果硬件有抖动,可以在驱动层做简单的消抖滤波。
3.3 indev_read_callback 实现要点
这个回调是 LVGL 输入子系统的核心。LVGL 会在 lv_task_handler 中周期调用它,频率通常和屏幕刷新率一致(比如 30fps 就是每 33ms 调用一次)。
3.3.1 回调的职责
- 快速返回:回调里不要做耗时操作,比如延时等待触摸芯片应答。LVGL 是单线程的,你卡住了它,整个 UI 就卡住了。
- 只填充数据:不要在这里处理手势、双击等逻辑,那是 LVGL 上层的事。
- 状态机清晰:按下和释放状态要明确,不要出现「既按下又释放」的歧义。
3.3.2 性能优化技巧
| 场景 | 做法 | 原因 |
|---|---|---|
| 触摸屏数据读取 | 使用 DMA + 中断,回调只读缓存 | 避免 SPI/I2C 同步等待 |
| 按键扫描 | 定时器中断中扫描,回调只取最新值 | 减少回调执行时间 |
| 编码器计数 | 硬件定时器捕获,回调直接读寄存器 | 软件计数容易丢步 |
我曾经犯过的错:在回调里用 printf 打印调试信息,结果 LVGL 界面卡成幻灯片。回调里绝对不要做任何 I/O 操作,尤其是串口打印。
3.4 多点触控支持
LVGL 从 v7 开始支持多点触控,但默认只处理第一个触摸点。要开启多点触控,需要做两件事:
- 使能宏:在
lv_conf.h中设置LV_INDEV_DEF_POINT_NUM为需要的点数(比如 5)。 - 修改回调:使用
lv_indev_data_t的points数组。
static void mt_touchpad_read(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
uint8_t touch_cnt = ft5x06_read_all_points(data->points);
if (touch_cnt > 0) {
data->state = LV_INDEV_STATE_PR;
data->continue_read = (touch_cnt > 1); // 告诉 LVGL 还有更多点
} else {
data->state = LV_INDEV_STATE_REL;
data->continue_read = false;
}
}
注意:continue_read 这个字段很关键。如果触摸芯片一次只能读一个点,你需要设置它为 true,LVGL 会再次调用回调读取下一个点,直到你返回 false。我刚开始没设这个,结果多点触控永远只识别到一个手指。
多点触控的应用场景主要是双指缩放、旋转等手势。LVGL 内部有手势识别模块,你只需要把原始点数据喂给它就行。
嗯,到这里输入设备移植的核心内容就差不多了。触摸屏、按键、编码器,三种设备各有各的脾气,但归根结底都是通过 indev_read_callback 这个接口和 LVGL 对话。你只要把数据喂对了,剩下的手势识别、焦点切换、滚动等高级功能,LVGL 自己会搞定。