2. Framebuffer核心机制
说实话,Framebuffer 这玩意儿,在 Linux 显示驱动里算是老前辈了。我刚开始接触内核驱动时,第一个正经写的显示驱动就是基于 fbdev 框架的。那时候 DRM 还没这么普及,大家都是在 fb_info 这个结构体上做文章。今天咱们就把它拆开看看,里面到底装了些什么。
fb_info:这个庞然大物到底长什么样?
fb_info 是整个 Framebuffer 驱动的核心数据结构。你可以把它想象成一个超级控制中心——所有和显示相关的信息、操作、状态都塞在里面。我习惯把它分成几个功能区来看:
| 字段区域 | 典型成员 | 作用 |
|---|---|---|
| 设备信息 | struct fb_var_screeninfo varstruct fb_fix_screeninfo fix |
可变参数(分辨率、位深、时序) 固定参数(显存地址、步长、类型) |
| 操作接口 | struct fb_ops *fbops |
所有硬件操作的函数指针集合 |
| 显存管理 | void *screen_baseunsigned long screen_size |
显存虚拟地址和大小 |
| 设备模型 | struct device *devicestruct module *owner |
内核设备模型挂载点、模块引用计数 |
| 辅助设施 | struct fb_blit_cursor cursorstruct fb_pixmap pixmap |
硬件光标加速、像素格式转换辅助 |
这里有个细节我想提一下——var 和 fix 的区别。我在项目中见过不少新手把这两个搞混。var 是用户态可以通过 FBIOPUT_VSCREENINFO 改的,比如你调分辨率、改刷新率,动的就是它。fix 是硬件决定的,用户只能读不能写。说白了,一个是「我想怎么显示」,一个是「硬件只能这么显示」。
fb_ops 操作集:驱动的心脏
fb_ops 就是驱动真正干活的函数表。每个函数指针对应一个硬件操作。我列一下最核心的几个:
- fb_open / fb_release:设备打开和关闭时的钩子。我一般在这里做电源管理和引用计数。
- fb_read / fb_write:直接读写显存。说实话,现在很少用了,但 legacy 代码里还有。
- fb_setcoloreg:设置调色板。8 位色深时代必备,现在真彩色用得少,但兼容性不能丢。
- fb_blank:开关显示。省电场景下很重要。
- fb_pan_display:画面平移。双缓冲切换时用,我当年做视频播放器驱动就靠它实现无撕裂。
- fb_mmap:这个最核心,后面单独讲。
注册与注销:驱动的一生
流程其实不复杂,但顺序错了就会出问题。我吃过这个亏——有一次注销时先释放了显存,再注销设备,结果内核 panic 了。嗯,顺序很重要。
注册流程
- 分配 fb_info:
framebuffer_alloc(),顺便带上你的私有数据大小。 - 填充固定参数:设置
fix结构体,包括显存物理地址、步长、类型标志。 - 填充可变参数:设置
var的默认分辨率、位深、时序。 - 挂载 fb_ops:把实现好的操作集指针赋给
fbops。 - 分配显存:用
dma_alloc_coherent()或ioremap(),把地址填进screen_base。 - 注册设备:
register_framebuffer(),这步之后用户就能看到/dev/fbX了。
注销流程
- 注销设备:
unregister_framebuffer(),确保没有用户还在操作。 - 释放显存:根据分配方式调用
dma_free_coherent()或iounmap()。 - 释放 fb_info:
framebuffer_release()。
fb_open 里做了 try_module_get(),但 fb_release 里忘了 module_put()。结果模块卸载时引用计数不为零,死活卸载不掉。调试了一下午才发现。
mmap 机制:用户态直接操作显存
这是 Framebuffer 最精髓的地方。用户态程序通过 mmap 把显存映射到自己的地址空间,然后直接读写像素数据,不需要经过系统调用。性能就是这么来的。
驱动里要做的事情其实就一件——实现 fb_mmap 回调。内核提供了辅助函数 fb_deferred_io_mmap() 和 fb_mmap(),但如果你想自己控制,核心逻辑是:
static int my_fb_mmap(struct fb_info *info, struct vm_area_struct *vma)
{
unsigned long start = info->fix.smem_start;
unsigned long size = info->fix.smem_len;
pgprot_t prot = vma->vm_page_prot;
// 如果是 DMA 内存,需要设置非缓存属性
prot = pgprot_writecombine(prot);
// 建立物理地址到用户空间的映射
return remap_pfn_range(vma, vma->vm_start,
start >> PAGE_SHIFT,
size, prot);
}
这里有个关键点——缓存一致性。显存通常是 DMA 内存,CPU 和显示控制器都会访问。如果 CPU 用了 cache,显示控制器读到的可能是 stale 数据。我一般用 pgprot_writecombine(),它允许写合并但禁止缓存,性能和正确性都能兼顾。
- 直接绘制像素数据(游戏、GUI 框架)
- 双缓冲切换(通过 pan display 配合)
- 视频帧渲染(ffmpeg 等直接输出到显存)
知识结构总览
下面这张图把 fb_info、fb_ops、注册注销、mmap 的关系串起来了。我画图时习惯把数据流和控制流分开,这样看起来更清楚。
你看,整个框架其实就围绕三个核心:数据结构(fb_info)、操作接口(fb_ops)、生命周期管理(注册/注销)。mmap 是连接用户态和内核态显存的关键桥梁。搞懂了这些,Framebuffer 驱动对你来说就没秘密了。
cat /sys/class/graphics/fb0/virtual_size 可以查看分辨率。如果显示异常,先检查 fix.line_length 是不是正确——步长算错了,画面会斜着撕裂,我遇到过两次。