Bootstrap

✌Linux Frame Buffer(Linux 底层的帧缓冲设备)

开发板的参数:

        芯片:三星S5P6818

        处理器:ARM  Cortex-A53          64bits

        OS:Linux

        lcd屏幕:800 * 480

Linux Frame Buffer(Linux 底层的帧缓冲设备)
        可以显示一帧一帧的图像(屏幕的显示)

1. 基本操作

帧缓冲是Linux系统为显示设备提供的一个接口,把显示设备抽象成一个设备文件,它可以让上层的图像应用程序不需要关心具体的硬件实现细节,上层的图像应用程序只需要操作对应的"文件",往文件中写入"数据",经过帧缓冲驱动,就可以在硬件设备(LCD)上面显示对应的图像

 
如何让LCD设备显示颜色呢?
        Linux中一切都是文件,只需要利用文件IO的接口去操作LCD设备就可以了
操作文件的大概步骤:
        open 
        read/write 
        close 
操作文件的时候需要知道文件名:
        lcd的文件名  "
/dev/fb0"   是一个绝对路径

                linux把LCD这种硬件设备抽象成  Frame  Buffer(帧缓冲设备)

Lcd上图像的显示,本质上是由一个一个的像素点组成的
        像素点:可以描述一个点的颜色
        将LCD上面每一个像素点"描绘"成不同的颜色,就可以显示一副图画

分辨率(描述屏幕的像素点信息):分辨率决定了位图细节的精细程度,通常情况下,分辨率越高包含的像素就越高,图像就越清晰

                6818开发板   800 * 480

                有480行,每一行有800个像素点

                可以通过函数获取

颜色的组成:RGB三基(原)色

        世界上所有的颜色都是由RGB合成的,每一个基色的比重不一样就会合成不同的颜色
        每一个基色使用一个byte(8bits)表示,给基色中的每一个bit赋为不同的值,就可以表示不同的颜色
            问题:
                一个基色可以表示多少种颜色?   0~255 ------>256
                        8个bit的值不同,就可以表示不同的颜色 
                三个基色可以表示多少种颜色?   2^24 ------>1670万种
                        24个bit的值不同,就可以表示不同的颜色 

        LCD上面一个像素点的表示: ARGB      A:透明度(1byte)
                在LCD上面一个像素点使用几个字节表示?   4byte
                使用4byte就可以表示一个像素点的颜色

如果使用一种数据类型来表示一个像素点的颜色,可以使用什么数据类型?
        int  刚好4个字节
        int color;  // color就是一个int类型的变量,占用4byte
        给color赋值为不同的值,color就表示不同的颜色

        

        使用RGB颜色查询对照表查找你所需要的颜色:

                红色: 0x00FF0000
                绿色: 0x0000FF00
                蓝色: 0x000000FF
                黑色: 0x0
                白色: 0x00FFFFFF

只需要把颜色数据写入到屏幕对应的文件中,就可以让屏幕显示对应的颜色
            像素点在"文件"中的排列顺序是从上至下,从左至右
            理论上就可以通过文件去操作屏幕上面每一个像素点的颜色

屏幕上有多少个像素点:800*480
每一行有800个像素点,有480行

写一个简单的代码,让开发板显示对应的颜色
        打开屏幕------->(只能用系统IO打开,因为标准IO只能操作"普通文件")

                // 获取屏幕信息(分辨率,颜色分量)
        写入颜色数据

                // lcd上面从上至下、从左至右每一个像素点都是独立的,每一个像素点都可以使                       用一个int类型的颜色去描述

                // 每一次操作都必须全部给一个颜色值
        关闭屏幕

注意:
        有的开发板开机就会自动运行一个iot的程序
        关闭方法:
                killall iot 
                or 
                设置 /etc/profile 文件

练习:

        1. 画一个矩形,x0 = 100,y0 = 100,w = 200,h = 200;

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    // 打开lcd屏幕
    int fd = open("/dev/fb0", O_RDWR);
    if (-1 == fd) {
        perror("open lcd faile");
        return -1;
    }

    int colour[480][800] = {0};
    for (int y = 100; y < 300; y++) {
        for (int x = 100; x < 300; x++) {
            colour[y][x] = 0xff0000; // 红色
        }
    }
    ssize_t w = write(fd, colour, 800 * 480 * 4);
    if (-1 == w) {
        perror("write lcd faile");
        // 关闭lcd屏幕
        close(fd);
        return -1;
    }

    // 关闭屏幕
    close(fd);

    return 0;
}

        2. 使用基本的文件IO操作函数,在屏幕上面显示简单的颜色
                先显示红色
                1s后(sleep(1))
                显示绿色
                1s后 
                显示蓝色

        注意光标的位置!!!

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    // 打开lcd屏幕
    int fd = open("/dev/fb0", O_RDWR);
    if (-1 == fd) {
        perror("open lcd faile");
        return -1;
    }

    // 操作lcd屏幕
    // int colour = 0xff0000; // 红色
    // for (int y = 0; y < 480; y++) {
    //     for (int x = 0; x < 800; x++) {
    //         ssize_t w = write(fd, &colour, 4);
    //         if (-1 == w) {
    //             perror("write lcd faile");
    //             // 关闭lcd屏幕
    //             close(fd);
    //             return -1;
    //         }
    //     }
    // }

    int colour[480][800] = {0};
    for (int y = 0; y < 480; y++) {
        for (int x = 0; x < 800; x++) {
            colour[y][x] = 0xff0000; // 红色
        }
    }
    ssize_t w = write(fd, colour, 800 * 480 * 4);
    if (-1 == w) {
        perror("write lcd faile");
        // 关闭lcd屏幕
        close(fd);
        return -1;
    }

    sleep(1);
    lseek(fd, SEEK_SET, 0); // 将光标移到文件开头

    for (int y = 0; y < 480; y++) {
        for (int x = 0; x < 800; x++) {
            colour[y][x] = 0x00ff00; // 绿色
        }
    }
    w = write(fd, colour, 800 * 480 * 4);
    if (-1 == w) {
        perror("write lcd faile");
        // 关闭lcd屏幕
        close(fd);
        return -1;
    }

    sleep(1);
    lseek(fd, SEEK_SET, 0); // 将光标移到文件开头

    for (int y = 0; y < 480; y++) {
        for (int x = 0; x < 800; x++) {
            colour[y][x] = 0x0000ff; // 蓝色 
        }
    }
    w = write(fd, colour, 800 * 480 * 4);
    if (-1 == w) {
        perror("write lcd faile");
        // 关闭lcd屏幕
        close(fd);
        return -1;
    }

    // 关闭屏幕
    close(fd);

    return 0;
}

2. 使用ioctl函数去获取设备信息

ioctl函数是用来对文件/设备进行除了读写以外的其他控制操作,每一个设备的控制操作都是不一样的,这些设备具体有哪些操作是由设备的驱动程序决定的

尝试自己获取(有些设备不支持)

NAME
    ioctl - control device
        控制设备(具体的操作由驱动程序决定)
        此处我们可以使用函数获取LCD屏幕的一些基本信息
SYNOPSIS
    #include <sys/ioctl.h>

int ioctl(int fd, unsigned long request, ...);

fd:文件描述符,表示你要操作哪一个设备
      
request:命令号码,在驱动实现的时候,一般会把某些特定的操作取一个命令号
        (进行什么操作),命令号的具体含义是由提供该命令号的驱动程序决定

...:可变参数,具体的操作由命令号决定
        
返回值:
    成功返回0
    失败返回-1,同时errno被设置

例子:
    如果程序需要知道帧缓冲设备的相关信息,可以使用ioctl来完成
    对于帧缓冲设备,最常用的有两条命令(/usr/include/linux/fb.h):
        #define FBIOGET_VSCREENINFO    0x4600
            返回和设备相关的可变信息
            如:帧缓冲设备的大小(宽度,高度),以及颜色显示等信息

        #define FBIOGET_FSCREENINFO    0x4602
            返回和设备相关的固定信息
            如:设备本身的一些信息,硬件加速度等信息

        两个命令号分别有对应的结构体:
        定义在内核的头文件中:/usr/include/linux/fb.h
            struct fb_var_screeninfo{}    
            和
            struct fb_fix_screeninfo{}

        获取方法:
            // 定义一个结构体用来保存即将获取到的数据
            struct fb_var_screeninfo  vinfo;      
            // 把获取到的信息保存到结构体中
            ioctl(fd, FBIOGET_VSCREENINFO, &vinfo);

    struct fb_var_screeninfo {
        __u32 xres;  // 屏幕的宽度,每一行上面的像素点个数
        __u32 yres;  // 屏幕的高度,屏幕有多少行

        __u32 xres_virtual;  /* virtual resolution */
        __u32 yres_virtual;

        __u32 xoffset;      /* offset from virtual to visible */
        __u32 yoffset;      /* resolution */

        __u32 bits_per_pixel;  // 每一个像素点占用的bit位数量
        ...
       
        struct fb_bitfield red;     // R 描述红色的信息
        struct fb_bitfield green;   // G
        struct fb_bitfield blue;    // B
        struct fb_bitfield transp;  // A
        ......
    };       

    // 描述每一个颜色分量(A,R,G,B)占用的bit的区域信息
    struct fb_bitfield {
        __u32 offset;        // 颜色位从哪一个位开始
        __u32 length;        // 这个分量占用多少个bit  
        __u32 msb_right;     // true(!0)表示最高位在右边
    };

代码实现:

#include <stdio.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 fd = open("/dev/fb0", O_RDWR);
    if (-1 == fd) {
        perror("open lcd error");
        return -1;
    }

    // 获取屏幕的信息
    // 定义一个结构体用来保存即将获取到的数据
    struct fb_var_screeninfo vinfo; // 头文件:#include <linux/fb.h>       
    // 把获取到的信息保存到结构体中
    int r = ioctl(fd, FBIOGET_VSCREENINFO, &vinfo);
    if (-1 == r) {
        perror("ioctl failed");
        close(fd);
        return -1;
    }

    // 解析结构体,打印获取到的屏幕的信息
    // 分辨率
    printf("resolution: %d * %d\n", vinfo.xres, vinfo.yres);
    // 每一个像素点占用的bit数量
    printf("bits_per_pixel: %d\n", vinfo.bits_per_pixel);
    // 每一种颜色的信息
    printf("transp.offset: %d\n", vinfo.transp.offset);
    printf("transp.length: %d\n", vinfo.transp.length);
    printf("transp.msb_right: %d\n", vinfo.transp.msb_right);

    printf("red.offset: %d\n", vinfo.red.offset);
    printf("red.length: %d\n", vinfo.red.length);
    printf("red.msb_right: %d\n", vinfo.red.msb_right);

    printf("green.offset: %d\n", vinfo.green.offset);
    printf("green.length: %d\n", vinfo.green.length);
    printf("green.msb_right: %d\n", vinfo.green.msb_right);

    printf("blue.offset: %d\n", vinfo.blue.offset);
    printf("blue.length: %d\n", vinfo.blue.length);
    printf("blue.msb_right: %d\n", vinfo.blue.msb_right);

    // 关闭屏幕
    close(fd);

    return 0;
}
resolution: 800 * 480
bits_per_pixel: 32
transp.offset: 24
transp.length: 8
transp.msb_right: 0
red.offset: 16
red.length: 8
red.msb_right: 0
green.offset: 8
green.length: 8
green.msb_right: 0
blue.offset: 0
blue.length: 8
blue.msb_right: 0

3. 操作LCD屏幕的效率问题 (mmap)

每一次操作屏幕的图像,都需要使用write函数
        但是write函数会直接去访问硬件资源,占用总线,效率是非常低的

        连续调用write函数,需要lseek函数的配合,write函数会改变文件的偏移量
        ========>
        内存映射,把屏幕文件映射到内存中(操作内存就相当于操作屏幕)

映射:把文件和一段内存一一对应(建立一个联系),操作文件的时候就不需要使用write函数,只需要使用指针操作对应的内存就可以了(C语言中指针可以直接操作内存),内存内容的改变会由具体的映射驱动同步到文件中去

        在LCD中,如果把屏幕映射到内存中,就不需要使用write函数了,只需要去操作映射之后的内存即可

NAME
    mmap(映射), munmap(解映射) - map or unmap files or devices into memory
SYNOPSIS
    #include <sys/mman.h>
        
mmap是把指定的文件或者设备映射到进程地址空间(堆和栈中间),应用程序就可以通过指针去
操作映射后的地址空间从而操作文件或者设备
        
void* mmap(void *addr, size_t length, int prot, int flags,
               int fd, off_t offset);
           
addr:表示映射地址,你要把文件映射到内存的哪一个位置
      一般为NULL,表示让OS自行选择一个合适的地址
           
length:表示你要映射的长度(字节),你的文件多大
            如:映射lcd.  800*480*4
            
prot:应用程序对映射之后的内存的操作权限
          PROT_EXEC   可执行
          PROT_READ   可读
          PROT_WRITE  可写
          PROT_NONE   没有任何权限
          可读可写:PROT_READ | PROT_WRITE     
           
flags:映射标记
           MAP_SHARED:共享映射,内存的改变会同时同步到文件,对其他进程可见
           MAP_PRIVATE:私有映射,映射的更新对于映射同一文件的其他进程不可见,也不
                       会传递到基础文件
            
fd:你要映射的文件的文件描述符
            
offset:偏移量,表示你要从文件的哪一个位置开始映射,一般为0,表示从文件的开头开始映射
        
返回值:
    成功返回映射之后的内存的首地址(相当于文件的开头)
    失败返回MAP_FAILED,errno被设置
---------------------------------------------------------------------------
munmap是解除内存的映射关系
int munmap(void *addr, size_t length);
            
addr:你要解映射的首地址,是mmap的返回值
          
length:你要解映射的长度

流程:
        打开文件 
        映射文件到内存
        ==============
        操作内存就是操作文件
        ==============      
        解除映射
        关闭文件  

int *plcd = (int*)mmap(NULL, 800 * 480 * 4, PROT_READ | PROT_WRITE, 
                MAP_SHARED, fd, 0);
if (plcd == MAP_FAILED) {
    perror("mmap failed");
    close(fd);
    return -1;
}

假设映射成功后,映射后的像素点在内存中的对应关系是从左至右,从上至下
        也就是说,plcd指向的位置对应屏幕上面第0行的第0个点
        plcd+1对应屏幕上面第0行的第1个点(指针做偏移,是偏移单位个指向类型的长度)

如果想要通过plcd把屏幕的第0行的第0个点设置为红色,应该如何操作?
        把0x00ff0000这个数据放到第0行的第0个点对应的内存地址
        第0行的第0个点对应的内存地址就是plcd
        *plcd = 0x00FF0000

如果想要通过plcd把屏幕的第0行的第1个点设置为红色,应该如何操作?
        把0x00ff0000这个数据放到第0行的第1个点对应的内存地址
        第0行的第1个点对应的内存地址就是(plcd+1)
        *(plcd+1) = 0x00FF0000

如果想要通过plcd把屏幕的第0行的第x个点设置为红色,应该如何操作?
        把0x00ff0000这个数据放到第0行的第x个点对应的内存地址
        第0行的第x个点对应的内存地址就是(plcd+x)
        *(plcd+x) = 0x00FF0000  

如果想要通过plcd把屏幕的第y行的第x个点设置为红色,应该如何操作?
        把0x00ff0000这个数据放到第y行的第x个点对应的内存地址
        第y行的第x个点对应的内存地址就是(plcd+800*y+x)
        *(plcd + 800 * y + x) = 0x00FF0000

我们可以通过plcd去操作屏幕上面的每一个像素点
注意:因为内存是连续的,plcd向后只能偏移800*480*4个字节有权限使用

4. lcd代码实现

lcd.h
#ifndef __LCD_H__
#define __LCD_H__

#include <stdio.h>
#include <string.h>
#include <math.h> // arm-linux-gcc *.c -lm
#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>

// 定义一个结构体,描述一个打开的文件
typedef struct lcd {
    char name[256];
    int fd; // 文件描述符
    int *plcd; // 映射之后的首地址
    int xres;
    int yres;
    int bits_per_pixel; // 每一个像素点占用的bit位数量 / 8
} lcd;

/*
    init_lcd:初始化屏幕(打开文件/映射到内存)
    name:要初始化的屏幕的路径名,把初始化后的屏幕信息保存到lp指针指向的空间
*/
void init_lcd(const char *name, lcd *lp);

/*
    close_lcd:关闭屏幕(解映射/关闭文件)
    lp:要关闭的文件
*/
void close_lcd(lcd *lp);

/*
    lcd_draw_point:能够在指定的屏幕的指定的位置显示一个指定的颜色
*/
void lcd_draw_point(lcd *lp, int x, int y, int colour);

/*
    lcd_clear:把屏幕刷成指定的颜色
*/
void lcd_clear(lcd *lp, int colour);

/*
    在屏幕上指定的位置(x,y)显示一个指定大小,指定颜色的矩形
*/
void lcd_draw_rect(lcd *lp, int x, int y, int w, int h, int colour);

/*
    在屏幕上指定的位置(x,y)显示一个指定大小,指定颜色的圆
*/
void lcd_draw_cir(lcd *lp, int x, int y, int r, int colour);

#endif
lcd.c
#include "lcd.h"

/*
    init_lcd:初始化屏幕(打开文件/映射到内存)
    name:要初始化的屏幕的路径名,把初始化后的屏幕信息保存到lp指针指向的空间
*/
void init_lcd(const char *name, lcd *lp) 
{
    strcpy(lp->name, name);

    // 打开屏幕
    lp->fd = open(name, O_RDWR);
    if (-1 == lp->fd) 
    {
        perror("open lcd failed");
        return ;
    }

    // 通过ioctl获取设备信息
    struct fb_var_screeninfo vinfo; // 定义一个结构体用来保存即将获取到的信息
    int r = ioctl(lp->fd, FBIOGET_VSCREENINFO, &vinfo);
    if (-1 == r)
    {
        perror("ioctl failed");
        close(lp->fd);
        return ;
    }
    // 分辨率
    lp->xres = vinfo.xres;
    lp->yres = vinfo.yres;
    lp->bits_per_pixel = vinfo.bits_per_pixel / 8;
    // 把屏幕映射到内存中去
    lp->plcd = mmap(NULL, (lp->xres) * (lp->yres) * (lp->bits_per_pixel), PROT_READ | PROT_WRITE, MAP_SHARED, lp->fd, 0);
    if (lp->plcd == MAP_FAILED) 
    {
        perror("mmap error");
        close(lp->fd);
        return ;
    }
}

/*
    close_lcd:关闭屏幕(解映射/关闭文件)
    lp:要关闭的文件
*/
void close_lcd(lcd *lp) 
{
    // 解映射
    munmap(lp->plcd, (lp->xres) * (lp->yres) * (lp->bits_per_pixel));
    // 关闭屏幕
    close(lp->fd);
}

/*
    lcd_draw_point:能够在指定的屏幕的指定的位置显示一个指定的颜色
*/
void lcd_draw_point(lcd *lp, int x, int y, int colour) 
{
    if (lp == NULL) return ;

    if (x >= lp->xres || x < 0 || y >= lp->yres || y < 0) {
        printf("brother,point out of the lcd\n");
        return ;
    }
    *(lp->plcd + lp->xres * y + x) = colour;
}

/*
    lcd_clear:把屏幕刷成指定的颜色
*/
void lcd_clear(lcd *lp, int colour) 
{
    for (int y = 0; y < lp->yres; y++) 
    {
        for (int x = 0; x < lp->xres; x++)
        {
            lcd_draw_point(lp, x, y, colour);
        }
    }
}

/*
    在屏幕上指定的位置(x,y)显示一个指定大小,指定颜色的矩形
*/
void lcd_draw_rect(lcd *lp, int x, int y, int w, int h, int colour) 
{
    if (lp == NULL) return ;

    for (int i = x; i <= x + w; i++) {
        for (int j = y; j <= y + h; j++) {
            if (i >= 0 && i < lp->xres && j >= 0 && j < lp->yres)
            {
                lcd_draw_point(lp, i, j, colour);
            }
        }
    }
}

/*
    在屏幕上指定的位置(x,y)显示一个指定大小,指定颜色的圆
*/
void lcd_draw_cir(lcd *lp, int x, int y, int r, int colour) 
{
    if (lp == NULL) return ;

    for (int i = x - r; i <= x + r; i++)
    {
        for (int j = y - r; j <= y + r; j++)
        {
            if (i >= 0 && i < lp->xres && j >= 0 && j < lp->yres && 
                (i - x) * (i - x) + (j - y) * (j - y) <= r * r)
            {
                lcd_draw_point(lp, i, j, colour);
            }
        }
    }
}

5. 模块化思想:把大问题分解成小问题

1. 建立一个源代码工程

        src/                          源代码 (.c,.h,Makefile)

 

        libs/                         项目用到的第三方库 (jpg解析)

 

        inc/                          第三方库的头文件

        readme.txt              项目介绍,功能,作者,编译方法,bugs......

        changelog.txt         修改日志

2. 确定程序的大致框架

        思路流程

;