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 其实不复杂。我带你走一遍流程:

  1. 打开设备文件:通常是 /dev/fb0。用 open() 系统调用。
  2. 获取固定信息:通过 ioctl() 调用 FBIOGET_FSCREENINFO,获取显存大小、起始地址等。
  3. 获取可变信息:通过 ioctl() 调用 FBIOGET_VSCREENINFO,获取分辨率、位深、像素格式等。
  4. 设置可变信息:如果需要修改分辨率或位深,用 FBIOPUT_VSCREENINFO 设置。
  5. 映射显存:用 mmap() 把显存映射到用户空间。
  6. 开始绘制:直接往映射后的内存地址写数据。

嗯,这里要注意: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) 接口。流程如下:

  1. 打开 DRM 设备:通常是 /dev/dri/card0
  2. 分配 Dumb Buffer:用 DRM_IOCTL_MODE_CREATE_DUMB 分配一块显存。
  3. 获取句柄:分配成功后,会返回一个 handle。
  4. 获取偏移量:用 DRM_IOCTL_MODE_MAP_DUMB 获取显存的偏移量。
  5. 映射到用户空间:用 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 缓存,它写数据时可能只写到了缓存里,还没刷到主存。这时候显示控制器去读显存,读到的可能是旧数据。

结果就是:你明明写了红色像素,屏幕上显示的却是绿色。或者画面出现撕裂。

解决办法有两个:

  1. 使用 MAP_SHARED 标志:这个标志告诉内核,这块内存可能被多个进程或设备访问。内核会帮你处理缓存同步。
  2. 手动刷缓存:用 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 的地方。

好了,这一章就到这里。下一章我们会讲更高级的显示技术——双缓冲与页面翻转。到时候你会看到,怎么让画面切换更流畅,不撕裂。