3、输入设备驱动移植:触摸屏与物理按键
输入设备,说白了就是让用户和屏幕交互的桥梁。没有它,你的仪表盘再漂亮也只是个摆设。
这一章我们重点讲触摸屏和物理按键的驱动移植。我会带着你一步步写 lv_port_indev.c,把坐标校准、事件上报这些坑都踩一遍。最后,我会分享一个我在项目中遇到的触摸抖动问题——嗯,那会儿差点把我整崩溃。
3.1 输入设备在LVGL中的角色
LVGL把输入设备抽象成了几个类型:触摸屏(指针设备)、物理按键、编码器、键盘等。每个类型对应一个 lv_indev_drv_t 结构体。
我个人习惯把输入设备驱动单独放在 lv_port_indev.c 和 lv_port_indev.h 里。这样移植起来清爽,后期维护也方便。
核心流程其实就三步:
- 初始化硬件(触摸IC、GPIO等)
- 注册读取函数(告诉LVGL怎么拿数据)
- 在LVGL的tick循环里调用读取函数
你想想看,LVGL本身不关心你的触摸屏是I2C还是SPI,它只认一个回调函数——你给我坐标,我就处理。
3.2 编写lv_port_indev.c
我们先从触摸屏开始。假设你用的是FT5x06电容触摸屏,I2C接口。
这是最基本的框架:
#include "lv_port_indev.h"
#include "lvgl.h"
static lv_indev_drv_t indev_drv;
static lv_indev_t *indev_touchpad;
// 触摸屏读取函数
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;
if (touchpad_is_pressed()) {
touchpad_get_xy(&last_x, &last_y);
data->point.x = last_x;
data->point.y = last_y;
data->state = LV_INDEV_STATE_PR;
} else {
data->point.x = last_x;
data->point.y = last_y;
data->state = LV_INDEV_STATE_REL;
}
}
void lv_port_indev_init(void)
{
// 初始化触摸硬件
touchpad_init();
// 注册输入设备
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = touchpad_read;
indev_touchpad = lv_indev_drv_register(&indev_drv);
}
这里有个细节:data->point.x 和 data->point.y 在释放状态时,最好保留上一次的坐标。为什么?因为LVGL内部有些手势识别需要知道最后的触摸位置。我刚开始没注意这个,结果滑动事件一直不触发,查了半天才发现是坐标清零了。
3.3 物理按键的驱动方式
物理按键在仪表盘上也很常见,比如菜单键、确认键、返回键。LVGL支持将按键映射成编码器或者键盘类型。
我个人更推荐用 LV_INDEV_TYPE_KEYPAD,因为可以配合 lv_group 做焦点切换。
static uint32_t keypad_get_key(void)
{
if (HAL_GPIO_ReadPin(KEY_UP_GPIO_Port, KEY_UP_Pin) == GPIO_PIN_RESET) {
return LV_KEY_UP;
}
if (HAL_GPIO_ReadPin(KEY_DOWN_GPIO_Port, KEY_DOWN_Pin) == GPIO_PIN_RESET) {
return LV_KEY_DOWN;
}
if (HAL_GPIO_ReadPin(KEY_ENTER_GPIO_Port, KEY_ENTER_Pin) == GPIO_PIN_RESET) {
return LV_KEY_ENTER;
}
return 0;
}
static void keypad_read(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
static uint32_t last_key = 0;
uint32_t key = keypad_get_key();
if (key != 0) {
data->key = key;
data->state = LV_INDEV_STATE_PR;
last_key = key;
} else {
data->key = last_key;
data->state = LV_INDEV_STATE_REL;
}
}
嗯,这里要注意:按键的消抖最好在硬件层或者驱动层做掉,不要丢给LVGL。LVGL只关心「按下」和「释放」两个状态。
3.4 坐标校准与事件上报
触摸屏的坐标校准,说白了就是把ADC原始值映射到屏幕分辨率上。比如你的屏幕是480x272,但触摸IC返回的坐标范围是0~4095。
最简单的做法是线性映射:
static void touchpad_get_xy(lv_coord_t *x, lv_coord_t *y)
{
uint16_t raw_x, raw_y;
touchpad_read_raw(&raw_x, &raw_y);
// 线性映射到屏幕分辨率
*x = (lv_coord_t)((uint32_t)raw_x * 480 / 4096);
*y = (lv_coord_t)((uint32_t)raw_y * 272 / 4096);
}
但实际项目中,触摸屏的物理坐标和屏幕坐标往往有偏移和旋转。这时候就需要做三点校准或者五点校准。
我在项目中用过一种简单粗暴的方法:在屏幕上画几个十字标记,点击后记录原始坐标,然后计算仿射变换矩阵。代码量不大,但效果很好。
3.5 触摸抖动问题——我踩过的坑
终于到了这一节的重点。触摸抖动,说白了就是手指明明没动,但上报的坐标在几个像素之间来回跳。
为什么会这样?
- 触摸IC的ADC噪声
- 电源纹波干扰
- 触摸屏表面有水滴或污渍
- 手指轻微颤抖(尤其是长时间触摸时)
我曾经在一个车载项目中遇到过这个问题。仪表盘上有个滑动条,用户想拖动它,结果因为坐标抖动,滑块像抽风一样来回晃。客户当场就炸了。
我当时的解决方案是「滑动平均滤波 + 死区判断」:
#define FILTER_WINDOW 4
#define DEAD_ZONE 3
static lv_coord_t filter_buf_x[FILTER_WINDOW];
static lv_coord_t filter_buf_y[FILTER_WINDOW];
static uint8_t filter_index = 0;
static void touchpad_filter(lv_coord_t *x, lv_coord_t *y)
{
filter_buf_x[filter_index] = *x;
filter_buf_y[filter_index] = *y;
filter_index = (filter_index + 1) % FILTER_WINDOW;
uint32_t sum_x = 0, sum_y = 0;
for (int i = 0; i < FILTER_WINDOW; i++) {
sum_x += filter_buf_x[i];
sum_y += filter_buf_y[i];
}
lv_coord_t avg_x = sum_x / FILTER_WINDOW;
lv_coord_t avg_y = sum_y / FILTER_WINDOW;
// 死区判断:如果滤波后的坐标和上次相差小于3个像素,就不更新
static lv_coord_t last_x = 0, last_y = 0;
if (abs(avg_x - last_x) > DEAD_ZONE || abs(avg_y - last_y) > DEAD_ZONE) {
*x = avg_x;
*y = avg_y;
last_x = avg_x;
last_y = avg_y;
} else {
*x = last_x;
*y = last_y;
}
}
这个方案把抖动从±5像素降到了±1像素以内。滑动条终于不抽风了。
另外,我还发现一个容易被忽略的问题:触摸中断的触发方式。有些触摸IC支持「按下时连续中断」,但如果你在中断里直接读取坐标,可能会读到不稳定的值。我后来改成在定时器里轮询读取,反而更稳定。
3.6 事件上报的时机
LVGL的事件上报,其实是在 lv_task_handler() 里触发的。它会调用你注册的 read_cb 函数,然后根据返回的 state 和 point 生成事件。
这里有个性能相关的点:如果你的 read_cb 函数执行时间太长(比如I2C通信有延迟),会拖慢整个LVGL的刷新率。
我建议的做法是:
- 在
read_cb里只做简单的状态判断和坐标读取 - 把复杂的滤波算法放在后台任务里,更新一个全局变量
read_cb直接拿这个全局变量返回
说白了,就是「生产者和消费者」模式。触摸中断负责生产数据,LVGL的读取函数负责消费数据。中间用队列或者全局变量缓冲一下。
3.7 本章小结
这一章我们讲了输入设备驱动的核心要点:
lv_port_indev.c的编写框架- 触摸屏和物理按键的驱动方式
- 坐标校准的线性映射方法
- 触摸抖动问题的实战解决方案
- 事件上报的性能优化思路
下一章我们会进入LVGL的性能优化部分。说实话,输入设备驱动只是开胃菜,真正的硬骨头还在后面——内存管理、帧率优化、DMA加速……到时候我们一个一个啃。
嗯,今天就到这里。如果你在移植过程中遇到什么奇怪的问题,欢迎在课程群里交流。我踩过的坑,说不定你也会遇到。