Linux 帧缓冲

简介

FrameBuffer 是内核当中的一种驱动程序接口。Linux 是工作在保护模式下,所以用户态进程是无法象 DOS 那样使用显卡 BIOS 里提供的中断调用来实现直接写屏,Linux 抽象出 FrameBuffer 这个设备来供用户态进程实现直接写屏。

帧缓冲主要结构

  • fb_info
    该结构体记录当前帧缓冲设备的状态信息,如果系统中有多个帧缓冲设备,就需要两个fb_info结构,这个结构只在内核中可以看到,对用户空间不可见。

  • fb_var_screeninfo
    该结构体记录指定的帧缓冲设备和显示模式中可以被修改的信息,其中包括显示器分辨率等信息。

  • fb_fix_screeninfo
    该结构体表示帧缓冲设备中一些不能修改的参数,包括特定的显示模式,屏幕缓冲区的物理地址,显示缓冲区的长度信息。

  • fb_ops
    LCD底层硬件操作接口集。比如fb_openfb_releasefb_readfb_writefb_ioctlfb_mmap等:

  • fb_cmap
    fb_cmap指定颜色映射,用于以内核可以理解的方式存储用户的颜色定义。

帧缓冲显示原理

帧缓冲设备是一种显示抽象的设备,也可以被理解为它是一个内存区域,上面的应用程序可以直接对显示缓冲区进行读和写操作,就像访问文件的通用接口一样,用户可以认为帧缓冲是一块内存,能读取数据的内存块也可以向这个内存写入数据,因此显示器显示图形界面实际上根据根据的是指定的内存数据块内的数据。

帧缓冲的显示缓冲区位于 Linux 内核地址空间,应用程序不能直接访问内核地址空间,在 Linux 中,只有一个内存的内核地址空间映射到用户地址空间才可以由用户访问,内存的映射是通过MMAP函数实现的在 Linux 中。对于帧缓冲,虚拟地址是通过内存映射的方法将显示缓冲区内核地址映射到用户空间的,然后用户可以通过读和写这部分的虚拟地址来访问显示缓冲区,在屏幕上绘图。

使用流程

使用帧缓冲之前应该首先确定 Linux 系统上已安装了帧缓冲驱动,可以在目录/dev/下查找fb*如,/dev/fb0, /dev/fb1等设备来确定是否安装。如果没有需要安装一个帧缓冲驱动的模块到内核,或者重新编译内核生成一个带帧缓冲模块的镜像。

使用帧缓冲需要进入控制台模式,即纯命令行的模式进行编程。一般可以通过快捷键CTRL+ALT+F1进入控制台模式,CTRL+ALT+F7切回图形窗口。如果控制台模式没有登录,可以CTRL+ALT+F6尝试登录。

因硬件显示设备的物理显示区是通过帧缓存区操作,而帧缓存区是处于内核空间,应用程序不能随意操作,此时可以通过系统调用mmap把帧缓存映射到用户空间,在用户空间中创建出帧缓存映射区(用户图像数据缓存区),以后只需把用户图像数据写入到帧缓存映射区就可在硬件设备上显示图像。

具体实现流程如下:

打开帧缓冲设备/dev/f0

在Linux的/dev目录的寻找b*设备文件然后使用读写模式打开它,Linux 系统将使用通用的open系统调用来完成功能, open的功能原型如下:

int open(const char *path, int oflags);
  • Path是准备打开的文件或设备的路径参数;
  • oflags指定打开文件时使用的参数;
  • flags参数的指定,是通过组合文件访问模式和其他的可选模式一起的,可以支持多个模式或,参数必须是指定下列文件的访问模式。
    • 只读:O_RDONLLY
    • 只写:O_WRONLY
    • 读写:O_RDWR

简而言之, open函数建立设备文件的访问路径。如果操作成功,它返回一个文件描述符,只是一个文件描述符,它将不使用其他任何正在运行的进程共享。如果两个程序同时打开相同的文件,将得到两个不同的文件描述符。如果他们执行文件写入操作,他们将操作每个文件描述符,不会发生冲突,写完之后退出。他们的数据不会互相交织在一起的,但会互相的彼此覆盖 (后写完的内容覆盖前面写的内容),两个程序来读取和写入的文件位置看似一样但是有各自不同拷贝所以不会发生交织。如果open调用未能返回1,则将全局变量errno设置为指示失败的原因。

通过系统调用ioctl函数获得帧设备相关信息

通过顿缓冲文件描述符,屏幕的分辨率、颜色深度等信息可以被获得,帧缓冲驱动中存放了这些对应的信息,必须使用 Linux 系统调用ioctl首先将帧缓冲的文件描述符和fb_var_screeninfo 结构体对应起来。

结构体fb_var_screeninfo包含以下三个重要数据结构:

  • 屏幕的 x 方向分辨率,像素作为单位。
  • 屏幕的 Y 方向分辨率,像素作为单位。
  • 屏幕的像素颜色深度,每个像素用多少比特数表示。

ioctl函数原型如下:

extern int ioctl (int __fd, unsigned long int __request, ...) __THROW;

ioctl调用实现访问设备驱动各种各样的配置信息功能,它提供了一个控制设备的行为和配置底层服务接口的驱动函数,各种设备驱动程序,例如套接字和系统终端,还有磁带机都有ioctl命令可以支持。

  • __fdioctl命令中是该帧缓冲的文件描述符;
  • __requestioctl函数将要执行的命令,实现参数给定的对象描述符中指定的函数操作,各种设备支持的功能是有差异的
    • FBIOGET_VSCREENINFO命令字返回与Framebuffer有关的固定的信息;
    • FBIOGET_VSCREENINFO命令字返回与 Framebuffer 有关的可变的信息;
  • 第三个参数是一个指针用来指向结构体fb_var_screeninfo

最后使用者可以通过结构体fb_var_screeninfo来获得屏幕的分辨率和颜色位深和其他重要的屏幕信息。根据这些信息可以计算屏幕缓冲区的大小:屏幕缓冲区大小 (以字节为单位) = 屏幕宽度x高度x屏幕颜色深度/8

帧缓冲映射

在进行帧缓冲的MMAP映射之前,要先得到帧缓冲文件描述符,才能像屏幕上面显示,必须首先将缓冲区的内核地址映射映射到用户地址空间。Linux 系统将使用MMAP系统调用完成功能,MMAP函数原型如下:

extern void *mmap (void *__addr, size_t __len, int __prot,
     int __flags, int __fd, __off_t __offset) __THROW;
  • __addr:返回一个指向mmap函数的内存区域的指针,与内容相关的文件指针,通过指针可以访问帧缓冲区的内存区域。

  • __len:可以请求使用特定内存地址,通过设置地址参数,如果值为0,将自动分配指针,这是推荐的做法,否则会降低程序的可移植性,因为不同的系统可用的地址范围是不一样的。

  • __prot:设置内存访问的权限设定,通过端口相关的参数定义,位的定义值如下:

    • PORT_EXEC:允许内存段的执行。
    • PORT_NONE:无法访问内存段。
    • PORT_READ:允许读取内存段。
    • PORT_WRITE:允许编写内存段。
  • __flags:改变控制参数标志,能够影响该内存段的作用域,如下所示:

    • MAP_FIXED:内存段必须位于addr中指定的地址。
    • MAP_SHARED:内存的修改保存到一个文件中。
    • MAP_PRIVATE:内存段是私人的,变化仅在本地范围内有效。
  • __fd:是通过一个open调用得到的访问文件的描述符。

  • offset:用于指定访问数据的开始偏移量在内存段中,和访问普通文件使用方式是相同的,再指定文件描述符参数,以及访问的数据长度参数即可。

读写帧缓冲

MMAP返回的指针,可以访问到帧缓冲内存区,可以定位到屏幕缓冲区具体为每个显示像素的位置,通过读函数调用读取对应的位置数据在帧缓冲内存中,相反写操作对应于内存的写入数据可以显示内容写到屏幕上。

解除帧缓冲映射

在绘图完成后,帧缓冲文件描述符必须被释放之前,解除帧缓冲区的地址映射,使用 Linux 系统调用完成mmap函数的逆函数实现,即是munmap,函数的原型如下:

extern int munmap (void *__addr, size_t __len) __THROW;

addr参数应该与调用MMAP时指定的参数值一致, len参数也应该与之前调用MMAP时指定的len参数保持一致。

mmap调用返回0成功,失败则返回1,同时将全局变量erno设置为指示失败的原因。

调用close关闭设备

使用帧缓冲设备后,应关闭相应的文件描述符,使用 Linux 系统标准的函数完成关闭功能,close函数的原型如下:

extern int close (int __fd);

close的参数和在开始调用open时指定的参数一致,文件描述符释放后可以重用,结束调用成功返回0,失败返回1

帧缓冲实例

以下代码摘自xianjimli/linux-framebuffer-tools: linux framebuffer tool,演示了帧缓冲设备的使用流程。

fb_info_t *linux_fb_open(const char *filename)
{
    uint32_t                 size = 0;
    fb_info_t               *fb   = NULL;
    struct fb_fix_screeninfo fix;
    struct fb_var_screeninfo var;
    return_value_if_fail(filename != NULL, NULL);

    fb = (fb_info_t *)calloc(1, sizeof(fb_info_t));
    return_value_if_fail(fb != NULL, NULL);

    // 打开帧缓冲设备,O_RDWR 读写模式
    fb->fd = open(filename, O_RDWR);
    if (fb->fd < 0)
    {
        log_debug("open %s failed(%d)\n", filename, errno);
        free(fb);
        return NULL;
    }
    // 通过系统调用 ioctl 函数获得帧设备相关信息
    // FBIOGET_FSCREENINFO 命令字返回与 Framebuffer 有关的固定的信息
    if (ioctl(fb->fd, FBIOGET_FSCREENINFO, &fix) < 0)
        goto fail;
    //命令字返回与 Framebuffer 有关的可变的信息
    if (ioctl(fb->fd, FBIOGET_VSCREENINFO, &var) < 0)
        goto fail;

    var.xoffset = 0;
    var.yoffset = 0;
    // 显示
    ioctl(fb->fd, FBIOPAN_DISPLAY, &(var));

    log_debug("fb_info_t: %s\n", filename);
    log_debug("fb_info_t: xres=%d yres=%d bits_per_pixel=%d mem_size=%d\n", var.xres, var.yres,
              var.bits_per_pixel, fb_size(fb));
    log_debug("fb_info_t: red(%d %d) green(%d %d) blue(%d %d)\n", var.red.offset, var.red.length,
              var.green.offset, var.green.length, var.blue.offset, var.blue.length);

    fb->w           = var.xres;
    fb->h           = var.yres;
    fb->bpp         = var.bits_per_pixel / 8;
    fb->line_length = fix.line_length;

    size = fb_size(fb);
    // 帧缓冲映射
    // PROT_READ | PROT_WRITE:可读写
    // MAP_SHARED:内存的修改保存到一个文件
    fb->data = (uint8_t *)mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, fb->fd, 0);

    if (fb->data == MAP_FAILED)
    {
        log_debug("map framebuffer failed.\n");
        goto fail;
    }

    log_debug("line_length=%d mem_size=%d\n", fix.line_length, fb_size(fb));
    log_debug("xres_virtual =%d yres_virtual=%d xpanstep=%d ywrapstep=%d\n", var.xres_virtual,
              var.yres_virtual, fix.xpanstep, fix.ywrapstep);

    return fb;
fail:
    log_debug("%s is not a framebuffer.\n", filename);
    close(fb->fd);
    free(fb);

    return NULL;
}

感兴趣可以下载源码编译运行,其中/bin/fbshow可以使用帧缓冲设备显示图片。图形界面下直接运行可能提示无法运行,需要Chrtl+Alt+F1切换到控制台模式。

LCD 与 Framebuffer 的关系

LCD 控制器首先通过 VDEN 信号,使能。接下来根据 VCLK 时钟信号,在像素点上“喷涂”不同的颜色(打个比方),控制器有 VD(video data)信号,传送不同颜色信息。每来一个时钟信号,就向右移动一个像素,根据行同步信号 HSYNC,就从最右边移动到最左边。当移动到右下角时根据垂直同步信号 VSYNC。

那么问题来了,不同颜色的信息从哪里来?就是从上文介绍的 Framebuffer 中来的。

很多人都会说操纵 LCD 显示就是操纵 FrameBuffer,表面上来看是这样的。实际上是 FrameBuffer 就是 Linux 内核驱动申请的一片内存空间,然后 LCD 内有一片 sram,CPU 内部有个 LCD 控制器,它有个单独的 dma 用来将 FrameBuffer 中的数据拷贝到 LCD 的 sram 中去 拷贝到 LCD 的 sram 中的数据就会显示在 LCD 上,LCD 驱动和 FrameBuffer 驱动没有必然的联系,它只是驱动 LCD 正常工作的,比如有信号传过来,那么 LCD 驱动负责把信号转成显示屏上的内容,至于什么内容这就是应用层要处理的。

静态随机存取存储器(Static Random-Access Memory,SRAM)是随机存取存储器的一种。所谓的“静态”,是指这种存储器只要保持通电,里面储存的数据就可以恒常保持。
DMA(Direct Memory Access),直接内存访问。使用 DMA 的好处就是它不需要 CPU 的干预而直接服务外设,这样 CPU 就可以去处理别的事务,从而提高系统的效率。

Reference

Linux 驱动之 Framebuffer 子系统 | 量子范式
Linux 驱动开发(9)——- framebuffer 驱动详解 | 码农家园
嵌入式系统中帧缓冲显示模块的设计与实现 - 中国知网
research/framebuffer/fivechess/fivechess-0.1 at master · tsuibin/research
五子棋 framebuffer 版 - 尚码园
FrameBuffer 驱动程序分析_深入剖析 Android 系统-CSDN 博客_framebuffer
xianjimli/linux-framebuffer-tools: linux framebuffer tool
韦东山_嵌入式 Linux_第 2 期_Linux 高级驱动视频教程_免费试看版_哔哩哔哩_bilibili
Linux LCD Frambuffer 基础介绍和使用(1) - 知乎