帧缓冲入门:Linux Framebuffer设备、mmap映射与直接像素操作

说实话,很多做嵌入式显示的朋友,一开始就被「帧缓冲」这三个字唬住了。我当年第一次接触 LCD 驱动时,也觉得这东西玄乎。后来发现,说白了它就是一块内存区域,你把颜色数据填进去,硬件自动把内容刷到屏幕上。

这一章,我们就把它彻底拆开看。

什么是 Linux Framebuffer 设备?

Framebuffer(帧缓冲)是 Linux 内核提供的一个抽象层。它把显示设备模拟成一个普通的字符设备,路径通常是 /dev/fb0。你往这个设备里写数据,屏幕上就会显示对应的内容。

我习惯把它理解成「显存的门卫」。应用程序不需要知道底层用的是 SPI、RGB 还是 MIPI 接口,只要通过 ioctl 和 mmap 操作这个设备文件,就能控制屏幕。

核心概念:Framebuffer 设备本质上是一段连续的物理内存,映射到用户空间后,应用程序可以直接读写像素数据。

设备节点与基本操作

先看看系统里有没有这个设备:

ls -l /dev/fb*

通常你会看到 /dev/fb0。如果有多个屏幕,可能会有 fb1、fb2。

获取设备信息,需要用 ioctl 调用 FBIOGET_VSCREENINFO。这个结构体里包含了分辨率、像素格式、行宽等关键参数。

#include <linux/fb.h>
#include <sys/ioctl.h>
#include <fcntl.h>

int fd = open("/dev/fb0", O_RDWR);
struct fb_var_screeninfo vinfo;
ioctl(fd, FBIOGET_VSCREENINFO, &vinfo);

printf("分辨率: %d x %d\n", vinfo.xres, vinfo.yres);
printf("像素位深: %d\n", vinfo.bits_per_pixel);

嗯,这里要注意:fb_var_screeninfo 是可变参数,你甚至可以修改它来切换分辨率。但实际项目中,我建议先读出来,别急着改——改坏了可能屏幕直接黑掉。

mmap 映射:把显存变成你的数组

直接 read/write 操作 /dev/fb0 效率太低。正确的做法是用 mmap 把显存映射到进程的虚拟地址空间。

为什么?因为 mmap 省去了内核态到用户态的数据拷贝。你想想看,每帧 800x480 的 RGB565 图像,一帧就将近 800KB。如果每次都用 write 系统调用,性能根本扛不住。

// 计算显存大小
size_t screensize = vinfo.xres * vinfo.yres * vinfo.bits_per_pixel / 8;

// mmap 映射
char *fbp = (char *)mmap(0, screensize, 
                         PROT_READ | PROT_WRITE, 
                         MAP_SHARED, 
                         fd, 0);

if (fbp == MAP_FAILED) {
    perror("mmap failed");
    close(fd);
    return -1;
}

// 现在 fbp 就是显存指针,直接当数组用
// 例如:把屏幕左上角第一个像素设为红色(RGB565格式)
*(unsigned short *)fbp = 0xF800;

我曾经踩过的坑:mmap 的 MAP_SHARED 标志不能写成 MAP_PRIVATE。否则你写的数据只在进程内部生效,屏幕不会有任何变化。这个 bug 我调了整整一个下午。

直接像素操作:RGB565 与 ARGB8888

映射完成后,剩下的就是像素格式的问题了。嵌入式设备上最常见的两种格式:

格式 位深 布局 常见场景
RGB565 16 bpp RRRRR GGGGGG BBBBB 低成本 MCU、小尺寸屏
ARGB8888 32 bpp AAAAAAA RRRRRRRR GGGGGGGG BBBBBBBB 高性能 SoC、Android 设备

我个人习惯在项目初始化时,先打印 vinfo.bits_per_pixel 和 vinfo.red.offset 等字段,确认硬件实际配置。因为有些驱动会「说谎」——明明设了 RGB565,实际偏移量却不对。

画一个像素点的通用写法:

void draw_pixel(char *fbp, int x, int y, 
                unsigned int color, int bpp) {
    // 计算偏移:每行可能有对齐填充,要用 line_length
    long offset = y * vinfo.line_length + x * bpp / 8;
    
    switch (bpp) {
        case 16: {
            // RGB565
            unsigned short c = 
                ((color >> 19) & 0x1F) << 11 |  // R
                ((color >> 10) & 0x3F) << 5  |  // G
                ((color >> 3)  & 0x1F);            // B
            *(unsigned short *)(fbp + offset) = c;
            break;
        }
        case 32: {
            // ARGB8888
            *(unsigned int *)(fbp + offset) = color;
            break;
        }
    }
}

小技巧:vinfo.line_length 不一定等于 xres * bpp / 8。很多硬件会按 4 字节或 8 字节对齐每行。用 line_length 计算偏移,比手动算更安全。

Framebuffer 操作全流程

我把整个流程总结成一张图,方便你对照理解:

Framebuffer 操作全流程 open("/dev/fb0") ioctl(FBIOGET_VSCREENINFO) mmap(MAP_SHARED) 直接读写 fbp 指针 munmap + close 注意事项 • 必须 O_RDWR 打开 • 先读 vinfo,再 mmap • 用 line_length 算偏移 常见像素格式 • RGB565: 16bpp • ARGB8888: 32bpp

一个完整的画矩形示例

把上面的知识点串起来,写一个填充矩形的函数:

void fill_rect(char *fbp, int x, int y, 
               int w, int h, unsigned int color) {
    int bpp = vinfo.bits_per_pixel;
    int line_len = vinfo.line_length;
    
    for (int row = 0; row < h; row++) {
        for (int col = 0; col < w; col++) {
            long offset = (y + row) * line_len + 
                          (x + col) * bpp / 8;
            
            if (bpp == 16) {
                unsigned short c = 
                    ((color >> 19) & 0x1F) << 11 |
                    ((color >> 10) & 0x3F) << 5  |
                    ((color >> 3)  & 0x1F);
                *(unsigned short *)(fbp + offset) = c;
            } else if (bpp == 32) {
                *(unsigned int *)(fbp + offset) = color;
            }
        }
    }
}

调用方式:

// 在屏幕中央画一个 100x100 的红色方块
fill_rect(fbp, 
          (vinfo.xres - 100) / 2, 
          (vinfo.yres - 100) / 2, 
          100, 100, 0xFF0000);

性能提示:上面的双重循环在画大块区域时效率不高。实际项目中,我会用 memset 或 memcpy 按行填充,速度能快 5-10 倍。但初学阶段,先理解像素操作逻辑更重要。

避坑指南

  • 权限问题:/dev/fb0 通常属于 root:video 组。如果你不是 root,记得把用户加到 video 组里。
  • 屏幕闪烁:直接往显存写数据,如果写入速度跟不上刷新率,会出现撕裂。这就是后面章节要讲的双缓冲解决的问题。
  • 字节序:ARM 平台通常是小端模式。RGB565 的 0xF800 在内存里是 0x00 0xF8,写的时候要注意。

嗯,这一章的内容就到这里。Framebuffer 是嵌入式图形开发的基石,把 mmap 和像素格式搞明白,后面的双缓冲、多图层、硬件加速才能顺利推进。