3. Framebuffer 驱动基础

Framebuffer,说白了就是一块内存区。内核把显示缓冲区映射到用户空间,应用程序直接往里面写像素数据,硬件就能在屏幕上显示出来。我最早接触 Framebuffer 是在做一款工控平板的时候,那时候觉得这东西太简单了——不就是个内存搬运工吗?后来踩了几个坑才发现,里面的门道还真不少。

这一章,我们就从 Framebuffer 的设备驱动模型讲起,把 struct fb_info 和 fb_ops 这两个核心结构体掰开揉碎了说清楚,最后再手写一个最简单的驱动示例。

3.1 Framebuffer 设备驱动模型

在 Linux 内核中,Framebuffer 驱动遵循标准的字符设备驱动模型,但它又有自己的一套框架。内核把 Framebuffer 抽象成 fbmem.c 这个核心层,驱动开发者只需要填充几个关键结构体,剩下的注册、文件操作接口都由框架帮你搞定。

核心层级关系:

  • 应用层:用户程序通过 /dev/fbX 设备节点操作显示缓冲区
  • 核心层:fbmem.c 提供统一的文件操作接口(open、read、write、ioctl、mmap)
  • 驱动层:开发者实现 fb_ops 中的硬件操作函数
  • 硬件层:LCD 控制器、显存、显示时序等物理设备

我习惯把 Framebuffer 驱动比作一个「内存窗口」。内核注册一个字符设备,把显存映射到用户空间,应用程序直接读写这块内存,驱动负责把内存里的数据刷新到屏幕上。整个过程不需要经过复杂的图形栈,所以 Framebuffer 特别适合嵌入式场景——资源有限、不需要 3D 加速、显示逻辑简单。

Framebuffer 驱动模型架构 应用层:/dev/fbX 核心层:fbmem.c register_framebuffer() / fb_read() / fb_write() / fb_ioctl() / fb_mmap() 驱动层:fb_ops 操作函数集 fb_open() / fb_release() / fb_setcolreg() / fb_blank() / fb_fillrect() ... 硬件层:LCD 控制器 / 显存 / 时序 用户空间直接读写 无需经过图形栈 统一文件操作接口 驱动只需填充结构体 硬件操作函数集 驱动开发者核心工作

3.2 struct fb_info 结构体详解

struct fb_info 是整个 Framebuffer 驱动的核心数据结构。每个 Framebuffer 设备对应一个 fb_info 实例,它包含了设备的所有信息——从硬件参数到操作函数指针,从显存地址到状态标志。

我记得第一次看这个结构体的时候,被它几十个成员变量吓了一跳。其实不用慌,大部分成员由核心层维护,驱动开发者只需要关注其中几个关键字段。

成员变量 类型 说明
fbops struct fb_ops * 硬件操作函数集,驱动必须实现
var struct fb_var_screeninfo 可变参数:分辨率、位深、时序等
fix struct fb_fix_screeninfo 固定参数:显存地址、行长、类型等
screen_base char __iomem * 显存虚拟地址,驱动映射后赋值
screen_size unsigned long 显存大小(字节)
pseudo_palette void * 伪调色板,用于 8/16 位色模式
state int 设备状态:FBINFO_STATE_RUNNING 等
node int 设备编号,对应 /dev/fbX 中的 X

我的经验: 调试 Framebuffer 驱动时,最常出问题的是 var 和 fix 这两个结构体。我曾经遇到过一次屏幕花屏,查了两天才发现是 var.xres_virtual 和 var.yres_virtual 没设置对——虚拟分辨率必须大于等于实际分辨率,否则 mmap 映射会出问题。

3.3 fb_ops 操作函数集

fb_ops 是驱动与核心层的契约。你实现了哪些函数,核心层就调用哪些函数。我建议至少实现以下几个,其他的可以根据硬件能力选择性实现。

函数 原型 说明
fb_open int (*fb_open)(struct fb_info *, int) 设备打开时调用,可做初始化
fb_release int (*fb_release)(struct fb_info *, int) 设备关闭时调用,可做清理
fb_setcolreg int (*fb_setcolreg)(unsigned, unsigned, unsigned, unsigned, unsigned, struct fb_info *) 设置颜色寄存器,调色板模式必须实现
fb_blank int (*fb_blank)(int, struct fb_info *) 屏幕开关控制,省电时有用
fb_pan_display int (*fb_pan_display)(struct fb_var_screeninfo *, struct fb_info *) 虚拟显示平移,双缓冲场景常用
fb_fillrect void (*fb_fillrect)(struct fb_info *, const struct fb_fillrect *) 矩形填充,硬件加速可选
fb_copyarea void (*fb_copyarea)(struct fb_info *, const struct fb_copyarea *) 区域拷贝,硬件加速可选
fb_imageblit void (*fb_imageblit)(struct fb_info *, const struct fb_image *) 图像位块传输,硬件加速可选

避坑指南: 我曾经在一个项目中只实现了 fb_setcolreg 而没实现 fb_blank,结果系统休眠后屏幕无法唤醒。因为核心层在电源管理时会调用 fb_blank,如果没实现,默认返回 0 表示成功,但硬件根本没做任何操作。所以,即使你的硬件不支持开关屏,也建议实现一个空函数,至少打印一条调试信息。

3.4 简单的 Framebuffer 驱动示例

理论说再多,不如直接看代码。下面是一个最简单的 Framebuffer 驱动,它模拟了一个 800x600、32 位色的显示设备。显存用 kmalloc 分配,所有像素数据都存在内存里。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fb.h>
#include <linux/platform_device.h>

#define WIDTH  800
#define HEIGHT 600
#define BPP    32  /* 每个像素 32 位 */

static struct fb_info *my_fb_info;
static unsigned long *my_fb_mem;

/* 固定参数:描述显存布局 */
static struct fb_fix_screeninfo my_fix = {
    .id          = "my_fb",
    .type        = FB_TYPE_PACKED_PIXELS,
    .visual      = FB_VISUAL_TRUECOLOR,
    .accel       = FB_ACCEL_NONE,
    .line_length = WIDTH * BPP / 8,
};

/* 可变参数:描述显示模式 */
static struct fb_var_screeninfo my_var = {
    .xres        = WIDTH,
    .yres        = HEIGHT,
    .xres_virtual = WIDTH,
    .yres_virtual = HEIGHT,
    .bits_per_pixel = BPP,
    .red         = {16, 8, 0},   /* R: 位偏移16, 长度8 */
    .green       = {8,  8, 0},   /* G: 位偏移8,  长度8 */
    .blue        = {0,  8, 0},   /* B: 位偏移0,  长度8 */
    .transp      = {24, 8, 0},   /* A: 位偏移24, 长度8 */
    .activate    = FB_ACTIVATE_NOW,
};

/* 设置颜色寄存器:真彩色模式不需要,但必须实现 */
static int my_fb_setcolreg(unsigned regno, unsigned red, unsigned green,
                           unsigned blue, unsigned transp,
                           struct fb_info *info)
{
    return 0;  /* 真彩色模式直接返回成功 */
}

/* 屏幕开关 */
static int my_fb_blank(int blank_mode, struct fb_info *info)
{
    /* 这里可以控制 LCD 背光或电源 */
    return 0;
}

/* fb_ops 操作函数集 */
static struct fb_ops my_fb_ops = {
    .owner       = THIS_MODULE,
    .fb_setcolreg = my_fb_setcolreg,
    .fb_blank    = my_fb_blank,
    .fb_fillrect = cfb_fillrect,   /* 使用通用软件实现 */
    .fb_copyarea = cfb_copyarea,
    .fb_imageblit = cfb_imageblit,
};

static int my_fb_probe(struct platform_device *pdev)
{
    int ret;

    /* 1. 分配 fb_info 结构体 */
    my_fb_info = framebuffer_alloc(0, &pdev->dev);
    if (!my_fb_info)
        return -ENOMEM;

    /* 2. 分配显存 */
    my_fb_mem = kmalloc(WIDTH * HEIGHT * BPP / 8, GFP_KERNEL);
    if (!my_fb_mem) {
        framebuffer_release(my_fb_info);
        return -ENOMEM;
    }

    /* 3. 填充 fb_info */
    my_fb_info->screen_base = (char __iomem *)my_fb_mem;
    my_fb_info->screen_size = WIDTH * HEIGHT * BPP / 8;
    my_fb_info->fbops       = &my_fb_ops;
    my_fb_info->var         = my_var;
    my_fb_info->fix         = my_fix;
    my_fb_info->pseudo_palette = NULL;

    /* 4. 注册 Framebuffer 设备 */
    ret = register_framebuffer(my_fb_info);
    if (ret) {
        kfree(my_fb_mem);
        framebuffer_release(my_fb_info);
        return ret;
    }

    dev_info(&pdev->dev, "my_fb: %dx%d %dbpp registered\n",
             WIDTH, HEIGHT, BPP);
    return 0;
}

static int my_fb_remove(struct platform_device *pdev)
{
    unregister_framebuffer(my_fb_info);
    kfree(my_fb_mem);
    framebuffer_release(my_fb_info);
    return 0;
}

static struct platform_driver my_fb_driver = {
    .probe  = my_fb_probe,
    .remove = my_fb_remove,
    .driver = {
        .name = "my_fb",
    },
};

module_platform_driver(my_fb_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");

代码要点:

  • framebuffer_alloc 分配 fb_info,register_framebuffer 注册设备
  • 显存用 kmalloc 分配,实际项目中通常用 dma_alloc_coherent
  • cfb_fillrect、cfb_copyarea、cfb_imageblit 是内核提供的软件实现,硬件不支持加速时直接用
  • 真彩色模式下 fb_setcolreg 可以空实现,但函数指针不能为 NULL

这个驱动加载后,你会看到 /dev/fb1(如果系统已有 fb0)设备节点。用 cat /dev/urandom > /dev/fb1 就能看到屏幕上出现随机噪点——嗯,这说明驱动跑通了。

我的习惯: 调试 Framebuffer 驱动时,我喜欢先用 fbset 工具查看当前显示参数,再用 dd 命令往显存里写数据。如果屏幕有反应,说明数据通路没问题。如果没反应,先检查 screen_base 的映射地址是否正确,再检查 fix.line_length 是否等于 var.xres * var.bits_per_pixel / 8。这两个地方出错的概率最高。