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 设备模型节点
struct fb_info 核心结构图 struct fb_info fb_ops (操作函数表) fix (固定信息) var (可变信息) screen_base/size 关键说明 • fb_ops:驱动必须实现的回调函数集合,包括 open/release/read/write/ioctl/mmap/pan_display 等 • fix:硬件固定参数,如显存物理地址、行宽(line_length)、类型(FB_TYPE_PACKED_PIXELS) • var:用户可调节参数,如 xres/yres(分辨率)、bits_per_pixel、red/green/blue/transp 偏移 • screen_base:显存映射后的内核虚拟地址,mmap 操作依赖此地址进行页表建立

3.2 注册 Framebuffer 设备:从 probe 到 register

Framebuffer 设备的注册流程通常发生在驱动的 probe 函数中。核心步骤包括:分配 fb_info、填充字段、分配显存、注册设备。

开发提示: 使用 framebuffer_alloc() 分配 fb_info,该函数会自动分配 fb_info 及其私有数据 par 的连续内存。不要直接使用 kzalloc,否则会缺少必要的初始化。

3.2.1 注册流程步骤

  1. 分配 fb_info: info = framebuffer_alloc(sizeof(struct myfb_par), &pdev->dev);
  2. 填充固定信息 fix: 设置 smem_start(物理地址)、smem_len(显存大小)、line_length(行字节数)、type(像素类型)
  3. 填充可变信息 var: 设置默认分辨率、位深、像素格式(RGB 偏移和长度)
  4. 设置 fb_ops: info->fbops = &myfb_ops;
  5. 分配显存: 使用 dma_alloc_coherent 或 ioremap 获取连续物理内存
  6. 注册设备: 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;
}
Framebuffer 设备注册流程 framebuffer_alloc() 填充 fix / var 字段 设置 fb_ops 回调表 分配显存 (dma_alloc) register_framebuffer()

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);
}
mmap 映射流程:用户空间 → 内核 → 显存 用户空间进程 mmap("/dev/fb0") 内核 fb_mmap remap_pfn_range() 物理显存 smem_start (物理地址) mmap 关键点 • 使用 pgprot_writecombine() 设置写合并属性,避免 CPU 缓存污染 • remap_pfn_range() 将物理页帧映射到用户空间虚拟地址 • 映射完成后,用户空间可直接读写显存,无需系统调用 • 必须检查映射范围,防止越界访问导致内核崩溃

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);
双缓冲 pan_display 原理 Buffer 0(前台) yoffset = 0 当前显示 Buffer 1(后台) yoffset = 600 正在渲染 显示控制器 起始地址寄存器 指向 Buffer 0 pan 后指向 Buffer 1 pan_display 通过修改起始地址寄存器实现缓冲区切换,无需拷贝数据,效率极高

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