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 中。其执行流程可分为四个阶段:
- 参数校验:检查
fb_info指针及关键回调函数(如fb_open、fb_release)是否为空。 - 设备号分配:遍历全局帧缓冲设备数组
registered_fb[],找到第一个空闲槽位,将槽位索引作为次设备号。 - 设备注册:调用
device_create()在 sysfs 中创建设备节点,并初始化fb_info内部的锁与等待队列。 - 回调通知:通过
fb_notifier_call_chain()通知注册到帧缓冲事件链的监听者(如控制台驱动)。
关键数据结构:内核维护一个全局数组
struct fb_info *registered_fb[FB_MAX],其中 FB_MAX 默认为 32。每个成功注册的 fb_info 指针被存入该数组,次设备号即数组索引。
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 为固定值。
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),说明注册失败,需检查返回值。