3. 帧缓冲设备注册:register_framebuffer()、unregister_framebuffer()、设备号分配

帧缓冲设备(/dev/fbX)的注册与注销是显示驱动与用户空间交互的桥梁。内核通过 register_framebuffer() 将驱动创建的 fb_info 结构体注册到帧缓冲子系统,并自动分配主次设备号;unregister_framebuffer() 则完成反向清理。本章深入剖析这两个核心 API 的实现机制、设备号分配策略及调用实战要点。

3.1 设备号分配机制

帧缓冲设备的主设备号固定为 29,次设备号从 0 开始递增,每个设备对应一个独立的帧缓冲实例。内核通过 FB_MAJOR 宏定义主设备号,次设备号由帧缓冲核心层在注册时动态分配。

参数 值 说明
主设备号 29 由内核 include/linux/major.h 定义
次设备号范围 0 ~ 31 默认最多支持 32 个帧缓冲设备
设备节点 /dev/fb0 ~ /dev/fb31 用户空间通过 open 系统调用访问
分配方式 动态分配 由 register_framebuffer() 内部完成
实战提示:在嵌入式系统中,若同时存在 LCD 显示控制器和 HDMI 输出,通常 /dev/fb0 分配给主屏,/dev/fb1 分配给副屏。驱动开发时需确保 fb_info->node 字段在注册前设置为 -1,以触发内核自动分配次设备号。

3.2 register_framebuffer() 核心流程

register_framebuffer() 是帧缓冲设备注册的入口函数,定义在 drivers/video/fbdev/core/fbmem.c 中。其执行流程可分为四个阶段:

  1. 参数校验:检查 fb_info 指针及关键回调函数(如 fb_open、fb_release)是否为空。
  2. 设备号分配:遍历全局帧缓冲设备数组 registered_fb[],找到第一个空闲槽位,将槽位索引作为次设备号。
  3. 设备注册:调用 device_create() 在 sysfs 中创建设备节点,并初始化 fb_info 内部的锁与等待队列。
  4. 回调通知:通过 fb_notifier_call_chain() 通知注册到帧缓冲事件链的监听者(如控制台驱动)。
关键数据结构:内核维护一个全局数组 struct fb_info *registered_fb[FB_MAX],其中 FB_MAX 默认为 32。每个成功注册的 fb_info 指针被存入该数组,次设备号即数组索引。
register_framebuffer() 注册流程图 register_framebuffer() 阶段1:参数校验 (fb_info 非空检查) 阶段2:遍历 registered_fb[] 分配次设备号 阶段3:device_create() 创建 sysfs 设备节点 阶段4:fb_notifier_call_chain() 通知监听者 注册完成,返回次设备号 说明:若阶段2未找到空闲槽位,返回 -ENOSPC;阶段3失败则回滚槽位释放

3.3 unregister_framebuffer() 注销流程

设备注销是注册的逆过程,由 unregister_framebuffer() 完成。该函数在驱动卸载或设备热插拔时被调用,执行以下操作:

  • 状态检查:确认 fb_info 当前未被用户空间打开(fb_info->count 为 0),若被占用则返回 -EBUSY。
  • 通知注销:调用 fb_notifier_call_chain() 发送 FB_EVENT_FB_UNREGISTER 事件,通知控制台等模块清理资源。
  • 移除设备:调用 device_destroy() 删除 sysfs 设备节点。
  • 释放槽位:将 registered_fb[minor] 置为 NULL,释放次设备号。
常见错误:在 unregister_framebuffer() 之前未释放 fb_info 中的 screen_base 和 cmap 动态内存,会导致内存泄漏。标准做法是在驱动 remove 回调中先调用 unregister_framebuffer(),再释放私有数据。

3.4 驱动实战代码模板

以下是一个典型的帧缓冲设备注册与注销代码片段,展示 fb_info 的初始化、注册及清理流程:

#include <linux/fb.h>
#include <linux/module.h>

static struct fb_info *my_fb_info;

static int my_fb_probe(struct platform_device *pdev)
{
    int ret;

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

    // 2. 初始化固定和可变参数
    my_fb_info->fix = (struct fb_fix_screeninfo){
        .id          = "my_fb",
        .smem_len    = 800 * 480 * 4,  // 800x480 32bpp
        .type        = FB_TYPE_PACKED_PIXELS,
        .visual      = FB_VISUAL_TRUECOLOR,
        .line_length = 800 * 4,
    };

    my_fb_info->var = (struct fb_var_screeninfo){
        .xres        = 800,
        .yres        = 480,
        .bits_per_pixel = 32,
    };

    // 3. 设置操作回调
    my_fb_info->fbops = &my_fb_ops;
    my_fb_info->screen_base = dma_alloc_coherent(...);
    my_fb_info->screen_size = 800 * 480 * 4;

    // 4. 注册帧缓冲设备
    ret = register_framebuffer(my_fb_info);
    if (ret < 0) {
        dev_err(&pdev->dev, "register_framebuffer failed: %d\n", ret);
        framebuffer_release(my_fb_info);
        return ret;
    }

    dev_info(&pdev->dev, "fb registered as /dev/fb%d\n", my_fb_info->node);
    return 0;
}

static int my_fb_remove(struct platform_device *pdev)
{
    // 1. 注销帧缓冲设备
    unregister_framebuffer(my_fb_info);

    // 2. 释放 DMA 缓冲
    dma_free_coherent(...);

    // 3. 释放 fb_info 结构体
    framebuffer_release(my_fb_info);

    return 0;
}

3.5 设备注册状态查询

驱动开发调试阶段,可通过 /proc/fb 文件查看当前系统中所有已注册的帧缓冲设备信息:

# cat /proc/fb
0 my_fb
1 s3c-fb

每行格式为:次设备号 驱动名称。该文件由帧缓冲核心层自动维护,驱动无需额外操作。

调试手段 命令/方法 获取信息
查看已注册设备 cat /proc/fb 次设备号与驱动名称
查看设备详细信息 fbset -i 分辨率、像素格式、显存地址
查看 sysfs 属性 ls /sys/class/graphics/fb0/ 设备属性文件列表
动态注册测试 modprobe my_fb_driver 观察 dmesg 输出注册信息

3.6 多设备注册与次设备号管理

当系统存在多个帧缓冲设备时,内核按注册顺序依次分配次设备号。驱动可通过 fb_info->node 字段获取分配后的次设备号,用于日志输出或设备节点命名。

设计模式:在支持双屏显示的嵌入式平台中,通常主屏驱动先调用 register_framebuffer() 获得 /dev/fb0,副屏驱动后调用获得 /dev/fb1。若需固定设备号,可在设备树中指定 video->fb-id 属性,驱动通过解析该属性设置 fb_info->node 为固定值。
多帧缓冲设备注册与次设备号分配示意 registered_fb[] 内核全局数组 (FB_MAX = 32) fb0 主屏LCD fb1 副屏HDMI fb2 VGA输出 空闲 ... 空闲 说明:次设备号 = 数组索引,注册时从索引0开始查找第一个空闲槽位 注销时对应槽位置为 NULL,后续注册可复用该次设备号

3.7 常见问题与避坑指南

  • 重复注册:同一 fb_info 指针不可重复调用 register_framebuffer(),否则内核 panic。应在驱动 probe 中确保只注册一次。
  • 设备号耗尽:当系统注册超过 32 个帧缓冲设备时,register_framebuffer() 返回 -ENOSPC。可通过内核配置 CONFIG_FB_MAX 增大上限。
  • 注销顺序:必须在 unregister_framebuffer() 之后释放 screen_base 和 cmap,因为注销过程中可能还有控制台操作访问显存。
  • 休眠上下文:register_framebuffer() 内部可能调用 device_create() 触发 uevent,因此不能在原子上下文或自旋锁保护区域中调用。
调试技巧:在驱动 probe 函数中加入 dev_info(dev, "fb node=%d\n", my_fb_info->node);,配合 dmesg 观察设备号分配情况。若发现 node 值异常(如 -1),说明注册失败,需检查返回值。