4. LVGL移植流程总览

输入设备驱动适配,说白了就是让LVGL能“听懂”你的硬件在说什么。触摸屏点了一下,按键按了一下,这些物理事件怎么变成LVGL能处理的逻辑事件?这就是本章要解决的问题。

我刚开始做LVGL移植时,觉得这步很简单——不就是把触摸芯片的坐标读出来,传给LVGL嘛。结果呢?第一次上机,触摸乱跳,点A处响应到B处,折腾了两天才发现是坐标转换没做对。嗯,从那以后我再也不敢小看输入设备驱动了。

核心要点:LVGL的输入设备驱动不关心你的硬件是什么,它只认一个结构体——lv_indev_drv_t。你只要把这个结构体填好,注册进去,剩下的交给LVGL。

4.1 输入设备驱动接口

LVGL把所有输入设备抽象成5种类型:

类型 枚举值 典型硬件
触摸屏 LV_INDEV_TYPE_POINTER 电阻屏、电容屏
物理按键 LV_INDEV_TYPE_KEYPAD 矩阵键盘、GPIO按键
编码器 LV_INDEV_TYPE_ENCODER 旋转编码器
鼠标 LV_INDEV_TYPE_POINTER USB鼠标、PS/2鼠标
按钮 LV_INDEV_TYPE_BUTTON 屏幕外部物理按钮

你想想看,不管硬件多复杂,最终都要落到这5类里。我个人习惯先把硬件类型定下来,再写驱动,这样思路清晰很多。

4.2 驱动注册流程

注册一个输入设备驱动,其实就三步:

  1. 初始化驱动结构体——调用 lv_indev_drv_init()
  2. 设置回调函数——告诉LVGL怎么读你的设备
  3. 注册驱动——调用 lv_indev_drv_register()

代码长这样:

static lv_indev_drv_t indev_drv;
static lv_indev_t *my_indev = NULL;

/* 1. 初始化驱动结构体 */
lv_indev_drv_init(&indev_drv);

/* 2. 设置类型和回调 */
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = my_touchpad_read;

/* 3. 注册驱动 */
my_indev = lv_indev_drv_register(&indev_drv);

这里有个坑——lv_indev_drv_init() 必须在 lv_init() 之后调用。我曾经在初始化顺序上翻过车,LVGL还没准备好就注册驱动,结果系统直接挂掉。

注意:驱动结构体 lv_indev_drv_t 的生命周期必须贯穿整个程序运行期。不要定义成局部变量,否则注册后结构体被销毁,LVGL访问野指针,后果你懂的。

4.3 回调函数:触摸屏读取

回调函数是驱动的灵魂。LVGL会周期性地调用你注册的 read_cb,你需要在里面完成两件事:

  • 从硬件读取数据
  • 把数据填入 lv_indev_data_t 结构体

触摸屏的回调函数模板:

static void my_touchpad_read(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
    /* 读取触摸坐标(假设使用I2C接口的FT6336) */
    uint16_t x, y;
    uint8_t touched = ft6336_read_touch(&x, &y);

    if (touched) {
        data->point.x = x;
        data->point.y = y;
        data->state = LV_INDEV_STATE_PRESSED;
    } else {
        data->state = LV_INDEV_STATE_RELEASED;
    }
}

为什么这么写?因为LVGL内部的事件处理机制依赖 state 字段来判断按下和释放。你想想看,如果 state 一直不更新,LVGL就永远不知道手指已经抬起来了。

经验之谈:触摸屏的坐标通常需要做校准。我习惯在回调函数里做坐标映射,把硬件原始坐标映射到LVGL的显示坐标系。公式很简单:lv_x = (raw_x * LV_HOR_RES) / touch_max_x。

4.4 回调函数:按键读取

按键驱动和触摸屏不太一样。按键没有坐标,只有键值。LVGL为按键定义了一套标准键值映射:

LVGL键值 对应操作 典型硬件按键
LV_KEY_UP 向上移动焦点 上方向键
LV_KEY_DOWN 向下移动焦点 下方向键
LV_KEY_ENTER 确认/点击 确认键
LV_KEY_BACK 返回/退出 返回键
LV_KEY_HOME 回到主界面 主页键

按键回调函数示例:

static void my_keypad_read(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
    static uint32_t last_key = 0;

    /* 读取按键状态(假设使用GPIO扫描) */
    uint32_t key = keypad_scan();

    if (key != 0) {
        data->key = key;
        data->state = LV_INDEV_STATE_PRESSED;
        last_key = key;
    } else {
        data->state = LV_INDEV_STATE_RELEASED;
        /* 释放时保留最后一次按下的键值 */
        data->key = last_key;
    }
}

这里有个细节——释放时为什么要保留 last_key?因为LVGL在释放事件中需要知道是哪个键被释放了。我刚开始没注意这个,结果按键释放事件永远拿不到正确的键值。

4.5 事件上报机制

LVGL的事件上报机制,说白了就是“轮询+回调”的组合拳。LVGL的主循环 lv_task_handler() 会定期调用所有注册的 read_cb,然后把读取到的数据分发给对应的对象。

整个流程是这样的:

LVGL输入设备事件上报流程 lv_task_handler() 调用 read_cb() 获取 lv_indev_data_t 判断状态是否变化 分发事件到对象 关键点: • 轮询频率由LVGL内部定时器控制 • 状态无变化时不触发事件 • 事件通过lv_event_send()发送

你可能会问:LVGL怎么知道状态有没有变化?它内部维护了一个 last_state 变量,每次读取后和当前状态比较。如果状态没变,就不触发事件。这个设计很聪明,避免了大量无意义的事件处理。

性能提示:如果你的触摸屏支持中断,可以在中断服务函数里设置一个标志位,然后在 read_cb 中检查这个标志位。这样LVGL就不会在无触摸时频繁调用I2C/SPI读取,能省不少CPU资源。

4.6 多输入设备共存

实际项目中,一个设备往往同时有触摸屏和物理按键。LVGL支持同时注册多个输入设备,每个设备独立工作。

注册方式很简单:

/* 注册触摸屏 */
lv_indev_drv_t touch_drv;
lv_indev_drv_init(&touch_drv);
touch_drv.type = LV_INDEV_TYPE_POINTER;
touch_drv.read_cb = my_touch_read;
lv_indev_t *touch_indev = lv_indev_drv_register(&touch_drv);

/* 注册按键 */
lv_indev_drv_t key_drv;
lv_indev_drv_init(&key_drv);
key_drv.type = LV_INDEV_TYPE_KEYPAD;
key_drv.read_cb = my_key_read;
lv_indev_t *key_indev = lv_indev_drv_register(&key_drv);

/* 将按键设备关联到默认组 */
lv_group_t *g = lv_group_get_default();
if (g == NULL) {
    g = lv_group_create();
    lv_group_set_default(g);
}
lv_indev_set_group(key_indev, g);

这里要注意,按键设备必须关联到一个组(lv_group_t),否则按键事件没有对象可以接收。触摸屏不需要组,因为它通过坐标直接定位对象。

避坑指南:我曾经在一个项目里同时用了触摸屏和编码器,结果发现编码器转动时触摸屏也误触发。排查了半天,原来是两个设备的I2C地址冲突了。所以,多设备共存时一定要检查硬件资源是否独立。

4.7 调试技巧

驱动写好了,怎么验证对不对?我分享几个实用方法:

  • 打印原始数据——在 read_cb 里加 printf,看坐标和状态是否合理
  • 使用LVGL内置demo——运行 lv_demo_widgets(),触摸各个控件看响应
  • 检查事件日志——LVGL有 LV_LOG_TRACE 级别的日志,可以打开看事件流

嗯,调试阶段最怕的就是“触摸没反应”。这时候先别怀疑LVGL,用示波器或逻辑分析仪看看I2C/SPI波形,确认硬件在正常工作。我遇到过好几次,折腾半天发现是触摸芯片的复位引脚没拉高。

小技巧:如果触摸坐标总是反的(比如点左上角响应到右下角),检查一下你的坐标映射公式。LCD坐标系和触摸屏坐标系可能不一样,需要做X/Y镜像或旋转。

好了,输入设备驱动适配的核心内容就这些。说白了,就是填好 lv_indev_drv_t,写好 read_cb,然后注册进去。剩下的,LVGL帮你搞定。