4、Framebuffer与Dumb Buffer:Framebuffer概念与创建流程、Dumb Buffer分配与映射、CPU直接操作显存、mmap与缓存一致性处理
好,咱们今天聊点实在的。Framebuffer 和 Dumb Buffer,这两个词你肯定不陌生。说白了,它们就是 CPU 和显示器之间的「快递员」。CPU 把画面数据打包好,交给它们,显示器那边就能看到图像了。
我刚开始做嵌入式图形驱动时,总觉得 Framebuffer 就是个简单的内存块。后来踩了不少坑才明白,这里面的门道可不少。尤其是 Dumb Buffer,名字听着挺「笨」,但用好了其实很灵活。
4.1 Framebuffer 是什么?
Framebuffer,帧缓冲。你可以把它想象成一块画布。CPU 在这块画布上画东西,显示器就照着画布的内容刷新屏幕。
在 Linux 系统中,Framebuffer 是一个内核提供的抽象层。它把显存映射到用户空间,让应用程序可以直接读写。这样你就不需要关心底层显卡的细节了。
我个人的习惯是,把 Framebuffer 理解成「显存的门卫」。它不负责具体怎么画,只负责把数据送到该去的地方。
核心要点:Framebuffer 是显存与用户空间之间的桥梁。它屏蔽了硬件差异,让开发者用统一的方式操作显示设备。
4.2 Framebuffer 的创建流程
创建 Framebuffer 其实不复杂。我带你走一遍流程:
- 打开设备文件:通常是
/dev/fb0。用open()系统调用。 - 获取固定信息:通过
ioctl()调用FBIOGET_FSCREENINFO,获取显存大小、起始地址等。 - 获取可变信息:通过
ioctl()调用FBIOGET_VSCREENINFO,获取分辨率、位深、像素格式等。 - 设置可变信息:如果需要修改分辨率或位深,用
FBIOPUT_VSCREENINFO设置。 - 映射显存:用
mmap()把显存映射到用户空间。 - 开始绘制:直接往映射后的内存地址写数据。
嗯,这里要注意:mmap() 的返回值一定要检查。我遇到过好几次映射失败,结果程序直接段错误。那叫一个惨。
#include <stdio.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <sys/mman.h>
int main() {
int fbfd = open("/dev/fb0", O_RDWR);
if (fbfd == -1) {
perror("Error: cannot open framebuffer device");
return 1;
}
struct fb_var_screeninfo vinfo;
if (ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo)) {
perror("Error reading variable information");
close(fbfd);
return 1;
}
// 计算显存大小
long screensize = vinfo.xres * vinfo.yres * vinfo.bits_per_pixel / 8;
// 映射显存
char *fbp = (char *)mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0);
if ((int)fbp == -1) {
perror("Error: failed to map framebuffer device to memory");
close(fbfd);
return 1;
}
// 画一个红色像素
fbp[0] = 0x00; // 蓝色分量
fbp[1] = 0x00; // 绿色分量
fbp[2] = 0xFF; // 红色分量
munmap(fbp, screensize);
close(fbfd);
return 0;
}
小技巧:我习惯在 mmap() 之后先 memset 整个显存为黑色。这样能避免启动时屏幕出现花屏。你想想看,用户第一眼看到的是乱码,印象分直接归零。
4.3 Dumb Buffer 是什么?
Dumb Buffer,直译就是「笨缓冲」。为什么叫笨?因为它真的啥也不干。它只是一块连续的内存区域,没有硬件加速,没有复杂的渲染管线。
但你别小看它。Dumb Buffer 在嵌入式系统中非常实用。尤其是当你只需要显示静态图像或简单 UI 时,它比复杂的 GPU 驱动要轻量得多。
我曾经在一个低功耗的 IoT 设备上做显示驱动。硬件只有基本的显示控制器,没有 GPU。Dumb Buffer 就成了我的救命稻草。说白了,它就是一块「裸显存」,CPU 直接往上写数据就行。
4.4 Dumb Buffer 的分配与映射
分配 Dumb Buffer 需要走 DRM (Direct Rendering Manager) 接口。流程如下:
- 打开 DRM 设备:通常是
/dev/dri/card0。 - 分配 Dumb Buffer:用
DRM_IOCTL_MODE_CREATE_DUMB分配一块显存。 - 获取句柄:分配成功后,会返回一个 handle。
- 获取偏移量:用
DRM_IOCTL_MODE_MAP_DUMB获取显存的偏移量。 - 映射到用户空间:用
mmap()把偏移量映射到用户空间。
你看,和 Framebuffer 的流程很像。但 Dumb Buffer 更灵活,你可以分配多块,每块用于不同的用途。
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
int main() {
int fd = open("/dev/dri/card0", O_RDWR);
if (fd < 0) {
perror("Cannot open DRM device");
return 1;
}
// 分配 Dumb Buffer
struct drm_mode_create_dumb create = {0};
create.width = 1024;
create.height = 768;
create.bpp = 32;
int ret = drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create);
if (ret) {
perror("Cannot create dumb buffer");
close(fd);
return 1;
}
// 获取映射偏移量
struct drm_mode_map_dumb map = {0};
map.handle = create.handle;
ret = drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map);
if (ret) {
perror("Cannot map dumb buffer");
close(fd);
return 1;
}
// 映射到用户空间
size_t size = create.size;
uint32_t *fbp = mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, map.offset);
if (fbp == MAP_FAILED) {
perror("Cannot mmap dumb buffer");
close(fd);
return 1;
}
// 画一个蓝色像素
fbp[0] = 0xFF0000FF; // ARGB 格式
munmap(fbp, size);
close(fd);
return 0;
}
注意:Dumb Buffer 分配后一定要检查 create.size。不同驱动返回的大小可能不一样。我曾经遇到过分配 1024x768 的缓冲区,结果返回的大小比预期多了几行。后来才发现是驱动做了对齐。
4.5 CPU 直接操作显存
CPU 直接操作显存,听起来很暴力。但这就是嵌入式开发的常态。没有 GPU 加速,所有像素都得靠 CPU 一个一个填。
操作方式很简单:拿到映射后的地址,直接写数据。比如你想在屏幕上画一个渐变矩形:
// 假设 fbp 是映射后的地址,vinfo 是可变信息
int x, y;
for (y = 0; y < vinfo.yres; y++) {
for (x = 0; x < vinfo.xres; x++) {
long offset = (x + y * vinfo.xres) * (vinfo.bits_per_pixel / 8);
fbp[offset] = x & 0xFF; // 蓝色渐变
fbp[offset + 1] = y & 0xFF; // 绿色渐变
fbp[offset + 2] = 0x80; // 固定红色
}
}
嗯,这里有个坑。你想想看,如果分辨率是 1920x1080,32 位色,那显存大小就是 1920 * 1080 * 4 ≈ 8MB。CPU 要逐像素操作 8MB 的数据,速度能快吗?
我建议你只在以下场景用 CPU 直接操作:
- 分辨率较低(如 800x480)
- 画面更新频率低(如静态 UI)
- 调试阶段(快速验证)
如果要做视频播放或游戏,还是老老实实用 GPU 吧。
4.6 mmap 与缓存一致性处理
这是个大坑。我花了好几天才搞明白。
CPU 和 GPU(或显示控制器)访问同一块显存时,会涉及缓存一致性问题。CPU 有 L1、L2 缓存,它写数据时可能只写到了缓存里,还没刷到主存。这时候显示控制器去读显存,读到的可能是旧数据。
结果就是:你明明写了红色像素,屏幕上显示的却是绿色。或者画面出现撕裂。
解决办法有两个:
- 使用
MAP_SHARED标志:这个标志告诉内核,这块内存可能被多个进程或设备访问。内核会帮你处理缓存同步。 - 手动刷缓存:用
msync()或dma_buf_sync()强制刷新缓存。
// 手动刷缓存
msync(fbp, screensize, MS_SYNC);
我曾经在一个项目中,画面总是出现奇怪的条纹。查了两天,最后发现是 mmap() 时用了 MAP_PRIVATE 而不是 MAP_SHARED。改了之后问题立刻消失。嗯,从那以后我再也不敢用错标志了。
避坑指南:如果你发现画面更新后显示异常,先检查 mmap() 的 flags 参数。一定要用 MAP_SHARED。另外,多核 CPU 上还要考虑内存屏障,用 __sync_synchronize() 或 atomic_thread_fence() 保证顺序。
4.7 总结与建议
Framebuffer 和 Dumb Buffer 是嵌入式图形开发的基础。说白了,它们就是让你能用 CPU 直接画屏幕的工具。
我个人建议你:
- 新手先从 Framebuffer 入手,它更简单,文档也多。
- 需要多缓冲或复杂显示时,切换到 Dumb Buffer + DRM。
- 始终注意缓存一致性问题,这是最容易出 bug 的地方。
好了,这一章就到这里。下一章我们会讲更高级的显示技术——双缓冲与页面翻转。到时候你会看到,怎么让画面切换更流畅,不撕裂。