2. 显示基础
帧缓冲,英文叫 Framebuffer,简称 fb。说白了,它就是一块内存区域,专门用来存放屏幕上一帧一帧的图像数据。你想想看,手机屏幕要显示画面,总得有个地方先把像素数据准备好,然后硬件才能把它刷到屏幕上。这个“准备数据的地方”,就是帧缓冲。
在 Linux 内核里,帧缓冲被抽象成一种字符设备。用户空间程序可以通过标准的文件操作——open、read、write、mmap、ioctl——来访问它。我刚开始接触 Android 显示系统时,觉得这层抽象特别巧妙:上层应用不用关心底层是 LCD 还是 OLED,只要往这块内存里写像素数据就行。
Framebuffer 设备驱动模型
Linux 内核的 fb 驱动模型,核心是围绕 struct fb_info 这个结构体展开的。每个 fb 设备对应一个 fb_info 实例。驱动开发者需要做的主要工作,就是填充这个结构体,然后注册到内核。
整个模型分三层:
- 核心层:
drivers/video/fbdev/core/fbmem.c,提供通用的文件操作接口和注册/注销函数。 - 驱动层:你写的具体硬件驱动,比如
drivers/video/fbdev/msm/下的高通平台驱动。 - 设备层:通过
platform_device或i2c_client等总线机制注册到内核的设备节点。
注册一个 fb 设备的典型流程是这样的:
static int myfb_probe(struct platform_device *pdev)
{
struct fb_info *fbi;
struct myfb_par *par;
// 1. 分配 fb_info 结构体
fbi = framebuffer_alloc(sizeof(struct myfb_par), &pdev->dev);
if (!fbi)
return -ENOMEM;
par = fbi->par;
// 2. 填充 fb_info 的固定信息和可变信息
fbi->fix = myfb_fix;
fbi->var = myfb_var;
fbi->fbops = &myfb_ops;
fbi->screen_base = dma_alloc_coherent(...);
fbi->screen_size = MYFB_SIZE;
// 3. 注册 fb 设备
if (register_framebuffer(fbi) < 0) {
framebuffer_release(fbi);
return -EINVAL;
}
platform_set_drvdata(pdev, fbi);
return 0;
}
嗯,这里要注意:framebuffer_alloc 和 register_framebuffer 必须配对使用。我曾经见过同事在卸载驱动时只调了 unregister_framebuffer 却忘了 framebuffer_release,结果内存泄漏,系统跑几天就 OOM 了。
核心数据结构关系:fb_info 包含 fb_var_screeninfo(可变参数,如分辨率、位深)、fb_fix_screeninfo(固定参数,如帧缓冲物理地址、行长)、fb_ops(操作函数集)。
fb_ops 结构体
fb_ops 是驱动与内核核心层之间的契约。你实现了哪些回调,核心层就能提供哪些功能。这个结构体定义在 include/linux/fb.h 中,我挑几个最常用的说说:
| 回调函数 | 作用 | 我遇到的坑 |
|---|---|---|
fb_open |
设备打开时调用 | 记得做引用计数,防止重复映射 |
fb_release |
设备关闭时调用 | 清理 mmap 映射,否则内存泄漏 |
fb_read/fb_write |
读写帧缓冲数据 | 大块读写时注意 DMA 对齐 |
fb_check_var |
检查用户设置的显示参数是否合法 | 这是第一道防线,参数非法直接返回 -EINVAL |
fb_set_par |
根据 var 配置硬件寄存器 |
时序参数必须精确,否则屏幕闪 |
fb_setcolreg |
设置颜色寄存器(伪彩色模式用) | 真彩色模式下这个回调基本用不到 |
fb_blank |
控制屏幕开关/休眠 | 省电模式要关背光,但不要关帧缓冲内存 |
fb_mmap |
将帧缓冲物理内存映射到用户空间 | 缓存一致性要处理好,否则画面花 |
fb_ioctl |
自定义 ioctl 命令处理 | 权限检查不能省,否则安全漏洞 |
我个人习惯,在实现 fb_ops 时,会先写一个空的 fb_ioctl 占位,然后逐步添加需要的命令。这样调试起来方便,不会因为某个回调没实现导致内核崩溃。
小技巧:如果你的硬件支持硬件光标,可以实现 fb_cursor 回调。但 Android 显示系统基本不用内核光标,所以这个回调可以留空。
ioctl 接口与实现
ioctl 是用户空间与 fb 驱动交互的主要通道。用户程序通过 ioctl(fd, FBIOGET_VSCREENINFO, &var) 这样的调用,来获取或设置显示参数。
核心层 fbmem.c 已经帮我们处理了大部分标准 ioctl 命令:
- FBIOGET_VSCREENINFO:获取可变屏幕信息(分辨率、位深、时序等)
- FBIOPUT_VSCREENINFO:设置可变屏幕信息
- FBIOGET_FSCREENINFO:获取固定屏幕信息(物理地址、行长等)
- FBIOGETCMAP:获取颜色表
- FBIOPUTCMAP:设置颜色表
- FBIOBLANK:控制屏幕开关
但有些硬件特有的功能,比如调节背光亮度、切换显示模式、查询 EDID 信息,标准命令覆盖不到。这时候就需要在驱动里实现自定义 ioctl。
来看一个自定义 ioctl 的例子:
#define MYFB_IOCTL_GET_BRIGHTNESS _IOR('F', 0x10, int)
#define MYFB_IOCTL_SET_BRIGHTNESS _IOW('F', 0x11, int)
static int myfb_ioctl(struct fb_info *fbi, unsigned int cmd, unsigned long arg)
{
struct myfb_par *par = fbi->par;
int brightness;
switch (cmd) {
case MYFB_IOCTL_GET_BRIGHTNESS:
brightness = par->brightness;
if (copy_to_user((int __user *)arg, &brightness, sizeof(brightness)))
return -EFAULT;
break;
case MYFB_IOCTL_SET_BRIGHTNESS:
if (copy_from_user(&brightness, (int __user *)arg, sizeof(brightness)))
return -EFAULT;
if (brightness < 0 || brightness > 255)
return -EINVAL;
par->brightness = brightness;
// 调用 PWM 驱动调节背光
pwm_config(par->pwm, brightness, 255);
break;
default:
return -ENOTTY; // 不认识的命令
}
return 0;
}
这里有几个要点:
- 自定义命令的编号要避开标准命令。标准命令从
0x00到0x0F,所以我从0x10开始用。 - 一定要用
copy_to_user/copy_from_user,不能直接解引用用户空间指针。否则内核会 panic。 - 参数校验不能偷懒。我曾经遇到过上层传了个负数亮度值,结果 PWM 驱动直接炸了。
避坑指南:自定义 ioctl 命令的幻数('F')不要和内核其他子系统冲突。我见过一个驱动用了 'M' 作为幻数,结果和多媒体框架的 ioctl 冲突,调试了两天才发现。
帧缓冲驱动整体架构
为了让你更直观地理解整个框架,我画了一张结构图:
这张图把整个 fb 驱动栈分成了五层。你写的驱动代码主要工作在驱动层,但必须理解上下层之间的交互方式。比如,用户空间调用 ioctl(fd, FBIOPUT_VSCREENINFO, &var) 时,核心层会先调用你的 fb_check_var 校验参数,然后调用 fb_set_par 配置硬件。这两个回调如果实现得不好,屏幕就会出问题。
总结一下关键点
- fb_info 是驱动核心,所有信息都挂在这个结构体上。
- fb_ops 是驱动与内核的接口,每个回调都有明确的职责。
- ioctl 是用户空间控制硬件的通道,标准命令由核心层处理,自定义命令由驱动处理。
- 参数校验和权限检查是驱动开发的生命线,马虎不得。
我记得刚做 fb 驱动时,总觉得 fb_check_var 这个回调可有可无。直到有一次,上层应用传了一个分辨率,我的硬件根本不支持,结果屏幕直接黑掉了。从那以后,我每个参数都老老实实校验,宁可多写几行代码,也不让非法参数流到硬件层。
调试建议:在 fb_set_par 里加一些 dev_dbg 打印,把最终写入硬件的时序参数打出来。配合示波器量一下实际波形,能快速定位时序配置错误。