3、FBDEV驱动开发实战:编写一个简单的虚拟帧缓冲驱动,实现probe/remove/ops
本章将带领你从零开始编写一个完整的虚拟帧缓冲驱动。该驱动不依赖任何真实硬件,而是通过系统内存模拟显存,实现标准的 fb_ops 接口。通过这个实战,你将深入理解 FBDEV 驱动的核心生命周期:probe(探测)、remove(移除) 以及 ops(操作函数集) 的实现细节。
实战目标: 编写一个名为
vfb 的虚拟帧缓冲驱动,在 /dev/fbX 下注册设备,支持 mmap、pan_display 和基本的 ioctl 操作。驱动加载后,用户可直接通过 fbset 和 fbdev 工具进行验证。
3.1 驱动框架与数据结构
一个标准的 FBDEV 驱动由三个核心数据结构支撑:
| 数据结构 | 作用 | 关键字段 |
|---|---|---|
struct fb_info |
帧缓冲设备的核心表示,包含所有状态信息 | fbops, screen_base, screen_size, var, fix |
struct fb_ops |
操作函数集,定义驱动对用户空间暴露的接口 | fb_open, fb_release, fb_read, fb_write, fb_mmap, fb_pan_display, fb_ioctl |
struct fb_var_screeninfo |
可变屏幕参数,可由用户空间动态修改 | xres, yres, bits_per_pixel, pixclock |
struct fb_fix_screeninfo |
固定屏幕参数,驱动初始化后保持不变 | smem_start, smem_len, line_length, type |
关键设计原则:
fb_info 是驱动与内核框架之间的桥梁。驱动在 probe 阶段分配并初始化 fb_info,在 remove 阶段释放它。所有硬件操作都通过 fb_ops 间接调用。
下面的 SVG 展示了虚拟帧缓冲驱动的整体软件架构,以及 probe/remove/ops 三者之间的协作关系:
3.2 probe 函数:设备初始化
probe 是驱动的入口点,负责完成以下关键步骤:
- 分配 fb_info 结构体:使用
framebuffer_alloc()分配,该函数会同时分配私有数据区域。 - 设置固定参数 (fix):填写
fb_fix_screeninfo,包括显存起始地址、长度、行字节数等。 - 设置可变参数 (var):填写
fb_var_screeninfo,包括分辨率、色深、像素时钟等默认值。 - 分配虚拟显存:使用
vmalloc()或kzalloc()分配一块连续内存作为显存。 - 注册设备:调用
register_framebuffer()将设备注册到内核。
以下是 probe 函数的完整实现:
static int vfb_probe(struct platform_device *pdev)
{
struct fb_info *info;
struct vfb_par *par;
int ret;
/* 1. 分配 fb_info 结构体,附带私有数据区域 */
info = framebuffer_alloc(sizeof(struct vfb_par), &pdev->dev);
if (!info)
return -ENOMEM;
par = info->par;
par->pdev = pdev;
/* 2. 设置固定参数 */
strscpy(info->fix.id, "Virtual FB", sizeof(info->fix.id));
info->fix.type = FB_TYPE_PACKED_PIXELS;
info->fix.visual = FB_VISUAL_TRUECOLOR;
info->fix.line_length = VFB_XRES * (VFB_BPP / 8);
info->fix.smem_len = VFB_XRES * VFB_YRES * (VFB_BPP / 8);
/* 3. 设置可变参数 */
info->var.xres = VFB_XRES;
info->var.yres = VFB_YRES;
info->var.xres_virtual = VFB_XRES;
info->var.yres_virtual = VFB_YRES * 2; /* 双缓冲 */
info->var.bits_per_pixel = VFB_BPP;
info->var.red.offset = 16;
info->var.red.length = 8;
info->var.green.offset = 8;
info->var.green.length = 8;
info->var.blue.offset = 0;
info->var.blue.length = 8;
info->var.transp.offset = 0;
info->var.transp.length = 0;
info->var.activate = FB_ACTIVATE_NOW;
/* 4. 分配虚拟显存 */
info->screen_base = vmalloc(info->fix.smem_len * 2);
if (!info->screen_base) {
ret = -ENOMEM;
goto err_release_fb;
}
info->screen_size = info->fix.smem_len * 2;
info->fix.smem_start = (unsigned long)info->screen_base;
/* 5. 挂载操作函数集 */
info->fbops = &vfb_ops;
/* 6. 注册帧缓冲设备 */
ret = register_framebuffer(info);
if (ret) {
dev_err(&pdev->dev, "Failed to register framebuffer\n");
goto err_free_vmem;
}
platform_set_drvdata(pdev, info);
dev_info(&pdev->dev, "Virtual FB device registered\n");
return 0;
err_free_vmem:
vfree(info->screen_base);
err_release_fb:
framebuffer_release(info);
return ret;
}
常见陷阱:
framebuffer_alloc与framebuffer_release必须成对使用,不能直接kfree。- 虚拟显存使用
vmalloc而非kmalloc,因为帧缓冲通常需要较大的连续虚拟地址空间。 fix.smem_start必须设置为显存的物理地址或虚拟地址(取决于架构),在虚拟驱动中直接赋值为screen_base的虚拟地址。
3.3 remove 函数:设备清理
remove 函数是 probe 的逆操作,负责释放所有已分配的资源:
static int vfb_remove(struct platform_device *pdev)
{
struct fb_info *info = platform_get_drvdata(pdev);
if (!info)
return 0;
/* 1. 注销帧缓冲设备 */
unregister_framebuffer(info);
/* 2. 释放虚拟显存 */
if (info->screen_base)
vfree(info->screen_base);
/* 3. 释放 fb_info 结构体 */
framebuffer_release(info);
dev_info(&pdev->dev, "Virtual FB device removed\n");
return 0;
}
remove 的执行顺序必须与 probe 完全相反,以避免资源泄漏或野指针访问。
3.4 fb_ops 操作函数集实现
fb_ops 是驱动与用户空间交互的核心接口。对于虚拟驱动,我们至少需要实现以下函数:
| 函数 | 功能 | 虚拟驱动实现要点 |
|---|---|---|
fb_open |
设备打开时的初始化 | 可选的,记录打开计数 |
fb_release |
设备关闭时的清理 | 可选的,递减打开计数 |
fb_read |
从帧缓冲读取数据 | 直接 memcpy 到用户空间 |
fb_write |
向帧缓冲写入数据 | 直接 memcpy 从用户空间 |
fb_mmap |
将帧缓冲映射到用户空间 | 使用 vmalloc 的 remap_vmalloc_range |
fb_pan_display |
实现虚拟屏幕滚动 | 更新 var.yoffset 并刷新 |
fb_ioctl |
自定义 ioctl 命令 | 可选的,处理私有命令 |
fb_check_var |
验证用户设置的参数 | 检查分辨率、色深是否合法 |
fb_set_par |
应用新的显示参数 | 更新内部状态,无需硬件操作 |
以下是核心操作函数的实现:
/* mmap 实现:将虚拟显存映射到用户空间 */
static int vfb_mmap(struct fb_info *info, struct vm_area_struct *vma)
{
unsigned long start = vma->vm_start;
unsigned long size = vma->vm_end - vma->vm_start;
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
unsigned long page;
/* 检查映射范围是否合法 */
if (offset + size > info->screen_size)
return -EINVAL;
/* 设置映射属性:非缓存、可写 */
vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
vma->vm_flags |= VM_IO;
/* 逐页映射 */
for (page = 0; page < (size >> PAGE_SHIFT); page++) {
unsigned long pfn = vmalloc_to_pfn(info->screen_base + offset +
(page << PAGE_SHIFT));
if (remap_pfn_range(vma, start + (page << PAGE_SHIFT),
pfn, PAGE_SIZE, vma->vm_page_prot))
return -EAGAIN;
}
return 0;
}
/* pan_display 实现:虚拟屏幕滚动 */
static int vfb_pan_display(struct fb_var_screeninfo *var,
struct fb_info *info)
{
unsigned int yoffset = var->yoffset;
unsigned int line_length = info->fix.line_length;
unsigned int max_yoffset = info->var.yres_virtual - info->var.yres;
/* 检查 yoffset 是否在合法范围内 */
if (yoffset > max_yoffset)
return -EINVAL;
/* 更新 yoffset,实际显示起始地址 = screen_base + yoffset * line_length */
info->var.yoffset = yoffset;
return 0;
}
/* check_var 实现:验证用户参数 */
static int vfb_check_var(struct fb_var_screeninfo *var,
struct fb_info *info)
{
/* 只支持 32bpp 真彩色 */
if (var->bits_per_pixel != 32)
return -EINVAL;
/* 分辨率不能超过虚拟分辨率 */
if (var->xres > VFB_XRES || var->yres > VFB_YRES)
return -EINVAL;
/* 自动填充颜色位域 */
var->red.offset = 16;
var->red.length = 8;
var->green.offset = 8;
var->green.length = 8;
var->blue.offset = 0;
var->blue.length = 8;
return 0;
}
/* 定义 fb_ops 结构体 */
static struct fb_ops vfb_ops = {
.owner = THIS_MODULE,
.fb_open = vfb_open,
.fb_release = vfb_release,
.fb_read = vfb_read,
.fb_write = vfb_write,
.fb_mmap = vfb_mmap,
.fb_pan_display = vfb_pan_display,
.fb_ioctl = vfb_ioctl,
.fb_check_var = vfb_check_var,
.fb_set_par = vfb_set_par,
};
mmap 实现要点: 对于使用
vmalloc 分配的显存,必须使用 vmalloc_to_pfn 获取物理页帧号,然后通过 remap_pfn_range 建立映射。不能直接使用 phys_to_virt 或 __pa,因为 vmalloc 分配的内存物理上可能不连续。
3.5 平台驱动注册与模块化
最后,我们需要将驱动注册为平台驱动,并实现模块的加载/卸载函数:
static struct platform_driver vfb_driver = {
.probe = vfb_probe,
.remove = vfb_remove,
.driver = {
.name = "vfb",
.of_match_table = vfb_of_match, /* 可选:设备树匹配 */
},
};
/* 模块初始化 */
static int __init vfb_init(void)
{
/* 先注册平台设备(模拟硬件存在) */
vfb_device = platform_device_register_simple("vfb", -1, NULL, 0);
if (IS_ERR(vfb_device))
return PTR_ERR(vfb_device);
/* 注册平台驱动 */
return platform_driver_register(&vfb_driver);
}
/* 模块退出 */
static void __exit vfb_exit(void)
{
platform_driver_unregister(&vfb_driver);
platform_device_unregister(vfb_device);
}
module_init(vfb_init);
module_exit(vfb_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Virtual Framebuffer Driver for FBDEV");
下面的 SVG 展示了驱动从加载到卸载的完整生命周期流程:
3.6 编译与测试验证
完成驱动编写后,通过以下步骤进行编译和测试:
- 编译驱动:将
vfb.c放入内核源码树的drivers/video/fbdev/目录,修改Kconfig和Makefile,或直接使用外部模块编译。 - 加载模块:
sudo insmod vfb.ko,检查dmesg输出确认 probe 成功。 - 验证设备节点:
ls /dev/fb*应看到新的/dev/fbX设备。 - 使用 fbset 查看参数:
fbset -i /dev/fbX显示分辨率、色深、显存大小。 - 测试 mmap:编写用户空间程序,通过
mmap映射帧缓冲并绘制像素。 - 测试 pan_display:通过
FBIOPAN_DISPLAYioctl 切换显示缓冲区。 - 卸载模块:
sudo rmmod vfb,确认 remove 函数被调用且设备节点消失。
测试结果预期: 加载驱动后,
fbset 应显示 1024x768 分辨率、32bpp 色深、约 6MB 显存(双缓冲)。通过 mmap 写入像素数据后,虽然无法在物理屏幕上显示,但可以通过 fb_read 回读验证数据一致性。
3.7 本章小结
通过本章的实战,你已经掌握了 FBDEV 驱动的完整开发流程:
- probe:分配资源、初始化参数、注册设备。
- remove:注销设备、释放资源、清理状态。
- ops:实现
mmap、pan_display、check_var等核心操作。 - 模块化:使用平台驱动模型注册和注销。