3、显示驱动与触摸驱动:理解LVGL的显示驱动接口(lv_disp_drv_t)、实现像素点绘制函数、实现触摸驱动接口(lv_indev_drv_t)、调试驱动确保画面正常。
好,我们直接进入正题。这一章,我们要让LVGL真正“跑”起来。
前面两章,你装好了环境,也跑了个模拟器。但模拟器终究是模拟器。真正的嵌入式开发,你得跟硬件打交道。说白了,就是让LVGL知道:你的屏幕长什么样,怎么往上面画点,以及用户用手指戳屏幕时,坐标是多少。
这就是显示驱动和触摸驱动的活儿。
我个人习惯,先把这两个接口搞定,再谈别的。因为画面出不来,后面全是白搭。我记得刚入行那会儿,有一次调一个4.3寸屏,折腾了两天,最后发现是像素格式配错了。嗯,这种坑,咱们今天一并填上。
核心要点: LVGL的驱动层是硬件和UI引擎之间的“翻译官”。显示驱动负责把内存里的像素数据“搬运”到屏幕上;触摸驱动负责把物理触摸信号“翻译”成LVGL能理解的坐标事件。
3.1 显示驱动接口:lv_disp_drv_t 的结构与配置
先看显示驱动。LVGL里,所有跟显示相关的配置,都封装在一个结构体里——lv_disp_drv_t。你可以把它想象成一张“显示器身份证”。
这张身份证上要填什么?主要就三样:
- 屏幕尺寸:宽和高,单位是像素。
- 像素格式:比如RGB565、RGB888。这决定了每个像素占几个字节。
- 绘制回调函数:这是核心。LVGL画图时,最终会调用你提供的这个函数,把数据刷到屏幕上。
来看一段典型的初始化代码:
/* 定义一个显示缓冲区 */
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[LV_HOR_RES_MAX * 10]; /* 10行像素的缓冲区 */
/* 定义一个显示驱动结构体 */
static lv_disp_drv_t disp_drv;
void my_display_init(void)
{
/* 1. 初始化显示缓冲区 */
lv_disp_draw_buf_init(&draw_buf, buf, NULL, LV_HOR_RES_MAX * 10);
/* 2. 初始化显示驱动 */
lv_disp_drv_init(&disp_drv);
/* 3. 填写关键参数 */
disp_drv.hor_res = 480; /* 屏幕宽度 */
disp_drv.ver_res = 272; /* 屏幕高度 */
disp_drv.flush_cb = my_display_flush; /* 绘制回调函数 */
disp_drv.draw_buf = &draw_buf; /* 绑定缓冲区 */
/* 4. 注册驱动到LVGL */
lv_disp_drv_register(&disp_drv);
}
这里有个细节:draw_buf。LVGL不是直接往屏幕硬件写像素的。它先画到内存缓冲区里,画满一行或几行后,再一次性通过flush_cb刷出去。这种“双缓冲”机制,能有效避免屏幕闪烁。
我的经验: 缓冲区大小,建议至少是屏幕一行像素的10倍。太小了,LVGL频繁刷屏,CPU占用高;太大了,内存吃不消。对于320x240的屏,用10行缓冲区,也就是320*10*2=6400字节(RGB565),刚刚好。
3.2 实现像素点绘制函数:从缓冲区到屏幕
好,接下来是实现my_display_flush这个回调。这是整个驱动里最核心、也最容易出错的地方。
这个函数的原型是固定的:
void my_display_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p);
参数解释一下:
drv:指向当前显示驱动结构体的指针。area:需要刷新的矩形区域。包含左上角(x1, y1)和右下角(x2, y2)坐标。color_p:指向像素数据缓冲区的指针。数据是按行排列的。
说白了,LVGL告诉你:“从坐标(x1,y1)到(x2,y2)这个矩形,像素数据已经放在color_p里了,你帮我写到屏幕对应的位置上去。”
实现起来,通常就是两层循环:
void my_display_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p)
{
int32_t x, y;
uint32_t width = lv_area_get_width(area);
uint32_t height = lv_area_get_height(area);
/* 假设你的屏幕驱动芯片是ILI9341,通过SPI接口写入 */
ili9341_set_window(area->x1, area->y1, area->x2, area->y2);
for (y = area->y1; y <= area->y2; y++) {
for (x = area->x1; x <= area->x2; x++) {
/* 从缓冲区取出一个像素,写入屏幕 */
ili9341_write_pixel(color_p->full);
color_p++;
}
}
/* 重要!通知LVGL刷新完成 */
lv_disp_flush_ready(drv);
}
注意: 最后一定要调用 lv_disp_flush_ready(drv)。如果不调,LVGL会认为屏幕还在忙,不会再发新的绘制任务。我曾经因为这个函数漏掉了,画面卡死了一整天,排查得头都大了。
另外,如果你的屏幕控制器支持“批量写像素”指令(比如ILI9341的0x2C命令),千万别用逐像素写入。改成DMA或连续写入,速度能快10倍以上。我一般这么优化:
void my_display_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p)
{
uint32_t size = lv_area_get_width(area) * lv_area_get_height(area);
/* 设置窗口 */
ili9341_set_window(area->x1, area->y1, area->x2, area->y2);
/* 批量写入,一次发完所有像素数据 */
ili9341_write_pixels((uint16_t *)color_p, size);
lv_disp_flush_ready(drv);
}
你看,代码简洁多了,效率也高。
3.3 触摸驱动接口:lv_indev_drv_t 的实现
画面能显示了,接下来得让用户能交互。触摸驱动,就是干这个的。
LVGL里,输入设备(触摸屏、按键、编码器)都统一用lv_indev_drv_t来管理。触摸屏只是其中一种。
初始化流程跟显示驱动很像:
static lv_indev_drv_t indev_drv;
void my_touch_init(void)
{
/* 初始化触摸芯片(比如FT6236、GT911) */
touch_hw_init();
/* 初始化输入设备驱动 */
lv_indev_drv_init(&indev_drv);
/* 类型:触摸屏 */
indev_drv.type = LV_INDEV_TYPE_POINTER;
/* 读取回调:LVGL会周期性调用这个函数获取触摸状态 */
indev_drv.read_cb = my_touch_read;
/* 注册驱动 */
lv_indev_drv_register(&indev_drv);
}
关键在my_touch_read这个回调里。它的任务是:告诉LVGL“当前有没有手指按下?按在哪里?”
void my_touch_read(lv_indev_drv_t * drv, lv_indev_data_t * data)
{
static int16_t last_x = 0;
static int16_t last_y = 0;
static lv_indev_state_t last_state = LV_INDEV_STATE_REL;
/* 从硬件读取触摸坐标和状态 */
uint8_t touch_points = touch_get_point(&last_x, &last_y);
if (touch_points > 0) {
data->point.x = last_x;
data->point.y = last_y;
data->state = LV_INDEV_STATE_PR; /* 按下 */
} else {
data->state = LV_INDEV_STATE_REL; /* 释放 */
}
}
这里有个小坑:触摸屏的坐标原点,不一定跟屏幕显示的原点一致。有的触摸屏X轴是反的,有的Y轴是反的。你需要根据实际情况做坐标变换。
我曾经遇到过一块屏,触摸X轴跟显示X轴是镜像关系。调试时点左上角,光标跑到右下角去了。解决办法很简单:data->point.x = 479 - last_x;。嗯,一行代码搞定。
3.4 调试驱动:确保画面正常
驱动写完了,怎么验证对不对?别急着写UI。先跑几个“裸”测试。
第一步:纯色填充测试
直接调用你的flush_cb,往全屏填红、绿、蓝、白、黑。看看颜色对不对,有没有花屏。
/* 手动构造一个全屏区域 */
lv_area_t area;
area.x1 = 0; area.y1 = 0;
area.x2 = 479; area.y2 = 271;
/* 构造一个全红的缓冲区 */
lv_color_t red_buf[480 * 272];
for (int i = 0; i < 480 * 272; i++) {
red_buf[i].full = 0xF800; /* RGB565 红色 */
}
/* 调用flush */
my_display_flush(&disp_drv, &area, red_buf);
如果屏幕全红,说明像素写入没问题。如果出现条纹、错位,检查你的窗口设置和坐标计算。
第二步:LVGL内置测试
LVGL自带了一个lv_demo_widgets(),能显示各种控件。跑一下这个demo,看看按钮、滑块、图表是否正常显示。如果某个控件显示不全,可能是缓冲区太小,或者flush_cb里区域计算有误。
第三步:触摸坐标打印
在my_touch_read里,把读取到的坐标通过串口打印出来。用手指在屏幕上划动,看打印的坐标是否连续、是否跟手指位置对应。如果坐标跳变,检查触摸芯片的滤波配置;如果坐标反向,做坐标变换。
调试口诀: 先显示,后触摸;先纯色,后UI;先单点,后手势。每一步都确认无误了,再往下走。别想着一步到位,那只会让你花更多时间排查。
3.5 知识体系:显示与触摸驱动核心逻辑
为了让你更直观地理解整个流程,我画了一张图。它展示了从LVGL内核到硬件屏幕和触摸芯片的数据流向。
从这张图可以看得很清楚:LVGL内核只跟驱动结构体打交道。显示驱动负责把像素从缓冲区搬到屏幕;触摸驱动负责把物理坐标翻译成逻辑事件。两者独立,互不干扰。
3.6 常见问题与避坑指南
最后,我把这些年遇到过的典型问题列出来。你调试时如果碰到类似现象,直接对照排查。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 屏幕全白/全黑,无任何显示 | 1. 硬件初始化失败 2. 像素格式不匹配 |
1. 检查SPI/I2C通信是否正常 2. 确认 lv_color_t格式与屏幕一致 |
| 画面闪烁 | 显示缓冲区太小 | 增大缓冲区,至少10行像素 |
| 画面撕裂(上半部分和下半部分不同步) | 没有使用双缓冲,或刷新频率不匹配 | 启用LVGL的双缓冲模式,或降低刷新率 |
| 触摸无反应 | 1. 触摸芯片I2C地址错误 2. read_cb未正确返回状态 |
1. 用逻辑分析仪确认I2C通信 2. 确保 data->state正确赋值 |
| 触摸坐标偏移 | 触摸屏与显示屏坐标原点不一致 | 在read_cb中做坐标变换 |
| 触摸坐标跳变 | 触摸芯片未做滤波,或电源噪声大 | 1. 启用触摸芯片内部滤波 2. 在软件中做均值滤波 |
一个小技巧: 调试触摸时,可以在屏幕上画一个十字光标,显示当前触摸点位置。这样你点哪里,光标就跟到哪里,坐标对不对一目了然。LVGL的lv_demo_benchmark()里就有这个功能,你可以直接参考。
嗯,这一章的内容就到这里。显示驱动和触摸驱动,是LVGL和硬件之间的桥梁。桥搭稳了,后面的路就好走了。记住:驱动调试,耐心比技术更重要。每改一个参数,就验证一次。别偷懒,别跳步。