1. Framebuffer 基础概念
Framebuffer(帧缓冲)是 Linux 内核为显示设备提供的最基础、最直接的驱动接口。它本质上是一块内存区域,用于存储屏幕每个像素的颜色数据。应用程序通过操作这块内存,即可控制显示内容,无需关心底层 GPU 或显示控制器的具体实现。本章将深入剖析 Framebuffer 的设备驱动模型、mmap 映射原理以及显存与系统内存的关系。
1.1 Framebuffer 设备驱动模型
在 Linux 系统中,Framebuffer 被抽象为一个字符设备,通常位于 /dev/fb0、/dev/fb1 等路径。其驱动模型遵循标准的 Linux 设备驱动架构,核心结构体为 fb_info,它包含了显示设备的全部信息与控制函数指针。
核心数据结构:
struct fb_info 是 Framebuffer 驱动的中枢,包含 fb_var_screeninfo(可变参数,如分辨率、位深)、fb_fix_screeninfo(固定参数,如显存物理地址、长度)、fb_ops(操作函数集)等关键成员。
驱动模型的主要层次如下:
驱动注册流程的关键步骤:
- 分配 fb_info: 使用
framebuffer_alloc()分配并初始化fb_info结构体。 - 填充固定/可变参数: 设置分辨率(xres, yres)、位深(bits_per_pixel)、行宽(line_length)、显存物理地址(smem_start)等。
- 实现 fb_ops: 必须实现
fb_fillrect、fb_copyarea、fb_imageblit等加速函数,以及fb_mmap用于内存映射。 - 注册设备: 调用
register_framebuffer()将设备加入内核 Framebuffer 子系统。
1.2 mmap 映射原理
Framebuffer 的核心优势在于允许用户空间通过 mmap 系统调用直接映射显存到进程地址空间,从而避免每次像素操作都经过 read/write 系统调用,大幅提升性能。
性能关键: mmap 将内核空间的显存物理地址直接映射到用户空间的虚拟地址,应用程序可以像操作普通内存数组一样直接读写显存,零拷贝、零系统调用开销。
mmap 映射的完整流程:
mmap 映射的关键代码示例:
/* 用户空间应用程序通过 mmap 映射 Framebuffer */
int fb_fd = open("/dev/fb0", O_RDWR);
if (fb_fd < 0) {
perror("open /dev/fb0 failed");
return -1;
}
/* 获取固定信息,得到显存大小 */
struct fb_fix_screeninfo fix;
ioctl(fb_fd, FBIOGET_FSCREENINFO, &fix);
/* 获取可变信息,得到分辨率与位深 */
struct fb_var_screeninfo var;
ioctl(fb_fd, FBIOGET_VSCREENINFO, &var);
/* mmap 映射显存到用户空间 */
size_t screensize = var.xres * var.yres * (var.bits_per_pixel / 8);
void *fb_ptr = mmap(NULL, screensize, PROT_READ | PROT_WRITE,
MAP_SHARED, fb_fd, 0);
if (fb_ptr == MAP_FAILED) {
perror("mmap failed");
close(fb_fd);
return -1;
}
/* 直接通过指针操作像素:将屏幕左上角像素设为红色 */
unsigned int *pixel = (unsigned int *)fb_ptr;
*pixel = 0x00FF0000; /* ARGB 格式,红色通道 */
/* 解除映射 */
munmap(fb_ptr, screensize);
close(fb_fd);
注意事项: mmap 映射时使用
MAP_SHARED 标志,确保对映射内存的修改能同步回物理显存。使用 MAP_PRIVATE 会导致写时复制,修改不会反映到屏幕上。
1.3 显存与系统内存的关系
在嵌入式系统中,显存(VRAM)与系统内存(DRAM)的关系取决于硬件架构和驱动实现方式。理解两者的关系对于性能优化和内存管理至关重要。
| 架构类型 | 显存位置 | 访问方式 | 典型场景 | 性能特点 |
|---|---|---|---|---|
| 独立显存 | 专用 VRAM 芯片(如 SDRAM、DDR) | 通过显示控制器专用总线访问 | 高端 SoC、独立 GPU | 高带宽、低延迟,不占用系统内存 |
| 共享显存 | 系统内存中保留区域(DMA 内存) | 通过系统总线(AXI/AMBA)访问 | 大多数嵌入式 SoC(i.MX、STM32MP1、Allwinner) | 节省成本,但带宽与系统内存共享 |
| IOMMU 映射 | 系统内存任意位置,通过 IOMMU 重映射 | 显示控制器通过 IOMMU 访问分散物理页 | 支持虚拟化的平台、复杂内存布局 | 灵活但增加延迟,需 IOMMU 配置 |
共享显存架构是嵌入式系统中最常见的模式。显存通常通过内核的 DMA 内存分配器(如 dma_alloc_coherent)在系统内存中分配一块连续的物理内存区域。这块区域既被显示控制器(LCDC/HDMI)通过硬件总线直接读取,也被 CPU 通过 mmap 映射后访问。
显存分配关键函数:
dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL)— 分配 DMA 连续内存,返回 CPU 虚拟地址和 DMA 物理地址。dma_alloc_writecombine()— 分配写合并内存,提高 CPU 写入性能(适用于 Framebuffer)。alloc_pages_exact()— 分配连续物理页,适用于不需要 DMA 句柄的场景。
在驱动初始化时,显存的分配与注册流程如下:
/* Framebuffer 驱动中显存分配与注册示例 */
static int myfb_probe(struct platform_device *pdev)
{
struct fb_info *fbi;
struct myfb_par *par;
unsigned long mem_size;
dma_addr_t dma_handle;
void *screen_base;
/* 1. 分配 fb_info */
fbi = framebuffer_alloc(sizeof(struct myfb_par), &pdev->dev);
if (!fbi)
return -ENOMEM;
par = fbi->par;
/* 2. 计算显存大小:1920x1080x32bpp = 8MB */
mem_size = 1920 * 1080 * 4;
/* 3. 分配 DMA 连续内存(写合并模式,提高 CPU 写入性能) */
screen_base = dma_alloc_writecombine(&pdev->dev, mem_size,
&dma_handle, GFP_KERNEL);
if (!screen_base) {
framebuffer_release(fbi);
return -ENOMEM;
}
/* 4. 填充 fb_fix_screeninfo */
fbi->fix.smem_start = dma_handle; /* 显存物理地址 */
fbi->fix.smem_len = mem_size; /* 显存大小 */
fbi->screen_base = screen_base; /* CPU 虚拟地址 */
/* 5. 填充 fb_var_screeninfo */
fbi->var.xres = 1920;
fbi->var.yres = 1080;
fbi->var.bits_per_pixel = 32;
fbi->var.xres_virtual = 1920;
fbi->var.yres_virtual = 1080;
/* 6. 设置 fb_ops(包含 fb_mmap 回调) */
fbi->fbops = &myfb_ops;
/* 7. 注册 Framebuffer 设备 */
if (register_framebuffer(fbi) < 0) {
dma_free_writecombine(&pdev->dev, mem_size,
screen_base, dma_handle);
framebuffer_release(fbi);
return -EINVAL;
}
dev_info(&pdev->dev, "Framebuffer registered: %dx%d %dbpp\n",
fbi->var.xres, fbi->var.yres, fbi->var.bits_per_pixel);
return 0;
}
性能优化提示: 使用
dma_alloc_writecombine() 而非 dma_alloc_coherent() 可以启用 CPU 写合并缓冲区,将多次小写入合并为一次突发写入,显著提升 CPU 向显存写入像素的性能。对于不需要读回显存的场景(如纯显示输出),这是最佳选择。