帧缓冲(FrameBuffer)原理:Linux Framebuffer设备、mmap映射、像素格式与颜色深度
好,咱们进入正题。上一章聊了双缓冲的整体思路,这一章得把地基打牢——帧缓冲到底是什么?
说白了,帧缓冲就是一块内存区域。你往里面写像素数据,显示硬件就自动把它刷到屏幕上。听起来简单吧?但实际用起来,坑不少。我最早接触这东西是在一个嵌入式项目上,当时想把摄像头画面直接怼到LCD上,结果屏幕花得一塌糊涂。后来才发现,我对帧缓冲的理解太浅了。
Linux Framebuffer设备
在Linux系统里,帧缓冲被抽象成一个设备文件——/dev/fb0。你打开它,操作它,就像操作一个普通文件。
但这不是普通文件。它背后对应的是显存(或者系统内存里划出来的一块区域)。显示驱动初始化的时候,会注册一个fb_info结构体,里面包含了分辨率、颜色深度、像素格式、显存地址等信息。
我习惯用fbset命令先看一眼当前配置:
# fbset
mode "1024x768-60"
# D: 65.00 MHz, H: 48.36 kHz, V: 60.00 Hz
geometry 1024 768 1024 768 32
timings 15384 168 16 30 2 136 6
accel true
rgba 8/16,8/8,8/0,8/24
endmode
注意geometry那行——1024 768 1024 768 32。前两个是可见分辨率,中间两个是虚拟分辨率(后面讲双缓冲时会用到),最后一个32是颜色深度,也就是每个像素占32位。
关键点:Framebuffer设备本质上是一个字符设备。你用open()打开,用ioctl()获取/设置参数,用mmap()映射显存到用户空间。就这么几个操作,但每个都有讲究。
mmap映射:把显存变成你的数组
为什么需要mmap?因为显存在内核空间,用户程序不能直接访问。你当然可以用read()/write()来读写,但那样效率太低——每次读写都要经过内核缓冲区,画一帧数据要拷贝好几次。
mmap直接把内核空间的显存映射到用户空间的虚拟地址。映射之后,你操作那个指针,就是在操作显存。零拷贝,效率拉满。
来看代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <linux/fb.h>
int main() {
int fbfd = open("/dev/fb0", O_RDWR);
if (fbfd == -1) {
perror("open fb0 failed");
return -1;
}
struct fb_var_screeninfo vinfo;
ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo);
// 计算显存大小
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, fbfd, 0);
if (fbp == MAP_FAILED) {
perror("mmap failed");
close(fbfd);
return -1;
}
// 现在 fbp 就是显存指针,直接写像素
// 比如把屏幕左上角第一个像素设为红色
*(unsigned int *)(fbp) = 0x00FF0000; // 假设是32位ARGB格式
munmap(fbp, screensize);
close(fbfd);
return 0;
}
我的经验:映射的时候一定要用MAP_SHARED,不能用MAP_PRIVATE。后者会创建写时拷贝,你写进去的数据根本到不了显存。我曾经在这个坑里浪费了半天。
像素格式与颜色深度
像素格式决定了内存里的二进制位怎么对应到屏幕上的颜色。常见的有这么几种:
| 颜色深度 | 像素格式 | 每个像素大小 | 常见场景 |
|---|---|---|---|
| 8位 | 索引色(调色板) | 1字节 | 老式VGA、低端MCU |
| 16位 | RGB565 / ARGB1555 | 2字节 | 嵌入式LCD、低成本设备 |
| 24位 | RGB888 | 3字节 | 标准桌面显示 |
| 32位 | ARGB8888 / XRGB8888 | 4字节 | 现代GPU、双缓冲常用 |
32位格式里,每个像素占4字节。通常的排列是:
- ARGB8888:Alpha(8位) + Red(8位) + Green(8位) + Blue(8位)
- XRGB8888:保留位(8位) + Red(8位) + Green(8位) + Blue(8位)
你想想看,如果颜色深度是32位,一帧1024x768的画面需要多少内存?
1024 × 768 × 4 = 3,145,728 字节 ≈ 3 MB
嗯,3MB。如果做双缓冲,就需要6MB。这在今天的硬件上不算什么,但在资源受限的嵌入式设备上,这个数字你得心里有数。
注意:不同硬件平台的像素字节序可能不同。x86是小端,ARM默认也是小端,但有些老式PowerPC是大端。写像素的时候,0x00FF0000在小端下是Blue通道,在大端下可能是Red通道。我建议用结构体或宏来定义颜色,别硬编码数值。
帧缓冲的核心结构图
下面这张图把帧缓冲的整个数据流串起来了。从用户程序到屏幕,中间经过了哪些环节,一目了然。
颜色格式的细节
刚才提到32位ARGB,但实际项目中更常见的是XRGB8888——Alpha位不用,保留。为什么?因为很多LCD控制器根本不支持Alpha混合,你写了Alpha值也没用。
我建议你通过FBIOGET_VSCREENINFO获取fb_var_screeninfo结构体,里面有个transp字段,告诉你Alpha通道的偏移和长度。如果transp.length是0,说明不支持Alpha。
16位RGB565的布局是这样的:
- 高5位:Red
- 中间6位:Green
- 低5位:Blue
为什么Green多1位?因为人眼对绿色最敏感。这是硬件设计上的一个经典取舍。
避坑指南:我曾经在一个项目里用16位色,结果显示出来的图片偏绿。查了半天,发现是驱动把RGB565的字节序搞反了——它按BGR565来解析。遇到这种情况,用FBIOGET_FSCREENINFO获取fb_fix_screeninfo,看visual字段是FB_VISUAL_TRUECOLOR还是FB_VISUAL_DIRECTCOLOR。前者是直彩色,后者需要查调色板。
虚拟分辨率与双缓冲的关联
还记得fbset输出里的虚拟分辨率吗?比如geometry 1024 768 2048 768 32,虚拟宽度是2048,是可见宽度的两倍。这意味着显存里有两帧数据并排存放。
怎么切换显示哪一帧?通过FBIOPAN_DISPLAY这个ioctl,设置fb_var_screeninfo的xoffset和yoffset。比如:
struct fb_var_screeninfo vinfo;
ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo);
// 切换到第二帧(偏移一个屏幕宽度)
vinfo.xoffset = 1024; // 假设可见宽度是1024
vinfo.yoffset = 0;
ioctl(fbfd, FBIOPAN_DISPLAY, &vinfo);
这就是双缓冲的硬件基础。你往后台帧写数据,然后通过FBIOPAN_DISPLAY把后台帧切换到前台。整个过程没有屏幕撕裂,因为切换发生在VBlank(垂直消隐)期间。
核心要点:帧缓冲的原理并不复杂——它就是一块内存,加上一个硬件DMA引擎。但理解它的细节,尤其是mmap的映射方式、像素格式的字节排列、以及虚拟分辨率与双缓冲的关系,是后面实现高效双缓冲刷新的前提。