2. Linux Framebuffer驱动框架:fbmem.c分析、fb_ops结构体、注册与注销流程

好,我们直接进入正题。上一章我们聊了Framebuffer是什么,这一章我们深入内核源码,看看它到底是怎么工作的。

Linux Framebuffer驱动框架,说白了就是一套标准化的“画布管理方案”。内核通过fbmem.c这个核心文件,把复杂的硬件操作抽象成统一的接口。你想想看,如果没有这套框架,每个显卡驱动都得自己实现open、ioctl、mmap这些系统调用,那得多乱?

2.1 fbmem.c:Framebuffer的“大管家”

fbmem.c位于内核源码的drivers/video/fbdev/core/目录下。它不直接操作硬件,而是负责三件事:

  • 注册字符设备:向内核注册主设备号为29的FB设备。
  • 提供统一的文件操作接口:实现fb_open、fb_read、fb_write、fb_ioctl、fb_mmap等。
  • 管理全局的fb_info链表:维护系统中所有注册的Framebuffer设备。

我个人习惯把fbmem.c看作一个“调度中心”。它不干活,但它知道谁在干活,以及怎么把活派下去。

核心数据结构:struct fb_info

每个Framebuffer设备对应一个struct fb_info实例。这个结构体就是驱动开发者需要填充的“简历”。

struct fb_info {
    struct fb_var_screeninfo var;   // 可变参数:分辨率、位深、时序
    struct fb_fix_screeninfo fix;   // 固定参数:显存地址、长度、行宽
    struct fb_ops *fbops;           // 操作函数集(重点!)
    struct device *device;          // 设备模型指针
    char __iomem *screen_base;      // 显存虚拟地址
    unsigned long screen_size;      // 显存大小
    // ... 还有其他成员,但上面这几个是核心
};

我在项目中遇到过好几次这样的问题:驱动加载成功了,/dev/fb0也出现了,但应用程序一mmap就崩溃。查到最后,都是screen_base没有正确映射。嗯,这个坑后面我们会专门讲。

2.2 fb_ops结构体:驱动与内核的“契约”

struct fb_ops是驱动开发者必须实现的函数指针集合。它定义了Framebuffer设备能做什么。

函数指针 作用 是否必须实现
fb_open 设备打开时的初始化 可选
fb_release 设备关闭时的清理 可选
fb_read 从显存读取数据 可选(默认用通用实现)
fb_write 向显存写入数据 可选(默认用通用实现)
fb_check_var 校验用户设置的显示参数 必须
fb_set_par 根据校验后的参数配置硬件 必须
fb_setcolreg 设置颜色寄存器(伪彩色时用) 可选
fb_blank 控制屏幕开关/休眠 可选
fb_pan_display 实现显示偏移(双缓冲常用) 可选
fb_mmap 将显存映射到用户空间 可选(默认用通用实现)
fb_ioctl 处理自定义ioctl命令 可选

避坑指南:我曾经见过一个驱动,fb_check_var里直接返回0,什么都不做。结果用户设置了一个硬件不支持的时序,屏幕直接黑掉。记住,fb_check_var是你的最后一道防线,一定要校验所有参数。

这里有个细节:fb_read和fb_write的通用实现是通过screen_base直接拷贝数据。如果你的硬件显存不是线性映射的(比如某些DSP或GPU),就必须自己实现这两个函数。

2.3 注册与注销流程:从零到一

我们来看一个典型的Framebuffer驱动加载流程。假设你写了一个简单的LCD控制器驱动:

注册流程(以module_init为例)

  1. 分配fb_info:调用framebuffer_alloc()分配并初始化struct fb_info。
  2. 填充固定参数:设置fix结构体,包括ID字符串、显存物理地址、行宽等。
  3. 填充可变参数:设置var结构体,包括默认分辨率、位深、上下边距等。
  4. 设置fb_ops:将实现的函数指针赋值给info->fbops。
  5. 分配显存:通过dma_alloc_coherent()或ioremap()分配/映射显存,赋值给screen_base。
  6. 注册设备:调用register_framebuffer(struct fb_info *info)。
static int myfb_probe(struct platform_device *pdev)
{
    struct fb_info *info;
    int ret;

    // 1. 分配fb_info
    info = framebuffer_alloc(0, &pdev->dev);
    if (!info)
        return -ENOMEM;

    // 2. 填充固定参数
    strcpy(info->fix.id, "myfb");
    info->fix.smem_start = dma_addr;  // 显存物理地址
    info->fix.smem_len = SZ_4M;       // 显存大小
    info->fix.line_length = width * (bpp / 8);

    // 3. 填充可变参数
    info->var.xres = 1024;
    info->var.yres = 600;
    info->var.bits_per_pixel = 32;

    // 4. 设置fb_ops
    info->fbops = &myfb_ops;

    // 5. 分配显存
    info->screen_base = dma_alloc_coherent(...);
    info->screen_size = SZ_4M;

    // 6. 注册设备
    ret = register_framebuffer(info);
    if (ret) {
        framebuffer_release(info);
        return ret;
    }

    platform_set_drvdata(pdev, info);
    return 0;
}

个人经验:register_framebuffer()内部会做几件重要的事:将fb_info加入全局链表、创建/dev/fbX设备节点、调用fb_set_par()初始化硬件。所以你的fb_set_par在注册时就会被调用一次,务必保证此时硬件已经就绪。

注销流程

注销就简单多了,但顺序不能乱:

  1. 调用unregister_framebuffer(info):移除设备节点,从全局链表删除。
  2. 释放显存:dma_free_coherent()或iounmap()。
  3. 调用framebuffer_release(info):释放fb_info结构体。
static int myfb_remove(struct platform_device *pdev)
{
    struct fb_info *info = platform_get_drvdata(pdev);

    unregister_framebuffer(info);   // 先注销
    dma_free_coherent(...);         // 再释放显存
    framebuffer_release(info);      // 最后释放info
    return 0;
}

我曾经踩过的坑:在unregister_framebuffer之后立即framebuffer_release,但忘了检查是否有用户空间程序还在通过mmap访问显存。结果内核直接Oops。正确的做法是:unregister_framebuffer会等待所有文件描述符关闭,但mmap的页面还在。最好在驱动中实现fb_release的引用计数,确保所有用户都释放了再清理。

2.4 核心流程图解

下面这张图展示了从驱动加载到用户空间访问的完整链路:

Framebuffer驱动注册与访问流程 用户空间应用 系统调用:open/ioctl/mmap fbmem.c 核心框架 fb_ops 函数指针集 具体硬件驱动 struct fb_info 实例 struct fb_var_screeninfo (可变参数) struct fb_fix_screeninfo (固定参数) screen_base / screen_size (显存映射) 关键点:register_framebuffer() 将 fb_info 注册到内核,同时触发 fb_set_par() 初始化硬件

2.5 关键数据结构关系

最后,我用一个表格总结一下fbmem.c中几个核心数据结构的关系:

数据结构 所属模块 作用 生命周期
struct fb_info fbmem.c 每个FB设备的完整描述 从注册到注销
struct fb_ops 驱动实现 硬件操作函数指针集 与fb_info绑定
struct fb_var_screeninfo fbmem.c 用户可修改的显示参数 每次ioctl可能更新
struct fb_fix_screeninfo fbmem.c 硬件固定的显示参数 驱动加载后不变
struct fb_cmap fbmem.c 颜色映射表(伪彩色) 按需设置

嗯,这一章的内容就到这里。记住,fbmem.c是框架,fb_ops是契约,注册注销是流程。搞懂这三样,Framebuffer驱动的基本骨架你就拿下了。