FrameBuffer 应用编程
- 19.1 什么是 FrameBuffer
- 19.2 LCD 的基础知识
- 19.3 LCD 应用编程介绍
- 使用 ioctl()获取屏幕参数信息
- 使用 mmap()将显示缓冲区映射到用户空间
- 19.4 LCD 应用编程练习之 LCD 基本操作
- 19.5 LCD 应用编程练习之显示 BMP 图片
- 在 LCD 上显示 BMP 图像
- 在开发板上测试
本章学习 Linux 下的 Framebuffer 应用编程,通过对本章内容的学习,大家将会了解到 Framebuffer 设备
究竟是什么?以及如何编写应用程序来操控 FrameBuffer 设备。
本章将会讨论如下主题。
⚫ 什么是 Framebuffer 设备?
⚫ LCD 显示的基本原理;
⚫ 使用存储映射 I/O 方式编写 LCD 应用程序。
⚫ 在 LCD 上打点、画线;
⚫ BMP 图片格式详解;
⚫ 在 LCD 上显示图片;
19.1 什么是 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。
19.2 LCD 的基础知识
关于 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 的应用编程还是非常简单的,这些知识点都是在前面的入门篇中给大家介绍过。
使用 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_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 结构体内容如下所示:
示例代码 19.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 结构体如下所示:
示例代码 19.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 结构体内容如下所示:
示例代码 19.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 屏幕的参数信息,示例代码如下所示:
示例代码 19.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 系统的用户家目录下,并直接运行它,如下所示:
笔者使用的是 7 寸 800480 RGB 屏,与上图打印显示的分辨率 800480 是相符的;像素深度为 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 寸 800480、4.3 寸 480272、7 寸 800480、7 寸 1024600 以 及 10.1 寸 1280*800 硬件上均支持 RGB888,但 ALPHA/Mini I.MX6U 开发板出厂系统中,LCD 驱动程序将其实现为一个 RGB565 格式的显示设备,用户可修改设备树使其支持 RGB888,或者通过 ioctl 修改。
前面我们提到可以通过 ioctl()去设置 LCD 的可变参数,使用 FBIOPUT_VSCREENINFO 宏,但不太建议大家去改这些参数,如果 FrameBuffer 驱动程序支持不够完善,改完之后可能会出现一些问题!这里就不再演示了。
使用 mmap()将显示缓冲区映射到用户空间
在入门篇 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 方式。
19.4 LCD 应用编程练习之 LCD 基本操作
本小节编写应用程序,在 LCD 上实现画点(俗称打点)、画线、画矩形等基本 LCD 操作,示例代码如下所示:
示例代码 19.4.1 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; //映射后的显存基地址
/********************************************************************
* 函数名称: 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 屏上将会显示程序中绘制的方块、矩形、以及线条:
忽略手机拍摄的问题,实际效果各位读者运行程序便知。
19.5 LCD 应用编程练习之显示 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 图像。
在 LCD 上显示 BMP 图像
通过上小节对 BMP 图像的介绍之后,相信大家对 BMP 文件的格式已经非常了解了,那么本小节我们将编写一个示例代码,在 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>
/**** BMP 文件头数据结构 ****/
typedef struct {
unsigned char type[2]; //文件类型unsigned int size; //文件大小unsigned short reserved1; //保留字段 1unsigned short reserved2; //保留字段 2unsigned 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;elsemin_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;elsemin_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 位图是正向的位图还是倒向的位图,它们的处理方式不一样,这些代码自己去看,笔者不好去解释,毕竟这只是文字描述的形式,不太好表述!代码只是一种参考,自己能够独立写出来才是硬道理!
关于本示例代码就介绍这么多,接下来使用交叉编译工具编译上述示例代码,如下:
在开发板上测试
将上小节编译得到的可执行文件 testApp 以及测试使用的 bmp 图像文件拷贝到开发板 Linux 系统的用户家目录下:
接着执行测试程序(在测试之前,先将出厂系统对应的 Qt GUI 应用程序退出):
此时 LCD 屏上会显示 image.bmp 图像。如下所示:
忽略手机拍摄的问题,由于周围物体以及光线导致上图显示的结果与实际 LCD 显示的图像存在差异,image.bmp 原图如下所示:
本章内容到此结束!