4、Framebuffer驱动开发:Framebuffer设备驱动模型、fb_ops结构体实现、mmap与缓存管理

好,咱们今天来啃一块硬骨头——Framebuffer驱动。说实话,这玩意儿在嵌入式多屏系统里,就像地基一样重要。你上层UI再花哨,底层显示驱动不给力,画面撕裂、闪烁、延迟,用户分分钟想砸设备。我在做第一块LCD驱动时,就吃过这个亏,后来才明白,Framebuffer不仅仅是「把像素数据丢给屏幕」那么简单。

4.1 Framebuffer设备驱动模型

先说说Framebuffer在Linux里的定位。它其实是一个字符设备,主设备号29,次设备号从0开始。你想想看,系统里挂了两块屏,那就会有两个设备节点:/dev/fb0/dev/fb1。用户空间程序直接往这些节点里写数据,就能看到画面了。

驱动模型的核心,是struct fb_info这个结构体。它就像一个大管家,管着显存地址、屏幕参数、操作函数指针。我习惯把它比作「显示控制中心」——你注册一个fb_info,就等于告诉内核:嘿,我这儿有一块屏,你让应用层来找我。

关键点:每个fb_info对应一个显示设备。多屏系统里,你得为每块屏单独分配并注册一个fb_info实例。

注册流程其实不复杂:

  1. 分配struct fb_info:用framebuffer_alloc(),传入设备指针和私有数据大小。
  2. 填充fb_var_screeninfofb_fix_screeninfo:告诉内核屏幕的分辨率、像素格式、显存物理地址等。
  3. 实现fb_ops:这是驱动的心脏,后面细说。
  4. 调用register_framebuffer():把fb_info注册进内核。

嗯,这里要注意:fb_fix_screeninfo里的smem_start字段,填的是显存的物理地址。如果你用的是DMA缓冲区,记得用dma_alloc_coherent()分配,别自己随便malloc一个地址就填进去——我曾经这么干过,结果系统直接panic,教训深刻。

4.2 fb_ops结构体实现

fb_ops是Framebuffer驱动的灵魂。它定义了一组回调函数,应用层通过ioctl或者mmap调用时,最终都会落到这些函数头上。

我列一下最常用的几个回调:

回调函数 作用 我的经验
fb_open 设备打开时调用 一般做引用计数,防止多进程同时操作
fb_release 设备关闭时调用 记得清理资源,否则内存泄漏
fb_read/fb_write 读写显存数据 性能一般,建议用mmap替代
fb_ioctl 处理控制命令 FBIOGET_VSCREENINFO、FBIOPUT_VSCREENINFO等
fb_mmap 将显存映射到用户空间 性能关键,后面重点讲
fb_setcolreg 设置颜色寄存器 伪彩色模式才需要,真彩色一般不用
fb_blank 开关显示 省电场景下有用

实现fb_ioctl时,我最常踩的坑是FBIOPUT_VSCREENINFO。应用层可能会修改分辨率或像素格式,你得检查参数是否合法。比如用户突然把分辨率改成4096x4096,而你的显存只有4MB,那肯定要拒绝。我一般会加个校验函数,先算算需要的显存大小,不够就直接返回-EINVAL。

小技巧:实现fb_check_var回调,它会在参数设置前被调用,给你一个「拦截」的机会。我习惯在这里做所有合法性校验,这样fb_set_par里就可以放心干活了。

代码骨架大概长这样:

static int myfb_open(struct fb_info *info, int user)
{
    // 引用计数+1
    atomic_inc(&info->count);
    return 0;
}

static int myfb_release(struct fb_info *info, int user)
{
    // 引用计数-1,为0时清理资源
    atomic_dec(&info->count);
    return 0;
}

static int myfb_setcolreg(unsigned regno, unsigned red, unsigned green,
                          unsigned blue, unsigned transp, struct fb_info *info)
{
    // 真彩色模式,直接返回0即可
    return 0;
}

static int myfb_blank(int blank_mode, struct fb_info *info)
{
    // 控制背光或LCD使能引脚
    switch (blank_mode) {
    case FB_BLANK_UNBLANK:
        // 开屏
        break;
    case FB_BLANK_POWERDOWN:
        // 关屏
        break;
    }
    return 0;
}

static struct fb_ops myfb_ops = {
    .owner          = THIS_MODULE,
    .fb_open        = myfb_open,
    .fb_release     = myfb_release,
    .fb_read        = fb_sys_read,
    .fb_write       = fb_sys_write,
    .fb_fillrect    = sys_fillrect,
    .fb_copyarea    = sys_copyarea,
    .fb_imageblit   = sys_imageblit,
    .fb_setcolreg   = myfb_setcolreg,
    .fb_blank       = myfb_blank,
    .fb_mmap        = myfb_mmap,
    .fb_ioctl       = myfb_ioctl,
};

看到没?fb_fillrectfb_copyareafb_imageblit这三个,我直接用了内核提供的通用实现。如果你的硬件有2D加速引擎,可以自己实现,性能会好很多。不过大多数嵌入式场景,通用实现够用了。

4.3 mmap与缓存管理

终于到了最核心的部分——mmap。为什么说它重要?因为Framebuffer的本质就是共享内存。应用层通过mmap把显存映射到自己的地址空间,然后直接读写像素数据,省去了read/write的系统调用开销。

实现fb_mmap回调时,核心是调用remap_pfn_range()dma_mmap_coherent()。我推荐用后者,尤其是当你用dma_alloc_coherent()分配显存时,它帮你处理了缓存一致性。

警告:千万别直接用remap_pfn_range()映射普通内存!普通内存有缓存,CPU写完后数据可能还在cache里,DMA控制器读到的却是旧数据。画面就会出现「鬼影」或者「条纹」。我曾经调试这个问题花了整整两天,最后发现是缓存没刷。

正确的做法是:

static int myfb_mmap(struct fb_info *info, struct vm_area_struct *vma)
{
    // 使用dma_mmap_coherent,自动处理缓存一致性
    return dma_mmap_coherent(info->device, vma,
                             info->screen_buffer,
                             info->fix.smem_start,
                             info->fix.smem_len);
}

这里info->screen_buffer是内核虚拟地址,info->fix.smem_start是物理地址,smem_len是显存大小。三个参数必须对应,否则映射出来的内存就是错的。

缓存管理这块,我再多说几句。如果你用的是非一致性DMA缓冲区(比如dma_alloc_noncoherent()),那就得手动处理缓存同步。具体来说:

  • CPU写显存后,调用dma_sync_single_for_device(),把数据刷到内存。
  • CPU读显存前,调用dma_sync_single_for_cpu(),把缓存中的脏数据无效掉。

不过说实话,我建议直接用dma_alloc_coherent()。它保证CPU和DMA看到的数据是一致的,省心很多。代价是分配和释放稍微慢一点,但对于显示驱动来说,这点开销可以忽略。

性能调优要点:

  • 显存尽量用连续物理内存,避免TLB miss。
  • 如果硬件支持,开启DMA burst模式,提高传输效率。
  • 多屏场景下,每块屏的显存要独立,避免互相干扰。

最后,提一下fb_pan_display。这个回调用于实现「平移显示」——你可以在显存里放多个帧缓冲区,然后通过调整起始地址快速切换画面。我在做双缓冲动画时经常用,能有效避免撕裂。实现起来也不复杂,就是更新fb_var_screeninfoxoffsetyoffset,然后通知硬件刷新。

好了,Framebuffer驱动的核心内容就这些。说白了,就是注册一个fb_info,实现fb_ops,然后搞定mmap和缓存管理。你动手写一个驱动试试,遇到问题再回来翻翻这部分,应该能解决大部分坑。