3. FBDEV架构

FBDEV(Framebuffer Device)是Linux内核中最早、最通用的显示子系统抽象。它把一块可以被CPU直接访问的显存区域抽象成字符设备/dev/fbX,用户空间通过mmap映射这块内存后,就能像操作普通内存一样绘制像素。理解FBDEV的四个核心要素——fb_info、fb_ops、framebuffer内存管理和ioctl接口,是后续从FBDEV迁移到KMS的心理模型基础。

一、核心原理:fb_info是设备的“总账本”

在FBDEV框架中,每一个显示设备都由一个struct fb_info实例描述。它既是驱动注册到内核的句柄,也是用户空间访问设备的入口。其关键字段可以按职责分为四类:

  • 固定参数:struct fb_fix_screeninfo fix,描述显存的物理地址、长度、行跨距(line_length)、视觉类型等,设备打开后不可更改。
  • 可变参数:struct fb_var_screeninfo var,描述分辨率、虚拟分辨率、色深、像素时钟、时序等,可通过ioctl动态调整。
  • 操作函数集:const struct fb_ops *fbops,指向驱动实现的具体硬件操作。
  • 显存与设备信息:screen_base(虚拟地址)、screen_size(显存大小)、pseudo_palette(伪调色板)、device(父设备)等。

驱动的工作,本质上就是填充这个结构体,然后调用register_framebuffer()把它交给内核。内核会据此创建/dev/fbX字符设备节点,并维护一个全局的registered_fb[]数组。

二、fb_ops:硬件操作的函数指针表

fb_ops是驱动与FBDEV核心之间的契约。核心层不关心硬件如何工作,只负责在合适的时机调用这些回调。典型驱动需要实现的操作包括:

回调函数职责是否必需
fb_check_var校验并修正用户请求的var参数必需
fb_set_par把校验后的参数真正写入硬件必需
fb_setcolreg设置调色板寄存器(伪彩色/索引色)视色深而定
fb_fillrect硬件加速矩形填充可选
fb_copyarea硬件加速区域拷贝可选
fb_imageblit硬件加速图像位块传输可选
fb_blank背光/显示开关控制可选
fb_pan_display调整显示起始偏移,实现平移可选

如果驱动不实现fb_fillrect等加速回调,FBDEV核心会使用cfb_fillrect等软件回退函数,直接操作screen_base指向的内存。这也是FBDEV驱动“能跑起来”门槛较低的原因。

三、framebuffer内存管理

FBDEV的显存管理相对朴素,通常分为两种模式:

  1. 独立显存:显卡或显示控制器自带VRAM,驱动通过ioremap把物理地址映射到内核虚拟地址,赋给screen_base。
  2. 系统内存DMA:SoC的显示控制器没有独立显存,驱动用dma_alloc_coherent或dma_alloc_writecombine从系统内存分配一块连续区域,同时得到CPU虚拟地址和DMA物理地址。

显存布局由var.xres_virtual、var.yres_virtual和var.bits_per_pixel共同决定,实际占用大小为:

size = xres_virtual * yres_virtual * bpp / 8
line_length = xres_virtual * bpp / 8

当yres_virtual > yres时,就形成了双缓冲或Pan Display机制:用户空间在不可见区域绘制,再通过FBIOPAN_DISPLAY切换var.yoffset,实现无撕裂翻页。

在嵌入式SoC中,显示控制器往往要求framebuffer物理地址对齐到页边界甚至更大的粒度,分配显存时需关注DMA对齐要求,否则可能出现花屏或DMA传输异常。

四、ioctl接口与用户空间交互

用户空间通过/dev/fbX上的ioctl获取和设置显示信息,最常用的命令如下:

  • FBIOGET_FSCREENINFO:读取固定参数,获取显存长度和行跨距。
  • FBIOGET_VSCREENINFO:读取可变参数,获取当前分辨率和色深。
  • FBIOPUT_VSCREENINFO:设置可变参数,触发fb_check_var和fb_set_par。
  • FBIOPAN_DISPLAY:设置yoffset,实现画面平移或翻页。
  • FBIOBLANK:控制显示开关和电源状态。

一个典型的用户空间初始化流程如下:

int fd = open("/dev/fb0", O_RDWR);
struct fb_var_screeninfo var;
struct fb_fix_screeninfo fix;

ioctl(fd, FBIOGET_VSCREENINFO, &var);
ioctl(fd, FBIOGET_FSCREENINFO, &fix);

size_t fb_size = fix.line_length * var.yres_virtual;
char *fb = mmap(NULL, fb_size, PROT_READ | PROT_WRITE,
                MAP_SHARED, fd, 0);

/* 直接向fb写入像素数据 */
memset(fb, 0x00, fb_size);

可以看到,FBDEV把“显示”简化成了“往一块内存写数据”,这也是它在早期嵌入式系统中广受欢迎的原因。

五、典型驱动结构

一个完整的FBDEV驱动通常遵循以下骨架:

static int xxx_probe(struct platform_device *pdev)
{
    struct fb_info *info;
    struct xxx_par *par;

    info = framebuffer_alloc(sizeof(*par), &pdev->dev);
    par = info->par;

    /* 1. 映射或分配显存 */
    info->screen_base = dma_alloc_writecombine(...);
    info->fix.smem_start = dma_addr;
    info->fix.smem_len = size;
    info->fix.line_length = line_length;

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

    /* 3. 绑定操作函数集 */
    info->fbops = &xxx_fbops;

    /* 4. 注册 */
    register_framebuffer(info);
    return 0;
}

对应的fb_ops实现中,fb_check_var负责参数合法性检查,fb_set_par负责把参数写入显示控制器寄存器,fb_blank负责背光控制。如果硬件支持2D加速,再实现fb_fillrect等回调。

六、误区与复盘

误区一:认为FBDEV就是“直接写显存”。实际上,用户空间写入的是映射后的虚拟地址,硬件扫描的是物理地址,两者通过DMA或显示控制器关联,缓存一致性必须由驱动保证。

误区二:忽略fb_check_var的修正职责。用户请求的分辨率可能不被硬件支持,驱动应在fb_check_var中调整var,而不是在fb_set_par中直接拒绝。

误区三:把line_length等同于xres * bpp / 8。当存在行对齐要求时,line_length可能大于理论值,用户空间必须使用fix.line_length计算偏移。

FBDEV的简洁性也带来了局限:它只描述“一块内存”,不描述显示管线、图层、原子提交和同步机制。当系统需要多图层合成、页面翻转与垂直同步精确配合时,FBDEV就显得力不从心,这正是KMS/D RM逐步取代它的根本原因。理解FBDEV的这四个核心概念,才能在迁移到KMS时清楚“哪些职责被保留、哪些被重新抽象”。

FBDEV 架构核心组成与数据流 用户空间 open /dev/fb0 ioctl 获取参数 mmap 映射显存 直接写像素 ioctl/mmap struct fb_info fix:固定参数(地址/长度/行跨距) var:可变参数(分辨率/色深/时序) fbops → 硬件操作函数集 screen_base / screen_size 调用 struct fb_ops fb_check_var fb_set_par fb_setcolreg fb_fillrect / copyarea fb_blank / pan_display Framebuffer 显存区域 size = xres_virtual × yres_virtual × bpp / 8 line_length = xres_virtual × bpp / 8 screen_base 指向 显示控制器 / DMA 扫描显存 → 输出时序 LCD / HDMI 面板 读取 关键交互路径: ① 用户空间 ioctl 读写 var/fix → ② 驱动 fb_check_var 校验 → ③ fb_set_par 写寄存器 → ④ mmap 映射 screen_base → ⑤ 用户写像素 → ⑥ 显示控制器扫描输出 FBDEV 核心:register_framebuffer() 注册 fb_info,内核创建 /dev/fbX 并维护 registered_fb[] 数组