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 显示模块。
二、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 时,核心层把这块物理内存映射到进程地址空间,应用即可直接读写。
基本显示流程可以概括为:
- 驱动 probe:分配
fb_info,填充var/fix,实现fb_ops,分配显存。 - 调用
register_framebuffer():核心层分配/dev/fbX设备号并创建节点。 - 应用 open:核心层调用驱动的
fb_open(可选)。 - 应用 ioctl:获取
var/fix,计算显存布局。 - 应用 mmap:把显存映射到用户空间。
- 应用写像素:直接写映射地址,显示控制器持续扫描显存,屏幕内容更新。
- 应用 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);
五、常见误区与复盘
fix.line_length 与 var.xres_virtual 的区别。行距可能大于可见宽度,写像素时必须按 line_length 计算偏移,否则图像会错位。
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 对象,从而完成概念上的平滑过渡。