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 三者之间的协作关系:

虚拟帧缓冲驱动软件架构图 用户空间应用程序 (fbset, fbdev, 用户程序) 系统调用:open / read / write / mmap / ioctl Linux 内核 FBDEV 核心框架 (fbmem.c) file_operations 分发 / 注册 / 注销 struct fb_ops (操作函数集) fb_open / fb_release / fb_read / fb_write / fb_mmap / fb_pan_display / fb_ioctl vfb 驱动实现 (vfb.c) probe: 分配 fb_info / 设置参数 / 注册设备 remove: 注销设备 / 释放内存 / 清理 fb_info 虚拟显存 (kmalloc 分配的内存区域) screen_base = vmalloc(screen_size)

3.2 probe 函数:设备初始化

probe 是驱动的入口点,负责完成以下关键步骤:

  1. 分配 fb_info 结构体:使用 framebuffer_alloc() 分配,该函数会同时分配私有数据区域。
  2. 设置固定参数 (fix):填写 fb_fix_screeninfo,包括显存起始地址、长度、行字节数等。
  3. 设置可变参数 (var):填写 fb_var_screeninfo,包括分辨率、色深、像素时钟等默认值。
  4. 分配虚拟显存:使用 vmalloc() 或 kzalloc() 分配一块连续内存作为显存。
  5. 注册设备:调用 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 展示了驱动从加载到卸载的完整生命周期流程:

虚拟帧缓冲驱动生命周期流程图 模块加载 (insmod) 注册平台设备 + 驱动 probe 被调用 返回错误码 设备不可用 注册 fbdev 设备 /dev/fbX 可用 正常运行 (ops 调用) read/write/mmap/ioctl

3.6 编译与测试验证

完成驱动编写后,通过以下步骤进行编译和测试:

  1. 编译驱动:将 vfb.c 放入内核源码树的 drivers/video/fbdev/ 目录,修改 Kconfig 和 Makefile,或直接使用外部模块编译。
  2. 加载模块:sudo insmod vfb.ko,检查 dmesg 输出确认 probe 成功。
  3. 验证设备节点:ls /dev/fb* 应看到新的 /dev/fbX 设备。
  4. 使用 fbset 查看参数:fbset -i /dev/fbX 显示分辨率、色深、显存大小。
  5. 测试 mmap:编写用户空间程序,通过 mmap 映射帧缓冲并绘制像素。
  6. 测试 pan_display:通过 FBIOPAN_DISPLAY ioctl 切换显示缓冲区。
  7. 卸载模块:sudo rmmod vfb,确认 remove 函数被调用且设备节点消失。
测试结果预期: 加载驱动后,fbset 应显示 1024x768 分辨率、32bpp 色深、约 6MB 显存(双缓冲)。通过 mmap 写入像素数据后,虽然无法在物理屏幕上显示,但可以通过 fb_read 回读验证数据一致性。

3.7 本章小结

通过本章的实战,你已经掌握了 FBDEV 驱动的完整开发流程:

  • probe:分配资源、初始化参数、注册设备。
  • remove:注销设备、释放资源、清理状态。
  • ops:实现 mmap、pan_display、check_var 等核心操作。
  • 模块化:使用平台驱动模型注册和注销。