Linux-FrameBuffer双缓冲机制显示图像
1. 液晶屏的基本概念
- 像素:
屏幕上显示颜色的最小单位,英文叫 pixel。注意,位图(如jpg、bmp等格式的常见图片)也是由一个个的像素点构成的,跟屏幕的像素点的概念一样。原理上讲,将一张位图显示到屏幕上,就是将图片上的像素点一个个复制到屏幕像素点上。
- 分辨率:
- 宽、高两个维度上的像素点数目。
- 分辨率越高,所需要的显存越大。
- 色深:
- 每个像素所对应的内存字节数,一般有8位、16位、24位或32位
- GEC6818开发板的屏幕的色深是32位的
- 32位色深的屏幕一般被称为真彩屏,或1600万色屏。
色深决定了一个像素点所能表达的颜色的丰富程度,色深越大,色彩表现力越强。
2. 内存映射基本原理
虽然LCD设备本质上也可以看作是一个文件,在文件系统中有其对应的设备节点,可以像普通文件一样对其进行读写操作(read/write),但由于对字符设备的读写操作是以字节流的方式进行的,因此除非操作的图像尺寸刚好与屏幕尺寸完全一致,如下图所示,图片的宽高与LCD的宽高完全一致,否则将会画面会乱。
以下是一段直接写设备节点的“不好”的示例代码:
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做内存映射,将屏幕的每一个像素点跟映射内存一一对应,而映射内存可以是二维数组,因此就可以非常方便地通过操作二维数组中的任意元素,来操作屏幕中的任意像素点了。这里的映射内存,有时被称为显存。
如上图所示,将一块内存与LCD的像素一一对应:
- LCD上面显示的图像色彩,由其对应的内存的数据决定
- 映射内存的大小至少得等于LCD的真实尺寸大小
- 映射内存的大小可以大于LCD的真实尺寸,有利于优化动态画面(视频)体验
下面是屏幕显示为红色的示例代码:
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <fcntl.h>
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
如上图所示,如果板卡已经具备LCD的驱动程序,那么应用程序就可以通过 ioctl() 来检索LCD的硬件参数信息。以粤嵌GEC6818开发板配套的群创AT070TN92-7英寸液晶显示屏为例,具体代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
#include <fcntl.h>
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」
根据以上示例代码,采用自动获取屏幕硬件参数的方式,在开发板上轮流显示红绿蓝三原色。
4. 多缓冲机制
仔细观察上述显示单色的程序运行效果,会发现屏幕上的颜色不是一瞬间整体显示的,而是有一个很明显的从上到下刷屏的过程,这实际上是由于我们是一个个像素点从左到右,从上到下刷屏导致的,如果不是速度比较快,我们将会看到屏幕上的点是一个个亮起来的,而不是整屏统一更新,这显然不是最佳的体验。
解决这个问题,可以采用多缓冲的办法,首先要搞明白所谓可见区和虚拟区的关系:
- 可见区、虚拟区都是内存区域,可见区是虚拟区的一部分,因此可见区尺寸至少等于虚拟区。
- 一般而言,可见区尺寸就是屏幕尺寸,比如800×480;而虚拟区是显示设备能支持的显存大小,比如800×480、800×960等。
- 为了提高画面体验,一般先在不可见区操作显存数据,然后在调整可见区位置,使得图像“瞬间”呈现,避免闪屏。
下面以示例代码的形式,来分析如何使用多缓冲机制提高画面体验。
- 1. 设定虚拟区
#include <stdio.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <string.h>
#include <fcntl.h>
#include <linux/fb.h>
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区。如下图所示:
将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区。如下图所示:
将B区设定为可见区,代码如下:
vinfo.xoffset = 0;
vinfo.yoffset = 480;
ioctl(lcd, FBIOPAN_DISPLAY, &vinfo);
容易想到,只要交替地改变可见区,使得填充数据的过程对用户不可见,等到数据填充完毕,再通过以上代码瞬间调整可见区区域,用户就能感受到画面流程呈现的体验,避免尴尬的闪屏。
下面是完整的使用“双缓冲”机制交替呈现红绿蓝的代码及演示效果图。
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <string.h>
#include <fcntl.h>
#include <linux/fb.h>
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(曾小美老师·微信)
更多推荐
所有评论(0)