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(操作函数集)等关键成员。

驱动模型的主要层次如下:

Framebuffer 设备驱动模型架构 用户空间 (User Space) 应用程序 / GUI 库 (Qt, LVGL, DirectFB) → 通过 /dev/fb0 访问 内核空间 (Kernel Space) VFS 层 → fb_open / fb_read / fb_write / fb_ioctl / fb_mmap struct fb_info (核心实例) fb_var_screeninfo | fb_fix_screeninfo | fb_ops | screen_base 硬件层 (Hardware) LCD 控制器 / HDMI 发送器 / GPU 2D 引擎 / 显存 (VRAM) 驱动通过 register_framebuffer(struct fb_info *) 向内核注册设备

驱动注册流程的关键步骤:

  1. 分配 fb_info: 使用 framebuffer_alloc() 分配并初始化 fb_info 结构体。
  2. 填充固定/可变参数: 设置分辨率(xres, yres)、位深(bits_per_pixel)、行宽(line_length)、显存物理地址(smem_start)等。
  3. 实现 fb_ops: 必须实现 fb_fillrect、fb_copyarea、fb_imageblit 等加速函数,以及 fb_mmap 用于内存映射。
  4. 注册设备: 调用 register_framebuffer() 将设备加入内核 Framebuffer 子系统。

1.2 mmap 映射原理

Framebuffer 的核心优势在于允许用户空间通过 mmap 系统调用直接映射显存到进程地址空间,从而避免每次像素操作都经过 read/write 系统调用,大幅提升性能。

性能关键: mmap 将内核空间的显存物理地址直接映射到用户空间的虚拟地址,应用程序可以像操作普通内存数组一样直接读写显存,零拷贝、零系统调用开销。

mmap 映射的完整流程:

mmap 映射原理与数据流 用户空间进程 虚拟地址空间 mmap() → 返回用户态虚拟地址 直接通过指针读写像素 内核空间 fb_mmap() 回调函数 remap_pfn_range() 建立页表 将物理页帧映射到用户虚拟地址 物理内存 / 显存 (VRAM) smem_start (物理基地址) → smem_len (总长度) DMA 连续内存 / 保留内存 / 通过 IOMMU 映射 关键函数:remap_pfn_range(vma, vaddr, pfn, size, prot) pfn = smem_start >> PAGE_SHIFT,将物理页帧号转换为虚拟地址映射

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 映射后访问。

共享显存架构:显存与系统内存的关系 系统内存 (DRAM) — 4GB Framebuffer 显存区域 物理地址:0x48000000 - 0x48FFFFFF 大小:16MB (DMA 连续内存) 其他系统内存 内核空间 | 用户进程 | 文件缓存 | 堆栈 CPU 通过 mmap 访问 显示控制器通过 DMA 直接读取 硬件访问路径 CPU 侧:系统总线 → 内存控制器 → DRAM 显示侧:LCD 控制器 → DMA 引擎 → 内存控制器 → DRAM 关键指标:显存带宽 = 分辨率 × 位深 × 刷新率;共享显存需考虑与 CPU 的带宽竞争
显存分配关键函数:
  • 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 向显存写入像素的性能。对于不需要读回显存的场景(如纯显示输出),这是最佳选择。