4. 触摸驱动框架:输入子系统、触摸屏协议、校准算法、实现触摸回调

好,到了这一章,我们终于要跟触摸屏打交道了。说实话,在嵌入式UI开发里,触摸驱动这块是最容易出幺蛾子的地方。我见过不少项目,LVGL界面画得漂漂亮亮,结果触摸一上去就乱跳,或者点不准,最后排查半天发现是驱动层的问题。

这一章,我会带你走一遍完整的触摸驱动框架。从Linux输入子系统讲起,到触摸屏协议、校准算法,最后实现LVGL的触摸回调。嗯,咱们一步步来。

4.1 Linux输入子系统:触摸事件的“高速公路”

先说说输入子系统。你想想看,一个嵌入式设备上可能有按键、触摸屏、鼠标、遥控器……如果每个外设都自己搞一套接口,那应用层得疯掉。Linux内核的做法是:统一抽象成输入子系统。

说白了,输入子系统就是一条“高速公路”。所有输入设备都在这条路上跑,应用层只管从路口收数据就行。

核心概念:

  • 输入设备节点:触摸屏注册后会在 /dev/input/ 下生成一个节点,比如 event0、event1
  • 事件结构体:每次触摸都打包成一个 struct input_event,包含时间戳、类型、编码、值
  • 事件类型:触摸相关的主要是 EV_ABS(绝对坐标)和 EV_KEY(按键,比如触摸按下/抬起)

我在项目里遇到过一个问题:明明触摸屏能上报数据,但LVGL就是没反应。后来发现是读取 input_event 时没处理好 EV_SYN 同步事件。内核发完一组坐标后,会发一个 EV_SYN 告诉你“这组数据完整了”。如果你不等同步信号就直接处理,拿到的坐标可能是半截的。

4.2 触摸屏协议:你的屏用哪种“语言”?

触摸屏上报坐标的方式,主要有两种协议。我建议你拿到一块新屏时,先搞清楚它用的是哪种。

协议类型 特点 典型场景
单点协议 (Single Touch) 一次只上报一个触摸点,坐标通过 ABS_X / ABS_Y 上报 老式电阻屏、简单电容屏
多点协议 (MT Protocol B) 支持多点触摸,每个触摸点有独立的 ABS_MT_POSITION_X / ABS_MT_POSITION_Y,通过 SLOT 区分 现代电容屏、手势识别

我个人习惯优先用 MT Protocol B。为什么?因为现在市面上的触摸IC,十有八九都支持这个协议。而且LVGL的多点手势(缩放、旋转)必须依赖多点协议。

小技巧: 如果你不确定触摸屏用的是什么协议,可以在板子上跑一下 hexdump /dev/input/event0,然后点一下屏幕。如果看到 ABS_MT_SLOT 和 ABS_MT_POSITION_X,那就是MT协议 B。如果只有 ABS_X,那就是单点协议。

4.3 校准算法:为什么点不准?

你有没有遇到过这种情况:屏幕显示一个按钮在左上角,你点下去,结果LVGL认为你点的是右下角?

这通常是因为触摸屏的物理坐标和LCD的显示坐标没有对齐。说白了,触摸屏和LCD是两个独立的硬件,它们的坐标系可能方向相反、原点不同,甚至是非线性的。

校准算法就是用来解决这个问题的。最常见的校准方法是三点校准法。

三点校准原理:

  1. 在LCD上显示3个已知坐标的点(比如左上、右上、右下)
  2. 用户依次点击这3个点,记录触摸屏上报的物理坐标
  3. 通过线性变换公式,计算出转换矩阵
  4. 后续每次触摸,都用这个矩阵把物理坐标映射到LCD坐标

我曾经在一个7寸屏上踩过坑。那个屏的触摸IC本身带校准功能,但出厂参数是错的。我一开始没做软件校准,结果触摸总是偏了大概10个像素。后来加了三点校准,问题就解决了。

校准公式其实不复杂:

// 假设触摸物理坐标为 (tx, ty),LCD坐标为 (lx, ly)
// 校准参数为 a, b, c, d, e, f
lx = a * tx + b * ty + c;
ly = d * tx + e * ty + f;

这6个参数通过3个点的已知坐标和物理坐标,解线性方程组就能算出来。嗯,数学上叫“仿射变换”。

注意: 如果你的触摸屏和LCD分辨率完全一致,且硬件安装没有偏移,可以跳过校准。但我在实际项目中,99%的情况都需要校准。别偷懒,加上吧。

4.4 实现触摸回调:让LVGL“感知”触摸

好了,前面都是铺垫。现在我们来干正事——实现LVGL的触摸回调。

LVGL的触摸输入,是通过一个 lv_indev_drv_t 结构体来注册的。你需要实现一个读取函数,LVGL会周期性地调用它,获取触摸状态。

先看代码框架:

// 1. 定义触摸状态结构
static lv_point_t last_point;
static lv_indev_state_t last_state = LV_INDEV_STATE_REL;

// 2. 实现触摸读取回调
static void touchpad_read(lv_indev_drv_t * drv, lv_indev_data_t * data)
{
    // 从触摸驱动获取原始坐标
    int raw_x, raw_y;
    int pressed = get_touch_point(&raw_x, &raw_y);

    // 应用校准算法
    int cal_x = calibrate_x(raw_x, raw_y);
    int cal_y = calibrate_y(raw_x, raw_y);

    // 填充LVGL数据结构
    data->point.x = cal_x;
    data->point.y = cal_y;
    data->state = pressed ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;

    // 保存状态(用于长按等场景)
    last_point = data->point;
    last_state = data->state;
}

// 3. 注册触摸设备
void touch_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);
}

这里有几个关键点,我跟你聊聊:

  • 读取频率:LVGL默认每5毫秒调用一次 read_cb。如果你的触摸驱动读取比较慢(比如通过I2C),可以适当降低频率,但别低于20ms,否则会感觉卡顿。
  • 状态切换:LV_INDEV_STATE_PR 表示按下,LV_INDEV_STATE_REL 表示释放。注意,如果触摸屏一直没被按下,你应该持续返回 REL 状态,而不是不调用回调。
  • 坐标范围:LVGL的坐标范围是0到屏幕分辨率。如果你的校准算法输出超出这个范围,LVGL会自动裁剪,但最好在回调里就处理好。

我曾经踩过的坑: 有一次我忘了在 touchpad_read 里处理 LV_INDEV_STATE_REL 的坐标。结果手指抬起来后,LVGL以为触摸点还在上一个位置,导致按钮一直处于高亮状态。记住:释放状态时,坐标值不会被使用,但状态一定要正确设置。

4.5 整体框架图:一张图看懂触摸驱动

说了这么多,我画了一张图帮你梳理整个流程。从硬件触摸中断,到内核输入子系统,再到LVGL的触摸回调,一条线串起来。

触摸驱动框架流程图 触摸驱动框架流程图 触摸硬件 (Touch IC) 中断 / 轮询读取 Linux 输入子系统 校准算法 (三点校准) LVGL touchpad_read 回调 UI 响应 (按钮/手势) 关键说明 • 触摸IC通过I2C/SPI上报 • 输入子系统统一事件格式 • 校准解决坐标偏移问题 常见问题 • 坐标反向:交换X/Y轴 • 触摸抖动:加软件滤波

从这张图你可以看到,整个链路其实不复杂。硬件上报原始数据,内核帮你封装成标准事件,你只需要在应用层做两件事:校准和回调。

4.6 实战避坑指南

最后,我把自己在触摸驱动上踩过的坑整理一下,你遇到了可以直接对照排查:

  • 触摸没反应:先检查 /dev/input/ 下有没有设备节点。没有的话,大概率是内核驱动没加载。
  • 坐标乱跳:可能是触摸IC的供电不稳定,或者I2C总线上有干扰。加个电容滤波试试。
  • 点击不准:校准参数没算对。重新跑一遍三点校准,或者检查LCD和触摸屏的安装是否平行。
  • 多点触摸失效:确认你用的是MT Protocol B,并且LVGL配置中启用了 LV_USE_GESTURE。
  • 触摸延迟高:检查 touchpad_read 回调里有没有做耗时操作(比如打印日志)。回调里尽量只做坐标转换,别干别的。

重要提醒: 如果你用的是电阻屏,别忘了做“触摸按下检测”。电阻屏的物理特性决定了它会有“抖动”,手指按下去的一瞬间,坐标会跳好几次。我建议加一个简单的去抖逻辑:连续采样3次,如果坐标差值小于5个像素,才认为是有效触摸。