18. FrameBuffer 应用编程
1. 什么是 FrameBuffer
FrameBuffer 就是帧缓冲,也就是一块内存,里面保存着一帧图像。是 Linux 系统中的一种显示驱动接口,它将显示设备进行抽象,屏蔽了不同显示设备硬件的实现,对应用层抽象为一块显示内存,允许上层应用程序直接对显示缓冲区进行读写操作,用户不必关系物理显存的位置等具体细节,这些由 FrameBuffer 设备驱动完成。
对应的设备文件为 /dev/fdX,Linux 最多有 32 个。
譬如 LCD 的分辨率是 800*480,每一个像素点的颜色用 24 位(譬如 RGB888)来表示,那么这个显示缓冲区的大小就是 800 x 480 x 24 / 8 = 1152000 个字节。
2. LCD 应用编程介绍
- 首先打开 /dev/fbX 设备文件
- 使用 ioctl() 获取当前显示设备的参数信息
- 通过存储映射 IO 将显示缓冲区映射到用户空间
- 映射成功后就可以直接读取屏幕的显示缓冲区
- 完成显示后,取消映射并关闭设备文件
2.1 使用 ioctl() 获取屏幕参数信息
对 于 Framebuffer 设 备 来 说 , 常 用 的 request 包 括 FBIOGET_VSCREENINFO、FBIOPUT_VSCREENINFO、FBIOGET_FSCREENINFO。
- FBIOGET_VSCREENINFO: 获取设备的可变参数信息,保存在 struct fb_var_screeninfo 结构体中
struct fb_var_screeninfo fb_var; ioctl(fd,FBIOGET_VSCREENINFO,&fb_var);
- FBIOPUT_VSCREENINFO: 设置可变参数信息
struct fb_var_screeninfo fb_var = {0}; // 对 fb_var 进行数据填充 ioctl(fd,FBIOPUT_VSCREENINFO,&fb_var);
- FBIOGET_FSCREENINFO: 获取设备的固定参数信息,意味着应用程序不可修改
struct fb_fix_screeninfo fb_fix; ioctl(fd,FBIOGET_FSCREENINFO,&fb_fix);
上述这些宏都定义在<linux/fb.h>
中
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
int main(int argc,char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
int fd=open("/dev/fb0",O_WDONLY);
// 获取参数信息
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
printf("分辨率: %d*%d\n"
"像素深度 bpp: %d\n"
"一行的字节数: %d\n"
"像素格式: R<%d %d> G<%d %d> B<%d %d>\n",
fb_var.xres, fb_var.yres, fb_var.bits_per_pixel,
fb_fix.line_length,
fb_var.red.offset, fb_var.red.length,
fb_var.green.offset, fb_var.green.length,
fb_var.blue.offset, fb_var.blue.length);
close(fd);
return 0;
}
2.2 为什么要使用 mmap() 将显示缓冲区映射到用户空间
使用普通文件 IO 的方式也可以,但是,当数据量比较大时,普通 IO 方式效率较低,而且显示器的图像往往是动态变化的,而 mmap() 可以将显存映射到进程的地址空间中,这样应用程序便可直接对显示缓冲区进行读写操作。
3. LCD 基本操作
在 LCD 上实现打点、画线等基本操作
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>
#define argb8888_to_rgb565(color) ({ \
unsigned int temp = (color); \
((temp & 0xF80000UL) >> 8) | \
((temp & 0xFC00UL) >> 5) | \
((temp & 0xF8UL) >> 3); \
})
static int width; // LCD X 分辨率
static int height; // LCD Y 分辨率
static unsigned short *screen_base = NULL; // 映射后的显存基地址
static void lcd_draw_point(unsigned int x,unsigned int y,unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color); // 得到 RGB565 颜色值
// 对传入参数的校验
if(x>=width)
x=width-1;
if(y>=height)
y=height-1;
// 填充颜色
screen_base[y*width + x]=rgb565_color;
}
static void lcd_draw_line(unsigned int x,unsigned int y,int dir,unsigned int length,unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);//得到 RGB565 颜色值
unsigned int end;
unsigned long temp;
if (x >= width)
x = width - 1;
if (y >= height)
y = height - 1;
// 填充颜色
temp = y * width + x;//定位到起点
if (dir) //水平线
{
end = x + length - 1;
if (end >= width)
end = width - 1;
for (;x <= end;x++,temp++)
screen_base[temp] = rgb565_color;
}
else //垂直线
{
end = y + length - 1;
if (end >= height)
end = height - 1;
for (;y <= end;y++,temp += width)
screen_base[temp] = rgb565_color;
}
}
static void lcd_draw_rectangle(unsigned int start_x, unsigned int end_x,unsigned int start_y, unsigned int end_y,unsigned int color)
{
int x_len = end_x - start_x + 1;
int y_len = end_y - start_y - 1;
lcd_draw_line(start_x, start_y, 1, x_len, color);//上边
lcd_draw_line(start_x, end_y, 1, x_len, color); //下边
lcd_draw_line(start_x, start_y + 1, 0, y_len, color);//左边
lcd_draw_line(end_x, start_y + 1, 0, y_len, color);//右边
}
static void lcd_fill(unsigned int start_x, unsigned int end_x,unsigned int start_y, unsigned int end_y,unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);//得到 RGB565 颜色值
unsigned long temp;
unsigned int x;
if (end_x >= width)
end_x = width - 1;
if (end_y >= height)
end_y = height - 1;
temp = start_y * width; //定位到起点行首
for (;start_y <= end_y;start_y++,temp+=width)
{
for (x = start_x; x <= end_x; x++)
screen_base[temp + x] = rgb565_color;
}
}
int main(int argc,char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
unsigned int screen_size;
int fd=open("/dev/fb0",O_RDWR);
ioctl(fd, FBIOGET_CSCREENINFO, &fb_fix);
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
screen_size = fb_fix.line_length * fb_var.yres;
width = fb_var.xres;
height= fb_var.yres;
// 将显示缓冲区映射到进程地址空间
screen_base = mmap(NULL,screen_size,PROT_WRITE,MAP_SHARED,fd,0);
if(MAP_FAILED == (void*)screen_base)
{
perror("mmap");
close(fd);
return -1;
}
// 画正方形方块
int w=height*0.25; // 方块的宽度为屏幕的1/4
lcd_fill(0,width-1,0,height-1,0x0); // 清屏,显示黑色
lcd_fill(0,w,0,w,0xFF0000); // 红色方块
lcd_fill(width-w,width-1,0,w,0fFF00); // 绿色方块
lcd_fill(0,w,height-w,height-1,0xFF); // 蓝色方块
lcd_fill(width-w,width-1,height-w,height-1,0xFFFF00); // 黄色方块
// 画十字交叉线
lcd_draw_line(0, height * 0.5, 1, width, 0xFFFFFF);//白色线
lcd_draw_line(width * 0.5, 0, 0, height, 0xFFFFFF);//白色线
// 画矩形
unsigned int s_x, s_y, e_x, e_y;
s_x = 0.25 * width;
s_y = w;
e_x = width - s_x;
e_y = height - s_y;
for (;(s_x <= e_x) && (s_y <= e_y);s_x+=5,s_y+=5,e_x-=5,e_y-=5)
lcd_draw_rectangle(s_x,e_x,s_y,e_y,0xFFFFFF);
munmap(screen_base,screen_size);
close(fd);
return 0;
}
4. 显示 BMP 图像
4.1 BMP 图像介绍
文件类型是 bmp,它采用位映射存储格式,除了图像深度可选外,图像数据没有进行任何压缩,所以 bmp 文件所占用的空间很大,但是没有失真,而且解析 bmp 图像简单。
bmp 图像深度可选 1bit、4bit、16、24 以及 32,典型的 bmp 图像文件由四部分构成:
- bmp 文件头:包含 bmp 文件的格式、大小、位图数据的偏移量等信息
- 位图信息头:包含位图信息头大小、图像的尺寸、图像大小、位平面数、压缩方式以及颜色索引等信息
- 调色板:这部分是可选的,如果使用索引来表示图像,调色板就是索引与其对应颜色的映射表
- 位图数据:也就是图像数据
一般常见的图像都是以 16 位(R、G、B 三种颜色分别使用 5bit、6bit、5bit 来表示)、24 位(R、G、B 三种颜色都使用 8bit 来表示)色图像为主,我们称这样的图像为真彩色图像,真彩色图像是不需要调色板的,即位图信息头后面紧跟的就是位图数据了。
对某些 BMP 位图文件说并非如此, 譬如 16 色位图、256 色位图,它们需要使用到调色板,具体调色板如何使用,我们不关心,我们将会以 16 位色(RGB565)BMP 图像为例
在 windows 下查看 bmp 文件属性,然后使用十六进制查看该文件,文件头的内容如下:
4.1.1 bmp 头文件
00~01H:0x42、0x4D 对应的 ASCII 字符分别为为 B、M,表示这是 Windows 所支持的位图格式,该字段必须是“BM”才是 Windows 位图文件。
02~05H:对应于文件大小,0x000BB848=768072 字节,与 image.bmp 文件大小是相符的。
06~09H:保留字段。
0A~0D:0x00000046=70,即从文件头部开始到位图数据需要偏移 70 个字节。
bmp 文件头的大小固定为 14 个字节
4.1.2 位图信息头
0E~11H:0x00000038=56,这说明这个位图信息头的大小为 56 个字节。
12~15H:0x00000320=800,图像宽度为 800 个像素,与文件属性一致。
16~19H:0x000001E0=480,图像高度为 480 个像素,与文件属性一致;这个数是一个正数,说明是一个倒向的位图,什么是正向的位图、什么是倒向的位图,说的是图像数据的排列问题;如果是正向的位图,图像数据是按照图像的左上角到右下角方式排列的,水平方向从左到右,垂直方向从上到下。倒向的位图,图像数据则是按照图像的左下角到右上角方式排列的,水平方向依然从左到右,垂直方向改为从下到上。
1A~1BH:0x0001=1,这个值总为 1。
1C~1DH:0x0010=16,表示每个像素占 16 个 bit。
1E~21H:0x00000003=3, bit-fileds 方式。
22~25H:0x000BB802=768002,图像的大小,注意图像的大小并不是 BMP 文件的大小,而是图像数据的大小。
26~29H:0x00000EC2=3778,水平分辨率为 3778 像素/米。
2A~2DH:0x00000EC2=3778,垂直分辨率为 3778 像素/米。
2E~31H:0x00000000=0,本位图未使用调色板。
32~35H:0x00000000=0。
只有压缩方式选项被设置为 bit-fileds(0x3)时,位图信息头的大小才会等于 56 字节,否则,为 40 字节。56 个字节相比于 40 个字节,多出了 16 个字节,那么多出的 16 个字节数据描述了什么信息呢? 稍后再给大家介绍。
4.1.3 位图数据
位图数据就是图像的数据,对于 24 位位图,使用 3 字节数据来表示一个像素点的颜色。当图像中引用的色彩超过 256 种时,就需要 16bpp 或更高 bpp 的位图(24 位、32 位)。调色板不适合 bpp 较大的位图,因此 16bpp 及以上的位图都不使用调色板,不使用调色板的位图图像有两种编码格式:RGB 和 Bit-Fields(下称 BF)
RGB 编码格式是一种均分的思想,使 Red、Green、Blue 三种颜色信息容量一样大,譬如 24bpp-RGB,它通常只有这一种编码格式,在 24bits 中,低 8 位表示 Blue 分量;中 8 为表示 Green 分量;高 8 位表示 Red分量
BF 编码格式与 RGB 不同,它利用位域操作,人为地确定 RGB 三分量所包含的信息容量。位图信息头介绍中提及到,当压缩方式选项置为 BF 时, 位图信息头大小比平时多出 16 字节, 这 16 个字节实际上是 4 个 32bit 的位域掩码,按照先后顺序,它们分别是 R、G、B、A 四个分量的位域掩码。位域掩码的作用是指出 R、G、B 三种颜色信息容量的大小,分别使用多少个 bit 数据来表示,以及三种颜色分量的位置偏移量。譬如对于 16 位色的 RGB565 图像,它的 R、G 和 B 分量的位域掩码分别是 0xF800、0x07E0 和 0x001F,也就是 R 通道使用 2 个字节中的高 5 位表示,G 通道使用 2 个字节中的中间 6 位表示。而 B 通道则使用 2 个字节中的最低 5 位表示, 如下图所示:
在 windows 下得到的通常是 24 位,我们需要使用 ps 来得到 RGB565 格式的位图。使用 ps 打开后,直接另存为 bmp 格式,然后选择 16 位,选择 R5 G6 B5 格式。
4.2 在 LCD 上显示 BMP 图像
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <linux/fb.h>
#include <sys/mman.h>
// 文件头结构体
typedef struct{
unsigned char type[2]; //文件类型
unsigned int size; //文件大小
unsigned short reserved1; //保留字段 1
unsigned short reserved2; //保留字段 2
unsigned int offset; //到位图数据的偏移量
}__attribute__ ((packed)) bmp_file_header;
// 位图信息头结构体
typedef struct {
unsigned int size; //位图信息头大小
int width; //图像宽度
int height; //图像高度
unsigned short planes; //位面数
unsigned short bpp; //像素深度
unsigned int compression; //压缩方式
unsigned int image_size; //图像大小
int x_pels_per_meter; //像素/米
int y_pels_per_meter; //像素/米
unsigned int clr_used;
unsigned int clr_omportant;
} __attribute__ ((packed)) bmp_info_header;
static int width;
static int height;
static unsigned short *screen_base = NULL;
static unsigned long line_length; // LCD 一行的长度
static int show_bmp_image(const char *path)
{
bmp_file_header file_h;
bmp_info_header info_h;
unsigned short *line_buf = NULL; //行缓冲区
unsigned long line_bytes; //BMP 图像一行的字节的大小
unsigned int min_h, min_bytes;
int fd = -1;
int j;
if (0 > (fd = open(path, O_RDONLY)))
{
perror("open error");
return -1;
}
// 读取 BMP 文件头
if (sizeof(bmp_file_header) != read(fd, &file_h, sizeof(bmp_file_header)))
{
perror("read error");
close(fd);
return -1;
}
if (0 != memcmp(file_h.type, "BM", 2))
{
fprintf(stderr, "it's not a BMP file\n");
close(fd);
return -1;
}
// 读取位图信息头
if (sizeof(bmp_info_header) != read(fd, &info_h, sizeof(bmp_info_header)))
{
perror("read error");
close(fd);
return -1;
}
// 打印信息
printf("文件大小: %d\n"
"位图数据的偏移量: %d\n"
"位图信息头大小: %d\n"
"图像分辨率: %d*%d\n"
"像素深度: %d\n",
file_h.size,
file_h.offset,
info_h.size,
info_h.width,
info_h.height,
info_h.bpp);
// 将文件读写位置移动到图像数据开始处
if (-1 == lseek(fd, file_h.offset, SEEK_SET))
{
perror("lseek error");
close(fd);
return -1;
}
// 申请一个 buf、暂存 bmp 图像的一行数据
line_bytes = info_h.width * info_h.bpp / 8;
line_buf = malloc(line_bytes);
if (NULL == line_buf)
{
fprintf(stderr, "malloc error\n");
close(fd);
return -1;
}
if (line_length > line_bytes)
min_bytes = line_bytes;
else
min_bytes = line_length;
// 读取图像数据显示到 LCD
if (0 < info_h.height) //倒向位图
{
if (info_h.height > height)
{
min_h = height;
lseek(fd, (info_h.height - height) * line_bytes, SEEK_CUR);
screen_base += width * (height - 1); //定位到屏幕左下角位置
}
else
{
min_h = info_h.height;
screen_base += width * (info_h.height - 1);
}
for (j = min_h; j > 0; screen_base -= width, j--)
{
read(fd, line_buf, line_bytes); //读取出图像数据
memcpy(screen_base, line_buf, min_bytes);//刷入 LCD 显存
}
}
else //正向位图
{
int temp = 0 - info_h.height; //负数转成正数
if (temp > height)
min_h = height;
else
min_h = temp;
for (j = 0; j < min_h; j++, screen_base += width)
{
read(fd, line_buf, line_bytes);
memcpy(screen_base, line_buf, min_bytes);
}
}
/* 关闭文件、函数返回 */
close(fd);
free(line_buf);
return 0;
}
}
int main(int argc,char *argv[])
{
if(argc!=2)
{
fprintf(stderr,"usage:%s <bmp_file>\n",argv[0]);
return -1;
}
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
unsigned int screen_size;
// 打开 framebuffer 设备
int fd=open("/dev/fb0",O_RDWR);
// 获取参数信息
ioctl(fd,FBIOGET_VSCREENINFO,&fb_var);
ioctl(fd,FBIOGET_FSCREENINFO,&fb_fix);
screen_size = fb_fix.line_length*fb_var.yres;
line_length = fb_fix.line_length;
width=fb_var.xres;
height=fb_var.yres;
// 将显示缓冲区映射到进程地址空间
screen_base = mmap(NULL,screen_size,PROT_WRITE,MAP_SHARED,fd,0);
if(MAP_FAILED==(void*)screen_base)
{
perror("mmap");
close(fd);
return -1;
}
// 显示 bmp 图片
memset(screen_base,0xFF,screen_size);
show_bmp_image(argv[1]);
munmap(screen_base,screen_size);
close(fd);
return 0;
}