3. Framebuffer 驱动开发实战:注册Framebuffer设备、实现fb_ops回调函数、实现mmap与pan_display
3.1 Framebuffer 设备模型与核心数据结构
Framebuffer(帧缓冲)是 Linux 内核中最基础的显示抽象层,它将显存抽象为一个连续的内存区域,用户空间可以直接通过 mmap 映射该区域进行像素操作。在驱动开发中,核心工作围绕 struct fb_info 结构体展开。
核心数据结构:
struct fb_info 是 Framebuffer 驱动的“身份证”,包含设备信息、显存描述、操作函数表、硬件参数等所有关键字段。
| 字段 | 类型 | 说明 |
|---|---|---|
par |
void * |
指向驱动私有数据,通常包含硬件寄存器基址、时钟配置等 |
fix |
struct fb_fix_screeninfo |
固定屏幕信息:显存物理地址、长度、行宽、类型等 |
var |
struct fb_var_screeninfo |
可变屏幕信息:分辨率、位深、像素格式、时序参数等 |
fbops |
struct fb_ops * |
操作函数表,驱动必须实现的核心回调 |
screen_base |
char __iomem * |
显存在内核空间的虚拟地址,用于 mmap 映射 |
screen_size |
unsigned long |
显存总大小(字节) |
device |
struct device * |
关联的 Linux 设备模型节点 |
3.2 注册 Framebuffer 设备:从 probe 到 register
Framebuffer 设备的注册流程通常发生在驱动的 probe 函数中。核心步骤包括:分配 fb_info、填充字段、分配显存、注册设备。
开发提示: 使用
framebuffer_alloc() 分配 fb_info,该函数会自动分配 fb_info 及其私有数据 par 的连续内存。不要直接使用 kzalloc,否则会缺少必要的初始化。
3.2.1 注册流程步骤
- 分配 fb_info:
info = framebuffer_alloc(sizeof(struct myfb_par), &pdev->dev); - 填充固定信息 fix: 设置
smem_start(物理地址)、smem_len(显存大小)、line_length(行字节数)、type(像素类型) - 填充可变信息 var: 设置默认分辨率、位深、像素格式(RGB 偏移和长度)
- 设置 fb_ops:
info->fbops = &myfb_ops; - 分配显存: 使用
dma_alloc_coherent或ioremap获取连续物理内存 - 注册设备:
register_framebuffer(info);
static int myfb_probe(struct platform_device *pdev)
{
struct fb_info *info;
struct myfb_par *par;
int ret;
// 1. 分配 fb_info 及私有数据
info = framebuffer_alloc(sizeof(struct myfb_par), &pdev->dev);
if (!info)
return -ENOMEM;
par = info->par;
par->dev = &pdev->dev;
// 2. 填充固定信息
strscpy(info->fix.id, "my_fb", sizeof(info->fix.id));
info->fix.type = FB_TYPE_PACKED_PIXELS;
info->fix.visual = FB_VISUAL_TRUECOLOR;
info->fix.line_length = 800 * 4; // 800x600, 32bpp
// 3. 填充可变信息(默认 800x600@32bpp)
info->var.xres = 800;
info->var.yres = 600;
info->var.bits_per_pixel = 32;
info->var.red.offset = 16; info->var.red.length = 8;
info->var.green.offset = 8; info->var.green.length = 8;
info->var.blue.offset = 0; info->var.blue.length = 8;
info->var.transp.offset = 24; info->var.transp.length = 8;
// 4. 设置操作函数表
info->fbops = &myfb_ops;
// 5. 分配显存(使用 DMA 连续内存)
info->screen_base = dma_alloc_coherent(info->device,
info->fix.smem_len,
&info->fix.smem_start,
GFP_KERNEL);
if (!info->screen_base) {
ret = -ENOMEM;
goto err_free_info;
}
info->screen_size = info->fix.smem_len;
// 6. 注册 Framebuffer 设备
ret = register_framebuffer(info);
if (ret < 0) {
dev_err(&pdev->dev, "register_framebuffer failed\n");
goto err_free_dma;
}
platform_set_drvdata(pdev, info);
dev_info(&pdev->dev, "Framebuffer registered: %dx%d@%d\n",
info->var.xres, info->var.yres, info->var.bits_per_pixel);
return 0;
err_free_dma:
dma_free_coherent(info->device, info->fix.smem_len,
info->screen_base, info->fix.smem_start);
err_free_info:
framebuffer_release(info);
return ret;
}
3.3 实现 fb_ops 回调函数
struct fb_ops 是 Framebuffer 驱动的“操作手册”,内核通过该结构体调用驱动提供的功能。必须实现的回调包括 fb_open、fb_release、fb_setcolreg、fb_blank、fb_pan_display、fb_mmap 等。
| 回调函数 | 触发时机 | 必须实现? | 典型实现 |
|---|---|---|---|
fb_open |
用户空间 open("/dev/fb0") | 推荐 | 增加引用计数,初始化硬件 |
fb_release |
用户空间 close() | 推荐 | 减少引用计数,清理状态 |
fb_setcolreg |
ioctl(FBIOPUTCMAP) 设置颜色寄存器 | 是(伪彩色) | 写入硬件 LUT 或直接返回(真彩色可空实现) |
fb_blank |
ioctl(FBIOBLANK) 或电源管理 | 推荐 | 控制背光或关闭显示控制器 |
fb_pan_display |
ioctl(FBIOPAN_DISPLAY) 或自动 pan | 推荐 | 更新显示起始地址,实现双缓冲切换 |
fb_mmap |
用户空间 mmap() | 是 | 将显存物理页映射到用户空间 |
fb_ioctl |
自定义 ioctl 命令 | 可选 | 扩展驱动特有功能 |
static struct fb_ops myfb_ops = {
.owner = THIS_MODULE,
.fb_open = myfb_open,
.fb_release = myfb_release,
.fb_setcolreg = myfb_setcolreg,
.fb_blank = myfb_blank,
.fb_pan_display = myfb_pan_display,
.fb_mmap = myfb_mmap,
.fb_fillrect = cfb_fillrect, // 使用内核通用辅助函数
.fb_copyarea = cfb_copyarea,
.fb_imageblit = cfb_imageblit,
};
static int myfb_open(struct fb_info *info, int user)
{
struct myfb_par *par = info->par;
atomic_inc(&par->open_count);
dev_dbg(info->device, "fb opened, count=%d\n",
atomic_read(&par->open_count));
return 0;
}
static int myfb_release(struct fb_info *info, int user)
{
struct myfb_par *par = info->par;
atomic_dec(&par->open_count);
dev_dbg(info->device, "fb released, count=%d\n",
atomic_read(&par->open_count));
return 0;
}
static int myfb_setcolreg(unsigned regno, unsigned red, unsigned green,
unsigned blue, unsigned transp,
struct fb_info *info)
{
// 真彩色模式下,颜色寄存器由用户空间直接计算,此函数可空实现
return 0;
}
static int myfb_blank(int blank_mode, struct fb_info *info)
{
struct myfb_par *par = info->par;
switch (blank_mode) {
case FB_BLANK_UNBLANK: // 唤醒显示
writel(0x1, par->regs + DISPLAY_ENABLE);
break;
case FB_BLANK_POWERDOWN: // 关闭显示
writel(0x0, par->regs + DISPLAY_ENABLE);
break;
default:
break;
}
return 0;
}
常见陷阱: 不要忘记设置
fb_fillrect、fb_copyarea、fb_imageblit 这三个辅助函数。如果驱动不提供,内核会使用默认的软件实现(cfb_fillrect 等),但性能较差。如果硬件支持图形加速,应实现硬件版本。
3.4 实现 mmap:用户空间直接操作显存
fb_mmap 是 Framebuffer 驱动最核心的功能之一,它允许用户空间通过 mmap 系统调用将显存映射到进程地址空间,实现零拷贝像素渲染。内核提供了辅助函数 fb_deferred_io_mmap 和 dma_mmap_coherent 来简化实现。
3.4.1 mmap 实现原理
- 物理地址连续性: 显存必须是物理连续的,否则无法一次性映射整个帧缓冲
- 页对齐: 映射的起始地址和长度必须按 PAGE_SIZE 对齐
- 缓存属性: 显存通常使用
pgprot_writecombine或pgprot_noncached属性,避免 CPU 缓存导致的显示不一致 - 安全性: 只允许映射驱动拥有的物理内存范围,防止越界访问
static int myfb_mmap(struct fb_info *info, struct vm_area_struct *vma)
{
unsigned long start = vma->vm_start;
unsigned long size = vma->vm_end - vma->vm_start;
unsigned long pfn;
int ret;
// 1. 检查映射范围是否超出显存
if (size > info->fix.smem_len)
return -EINVAL;
// 2. 设置缓存属性:写合并(Write Combine)提升性能
vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;
// 3. 计算物理页帧号
pfn = __phys_to_pfn(info->fix.smem_start);
// 4. 建立页表映射
ret = remap_pfn_range(vma, start, pfn, size, vma->vm_page_prot);
if (ret)
dev_err(info->device, "mmap remap_pfn_range failed: %d\n", ret);
return ret;
}
// 使用 dma_mmap_coherent 的简化版本(推荐)
static int myfb_mmap_dma(struct fb_info *info, struct vm_area_struct *vma)
{
return dma_mmap_coherent(info->device, vma,
info->screen_base,
info->fix.smem_start,
info->fix.smem_len);
}
3.5 实现 pan_display:双缓冲与平滑滚动
pan_display 回调用于实现显示起始地址的偏移,是实现双缓冲(double buffering)和垂直滚动的基础。当用户空间调用 FBIOPAN_DISPLAY ioctl 时,内核会调用此函数更新显示控制器的起始地址寄存器。
3.5.1 pan_display 工作原理
- 显存分区: 将显存划分为多个缓冲区(通常 2 个),每个缓冲区大小等于一帧画面
- 起始地址偏移: 通过修改显示控制器的起始地址寄存器,指向不同的缓冲区
- VSync 同步: 在垂直消隐期间切换缓冲区,避免画面撕裂(tearing)
- 滚动效果: 通过微调 yoffset 实现逐行平滑滚动
static int myfb_pan_display(struct fb_var_screeninfo *var,
struct fb_info *info)
{
struct myfb_par *par = info->par;
unsigned long base_offset;
// 1. 计算新的显示起始地址偏移(字节)
base_offset = var->yoffset * info->fix.line_length
+ var->xoffset * (info->var.bits_per_pixel / 8);
// 2. 检查偏移是否超出显存范围
if (base_offset + info->var.yres * info->fix.line_length > info->fix.smem_len)
return -EINVAL;
// 3. 更新硬件显示起始地址寄存器
writel(info->fix.smem_start + base_offset, par->regs + DISPLAY_START_ADDR);
// 4. 等待 VSync(可选,防止撕裂)
// while (!(readl(par->regs + DISPLAY_STATUS) & VSYNC_FLAG))
// cpu_relax();
dev_dbg(info->device, "pan_display: offset=%lu, x=%d, y=%d\n",
base_offset, var->xoffset, var->yoffset);
return 0;
}
// 用户空间使用示例(伪代码)
// struct fb_var_screeninfo var;
// var.xoffset = 0;
// var.yoffset = 600; // 切换到第二个缓冲区
// ioctl(fd, FBIOPAN_DISPLAY, &var);
3.6 完整驱动框架与卸载流程
一个完整的 Framebuffer 驱动需要包含 probe、remove、以及电源管理回调。卸载时需按注册的逆序释放资源。
static int myfb_remove(struct platform_device *pdev)
{
struct fb_info *info = platform_get_drvdata(pdev);
// 1. 注销 Framebuffer 设备
unregister_framebuffer(info);
// 2. 释放显存
dma_free_coherent(info->device, info->fix.smem_len,
info->screen_base, info->fix.smem_start);
// 3. 释放 fb_info
framebuffer_release(info);
return 0;
}
static const struct of_device_id myfb_of_match[] = {
{ .compatible = "vendor,myfb" },
{ }
};
MODULE_DEVICE_TABLE(of, myfb_of_match);
static struct platform_driver myfb_driver = {
.probe = my