4、输入设备驱动移植:触摸屏(I2C/SPI)与物理按键的适配、lv_port_indev.c的配置、坐标校准与事件上报
好,这一章我们来聊聊输入设备。说白了,就是怎么让LVGL“摸得着”、“按得到”。
我刚开始做嵌入式UI那会儿,觉得显示搞定就万事大吉了。结果一上电,界面是出来了,但点不动、按不了——那叫一个尴尬。后来才明白,输入设备驱动移植,才是真正让UI“活起来”的关键一步。
这一章,我会把触摸屏(I2C/SPI)和物理按键的适配讲透。包括 lv_port_indev.c 怎么配、坐标怎么校准、事件怎么上报。嗯,都是实战中踩过的坑。
4.1 输入设备的两种主流方案
在低内存MCU上,常见的输入设备就两种:
- 触摸屏:通过I2C或SPI接口读取触摸坐标。适合做交互复杂的界面。
- 物理按键:通过GPIO检测电平变化。适合做菜单选择、确认返回。
我个人习惯是:如果屏幕大于2.8寸,优先用触摸屏。如果屏幕很小(比如1.3寸以下),物理按键更靠谱。为什么?小屏上手指一按,半个屏幕都挡住了,用户体验很差。
4.2 触摸屏驱动移植(以I2C为例)
触摸屏的驱动芯片很多,比如FT6236、GT911、CST816S。它们底层协议大同小异,核心就是:
- 初始化触摸芯片(配置寄存器)
- 周期性读取触摸坐标
- 上报给LVGL
这里我以I2C接口的FT6236为例,给你看一个精简的驱动框架。
4.2.1 硬件初始化
// 触摸芯片初始化
void touch_init(void)
{
// 1. 初始化I2C外设
i2c_init(I2C_PORT, 400000); // 400kHz标准速率
// 2. 复位触摸芯片(如果有RST引脚)
gpio_set_level(RST_PIN, 0);
delay_ms(10);
gpio_set_level(RST_PIN, 1);
delay_ms(50);
// 3. 读取芯片ID,确认通信正常
uint8_t chip_id = 0;
i2c_read_reg(FT6236_ADDR, 0xA8, &chip_id, 1);
if (chip_id != 0x11) {
printf("Touch chip not found!\n");
// 这里可以加错误处理
}
}
我在项目中遇到过一个问题:I2C速率设得太高(超过400kHz),触摸芯片偶尔不响应。后来老老实实降到400kHz,再没出过事。嗯,有些芯片真的不能超频。
4.2.2 读取触摸坐标
// 读取触摸点
uint8_t touch_read_point(lv_coord_t *x, lv_coord_t *y)
{
uint8_t buf[6] = {0};
// 读取6个字节:状态 + 坐标
i2c_read_regs(FT6236_ADDR, 0x02, buf, 6);
// 解析触摸点数
uint8_t touch_count = buf[0] & 0x0F;
if (touch_count == 0) {
return 0; // 没有触摸
}
// 解析第一个触摸点的坐标
*x = ((buf[1] & 0x0F) << 8) | buf[2];
*y = ((buf[3] & 0x0F) << 8) | buf[4];
return 1; // 有触摸
}
你想想看,为什么这里要读6个字节?因为FT6236支持最多2点触摸,每个点占3字节。我们只取第一个点就够了。
4.3 物理按键适配
物理按键就简单多了。核心是:
- 检测GPIO电平变化
- 消抖处理
- 映射成LVGL的按键事件
// 按键初始化
void btn_init(void)
{
gpio_set_direction(BTN_UP_PIN, GPIO_MODE_INPUT);
gpio_set_pull_mode(BTN_UP_PIN, GPIO_PULLUP_ONLY);
gpio_set_direction(BTN_DOWN_PIN, GPIO_MODE_INPUT);
gpio_set_pull_mode(BTN_DOWN_PIN, GPIO_PULLUP_ONLY);
gpio_set_direction(BTN_ENTER_PIN, GPIO_MODE_INPUT);
gpio_set_pull_mode(BTN_ENTER_PIN, GPIO_PULLUP_ONLY);
}
// 读取按键状态(带消抖)
uint8_t btn_read(uint8_t pin)
{
if (gpio_get_level(pin) == 0) { // 按下(低电平有效)
delay_ms(20); // 消抖
if (gpio_get_level(pin) == 0) {
return 1; // 确认按下
}
}
return 0;
}
我曾经犯过一个低级错误:消抖延时写成了200ms。结果按一下按键,界面跳了两下。后来改成20ms,刚刚好。消抖时间别太长,否则用户会觉得“这按键反应好慢”。
4.4 lv_port_indev.c 的配置
这是LVGL输入设备适配的核心文件。说白了,就是告诉LVGL:你的输入设备长什么样、怎么读数据。
4.4.1 触摸屏适配
// 触摸屏初始化回调
static void touchpad_init(void)
{
touch_init();
}
// 触摸屏读取回调
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;
lv_coord_t x, y;
uint8_t touched = touch_read_point(&x, &y);
if (touched) {
// 坐标校准(后面会讲)
data->point.x = calibrate_x(x);
data->point.y = calibrate_y(y);
data->state = LV_INDEV_STATE_PR;
last_x = data->point.x;
last_y = data->point.y;
} else {
data->point.x = last_x;
data->point.y = last_y;
data->state = LV_INDEV_STATE_REL;
}
}
// 注册触摸屏输入设备
void lv_port_indev_init(void)
{
static 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; // 读取回调
indev_drv.feedback_cb = NULL; // 反馈回调(可选)
lv_indev_t *indev = lv_indev_drv_register(&indev_drv);
}
这里有个细节:data->point.x 和 data->point.y 在没触摸时,最好保持上一次的值。为什么?因为LVGL内部会做“最后位置”的判断,如果突然变成(0,0),可能会触发意外的点击事件。
4.4.2 物理按键适配
// 按键读取回调
static void btn_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
// 检测哪个按键被按下
if (btn_read(BTN_UP_PIN)) {
data->key = LV_KEY_UP;
data->state = LV_INDEV_STATE_PR;
} else if (btn_read(BTN_DOWN_PIN)) {
data->key = LV_KEY_DOWN;
data->state = LV_INDEV_STATE_PR;
} else if (btn_read(BTN_ENTER_PIN)) {
data->key = LV_KEY_ENTER;
data->state = LV_INDEV_STATE_PR;
} else {
data->state = LV_INDEV_STATE_REL;
}
}
// 注册按键输入设备
void lv_port_indev_keypad_init(void)
{
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_KEYPAD; // 键盘类型
indev_drv.read_cb = btn_read_cb;
lv_indev_t *indev = lv_indev_drv_register(&indev_drv);
}
4.5 坐标校准
触摸屏的原始坐标和屏幕显示坐标,往往不是一一对应的。原因很多:
- 触摸屏和LCD贴合有偏差
- ADC采样有非线性
- 屏幕旋转后坐标轴变了
最简单的校准方法:两点校准法。
// 校准参数(通过两点校准计算得出)
#define TOUCH_MIN_X 200
#define TOUCH_MAX_X 3800
#define TOUCH_MIN_Y 300
#define TOUCH_MAX_Y 3700
#define LCD_WIDTH 320
#define LCD_HEIGHT 240
// X坐标校准
static lv_coord_t calibrate_x(uint16_t raw_x)
{
// 线性映射
int32_t x = (int32_t)(raw_x - TOUCH_MIN_X) * LCD_WIDTH;
x = x / (TOUCH_MAX_X - TOUCH_MIN_X);
// 边界裁剪
if (x < 0) x = 0;
if (x >= LCD_WIDTH) x = LCD_WIDTH - 1;
return (lv_coord_t)x;
}
// Y坐标校准
static lv_coord_t calibrate_y(uint16_t raw_y)
{
int32_t y = (int32_t)(raw_y - TOUCH_MIN_Y) * LCD_HEIGHT;
y = y / (TOUCH_MAX_Y - TOUCH_MIN_Y);
if (y < 0) y = 0;
if (y >= LCD_HEIGHT) y = LCD_HEIGHT - 1;
return (lv_coord_t)y;
}
4.6 事件上报机制
LVGL的事件上报,其实就两个状态:
LV_INDEV_STATE_PR:按下LV_INDEV_STATE_REL:释放
触摸屏上报的是坐标+状态。按键上报的是键值+状态。
LVGL内部会做事件处理:
- 按下时,触发
LV_EVENT_PRESSING - 释放时,触发
LV_EVENT_RELEASED - 如果按下后短时间内释放,触发
LV_EVENT_SHORT_CLICKED - 如果按下后长时间不释放,触发
LV_EVENT_LONG_PRESSED
你不需要自己实现这些逻辑。LVGL都帮你做好了。你只需要保证:
- 按下时,
data->state = LV_INDEV_STATE_PR - 释放时,
data->state = LV_INDEV_STATE_REL - 坐标数据准确
就这么简单。
4.7 避坑指南
最后,分享几个我踩过的坑:
- 触摸中断不要放在回调里:我曾经把触摸读取放在GPIO中断里,结果中断频率太高,系统直接卡死。正确的做法是:在LVGL的
lv_tick_handler或者主循环里轮询。 - I2C通信要加超时:触摸芯片偶尔会挂死。加个超时机制,超时后重新初始化I2C,能大大提高稳定性。
- 物理按键不要用外部中断:低内存MCU的中断资源很宝贵。用轮询+消抖,完全够用。
- 校准参数要留余量:触摸边缘的坐标可能不太准。校准后,留出5%的边界余量,用户体验会好很多。
好了,这一章就到这里。下一章我们会讲LVGL的显示驱动优化——怎么让界面跑得更流畅。嗯,那才是真正考验功底的地方。