2. FrameBuffer基础

在进入 DRM 之前,必须先理解 Linux 显示子系统的“前身”——FrameBuffer 子系统(fbdev)。它用一套极简抽象把“一块可以被 CPU 直接写入的显存”暴露给用户空间:应用只要打开 /dev/fb0,通过 mmap 拿到显存指针,再按像素格式写数据,屏幕就会变化。理解 fbdev 的架构、fb_info、fb_ops、显存映射与显示流程,是后续迁移到 DRM 时做“概念映射”的基础。

一、fbdev 的整体架构

fbdev 采用典型的分层结构:最上层是字符设备节点 /dev/fbX,中间是核心层 fbmem.c,下层是具体硬件驱动。核心层负责注册设备号、创建 /dev/fbX、实现统一的 file_operations(open/read/write/mmap/ioctl),并把用户请求转发给具体驱动的 fb_ops。硬件驱动则负责初始化控制器、配置时序、分配显存,并填充 fb_info。

这种设计的关键在于:核心层提供“通用能力”,驱动层提供“硬件差异”。用户空间看到的接口是统一的,而底层可以是不同的 LCD 控制器、GPU 或 SoC 显示模块。

fbdev 分层架构与显示数据流 用户空间应用 open("/dev/fb0") → mmap() → 写像素 → ioctl(FBIOGET_VSCREENINFO) fbdev 核心层 fbmem.c 注册字符设备 /dev/fbX · 统一 file_operations · 转发 ioctl/mmap 到 fb_ops 具体硬件驱动 填充 fb_info · 实现 fb_ops · 分配显存 · 配置 LCD/HDMI 控制器时序 register_framebuffer(fb_info) → 核心层创建 /dev/fbX 显示硬件 显存 (VRAM) · 显示控制器 · LCD 面板 / HDMI 输出 用户可见 /dev/fb0 核心层 fbmem.c fb_ops 转发 驱动层 fb_info fb_ops

二、fb_info 结构体:驱动的“身份证”

fb_info 是 fbdev 的核心数据结构,定义在 include/linux/fb.h 中。它描述了一块 framebuffer 的全部属性:可变参数、固定参数、操作集、显存信息、设备节点等。驱动在 probe 阶段分配并填充它,然后调用 register_framebuffer() 注册。

struct fb_info {
    int node;                          // /dev/fbX 的 X
    struct fb_var_screeninfo var;      // 可变参数:分辨率、色深、时序
    struct fb_fix_screeninfo fix;      // 固定参数:显存物理地址、长度、行距
    struct fb_ops *fbops;              // 操作集
    struct device *device;
    struct screen_info *screen_info;
    char __iomem *screen_base;         // 显存虚拟地址
    unsigned long screen_size;         // 显存大小
    void *pseudo_palette;              // 伪调色板
    // ... 其他字段
};

其中 var 和 fix 是最常被用户空间读取的两个结构。FBIOGET_VSCREENINFO 返回 var,FBIOGET_FSCREENINFO 返回 fix。应用通过它们计算显存偏移、行字节数、像素格式,从而正确写入像素。

三、fb_ops 操作集:硬件差异的收敛点

fb_ops 是一组函数指针,核心层通过它调用具体硬件操作。并非所有函数都必须实现,但 fb_check_var、fb_set_par、fb_setcolreg、fb_fillrect、fb_copyarea、fb_imageblit 是最常见的成员。

fb_ops 成员 作用 典型实现要点
fb_check_var 校验并修正 var 参数 检查分辨率、色深、时序是否被硬件支持
fb_set_par 把 var 参数真正写入硬件 配置 LCD 控制器寄存器、重算行距
fb_setcolreg 设置调色板寄存器 用于 8bpp 伪彩色或 16bpp 以下模式
fb_fillrect 填充矩形区域 可用 CPU 或 2D 加速引擎
fb_copyarea 区域拷贝 滚动、双缓冲切换常用
fb_imageblit 图像位块传输 控制台字体渲染、图标绘制
提示:如果驱动不实现 fb_fillrect 等加速函数,核心层会使用 cfb_fillrect 等通用软件实现,功能仍然可用,只是性能较低。

四、显存映射与基本显示流程

显存映射是 fbdev 最核心的机制。驱动在初始化时通过 dma_alloc_coherent、ioremap 或预留 CMA 区域获得一块物理连续内存,并把物理地址填入 fix.smem_start,长度填入 fix.smem_len,虚拟地址填入 screen_base。用户空间调用 mmap 时,核心层把这块物理内存映射到进程地址空间,应用即可直接读写。

基本显示流程可以概括为:

  1. 驱动 probe:分配 fb_info,填充 var/fix,实现 fb_ops,分配显存。
  2. 调用 register_framebuffer():核心层分配 /dev/fbX 设备号并创建节点。
  3. 应用 open:核心层调用驱动的 fb_open(可选)。
  4. 应用 ioctl:获取 var/fix,计算显存布局。
  5. 应用 mmap:把显存映射到用户空间。
  6. 应用写像素:直接写映射地址,显示控制器持续扫描显存,屏幕内容更新。
  7. 应用 ioctl FBIOPAN_DISPLAY:切换显示起始偏移,实现双缓冲或滚动。
# 查看 framebuffer 信息
cat /sys/class/graphics/fb0/virtual_size
cat /sys/class/graphics/fb0/bits_per_pixel
cat /sys/class/graphics/fb0/stride

# 使用 fbset 查看和设置
fbset -i
fbset -xres 1920 -yres 1080 -depth 32

# 简单写屏测试(伪代码)
int fd = open("/dev/fb0", O_RDWR);
struct fb_var_screeninfo var;
ioctl(fd, FBIOGET_VSCREENINFO, &var);
size_t screensize = var.xres * var.yres * var.bits_per_pixel / 8;
char *fbp = mmap(NULL, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
memset(fbp, 0xFF, screensize);  // 全白
munmap(fbp, screensize);
close(fd);

五、常见误区与复盘

误区一:认为 fbdev 只能用于简单显示。实际上 fbdev 也支持双缓冲、pan display、硬件加速填充,只是接口较原始,缺乏原子提交和图层管理。
误区二:忽略 fix.line_length 与 var.xres_virtual 的区别。行距可能大于可见宽度,写像素时必须按 line_length 计算偏移,否则图像会错位。
误区三:在 mmap 后直接假设显存物理连续。部分平台显存不连续,fbdev 通过 fb_deferred_io 或分页映射处理,但性能和行为会不同。

复盘来看,fbdev 的价值在于“简单直接”:一个设备节点、一次 mmap、一块显存,就能点亮屏幕。它的局限也很明显:没有图层、没有原子提交、没有 GPU 同步、多显示管理困难。这些正是 DRM 要解决的问题。理解 fbdev 的 fb_info、fb_ops 和显存映射流程,就能在迁移到 DRM 时,把 fb_info 对应到 drm_framebuffer,把 fb_ops 对应到 drm_crtc/drm_plane 的操作,把 mmap 显存对应到 GEM 对象,从而完成概念上的平滑过渡。