2. fbdev 框架详解:核心数据结构、操作接口与生命周期
fbdev(Framebuffer Device)是 Linux 内核中历史最悠久的显示子系统框架,其核心设计围绕 fb_info 结构体、fb_ops 回调函数以及标准的字符设备注册/注销流程展开。理解这三部分,是掌握 fbdev 工作原理以及后续迁移到 DRM KMS 的基础。
2.1 fb_info 结构体:帧缓冲设备的核心描述符
fb_info 是 fbdev 框架中最重要的数据结构,每个帧缓冲设备在内存中对应一个 fb_info 实例。它包含了设备的所有硬件信息、状态、操作函数指针以及内存映射信息。
关键认知:
fb_info 既是设备描述符,也是驱动与用户空间交互的桥梁。用户空间通过 mmap 映射 fb_info 中的 screen_base 指向的显存,直接进行像素读写。
2.1.1 核心字段解析
| 字段名 | 类型 | 描述 |
|---|---|---|
fbops |
struct fb_ops * |
指向设备操作函数集的指针,驱动必须实现 |
screen_base |
char __iomem * |
显存在内核空间的虚拟基地址,用户空间 mmap 的目标 |
screen_size |
unsigned long |
显存总大小(字节),决定了可支持的显示分辨率 |
var |
struct fb_var_screeninfo |
可变屏幕信息:分辨率、位深、像素时钟、时序参数等 |
fix |
struct fb_fix_screeninfo |
固定屏幕信息:显存物理地址、行长度、MMIO 地址等 |
device |
struct device * |
指向 Linux 设备模型中的 device 结构,用于电源管理和 sysfs |
par |
void * |
驱动私有数据指针,用于存放硬件寄存器基址、时钟配置等 |
state |
int |
设备状态标志,如 FBINFO_STATE_RUNNING |
node |
int |
设备编号,对应 /dev/fb<N> 中的 N |
开发提示:
var 和 fix 是用户空间通过 ioctl(FBIOGET_VSCREENINFO / FBIOGET_FSCREENINFO)直接读取的对象。驱动在 fb_set_par 回调中根据 var 的配置更新硬件,并同步修改 fix 中的行长度等参数。
2.1.2 fb_info 结构体关系图
2.2 fb_ops 回调函数:驱动与内核的契约
fb_ops 结构体定义了帧缓冲设备驱动必须实现或可选实现的回调函数集合。内核在用户空间发起系统调用(如 open、ioctl、mmap)时,通过 fb_info.fbops 指针调用对应的驱动函数。
2.2.1 核心回调函数分类
| 回调函数 | 触发时机 | 必须实现 | 典型行为 |
|---|---|---|---|
fb_open |
用户空间 open("/dev/fb0") | 否 | 增加引用计数、初始化硬件状态 |
fb_release |
用户空间 close(fd) | 否 | 减少引用计数、清理资源 |
fb_read |
用户空间 read(fd, buf, count) | 否 | 从显存读取数据到用户缓冲区 |
fb_write |
用户空间 write(fd, buf, count) | 否 | 从用户缓冲区写入数据到显存 |
fb_setcolreg |
ioctl: FBIOPUTCMAP | 是(伪彩色) | 设置硬件颜色查找表(CLUT) |
fb_blank |
ioctl: FBIOBLANK | 否 | 控制显示面板的开关/休眠 |
fb_pan_display |
ioctl: FBIOPAN_DISPLAY | 否 | 实现硬件滚动(panning) |
fb_set_par |
ioctl: FBIOPUT_VSCREENINFO | 是 | 根据 var 配置硬件时序、分辨率、位深 |
fb_check_var |
ioctl: FBIOPUT_VSCREENINFO 前 | 是 | 校验用户传入的 var 参数是否合法,并调整到最接近的值 |
fb_mmap |
用户空间 mmap(fd, ...) | 否 | 自定义显存映射方式(默认使用 fb_deferred_io_mmap) |
常见陷阱: 许多简单 fbdev 驱动只实现
fb_set_par、fb_check_var 和 fb_setcolreg,而忽略 fb_blank。这会导致系统进入休眠后,显示器无法正常关闭或唤醒,出现“黑屏但背光亮”的异常现象。
2.2.2 fb_ops 回调调用流程
2.3 注册与注销流程:设备生命周期管理
fbdev 驱动的注册和注销遵循标准的 Linux 设备模型,核心函数为 register_framebuffer 和 unregister_framebuffer。驱动开发者需要理解这两个函数背后的完整流程,以避免资源泄漏和设备冲突。
2.3.1 注册流程 (register_framebuffer)
- 分配 fb_info: 使用
framebuffer_alloc分配fb_info结构体,并设置screen_base、screen_size、var、fix等字段。 - 填充 fb_ops: 将驱动实现的回调函数赋值给
fb_info.fbops。 - 设置私有数据: 通过
fb_info.par指向驱动私有数据结构(如寄存器基址、时钟配置)。 - 调用 register_framebuffer: 内核执行以下操作:
- 检查
fb_info合法性(fbops不能为 NULL)。 - 分配设备节点编号
node,创建/dev/fb<N>设备文件。 - 调用
fb_set_par初始化硬件时序和分辨率。 - 将
fb_info加入全局链表registered_fb。 - 注册 sysfs 属性,允许用户空间通过
/sys/class/graphics/fb<N>访问设备信息。
- 检查
- 初始化完成: 设备进入
FBINFO_STATE_RUNNING状态,用户空间可以打开设备。
2.3.2 注销流程 (unregister_framebuffer)
- 调用 unregister_framebuffer: 传入要注销的
fb_info指针。 - 内核执行:
- 从全局链表
registered_fb中移除该设备。 - 删除
/dev/fb<N>设备文件和 sysfs 属性。 - 等待所有用户空间文件描述符关闭(通过引用计数)。
- 调用
fb_blank(如果实现)关闭显示面板。
- 从全局链表
- 释放资源: 驱动调用
framebuffer_release释放fb_info结构体及其关联内存。
最佳实践: 在模块卸载函数中,务必先调用
unregister_framebuffer,再调用 framebuffer_release。顺序错误会导致内核访问已释放的内存,引发 Oops 或系统崩溃。
2.3.3 注册与注销流程状态图
2.4 典型驱动代码骨架
以下是一个简化的 fbdev 驱动注册与注销代码示例,展示了 fb_info 的分配、fb_ops 的填充以及注册/注销函数的调用顺序。
#include <linux/fb.h>
#include <linux/module.h>
static struct fb_info *my_fb_info;
static int my_fb_check_var(struct fb_var_screeninfo *var, struct fb_info *info)
{
/* 校验并修正分辨率、位深等参数 */
if (var->xres > 1920 || var->yres > 1080)
return -EINVAL;
return 0;
}
static int my_fb_set_par(struct fb_info *info)
{
/* 根据 info->var 配置硬件时序寄存器 */
/* 更新 info->fix.line_length */
info->fix.line_length = info->var.xres * info->var.bits_per_pixel / 8;
return 0;
}
static struct fb_ops my_fb_ops = {
.owner = THIS_MODULE,
.fb_check_var = my_fb_check_var,
.fb_set_par = my_fb_set_par,
.fb_setcolreg = NULL, /* 真彩色模式不需要 */
.fb_fillrect = sys_fillrect,
.fb_copyarea = sys_copyarea,
.fb_imageblit = sys_imageblit,
};
static int my_fb_probe(struct platform_device *pdev)
{
/* 1. 分配 fb_info */
my_fb_info = framebuffer_alloc(0, &pdev->dev);
if (!my_fb_info)
return -ENOMEM;
/* 2. 设置固定参数 */
strcpy(my_fb_info->fix.id, "my_fb");
my_fb_info->fix.smem_start = 0x10000000; /* 物理地址 */
my_fb_info->fix.smem_len = 8 * 1024 * 1024; /* 8MB */
/* 3. 设置可变参数默认值 */
my_fb_info->var.xres = 1024;
my_fb_info->var.yres = 768;
my_fb_info->var.bits_per_pixel = 32;
/* 4. 映射显存 */
my_fb_info->screen_base = ioremap(my_fb_info->fix.smem_start,
my_fb_info->fix.smem_len);
my_fb_info->screen_size = my_fb_info->fix.smem_len;
/* 5. 挂载 fb_ops */
my_fb_info->fbops = &my_fb_ops;
/* 6. 注册帧缓冲设备 */
if (register_framebuffer(my_fb_info) < 0) {
iounmap(my_fb_info->screen_base);
framebuffer_release(my_fb_info);
return -EINVAL;
}
return 0;
}
static int my_fb_remove(struct platform_device *pdev)
{
/* 1. 注销帧缓冲设备 */
unregister_framebuffer(my_fb_info);
/* 2. 取消显存映射 */
iounmap(my_fb_info->screen_base);
/* 3. 释放 fb_info */
framebuffer_release(my_fb_info);
return 0;
}
代码要点: 注意
framebuffer_alloc 的第二个参数是 struct device *,用于关联设备模型。在 my_fb_set_par 中更新 fix.line_length 是常见遗漏点,该字段直接影响用户空间计算每行像素的偏移量。
2.5 fbdev 框架的局限性
虽然 fbdev 框架简单易用,但在现代嵌入式系统中存在明显不足,这也是推动迁移到 DRM KMS 的主要原因:
| 局限性 | 具体表现 | 对开发的影响 |
|---|---|---|
| 单缓冲模型 | 不支持硬件多缓冲(double/triple buffer) | 画面撕裂(tearing)难以避免 |
| 缺少合成能力 | 不支持硬件图层叠加(overlay/plane) | 无法实现高效的 UI 合成 |
| 无原子更新 | 参数配置不是原子操作,中间状态可见 | 分辨率切换时可能出现闪屏 |
| 缺少 VBlank 同步 | 没有标准的垂直消隐信号管理 | 动画 |