帧缓冲区(FrameBuffer)详解:显存映射、像素格式、颜色深度、行对齐
好,咱们进入正题。帧缓冲区,说白了就是一块内存区域,专门用来存放一帧画面的像素数据。你在屏幕上看到的每一个像素,背后都对应着这块内存里的几个字节。我刚开始接触嵌入式图形时,总觉得这玩意儿很玄乎,后来自己动手写了一个简单的 framebuffer 驱动,才真正搞明白——它其实就是显存的一种映射方式。
显存映射:从物理地址到用户空间
在嵌入式 Linux 系统中,帧缓冲区通常通过 mmap 系统调用映射到用户空间。内核负责分配一块连续的物理内存,然后把它映射到进程的虚拟地址空间。你想想看,这样一来,应用程序就能像操作普通数组一样直接读写显存了。
核心流程:
- 打开
/dev/fb0设备文件 - 通过
ioctl获取屏幕参数(分辨率、位深、行宽等) - 调用
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("打开 framebuffer 失败");
return 1;
}
struct fb_var_screeninfo vinfo;
ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo);
// 计算显存大小:行宽 × 高度
size_t screensize = vinfo.xres_virtual * vinfo.yres_virtual * vinfo.bits_per_pixel / 8;
char *fbp = (char *)mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0);
if ((intptr_t)fbp == -1) {
perror("mmap 失败");
return 1;
}
// 现在可以直接操作 fbp 指针了
// 比如把屏幕左上角第一个像素设为红色
*(fbp + 0) = 0x00; // B
*(fbp + 1) = 0x00; // G
*(fbp + 2) = 0xFF; // R
*(fbp + 3) = 0x00; // A (如果支持)
munmap(fbp, screensize);
close(fbfd);
return 0;
}
我的经验: 映射完成后,记得检查 fbp 是否等于 MAP_FAILED。我曾经在一个老平台上踩过坑——内核配置里没开 framebuffer 支持,结果 mmap 一直返回失败,排查了半天才发现是内核问题。
像素格式与颜色深度
像素格式决定了每个像素在内存中怎么排列。常见的格式有 RGB565、RGB888、ARGB8888 等。颜色深度就是每个颜色通道用多少位来表示。
| 格式 | 总位数 | R | G | B | A | 常见场景 |
|---|---|---|---|---|---|---|
| RGB565 | 16 | 5 | 6 | 5 | — | 低端 MCU、小屏幕 |
| RGB888 | 24 | 8 | 8 | 8 | — | 嵌入式 Linux、中端设备 |
| ARGB8888 | 32 | 8 | 8 | 8 | 8 | Android、桌面系统 |
| XRGB8888 | 32 | 8 | 8 | 8 | 8(忽略) | 兼容性考虑 |
我个人习惯在项目初期就确定好像素格式,因为一旦选错,后期改起来非常痛苦。比如 RGB565 虽然省内存,但颜色精度不够,做渐变效果时会出现明显的色带。而 ARGB8888 虽然占内存多,但兼容性最好,移植起来也省心。
行对齐:一个容易被忽视的坑
行对齐,也叫 stride 或 pitch,是指每一行像素在内存中占用的实际字节数。它不一定等于 宽度 × 每个像素字节数。为什么?因为硬件通常要求每行起始地址对齐到某个边界,比如 4 字节、8 字节甚至 32 字节。
我曾经踩过的坑: 有一次在移植一个 GUI 库时,我直接按 width * bytes_per_pixel 计算每行偏移,结果在 800×480 的屏幕上显示正常,换到 1024×600 的屏幕后,画面出现了奇怪的斜条纹。排查了半天才发现,新屏幕的行对齐要求是 32 字节,而我的计算忽略了填充字节。
正确的做法是从 fb_var_screeninfo 结构体中读取 xres_virtual 和 yres_virtual,然后通过 FBIOGET_FSCREENINFO 获取 smem_len 和 line_length。其中 line_length 就是实际的行宽。
struct fb_fix_screeninfo finfo;
ioctl(fbfd, FBIOGET_FSCREENINFO, &finfo);
// 实际行宽(包含填充字节)
int stride = finfo.line_length;
// 计算像素在内存中的位置
int x = 100, y = 200;
int offset = y * stride + x * (vinfo.bits_per_pixel / 8);
避坑指南: 永远不要自己计算行宽,直接从 line_length 读取。不同硬件、不同驱动版本的行对齐策略可能完全不同,自己算很容易出错。
帧缓冲区的整体结构
下面这张图展示了帧缓冲区在内存中的布局,以及行对齐带来的填充区域。
颜色深度对性能的影响
颜色深度不仅影响画质,还直接影响内存带宽和刷新率。举个例子,同样是 1920×1080 的分辨率:
- RGB565: 每帧 1920×1080×2 = 4.15 MB,60fps 需要 249 MB/s 带宽
- RGB888: 每帧 1920×1080×3 = 6.22 MB,60fps 需要 373 MB/s 带宽
- ARGB8888: 每帧 1920×1080×4 = 8.29 MB,60fps 需要 497 MB/s 带宽
在资源受限的嵌入式设备上,带宽往往是瓶颈。我做过一个项目,用 RGB888 时画面流畅,换成 ARGB8888 后帧率直接掉了一半。后来查了 datasheet,发现 LCD 控制器的 DMA 带宽上限刚好卡在 400 MB/s 左右。
我的建议: 选颜色深度时,先查硬件手册里的带宽上限,再算一下目标分辨率下的理论带宽需求。留出 20% 的余量,不然高负载时画面会撕裂。
实际项目中的常见问题
- 画面偏移: 通常是行对齐计算错误导致的。检查
line_length是否与width * bpp/8一致。 - 颜色失真: 像素格式不匹配。比如硬件是 RGB565,但软件按 RGB888 写入数据。
- 内存溢出: 映射的显存大小不够。记得用
smem_len而不是自己算。 - 性能瓶颈: 颜色深度太高,带宽不够。考虑降低位深或使用双缓冲 + 局部刷新。
嗯,帧缓冲区这块内容就讲到这里。说白了,它就是一块内存,但细节不少。行对齐和像素格式是新手最容易翻车的地方,你写代码时多留个心眼就好。