3. Framebuffer子系统入门

在进入 DRM 之前,几乎每一位显示驱动工程师都会先与 Framebuffer(fbdev)子系统打交道。它是 Linux 内核中最古老、最直接的显示抽象层:把一块可以被 CPU 直接访问的显存区域,抽象成用户空间可见的 /dev/fbX 设备。理解 fbdev,不仅是移植旧驱动的必要技能,也是理解 DRM 中 framebuffer、plane、CRTC 等概念的历史参照系。

一、fbdev 的整体架构

fbdev 的核心思想非常朴素:内核负责管理一块线性显存(framebuffer memory),用户空间通过 mmap 把它映射到进程地址空间,然后直接往里面写像素。显示控制器(Display Controller)周期性地扫描这块内存,把内容送到屏幕上。整个子系统由三部分组成:

  • 核心层(fbmem.c):提供字符设备注册、file_operations、ioctl 分发、mmap 实现,是所有 fb 设备共用的骨架。
  • 驱动层(xxxfb.c):具体硬件驱动,负责填充 fb_info、实现 fb_ops 回调、申请显存、配置时序。
  • 用户空间接口:设备节点 /dev/fb0,配合 FBIOGET_VSCREENINFO 等 ioctl 和 mmap 使用。

下图展示了 fbdev 从用户空间到硬件的完整数据通路与关键结构关系:

用户空间 应用程序 / Qt / SDL fbset / fbv / fbtft /dev/fb0 设备节点 mmap + ioctl 系统调用 内核空间 — fbdev 子系统 fbmem.c 核心层 register_framebuffer() fb_ioctl / fb_mmap fb_open / fb_release registered_fb[] 数组 struct fb_info fb_var_screeninfo fb_fix_screeninfo fb_ops *fbops screen_base / screen_size 硬件驱动 xxxfb.c fb_check_var fb_set_par fb_fillrect / fb_copyarea probe / remove 寄存器配置 / DMA 硬件层 显示控制器 (LCD Controller) 显存 (VRAM / DDR) LCD 面板 / HDMI 输出

二、fb_info 结构:驱动的核心数据结构

struct fb_info 是 fbdev 驱动的“身份证”,它把硬件描述、显存信息、操作函数集全部聚合在一起。驱动 probe 阶段的主要工作,就是分配并填充这个结构,然后调用 register_framebuffer() 把它挂入内核。

其中最关键的两个子结构是 fb_var_screeninfo 和 fb_fix_screeninfo:前者描述“可变”的显示参数(分辨率、色深、时序),后者描述“固定”的硬件属性(显存物理地址、长度、行 stride)。用户空间通过 ioctl 读取它们,就能知道当前屏幕的能力边界。

struct fb_info {
    struct fb_var_screeninfo var;   /* 可变参数:分辨率、色深、时序 */
    struct fb_fix_screeninfo fix;   /* 固定参数:显存地址、长度、stride */
    struct fb_ops *fbops;           /* 硬件操作回调集合 */
    char __iomem *screen_base;      /* 显存虚拟基地址 */
    unsigned long screen_size;      /* 显存大小(字节) */
    struct device *device;          /* 关联的设备模型 */
    int node;                       /* 分配到的 /dev/fbX 编号 */
    /* ... 省略若干字段 ... */
};

三、注册与注销流程

fbdev 驱动的生命周期非常清晰,可以概括为“分配 → 填充 → 注册 → 使用 → 注销 → 释放”六步:

  1. 分配 fb_info:使用 framebuffer_alloc(),它会同时分配私有数据区。
  2. 填充关键字段:设置 var、fix、fbops、screen_base、screen_size。
  3. 注册设备:调用 register_framebuffer(info),内核分配 /dev/fbX 节点。
  4. 运行期:用户空间 mmap 显存、ioctl 查询/设置参数,驱动响应 fb_ops 回调。
  5. 注销设备:调用 unregister_framebuffer(info),释放设备节点。
  6. 释放内存:调用 framebuffer_release(info),回收 fb_info 及私有数据。
提示:注册顺序很重要。必须在 register_framebuffer() 之前完成 screen_base 映射和 fbops 赋值,否则用户空间一旦打开设备就可能触发空指针回调。

四、用户空间接口与典型应用场景

fbdev 对用户空间暴露的接口非常精简,主要就是设备节点加一组 ioctl:

接口 / ioctl 作用 典型使用场景
open("/dev/fb0") 打开 framebuffer 设备 所有 fb 应用的第一步
FBIOGET_VSCREENINFO 获取可变屏幕信息 查询分辨率、色深、像素格式
FBIOPUT_VSCREENINFO 设置可变屏幕信息 切换分辨率或色深
FBIOGET_FSCREENINFO 获取固定屏幕信息 计算显存大小与行 stride
mmap() 映射显存到用户空间 直接绘制像素、双缓冲
FBIOPAN_DISPLAY 切换显示起始行 实现页面翻转(page flipping)

典型应用场景包括:嵌入式设备的开机 logo 显示、控制台终端(fbcon)、轻量级 GUI 框架(如 Qt Embedded 的 linuxfb 后端)、视频播放器的直接渲染层,以及工业 HMI 中的低延迟画面刷新。这些场景的共同点是:不需要复杂的 GPU 合成,只需要一块能直接写的显存。

五、常见误区与复盘

误区一:把 fbdev 当成“万能显示框架”。fbdev 只适合单图层、无硬件合成、无 GPU 加速的场景。一旦涉及多图层叠加、硬件光标、VSync 精确控制,fbdev 就力不从心,这正是 DRM 出现的根本原因。
误区二:忽略 stride 与显存大小的关系。很多驱动在设置 fix.line_length 时直接写成 xres * bpp / 8,但硬件往往要求行对齐(如 64 字节对齐)。如果 stride 算错,画面会出现斜纹或错位。
误区三:注销顺序颠倒。先 framebuffer_release() 再 unregister_framebuffer(),会导致内核访问已释放内存。正确顺序永远是先注销、后释放。

复盘来看,fbdev 的价值在于“简单直接”:它用最小的抽象代价换来了最快的上手速度。对于移植适配工作而言,先把 fbdev 跑通,再迁移到 DRM,是一条风险可控的路径。理解 fb_info 的字段含义、注册注销的时序约束,以及用户空间 ioctl 的语义,是后续 DRM 章节中理解 framebuffer 对象、plane 和 CRTC 的重要基础。