搜索
bottom↓
回复: 0

《I.MX6U嵌入式Linux C应用编程指南》第二十章 FrameBuffer应用编程

[复制链接]

出0入234汤圆

发表于 2021-8-23 16:59:37 | 显示全部楼层 |阅读模式
本帖最后由 正点原子 于 2021-8-23 19:36 编辑

1)实验平台:正点原子i.MX6ULL Linux阿尔法开发板
2)  章节摘自【正点原子】I.MX6U嵌入式Linux C应用编程指南
3)购买链接:https://item.taobao.com/item.htm?&id=603672744434
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/arm-linux/zdyz-i.mx6ull.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子Linux技术交流群:1027879335
1.png

2.jpg


3.png


第二十章 FrameBuffer应用编程

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

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

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

  2. 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类型对象中,如下所示:
  1. struct fb_var_screeninfo fb_var;

  2. ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
复制代码

FBIOPUT_VSCREENINFO:表示设置FrameBuffer设备的可变参数信息,既然是可变参数,那说明应用层可对其进行修改、重新配置,当然前提条件是底层驱动支持这些参数的动态调整,譬如在我们的Windows系统中,用户可以修改屏幕的显示分辨率,这就是一种动态调整。同样此时ioctl()需要有第三个参数,也是一个struct fb_var_screeninfo类型指针,指向struct fb_var_screeninfo类型对象,如下所示:
  1. struct fb_var_screeninfo fb_var = {0};

  2. /* 对fb_var进行数据填充 */
  3. ......
  4. ......

  5. /* 设置可变参数信息 */
  6. 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类型对象中,如下所示:
  1. struct fb_fix_screeninfo fb_fix;

  2. ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
复制代码

上面所提到的三个宏定义FBIOGET_VSCREENINFO、FBIOPUT_VSCREENINFO、FBIOGET_FSCREENINFO以及2个数据结构struct fb_var_screeninfo和struct fb_fix_screeninfo都定义在<linux/fb.h>头文件中,所以在我们的应用程序中需要包含该头文件。
  1. #define FBIOGET_VSCREENINFO        0x4600
  2. #define FBIOPUT_VSCREENINFO        0x4601
  3. #define FBIOGET_FSCREENINFO        0x4602
复制代码

struct fb_var_screeninfo结构体
struct fb_var_screeninfo结构体内容如下所示:
示例代码 20.3.1 struct fb_var_screeninfo结构体
  1. struct fb_var_screeninfo {
  2.     __u32 xres;                         /* 可视区域,一行有多少个像素点,X分辨率 */
  3.     __u32 yres;                         /* 可视区域,一列有多少个像素点,Y分辨率 */
  4.     __u32 xres_virtual;         /* 虚拟区域,一行有多少个像素点 */
  5.     __u32 yres_virtual;         /* 虚拟区域,一列有多少个像素点 */
  6.     __u32 xoffset;                 /* 虚拟到可见屏幕之间的行偏移 */
  7.     __u32 yoffset;                 /* 虚拟到可见屏幕之间的列偏移 */

  8.     __u32 bits_per_pixel;         /* 每个像素点使用多少个bit来描述,也就是像素深度bpp */
  9.     __u32 grayscale;                 /* =0表示彩色, =1表示灰度, >1表示FOURCC颜色 */

  10.     /* 用于描述R、G、B三种颜色分量分别用多少位来表示以及它们各自的偏移量 */
  11.     struct fb_bitfield red;                  /* Red颜色分量色域偏移 */
  12.     struct fb_bitfield green;           /* Green颜色分量色域偏移 */
  13.     struct fb_bitfield blue;            /* Blue颜色分量色域偏移 */
  14.     struct fb_bitfield transp;          /* 透明度分量色域偏移 */

  15.     __u32 nonstd;         /* nonstd等于0,表示标准像素格式;不等于0则表示非标准像素格式 */
  16.     __u32 activate;

  17.     __u32 height;         /* 用来描述LCD屏显示图像的高度(以毫米为单位) */
  18.     __u32 width;                 /* 用来描述LCD屏显示图像的宽度(以毫米为单位) */

  19.     __u32 accel_flags;

  20.     /* 以下这些变量表示时序参数 */
  21.     __u32 pixclock;                 /* pixel clock in ps (pico seconds) */
  22.     __u32 left_margin;         /* time from sync to picture */
  23.     __u32 right_margin;         /* time from picture to sync */
  24.     __u32 upper_margin;         /* time from sync to picture */
  25.     __u32 lower_margin;
  26.     __u32 hsync_len;                 /* length of horizontal sync */
  27.     __u32 vsync_len;                 /* length of vertical sync */
  28.     __u32 sync;                         /* see FB_SYNC_* */
  29.     __u32 vmode;                 /* see FB_VMODE_* */
  30.     __u32 rotate;                         /* angle we rotate counter clockwise */
  31.     __u32 colorspace;                 /* colorspace for FOURCC-based modes */
  32.     __u32 reserved[4];         /* Reserved for future compatibility */
  33. };
复制代码

         通过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结构体
  1. struct fb_bitfield {
  2.     __u32 offset;            /* 偏移量 */
  3.     __u32 length;           /* 长度 */
  4.     __u32 msb_right;         /* != 0 : Most significant bit is right */
  5. };
复制代码

struct fb_fix_screeninfo结构体
struct fb_fix_screeninfo结构体内容如下所示:
示例代码 20.3.3 struct fb_fix_screeninfo结构体
  1. struct fb_fix_screeninfo {
  2.     char id[16];                        /* 字符串形式的标识符 */
  3.     unsigned long smem_start;         /* 显存的起始地址(物理地址) */

  4.     __u32 smem_len;                         /* 显存的长度 */
  5.     __u32 type;
  6.     __u32 type_aux;
  7.     __u32 visual;
  8.     __u16 xpanstep;
  9.     __u16 ypanstep;
  10.     __u16 ywrapstep;
  11.     __u32 line_length;                  /* 一行的字节数 */
  12.     unsigned long mmio_start;         /* Start of Memory Mapped I/O(physical address) */
  13.     __u32 mmio_len;                         /* Length of Memory Mapped I/O */
  14.     __u32 accel;                                 /* Indicate to driver which specific chip/card we have */
  15.     __u16 capabilities;
  16.     __u16 reserved[2];
  17. };
复制代码

       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 获取屏幕的参数信息
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <sys/types.h>
  4. #include <sys/stat.h>
  5. #include <fcntl.h>
  6. #include <unistd.h>
  7. #include <sys/ioctl.h>
  8. #include <linux/fb.h>

  9. int main(int argc, char *argv[])
  10. {
  11.     struct fb_fix_screeninfo fb_fix;
  12.     struct fb_var_screeninfo fb_var;
  13.     int fd;

  14.     /* 打开framebuffer设备 */
  15.     if (0 > (fd = open("/dev/fb0", O_WRONLY))) {
  16.         perror("open error");
  17.         exit(-1);
  18.     }

  19.     /* 获取参数信息 */
  20.     ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
  21.     ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
  22.     printf("分辨率: %d*%d\n"
  23.         "像素深度bpp: %d\n"
  24.         "一行的字节数: %d\n"
  25.         "像素格式: R<%d %d> G<%d %d> B<%d %d>\n",
  26.         fb_var.xres, fb_var.yres, fb_var.bits_per_pixel,
  27.         fb_fix.line_length,
  28.         fb_var.red.offset, fb_var.red.length,
  29.         fb_var.green.offset, fb_var.green.length,
  30.         fb_var.blue.offset, fb_var.blue.length);

  31.     /* 关闭设备文件退出程序 */
  32.     close(fd);
  33.     exit(0);
  34. }
复制代码

       首先打开LCD设备文件,阿尔法开发板出厂系统中,LCD对应的设备文件为/dev/fb0;打开设备文件之后得到文件描述符fd,接着使用ioctl()函数获取LCD的可变参数信息和固定参数信息,并将这些信息打印出来。
       在测试之前,需将LCD屏通过软排线连接到阿尔法开发板(掉电情况下连接),连接好之后启动开发板,如图 17.4.1所示,为了测试方便,可以将出厂系统的GUI应用程序退出。
       使用交叉编译工具编译上述示例代码,将编译得到的可执行文件拷贝到开发板出厂系统的家目录下,并直接运行它,如下所示:
FrameBuffer应用编程7794.png

图 20.3.1 获取到屏幕参数

       笔者测试用的阿尔法开发板连接的LCD屏是正点原子的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,但阿尔法I.MX6开发板出厂烧录的Linux系统,其LCD驱动程序实现支持的是RGB565格式,用户可修改驱动程序或设备树使其支持RGB888。
1.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方式。
1.4LCD应用编程练习之刷LCD背景颜色
本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->19_lcd->lcd_background.c。
本小节编写一个简单的应用程序,将LCD的背景颜色修改为指定的颜色,示例代码如下所示:
示例代码 20.4.1 刷LCD的背景颜色
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <sys/types.h>
  4. #include <sys/stat.h>
  5. #include <fcntl.h>
  6. #include <unistd.h>
  7. #include <sys/ioctl.h>
  8. #include <linux/fb.h>
  9. #include <sys/mman.h>

  10. /**** RGB888颜色定义 ****/
  11. typedef struct rgb888_type {
  12.     unsigned char blue;
  13.     unsigned char green;
  14.     unsigned char red;
  15. } __attribute__ ((packed)) rgb888_t;

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

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

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

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

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

  49.         for (j = 0; j < size; j++)
  50.             base[j] = rgb888_color;
  51.     }
  52.         break;

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

  55.         for (j = 0; j < size; j++)
  56.             base[j] = color;
  57.     }
  58.         break;

  59.     default:
  60.         fprintf(stderr, "can't surport %dbpp\n", bpp);
  61.         break;
  62.     }
  63. }

  64. int main(int argc, char *argv[])
  65. {
  66.     struct fb_fix_screeninfo fb_fix;
  67.     struct fb_var_screeninfo fb_var;
  68.     unsigned int screen_size;
  69.     int fd;

  70.     /* 校验传参 */
  71.     if (2 != argc) {
  72.         fprintf(stderr, "usage: %s <color>\n", argv[0]);
  73.         exit(-1);
  74.     }

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

  80.     /* 获取参数信息 */
  81.     ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
  82.     ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);

  83.     screen_size = fb_fix.line_length * fb_var.yres;
  84.     width = fb_var.xres;
  85.     height = fb_var.yres;
  86.     bpp = fb_var.bits_per_pixel;

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

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

  96.     /* 退出 */
  97.     munmap(screen_base, screen_size);  //取消映射
  98.     close(fd);  //关闭文件
  99.     exit(0);    //退出进程
  100. }
复制代码

       先来看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分支。
       编译测试
使用交叉编译工具编译上述示例代码,得到可执行文件:
FrameBuffer应用编程12830.png

图 20.4.1 编译示例代码

将编译得到的可执行文件拷贝到开发板家目录下,执行程序:
FrameBuffer应用编程12919.png

图 20.4.2 刷新LCD背景颜色

上述命令中,我们将LCD背景色变成RGB 0xFF00FF颜色,RGB颜色的对照表,大家自己百度解决。执行命令之后,可以看到LCD此时显示出的效果为:
FrameBuffer应用编程13059.png

图 20.4.3 显示效果

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

  11. /**** RGB888颜色定义 ****/
  12. typedef struct rgb888_type {
  13.     unsigned char blue;
  14.     unsigned char green;
  15.     unsigned char red;
  16. } __attribute__ ((packed)) rgb888_t;

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

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

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

  39.         base[y * width + x] = rgb565_color;
  40.     }
  41.         break;

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

  49.         base[y * width + x] = rgb888_color;
  50.     }
  51.         break;

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

  54.         base[y * width + x] = color;
  55.     }
  56.         break;

  57.     default:
  58.         fprintf(stderr, "can't surport %dbpp\n", bpp);
  59.         break;
  60.     }
  61. }

  62. int main(int argc, char *argv[])
  63. {
  64.     struct fb_fix_screeninfo fb_fix;
  65.     struct fb_var_screeninfo fb_var;
  66.     unsigned int screen_size;
  67.     int fd;
  68.     int j;

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

  74.     /* 获取参数信息 */
  75.     ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
  76.     ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);

  77.     screen_size = fb_fix.line_length * fb_var.yres;
  78.     width = fb_var.xres;
  79.     height = fb_var.yres;
  80.     bpp = fb_var.bits_per_pixel;

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

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

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

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

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

  97.     /* 退出 */
  98.     munmap(screen_base, screen_size);  //取消映射
  99.     close(fd);  //关闭文件
  100.     exit(0);    //退出进程
  101. }
复制代码

代码就不再解释了,比较简单。
使用交叉编译工具编译示例代码,将可执行文件拷贝到开发板根文件系统家目录下,执行程序LCD效果如下:
FrameBuffer应用编程16431.png

示例代码 20.5.2 运行效果

1.6LCD应用编程练习之显示BMP图片
       本小节编写一个应用程序,在LCD上显示一张BMP图片,在编写程序之前,我们需要对BMP格式图片进行简单地介绍。
1.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文件头、位图信息头、调色板和位图数据,总结如下表所示:
1.png

表 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文件结构,如下图所示:
FrameBuffer应用编程17771.png

图 20.6.1 16位BMP示例图片

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

图 20.6.2 示例图片属性

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

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

一、bmp文件头
Windows下为bmp文件头定义了如下结构体:
  1. typedef struct tagBITMAPFILEHEADER
  2. {
  3.         UINT16 bfType;
  4.         DWORD bfSize;
  5.         UINT16 bfReserved1;
  6.         UINT16 bfReserved2;
  7.         DWORD bfOffBits;
  8. } BITMAPFILEHEADER;
复制代码

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

从上面的描述信息,再来对照文件数据:
FrameBuffer应用编程18874.png

图 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下为位图信息头定义了如下结构体:
  1. typedef struct tagBITMAPINFOHEADER {
  2.         DWORD biSize;
  3.         LONG biWidth;
  4.         LONG biHeight;
  5.         WORD biPlanes;
  6.         WORD biBitCount;
  7.         DWORD biCompression;
  8.         DWORD biSizeImage;
  9.         LONG biXPelsPerMeter;
  10.         LONG biYPelsPerMeter;
  11.         DWORD biClrUsed;
  12.         DWORD biClrImportant;
  13. } BITMAPINFOHEADER;
复制代码
结构体中每一个成员说明如下:
4.png

表 18.6.3 位图信息头成员说明

从上面的描述信息,再来对照文件数据:
FrameBuffer应用编程20402.png

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

图 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位表示,如下图所示:
FrameBuffer应用编程22304.png

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

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

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

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

图 20.6.9 BMP选项

点击选择16位色图,接着点击高级模式按钮:
FrameBuffer应用编程22869.png

图 20.6.10 BMP高级模式

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

  11. /**** RGB888颜色定义 ****/
  12. typedef struct rgb888_type {
  13.     unsigned char blue;
  14.     unsigned char green;
  15.     unsigned char red;
  16. } __attribute__ ((packed)) rgb888_t;

  17. /**** BMP文件头数据结构 ****/
  18. typedef struct {
  19.     unsigned char type[2];             //文件类型
  20.     unsigned int size;                //文件大小
  21.     unsigned short reserved1;          //保留字段1
  22.     unsigned short reserved2;           //保留字段2
  23.     unsigned int offset;             //到位图数据的偏移量
  24. } __attribute__ ((packed)) bmp_file_header;

  25. /**** 位图信息头数据结构 ****/
  26. typedef struct {
  27.     unsigned int size;              //位图信息头大小
  28.     int width;                      //图像宽度
  29.     int height;                     //图像高度
  30.     unsigned short planes;                  //位面数
  31.     unsigned short bpp;             //像素深度
  32.     unsigned int compression;        //压缩方式
  33.     unsigned int image_size;           //图像大小
  34.     int x_pels_per_meter;             //像素/米
  35.     int y_pels_per_meter;             //像素/米
  36.     unsigned int clr_used;
  37.     unsigned int clr_omportant;
  38. } __attribute__ ((packed)) bmp_info_header;

  39. /**** 静态全局变量 ****/
  40. static int width;                       //LCD X分辨率
  41. static int height;                      //LCD Y分辨率
  42. static void *screen_base = NULL;        //映射后的显存基地址
  43. static int bpp;                         //像素深度

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

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

  60.     /* 读取BMP文件头 */
  61.     if (sizeof(bmp_file_header) !=
  62.         read(fd, &file_h, sizeof(bmp_file_header))) {
  63.         perror("read error");
  64.         close(fd);
  65.         return;
  66.     }

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

  72.     /* 读取位图信息头 */
  73.     if (sizeof(bmp_info_header) !=
  74.         read(fd, &info_h, sizeof(bmp_info_header))) {
  75.         perror("read error");
  76.         close(fd);
  77.         return;
  78.     }

  79.     /* 打印信息 */
  80.     printf("文件大小:%d\n"
  81.          "到位图数据的偏移量:%d\n"
  82.          "位图信息头大小:%d\n"
  83.          "图像分辨率:%d*%d\n"
  84.          "像素深度:%d\n", file_h.size, file_h.offset,
  85.          info_h.size, info_h.width, info_h.height,
  86.          info_h.bpp);

  87.     /* 将文件读写位置移动到图像数据开始处 */
  88.     if (-1 == lseek(fd, file_h.offset, SEEK_SET)) {
  89.         perror("lseek error");
  90.         close(fd);
  91.         return;
  92.     }

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

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

  104.     close(fd);
  105. }

  106. int main(int argc, char *argv[])
  107. {
  108.     struct fb_fix_screeninfo fb_fix;
  109.     struct fb_var_screeninfo fb_var;
  110.     unsigned int screen_size;
  111.     int fd;

  112.     /* 传参校验 */
  113.     if (2 != argc) {
  114.         fprintf(stderr, "usage: %s <path>\n", argv[0]);
  115.         exit(-1);
  116.     }

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

  122.     /* 获取参数信息 */
  123.     ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
  124.     ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);

  125.     screen_size = fb_fix.line_length * fb_var.yres;
  126.     width = fb_var.xres;
  127.     height = fb_var.yres;
  128.     bpp = fb_var.bits_per_pixel;

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

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

  139.     /* 退出 */
  140.     munmap(screen_base, screen_size);  //取消映射
  141.     close(fd);  //关闭文件
  142.     exit(0);    //退出进程
  143. }
复制代码

       代码中有两个自定义结构体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显存对应的代码如下:
  1.         unsigned short *base = screen_base;
  2.         unsigned int line_bytes = info_h.width * info_h.bpp / 8;

  3.         for (int j = info_h.height - 1; j >= 0; j--)
  4.                 read(fd, base + j * width, line_bytes);
复制代码

       这是按照倒向位图方式进行解析的。
       关于本示例代码就介绍这么多,接下来使用交叉编译工具编译上述示例代码,如下:
FrameBuffer应用编程28217.png

图 20.6.11 编译示例代码

1.6.3在开发板上测试
       将上小节编译得到的可执行文件testApp以及测试使用的bmp图像文件拷贝到开发板根文件系统用户家目录下:
FrameBuffer应用编程28341.png

图 20.6.12 可执行文件和bmp文件

       接着执行程序,并且传入参数、指定bmp文件路径,执行之后将会在串口终端打印相应的信息,如下:
FrameBuffer应用编程28455.png

图 20.6.13 打印信息

此时LCD上将会显示bmp图像:
FrameBuffer应用编程28532.png

图 20.6.14 LCD上显示出bmp图像

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

阿莫论坛20周年了!感谢大家的支持与爱护!!

月入3000的是反美的。收入3万是亲美的。收入30万是移民美国的。收入300万是取得绿卡后回国,教唆那些3000来反美的!
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-25 15:08

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表