3、输入设备驱动移植:触摸屏与按键的驱动模型、indev_read_cb回调、坐标上报与事件处理

输入设备驱动移植,说白了就是让LVGL能「感知」到用户的操作。触摸屏点一下、按键按一下,这些物理信号怎么变成LVGL能理解的事件?这就是本章要解决的问题。

我刚开始做LVGL移植时,觉得输入设备很简单——不就是读个坐标嘛。结果踩了不少坑。比如触摸屏上报坐标的时序不对,导致界面点击无响应;按键的抖动没处理好,一次按下触发了两次事件。嗯,这些坑咱们今天一个个填上。

核心要点:LVGL的输入设备驱动模型是统一的——无论触摸屏还是按键,最终都通过 indev_read_cb 回调函数上报数据。你只需要实现这个回调,LVGL就会自动处理后续的事件分发和响应。

3.1 输入设备驱动模型:统一抽象层

LVGL把输入设备抽象成几个类型:触摸屏(指针设备)、按键(键盘)、编码器、按钮矩阵等。每种类型都有对应的数据结构,但注册流程几乎一样。

我个人习惯把驱动模型分成三层:

  • 硬件层:直接操作寄存器或调用HAL库函数,读取物理信号
  • 适配层:将硬件数据转换成LVGL需要的格式(坐标、按键码)
  • 注册层:调用 lv_indev_drv_register() 将设备注册到LVGL

你想想看,这种分层设计的好处是什么?换硬件平台时,只需要改硬件层,适配层和注册层基本不用动。我在一个项目里从FT5x06触摸芯片换成GT911,只改了硬件层的I2C读写函数,其他代码直接复用。

LVGL输入设备驱动模型架构 硬件层 I2C/SPI/GPIO读取 | 触摸芯片寄存器操作 | 按键电平检测 适配层 坐标转换 | 按键码映射 | 抖动滤波 | 数据缓存 注册层 lv_indev_drv_init() → lv_indev_drv_register() → indev_read_cb 各层职责清晰,换硬件只需改硬件层

3.2 indev_read_cb回调:核心接口

indev_read_cb 是LVGL输入设备驱动的灵魂。LVGL会周期性调用这个回调函数,询问设备是否有新数据。回调函数的原型长这样:

void my_touchpad_read(lv_indev_drv_t * drv, lv_indev_data_t * data)
{
    // 1. 读取硬件数据
    int16_t x, y;
    uint8_t pressed = read_touch_point(&x, &y);

    // 2. 填充数据到LVGL结构体
    if(pressed) {
        data->point.x = x;
        data->point.y = y;
        data->state = LV_INDEV_STATE_PR;
    } else {
        data->state = LV_INDEV_STATE_REL;
    }
}

这里有个关键点:lv_indev_data_t 结构体里最重要的两个字段是 point(坐标)和 state(状态)。状态只有两种:按下(LV_INDEV_STATE_PR)和释放(LV_INDEV_STATE_REL)。

我的经验:回调函数里不要做耗时操作。LVGL的输入处理是在主循环或定时器里调用的,如果你在回调里做I2C通信时加上100ms延时,整个UI都会卡顿。正确的做法是:在中断或单独线程里读取硬件数据并缓存,回调里只做数据搬运。

3.3 触摸屏驱动移植:坐标上报

触摸屏的坐标上报,最核心的问题有两个:坐标转换和多点触摸处理。

3.3.1 坐标转换

触摸芯片返回的原始坐标,通常和屏幕分辨率不是一一对应的。比如你的屏幕是480×272,但触摸芯片返回的坐标范围是0~4095。这时候就需要做线性映射:

int16_t map_coord(int16_t raw, int16_t raw_min, int16_t raw_max, 
                  int16_t screen_min, int16_t screen_max)
{
    return (raw - raw_min) * (screen_max - screen_min) / (raw_max - raw_min) + screen_min;
}

// 在回调中使用
data->point.x = map_coord(raw_x, 0, 4095, 0, 479);
data->point.y = map_coord(raw_y, 0, 4095, 0, 271);

我曾经在一个项目里遇到过触摸反向的问题——手指往左滑,光标往右跑。排查了半天,发现是触摸芯片的X轴方向和屏幕的X轴方向反了。解决办法很简单:data->point.x = 479 - map_coord(...)。

3.3.2 多点触摸处理

LVGL默认只支持单点触摸。如果你的触摸芯片支持多点,需要做「第一点优先」策略——只上报第一个触摸点的坐标。我一般这样处理:

void my_touchpad_read(lv_indev_drv_t * drv, lv_indev_data_t * data)
{
    uint8_t touch_count;
    touch_point_t points[5];

    touch_count = read_multi_touch(points, 5);

    if(touch_count > 0) {
        // 只取第一个触摸点
        data->point.x = points[0].x;
        data->point.y = points[0].y;
        data->state = LV_INDEV_STATE_PR;
    } else {
        data->state = LV_INDEV_STATE_REL;
    }
}

注意:有些触摸芯片在手指抬起后,不会主动清除坐标寄存器。如果你不处理,LVGL会认为手指一直按在最后一个位置。正确的做法是:检测到触摸释放时,将坐标清零或保持上次值,但状态必须设为 LV_INDEV_STATE_REL。

3.4 按键驱动移植:按键码与事件映射

按键驱动和触摸屏不同,它上报的不是坐标,而是按键码。LVGL为按键定义了标准按键码,比如方向键、确认键、返回键等。

LVGL按键码 对应物理按键 功能说明
LV_KEY_UP 上方向键 焦点上移 / 数值增加
LV_KEY_DOWN 下方向键 焦点下移 / 数值减少
LV_KEY_LEFT 左方向键 焦点左移 / 滑块左滑
LV_KEY_RIGHT 右方向键 焦点右移 / 滑块右滑
LV_KEY_ENTER 确认键 触发点击事件
LV_KEY_ESC 返回键 返回上一级 / 关闭弹窗

按键驱动的回调实现也很直接:

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

    uint8_t key = read_keypad();

    if(key != KEY_NONE) {
        // 按键按下
        last_key = key_to_lv_key(key);  // 物理按键码 → LVGL按键码
        last_state = LV_INDEV_STATE_PR;
    } else {
        // 按键释放
        last_state = LV_INDEV_STATE_REL;
    }

    data->key = last_key;
    data->state = last_state;
}

这里有个细节:data->key 只在按下时有效,释放时LVGL不会处理按键码。所以释放时只需要设置状态,不需要修改 data->key。

3.5 事件处理:从输入到响应

数据上报到LVGL后,事件处理是自动的。但理解它的流程,能帮你更好地调试问题。

LVGL的事件处理流程大致是:

  1. 输入读取:LVGL调用 indev_read_cb 获取当前输入状态
  2. 命中检测:判断触摸点落在哪个对象上(或按键焦点在哪个对象上)
  3. 事件生成:根据输入状态变化生成 LV_EVENT_PRESSED、LV_EVENT_CLICKED 等事件
  4. 事件分发:将事件发送给目标对象的事件回调

举个例子,你点击了一个按钮:

  • 手指按下 → 生成 LV_EVENT_PRESSED → 按钮进入按下状态
  • 手指抬起 → 生成 LV_EVENT_RELEASED → 如果抬起位置在按钮内,再生成 LV_EVENT_CLICKED
  • 如果手指按下后滑出按钮再抬起 → 只生成 LV_EVENT_RELEASED,不生成 LV_EVENT_CLICKED

避坑指南:我曾经遇到一个bug——按钮点击后没有反应。排查发现是触摸屏的坐标精度不够,手指按下时坐标抖动,导致命中检测失败。解决办法是在适配层加一个简单的均值滤波:连续读5次坐标取平均,再上报给LVGL。

3.6 调试技巧:验证驱动是否正确

驱动写完后,怎么验证它工作正常?我一般用LVGL自带的demo:

// 1. 注册输入设备
lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;  // 触摸屏
indev_drv.read_cb = my_touchpad_read;
lv_indev_drv_register(&indev_drv);

// 2. 运行触摸测试demo
lv_demo_widgets();  // 或者 lv_demo_music()

如果触摸正常,你应该能点击按钮、滑动滑块。如果没反应,先检查:

  • 回调函数是否被调用?(加个打印看看)
  • 坐标范围是否正确?(打印原始坐标和映射后的坐标)
  • 状态切换是否正常?(按下/释放状态是否交替出现)

对于按键,可以用 lv_demo_keypad_encoder() 来测试,它会显示当前焦点和按键事件。

我的调试习惯:在回调函数里加一个静态计数器,每次调用时递增。如果计数器不增长,说明回调没注册成功;如果增长太快(比如每毫秒几十次),说明LVGL的输入处理频率太高,可能需要调整 LV_INDEV_DEF_READ_PERIOD 宏。

好了,输入设备驱动移植的核心内容就这些。记住三个关键点:统一回调接口、坐标正确转换、状态准确上报。做到这三点,你的LVGL就能「听懂」用户的操作了。