目录
什么是 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 应用编程介绍
本小节介绍如何对
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()
关闭设备文件。
使用 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 {__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 {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
屏幕的参数信息,示例代码如下所示:
#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
系统的用户家目 录下,并直接运行它,如下所示
![](https://i-blog.csdnimg.cn/blog_migrate/e3bb9fa4e198de9151e220f4e19d172e.png)
笔者使用的是
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
格式的显示设备。
前面我们提到可以通过
ioctl()
去设置
LCD
的可变参数,使用
FBIOPUT_VSCREENINFO
宏,但不太建 议大家去改这些参数,如果 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
操作,示例代码如 下所示:
#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
设备文件,退出程序。
编译应用程序:
![](https://i-blog.csdnimg.cn/blog_migrate/8f510009e6785a4290b3e6fdacee67aa.png)
将编译得到的可执行文件拷贝到开发板
Linux
系统的用户家目录下,执行应用程序(在测试之前,先将 出厂系统对应的 Qt GUI
应用程序退出):
![](https://i-blog.csdnimg.cn/blog_migrate/2f70887fb01e1b271b59746e0d715feb.png)
此时 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
文件头、位图信息头、调色板和位图数据,总结如下表所示:
![](https://i-blog.csdnimg.cn/blog_migrate/31d9e3a505b3648d188ccbf7b022bc40.png)
一般常见的图像都是以
16
位(
R
、
G
、
B
三种颜色分别使用
5bit
、
6bit
、
5bit
来表示)、
24
位(
R
、
G
、 B 三种颜色都使用
8bit
来表示)色图像为主,我们称这样的图像为真彩色图像,真彩色图像是不需要调色板 的,即位图信息头后面紧跟的就是位图数据了。
对某些
BMP
位图文件说并非如此,譬如
16
色位图、
256
色位图,它们需要使用到调色板,具体调色板 如何使用,我们不关心,本节我们将会以 16
位色(
RGB565
)
BMP
图像为例。
![](https://i-blog.csdnimg.cn/blog_migrate/c2b1b25ac089374333d7516029ec736a.png)
首先在 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;
结构体中每一个成员说明如下:
![](https://i-blog.csdnimg.cn/blog_migrate/e09ded090a45519d84b15e523538e9eb.png)
从上面的描述信息,再来对照文件数据:
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
位表示,如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/5900d85d4ff4ce4c6ef71c0e55ab89c2.png)
如何得到 16 位色 RGB565 格式 BMP 图像?
在
Windows
下我们转换得到的
BMP
位图通常是
24
位色的
RGB888
格式图像,那如何得到
RGB565
格 式 BMP
位图呢?当然这个方法很多,这里笔者向大家介绍一种方法就是通过
Photoshop
软件来得到
RGB565 格式的 BMP
位图。
首先,找一张图片,图片格式无所谓,只要
Photoshop
软件能打开即可;确定图片之后,我们启动
Photoshop 软件,并且使用 Photoshop
软件打开这张图片,打开之后点击菜单栏中的文件
--->
存储为,接着出现如下界 面:
![](https://i-blog.csdnimg.cn/blog_migrate/6592e32711fbddecfeba7931c00a4c32.png)
在这个界面中,首先选择文件保存的路径,然后设置文件名以及文件格式,选择文件格式为
BMP
格式, 之后点击保存,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/0cd4640cf6e7012f9f81afe344120c84.png)
点击选择 16 位色图,接着点击高级模式按钮:
点击选择 RGB565,接着点击确定按钮即可,这样就可得到 16 位色 RGB565 格式的 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; //保留字段 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
位图是正向的位图还是倒向的位图,它们的处理方式不一样,这些代码 自己去看,笔者不好去解释,毕竟这只是文字描述的形式,不太好表述!代码只是一种参考,自己能够独立 写出来才是硬道理!
关于本示例代码就介绍这么多,接下来使用交叉编译工具编译上述示例代码,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/941b3fd5c842373e48bb84b0c0a8cb0f.png)