4. FBDEV驱动移植实践

FBDEV(Framebuffer Device)是Linux内核为显示设备提供的一套经典抽象:它把一块可以被CPU访问的显存(framebuffer)暴露为/dev/fbX字符设备,用户态通过ioctl获取分辨率、像素格式、行跨度等信息,并通过mmap把显存映射到进程空间直接写像素。在从FBDEV向KMS迁移的课程中,先把FBDEV驱动完整跑通,是理解后续DRM/KMS中CRTC、Plane、Encoder、Connector等抽象的最佳铺垫。本节围绕“寄存器操作→显存管理→fb_ops实现→注册→测试→调试”这条主线展开。

一、核心原理:FBDEV驱动在做什么

一个FBDEV驱动本质上要完成四件事:第一,初始化显示控制器(LCD Controller、VOP、DPU等)的时钟、电源、引脚复用;第二,配置时序参数(hactive、vactive、hfront porch、hsync、vbp、vfp、像素时钟),让控制器输出正确的RGB/LVDS/MIPI信号;第三,分配并描述一块显存,填充struct fb_info中的var(可变参数)与fix(固定参数);第四,实现fb_ops回调并调用register_framebuffer()注册,最终在/dev下生成fb0节点。

其中var描述当前可变的显示状态:xres/yres为可见分辨率,xres_virtual/yres_virtual为虚拟分辨率(用于双缓冲或平移),bits_per_pixel为色深,red/green/blue/transp四个fb_bitfield描述像素内各通道的偏移与长度。fix则描述不可变信息:显存物理地址smem_start、长度smem_len、行跨度line_length、视觉类型visual。理解这两组参数,是后续KMS中drm_mode_create()与drm_framebuffer格式描述的直接前身。

关键点:FBDEV把“显存”当作一等公民,驱动只需保证line_length与像素格式正确,用户态即可用最朴素的方式画图;而KMS把“显示管线”当作一等公民,显存只是Plane的输入。二者视角不同,但寄存器操作层是共用的。

二、方法流程:从寄存器到/dev/fb0

移植实践建议按以下顺序推进,每一步都可独立验证,避免“一把梭”后无法定位问题。

  1. 硬件确认:阅读SoC手册,确认显示控制器基地址、时钟源、复位位、引脚复用寄存器,以及面板时序表(从面板datasheet获取)。
  2. 时钟与电源:在设备树中声明clocks、power-domains、resets,驱动中用devm_clk_get_enabled()、devm_reset_control_get_exclusive()获取并使能。
  3. 寄存器初始化:用readl/writel配置时序寄存器、格式寄存器、DMA起始地址寄存器,使控制器开始扫描输出。
  4. 显存分配:用dma_alloc_coherent()或drm_gem_cma分配连续内存,得到虚拟地址与物理地址。
  5. 填充fb_info:设置fix.smem_start/smem_len/line_length,设置var的分辨率与像素格式,实现fb_ops中的fb_fillrect/fb_copyarea/fb_imageblit(可直接复用cfb_*或sys_*辅助函数)。
  6. 注册与注销:register_framebuffer(info),在remove中unregister_framebuffer()并释放资源。

下面是一段最小化的注册骨架,突出关键字段:

static int myfb_probe(struct platform_device *pdev)
{
    struct fb_info *info;
    struct myfb_priv *priv;
    int ret;

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

    priv = info->par;
    priv->base = devm_platform_ioremap_resource(pdev, 0);
    priv->clk  = devm_clk_get_enabled(&pdev->dev, "lcdc");

    /* 1. 配置时序寄存器 */
    writel(TIMING_HACTIVE(800) | TIMING_VACTIVE(480), priv->base + REG_TIMING);
    writel(FORMAT_RGB565, priv->base + REG_FORMAT);

    /* 2. 分配显存 */
    priv->vaddr = dma_alloc_coherent(&pdev->dev, 800*480*2,
                                     &priv->paddr, GFP_KERNEL);

    /* 3. 填充 fix / var */
    info->fix.smem_start = priv->paddr;
    info->fix.smem_len   = 800*480*2;
    info->fix.line_length = 800*2;
    info->var.xres = info->var.xres_virtual = 800;
    info->var.yres = info->var.yres_virtual = 480;
    info->var.bits_per_pixel = 16;
    info->var.red.offset = 11; info->var.red.length = 5;
    info->var.green.offset = 5; info->var.green.length = 6;
    info->var.blue.offset = 0; info->var.blue.length = 5;

    info->screen_base = priv->vaddr;
    info->screen_size = info->fix.smem_len;
    info->fbops = &myfb_ops;

    /* 4. 把显存地址写入控制器DMA寄存器 */
    writel(priv->paddr, priv->base + REG_DMA_ADDR);
    writel(CTRL_ENABLE, priv->base + REG_CTRL);

    ret = register_framebuffer(info);
    if (ret)
        return ret;

    platform_set_drvdata(pdev, info);
    return 0;
}

三、测试与调试方法

驱动加载后,先用dmesg确认注册日志,再检查设备节点与参数:

$ dmesg | grep -i myfb
myfb 1c0c000.lcdc: registered fb0: 800x480-16bpp

$ ls -l /dev/fb*
crw-rw---- 1 root video 29, 0 Jan  1 00:00 /dev/fb0

$ cat /sys/class/graphics/fb0/virtual_size
800,480

$ fbset -i
mode "800x480"
    geometry 800 480 800 480 16
    timings 0 0 0 0 0 0 0
endmode

功能测试可用fbset改分辨率、用fbgrab截屏、用cat /dev/urandom > /dev/fb0观察雪花噪声,或用fbtft风格的测试程序填充纯色。若屏幕无输出,按“时钟→复位→时序→DMA地址→格式”的顺序逐级排查。

现象可能原因排查手段
无/dev/fb0probe失败或未注册dmesg看错误码,检查compatible匹配
屏幕全黑但有背光时序参数错误或DMA地址为0读回寄存器,确认REG_DMA_ADDR非0
图像偏移/花屏line_length与控制器stride不一致对比fix.line_length与寄存器stride
颜色错乱RGB位域偏移错误检查var.red/green/blue.offset
刷新撕裂未使用双缓冲或未等待vsync增大yres_virtual,用FBIOPAN_DISPLAY

四、误区与复盘

常见误区有三:其一,把xres_virtual设成等于xres却想用双缓冲,导致FBIOPAN_DISPLAY无效;其二,忽略line_length对齐要求,某些控制器要求行跨度按16或32字节对齐,否则DMA取数错位;其三,在fb_ops中直接操作寄存器而不做锁保护,多进程同时写fb时出现竞态。复盘时应把“寄存器配置”与“fb_info描述”分开验证:先用示波器或逻辑分析仪确认时序信号正确,再确认显存内容与屏幕一致,最后才排查用户态接口。

调试技巧:在probe中把关键寄存器读回并打印,例如dev_dbg(&pdev->dev, "CTRL=%08x TIMING=%08x\n", readl(base+REG_CTRL), readl(base+REG_TIMING));,可快速判断配置是否真正生效。
注意:FBDEV的fb_ops中fb_mmap默认使用info->fix.smem_start做物理映射,若使用CMA或IOMMU,需确保物理地址对用户态可见,否则mmap会失败或映射到错误区域。
FBDEV驱动移植:从寄存器到/dev/fb0 SoC显示控制器 时序/格式/DMA寄存器 基地址 0x1c0c0000 时钟与电源 clk / reset / power-domain 显存 Framebuffer dma_alloc_coherent 800×480×2 = 768000 B myfb 驱动 probe() ioremap / clk / reset 寄存器初始化 writel(TIMING/FORMAT) 填充 fb_info fix / var / fb_ops register_framebuffer() 用户态接口 /dev/fb0 字符设备 29:0 ioctl(FBIOGET_VSCREENINFO) 获取分辨率/像素格式 mmap() 映射显存直接写像素 fbset / fbgrab / 测试程序 调试路径: dmesg → /sys/class/graphics/fb0 → fbset -i → 寄存器读回 → 示波器测时序 → 显存内容比对 常见故障:无fb0 / 全黑 / 花屏 / 颜色错 / 撕裂 → 对应时钟、DMA地址、line_length、RGB位域、双缓冲

把FBDEV驱动跑通后,你会对“显存—控制器—面板”这条链路有直观认识。后续切换到KMS时,同样的寄存器操作会被封装进CRTC与Encoder,而显存管理会升级为GEM对象与drm_framebuffer。先掌握本节这条最小可用路径,再迁移到DRM框架,理解成本会显著降低。