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 冲突,调试了两天才发现。

帧缓冲驱动整体架构

为了让你更直观地理解整个框架,我画了一张结构图:

Linux Framebuffer 驱动架构 用户空间 应用程序 / SurfaceFlinger / fbdev 工具 系统调用:open / read / write / mmap / ioctl 核心层:fbmem.c 通用文件操作 / 标准 ioctl 分发 / fb_info 管理 驱动层:你的 fb_ops 实现 fb_check_var / fb_set_par / fb_ioctl / fb_mmap 硬件层:LCD 控制器 / MIPI DSI / HDMI 发送器 数据流方向:用户空间 → 系统调用 → 核心层 → 驱动层 → 硬件

这张图把整个 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 打印,把最终写入硬件的时序参数打出来。配合示波器量一下实际波形,能快速定位时序配置错误。