4. LVGL源码结构与配置

输入设备,说白了就是LVGL跟用户交互的「手」和「脚」。没有它,界面再漂亮也只是个花瓶。我早期做的一个项目,屏幕点亮了,按钮画好了,结果触摸没反应——那种尴尬,至今难忘。

这一章,咱们就搞定两件事:触摸屏和物理按键。我会把驱动接口、I2C/SPI适配、按键映射这些坑,一个一个填平。

核心要点:LVGL的输入设备驱动只认一个回调函数——read_cb。你只要把硬件数据塞进这个接口,剩下的交给LVGL。

4.1 输入设备驱动接口:read_cb

LVGL里注册一个输入设备,其实就三步:

  1. 定义一个 lv_indev_drv_t 结构体
  2. 设置类型(触摸/按键/编码器)
  3. 绑定 read_cb 回调

代码长这样:

static lv_indev_drv_t indev_drv;
static lv_indev_t *my_indev = NULL;

static void my_touchpad_read(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
    // 这里填你的硬件读取逻辑
    data->point.x = touch_x;
    data->point.y = touch_y;
    data->state = touch_pressed ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;
}

void input_device_init(void)
{
    lv_indev_drv_init(&indev_drv);
    indev_drv.type = LV_INDEV_TYPE_POINTER;  // 触摸屏
    indev_drv.read_cb = my_touchpad_read;
    my_indev = lv_indev_drv_register(&indev_drv);
}

嗯,这里要注意:read_cb 会被LVGL的定时器周期性调用。你不需要自己开线程,也不需要轮询。LVGL内部会处理好频率和同步。

我的习惯:在 read_cb 里只做数据读取,不做耗时处理。如果触摸芯片需要校准或滤波,我会放在硬件初始化阶段完成,或者用独立任务处理。

4.2 触摸屏驱动适配(I2C/SPI)

触摸屏的通信接口,最常见的就是I2C和SPI。我遇到过不少新手,直接把触摸芯片的驱动写在 read_cb 里,结果界面卡成PPT。

为什么会这样?因为I2C/SPI通信本身就有延迟,再加上触摸芯片内部滤波,一次读取可能耗时几毫秒。如果LVGL的刷新周期是20ms,你光读触摸就占了5ms,那剩下的15ms还要画图、处理动画,不卡才怪。

4.2.1 I2C触摸屏适配(以FT5x06为例)

FT5x06是电容触摸屏的常青树。驱动逻辑很简单:

#define FT5x06_ADDR         0x38
#define FT5x06_REG_TOUCH    0x02
#define FT5x06_REG_POINTS   0x02

static uint8_t ft5x06_read_byte(uint8_t reg)
{
    uint8_t val = 0;
    // I2C读操作,具体实现依赖你的HAL库
    i2c_read(FT5x06_ADDR, reg, &val, 1);
    return val;
}

static void ft5x06_read_touch(uint16_t *x, uint16_t *y, uint8_t *pressed)
{
    uint8_t buf[4];
    uint8_t status = ft5x06_read_byte(FT5x06_REG_TOUCH);
    
    if (status & 0x0F) {  // 有触摸点
        // 读取第一个触摸点的坐标
        buf[0] = ft5x06_read_byte(0x03);
        buf[1] = ft5x06_read_byte(0x04);
        buf[2] = ft5x06_read_byte(0x05);
        buf[3] = ft5x06_read_byte(0x06);
        
        *x = ((buf[0] & 0x0F) << 8) | buf[1];
        *y = ((buf[2] & 0x0F) << 8) | buf[3];
        *pressed = 1;
    } else {
        *pressed = 0;
    }
}

我曾经踩过的坑:FT5x06的坐标寄存器是12位数据,分两个字节存放。如果你直接读两个字节拼起来,不处理高4位,坐标会乱跳。正确做法是:高字节的低4位 + 低字节的8位。

4.2.2 SPI触摸屏适配(以XPT2046为例)

XPT2046是电阻屏的老将,SPI接口,速度比I2C快不少。但电阻屏需要ADC采样,精度和稳定性不如电容屏。

static uint16_t xpt2046_read_adc(uint8_t channel)
{
    uint8_t tx_buf[3] = {0};
    uint8_t rx_buf[3] = {0};
    uint16_t result = 0;
    
    // 构造SPI命令:起始位+模式+通道
    tx_buf[0] = 0x80 | (channel << 4);
    
    spi_transfer(tx_buf, rx_buf, 3);
    
    // 12位数据,分布在rx_buf[1]和rx_buf[2]
    result = ((rx_buf[1] << 8) | rx_buf[2]) >> 3;
    return result;
}

static void xpt2046_read_touch(uint16_t *x, uint16_t *y, uint8_t *pressed)
{
    uint16_t adc_x, adc_y;
    uint16_t pressure;
    
    adc_x = xpt2046_read_adc(0x01);  // X通道
    adc_y = xpt2046_read_adc(0x05);  // Y通道
    pressure = xpt2046_read_adc(0x03); // 压力通道
    
    // 压力阈值判断
    if (pressure > 100) {
        *x = adc_x;
        *y = adc_y;
        *pressed = 1;
    } else {
        *pressed = 0;
    }
}

性能优化建议:SPI触摸屏的ADC采样需要时间,我一般会在 read_cb 里加一个简单的均值滤波——连续采3次取平均,能有效消除抖动。

4.3 物理按键映射为LVGL逻辑按键

有些场景不适合触摸屏,比如工业设备、车载中控。这时候物理按键就派上用场了。LVGL支持将物理按键映射为逻辑按键,比如「上」「下」「确定」「取消」。

你想想看,一个旋转编码器加两个按键,就能控制整个菜单系统。我在一个智能家居项目里就是这么干的,成本低,可靠性高。

4.3.1 按键驱动注册

static uint8_t key_value = 0;

static void my_keypad_read(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
    // 读取物理按键状态
    key_value = read_keypad_gpio();
    
    // 映射为LVGL按键码
    switch (key_value) {
        case KEY_UP:
            data->key = LV_KEY_UP;
            break;
        case KEY_DOWN:
            data->key = LV_KEY_DOWN;
            break;
        case KEY_ENTER:
            data->key = LV_KEY_ENTER;
            break;
        case KEY_BACK:
            data->key = LV_KEY_ESC;
            break;
        default:
            data->key = 0;
            break;
    }
    
    data->state = (key_value != KEY_NONE) ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;
}

void keypad_init(void)
{
    lv_indev_drv_t key_drv;
    lv_indev_drv_init(&key_drv);
    key_drv.type = LV_INDEV_TYPE_KEYPAD;
    key_drv.read_cb = my_keypad_read;
    lv_indev_drv_register(&key_drv);
}

4.3.2 按键映射表

LVGL预定义了一组按键码,你可以自由映射。我整理了一个常用映射表:

物理按键 LVGL按键码 功能说明
上键 LV_KEY_UP 焦点上移 / 数值增加
下键 LV_KEY_DOWN 焦点下移 / 数值减少
左键 LV_KEY_LEFT 焦点左移 / 上一页
右键 LV_KEY_RIGHT 焦点右移 / 下一页
确认键 LV_KEY_ENTER 选中 / 进入
返回键 LV_KEY_ESC 返回 / 取消
菜单键 LV_KEY_HOME 回到主界面

注意:按键去抖一定要在硬件层或驱动层处理。我见过有人在 read_cb 里加延时去抖,结果LVGL的定时器被阻塞,界面直接卡死。正确做法是用状态机或定时器去抖。

4.4 输入设备知识体系

下面这张图,把本章的核心逻辑串起来了:

LVGL输入设备移植知识体系 LVGL输入设备层 read_cb 回调接口 触摸屏(Pointer) 物理按键(Keypad) 编码器(Encoder) I2C触摸芯片 SPI触摸芯片 GPIO按键矩阵 ADC/编码器 核心原则:read_cb 只做数据搬运,不做耗时处理。硬件滤波、去抖、校准在驱动层完成。 映射关系:物理按键 → LV_KEY_xxx → 焦点导航/事件响应

4.5 实战中的避坑指南

  • 坐标旋转:触摸屏的X/Y轴可能和屏幕显示方向不一致。我一般会在 read_cb 里做一次坐标变换,而不是改LVGL的显示方向。
  • 多点触摸:LVGL原生支持单点触摸。如果你用多点触摸屏,需要在 read_cb 里只上报第一个触摸点,或者自己实现手势识别。
  • 按键重复:长按按键时,LVGL会自动触发重复事件。你不需要在驱动层做重复逻辑,只需要保证 read_cb 正确上报按下/释放状态。

我的经验:调试触摸屏时,我会先用一个简单的测试程序,把原始坐标打印出来,用手指在屏幕上画圈。如果画出来的轨迹是乱的,说明坐标解析有问题。如果轨迹是断的,说明触摸芯片的通信不稳定。

好了,这一章的内容就到这里。输入设备移植,说白了就是「硬件数据 → read_cb → LVGL事件」这条链路。只要链路通了,剩下的就是调参和优化。