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 加速、显示逻辑简单。
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。这两个地方出错的概率最高。