帧缓冲入门: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 操作全流程
我把整个流程总结成一张图,方便你对照理解:
一个完整的画矩形示例
把上面的知识点串起来,写一个填充矩形的函数:
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 和像素格式搞明白,后面的双缓冲、多图层、硬件加速才能顺利推进。