2. 嵌入式显示基础
好,我们直接进入正题。上一章我们聊了Framebuffer是什么,这一章我们深入内核源码,看看它到底是怎么工作的。
Linux Framebuffer驱动框架,说白了就是一套标准化的“画布管理方案”。内核通过fbmem.c这个核心文件,把复杂的硬件操作抽象成统一的接口。你想想看,如果没有这套框架,每个显卡驱动都得自己实现open、ioctl、mmap这些系统调用,那得多乱?
2.1 fbmem.c:Framebuffer的“大管家”
fbmem.c位于内核源码的drivers/video/fbdev/core/目录下。它不直接操作硬件,而是负责三件事:
- 注册字符设备:向内核注册主设备号为29的FB设备。
- 提供统一的文件操作接口:实现
fb_open、fb_read、fb_write、fb_ioctl、fb_mmap等。 - 管理全局的fb_info链表:维护系统中所有注册的Framebuffer设备。
我个人习惯把fbmem.c看作一个“调度中心”。它不干活,但它知道谁在干活,以及怎么把活派下去。
核心数据结构:struct fb_info
每个Framebuffer设备对应一个struct fb_info实例。这个结构体就是驱动开发者需要填充的“简历”。
struct fb_info {
struct fb_var_screeninfo var; // 可变参数:分辨率、位深、时序
struct fb_fix_screeninfo fix; // 固定参数:显存地址、长度、行宽
struct fb_ops *fbops; // 操作函数集(重点!)
struct device *device; // 设备模型指针
char __iomem *screen_base; // 显存虚拟地址
unsigned long screen_size; // 显存大小
// ... 还有其他成员,但上面这几个是核心
};
我在项目中遇到过好几次这样的问题:驱动加载成功了,/dev/fb0也出现了,但应用程序一mmap就崩溃。查到最后,都是screen_base没有正确映射。嗯,这个坑后面我们会专门讲。
2.2 fb_ops结构体:驱动与内核的“契约”
struct fb_ops是驱动开发者必须实现的函数指针集合。它定义了Framebuffer设备能做什么。
| 函数指针 | 作用 | 是否必须实现 |
|---|---|---|
fb_open |
设备打开时的初始化 | 可选 |
fb_release |
设备关闭时的清理 | 可选 |
fb_read |
从显存读取数据 | 可选(默认用通用实现) |
fb_write |
向显存写入数据 | 可选(默认用通用实现) |
fb_check_var |
校验用户设置的显示参数 | 必须 |
fb_set_par |
根据校验后的参数配置硬件 | 必须 |
fb_setcolreg |
设置颜色寄存器(伪彩色时用) | 可选 |
fb_blank |
控制屏幕开关/休眠 | 可选 |
fb_pan_display |
实现显示偏移(双缓冲常用) | 可选 |
fb_mmap |
将显存映射到用户空间 | 可选(默认用通用实现) |
fb_ioctl |
处理自定义ioctl命令 | 可选 |
避坑指南:我曾经见过一个驱动,fb_check_var里直接返回0,什么都不做。结果用户设置了一个硬件不支持的时序,屏幕直接黑掉。记住,fb_check_var是你的最后一道防线,一定要校验所有参数。
这里有个细节:fb_read和fb_write的通用实现是通过screen_base直接拷贝数据。如果你的硬件显存不是线性映射的(比如某些DSP或GPU),就必须自己实现这两个函数。
2.3 注册与注销流程:从零到一
我们来看一个典型的Framebuffer驱动加载流程。假设你写了一个简单的LCD控制器驱动:
注册流程(以module_init为例)
- 分配fb_info:调用
framebuffer_alloc()分配并初始化struct fb_info。 - 填充固定参数:设置
fix结构体,包括ID字符串、显存物理地址、行宽等。 - 填充可变参数:设置
var结构体,包括默认分辨率、位深、上下边距等。 - 设置fb_ops:将实现的函数指针赋值给
info->fbops。 - 分配显存:通过
dma_alloc_coherent()或ioremap()分配/映射显存,赋值给screen_base。 - 注册设备:调用
register_framebuffer(struct fb_info *info)。
static int myfb_probe(struct platform_device *pdev)
{
struct fb_info *info;
int ret;
// 1. 分配fb_info
info = framebuffer_alloc(0, &pdev->dev);
if (!info)
return -ENOMEM;
// 2. 填充固定参数
strcpy(info->fix.id, "myfb");
info->fix.smem_start = dma_addr; // 显存物理地址
info->fix.smem_len = SZ_4M; // 显存大小
info->fix.line_length = width * (bpp / 8);
// 3. 填充可变参数
info->var.xres = 1024;
info->var.yres = 600;
info->var.bits_per_pixel = 32;
// 4. 设置fb_ops
info->fbops = &myfb_ops;
// 5. 分配显存
info->screen_base = dma_alloc_coherent(...);
info->screen_size = SZ_4M;
// 6. 注册设备
ret = register_framebuffer(info);
if (ret) {
framebuffer_release(info);
return ret;
}
platform_set_drvdata(pdev, info);
return 0;
}
个人经验:register_framebuffer()内部会做几件重要的事:将fb_info加入全局链表、创建/dev/fbX设备节点、调用fb_set_par()初始化硬件。所以你的fb_set_par在注册时就会被调用一次,务必保证此时硬件已经就绪。
注销流程
注销就简单多了,但顺序不能乱:
- 调用
unregister_framebuffer(info):移除设备节点,从全局链表删除。 - 释放显存:
dma_free_coherent()或iounmap()。 - 调用
framebuffer_release(info):释放fb_info结构体。
static int myfb_remove(struct platform_device *pdev)
{
struct fb_info *info = platform_get_drvdata(pdev);
unregister_framebuffer(info); // 先注销
dma_free_coherent(...); // 再释放显存
framebuffer_release(info); // 最后释放info
return 0;
}
我曾经踩过的坑:在unregister_framebuffer之后立即framebuffer_release,但忘了检查是否有用户空间程序还在通过mmap访问显存。结果内核直接Oops。正确的做法是:unregister_framebuffer会等待所有文件描述符关闭,但mmap的页面还在。最好在驱动中实现fb_release的引用计数,确保所有用户都释放了再清理。
2.4 核心流程图解
下面这张图展示了从驱动加载到用户空间访问的完整链路:
2.5 关键数据结构关系
最后,我用一个表格总结一下fbmem.c中几个核心数据结构的关系:
| 数据结构 | 所属模块 | 作用 | 生命周期 |
|---|---|---|---|
struct fb_info |
fbmem.c | 每个FB设备的完整描述 | 从注册到注销 |
struct fb_ops |
驱动实现 | 硬件操作函数指针集 | 与fb_info绑定 |
struct fb_var_screeninfo |
fbmem.c | 用户可修改的显示参数 | 每次ioctl可能更新 |
struct fb_fix_screeninfo |
fbmem.c | 硬件固定的显示参数 | 驱动加载后不变 |
struct fb_cmap |
fbmem.c | 颜色映射表(伪彩色) | 按需设置 |
嗯,这一章的内容就到这里。记住,fbmem.c是框架,fb_ops是契约,注册注销是流程。搞懂这三样,Framebuffer驱动的基本骨架你就拿下了。