第二讲:显示驱动移植——framebuffer机制与缓冲区策略
大家好,欢迎来到第二讲。
上一讲我们把LVGL的工程骨架搭好了,但屏幕还是黑的,对吧?这一讲,我们就来点亮它。
显示驱动移植,说白了就是让LVGL知道:你的像素该往哪里画。在Linux嵌入式系统中,最常见的显示接口就是framebuffer。我最早接触framebuffer时也觉得挺神秘,后来发现它其实就是一块内存区域,你把颜色数据填进去,屏幕就亮了。
2.1 理解framebuffer机制
Framebuffer,帧缓冲。名字听着高大上,其实就是一个内存数组。
你想想看,屏幕上一共有多少个像素点?假设是800×480的分辨率,那就是384000个点。每个点用2个字节(RGB565)表示颜色,那么一帧画面就需要768000字节。这块内存,就是framebuffer。
在Linux中,framebuffer被抽象为一个设备文件:/dev/fb0。你只需要:
open("/dev/fb0", O_RDWR)打开设备mmap()把显存映射到用户空间- 往映射的内存里写像素数据
- 屏幕自动刷新(硬件完成)
嗯,就是这么简单。我在项目中遇到过有人问:要不要手动调用刷新函数?其实不用。你写完了显存,LCD控制器会自动把数据推送到屏幕上。当然,有些硬件需要你手动触发刷新,但那是少数情况。
核心要点:framebuffer就是一块内存,你写什么,屏幕显示什么。没有中间商赚差价。
2.2 编写lv_port_disp.c
LVGL的显示驱动接口,核心文件就是lv_port_disp.c。这个文件在LVGL源码的porting目录下,你复制出来改就行。
我们需要实现三个回调函数:
| 回调函数 | 作用 | 说明 |
|---|---|---|
disp_init() |
初始化显示硬件 | 打开fb0、mmap显存 |
disp_flush() |
刷新指定区域 | 把LVGL的绘制缓冲区拷贝到显存 |
rounder_callback() |
对齐刷新区域 | 有些硬件要求坐标对齐到2或4的倍数 |
下面是我常用的代码骨架,你可以直接参考:
// lv_port_disp.c 核心片段
static int fb_fd = -1;
static struct fb_var_screeninfo vinfo;
static struct fb_fix_screeninfo finfo;
static uint8_t *fb_mem = NULL;
void disp_init(void)
{
// 1. 打开framebuffer设备
fb_fd = open("/dev/fb0", O_RDWR);
if (fb_fd < 0) {
perror("open fb0 failed");
return;
}
// 2. 获取屏幕参数
ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo);
ioctl(fb_fd, FBIOGET_FSCREENINFO, &finfo);
// 3. mmap映射显存
fb_mem = mmap(NULL, finfo.smem_len,
PROT_READ | PROT_WRITE, MAP_SHARED,
fb_fd, 0);
if (fb_mem == MAP_FAILED) {
perror("mmap failed");
close(fb_fd);
return;
}
printf("Framebuffer: %dx%d, %dbpp\n",
vinfo.xres, vinfo.yres, vinfo.bits_per_pixel);
}
void disp_flush(lv_disp_drv_t *drv,
const lv_area_t *area,
lv_color_t *color_p)
{
int32_t x, y;
uint32_t fb_offset;
uint32_t line_bytes;
// 计算每行字节数
line_bytes = (area->x2 - area->x1 + 1) * (vinfo.bits_per_pixel / 8);
for (y = area->y1; y <= area->y2; y++) {
fb_offset = (y * finfo.line_length) +
(area->x1 * (vinfo.bits_per_pixel / 8));
memcpy(fb_mem + fb_offset, color_p, line_bytes);
color_p += (area->x2 - area->x1 + 1);
}
// 通知LVGL刷新完成
lv_disp_flush_ready(drv);
}
个人经验:我在调试时经常遇到屏幕花屏,后来发现是line_length和xres * bpp/8不一致导致的。有些硬件为了对齐,每行会多几个字节的padding。一定要用finfo.line_length,别自己算!
2.3 配置颜色深度与缓冲区
颜色深度,就是每个像素用多少位来表示。常见的有:
- RGB565(16位):R占5位,G占6位,B占5位。最常用,性价比高。
- RGB888(32位):每个颜色8位,色彩更丰富,但显存翻倍。
- RGB332(8位):颜色少,基本淘汰了。
在LVGL中,通过lv_conf.h配置:
/* lv_conf.h */
#define LV_COLOR_DEPTH 16 /* 16位RGB565 */
#define LV_COLOR_16_SWAP 0 /* 字节序,有些硬件需要交换高低字节 */
为什么我推荐RGB565?
说白了,就是性能和画质的平衡。800×480的屏幕,RGB565一帧只需要768KB。RGB888要1.5MB,翻了一倍。对于嵌入式设备,内存就是钱啊。
我曾经在一个项目里用了RGB888,结果内存不够,不得不加外挂SDRAM。后来改成RGB565,省了硬件成本,用户也看不出区别。嗯,从那以后我就学乖了。
2.4 双缓冲与全缓冲策略选择
这是显示驱动里最关键的决策点。LVGL支持三种缓冲模式:
| 模式 | 缓冲区大小 | 优点 | 缺点 |
|---|---|---|---|
| 单缓冲 | 1/10屏幕 | 内存占用极小 | 刷新次数多,可能有撕裂 |
| 双缓冲 | 1个屏幕 | 无撕裂,性能好 | 内存占用大 |
| 全缓冲 | 2个屏幕 | 极致流畅 | 内存占用巨大 |
怎么选?我给出我的建议:
- 内存紧张(比如只有8MB RAM):用单缓冲,缓冲区大小设为屏幕高度的1/10。比如480行,就设48行。牺牲一点性能,但能跑起来。
- 内存够用(32MB以上):用双缓冲。这是最推荐的方式。LVGL在一个缓冲区里绘制,同时另一个缓冲区的内容被送到屏幕。互不干扰。
- 土豪配置(64MB以上):全缓冲。两个完整的屏幕缓冲区,LVGL画完一个,直接切换显示。性能最好,但内存翻倍。
配置代码示例:
/* 在lv_port_disp.c中 */
static lv_disp_drv_t disp_drv;
static lv_disp_buf_t disp_buf;
static lv_color_t buf1[LV_HOR_RES_MAX * 100]; /* 100行缓冲区 */
void lv_port_disp_init(void)
{
disp_init(); /* 初始化硬件 */
/* 初始化缓冲区 */
lv_disp_buf_init(&disp_buf, buf1, NULL,
LV_HOR_RES_MAX * 100);
/* 注册驱动 */
lv_disp_drv_init(&disp_drv);
disp_drv.buffer = &disp_buf;
disp_drv.flush_cb = disp_flush;
lv_disp_drv_register(&disp_drv);
}
避坑指南:我曾经在双缓冲模式下踩过一个坑——缓冲区大小设成了整个屏幕,但LVGL的绘制区域是动态的。结果每次刷新都要拷贝全屏数据,性能反而下降了。正确的做法是:缓冲区大小设为屏幕的1/10到1/4,让LVGL分块绘制,这样内存和性能都兼顾。
2.5 性能优化小技巧
最后分享几个我在实际项目中用过的优化手段:
- 使用DMA传输:如果硬件支持,用DMA把缓冲区数据拷贝到显存,CPU可以干别的事。
- 只刷新脏区域:LVGL默认只刷新变化的部分,别全屏刷新。我的
disp_flush函数里只拷贝area指定的区域。 - 调整缓冲区大小:缓冲区越大,刷新次数越少,但内存占用越大。找到平衡点。
- 关闭颜色转换:如果LVGL的颜色格式和framebuffer一致,关闭颜色转换可以省不少CPU。
好了,这一讲的内容就到这里。下一讲我们会深入触摸屏的移植,让仪表盘不仅能看,还能点。到时候见!