2. fbdev基础回顾

在把显示驱动从 fbdev 迁移到 DRM KMS 之前,必须先回到 fbdev 的原始模型,理解它到底向用户空间暴露了什么、内核内部用什么结构承载这些能力。fbdev 的设计目标非常朴素:把一块可以被 CPU 直接读写、由显示控制器扫描输出的显存抽象成字符设备 /dev/fbX,让应用通过 mmap 拿到像素缓冲,通过 ioctl 查询和修改显示参数。它不负责 GPU 命令提交,也不负责多图层合成,只解决“帧缓冲”这一件事。

一、framebuffer 设备模型:从 /dev/fbX 到显存

fbdev 的设备模型可以概括为“一个字符设备 + 一段显存 + 一组显示参数”。内核为每个 framebuffer 注册一个 fb_info,并创建对应的字符设备节点 /dev/fb0、/dev/fb1。用户空间通过 open 打开设备,通过 ioctl 获取 fb_var_screeninfo 和 fb_fix_screeninfo,再通过 mmap 把显存映射到进程地址空间,之后直接写像素即可看到画面变化。

这里有两个关键结构:fb_var_screeninfo 描述“可变”的显示参数,如分辨率、虚拟分辨率、色深、像素时钟、时序;fb_fix_screeninfo 描述“固定”的硬件属性,如显存物理地址、显存长度、行跨度 line_length、加速器类型。应用通常先读 var 判断是否支持目标分辨率,再读 fix 计算 mmap 长度和行偏移。

fbdev 的核心假设是:显存是线性可映射的,像素格式是固定的,显示控制器只扫描这一块缓冲。这个假设在简单 SoC 显示上成立,但在多图层、多平面、原子提交场景下就会成为限制。

二、fb_info 结构:fbdev 驱动的中枢

struct fb_info 是 fbdev 驱动的核心数据结构,它把设备、显存、操作集和显示参数全部聚合在一起。典型字段包括:

  • struct fb_var_screeninfo var:当前可变显示参数;
  • struct fb_fix_screeninfo fix:固定硬件参数;
  • struct fb_ops *fbops:驱动实现的操作集;
  • struct device *device:关联的设备模型对象;
  • struct fb_info *next:全局 fb 链表指针;
  • char __iomem *screen_base:显存虚拟基地址;
  • unsigned long screen_size:显存大小;
  • struct fb_monspecs monspecs:显示器 EDID 信息;
  • struct work_struct deferred_work:延迟工作,用于热插拔或模式变更。

驱动初始化时通常调用 framebuffer_alloc() 分配 fb_info,填充 var/fix/fbops,再调用 register_framebuffer() 注册。注销时调用 unregister_framebuffer() 和 framebuffer_release()。整个生命周期围绕 fb_info 展开,因此迁移到 DRM 时,第一步往往是把 fb_info 中的显示参数映射到 DRM 的 drm_display_mode、drm_framebuffer 和 drm_crtc 上。

三、fb_ops 操作集:驱动能力的入口

struct fb_ops 是 fbdev 驱动向内核注册的回调集合。用户空间的 ioctl、mmap、read/write 最终都会落到这些回调上。常见成员如下表所示:

回调作用迁移关注点
fb_open打开设备时调用对应 DRM open,通常无需特殊处理
fb_release关闭设备时调用对应 DRM release
fb_check_var校验并调整 var 参数对应 DRM mode_valid / atomic check
fb_set_par应用 var 参数到硬件对应 DRM crtc mode_set / atomic commit
fb_setcolreg设置调色板寄存器对应 DRM gamma / LUT
fb_blank背光/电源管理对应 DRM connector DPMS
fb_pan_display平移显示起始地址对应 DRM plane 位置更新
fb_fillrect填充矩形对应 DRM 软件渲染或 GPU 加速
fb_copyarea区域拷贝对应 DRM blit 或 GPU 加速
fb_imageblit图像位块传输对应 DRM 软件渲染
fb_mmap映射显存到用户空间对应 DRM dumb buffer mmap

其中 fb_check_var 和 fb_set_par 是最关键的一对:前者做参数合法性检查,后者真正写寄存器。很多老驱动把时序计算、PLL 配置、DMA 地址设置都塞在 fb_set_par 里,迁移到 DRM 时需要把这些逻辑拆解到 atomic_check 和 atomic_commit 两个阶段。

四、ioctl 接口与典型驱动代码结构

fbdev 的 ioctl 接口定义在 linux/fb.h 中,常用命令包括:

  • FBIOGET_VSCREENINFO / FBIOPUT_VSCREENINFO:获取/设置可变参数;
  • FBIOGET_FSCREENINFO:获取固定参数;
  • FBIOGETCMAP / FBIOPUTCMAP:获取/设置调色板;
  • FBIOBLANK:屏幕消隐;
  • FBIOPAN_DISPLAY:平移显示;
  • FBIO_WAITFORVSYNC:等待垂直同步。

一个典型的 fbdev 驱动代码结构通常包含以下步骤:

static int myfb_probe(struct platform_device *pdev)
{
    struct fb_info *info;
    struct myfb_par *par;
    int ret;

    info = framebuffer_alloc(sizeof(struct myfb_par), &pdev->dev);
    if (!info)
        return -ENOMEM;

    par = info->par;
    par->dev = &pdev->dev;

    /* 1. 映射寄存器与显存 */
    par->regs = devm_ioremap_resource(&pdev->dev, res);
    par->screen_base = dma_alloc_coherent(...);

    /* 2. 填充 fix */
    info->fix.smem_start = par->screen_phys;
    info->fix.smem_len   = par->screen_size;
    info->fix.line_length = width * bpp / 8;
    info->fix.type = FB_TYPE_PACKED_PIXELS;

    /* 3. 填充 var */
    info->var.xres = 1920;
    info->var.yres = 1080;
    info->var.bits_per_pixel = 32;

    /* 4. 绑定操作集 */
    info->fbops = &myfb_ops;

    /* 5. 注册 */
    ret = register_framebuffer(info);
    if (ret)
        goto err_release;

    return 0;
}

对应的 fb_ops 实现通常只填充必要回调,其余用 fb_sys_* 或 cfb_* 辅助函数补齐。例如:

static const struct fb_ops myfb_ops = {
    .owner        = THIS_MODULE,
    .fb_check_var = myfb_check_var,
    .fb_set_par   = myfb_set_par,
    .fb_setcolreg = myfb_setcolreg,
    .fb_blank     = myfb_blank,
    .fb_pan_display = myfb_pan_display,
    .fb_fillrect  = sys_fillrect,
    .fb_copyarea  = sys_copyarea,
    .fb_imageblit = sys_imageblit,
    .fb_mmap      = myfb_mmap,
};

五、常见误区与迁移前的复盘要点

误区一:认为 fbdev 的 fb_set_par 可以直接平移到 DRM 的 mode_set。实际上 DRM 要求先做 atomic check,再提交 commit,且模式设置与 plane 更新是分离的。误区二:把 fb_info->screen_base 直接当作 DRM framebuffer 的映射地址。DRM 使用 GEM 对象管理缓冲,需要创建 dumb buffer 或导入 DMA-BUF。误区三:忽略 fb_pan_display 与 DRM plane 位置更新的语义差异,前者只改扫描起始地址,后者涉及 plane 的 src/dst 坐标和缩放。

迁移前建议做一次“能力盘点”:列出当前 fbdev 驱动实现了哪些 fb_ops 回调、支持哪些像素格式、是否支持 pan display、是否支持 gamma、是否依赖 fb_info->par 私有数据。把这些能力映射到 DRM 的 CRTC、plane、connector、encoder 和 GEM 对象上,才能设计出合理的迁移路径。

fbdev 设备模型:用户空间 → 内核 fb_info → 硬件 用户空间 open("/dev/fb0") ioctl(FBIOGET_VSCREENINFO) ioctl(FBIOPUT_VSCREENINFO) mmap() → 像素缓冲 内核:struct fb_info var / fix 显示参数 screen_base / screen_size fbops → 操作集 device / monspecs 显示硬件 显示控制器 显存 / DMA PLL / 时序 面板 / 背光 系统调用 寄存器/显存 struct fb_ops fb_check_var / fb_set_par fb_setcolreg / fb_blank fb_pan_display fb_fillrect / fb_copyarea fb_imageblit / fb_mmap 驱动能力入口 fbops 指针 ioctl 接口 FBIOGET_VSCREENINFO FBIOPUT_VSCREENINFO FBIOGET_FSCREENINFO FBIOPUTCMAP FBIOBLANK FBIOPAN_DISPLAY 分发到回调 迁移关注点 var → drm_display_mode screen_base → GEM 对象 fb_set_par → atomic commit fb_pan_display → plane 更新 fb_mmap → dumb buffer fb_blank → DPMS 能力映射

理解这张图后,迁移工作就有了清晰的对照表:用户空间的 ioctl 语义要保留或替换,fb_info 中的显示参数要映射到 DRM 的 mode 和 framebuffer,fb_ops 中的硬件操作要拆解到 atomic check/commit 和 plane 更新中。fbdev 不是被“废弃”,而是被 DRM KMS 以更现代的方式重新组织。