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格式描述的直接前身。
line_length与像素格式正确,用户态即可用最朴素的方式画图;而KMS把“显示管线”当作一等公民,显存只是Plane的输入。二者视角不同,但寄存器操作层是共用的。二、方法流程:从寄存器到/dev/fb0
移植实践建议按以下顺序推进,每一步都可独立验证,避免“一把梭”后无法定位问题。
- 硬件确认:阅读SoC手册,确认显示控制器基地址、时钟源、复位位、引脚复用寄存器,以及面板时序表(从面板datasheet获取)。
- 时钟与电源:在设备树中声明
clocks、power-domains、resets,驱动中用devm_clk_get_enabled()、devm_reset_control_get_exclusive()获取并使能。 - 寄存器初始化:用
readl/writel配置时序寄存器、格式寄存器、DMA起始地址寄存器,使控制器开始扫描输出。 - 显存分配:用
dma_alloc_coherent()或drm_gem_cma分配连续内存,得到虚拟地址与物理地址。 - 填充fb_info:设置
fix.smem_start/smem_len/line_length,设置var的分辨率与像素格式,实现fb_ops中的fb_fillrect/fb_copyarea/fb_imageblit(可直接复用cfb_*或sys_*辅助函数)。 - 注册与注销:
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/fb0 | probe失败或未注册 | 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));,可快速判断配置是否真正生效。fb_ops中fb_mmap默认使用info->fix.smem_start做物理映射,若使用CMA或IOMMU,需确保物理地址对用户态可见,否则mmap会失败或映射到错误区域。把FBDEV驱动跑通后,你会对“显存—控制器—面板”这条链路有直观认识。后续切换到KMS时,同样的寄存器操作会被封装进CRTC与Encoder,而显存管理会升级为GEM对象与drm_framebuffer。先掌握本节这条最小可用路径,再迁移到DRM框架,理解成本会显著降低。