3. 开发环境搭建
输入设备驱动移植,说白了就是让LVGL能「感知」到用户的操作。触摸屏点一下、按键按一下,这些物理信号怎么变成LVGL能理解的事件?这就是本章要解决的问题。
我刚开始做LVGL移植时,觉得输入设备很简单——不就是读个坐标嘛。结果踩了不少坑。比如触摸屏上报坐标的时序不对,导致界面点击无响应;按键的抖动没处理好,一次按下触发了两次事件。嗯,这些坑咱们今天一个个填上。
核心要点:LVGL的输入设备驱动模型是统一的——无论触摸屏还是按键,最终都通过 indev_read_cb 回调函数上报数据。你只需要实现这个回调,LVGL就会自动处理后续的事件分发和响应。
3.1 输入设备驱动模型:统一抽象层
LVGL把输入设备抽象成几个类型:触摸屏(指针设备)、按键(键盘)、编码器、按钮矩阵等。每种类型都有对应的数据结构,但注册流程几乎一样。
我个人习惯把驱动模型分成三层:
- 硬件层:直接操作寄存器或调用HAL库函数,读取物理信号
- 适配层:将硬件数据转换成LVGL需要的格式(坐标、按键码)
- 注册层:调用
lv_indev_drv_register()将设备注册到LVGL
你想想看,这种分层设计的好处是什么?换硬件平台时,只需要改硬件层,适配层和注册层基本不用动。我在一个项目里从FT5x06触摸芯片换成GT911,只改了硬件层的I2C读写函数,其他代码直接复用。
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的事件处理流程大致是:
- 输入读取:LVGL调用
indev_read_cb获取当前输入状态 - 命中检测:判断触摸点落在哪个对象上(或按键焦点在哪个对象上)
- 事件生成:根据输入状态变化生成
LV_EVENT_PRESSED、LV_EVENT_CLICKED等事件 - 事件分发:将事件发送给目标对象的事件回调
举个例子,你点击了一个按钮:
- 手指按下 → 生成
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就能「听懂」用户的操作了。