Linux-FrameBuffer双缓冲机制显示图像

1. 液晶屏的基本概念

  • 像素:
    屏幕上显示颜色的最小单位,英文叫 pixel。注意,位图(如jpg、bmp等格式的常见图片)也是由一个个的像素点构成的,跟屏幕的像素点的概念一样。原理上讲,将一张位图显示到屏幕上,就是将图片上的像素点一个个复制到屏幕像素点上。

Linux-FrameBuffer双缓冲机制显示图像_第1张图片

  • 分辨率:
    • 宽、高两个维度上的像素点数目。
    • 分辨率越高,所需要的显存越大。

Linux-FrameBuffer双缓冲机制显示图像_第2张图片

  • 色深:
    • 每个像素所对应的内存字节数,一般有8位、16位、24位或32位
    • GEC6818开发板的屏幕的色深是32位的
    • 32位色深的屏幕一般被称为真彩屏,或1600万色屏。

色深决定了一个像素点所能表达的颜色的丰富程度,色深越大,色彩表现力越强。


2. 内存映射基本原理

虽然LCD设备本质上也可以看作是一个文件,在文件系统中有其对应的设备节点,可以像普通文件一样对其进行读写操作(read/write),但由于对字符设备的读写操作是以字节流的方式进行的,因此除非操作的图像尺寸刚好与屏幕尺寸完全一致,如下图所示,图片的宽高与LCD的宽高完全一致,否则将会画面会乱。
Linux-FrameBuffer双缓冲机制显示图像_第3张图片


以下是一段直接写设备节点的“不好”的示例代码:

void bad_display()
{
    // 打开LCD设备
    int lcd = open("/dev/fb0", O_RDWR);

    // 从JPG图片中获取ARGB数据
    char *argbbuf;
    int   argbsize;
    argbsize = jpg2rgb("dogs.jpg", &argbbuf);

    // 将RGB数据直接线性灌入LCD设备节点
    write(lcd, argbbuf, argbsize);

    // ...
}

像上述代码这样,直接将数据通过设备节点 /dev/fb0 写入的话,这些数据会自动地从LCD映射内存的入口处(对应LCD屏幕的左上角)开始呈现,并且会以线性的字节流形式逐个字节往后填充,除非图像尺寸与显示器刚好完全一致,否则显示是失败的。


一般而言,图像的尺寸大小是随机的,因此更方便的做法是为LCD做内存映射,将屏幕的每一个像素点跟映射内存一一对应,而映射内存可以是二维数组,因此就可以非常方便地通过操作二维数组中的任意元素,来操作屏幕中的任意像素点了。这里的映射内存,有时被称为显存。

Linux-FrameBuffer双缓冲机制显示图像_第4张图片

如上图所示,将一块内存与LCD的像素一一对应:

  1. LCD上面显示的图像色彩,由其对应的内存的数据决定
  2. 映射内存的大小至少得等于LCD的真实尺寸大小
  3. 映射内存的大小可以大于LCD的真实尺寸,有利于优化动态画面(视频)体验

下面是屏幕显示为红色的示例代码:

#include 
#include 
#include 
#include 

int main()
{
    // 打开液晶屏文件
    int lcd = open("/dev/fb0", O_RDWR);

    // 给LCD设备映射一块内存(或称显存)
    char *p = mmap(NULL, 800*480*4, PROT_WRITE,
                   MAP_SHARED, lcd, 0);

    // 通过映射内存,将LCD屏幕的每一个像素点涂成红色
    int red = 0x00FF0000;
    for(int i=0; i<800*480; i++)
        memcpy(p+i*4, &red, 4);

    // 解除映射
    munmap(p, 800*480*4);
    return 0;
}

注意,上述代码存在诸多假设,比如屏幕的尺寸是800×480、屏幕色深是4个字节、每个像素内部的颜色分量是ARGB等等,这些信息都是“生搬硬凑”的,只能适用于某一款特定的LCD屏,如果屏幕的这些参数变了,上述代码就无法正常运行了,要想让程序在其他规格尺寸的屏幕下也能正常工作,就得让程序自动获取这些硬件参数信息。


3. 屏幕参数设定

首先明确,屏幕的硬件参数,都是由硬件驱动工程师,根据硬件数据手册和内核的相关规定,填入某个固定的地方的,然后再由应用开发工程师,使用特定的函数接口,将这些特定的信息读出来。

对于GEC6818开发板而言,上述所谓“某个固定的地方”,指的是如下这些重要的结构体(节选):

struct fb_fix_screeninfo
{
    char id[16];              /* identification string eg "TT Builtin" */
    unsigned long smem_start; /* Start of frame buffer mem */
                              /* (physical address) */
    __u32 smem_len;           /* Length of frame buffer mem */
    __u32 type;               /* see FB_TYPE_*        */
    __u32 type_aux;           /* Interleave for interleaved Planes */
    __u32 visual;             /* see FB_VISUAL_*        */ 
    __u16 xpanstep;           /* zero if no hardware panning  */
    __u16 ypanstep;           /* zero if no hardware panning  */
    __u16 ywrapstep;          /* zero if no hardware ywrap    */
    __u32 line_length;        /* length of a line in bytes    */
    ...
    ...
};

struct fb_var_screeninfo
{
    __u32 xres;           /* 可见区宽度(单位:像素) */
    __u32 yres;           /* 可见区高度(单位:像素) */
    __u32 xres_virtual;   /* 虚拟区宽度(单位:像素) */
    __u32 yres_virtual;   /* 虚拟区高度(单位:像素) */
    __u32 xoffset;        /* 虚拟区到可见区x轴偏移量 */
    __u32 yoffset;        /* 虚拟区到可见区y轴偏移量 */

    __u32 bits_per_pixel; /* 色深 */

    // 像素内颜色结构
    struct fb_bitfield red;   // 红色  
    struct fb_bitfield green; // 绿色
    struct fb_bitfield blue;  // 蓝色
    struct fb_bitfield transp;// 透明度
    ...
    ...
};

struct fb_bitfield
{
    __u32 offset;   /* 颜色在像素内偏移量 */
    __u32 length;   /* 颜色占用数位长度 */
    ...
    ...
};

上述结构体的具体定义在系统的如下路径中:

/usr/include/linux/fb.h

Linux-FrameBuffer双缓冲机制显示图像_第5张图片

如上图所示,如果板卡已经具备LCD的驱动程序,那么应用程序就可以通过 ioctl() 来检索LCD的硬件参数信息。以粤嵌GEC6818开发板配套的群创AT070TN92-7英寸液晶显示屏为例,具体代码如下:

#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 

int lcd;
struct fb_fix_screeninfo fixinfo; // 固定属性
struct fb_var_screeninfo varinfo; // 可变属性

void get_fixinfo()
{
    if(ioctl(lcd, FBIOGET_FSCREENINFO, &fixinfo) != 0)
    {
        perror("获取LCD设备固定属性信息失败");
        return;
    }

}

void get_varinfo()
{
    if(ioctl(lcd, FBIOGET_VSCREENINFO, &varinfo) != 0)
    {
        perror("获取LCD设备可变属性信息失败");
        return;
    }
}

void show_info()
{
    // 获取LCD设备硬件fix属性
    get_fixinfo();
    printf("\n获取LCD设备固定属性信息成功:\n");
    printf("[ID]: %s\n", fixinfo.id);
    printf("[FB类型]: ");
    switch(fixinfo.type)
    {
    case FB_TYPE_PACKED_PIXELS:      printf("组合像素\n");break;
    case FB_TYPE_PLANES:             printf("非交错图层\n");break;
    case FB_TYPE_INTERLEAVED_PLANES: printf("交错图层\n");break;
    case FB_TYPE_TEXT:               printf("文本或属性\n");break;
    case FB_TYPE_VGA_PLANES:         printf("EGA/VGA图层\n");break;
    }
    printf("[FB视觉]: ");
    switch(fixinfo.visual)
    {
    case FB_VISUAL_MONO01:             printf("灰度. 1=黑;0=白\n");break;
    case FB_VISUAL_MONO10:             printf("灰度. 0=黑;1=白\n");break;
    case FB_VISUAL_TRUECOLOR:          printf("真彩色\n");break;
    case FB_VISUAL_PSEUDOCOLOR:        printf("伪彩色\n");break;
    case FB_VISUAL_DIRECTCOLOR:        printf("直接彩色\n");break;
    case FB_VISUAL_STATIC_PSEUDOCOLOR: printf("只读伪彩色\n");break;
    }
    printf("[行宽]: %d 字节\n", fixinfo.line_length);

    // 获取LCD设备硬件var属性
    get_varinfo();
    printf("\n获取LCD设备可变属性信息成功:\n");
    printf("[可见区分辨率]: %d×%d\n", varinfo.xres, varinfo.yres);
    printf("[虚拟区分辨率]: %d×%d\n", varinfo.xres_virtual, varinfo.yres_virtual);
    printf("[从虚拟区到可见区偏移量]: (%d,%d)\n", varinfo.xoffset, varinfo.yoffset);
    printf("[色深]: %d bits\n", varinfo.bits_per_pixel);
    printf("[像素内颜色结构]:\n");
    printf("  [红] 偏移量:%d, 长度:%d bits\n", varinfo.red.offset, varinfo.red.length);
    printf("  [绿] 偏移量:%d, 长度:%d bits\n", varinfo.green.offset, varinfo.green.length);
    printf("  [蓝] 偏移量:%d, 长度:%d bits\n", varinfo.blue.offset, varinfo.blue.length);
    printf("  [透明度] 偏移量:%d, 长度:%d bits\n", varinfo.transp.offset, varinfo.transp.length);
    printf("\n");
}

int main()
{
    lcd = open("/dev/fb0", O_RDWR);
    if(lcd == -1)
    {
        perror("打开 /dev/fb0 失败");
        exit(0);
    }

    // 显示LCD设备属性信息
    show_info();

    return 0;
}

「课堂练习1」

根据以上示例代码,采用自动获取屏幕硬件参数的方式,在开发板上轮流显示红绿蓝三原色。
Linux-FrameBuffer双缓冲机制显示图像_第6张图片



4. 多缓冲机制

仔细观察上述显示单色的程序运行效果,会发现屏幕上的颜色不是一瞬间整体显示的,而是有一个很明显的从上到下刷屏的过程,这实际上是由于我们是一个个像素点从左到右,从上到下刷屏导致的,如果不是速度比较快,我们将会看到屏幕上的点是一个个亮起来的,而不是整屏统一更新,这显然不是最佳的体验。


解决这个问题,可以采用多缓冲的办法,首先要搞明白所谓可见区和虚拟区的关系:

  1. 可见区、虚拟区都是内存区域,可见区是虚拟区的一部分,因此可见区尺寸至少等于虚拟区。
  2. 一般而言,可见区尺寸就是屏幕尺寸,比如800×480;而虚拟区是显示设备能支持的显存大小,比如800×480、800×960等。
  3. 为了提高画面体验,一般先在不可见区操作显存数据,然后在调整可见区位置,使得图像“瞬间”呈现,避免闪屏。
    Linux-FrameBuffer双缓冲机制显示图像_第7张图片

下面以示例代码的形式,来分析如何使用多缓冲机制提高画面体验。

  • 1. 设定虚拟区
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    // 打开LCD设备
    int lcd = open("/dev/fb0", O_RDWR|O_EXCL);

    struct fb_var_screeninfo vinfo; // 显卡设备的可变属性结构体
    ioctl(lcd, FBIOGET_VSCREENINFO, &vinfo); // 获取可变属性

    // 获得当前显卡所支持的虚拟区显存大小
    unsigned long VWIDTH  = vinfo.xres_virtual;
    unsigned long VHEIGHT = vinfo.yres_virtual;
    unsigned long BPP = vinfo.bits_per_pixel;

    printf("虚拟区显存大小为: %d×%d\n", VWIDTH, VHEIGHT);

    // 申请一块虚拟区大小的映射内存
    char *p = mmap(NULL, VWIDTH * VHEIGHT * BPP/8,
                PROT_READ|PROT_WRITE,
                MAP_SHARED, lcd, 0); 
    if(p != MAP_FAILED)
    {
        printf("申请显存成功\n");
    }
}

在开发板运行结果:

[root@GEC6818 ~]#./a.out
虚拟区显存大小为: 800×1440
申请显存成功
[root@GEC6818 ~]#

从上述执行结果来看,粤嵌GEC6818开发板配套的群创AT070TN92-7英寸液晶显示屏支持三倍与屏幕尺寸的虚拟显存的设定。当然,在实际设定的时候,不一定要三倍,也可以是两倍大小,比如800×960。


  • 2. 显示A区,但在B区作画

    为了方便讨论,假设设定两倍屏幕尺寸的虚拟区内存,上半部分为A区,下半部分为B区。如下图所示:

Linux-FrameBuffer双缓冲机制显示图像_第8张图片

将A区设定为可见区,代码如下:

struct fb_var_screeninfo vinfo; // 显卡设备的可变属性结构体
ioctl(lcd, FBIOGET_VSCREENINFO, &vinfo); // 获取可变属性

// 获得当前显卡所支持的虚拟区显存大小
unsigned long width  = vinfo.xres;
unsigned long height = vinfo.yres;
unsigned long bpp    = vinfo.bits_per_pixel;
unsigned long screen_size = width * height * bpp/8;

// 申请一块两倍与屏幕的映射内存
char *p = mmap(NULL, 2 * screen_size,
            PROT_READ|PROT_WRITE,
            MAP_SHARED, lcd, 0); 

// 将可见区设定为A区
vinfo.xoffset = 0;
vinfo.yoffset = 0;
ioctl(lcd, FBIOPAN_DISPLAY, &vinfo);

// 在B区绘图
int red = 0x00FF0000;
for(int i=0; i<width*height; i++)
    memcpy(p+screen_size+i*4, &red, 4);

执行上述代码,会发现虽然在B区已经填充了某些图像数据,但是屏幕上没有出现任何反应。


  • 3. 将可见区设定为B区,瞬间出现画面,避免了“闪屏”

    为了方便讨论,假设设定两倍屏幕尺寸的虚拟区内存,上半部分为A区,下半部分为B区。如下图所示:
    Linux-FrameBuffer双缓冲机制显示图像_第9张图片

将B区设定为可见区,代码如下:

vinfo.xoffset = 0;
vinfo.yoffset = 480;
ioctl(lcd, FBIOPAN_DISPLAY, &vinfo);

容易想到,只要交替地改变可见区,使得填充数据的过程对用户不可见,等到数据填充完毕,再通过以上代码瞬间调整可见区区域,用户就能感受到画面流程呈现的体验,避免尴尬的闪屏。

下面是完整的使用“双缓冲”机制交替呈现红绿蓝的代码及演示效果图。

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    // 打开LCD设备
    int lcd = open("/dev/fb0", O_RDWR|O_EXCL);

    struct fb_var_screeninfo vinfo; // 显卡设备的可变属性结构体
    ioctl(lcd, FBIOGET_VSCREENINFO, &vinfo); // 获取可变属性

    // 获得当前显卡所支持的虚拟区显存大小
    unsigned long width  = vinfo.xres;
    unsigned long height = vinfo.yres;
    unsigned long bpp    = vinfo.bits_per_pixel;
    unsigned long screen_size = width * height * bpp/8;

    // 申请一块两倍与屏幕的映射内存
    char *p = mmap(NULL, 2 * screen_size,
                PROT_READ|PROT_WRITE,
                MAP_SHARED, lcd, 0); 

    bzero(p, 2*screen_size);

    // 将起始可见区设定为B区
    vinfo.xoffset = 0;
    vinfo.yoffset = 480;
    ioctl(lcd, FBIOPAN_DISPLAY, &vinfo);

    int colors[] = {0x00FF0000, 0x0000FF00, 0x000000FF};
    for(int k=0,n=0;; n++,k++,k%=3)
    {
        for(int i=0; i<width*height; i++)
            memcpy(p+ screen_size*(n%2) +i*4, &colors[k], 4);

        vinfo.xoffset = 0;
        vinfo.yoffset = 480*(n%2);
        ioctl(lcd, FBIOPAN_DISPLAY, &vinfo);

        sleep(1);
    }
}

节选自:粤嵌-嵌入式课堂笔记

联系人:18028569040(曾小美老师·微信)

你可能感兴趣的:(Linux,嵌入式,linux)