3. Linux帧缓冲(FBDEV)架构
FBDEV(Frame Buffer Device)是Linux内核中最早、最经典的显示子系统抽象。它把一块可以被CPU直接读写的显存,抽象成一个字符设备/dev/fbX,用户空间通过mmap映射显存、通过ioctl查询和设置显示参数。理解FBDEV架构,是理解后续KMS/DRM为何要取代它的关键前提——因为FBDEV的每一个设计局限,几乎都对应着DRM的一个核心概念。
一、核心对象:fb_info 是整块屏幕的“总控台”
FBDEV的核心数据结构是struct fb_info,它定义在include/linux/fb.h中,代表一个帧缓冲设备的全部状态。一个fb_info实例对应一个/dev/fbX节点,它把“硬件操作”“显示参数”“显存信息”“设备模型”四类信息聚合在一起。
struct fb_var_screeninfo var:可变参数,描述当前显示模式,如分辨率、虚拟分辨率、色深、像素时钟、时序等,用户可修改。struct fb_fix_screeninfo fix:固定参数,描述显存物理属性,如显存起始物理地址、显存长度、行跨度line_length、加速能力等,用户只读。const struct fb_ops *fbops:硬件操作函数集,是驱动与硬件交互的入口。struct device *device:嵌入设备模型,用于sysfs、电源管理等。char __iomem *screen_base:显存的内核虚拟地址,mmap时映射给用户。struct fb_info *next:全局链表registered_fb[]的挂接指针。
可以把fb_info理解为“显存 + 参数 + 操作函数 + 设备节点”的集合体。驱动要做的事,就是填充这个结构体,然后把它注册进内核。
二、fb_ops:驱动必须实现的硬件操作集
fb_ops是FBDEV的“驱动接口层”,用户空间的ioctl、read、write、mmap最终都会被翻译成对fb_ops中函数指针的调用。它不是每个函数都必须实现,但核心几个缺一不可。
| 函数指针 | 作用 | 是否必需 |
|---|---|---|
fb_check_var | 校验并修正用户传入的var参数,如分辨率、色深是否被硬件支持 | 必需 |
fb_set_par | 把校验后的var真正写入硬件寄存器,切换显示模式 | 必需 |
fb_setcolreg | 设置调色板寄存器,用于伪彩色或8bpp以下模式 | 视模式而定 |
fb_blank | 背光/显示开关控制,对应FBIOBLANK | 可选 |
fb_pan_display | 设置显示起始偏移,实现双缓冲/平移 | 可选 |
fb_fillrect/copyarea/imageblit | 2D加速回调,供fbcon和控制台使用 | 可选 |
fb_mmap | 自定义mmap实现,多数驱动用fb_deferred_io或默认实现 | 可选 |
典型的最小实现只需要fb_check_var和fb_set_par,其余交给内核默认逻辑。这也是FBDEV“简单”的来源,但简单也意味着能力有限。
三、var 与 fix:可变参数与固定参数的分工
初学者最容易混淆的就是fb_var_screeninfo和fb_fix_screeninfo。一句话区分:var是“用户想怎么显示”,fix是“硬件实际长什么样”。
var包含xres/yres(可见分辨率)、xres_virtual/yres_virtual(虚拟分辨率,用于双缓冲)、bits_per_pixel、red/green/blue/transp四个fb_bitfield(描述各颜色分量在像素中的偏移和长度)、pixclock、left_margin等时序参数。fix包含smem_start(显存物理起始地址)、smem_len(显存字节数)、line_length(一行字节数,通常等于xres_virtual * bpp/8)、type、visual、accel等。
line_length会随xres_virtual变化,但它在一次模式设置后是固定的,因此放在fix中。四、注册与注销流程
FBDEV驱动的生命周期围绕register_framebuffer()和unregister_framebuffer()展开。下面是一个典型的probe流程:
static int myfb_probe(struct platform_device *pdev)
{
struct fb_info *info;
int ret;
info = framebuffer_alloc(0, &pdev->dev);
if (!info)
return -ENOMEM;
/* 1. 填充 fix */
info->fix.smem_start = res->start;
info->fix.smem_len = resource_size(res);
info->fix.line_length = info->var.xres_virtual * info->var.bits_per_pixel / 8;
info->fix.type = FB_TYPE_PACKED_PIXELS;
info->fix.visual = FB_VISUAL_TRUECOLOR;
/* 2. 填充 var */
info->var.xres = 1920;
info->var.yres = 1080;
info->var.xres_virtual = 1920;
info->var.yres_virtual = 2160; /* 双缓冲 */
info->var.bits_per_pixel = 32;
/* 3. 挂接 fb_ops */
info->fbops = &myfb_ops;
/* 4. 映射显存 */
info->screen_base = ioremap(info->fix.smem_start, info->fix.smem_len);
info->screen_size = info->fix.smem_len;
/* 5. 注册 */
ret = register_framebuffer(info);
if (ret)
goto err_unmap;
platform_set_drvdata(pdev, info);
return 0;
}
注销流程则相反:先unregister_framebuffer(info),再iounmap释放显存映射,最后framebuffer_release(info)释放结构体内存。顺序不能颠倒,否则会出现用户空间仍持有/dev/fbX引用而内核已释放资源的竞态。
五、用户空间视角与典型场景
用户空间通过标准文件接口操作FBDEV:
# 查看当前模式
fbset -i
# 查询fix信息
cat /sys/class/graphics/fb0/virtual_size
# 映射显存并直接写像素
int fd = open("/dev/fb0", O_RDWR);
struct fb_var_screeninfo var;
ioctl(fd, FBIOGET_VSCREENINFO, &var);
size_t size = var.xres_virtual * var.yres_virtual * var.bits_per_pixel / 8;
char *fb = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
memset(fb, 0x00, size); /* 清屏为黑 */
这种“直接写显存”的模型在嵌入式设备、早期Android、控制台fbcon中非常常见。它的优点是简单、零拷贝、无需合成器;缺点是缺乏多图层、原子提交、显存管理和GPU同步能力。
六、常见误区与复盘
误区一:把var和fix混用。在fb_check_var中修改fix字段是无效的,因为fix在注册后基本不再变化;模式切换应只改var,并在fb_set_par中同步更新line_length等派生量。
误区二:忘记设置xres_virtual。如果xres_virtual小于xres,内核会拒绝注册;如果只等于xres,则无法使用fb_pan_display做双缓冲。
误区三:在fb_set_par中直接操作未映射的寄存器。应先确保时钟、电源、IO映射都已就绪,否则会出现挂起或花屏。
误区四:注销顺序错误。必须先unregister_framebuffer再释放显存,否则用户态mmap的页会指向已释放内存,触发oops。
从架构上看,FBDEV把“显存”当作唯一的一等公民,所有显示能力都围绕一块线性缓冲区展开。这正是它在多图层、GPU合成、原子显示时代逐渐被DRM/KMS取代的根本原因。但掌握fb_info、fb_ops、var/fix以及注册注销流程,仍然是阅读大量遗留驱动、理解fbcon和控制台显示路径的必备基础。