4、输入设备移植(触摸与按键)
输入设备,说白了就是LVGL跟用户交互的「手」和「脚」。没有它,界面再漂亮也只是个花瓶。我早期做的一个项目,屏幕点亮了,按钮画好了,结果触摸没反应——那种尴尬,至今难忘。
这一章,咱们就搞定两件事:触摸屏和物理按键。我会把驱动接口、I2C/SPI适配、按键映射这些坑,一个一个填平。
核心要点:LVGL的输入设备驱动只认一个回调函数——read_cb。你只要把硬件数据塞进这个接口,剩下的交给LVGL。
4.1 输入设备驱动接口:read_cb
LVGL里注册一个输入设备,其实就三步:
- 定义一个
lv_indev_drv_t结构体 - 设置类型(触摸/按键/编码器)
- 绑定
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 输入设备知识体系
下面这张图,把本章的核心逻辑串起来了:
4.5 实战中的避坑指南
- 坐标旋转:触摸屏的X/Y轴可能和屏幕显示方向不一致。我一般会在
read_cb里做一次坐标变换,而不是改LVGL的显示方向。 - 多点触摸:LVGL原生支持单点触摸。如果你用多点触摸屏,需要在
read_cb里只上报第一个触摸点,或者自己实现手势识别。 - 按键重复:长按按键时,LVGL会自动触发重复事件。你不需要在驱动层做重复逻辑,只需要保证
read_cb正确上报按下/释放状态。
我的经验:调试触摸屏时,我会先用一个简单的测试程序,把原始坐标打印出来,用手指在屏幕上画圈。如果画出来的轨迹是乱的,说明坐标解析有问题。如果轨迹是断的,说明触摸芯片的通信不稳定。
好了,这一章的内容就到这里。输入设备移植,说白了就是「硬件数据 → read_cb → LVGL事件」这条链路。只要链路通了,剩下的就是调参和优化。