zl程序教程

您现在的位置是:首页 >  系统

当前栏目

【正点原子Linux连载】第十八章 FrameBuffer应用编程-摘自【正点原子】I.MX6U嵌入式Linux C应用编程指南V1.1

Linux嵌入式应用编程 指南 原子 连载 摘自
2023-09-11 14:20:39 时间

1)实验平台:正点原子阿尔法Linux开发板
2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434
2)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html
3)对正点原子Linux感兴趣的同学可以加群讨论:935446741
4)关注正点原子公众号,获取最新资料更新
在这里插入图片描述

第十八章 FrameBuffer应用编程

本章学习Linux下的Framebuffer应用编程,通过对本章内容的学习,大家将会了解到Framebuffer设备究竟是什么?以及如何编写应用程序来操控FrameBuffer设备。
本章将会讨论如下主题。
什么是Framebuffer设备?
LCD显示的基本原理;
使用存储映射I/O方式编写LCD应用程序。
在LCD上显示不同的颜色;
在LCD上显示图片;

20.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,譬如阿尔法开发板出厂烧录的Linux系统中,/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。
20.2LCD的基础知识
关于LCD相关的基础知识,本书不再介绍,在阿尔法I.MX6U开发板配套提供的驱动教程中已经有过详细的介绍,除此之外,网络上也能找到相关内容。
20.3LCD应用编程介绍
本小节介绍如何对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的应用编程还是非常简单的,这些知识点都是在前面的入门篇中给大家介绍过。
20.3.1使用ioctl()获取屏幕参数信息
当打开LCD设备文件之后,需要先获取到LCD屏幕的参数信息,譬如LCD的X轴分辨率、Y轴分辨率以及像素格式等信息,通过这些参数计算出LCD显示缓冲区的大小。
通过ioctl()函数来获取屏幕参数,3.10.2小节给大家介绍过该函数,ioctl()是一个文件IO操作的杂物箱,可以处理的事情非常杂、不统一,一般用于操作特殊文件或设备文件,为了方便讲解,再次把ioctl()函数的原型列出:
#include <sys/ioctl.h>

int ioctl(int fd, unsigned long request, …);
第一个参数fd对应文件描述符;第二个参数request与具体要操作的对象有关,没有统一值;此函数是一个可变参函数,第三个参数需要根据request参数来决定,配合request来使用。
譬如对于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()调用成功之后会将可变参数信息保存在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 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()调用成功之后会将固定参数信息保存在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表示显存的长度。line_length表示屏幕的一行像素点有多少个字节,等价于xres * bits_per_pixel / 8;通常可以使用line_length * yres来得到屏幕显示缓冲区的大小。
通过上面介绍,接下来我们编写一个示例代码,获取LCD屏幕的参数信息,示例代码如下所示:
本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->19_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屏通过软排线连接到阿尔法开发板(掉电情况下连接),连接好之后启动开发板,如图 17.4.1所示,为了测试方便,可以将出厂系统的GUI应用程序退出。
使用交叉编译工具编译上述示例代码,将编译得到的可执行文件拷贝到开发板出厂系统的家目录下,并直接运行它,如下所示:
在这里插入图片描述

图 20.3.1 获取到屏幕参数
笔者测试用的阿尔法开发板连接的LCD屏是正点原子的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寸800
480、4.3寸480272、7寸800480、7寸1024600以及10.1寸1280800硬件上均支持RGB888,但阿尔法I.MX6开发板出厂烧录的Linux系统,其LCD驱动程序实现支持的是RGB565格式,用户可修改驱动程序或设备树使其支持RGB888。
20.3.2使用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方式。
20.4LCD应用编程练习之刷LCD背景颜色
本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->19_lcd->lcd_background.c。
本小节编写一个简单的应用程序,将LCD的背景颜色修改为指定的颜色,示例代码如下所示:
示例代码 20.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 <linux/fb.h>
#include <sys/mman.h>

/**** RGB888颜色定义 ****/
typedef struct rgb888_type {
    unsigned char blue;
    unsigned char green;
    unsigned char red;
} __attribute__ ((packed)) rgb888_t;

static int width;                 	//LCD X分辨率
static int height;                  	//LCD Y分辨率
static void *screen_base = NULL;   	//映射后的显存基地址
static int bpp;                    	//像素深度

/********************************************************************
 * 函数名称: set_background_color
 * 功能描述: 将LCD背景颜色设置为指定的颜色
 * 输入参数: 颜色
 * 返 回 值: 无
 ********************************************************************/
static void set_background_color(unsigned int color)
{
    int size = height * width;  //计算出像素点个数
    int j;

    switch (bpp) {
    case 16: {  //RGB565
        unsigned short *base = screen_base;
        unsigned short rgb565_color =
            ((color & 0xF80000UL) >> 8) |
            ((color & 0xFC00UL) >> 5) |
            ((color & 0xF8UL) >> 3); //得到RGB565颜色

        /* 向每一个像素点填充颜色 */
        for (j = 0; j < size; j++)
            base[j] = rgb565_color;
    }
        break;

    case 24: {  //RGB888
        rgb888_t *base = screen_base;
        rgb888_t rgb888_color = {
            .blue = color & 0xFFUL,
            .green = (color & 0xFF00UL) >> 8,
            .red = (color & 0xFF0000UL) >> 16,
        };

        for (j = 0; j < size; j++)
            base[j] = rgb888_color;
    }
        break;

    case 32: {  //ARGB8888
        unsigned int *base = screen_base;

        for (j = 0; j < size; j++)
            base[j] = color;
    }
        break;

    default:
        fprintf(stderr, "can't surport %dbpp\n", bpp);
        break;
    }
}

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 <color>\n", argv[0]);
        exit(-1);
    }

    /* 打开framebuffer设备 */
    if (0 > (fd = open("/dev/fb0", O_RDWR))) {
        perror("open error");
        exit(-1);
    }

    /* 获取参数信息 */
    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;
    bpp = fb_var.bits_per_pixel;

    /* 将显示缓冲区映射到进程地址空间 */
    screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
    if (MAP_FAILED == screen_base) {
        perror("mmap error");
        close(fd);
        exit(-1);
    }

    /* 刷背景 */
    set_background_color(strtoul(argv[1], NULL, 16));

    /* 退出 */
    munmap(screen_base, screen_size);  //取消映射
    close(fd);  //关闭文件
    exit(0);    //退出进程
}

先来看main()函数,在main()函数中,首先第一步是校验传参,执行程序时需要传入一个参数,这个参数就是RGB颜色值,并且格式为RGB888,程序的最终目的是将LCD的背景色设置为该指定颜色。
调用open()函数打开LCD设备节点得到文件描述符fd,使用ioctl()获取到LCD屏幕的可变参数信息和固定参数信息,计算出LCD的显示缓冲区的大小、获取到LCD屏幕的X分辨率和Y分辨率以及像素深度。接着调用mmap()将LCD的显示缓冲区映射到进程的地址空间中,此时显示缓冲区的基地址为screen_base。mmap()函数在13.5小节中已有详细介绍,这里不再重述!
接着调用自定义函数set_background_color()刷LCD的背景颜色,调用strtoul()函数将外部传入的参数(字符串形式)转换为整形数据得到rgb颜色值,将颜色值作为参数传递给set_background_color()函数。在set_background_color()函数中,如果像素深度为16,默认为RGB565格式;如果像素深度为24,默认为RGB888格式;如果像素深度为32,默认为ARGB8888格式。
如果是RGB565,则需要将颜色值转换为RGB565格式,因为传入的颜色值为RGB888格式,怎么转换呢?其实非常简单,只需取R、G、B每个颜色通道的最高位即可,譬如R通道和B通道取高5位数据,G通道取高6位数据,然后在组合成一个16位的RGB565颜色值,最后通过一个for循环将RGB565颜色值写入到每一个像素点即可!
如果是RGB888,因为C语言中没有3个字节大小的整形数据类型,为了方便操作,程序中自定义了一个数据类型rgb888_t,其实就是一个结构体,该结构体占用内存空间大小为3个字节,包括3个无符号char类型变量red、green、blue,分别用于存放R、G、B三个通道颜色,然后写入到LCD显存中。
如果是ARGB8888,直接写入颜色值即可!
前面已经给大家提到过,阿尔法开发板出厂烧录的Linux系统,其LCD驱动实现支持的是RGB565格式,所以自然会进入到case 16分支。
编译测试
使用交叉编译工具编译上述示例代码,得到可执行文件:
在这里插入图片描述

图 20.4.1 编译示例代码
将编译得到的可执行文件拷贝到开发板家目录下,执行程序:
在这里插入图片描述

图 20.4.2 刷新LCD背景颜色
上述命令中,我们将LCD背景色变成RGB 0xFF00FF颜色,RGB颜色的对照表,大家自己百度解决。执行命令之后,可以看到LCD此时显示出的效果为:
在这里插入图片描述

图 20.4.3 显示效果
由于手机拍摄的问题,实际的效果与图片展示的效果可能存在些许差异。对于其它颜色,大家自己动手测试,本文不再演示。
20.5LCD应用编程练习之LCD打点
本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->19_lcd->point_color.c。
本小节编写一个打点的函数,在LCD指定位置上输出指定颜色(描点),对于LCD应用编程来说,这是一个非常基本的操作,示例代码如下所示:
示例代码 20.5.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 <linux/fb.h>
#include <sys/mman.h>
#include <string.h>

/**** RGB888颜色定义 ****/
typedef struct rgb888_type {
    unsigned char blue;
    unsigned char green;
    unsigned char red;
} __attribute__ ((packed)) rgb888_t;

static int width;              		//LCD X分辨率
static int height;                  	//LCD Y分辨率
static void *screen_base = NULL;   	//映射后的显存基地址
static int bpp;                  	//像素深度

/********************************************************************
 * 函数名称: set_point_color
 * 功能描述: 在LCD指定位置(某个像素点)上输出指定颜色(描点)
 * 输入参数: x坐标, y坐标, 颜色
 * 返 回 值: 无
 ********************************************************************/
static void set_point_color(int x, int y, unsigned int color)
{
    /* 校验(x,y)坐标的合法性 */
    if ((x >= width) || (y >= height))
        return;

    switch (bpp) {
    case 16: {  //RGB565
        unsigned short *base = screen_base;
        unsigned short rgb565_color =
            ((color & 0xF80000UL) >> 8) |
            ((color & 0xFC00UL) >> 5) |
            ((color & 0xF8UL) >> 3); //得到RGB565颜色

        base[y * width + x] = rgb565_color;
    }
        break;

    case 24: {  //RGB888
        rgb888_t *base = screen_base;
        rgb888_t rgb888_color = {
            .blue = color & 0xFFUL,
            .green = (color & 0xFF00UL) >> 8,
            .red = (color & 0xFF0000UL) >> 16,
        };

        base[y * width + x] = rgb888_color;
    }
        break;

    case 32: {  //ARGB8888
        unsigned int *base = screen_base;

        base[y * width + x] = color;
    }
        break;

    default:
        fprintf(stderr, "can't surport %dbpp\n", bpp);
        break;
    }
}

int main(int argc, char *argv[])
{
    struct fb_fix_screeninfo fb_fix;
    struct fb_var_screeninfo fb_var;
    unsigned int screen_size;
    int fd;
    int j;

    /* 打开framebuffer设备 */
    if (0 > (fd = open("/dev/fb0", O_RDWR))) {
        perror("open error");
        exit(-1);
    }

    /* 获取参数信息 */
    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;
    bpp = fb_var.bits_per_pixel;

    /* 将显示缓冲区映射到进程地址空间 */
    screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
    if (MAP_FAILED == screen_base) {
        perror("mmap error");
        close(fd);
        exit(-1);
    }

    /* 清屏 */
    memset(screen_base, 0xFF, screen_size);

    /* 利用打点函数画线 */
    for (j = 0; j < width; j++)    //连续打点画一条红线
        set_point_color(j, 100, 0xFF0000);

    for (j = 0; j < width; j++)    //连续打点画一条绿线
        set_point_color(j, 200, 0xFF00);

    for (j = 0; j < width; j++)    //连续打点画一条蓝线
        set_point_color(j, 300, 0xFF);

    /* 退出 */
    munmap(screen_base, screen_size);  //取消映射
    close(fd);  //关闭文件
    exit(0);    //退出进程
}

代码就不再解释了,比较简单。
使用交叉编译工具编译示例代码,将可执行文件拷贝到开发板根文件系统家目录下,执行程序LCD效果如下:
在这里插入图片描述

示例代码 20.5.2 运行效果
20.6LCD应用编程练习之显示BMP图片
本小节编写一个应用程序,在LCD上显示一张BMP图片,在编写程序之前,我们需要对BMP格式图片进行简单地介绍。
20.6.1BMP图像介绍
我们常用的图片格式有很多,一般最常用的有三种: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) 由图像尺寸决定 图像数据
表 18.6.1 BMP图像各数据段说明
一般常见的图像都是以16位(R、G、B三种颜色分别使用5bit、6bit、5bit来表示)、24位(R、G、B三种颜色都使用8bit来表示)色图像为主,我们称这样的图像为真彩色图像,真彩色图像是不需要调色板的,即位图信息头后面紧跟的就是位图数据了。
对某些BMP位图文件说并非如此,譬如16色位图、256色位图,它们需要使用到调色板,具体调色板如何使用,我们不关心,本小节我们将会以16位色(RGB565)BMP图像为例。
以一张16位BMP图像为例(如何的到16位色BMP图像,后面向大家介绍),向大家介绍BMP文件结构,如下图所示:
在这里插入图片描述

图 20.6.1 16位BMP示例图片
首先在Windows下查看该图片的属性,如下所示:
在这里插入图片描述

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

图 20.6.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 说明从文件起始位置到图像数据之间的字节偏移量。
这个参数非常有用,因为位图信息头和调色板的长度会根据不同的情况而变化,所以我们可以用这个偏移量迅速从文件中找到图像数据的起始地址。
表 18.6.2 bmp文件头成员说明
从上面的描述信息,再来对照文件数据:
在这里插入图片描述

图 20.6.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,则表示都重要。
表 18.6.3 位图信息头成员说明
从上面的描述信息,再来对照文件数据:
在这里插入图片描述

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

图 20.6.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位表示,如下图所示:
在这里插入图片描述

图 20.6.7 R、G、B、A四个分量的位域掩码
关于BMP图像文件的格式就给大家介绍这么多,后面的程序代码中将不会再做解释!
如何得到16位色RGB565格式BMP图像?
在Windows下我们转换得到的BMP位图通常是24位色的RGB888格式图像,那如何得到RGB565格式BMP位图呢?当然这个方法很多,这里笔者向大家介绍一种方法就是通过Photoshop软件来得到RGB565格式的BMP位图。
首先,找一张图片,图片格式无所谓,只要Photoshop软件能打开即可;确定图片之后,我们启动Photoshop软件,并且使用Photoshop软件打开这张图片,打开之后点击菜单栏中的文件—>存储为,接着出现如下界面:
在这里插入图片描述

图 20.6.8 设置文件名和文件格式
在这个界面中,首先选择文件保存的路径,然后设置文件名以及文件格式,选择文件格式为BMP格式,之后点击保存,如下:
在这里插入图片描述

图 20.6.9 BMP选项
点击选择16位色图,接着点击高级模式按钮:
在这里插入图片描述

图 20.6.10 BMP高级模式
点击选择RGB565,接着点击确定按钮即可,这样就可得到16位色RGB565格式的BMP图像。
20.6.2编写应用程序
本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->19_lcd->bmp_show.c。
通过上小节对BMP图像的介绍之后,相信大家对BMP文件的格式已经非常了解了,那么本小节我们将编写一个示例代码,在阿尔法I.MX6U开发板上显示一张指定的BMP图像,示例代码笔者已经完成了,如下所示。
示例代码 20.6.1 显示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 <linux/fb.h>
#include <sys/mman.h>
#include <string.h>

/**** RGB888颜色定义 ****/
typedef struct rgb888_type {
    unsigned char blue;
    unsigned char green;
    unsigned char red;
} __attribute__ ((packed)) rgb888_t;

/**** 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 void *screen_base = NULL;        //映射后的显存基地址
static int bpp;                         //像素深度

/********************************************************************
 * 函数名称: show_bmp_image
 * 功能描述: 在LCD上显示指定的BMP图片
 * 输入参数: 文件路径
 * 返 回 值: 无
 ********************************************************************/
static void show_bmp_image(const char *path)
{
    bmp_file_header file_h;
    bmp_info_header info_h;
    int fd = -1;

    /* 打开文件 */
    if (0 > (fd = open(path, O_RDONLY))) {
        perror("open error");
        return;
    }

    /* 读取BMP文件头 */
    if (sizeof(bmp_file_header) !=
        read(fd, &file_h, sizeof(bmp_file_header))) {
        perror("read error");
        close(fd);
        return;
    }

    if (0 != memcmp(file_h.type, "BM", 2)) {
        fprintf(stderr, "it's not a BMP file\n");
        close(fd);
        return;
    }

    /* 读取位图信息头 */
    if (sizeof(bmp_info_header) !=
        read(fd, &info_h, sizeof(bmp_info_header))) {
        perror("read error");
        close(fd);
        return;
    }

    /* 打印信息 */
    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;
    }

    /**** 读取图像数据显示到LCD ****/
    /*******************************************
     * 为了软件处理上方便,这个示例代码便不去做兼容性设计了
     * 我们默认传入的bmp图像是RGB565格式
     * bmp图像分辨率大小与LCD屏分辨率一样
     * 并且是倒向的位图
     *******************************************/
    unsigned short *base = screen_base;
    unsigned int line_bytes = info_h.width * info_h.bpp / 8;

    for (int j = info_h.height - 1; j >= 0; j--)
        read(fd, base + j * width, line_bytes);

    close(fd);
}

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 <path>\n", argv[0]);
        exit(-1);
    }

    /* 打开framebuffer设备 */
    if (0 > (fd = open("/dev/fb0", O_RDWR))) {
        perror("open error");
        exit(-1);
    }

    /* 获取参数信息 */
    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;
    bpp = fb_var.bits_per_pixel;

    /* 将显示缓冲区映射到进程地址空间 */
    screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
    if (MAP_FAILED == screen_base) {
        perror("mmap error");
        close(fd);
        exit(-1);
    }

    /* 显示BMP图片 */
    memset(screen_base, 0xFF, screen_size);
    show_bmp_image(argv[1]);

    /* 退出 */
    munmap(screen_base, screen_size);  //取消映射
    close(fd);  //关闭文件
    exit(0);    //退出进程
}

代码中有两个自定义结构体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变量指定的地址偏移量,
最后读取图像的数据,写入到LCD显存中,为了在软件处理上的方便,上述示例代码没有做兼容性设计,默认用户指定的文件都满足下面三个条件:
是16位色的RGB565格式位图文件;
是倒立的位图;
图像的分辨率与LCD屏分辨率相同。
笔者使用图 18.6.1所示的bmp图像作为本次测试的示例图。
读取图像数据、写入LCD显存对应的代码如下:
unsigned short *base = screen_base;
unsigned int line_bytes = info_h.width * info_h.bpp / 8;

for (int j = info_h.height - 1; j >= 0; j--)
	read(fd, base + j * width, line_bytes);

这是按照倒向位图方式进行解析的。
关于本示例代码就介绍这么多,接下来使用交叉编译工具编译上述示例代码,如下:
在这里插入图片描述

图 20.6.11 编译示例代码
20.6.3在开发板上测试
将上小节编译得到的可执行文件testApp以及测试使用的bmp图像文件拷贝到开发板根文件系统用户家目录下:
在这里插入图片描述

图 20.6.12 可执行文件和bmp文件
接着执行程序,并且传入参数、指定bmp文件路径,执行之后将会在串口终端打印相应的信息,如下:
在这里插入图片描述

图 20.6.13 打印信息
此时LCD上将会显示bmp图像:
在这里插入图片描述

图 20.6.14 LCD上显示出bmp图像
Tips:请忽略手机拍摄问题,大家自己动手测试!
20.7练习
在本章的最后,给大家出一个练习题,要求在LCD上显示中文字符或英文字符,这个任务交给大家自己完成!下一章我们会向大家介绍,如何在LCD上显示中文或英文字符,那么本章我们的内容到这里就结束了,谢谢大家!