3. 显示驱动适配基础:framebuffer 原理、显示接口抽象层、注册显示驱动
好,咱们正式开始啃显示驱动这块硬骨头。说实话,很多做LVGL开发的朋友,一开始都卡在驱动适配这一步。明明代码逻辑看着没问题,屏幕就是黑漆漆一片,或者花屏、闪屏。我当年第一次调驱动,整整折腾了两天,最后发现是framebuffer的地址算错了。嗯,这种坑踩过一次,你就再也不会忘了。
这一节,咱们把显示驱动最核心的三个东西讲透:framebuffer是什么、显示接口抽象层怎么设计的、以及怎么把驱动注册进LVGL。搞懂这些,后面30个章节你都会走得很顺。
3.1 framebuffer 原理:说白了就是一块内存画板
framebuffer,帧缓冲。名字听着高大上,其实就是一个内存区域。你把像素数据往里面写,硬件就自动把它刷到屏幕上。你可以把它想象成一个画板——你在画板上画什么,屏幕就显示什么。
framebuffer的核心参数就三个:
| 参数 | 含义 | 我踩过的坑 |
|---|---|---|
| 分辨率 | 宽 × 高(像素) | 宽高搞反,画面直接歪了 |
| 颜色深度 | 每个像素占多少bit | RGB565和RGB888混用,颜色全乱 |
| 行跨度 | 一行实际占用的字节数 | 没对齐DMA,传输效率掉一半 |
举个例子。一个320×240的屏幕,颜色格式用RGB565(每个像素2字节)。那framebuffer的大小就是:
320 × 240 × 2 = 153,600 字节 ≈ 150KB
就这么简单。但实际项目中,我建议你多留一点余量。有些硬件要求行首地址4字节对齐,或者需要双缓冲。我曾经在一个项目里只算了刚好150KB,结果加上双缓冲直接爆内存了。所以,framebuffer的大小,永远按实际需求再加10%的余量。
3.2 显示接口抽象层:LVGL怎么跟你的屏幕对话
LVGL不直接操作硬件。它定义了一套接口,你只要实现这几个函数,它就能用你的屏幕。这套接口就是显示驱动接口(Display Driver Interface)。
核心接口就两个:
flush_cb:把framebuffer里的数据刷到屏幕上。这是最重要的函数。rounder_cb:可选。用来对齐刷新区域,有些硬件要求区域坐标是偶数。
我画了一张图,帮你理解整个数据流向:
你看,整个流程其实很清晰。LVGL核心画图,画完通知驱动去刷。驱动拿到framebuffer里的数据,通过SPI、I2C、或者并行总线发给屏幕硬件。
那flush_cb具体长什么样?我给你看一个最简的实现:
static void my_flush_cb(lv_disp_drv_t * disp_drv,
const lv_area_t * area,
lv_color_t * color_p)
{
// 拿到要刷新的区域和像素数据
int32_t x1 = area->x1;
int32_t y1 = area->y1;
int32_t x2 = area->x2;
int32_t y2 = area->y2;
// 调用硬件接口,把color_p里的数据刷到屏幕上
// 比如用SPI发送
tft_write_rect(x1, y1, x2, y2, color_p);
// 通知LVGL刷新完成
lv_disp_flush_ready(disp_drv);
}
这里有个关键点:一定要调用 lv_disp_flush_ready()。你不调,LVGL就以为还没刷完,会一直等。我见过有人忘了加这行,屏幕死活不显示,查了两天才找到原因。
3.3 注册显示驱动:把驱动和LVGL连起来
接口写好了,怎么告诉LVGL用你的驱动?注册。说白了就是填一个结构体,然后调用注册函数。
LVGL用 lv_disp_drv_t 这个结构体来管理显示驱动。你需要做这几步:
- 初始化结构体:用
lv_disp_drv_init()清零并设置默认值。 - 设置回调函数:把
flush_cb指向你的函数。 - 设置framebuffer:告诉LVGL你的画板在哪。
- 注册驱动:调用
lv_disp_drv_register()。
看代码:
// 1. 定义framebuffer
static lv_color_t buf_1[MY_DISP_HOR_RES * 100];
// 2. 初始化显示驱动
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
// 3. 设置参数
disp_drv.hor_res = 320;
disp_drv.ver_res = 240;
disp_drv.flush_cb = my_flush_cb;
disp_drv.draw_buf = &draw_buf;
// 4. 初始化并绑定draw buffer
static lv_disp_draw_buf_t draw_buf;
lv_disp_draw_buf_init(&draw_buf, buf_1, NULL, MY_DISP_HOR_RES * 100);
// 5. 注册驱动
lv_disp_t * disp = lv_disp_drv_register(&disp_drv);
这里有个细节:draw_buf 的大小。我习惯用 水平分辨率 × 100,也就是10行像素。太小了LVGL频繁刷,效率低;太大了占内存。100行是个不错的平衡点。
3.4 避坑指南:我当年踩过的雷
讲完基础,我分享几个真实踩过的坑。你遇到了可以少走弯路。
- 颜色格式不匹配:LVGL默认用RGB565。如果你的屏幕是RGB888,记得在驱动里做转换。我曾经没转,屏幕显示出来全是紫色调。
- 刷新区域不对齐:有些屏幕硬件要求刷新区域的坐标是偶数。这时候就要实现
rounder_cb,把区域对齐。不然后果可能是花屏。 - 忘记调用
lv_disp_flush_ready():这个我前面提过。再强调一次——不调这个,LVGL会一直等待,UI卡死。 - 中断里调用LVGL函数:不要在硬件中断里直接调LVGL的API。我曾经在SPI传输完成中断里调了
lv_disp_flush_ready(),结果死锁了。正确的做法是用标志位,在主循环里处理。
flush_cb 里加个打印,看看color_p的第一个像素值对不对。如果值是0xFFFF(白色),说明LVGL在画图;如果是0x0000(黑色),说明根本没画。