本章学习Linux 下的Framebuffer 应用编程,通过对本章内容的学习,大家将会了解到Framebuffer 设备究竟是什么?以及如何编写应用程序来操控FrameBuffer 设备。
Frame 是帧的意思,buffer 是缓冲的意思,所以Framebuffer 就是帧缓冲,这意味着Framebuffer 就是一块内存,里面保存着一帧图像。帧缓冲(framebuffer)是Linux 系统中的一种显示驱动接口,它将显示设备(譬如LCD)进行抽象、屏蔽了不同显示设备硬件的实现,对应用层抽象为一块显示内存(显存),它允许上层应用程序直接对显示缓冲区进行读写操作,而用户不必关心物理显存的位置等具体细节,这些都由
Framebuffer 设备驱动来完成。
所以在Linux 系统中,显示设备被称为FrameBuffer 设备(帧缓冲设备),所以LCD 显示屏自然而言就是FrameBuffer 设备。FrameBuffer 设备对应的设备文件为/dev/fbX(X 为数字,0、1、2、3 等),Linux
下可支持多个FrameBuffer 设备,最多可达32 个,分别为/dev/fb0 到/dev/fb31,开发板出厂系统中,/dev/fb0
设备节点便是LCD 屏。
应用程序读写/dev/fbX 就相当于读写显示设备的显示缓冲区(显存),譬如LCD 的分辨率是800480,每一个像素点的颜色用24 位(譬如RGB888)来表示,那么这个显示缓冲区的大小就是800 x 480 x 24 / 8 = 1152000 个字节。譬如执行下面这条命令将LCD 清屏,也就是将其填充为黑色(假设LCD 对应的设备节点是/dev/fb0,分辨率为800480,RGB888 格式):
dd if=/dev/zero of=/dev/fb0 bs=1024 count=1125
这条命令的作用就是将1125x1024 个字节数据全部写入到LCD 显存中,并且这些数据都是0x0。
关于LCD 相关的基础知识,本书不再介绍,开发板配套提供的驱动教程中已经有过详细的介绍,除此之外,网络上也能找到相关内容。
19.3 LCD 应用编程介绍
本小节介绍如何对FrameBuffer 设备(譬如LCD)进行应用编程,通过上面的介绍,相信大家应该已经知道如何操作LCD 显示设备了,应用程序通过对LCD 设备节点/dev/fb0(假设LCD 对应的设备节点是
/dev/fb0)进行I/O 操作即可实现对LCD 的显示控制,实质就相当于读写了LCD 的显存,而显存是LCD 的显示缓冲区,LCD 硬件会从显存中读取数据显示到LCD 液晶面板上。
在应用程序中,操作/dev/fbX 的一般步骤如下:
①、首先打开/dev/fbX 设备文件。
②、使用ioctl()函数获取到当前显示设备的参数信息,譬如屏幕的分辨率大小、像素格式,根据屏幕参数计算显示缓冲区的大小。
③、通过存储映射I/O 方式将屏幕的显示缓冲区映射到用户空间(mmap)。
④、映射成功后就可以直接读写屏幕的显示缓冲区,进行绘图或图片显示等操作了。
⑤、完成显示后,调用munmap()取消映射、并调用close()关闭设备文件。
从上面介绍的操作步骤来看,LCD 的应用编程还是非常简单的,这些知识点都是在前面的入门篇中给大家介绍过。
当打开LCD 设备文件之后,需要先获取到LCD 屏幕的参数信息,譬如LCD 的X 轴分辨率、Y 轴分辨率以及像素格式等信息,通过这些参数计算出LCD 显示缓冲区的大小。
通过ioctl() 函数来获取屏幕参数信息,对于Framebuffer 设备来说,常用的request 包括
FBIOGET_VSCREENINFO、FBIOPUT_VSCREENINFO、FBIOGET_FSCREENINFO。
⚫ FBIOGET_VSCREENINFO:表示获取FrameBuffer 设备的可变参数信息,可变参数信息使用struct fb_var_screeninfo 结构体来描述,所以此时ioctl() 需要有第三个参数,它是一个struct fb_var_screeninfo *指针,指向struct fb_var_screeninfo 类型对象,调用ioctl()会将LCD 屏的可变参数信息保存在struct fb_var_screeninfo 类型对象中,如下所示:
struct fb_var_screeninfo fb_var;
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
⚫ FBIOPUT_VSCREENINFO:表示设置FrameBuffer 设备的可变参数信息,既然是可变参数,那说明应用层可对其进行修改、重新配置,当然前提条件是底层驱动支持这些参数的动态调整,譬如在我们的Windows 系统中,用户可以修改屏幕的显示分辨率,这就是一种动态调整。同样此时ioctl()
需要有第三个参数,也是一个struct fb_var_screeninfo *指针,指向struct fb_var_screeninfo 类型对象,表示用struct fb_var_screeninfo 对象中填充的数据设置LCD,如下所示:
struct fb_var_screeninfo fb_var = {0};
/* 对fb_var 进行数据填充*/
......
......
/* 设置可变参数信息*/
ioctl(fd, FBIOPUT_VSCREENINFO, &fb_var);
⚫ FBIOGET_FSCREENINFO:表示获取FrameBuffer 设备的固定参数信息,既然是固定参数,那就意味着应用程序不可修改。固定参数信息使用struct fb_fix_screeninfo 结构体来描述,所以此时ioctl()
需要有第三个参数,它是一个struct fb_fix_screeninfo *指针,指向struct fb_fix_screeninfo 类型对象,调用ioctl()会将LCD 的固定参数信息保存在struct fb_fix_screeninfo 对象中,如下所示:
struct fb_fix_screeninfo fb_fix;
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
上面所提到的三个宏定义FBIOGET_VSCREENINFO 、FBIOPUT_VSCREENINFO 、
FBIOGET_FSCREENINFO 以及2 个数据结构struct fb_var_screeninfo 和struct fb_fix_screeninfo 都定义在
#define FBIOGET_VSCREENINFO 0x4600
#define FBIOPUT_VSCREENINFO 0x4601
#define FBIOGET_FSCREENINFO 0x4602
struct fb_var_screeninfo 结构体
struct fb_var_screeninfo 结构体内容如下所示:
struct fb_var_screeninfo
{
__u32 xres; /* 可视区域,一行有多少个像素点,X 分辨率*/
__u32 yres; /* 可视区域,一列有多少个像素点,Y 分辨率*/
__u32 xres_virtual; /* 虚拟区域,一行有多少个像素点*/
__u32 yres_virtual; /* 虚拟区域,一列有多少个像素点*/
__u32 xoffset; /* 虚拟到可见屏幕之间的行偏移*/
__u32 yoffset; /* 虚拟到可见屏幕之间的列偏移*/
__u32 bits_per_pixel; /* 每个像素点使用多少个bit 来描述,也就是像素深度bpp */
__u32 grayscale; /* =0 表示彩色, =1 表示灰度, >1 表示FOURCC 颜色*/
/* 用于描述R、G、B 三种颜色分量分别用多少位来表示以及它们各自的偏移量*/
struct fb_bitfield red; /* Red 颜色分量色域偏移*/
struct fb_bitfield green; /* Green 颜色分量色域偏移*/
struct fb_bitfield blue; /* Blue 颜色分量色域偏移*/
struct fb_bitfield transp; /* 透明度分量色域偏移*/
__u32 nonstd; /* nonstd 等于0,表示标准像素格式;不等于0 则表示非标准像素格式*/
__u32 activate;
__u32 height; /* 用来描述LCD 屏显示图像的高度(以毫米为单位)*/
__u32 width; /* 用来描述LCD 屏显示图像的宽度(以毫米为单位)*/
__u32 accel_flags;
/* 以下这些变量表示时序参数*/
__u32 pixclock; /* pixel clock in ps (pico seconds) */
__u32 left_margin; /* time from sync to picture */
__u32 right_margin; /* time from picture to sync */
__u32 upper_margin; /* time from sync to picture */
__u32 lower_margin;
__u32 hsync_len; /* length of horizontal sync */
__u32 vsync_len; /* length of vertical sync */
__u32 sync; /* see FB_SYNC_* */
__u32 vmode; /* see FB_VMODE_* */
__u32 rotate; /* angle we rotate counter clockwise */
__u32 colorspace; /* colorspace for FOURCC-based modes */
__u32 reserved[4]; /* Reserved for future compatibility */
};
通过xres、yres 获取到屏幕的水平分辨率和垂直分辨率,bits_per_pixel 表示像素深度bpp,即每一个像素点使用多少个bit 位来描述它的颜色,通过xres * yres * bits_per_pixel / 8 计算可得到整个显示缓存区的大小。
red、green、blue 描述了RGB 颜色值中R、G、B 三种颜色通道分别使用多少bit 来表示以及它们各自的偏移量,通过red、green、blue 变量可知道LCD 的RGB 像素格式,譬如是RGB888 还是RGB565,亦或者是BGR888、BGR565 等。struct fb_bitfield 结构体如下所示:
struct fb_bitfield
{
__u32 offset; /* 偏移量*/
__u32 length; /* 长度*/
__u32 msb_right; /* != 0 : Most significant bit is right */
};
struct fb_fix_screeninfo 结构体
struct fb_fix_screeninfo 结构体内容如下所示:
struct fb_fix_screeninfo
{
char id[16]; /* 字符串形式的标识符*/
unsigned long smem_start; /* 显存的起始地址(物理地址)*/
__u32 smem_len; /* 显存的长度*/
__u32 type;
__u32 type_aux;
__u32 visual;
__u16 xpanstep;
__u16 ypanstep;
__u16 ywrapstep;
__u32 line_length; /* 一行的字节数*/
unsigned long mmio_start; /* Start of Memory Mapped I/O(physical address) */
__u32 mmio_len; /* Length of Memory Mapped I/O */
__u32 accel; /* Indicate to driver which specific chip/card we have */
__u16 capabilities;
__u16 reserved[2];
};
smem_start 表示显存的起始地址,这是一个物理地址,当然在应用层无法直接使用;smem_len 表示显存的长度,这个长度并一定等于LCD 实际的显存大小。line_length 表示屏幕的一行像素点有多少个字节,通常可以使用line_length * yres 来得到屏幕显示缓冲区的大小。
通过上面介绍,接下来我们编写一个示例代码,获取LCD 屏幕的参数信息,示例代码如下所示:
本例程源码对应的路径为:开发板光盘->11、Linux C 应用编程例程源码->19_lcd->lcd_info.c。
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
int fd;
/* 打开framebuffer 设备*/
if (0 > (fd = open("/dev/fb0", O_WRONLY)))
{
perror("open error");
exit(-1);
}
/* 获取参数信息*/
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);
exit(0);
}
首先打开LCD 设备文件,开发板出厂系统,LCD 对应的设备文件为/dev/fb0;打开设备文件之后得到文件描述符fd,接着使用ioctl()函数获取LCD 的可变参数信息和固定参数信息,并将这些信息打印出来。
在测试之前,需将LCD 屏通过软排线连接到开发板(掉电情况下连接),连接好之后启动开发板。
使用交叉编译工具编译上述示例代码,将编译得到的可执行文件拷贝到开发板Linux 系统的用户家目录下,并直接运行它,如下所示:
笔者使用的是7 寸800480 RGB 屏,与上图打印显示的分辨率800480 是相符的;像素深度为16,也就意味着一个像素点的颜色值将使用16bit(也就是2 个字节)来表示;一行的字节数为1600,一行共有800
个像素点,每个像素点使用16bit 来描述,一共就是80016/8=1600 个字节数据,这也是没问题的。
打印出像素格式为R<11 5> G<5 6> B<0 5>,分别表示R、G、B 三种颜色分量对应的偏移量和长度,第一个数字表示偏移量,第二个参数为长度,从打印的结果可知,16bit 颜色值中高5 位表示R 颜色通道、中间6 位表示G 颜色通道、低5 位表示B 颜色通道,所以这是一个RGB565 格式的显示设备。
Tips:正点原子的RGB LCD 屏幕,包括4.3 寸800480、4.3 寸480272、7 寸800480、7 寸1024600 以及10.1 寸1280800 硬件上均支持RGB888,但ALPHA/Mini I.MX6U 开发板出厂系统中,LCD 驱动程序将其实现为一个RGB565 格式的显示设备,用户可修改设备树使其支持RGB888,或者通过ioctl 修改。
前面我们提到可以通过ioctl()去设置LCD 的可变参数,使用FBIOPUT_VSCREENINFO 宏,但不太建议大家去改这些参数,如果FrameBuffer 驱动程序支持不够完善,改完之后可能会出现一些问题!这里就不再演示了。
在入门篇13.5 小节中给大家介绍了存储映射I/O 这种高级I/O 方式,它的一个非常经典的使用场景便是用在Framebuffer 应用编程中。通过mmap()将显示器的显示缓冲区(显存)映射到进程的地址空间中,这样应用程序便可直接对显示缓冲区进行读写操作。
为什么这里需要使用存储映射I/O 这种方式呢?其实使用普通的I/O 方式(譬如直接read、write)也是可以的,只是,当数据量比较大时,普通I/O 方式效率较低。假设某一显示器的分辨率为1920 * 1080,像素格式为ARGB8888,针对该显示器,刷一帧图像的数据量为1920 x 1080 x 32 / 8 = 8294400 个字节(约等于8MB),这还只是一帧的图像数据,而对于显示器来说,显示的图像往往是动态改变的,意味着图像数据会被不断更新。
在这种情况下,数据量是比较庞大的,使用普通I/O 方式必然导致效率低下,所以才会采用存储映射
I/O 方式。
本小节编写应用程序,在LCD 上实现画点(俗称打点)、画线、画矩形等基本LCD 操作,示例代码如下所示:
本例程源码对应的路径为:开发板光盘->11、Linux C 应用编程例程源码->19_lcd->lcd_test.c。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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; // 映射后的显存基地址
/********************************************************************
* 函数名称:lcd_draw_point
* 功能描述:打点
* 输入参数:x, y, color
* 返回值:无
********************************************************************/
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;
}
/********************************************************************
* 函数名称:lcd_draw_line
* 功能描述:画线(水平或垂直线)
* 输入参数:x, y, dir, length, 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;
}
}
/********************************************************************
* 函数名称:lcd_draw_rectangle
* 功能描述:画矩形
* 输入参数:start_x, end_x, start_y, end_y, 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); // 右边
}
/********************************************************************
* 函数名称:lcd_fill
* 功能描述:将一个矩形区域填充为参数color 所指定的颜色
* 输入参数:start_x, end_x, start_y, end_y, 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;
/* 打开framebuffer 设备*/
if (0 > (fd = open("/dev/fb0", O_RDWR)))
{
perror("open error");
exit(EXIT_FAILURE);
}
/* 获取参数信息*/
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
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 error");
close(fd);
exit(EXIT_FAILURE);
}
/* 画正方形方块*/
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, 0xFF00); // 绿色方块
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); // 关闭文件
exit(EXIT_SUCCESS); // 退出进程
}
在示例代码中定义了一个宏argb8888_to_rgb565,用于实现将unsigned int 类型的颜色(也就是
ARGB8888 颜色)转换为RGB565 颜色。
程序中自定义了4 个函数:
lcd_draw_point:用于实现画点、打点操作,参数x 和y 指定像素点的位置,参数color 表示颜色。
lcd_draw_line:用于实现画线操作,参数x 和y 指定线的起始位置;参数dir 表示方向,水平方向(dir!=0)还是垂直方向(dir=0),不支持斜线画法,画斜线需要一些算法去操作,这不是本章内容需要去关注的知识点;参数length 表示线的长度,以像素为单位;参数color 表示线条的颜色。
lcd_draw_rectangle:用于实现画矩形操作,参数start_x 和start_y 指定矩形左上角的位置;参数end_x
和end_y 指定矩形右下角的位置;参数color 指定矩形4 个边的线条颜色。
lcd_fill:将一个指定的矩形区域填充为参数color 指定的颜色,参数start_x 和start_y 指定矩形左上角的位置;参数end_x 和end_y 指定矩形右下角的位置;参数color 指定矩形区域填充的颜色。
具体代码的实现各位读者自己去看,非常简单,来看下main()中做了哪些事情:
⚫ 首先调用open()打开LCD 设备文件得到文件描述符fd;
⚫ 接着使用ioctl 函数获取LCD 的可变参数信息和固定参数信息,通过得到的信息计算LCD 显存大小、得到LCD 屏幕的分辨率,从图19.3.1 可知,ALPHA/Mini I.MX6U 开发板出厂系统将LCD 实现为一个RGB565 显示设备,所以程序中自定义的4 个函数在操作LCD 像素点时、都是以RGB565
的格式写入颜色值。
⚫ 接着使用mmap 建立映射;
⚫ 映射成功之后就可以在应用层直接操作LCD 显存了,调用自定义的函数在LCD 上画线、画矩形、画方块;
⚫ 操作完成之后,调用munmap 取消映射,调用close 关闭LCD 设备文件,退出程序。
编译应用程序:
将编译得到的可执行文件拷贝到开发板Linux 系统的用户家目录下,执行应用程序(在测试之前,先将出厂系统对应的Qt GUI 应用程序退出):
此时LCD 屏上将会显示程序中绘制的方块、矩形、以及线条:
忽略手机拍摄的问题,实际效果各位读者运行程序便知。
本小节介绍如何在LCD 上显示一张BMP 图片,在编写程序之前,首先需要对BMP 格式图片进行简单地介绍。
我们常用的图片格式有很多,一般最常用的有三种:JPEG(或JPG)、PNG、BMP 和GIF。其中JPEG
(或JPG)、PNG 以及BMP 都是静态图片,而GIF 则可以实现动态图片。在本小节实验中,我们选择使用
BMP 图片格式。
BMP(全称Bitmap)是Window 操作系统中的标准图像文件格式,文件后缀名为“.bmp”,使用非常广。它采用位映射存储格式,除了图像深度可选以外,图像数据没有进行任何压缩,因此,BMP 图像文件所占用的空间很大,但是没有失真、并且解析BMP 图像简单。
BMP 文件的图像深度可选lbit、4bit、8bit、16bit、24bit 以及32bit,典型的BMP 图像文件由四部分组成:
①、BMP 文件头(BMP file header),它包含BMP 文件的格式、大小、位图数据的偏移量等信息;
②、位图信息头(bitmap information),它包含位图信息头大小、图像的尺寸、图像大小、位平面数、压缩方式以及颜色索引等信息;
③、调色板(color palette),这部分是可选的,如果使用索引来表示图像,调色板就是索引与其对应颜色的映射表;
④、位图数据(bitmap data),也就是图像数据。
BMP 文件头、位图信息头、调色板和位图数据,总结如下表所示:
一般常见的图像都是以16 位(R、G、B 三种颜色分别使用5bit、6bit、5bit 来表示)、24 位(R、G、
B 三种颜色都使用8bit 来表示)色图像为主,我们称这样的图像为真彩色图像,真彩色图像是不需要调色板的,即位图信息头后面紧跟的就是位图数据了。
对某些BMP 位图文件说并非如此,譬如16 色位图、256 色位图,它们需要使用到调色板,具体调色板如何使用,我们不关心,本节我们将会以16 位色(RGB565)BMP 图像为例。
以一张16 位BMP 图像为例(如何的到16 位色BMP 图像,后面向大家介绍),如下图所示:
首先在Windows 下查看该图片的属性,如下所示:
可以看到该图片的分辨率为800*480,位深度为16bit,每个像素点使用16 位表示,也就是RGB565。为了向大家介绍BMP 文件结构,接下来使用十六进制查看工具将image.bmp 文件打开,文件头部分的内容如下所示:
一、bmp 文件头
Windows 下为bmp 文件头定义了如下结构体:
typedef struct tagBITMAPFILEHEADER
{
UINT16 bfType;
DWORD bfSize;
UINT16 bfReserved1;
UINT16 bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER;
结构体中每一个成员说明如下:
从上面的描述信息,再来对照文件数据:
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 个字节。
二、位图信息头
同样,Windows 下为位图信息头定义了如下结构体:
typedef struct tagBITMAPINFOHEADER {
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount;
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER;
结构体中每一个成员说明如下:
从上面的描述信息,再来对照文件数据:
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 个字节数据描述了什么信息呢?稍后再给大家介绍。
三、调色板
调色板是单色、16 色、256 色位图图像文件所持有的,如果是16 位、24 位以及32 位位图文件,则BMP
文件组成部分中不包含调色板,关于调色板这里不过多介绍,有兴趣可以自己去了解。
四、位图数据
位图数据其实就是图像的数据,对于24 位位图,使用3 个字节数据来表示一个像素点的颜色,对于16
位位图,使用2 个字节数据来表示一个像素点的颜色,同理,32 位位图则使用4 个字节来描述。
BMP 位图分为正向的位图和倒向的位图,主要区别在于图像数据存储的排列方式,前面已经给大家解释的比较清楚了,如下如所示(左边对应的是正向位图,右边对应的则是倒向位图):
所以正向位图先存储图像的第一行数据,从左到右依次存放,接着存放第二行,依次这样;而倒向位图,则先存储图像的最后一行(倒数第一行)数据,也是从左到右依次存放,接着倒数二行,依次这样。
RGB 和Bit-Fields
当图像中引用的色彩超过256 种时,就需要16bpp 或更高bpp 的位图(24 位、32 位)。调色板不适合
bpp 较大的位图,因此16bpp 及以上的位图都不使用调色板,不使用调色板的位图图像有两种编码格式:
RGB 和Bit-Fields(下称BF)。
RGB 编码格式是一种均分的思想,使Red、Green、Blue 三种颜色信息容量一样大,譬如24bpp-RGB,它通常只有这一种编码格式,在24bits 中,低8 位表示Blue 分量;中8 为表示Green 分量;高8 位表示Red
分量。
而在32bpp-RGB 中,低24 位的编码方式与24bpp 位图相同,最高8 位用来表示透明度Alpha 分量。
32bpp 的位图尺寸太大,一般只有在图像处理的中间过程中使用。对于需要半透过效果的图像,更好的选择是PNG 格式。
BF 编码格式与RGB 不同,它利用位域操作,人为地确定RGB 三分量所包含的信息容量。位图信息头介绍中提及到,当压缩方式选项置为BF 时,位图信息头大小比平时多出16 字节,这16 个字节实际上是4
个32bit 的位域掩码,按照先后顺序,它们分别是R、G、B、A 四个分量的位域掩码,当然如果没有Alpha
分量,则Alpha 掩码没有实际意义。
位域掩码的作用是指出R、G、B 三种颜色信息容量的大小,分别使用多少个bit 数据来表示,以及三种颜色分量的位置偏移量。譬如对于16 位色的RGB565 图像,通常使用BF 编码格式,同样这也是BF 编码格式最著名和最普遍的应用之一,它的R、G 和B 分量的位域掩码分别是0xF800、0x07E0 和0x001F,也就是R 通道使用2 个字节中的高5 位表示,G 通道使用2 个字节中的中间6 位表示。而B 通道则使用2
个字节中的最低5 位表示,如下图所示:
关于BMP 图像文件的格式就给大家介绍这么多,后面的程序代码中将不会再做解释!
如何得到16 位色RGB565 格式BMP 图像?
在Windows 下我们转换得到的BMP 位图通常是24 位色的RGB888 格式图像,那如何得到RGB565 格式BMP 位图呢?当然这个方法很多,这里笔者向大家介绍一种方法就是通过Photoshop 软件来得到RGB565
格式的BMP 位图。
首先,找一张图片,图片格式无所谓,只要Photoshop 软件能打开即可;确定图片之后,我们启动Photoshop
软件,并且使用Photoshop 软件打开这张图片,打开之后点击菜单栏中的文件—>存储为,接着出现如下界面:
在这个界面中,首先选择文件保存的路径,然后设置文件名以及文件格式,选择文件格式为BMP 格式,之后点击保存,如下:
点击选择16 位色图,接着点击高级模式按钮:
点击选择RGB565,接着点击确定按钮即可,这样就可得到16 位色RGB565 格式的BMP 图像。
通过上小节对BMP 图像的介绍之后,相信大家对BMP 文件的格式已经非常了解了,那么本小节我们将编写一个示例代码,在LCD 上显示一张指定的BMP 图像,示例代码笔者已经完成了,如下所示。
本例程源码对应的路径为:开发板光盘->11、Linux C 应用编程例程源码->19_lcd->bmp_show.c。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/**** BMP 文件头数据结构****/
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; // LCD X 分辨率
static int height; // LCD Y 分辨率
static unsigned short *screen_base = NULL; // 映射后的显存基地址
static unsigned long line_length; // LCD 一行的长度(字节为单位)
/********************************************************************
* 函数名称:show_bmp_image
* 功能描述:在LCD 上显示指定的BMP 图片
* 输入参数:文件路径
* 返回值:成功返回0, 失败返回-1
********************************************************************/
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 ****/
/*******************************************
* 为了软件处理上方便,这个示例代码便不去做兼容性设计了
* 如果你想做兼容, 可能需要判断传入的BMP 图像是565 还是888
* 如何判断呢?文档里边说的很清楚了
* 我们默认传入的bmp 图像是RGB565 格式
*******************************************/
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[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
unsigned int screen_size;
int fd;
/* 传参校验*/
if (2 != argc)
{
fprintf(stderr, "usage: %s \n" , argv[0]);
exit(-1);
}
/* 打开framebuffer 设备*/
if (0 > (fd = open("/dev/fb0", O_RDWR)))
{
perror("open error");
exit(EXIT_FAILURE);
}
/* 获取参数信息*/
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 error");
close(fd);
exit(EXIT_FAILURE);
}
/* 显示BMP 图片*/
memset(screen_base, 0xFF, screen_size);
show_bmp_image(argv[1]);
/* 退出*/
munmap(screen_base, screen_size); // 取消映射
close(fd); // 关闭文件
exit(EXIT_SUCCESS); // 退出进程
}
代码中有两个自定义结构体bmp_file_header 和bmp_info_header,描述bmp 文件头的数据结构
bmp_file_header、以及描述位图信息头的数据结构bmp_info_header。
当执行程序时候,需要传入参数,指定一个bmp 文件。main()函数中会调用show_bmp_image()函数在
LCD 上显示bmp 图像,show_bmp_image()函数的参数为bmp 文件路径,在show_bmp_image()函数中首先会打开指定路径的bmp 文件,得到对应的文件描述符fd,接着调用read()函数读取bmp 文件头和位图信息头。
获取到信息之后使用printf 将其打印出来,接着使用lseek()函数将文件的读写位置移动到图像数据起始位置处,也就是bmp_file_header 结构体中的offset 变量指定的地址偏移量。
通过info_h.height 判断该BMP 位图是正向的位图还是倒向的位图,它们的处理方式不一样,这些代码自己去看,笔者不好去解释,毕竟这只是文字描述的形式,不太好表述!代码只是一种参考,自己能够独立写出来才是硬道理!
关于本示例代码就介绍这么多,接下来使用交叉编译工具编译上述示例代码,如下:
将上小节编译得到的可执行文件testApp 以及测试使用的bmp 图像文件拷贝到开发板Linux 系统的用户家目录下:
接着执行测试程序(在测试之前,先将出厂系统对应的Qt GUI 应用程序退出):
此时LCD 屏上会显示image.bmp 图像。
如下所示:
忽略手机拍摄的问题,由于周围物体以及光线导致上图显示的结果与实际LCD 显示的图像存在差异,
image.bmp 原图如下所示: