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的显存管理相对朴素,通常分为两种模式:
- 独立显存:显卡或显示控制器自带VRAM,驱动通过
ioremap把物理地址映射到内核虚拟地址,赋给screen_base。 - 系统内存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,实现无撕裂翻页。
四、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时清楚“哪些职责被保留、哪些被重新抽象”。