正点原子Linux驱动教程 (正点原子linux连载教程)

FrameBuffer应用编程

本章学习Linux下的Framebuffer应用编程,通过对本章内容的学习,大家将会了解到Framebuffer设备究竟是什么?以及如何编写应用程序来操控FrameBuffer设备。

本章将会讨论如下主题。

  • 什么是Framebuffer设备?
  • LCD显示的基本原理;
  • 使用存储映射I/O方式编写LCD应用程序。
  • 在LCD上打点、画线;
  • BMP图片格式详解;
  • 在LCD上显示图片;

什么是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的分辨率是800*480,每一个像素点的颜色用24位(譬如RGB888)来表示,那么这个显示缓冲区的大小就是800 x 480 x 24 / 8 = 1152000个字节。譬如执行下面这条命令将LCD清屏,也就是将其填充为黑色(假设LCD对应的设备节点是/dev/fb0,分辨率为800*480,RGB888格式):

dd if=/dev/zero of=/dev/fb0 bs=1024 count=1125

这条命令的作用就是将1125x1024个字节数据全部写入到LCD显存中,并且这些数据都是0x0。

LCD的基础知识

关于LCD相关的基础知识,本书不再介绍,开发板配套提供的驱动教程中已经有过详细的介绍,除此之外,网络上也能找到相关内容。

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的应用编程还是非常简单的,这些知识点都是在前面的入门篇中给大家介绍过。

使用ioctl()获取屏幕参数信息

当打开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都定义在<linux/fb.h>头文件中,所以在我们的应用程序中需要包含该头文件。

#define FBIOGET_VSCREENINFO 0x4600
#define FBIOPUT_VSCREENINFO 0x4601
#define FBIOGET_FSCREENINFO 0x4602

struct fb_var_screeninfo结构体

struct fb_var_screeninfo结构体内容如下所示:

示例代码 20.3.1 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结构体如下所示:

示例代码 20.3.2 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结构体内容如下所示:

示例代码 20.3.3 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应用编程例程源码->20_lcd->lcd_info.c

示例代码 20.3.4 获取屏幕的参数信息
#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;
/* 打开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系统的用户家目录下,并直接运行它,如下所示:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.3.1 获取到屏幕参数

笔者使用的是7寸800*480 RGB屏,与上图打印显示的分辨率800*480是相符的;像素深度为16,也就意味着一个像素点的颜色值将使用16bit(也就是2个字节)来表示;一行的字节数为1600,一行共有800个像素点,每个像素点使用16bit来描述,一共就是800*16/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寸800*480、4.3寸480*272、7寸800*480、7寸1024*600以及10.1寸1280*800硬件上均支持RGB888,但ALPHA/Mini I.MX6U开发板出厂系统中,LCD驱动程序将其实现为一个RGB565格式的显示设备,用户可修改设备树使其支持RGB888,或者通过ioctl修改。

前面我们提到可以通过ioctl()去设置LCD的可变参数,使用FBIOPUT_VSCREENINFO宏,但不太建议大家去改这些参数,如果FrameBuffer驱动程序支持不够完善,改完之后可能会出现一些问题!这里就不再演示了。

使用mmap()将显示缓冲区映射到用户空间

在入门篇14.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基本操作

本小节编写应用程序,在LCD上实现画点(俗称打点)、画线、画矩形等基本LCD操作,示例代码如下所示:

本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->20_lcd->lcd_test.c

示例代码 20.4.1 LCD画点、画线、画矩形操作
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
文件名 : lcd_test.c
作者 : 邓涛
版本 : V1.0
描述 : FrameBuffer应用程序示例代码
其他 : 无
论坛 : www.openedv.com
日志 : 初版 V1.0 2021/6/15 邓涛创建
***************************************************************/
#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; //映射后的显存基地址
/********************************************************************
* 函数名称: 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屏幕的分辨率,从图 20.3.1可知,ALPHA/Mini I.MX6U开发板出厂系统将LCD实现为一个RGB565显示设备,所以程序中自定义的4个函数在操作LCD像素点时、都是以RGB565的格式写入颜色值。
  • 接着使用mmap建立映射;
  • 映射成功之后就可以在应用层直接操作LCD显存了,调用自定义的函数在LCD上画线、画矩形、画方块;
  • 操作完成之后,调用munmap取消映射,调用close关闭LCD设备文件,退出程序。

编译应用程序:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.4.1 编译LCD测试程序

将编译得到的可执行文件拷贝到开发板Linux系统的用户家目录下,执行应用程序(在测试之前,先将出厂系统对应的Qt GUI应用程序退出):

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.4.2 执行应用程序

此时LCD屏上将会显示程序中绘制的方块、矩形、以及线条:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.4.3 LCD显示的效果

忽略手机拍摄的问题,实际效果各位读者运行程序便知。

LCD应用编程练习之显示BMP图片

本小节介绍如何在LCD上显示一张BMP图片,在编写程序之前,首先需要对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文件头、位图信息头、调色板和位图数据,总结如下表所示:

数据段名称

大小(Byte)

说明

bmp文件头

(bmp file header)

14

包含BMP文件的格式、大小、到位图数据的偏移量等信息

位图信息头

(bitmap information)

通常为40或56字节

包含位图信息头大小、图像的尺寸、图像大小、位平面数、压缩方式以及颜色索引等信息;

调色板

(color palette)

由颜色索引数决定

可选,如果使用索引来表示图像的颜色,则调色板就是索引与其对应颜色的映射表;

位图数据

(bitmap data)

由图像尺寸决定

图像数据

表 20.5.1 BMP图像各数据段说明

一般常见的图像都是以16位(R、G、B三种颜色分别使用5bit、6bit、5bit来表示)、24位(R、G、B三种颜色都使用8bit来表示)*图色**像为主,我们称这样的图像为真彩*图色**像,真彩*图色**像是不需要调色板的,即位图信息头后面紧跟的就是位图数据了。

对某些BMP位图文件说并非如此,譬如16色位图、256色位图,它们需要使用到调色板,具体调色板如何使用,我们不关心,本节我们将会以16位色(RGB565)BMP图像为例。

以一张16位BMP图像为例(如何的到16位色BMP图像,后面向大家介绍),如下图所示:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.1 16位BMP示例图片

首先在Windows下查看该图片的属性,如下所示:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.2 示例图片属性

可以看到该图片的分辨率为800*480,位深度为16bit,每个像素点使用16位表示,也就是RGB565。为了向大家介绍BMP文件结构,接下来使用十六进制查看工具将image.bmp文件打开,文件头部分的内容如下所示:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.3 image.bmp文件的十六进制数据

一、bmp文件头

Windows下为bmp文件头定义了如下结构体:

typedef struct tagBITMAPFILEHEADER
{
UINT16 bfType;
DWORD bfSize;
UINT16 bfReserved1;
UINT16 bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER;

结构体中每一个成员说明如下:

变量名

地址偏移

大小

作用

bfType

00H

2 bytes

说明bmp文件的类型,可取值为:

①BM – Windows

②BA – OS/2 Bitmap Array

③CI – OS/2 Color Icon

④CP – OS/2 Color Pointer

⑤IC – OS/2 Icon

⑥PT – OS/2 Pointer

bfSize

02H

4 bytes

说明该文件的大小,以字节为单位。

bfReserved1

06H

2 bytes

保留字段,必须设置为0。

bfReserved2

08H

2 bytes

保留字段,必须设置为0。

bfOffBits

0AH

4 bytes

说明从文件起始位置到图像数据之间的字节偏移量。

这个参数非常有用,因为位图信息头和调色板的长度会根据不同的情况而变化,所以我们可以用这个偏移量迅速从文件中找到图像数据的偏移地址。

表 20.5.2 bmp文件头成员说明

从上面的描述信息,再来对照文件数据:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.4 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个字节。

二、位图信息头

同样,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;

结构体中每一个成员说明如下:

变量名

地址偏移

大小

作用

biSize

0EH

4 bytes

位图信息头大小。

biWidth

12H

4 bytes

图像的宽度,以像素为单位。

biHeight

16H

4 bytes

图像的高度,以像素为单位。

注意,这个值除了用于描述图像的高度之外,它还有另外一个用途,用于指明该图像是倒向的位图、还是正向的位图。

如果该值是一个正数,说明是倒向的位图;如果该值是一个负数,则说明是正向的位图。

一般情况下,BMP图像都是倒向的位图,也就是该值是一个正数。

biPlanes

1AH

2 bytes

色彩平面数,该值总被设置为1。

biBitCount

1CH

2 bytes

像素深度,指明一个像素点需要多少个bit数据来描述,其值可为1、4、8、16、24、32

biCompression

1EH

4 bytes

说明图像数据的压缩类型,取值范围如下:

①0 – RGB方式

②1 – 8bpp的RLE方式,只用于8bit位图

③2 – 4bpp的RLE方式,只用于4bit位图

④3 – Bit-fields方式

⑤4 – 仅用于打印机

⑥5 – 仅用于打印机

biSizeImage

22H

4 bytes

说明图像的大小,以字节为单位,当压缩类型为BI_RGB时,可设置为0。

biXPelsPerMeter

26H

4 bytes

水平分辨率,用像素/米来表示,有符号整数。

biYPelsPerMeter

2AH

4 bytes

垂直分辨率,用像素/米来表示,有符号整数。

biClrUsed

2EH

4 bytes

说明位图实际使用的彩色表中的颜色索引数。

biClrImportant

32H

4 bytes

说明对图像显示有重要影响的颜色索引的数目,如果是0,则表示都重要。

表 20.5.3 位图信息头成员说明

从上面的描述信息,再来对照文件数据:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.5 位图信息头

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位图分为正向的位图和倒向的位图,主要区别在于图像数据存储的排列方式,前面已经给大家解释的比较清楚了,如下如所示(左边对应的是正向位图,右边对应的则是倒向位图):

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.6 正向位图和倒向位图

所以正向位图先存储图像的第一行数据,从左到右依次存放,接着存放第二行,依次这样;而倒向位图,则先存储图像的最后一行(倒数第一行)数据,也是从左到右依次存放,接着倒数二行,依次这样。

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位表示,如下图所示:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.7 R、G、B、A四个分量的位域掩码

关于BMP图像文件的格式就给大家介绍这么多,后面的程序代码中将不会再做解释!

如何得到16位色RGB565格式BMP图像?

在Windows下我们转换得到的BMP位图通常是24位色的RGB888格式图像,那如何得到RGB565格式BMP位图呢?当然这个方法很多,这里笔者向大家介绍一种方法就是通过Photoshop软件来得到RGB565格式的BMP位图。

首先,找一张图片,图片格式无所谓,只要Photoshop软件能打开即可;确定图片之后,我们启动Photoshop软件,并且使用Photoshop软件打开这张图片,打开之后点击菜单栏中的文件--->存储为,接着出现如下界面:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.8 设置文件名和文件格式

在这个界面中,首先选择文件保存的路径,然后设置文件名以及文件格式,选择文件格式为BMP格式,之后点击保存,如下:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.9 BMP选项

点击选择16位*图色**,接着点击高级模式按钮:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.10 BMP高级模式

点击选择RGB565,接着点击确定按钮即可,这样就可得到16位色RGB565格式的BMP图像。

在LCD上显示BMP图像

通过上小节对BMP图像的介绍之后,相信大家对BMP文件的格式已经非常了解了,那么本小节我们将编写一个示例代码,在LCD上显示一张指定的BMP图像,示例代码笔者已经完成了,如下所示。

本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->20_lcd->bmp_show.c

示例代码 20.5.1 显示BMP图像
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved.
文件名 : show_bmp.c
作者 : 邓涛
版本 : V1.0
描述 : FrameBuffer应用程序示例代码之显示BMP图像
其他 : 无
论坛 : www.openedv.com
日志 : 初版 V1.0 2021/6/15 邓涛创建
***************************************************************/
#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>
/**** 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 <bmp_file>\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位图是正向的位图还是倒向的位图,它们的处理方式不一样,这些代码自己去看,笔者不好去解释,毕竟这只是文字描述的形式,不太好表述!代码只是一种参考,自己能够独立写出来才是硬道理!

关于本示例代码就介绍这么多,接下来使用交叉编译工具编译上述示例代码,如下:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.11 编译示例代码

在开发板上测试

将上小节编译得到的可执行文件testApp以及测试使用的bmp图像文件拷贝到开发板Linux系统的用户家目录下:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.12 将bmp图像和测试程序拷贝到开发板

接着执行测试程序(在测试之前,先将出厂系统对应的Qt GUI应用程序退出):

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.13 执行测试程序

此时LCD屏上会显示image.bmp图像。

如下所示:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.14 LCD显示结果

忽略手机拍摄的问题,由于周围物体以及光线导致上图显示的结果与实际LCD显示的图像存在差异,image.bmp原图如下所示:

正点原子linux开发板使用教程学习,正点原子linux连载教程

图 20.5.15 image.bmp原图

本章内容到此结束!