4. FBDEV驱动实战

本节我们不再停留在概念层面,而是从零编写一个可加载、可测试的虚拟帧缓冲驱动。所谓“虚拟”,是指它不绑定任何真实显示控制器,而是用内核态内存模拟一块显存,通过 fb_ops 回调把这块内存暴露成标准 /dev/fbX 设备。这样既能完整走通 FBDEV 驱动的注册流程,又不会因硬件差异干扰学习重点。

核心目标:实现 fb_ops 中最关键的 fb_fillrect、fb_copyarea、fb_imageblit 三个回调,完成 register_framebuffer() 注册,并通过 fbset、fbv 或直接写 /dev/fb0 验证显示效果。

4.1 虚拟帧缓冲的整体结构

一个 FBDEV 驱动的最小骨架由三部分组成:帧缓冲信息结构 struct fb_info、显存缓冲区、以及 fb_ops 操作集。内核通过 fb_info 统一管理分辨率、色深、显存地址和回调函数。下图展示了虚拟帧缓冲从模块加载到用户空间访问的完整数据通路。

虚拟帧缓冲驱动数据通路 用户空间 fbset / fbv write(/dev/fb0) mmap 显存 FBDEV 核心层 fbmem.c register_framebuffer() fb_ops 分发 虚拟驱动模块 vfb_init() fb_info 填充 fb_ops 注册 系统调用 回调调用 显存缓冲区 vmalloc / __get_free_pages screen_base 指针 分配并映射 fb_ops 回调 fb_fillrect fb_copyarea / fb_imageblit 操作 关键流程: 1. 模块加载 → 分配 fb_info 与显存 → 填充 var/fix 参数 → 注册 fb_ops → register_framebuffer() 2. 用户写入 /dev/fb0 → 内核调用 fb_ops 回调 → 操作显存 → 屏幕内容更新

4.2 关键回调的实现要点

FBDEV 核心层并不直接操作显存,而是把绘制请求转交给驱动提供的 fb_ops。对于虚拟帧缓冲,我们只需实现三个最基础的加速回调,其余可留空或使用内核默认实现。

回调函数 触发场景 虚拟驱动实现策略
fb_fillrect 控制台清屏、填充色块 按色深逐像素写入 screen_base
fb_copyarea 滚动、区域拷贝 逐行 memcpy,注意重叠方向
fb_imageblit 显示 Logo、文字位图 解析 monochrome 位图并展开为像素
fb_set_par fbset 修改分辨率 虚拟驱动可返回 0 或重新分配显存
fb_blank DPMS 息屏 虚拟驱动可空实现

下面给出 fb_fillrect 的核心实现片段,假设色深为 32bpp,info->screen_base 指向显存起始地址:

static void vfb_fillrect(struct fb_info *info,
                         const struct fb_fillrect *rect)
{
    u32 __iomem *dst;
    u32 color = rect->color;
    int y;

    for (y = 0; y < rect->height; y++) {
        dst = (u32 __iomem *)info->screen_base
            + (rect->dy + y) * info->fix.line_length / 4
            + rect->dx;
        memset32(dst, color, rect->width);
    }
}
memset32 是内核提供的按 32 位填充函数,比逐字节循环快得多。若色深为 16bpp,应改用 memset16,并相应调整指针类型。

4.3 模块加载与注册流程

驱动以模块形式加载,入口函数完成 fb_info 的分配、参数填充和注册。退出函数则反向释放资源。典型流程如下:

  1. 调用 framebuffer_alloc() 分配 fb_info 结构。
  2. 设置 var 中的分辨率、色深、虚拟分辨率;设置 fix 中的显存长度和行长度。
  3. 使用 vmalloc() 或 __get_free_pages() 分配显存,并赋给 screen_base。
  4. 填充 fb_ops 结构体,绑定回调函数。
  5. 调用 register_framebuffer() 注册,成功后出现 /dev/fbX。
  6. 模块卸载时调用 unregister_framebuffer()、释放显存和 fb_info。
static int __init vfb_init(void)
{
    struct fb_info *info;
    int ret;

    info = framebuffer_alloc(0, NULL);
    if (!info)
        return -ENOMEM;

    info->var.xres = 640;
    info->var.yres = 480;
    info->var.bits_per_pixel = 32;
    info->var.xres_virtual = 640;
    info->var.yres_virtual = 480;

    info->fix.smem_len = 640 * 480 * 4;
    info->fix.line_length = 640 * 4;

    info->screen_base = vmalloc(info->fix.smem_len);
    if (!info->screen_base) {
        framebuffer_release(info);
        return -ENOMEM;
    }

    info->fbops = &vfb_ops;
    ret = register_framebuffer(info);
    if (ret) {
        vfree(info->screen_base);
        framebuffer_release(info);
        return ret;
    }
    return 0;
}

4.4 加载测试与常见误区

编译出 vfb.ko 后,使用 insmod vfb.ko 加载,通过 dmesg 查看注册日志,再用 ls /dev/fb* 确认设备节点。测试时可使用 fbset -i 查看参数,或用 fbv 显示图片,也可以直接向 /dev/fb0 写入数据观察变化。

$ make -C /lib/modules/$(uname -r)/build M=$PWD modules
$ sudo insmod vfb.ko
$ dmesg | tail -5
[ 1234.567] vfb: framebuffer registered, 640x480-32
$ ls /dev/fb*
/dev/fb0
$ sudo fbset -i
mode "640x480-32"
    geometry 640 480 640 480 32
endmode
常见误区一:忘记设置 fix.line_length,导致内核按 0 计算偏移,画面错乱。误区二:显存用 kmalloc 分配但未考虑大分辨率,超过 4MB 时可能失败,应优先使用 vmalloc。误区三:fb_fillrect 中未按色深选择 memset32 或 memset16,造成颜色异常。误区四:卸载模块时未调用 unregister_framebuffer(),导致 /dev/fb0 残留。

复盘整个流程,虚拟帧缓冲驱动的价值在于用最小代码量打通 FBDEV 的注册、回调分发和用户接口三条链路。掌握它之后,再迁移到真实硬件驱动时,只需替换显存分配方式和回调中的硬件操作,整体框架完全一致。这也是后续理解 KMS 迁移时,能够清晰对比两种显示模型差异的重要基础。