2、LVGL输入设备驱动架构:输入设备驱动模型、注册与回调机制、事件处理流程、驱动与UI的交互方式

好,咱们接着聊。上一章我们把LVGL的移植跑通了,屏幕能点亮,但光有显示还不够——你得能跟它交互才行。这一章,我就带你深入LVGL的输入设备驱动架构。说白了,就是搞清楚触摸屏、按键、编码器这些东西,是怎么跟LVGL“对话”的。

2.1 输入设备驱动模型:LVGL眼中的“输入设备”长什么样?

LVGL把输入设备抽象成了一个统一的结构体——lv_indev_drv_t。你想想看,触摸屏、鼠标、键盘、旋转编码器,它们本质上都是“输入”,只是数据格式不同。LVGL用这个结构体,把千奇百怪的硬件统一了起来。

这个结构体里,最重要的几个字段我列一下:

字段 类型 说明
type lv_indev_type_t 设备类型:触摸屏、按键、编码器、鼠标等
read_cb 函数指针 读取数据的回调函数,核心!
user_data void * 用户自定义数据,比如I2C句柄
disp lv_disp_t * 关联的显示器,触摸屏必须绑定

我个人习惯,在初始化驱动之前,先把这些字段想清楚。比如你用的是FT6336触摸芯片,那type就是LV_INDEV_TYPE_POINTERread_cb就是你自己写的读取函数。

2.2 注册与回调机制:把你的驱动“挂”到LVGL上

驱动模型搞清楚了,下一步就是注册。注册的过程,其实就是把lv_indev_drv_t初始化好,然后调用lv_indev_drv_register()。我贴一段典型的代码,你一看就明白:

static lv_indev_t * indev_touch;

void touch_driver_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 = touch_read_cb;      // 注册读取回调
    indev_drv.user_data = NULL;             // 暂时不用

    indev_touch = lv_indev_drv_register(&indev_drv); // 注册!
}

这里有个关键点——read_cb回调函数。LVGL会周期性地调用它,频率由LVGL的定时器决定。你的任务就是在回调里,把硬件数据填到lv_indev_data_t结构体中。

核心回调函数模板:

static void touch_read_cb(lv_indev_drv_t * drv, lv_indev_data_t * data)
{
    // 1. 读取触摸芯片寄存器
    uint8_t touch_points = 0;
    uint16_t x = 0, y = 0;
    
    // 假设这是你的I2C读取函数
    read_touch_point(&touch_points, &x, &y);

    // 2. 判断是否有触摸
    if(touch_points > 0) {
        data->state = LV_INDEV_STATE_PR;
        data->point.x = x;
        data->point.y = y;
    } else {
        data->state = LV_INDEV_STATE_REL;
    }

    // 3. 不需要返回,LVGL会自动处理
}

嗯,这里要注意一点:data->state只有两个值——LV_INDEV_STATE_PR(按下)和LV_INDEV_STATE_REL(释放)。别搞复杂了。

小技巧: 我在项目中遇到过触摸坐标反了的情况。后来发现是触摸芯片的X/Y轴和屏幕的坐标系不一致。解决办法很简单,在回调里做个映射:data->point.x = screen_width - raw_x; 就行了。

2.3 事件处理流程:从手指按下到UI响应,中间发生了什么?

你按下屏幕,UI上的按钮变色了。这中间经历了什么?我把它拆成三步:

  1. 硬件读取阶段: LVGL的定时器触发,调用你的read_cb,拿到原始坐标和状态。
  2. 软件处理阶段: LVGL内部判断这个坐标落在了哪个对象上,然后生成事件。
  3. UI响应阶段: 你注册的事件回调被调用,执行对应的逻辑。

说白了,LVGL帮你做了“坐标命中检测”这件事。你只需要告诉它“手指按在了(100, 200)”,它就知道这个点是不是在按钮A上。

避坑指南: 我曾经在回调里做了延时操作,比如用lv_task_delay()。结果整个UI都卡住了。为什么?因为read_cb是在LVGL的主循环里被调用的,你在这里阻塞,整个事件处理流程就停了。记住:回调里不要做耗时操作!

2.4 驱动与UI的交互方式:你的代码怎么“通知”UI?

驱动和UI的交互,其实就两种方式:

  • 轮询方式: LVGL定时调用read_cb,这是默认方式,简单可靠。
  • 中断方式: 触摸芯片产生中断,你通过lv_indev_read_cb()手动触发读取。适合低功耗场景。

我个人更推荐轮询方式,除非你对功耗有严格要求。因为中断方式容易引入竞态问题,调试起来比较头疼。

驱动和UI的交互,最终体现在事件上。比如你写了一个按钮的回调:

static void btn_event_cb(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);
    
    if(code == LV_EVENT_CLICKED) {
        LV_LOG_USER("按钮被点击了!");
        // 执行你的业务逻辑
    }
}

这个回调什么时候被调用?就是LVGL在事件处理流程的第三步触发的。驱动层不需要关心这个,它只负责把原始数据喂给LVGL。

总结一下驱动与UI的交互链路:

硬件中断/轮询 → read_cblv_indev_data_t → LVGL内部处理 → 事件回调 → UI响应

你想想看,这个链路其实很清晰。驱动层只做一件事:把硬件数据翻译成LVGL能理解的格式。剩下的命中检测、事件分发,LVGL全包了。这就是框架的好处——你不用重复造轮子。

好,这一章就到这里。下一章我会带你实战一个具体的触摸驱动——FT6336,从I2C初始化到坐标校准,一步步写出来。到时候你就知道,理论跟实践结合起来,其实没那么难。