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/imageblit2D加速回调,供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等。
判断技巧:如果这个参数会随分辨率或色深变化,它属于var;如果它由硬件布线决定、一旦确定就不再改变,它属于fix。例如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和控制台显示路径的必备基础。