3、输入设备移植:触摸屏驱动对接、按键与编码器适配

输入设备移植,说白了就是让 LVGL 能「听懂」你的硬件在说什么。触摸屏、按键、旋转编码器,每种设备的脾气都不一样。我刚开始做移植时,以为只要把坐标传进去就行,结果触摸屏要么乱跳,要么点不动。后来踩了不少坑,才摸清楚这里面的门道。

核心原则:LVGL 不关心你的硬件怎么采集数据,它只关心你通过 indev_read_callback 喂给它的数据是否规范、及时。

3.1 触摸屏驱动对接

触摸屏是最常见的输入设备。对接时,你需要实现一个回调函数,LVGL 会定期调用它来获取触摸状态。

3.1.1 基础对接流程

  1. 初始化触摸芯片:在 lv_indev_drv_register 之前,确保 I2C/SPI 通信正常。
  2. 实现回调:函数签名固定为 void my_touchpad_read(lv_indev_drv_t *drv, lv_indev_data_t *data)。
  3. 填充数据:设置 data->point.x、data->point.y 和 data->state。
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;

    bool touched;
    lv_coord_t x, y;

    // 调用硬件驱动读取坐标
    touched = ft5x06_read_point(&x, &y);

    if (touched) {
        data->point.x = x;
        data->point.y = y;
        data->state = LV_INDEV_STATE_PR;
        last_x = x;
        last_y = y;
    } else {
        data->point.x = last_x;
        data->point.y = last_y;
        data->state = LV_INDEV_STATE_REL;
    }
}

void touchpad_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);
}

我的习惯:在 touched 为 false 时,保留上一次的坐标。这样 LVGL 在释放状态时还能知道手指最后的位置,避免界面元素「跳变」。

3.1.2 坐标校准与旋转

很多触摸屏的物理坐标和屏幕显示方向不一致。我遇到过一款 480x272 的屏,触摸芯片输出的 X 范围是 0~4095,Y 是 0~4095,但实际有效区域只有中间一部分。这时候需要做映射。

// 线性映射示例
data->point.x = (x - x_min) * LV_HOR_RES / (x_max - x_min);
data->point.y = (y - y_min) * LV_VER_RES / (y_max - y_min);

如果屏幕旋转了 90 度,记得交换 X/Y 并反转方向:

// 旋转 90 度
data->point.x = LV_VER_RES - y;
data->point.y = x;

我曾经踩过的坑:坐标映射时用了整数除法,导致触摸点永远到不了屏幕边缘。记得用浮点运算或者先乘后除,避免精度丢失。

3.2 按键与编码器适配

不是所有设备都有触摸屏。按键和编码器在工业设备、家电中非常常见。LVGL 对这两种设备有专门的支持。

3.2.1 按键适配

按键通常映射为 LV_INDEV_TYPE_KEYPAD。你需要告诉 LVGL 哪个按键对应哪个 LVGL 标准键值。

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

    uint32_t key = read_key_gpio();  // 读取按键 GPIO

    if (key != KEY_NONE) {
        data->key = key;
        data->state = LV_INDEV_STATE_PR;
        last_key = key;
    } else {
        data->key = last_key;
        data->state = LV_INDEV_STATE_REL;
    }
}

常用的键值映射关系:

物理按键 LVGL 键值 说明
上 LV_KEY_UP 焦点上移
下 LV_KEY_DOWN 焦点下移
左 LV_KEY_LEFT 焦点左移
右 LV_KEY_RIGHT 焦点右移
确认 LV_KEY_ENTER 触发点击
返回 LV_KEY_ESC 返回/取消

3.2.2 编码器适配

旋转编码器是旋钮类设备的首选。它有两个信号:旋转方向和按键按下。LVGL 用 LV_INDEV_TYPE_ENCODER 来处理。

static void encoder_read(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
    static int16_t last_diff = 0;

    int16_t diff = encoder_get_diff();  // 获取旋转差值,正为顺时针,负为逆时针
    bool btn = encoder_get_btn();       // 获取按键状态

    data->enc_diff = diff;
    data->state = btn ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;

    last_diff = diff;
}

我个人建议:编码器的 enc_diff 不要累积太大值。LVGL 每次读取后会自动清零,你只需要返回本次周期内的变化量即可。如果硬件有抖动,可以在驱动层做简单的消抖滤波。

3.3 indev_read_callback 实现要点

这个回调是 LVGL 输入子系统的核心。LVGL 会在 lv_task_handler 中周期调用它,频率通常和屏幕刷新率一致(比如 30fps 就是每 33ms 调用一次)。

3.3.1 回调的职责

  • 快速返回:回调里不要做耗时操作,比如延时等待触摸芯片应答。LVGL 是单线程的,你卡住了它,整个 UI 就卡住了。
  • 只填充数据:不要在这里处理手势、双击等逻辑,那是 LVGL 上层的事。
  • 状态机清晰:按下和释放状态要明确,不要出现「既按下又释放」的歧义。

3.3.2 性能优化技巧

场景 做法 原因
触摸屏数据读取 使用 DMA + 中断,回调只读缓存 避免 SPI/I2C 同步等待
按键扫描 定时器中断中扫描,回调只取最新值 减少回调执行时间
编码器计数 硬件定时器捕获,回调直接读寄存器 软件计数容易丢步

我曾经犯过的错:在回调里用 printf 打印调试信息,结果 LVGL 界面卡成幻灯片。回调里绝对不要做任何 I/O 操作,尤其是串口打印。

3.4 多点触控支持

LVGL 从 v7 开始支持多点触控,但默认只处理第一个触摸点。要开启多点触控,需要做两件事:

  1. 使能宏:在 lv_conf.h 中设置 LV_INDEV_DEF_POINT_NUM 为需要的点数(比如 5)。
  2. 修改回调:使用 lv_indev_data_t 的 points 数组。
static void mt_touchpad_read(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
    uint8_t touch_cnt = ft5x06_read_all_points(data->points);

    if (touch_cnt > 0) {
        data->state = LV_INDEV_STATE_PR;
        data->continue_read = (touch_cnt > 1);  // 告诉 LVGL 还有更多点
    } else {
        data->state = LV_INDEV_STATE_REL;
        data->continue_read = false;
    }
}

注意:continue_read 这个字段很关键。如果触摸芯片一次只能读一个点,你需要设置它为 true,LVGL 会再次调用回调读取下一个点,直到你返回 false。我刚开始没设这个,结果多点触控永远只识别到一个手指。

多点触控的应用场景主要是双指缩放、旋转等手势。LVGL 内部有手势识别模块,你只需要把原始点数据喂给它就行。

输入设备移植知识体系 LVGL 输入子系统 触摸屏 (POINTER) 按键 (KEYPAD) 编码器 (ENCODER) 坐标校准 旋转映射 多点触控 键值映射表 焦点导航 旋转差值计算 按键消抖 indev_read_callback 实现

嗯,到这里输入设备移植的核心内容就差不多了。触摸屏、按键、编码器,三种设备各有各的脾气,但归根结底都是通过 indev_read_callback 这个接口和 LVGL 对话。你只要把数据喂对了,剩下的手势识别、焦点切换、滚动等高级功能,LVGL 自己会搞定。