正点原子Linux学习笔记(五)FrameBuffer 应用编程

FrameBuffer 应用编程

  • 19.1 什么是 FrameBuffer
  • 19.2 LCD 的基础知识
  • 19.3 LCD 应用编程介绍
    • 使用 ioctl()获取屏幕参数信息
    • 使用 mmap()将显示缓冲区映射到用户空间
  • 19.4 LCD 应用编程练习之 LCD 基本操作
  • 19.5 LCD 应用编程练习之显示 BMP 图片
    • 在 LCD 上显示 BMP 图像
    • 在开发板上测试

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

19.1 什么是 FrameBuffer

Frame 是帧的意思,buffer 是缓冲的意思,所以 Framebuffer 就是帧缓冲,这意味着 Framebuffer 就是一块内存,里面保存着一帧图像。帧缓冲(framebuffer)是 Linux 系统中的一种显示驱动接口,它将显示设备(譬如 LCD)进行抽象、屏蔽了不同显示设备硬件的实现,对应用层抽象为一块显示内存(显存),它允许上层应用程序直接对显示缓冲区进行读写操作,而用户不必关心物理显存的位置等具体细节,这些都由Framebuffer 设备驱动来完成。

所以在 Linux 系统中,显示设备被称为 FrameBuffer 设备(帧缓冲设备),所以 LCD 显示屏自然而言就是 FrameBuffer 设备。FrameBuffer 设备对应的设备文件为/dev/fbX(X 为数字,0、1、2、3 等),Linux下可支持多个 FrameBuffer 设备,最多可达 32 个,分别为/dev/fb0 到/dev/fb31,开发板出厂系统中,/dev/fb0设备节点便是 LCD 屏。

应用程序读写/dev/fbX 就相当于读写显示设备的显示缓冲区(显存),譬如 LCD 的分辨率是 800480,每一个像素点的颜色用 24 位(譬如 RGB888)来表示,那么这个显示缓冲区的大小就是 800 x 480 x 24 / 8 = 1152000 个字节。譬如执行下面这条命令将 LCD 清屏,也就是将其填充为黑色(假设 LCD 对应的设备节点是/dev/fb0,分辨率为 800480,RGB888 格式):

dd if=/dev/zero of=/dev/fb0 bs=1024 count=1125

这条命令的作用就是将 1125x1024 个字节数据全部写入到 LCD 显存中,并且这些数据都是 0x0。

19.2 LCD 的基础知识

关于 LCD 相关的基础知识,本书不再介绍,开发板配套提供的驱动教程中已经有过详细的介绍,除此之外,网络上也能找到相关内容。

19.3 LCD 应用编程介绍

本小节介绍如何对 FrameBuffer 设备(譬如 LCD)进行应用编程,通过上面的介绍,相信大家应该已经知道如何操作 LCD 显示设备了,应用程序通过对 LCD 设备节点/dev/fb0(假设 LCD 对应的设备节点是/dev/fb0)进行 I/O 操作即可实现对 LCD 的显示控制,实质就相当于读写了 LCD 的显存,而显存是 LCD 的显示缓冲区,LCD 硬件会从显存中读取数据显示到 LCD 液晶面板上。

在应用程序中,操作/dev/fbX 的一般步骤如下:
①、首先打开/dev/fbX 设备文件。
②、使用 ioctl()函数获取到当前显示设备的参数信息,譬如屏幕的分辨率大小、像素格式,根据屏幕参数计算显示缓冲区的大小。
③、通过存储映射 I/O 方式将屏幕的显示缓冲区映射到用户空间(mmap)。
④、映射成功后就可以直接读写屏幕的显示缓冲区,进行绘图或图片显示等操作了。
⑤、完成显示后,调用 munmap()取消映射、并调用 close()关闭设备文件。
从上面介绍的操作步骤来看,LCD 的应用编程还是非常简单的,这些知识点都是在前面的入门篇中给大家介绍过。

使用 ioctl()获取屏幕参数信息

当打开 LCD 设备文件之后,需要先获取到 LCD 屏幕的参数信息,譬如 LCD 的 X 轴分辨率、Y 轴分辨率以及像素格式等信息,通过这些参数计算出 LCD 显示缓冲区的大小。
通 过 ioctl() 函 数 来 获 取 屏 幕 参 数 信息, 对 于 Framebuffer 设备来说, 常 用 的 request 包 括FBIOGET_VSCREENINFO、FBIOPUT_VSCREENINFO、FBIOGET_FSCREENINFO。
FBIOGET_VSCREENINFO:表示获取 FrameBuffer 设备的可变参数信息,可变参数信息使用 struct fb_var_screeninfo 结 构 体 来 描 述 , 所 以 此 时 ioctl() 需 要 有 第 三 个 参 数 , 它 是 一 个 struct fb_var_screeninfo *指针,指向 struct fb_var_screeninfo 类型对象,调用 ioctl()会将 LCD 屏的可变参数信息保存在 struct fb_var_screeninfo 类型对象中,如下所示:

struct fb_var_screeninfo fb_var;
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);

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

struct fb_var_screeninfo fb_var = {0};
/* 对 fb_var 进行数据填充 */
......
......
/* 设置可变参数信息 */
ioctl(fd, FBIOPUT_VSCREENINFO, &fb_var);

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

#define FBIOGET_VSCREENINFO 0x4600
#define FBIOPUT_VSCREENINFO 0x4601
#define FBIOGET_FSCREENINFO 0x4602

struct fb_var_screeninfo 结构体
struct fb_var_screeninfo 结构体内容如下所示:

示例代码 19.3.1 struct fb_var_screeninfo 结构体
struct fb_var_screeninfo {
 __u32 xres; /* 可视区域,一行有多少个像素点,X 分辨率 */
 __u32 yres; /* 可视区域,一列有多少个像素点,Y 分辨率 */
 __u32 xres_virtual; /* 虚拟区域,一行有多少个像素点 */
 __u32 yres_virtual; /* 虚拟区域,一列有多少个像素点 */
 __u32 xoffset; /* 虚拟到可见屏幕之间的行偏移 */
 __u32 yoffset; /* 虚拟到可见屏幕之间的列偏移 */
 __u32 bits_per_pixel; /* 每个像素点使用多少个 bit 来描述,也就是像素深度 bpp */
 __u32 grayscale; /* =0 表示彩色, =1 表示灰度, >1 表示 FOURCC 颜色 */
 /* 用于描述 R、G、B 三种颜色分量分别用多少位来表示以及它们各自的偏移量 */
 struct fb_bitfield red; /* Red 颜色分量色域偏移 */
 struct fb_bitfield green; /* Green 颜色分量色域偏移 */
 struct fb_bitfield blue; /* Blue 颜色分量色域偏移 */
 struct fb_bitfield transp; /* 透明度分量色域偏移 */
 __u32 nonstd; /* nonstd 等于 0,表示标准像素格式;不等于 0 则表示非标准像素格式 */
 __u32 activate;
 __u32 height; /* 用来描述 LCD 屏显示图像的高度(以毫米为单位) */
 __u32 width; /* 用来描述 LCD 屏显示图像的宽度(以毫米为单位) */
 __u32 accel_flags;
 /* 以下这些变量表示时序参数 */
 __u32 pixclock; /* pixel clock in ps (pico seconds) */
 __u32 left_margin; /* time from sync to picture */
 __u32 right_margin; /* time from picture to sync */
 __u32 upper_margin; /* time from sync to picture */
 __u32 lower_margin;
 __u32 hsync_len; /* length of horizontal sync */
 __u32 vsync_len; /* length of vertical sync */
 __u32 sync; /* see FB_SYNC_* */
 __u32 vmode; /* see FB_VMODE_* */
 __u32 rotate; /* angle we rotate counter clockwise */
 __u32 colorspace; /* colorspace for FOURCC-based modes */
 __u32 reserved[4]; /* Reserved for future compatibility */
};

通过 xres、yres 获取到屏幕的水平分辨率和垂直分辨率,bits_per_pixel 表示像素深度 bpp,即每一个像素点使用多少个 bit 位来描述它的颜色,通过 xres * yres * bits_per_pixel / 8 计算可得到整个显示缓存区的大小。
red、green、blue 描述了 RGB 颜色值中 R、G、B 三种颜色通道分别使用多少 bit 来表示以及它们各自的偏移量,通过 red、green、blue 变量可知道 LCD 的 RGB 像素格式,譬如是 RGB888 还是 RGB565,亦或者是 BGR888、BGR565 等。struct fb_bitfield 结构体如下所示:

示例代码 19.3.2 struct fb_bitfield 结构体
struct fb_bitfield {
 __u32 offset; /* 偏移量 */
 __u32 length; /* 长度 */
 __u32 msb_right; /* != 0 : Most significant bit is right */
 };

struct fb_fix_screeninfo 结构体
struct fb_fix_screeninfo 结构体内容如下所示:

示例代码 19.3.3 struct fb_fix_screeninfo 结构体
struct fb_fix_screeninfo {
 char id[16]; /* 字符串形式的标识符 */
 unsigned long smem_start; /* 显存的起始地址(物理地址) */
 __u32 smem_len; /* 显存的长度 */
 __u32 type;
 __u32 type_aux;
 __u32 visual;
 __u16 xpanstep;
 __u16 ypanstep;
 __u16 ywrapstep;
 __u32 line_length; /* 一行的字节数 */
 unsigned long mmio_start; /* Start of Memory Mapped I/O(physical address) */
 __u32 mmio_len; /* Length of Memory Mapped I/O */
 __u32 accel; /* Indicate to driver which specific chip/card we have */
 __u16 capabilities;
 __u16 reserved[2];
};

smem_start 表示显存的起始地址,这是一个物理地址,当然在应用层无法直接使用;smem_len 表示显存的长度,这个长度并一定等于 LCD 实际的显存大小。line_length 表示屏幕的一行像素点有多少个字节,通常可以使用 line_length * yres 来得到屏幕显示缓冲区的大小。
通过上面介绍,接下来我们编写一个示例代码,获取 LCD 屏幕的参数信息,示例代码如下所示:

示例代码 19.3.4 获取屏幕的参数信息
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
int main(int argc, char *argv[])
{
 struct fb_fix_screeninfo fb_fix;
 struct fb_var_screeninfo fb_var;
 int fd;
/* 打开 framebuffer 设备 */
 if (0 > (fd = open("/dev/fb0", O_WRONLY))) {
 perror("open error");
 exit(-1);
 }
 /* 获取参数信息 */
 ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
 ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
 printf("分辨率: %d*%d\n"
 "像素深度 bpp: %d\n"
 "一行的字节数: %d\n"
 "像素格式: R<%d %d> G<%d %d> B<%d %d>\n",
 fb_var.xres, fb_var.yres, fb_var.bits_per_pixel,
 fb_fix.line_length,
 fb_var.red.offset, fb_var.red.length,
 fb_var.green.offset, fb_var.green.length,
 fb_var.blue.offset, fb_var.blue.length);
 /* 关闭设备文件退出程序 */
 close(fd);
 exit(0);
} 

首先打开 LCD 设备文件,开发板出厂系统,LCD 对应的设备文件为/dev/fb0;打开设备文件之后得到文件描述符 fd,接着使用 ioctl()函数获取 LCD 的可变参数信息和固定参数信息,并将这些信息打印出来。
在测试之前,需将 LCD 屏通过软排线连接到开发板(掉电情况下连接),连接好之后启动开发板。
使用交叉编译工具编译上述示例代码,将编译得到的可执行文件拷贝到开发板 Linux 系统的用户家目录下,并直接运行它,如下所示:
在这里插入图片描述
笔者使用的是 7 寸 800480 RGB 屏,与上图打印显示的分辨率 800480 是相符的;像素深度为 16,也就意味着一个像素点的颜色值将使用 16bit(也就是 2 个字节)来表示;一行的字节数为 1600,一行共有 800个像素点,每个像素点使用 16bit 来描述,一共就是 800*16/8=1600 个字节数据,这也是没问题的。

打印出像素格式为 R<11 5> G<5 6> B<0 5>,分别表示 R、G、B 三种颜色分量对应的偏移量和长度,第一个数字表示偏移量,第二个参数为长度,从打印的结果可知,16bit 颜色值中高 5 位表示 R 颜色通道、中间 6 位表示 G 颜色通道、低 5 位表示 B 颜色通道,所以这是一个 RGB565 格式的显示设备。

Tips:正点原子的 RGB LCD 屏幕,包括 4.3 寸 800480、4.3 寸 480272、7 寸 800480、7 寸 1024600 以 及 10.1 寸 1280*800 硬件上均支持 RGB888,但 ALPHA/Mini I.MX6U 开发板出厂系统中,LCD 驱动程序将其实现为一个 RGB565 格式的显示设备,用户可修改设备树使其支持 RGB888,或者通过 ioctl 修改。

前面我们提到可以通过 ioctl()去设置 LCD 的可变参数,使用 FBIOPUT_VSCREENINFO 宏,但不太建议大家去改这些参数,如果 FrameBuffer 驱动程序支持不够完善,改完之后可能会出现一些问题!这里就不再演示了。

使用 mmap()将显示缓冲区映射到用户空间

在入门篇 13.5 小节中给大家介绍了存储映射 I/O 这种高级 I/O 方式,它的一个非常经典的使用场景便是用在 Framebuffer 应用编程中。通过 mmap()将显示器的显示缓冲区(显存)映射到进程的地址空间中,这样应用程序便可直接对显示缓冲区进行读写操作。

为什么这里需要使用存储映射 I/O 这种方式呢?其实使用普通的 I/O 方式(譬如直接 read、write)也是可以的,只是,当数据量比较大时,普通 I/O 方式效率较低。假设某一显示器的分辨率为 1920 * 1080,像素格式为 ARGB8888,针对该显示器,刷一帧图像的数据量为 1920 x 1080 x 32 / 8 = 8294400 个字节(约等于 8MB),这还只是一帧的图像数据,而对于显示器来说,显示的图像往往是动态改变的,意味着图像数据会被不断更新。

在这种情况下,数据量是比较庞大的,使用普通 I/O 方式必然导致效率低下,所以才会采用存储映射I/O 方式。

19.4 LCD 应用编程练习之 LCD 基本操作

本小节编写应用程序,在 LCD 上实现画点(俗称打点)、画线、画矩形等基本 LCD 操作,示例代码如下所示:

示例代码 19.4.1 LCD 画点、画线、画矩形操作
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>
#define argb8888_to_rgb565(color) ({ \
 unsigned int temp = (color); \
 ((temp & 0xF80000UL) >> 8) | \
 ((temp & 0xFC00UL) >> 5) | \
 ((temp & 0xF8UL) >> 3); \
 })
static int width; //LCD X 分辨率
static int height; //LCD Y 分辨率
static unsigned short *screen_base = NULL; //映射后的显存基地址
/********************************************************************
* 函数名称: lcd_draw_point
* 功能描述: 打点
* 输入参数: x, y, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_point(unsigned int x, unsigned int y, unsigned int color) {
 unsigned short rgb565_color = argb8888_to_rgb565(color);//得到 RGB565 颜色值
 /* 对传入参数的校验 */
 if (x >= width)
 x = width - 1;
 if (y >= height)
 y = height - 1;
 /* 填充颜色 */
 screen_base[y * width + x] = rgb565_color; }
/********************************************************************
* 函数名称: lcd_draw_line
* 功能描述: 画线(水平或垂直线)
* 输入参数: x, y, dir, length, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_line(unsigned int x, unsigned int y, int dir,
 unsigned int length, unsigned int color) {
 unsigned short rgb565_color = argb8888_to_rgb565(color);//得到 RGB565 颜色值
 unsigned int end;
 unsigned long temp;
 /* 对传入参数的校验 */
 if (x >= width)
 x = width - 1;
 if (y >= height)
 y = height - 1;
 /* 填充颜色 */
 temp = y * width + x;//定位到起点
 if (dir) { //水平线
 end = x + length - 1;
 if (end >= width)
 end = width - 1;
 for ( ; x <= end; x++, temp++)
 screen_base[temp] = rgb565_color;
 }
 else { //垂直线
 end = y + length - 1;
 if (end >= height)
 end = height - 1;
 for ( ; y <= end; y++, temp += width)
 screen_base[temp] = rgb565_color;
 } }
/********************************************************************
* 函数名称: lcd_draw_rectangle
* 功能描述: 画矩形
* 输入参数: start_x, end_x, start_y, end_y, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_rectangle(unsigned int start_x, unsigned int end_x,
 unsigned int start_y, unsigned int end_y,
 unsigned int color) {
 int x_len = end_x - start_x + 1;
 int y_len = end_y - start_y - 1;
 lcd_draw_line(start_x, start_y, 1, x_len, color);//上边
 lcd_draw_line(start_x, end_y, 1, x_len, color); //下边
 lcd_draw_line(start_x, start_y + 1, 0, y_len, color);//左边
 lcd_draw_line(end_x, start_y + 1, 0, y_len, color);//右边
}
/********************************************************************
* 函数名称: lcd_fill
* 功能描述: 将一个矩形区域填充为参数 color 所指定的颜色
* 输入参数: start_x, end_x, start_y, end_y, color
* 返 回 值: 无
********************************************************************/
static void lcd_fill(unsigned int start_x, unsigned int end_x,
 unsigned int start_y, unsigned int end_y,
 unsigned int color) {
 unsigned short rgb565_color = argb8888_to_rgb565(color);//得到 RGB565 颜色值
 unsigned long temp;
 unsigned int x;
 /* 对传入参数的校验 */
 if (end_x >= width)
 end_x = width - 1;
 if (end_y >= height)
 end_y = height - 1;
 /* 填充颜色 */
 temp = start_y * width; //定位到起点行首
 for ( ; start_y <= end_y; start_y++, temp+=width) {
 for (x = start_x; x <= end_x; x++)
 screen_base[temp + x] = rgb565_color;
 } }
int main(int argc, char *argv[])
{
 struct fb_fix_screeninfo fb_fix;
 struct fb_var_screeninfo fb_var;
 unsigned int screen_size;
 int fd;
 /* 打开 framebuffer 设备 */
 if (0 > (fd = open("/dev/fb0", O_RDWR))) {
 perror("open error");
 exit(EXIT_FAILURE);
 }
 /* 获取参数信息 */
 ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
 ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
 screen_size = fb_fix.line_length * fb_var.yres;
 width = fb_var.xres;
 height = fb_var.yres;
 /* 将显示缓冲区映射到进程地址空间 */
 screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
 if (MAP_FAILED == (void *)screen_base) {
 perror("mmap error");
 close(fd);
 exit(EXIT_FAILURE);
 }
 /* 画正方形方块 */
 int w = height * 0.25;//方块的宽度为 1/4 屏幕高度
 lcd_fill(0, width-1, 0, height-1, 0x0); //清屏(屏幕显示黑色)
 lcd_fill(0, w, 0, w, 0xFF0000); //红色方块
 lcd_fill(width-w, width-1, 0, w, 0xFF00); //绿色方块
 lcd_fill(0, w, height-w, height-1, 0xFF); //蓝色方块
 lcd_fill(width-w, width-1, height-w, height-1, 0xFFFF00);//黄色方块
 /* 画线: 十字交叉线 */
 lcd_draw_line(0, height * 0.5, 1, width, 0xFFFFFF);//白色线
 lcd_draw_line(width * 0.5, 0, 0, height, 0xFFFFFF);//白色线
 /* 画矩形 */
 unsigned int s_x, s_y, e_x, e_y;
 s_x = 0.25 * width;
 s_y = w;
 e_x = width - s_x;
 e_y = height - s_y;
 for ( ; (s_x <= e_x) && (s_y <= e_y);
 s_x+=5, s_y+=5, e_x-=5, e_y-=5)
 lcd_draw_rectangle(s_x, e_x, s_y, e_y, 0xFFFFFF);
 /* 退出 */
 munmap(screen_base, screen_size); //取消映射
 close(fd); //关闭文件
 exit(EXIT_SUCCESS); //退出进程
}

在示例代码中定义了一个宏 argb8888_to_rgb565,用于实现将 unsigned int 类型的颜色(也就是ARGB8888 颜色)转换为 RGB565 颜色
程序中自定义了 4 个函数:
lcd_draw_point:用于实现画点、打点操作,参数 x 和 y 指定像素点的位置,参数 color 表示颜色。
lcd_draw_line:用于实现画线操作,参数 x 和 y 指定线的起始位置;参数 dir 表示方向,水平方向(dir!=0)还是垂直方向(dir=0),不支持斜线画法,画斜线需要一些算法去操作,这不是本章内容需要去关注的知识点;参数 length 表示线的长度,以像素为单位;参数 color 表示线条的颜色。
lcd_draw_rectangle:用于实现画矩形操作,参数 start_x 和 start_y 指定矩形左上角的位置;参数 end_x和 end_y 指定矩形右下角的位置;参数 color 指定矩形 4 个边的线条颜色。
lcd_fill:将一个指定的矩形区域填充为参数 color 指定的颜色,参数 start_x 和 start_y 指定矩形左上角的位置;参数 end_x 和 end_y 指定矩形右下角的位置;参数 color 指定矩形区域填充的颜色。
具体代码的实现各位读者自己去看,非常简单,来看下 main()中做了哪些事情:
⚫ 首先调用 open()打开 LCD 设备文件得到文件描述符 fd; ⚫ 接着使用 ioctl 函数获取 LCD 的可变参数信息和固定参数信息,通过得到的信息计算 LCD 显存大小、得到 LCD 屏幕的分辨率,从图 19.3.1 可知,ALPHA/Mini I.MX6U 开发板出厂系统将 LCD 实现为一个 RGB565 显示设备,所以程序中自定义的 4 个函数在操作 LCD 像素点时、都是以 RGB565的格式写入颜色值。
⚫ 接着使用 mmap 建立映射;
⚫ 映射成功之后就可以在应用层直接操作 LCD 显存了,调用自定义的函数在 LCD 上画线、画矩形、画方块;
⚫ 操作完成之后,调用 munmap 取消映射,调用 close 关闭 LCD 设备文件,退出程序。编译应用程序:
在这里插入图片描述
将编译得到的可执行文件拷贝到开发板 Linux 系统的用户家目录下,执行应用程序(在测试之前,先将出厂系统对应的 Qt GUI 应用程序退出):
在这里插入图片描述
此时 LCD 屏上将会显示程序中绘制的方块、矩形、以及线条:
在这里插入图片描述
忽略手机拍摄的问题,实际效果各位读者运行程序便知。

19.5 LCD 应用编程练习之显示 BMP 图片

我们常用的图片格式有很多,一般最常用的有三种:JPEG(或 JPG)、PNG、BMP 和 GIF。其中 JPEG(或 JPG)、PNG 以及 BMP 都是静态图片,而 GIF 则可以实现动态图片。在本小节实验中,我们选择使用BMP 图片格式。

BMP(全称 Bitmap)是 Window 操作系统中的标准图像文件格式,文件后缀名为“.bmp”,使用非常广。它采用位映射存储格式,除了图像深度可选以外,图像数据没有进行任何压缩,因此,BMP 图像文件所占用的空间很大,但是没有失真、并且解析 BMP 图像简单。

BMP 文件的图像深度可选 lbit、4bit、8bit、16bit、24bit 以及 32bit,典型的 BMP 图像文件由四部分组
成:

①、BMP 文件头(BMP file header),它包含 BMP 文件的格式、大小、位图数据的偏移量等信息;
②、位图信息头(bitmap information),它包含位图信息头大小、图像的尺寸、图像大小、位平面数、压缩方式以及颜色索引等信息;
③、调色板(color palette),这部分是可选的,如果使用索引来表示图像,调色板就是索引与其对应颜色的映射表;
④、位图数据(bitmap data),也就是图像数据。
BMP 文件头、位图信息头、调色板和位图数据,总结如下表所示:
在这里插入图片描述
一般常见的图像都是以 16 位(R、G、B 三种颜色分别使用 5bit、6bit、5bit 来表示)、24 位(R、G、 B 三种颜色都使用 8bit 来表示)色图像为主,我们称这样的图像为真彩色图像,真彩色图像是不需要调色板的,即位图信息头后面紧跟的就是位图数据了。

对某些 BMP 位图文件说并非如此,譬如 16 色位图、256 色位图,它们需要使用到调色板,具体调色板如何使用,我们不关心,本节我们将会以 16 位色(RGB565)BMP 图像为例。

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

typedef struct tagBITMAPFILEHEADER
{
UINT16 bfType;
DWORD bfSize;
UINT16 bfReserved1;
UINT16 bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER;

在这里插入图片描述
在这里插入图片描述
从上面的描述信息,再来对照文件数据:
在这里插入图片描述
00~01H:0x42、0x4D 对应的 ASCII 字符分别为为 B、M,表示这是 Windows 所支持的位图格式,该字段必须是“BM”才是 Windows 位图文件。
02~05H:对应于文件大小,0x000BB848=768072 字节,与 image.bmp 文件大小是相符的。
06~09H:保留字段。
0A~0D:0x00000046=70,即从文件头部开始到位图数据需要偏移 70 个字节。
bmp 文件头的大小固定为 14 个字节。

二、位图信息头
同样,Windows 下为位图信息头定义了如下结构体:

typedef struct tagBITMAPINFOHEADER {
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount;
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER;

结构体中每一个成员说明如下:
在这里插入图片描述
在这里插入图片描述
从上面的描述信息,再来对照文件数据:
在这里插入图片描述
0E~11H:0x00000038=56,这说明这个位图信息头的大小为 56 个字节。
12~15H:0x00000320=800,图像宽度为 800 个像素,与文件属性一致。
16~19H:0x000001E0=480,图像高度为 480 个像素,与文件属性一致;这个数是一个正数,说明是一个倒向的位图,什么是正向的位图、什么是倒向的位图,说的是图像数据的排列问题;如果是正向的位图,
图像数据是按照图像的左上角到右下角方式排列的,水平方向从左到右,垂直方向从上到下。倒向的位图,图像数据则是按照图像的左下角到右上角方式排列的,水平方向依然从左到右,垂直方向改为从下到上。

1A~1BH:0x0001=1,这个值总为 1。
1C~1DH:0x0010=16,表示每个像素占 16 个 bit。
1E~21H:0x00000003=3,bit-fileds 方式。
22~25H:0x000BB802=768002,图像的大小,注意图像的大小并不是 BMP 文件的大小,而是图像数据的大小。

26~29H:0x00000EC2=3778,水平分辨率为 3778 像素/米。
2A~2DH:0x00000EC2=3778,垂直分辨率为 3778 像素/米。
2E~31H:0x00000000=0,本位图未使用调色板。
32~35H:0x00000000=0。

只有压缩方式选项被设置为 bit-fileds(0x3)时,位图信息头的大小才会等于 56 字节,否则,为 40 字节。56 个字节相比于 40 个字节,多出了 16 个字节,那么多出的 16 个字节数据描述了什么信息呢?稍后再给大家介绍。

三、调色板
调色板是单色、16 色、256 色位图图像文件所持有的,如果是 16 位、24 位以及 32 位位图文件,则 BMP文件组成部分中不包含调色板,关于调色板这里不过多介绍,有兴趣可以自己去了解。

四、位图数据
位图数据其实就是图像的数据,对于 24 位位图,使用 3 个字节数据来表示一个像素点的颜色,对于 16位位图,使用 2 个字节数据来表示一个像素点的颜色,同理,32 位位图则使用 4 个字节来描述。

BMP 位图分为正向的位图和倒向的位图,主要区别在于图像数据存储的排列方式,前面已经给大家解释的比较清楚了,如下如所示(左边对应的是正向位图,右边对应的则是倒向位图):
在这里插入图片描述
所以正向位图先存储图像的第一行数据,从左到右依次存放,接着存放第二行,依次这样;而倒向位图,则先存储图像的最后一行(倒数第一行)数据,也是从左到右依次存放,接着倒数二行,依次这样。

RGB 和 Bit-Fields
当图像中引用的色彩超过 256 种时,就需要 16bpp 或更高 bpp 的位图(24 位、32 位)。调色板不适合bpp 较大的位图,因此 16bpp 及以上的位图都不使用调色板,不使用调色板的位图图像有两种编码格式:RGB 和 Bit-Fields(下称 BF)。

RGB 编码格式是一种均分的思想,使 Red、Green、Blue 三种颜色信息容量一样大,譬如 24bpp-RGB,它通常只有这一种编码格式,在 24bits 中,低 8 位表示 Blue 分量;中 8 为表示 Green 分量;高 8 位表示 Red分量。

而在 32bpp-RGB 中,低 24 位的编码方式与 24bpp 位图相同,最高 8 位用来表示透明度 Alpha 分量。32bpp 的位图尺寸太大,一般只有在图像处理的中间过程中使用。对于需要半透过效果的图像,更好的选择是 PNG 格式。

BF 编码格式与 RGB 不同,它利用位域操作,人为地确定 RGB 三分量所包含的信息容量。位图信息头介绍中提及到,当压缩方式选项置为 BF 时,位图信息头大小比平时多出 16 字节,这 16 个字节实际上是 4 个 32bit 的位域掩码,按照先后顺序,它们分别是 R、G、B、A 四个分量的位域掩码,当然如果没有 Alpha分量,则 Alpha 掩码没有实际意义。

位域掩码的作用是指出 R、G、B 三种颜色信息容量的大小,分别使用多少个 bit 数据来表示,以及三种颜色分量的位置偏移量。譬如对于 16 位色的 RGB565 图像,通常使用 BF 编码格式,同样这也是 BF 编码格式最著名和最普遍的应用之一,它的 R、G 和 B 分量的位域掩码分别是 0xF800、0x07E0 和 0x001F,也就是 R 通道使用 2 个字节中的高 5 位表示,G 通道使用 2 个字节中的中间 6 位表示。而 B 通道则使用 2个字节中的最低 5 位表示,如下图所示:
在这里插入图片描述
关于 BMP 图像文件的格式就给大家介绍这么多,后面的程序代码中将不会再做解释!
如何得到 16 位色 RGB565 格式 BMP 图像?
在 Windows 下我们转换得到的 BMP 位图通常是 24 位色的 RGB888 格式图像,那如何得到 RGB565 格 式 BMP 位图呢?当然这个方法很多,这里笔者向大家介绍一种方法就是通过 Photoshop 软件来得到 RGB565格式的 BMP 位图。

首先,找一张图片,图片格式无所谓,只要Photoshop软件能打开即可;确定图片之后,我们启动Photoshop软件,并且使用 Photoshop 软件打开这张图片,打开之后点击菜单栏中的文件—>存储为,接着出现如下界面:
在这里插入图片描述
在这个界面中,首先选择文件保存的路径,然后设置文件名以及文件格式,选择文件格式为 BMP 格式,之后点击保存,如下:
在这里插入图片描述
点击选择 16 位色图,接着点击高级模式按钮:
在这里插入图片描述
点击选择 RGB565,接着点击确定按钮即可,这样就可得到 16 位色 RGB565 格式的 BMP 图像。

在 LCD 上显示 BMP 图像

通过上小节对 BMP 图像的介绍之后,相信大家对 BMP 文件的格式已经非常了解了,那么本小节我们将编写一个示例代码,在 LCD 上显示一张指定的 BMP 图像,示例代码笔者已经完成了,如下所示。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <linux/fb.h>
#include <sys/mman.h>
/**** BMP 文件头数据结构 ****/
typedef struct {
unsigned char type[2]; //文件类型
 unsigned int size; //文件大小
 unsigned short reserved1; //保留字段 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 位图是正向的位图还是倒向的位图,它们的处理方式不一样,这些代码自己去看,笔者不好去解释,毕竟这只是文字描述的形式,不太好表述!代码只是一种参考,自己能够独立写出来才是硬道理!
关于本示例代码就介绍这么多,接下来使用交叉编译工具编译上述示例代码,如下:
在这里插入图片描述

在开发板上测试

将上小节编译得到的可执行文件 testApp 以及测试使用的 bmp 图像文件拷贝到开发板 Linux 系统的用户家目录下:
在这里插入图片描述
接着执行测试程序(在测试之前,先将出厂系统对应的 Qt GUI 应用程序退出):
在这里插入图片描述
此时 LCD 屏上会显示 image.bmp 图像。如下所示:
在这里插入图片描述
忽略手机拍摄的问题,由于周围物体以及光线导致上图显示的结果与实际 LCD 显示的图像存在差异,image.bmp 原图如下所示:
在这里插入图片描述
本章内容到此结束!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/604077.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

超强动画制作软件blender

blender中文手册&#xff1a;Blender 4.1 Manual Blender 是一款集3D建模、渲染、动画、视频编辑、音频处理、游戏设计等多功能于一体的软件。由于其开源性质&#xff0c;它拥有庞大的用户群体和活跃的开发者社区&#xff0c;这使得Blender的功能和性能得到了不断的提升和优化…

Windows内核开发:如何使用STL

前言 大家都知道应用层c的STL非常强大&#xff0c;非常好用&#xff0c;但是在内核下就没法用了。针对这个问题&#xff0c;经过我不懈的寻找&#xff0c;终于找到了解决内核无法使用STL的方法。 使用new/delete关键字 先说一下常用关键字如何在内核中使用。其实只需要在一个全…

第四十节实现主人公的技能释放功能(二)实现技能按钮

看看我们今天要实现的效果是&#xff0c;当我们按下数字1快捷键&#xff0c;我们的技能按钮会进入倒计时&#xff0c;如下图演示&#xff1a; 一、新建场景和根节点设置 新建场景&#xff0c;选择TextureButton作为根节点&#xff0c;重名为SpellButton&#xff0c;保存场景…

啸叫抑制器采用什么处理芯片?ES56031或PH56031

会议系统或卡拉OK最头疼的就是啸叫了吧&#xff0c;来看看啸叫抑制器采用什么芯片 四通道啸叫抑制器&#xff0c;采用了2个电路板&#xff0c;每个板子处理2路信号&#xff0c;每块电路板有2个卡侬输入插座&#xff0c;2个卡侬输出插座 ES56031S&#xff0c;该啸叫抑制器为4通道…

【优选算法】——双指针——Leetcode——283.移动零

目录 ​编辑 1.题目 2. 解法&#xff08;快排的思想&#xff1a;数组划分区间-数组分两块&#xff09;&#xff1a; 1.算法思路&#xff1a; 2.算法流程&#xff1a; 3.代码实现 1.C语言 2.C 1.题目 283. 移动零 提示 给定一个数组 nums&#xff0c;编写一个函数将所有…

MySQL增删查改(进阶)

目录 数据库约束 表的设计 查询操作的进阶 查询搭配插入使用 聚合查询 1>count(*) 2>sum(*) 3>avg(*) 4>max(*) 5>min(*) group by分组分别进行聚合查询 联合查询 / 多表查询[重点] 外连接 自连接 子查询 合并查询 小结: 数据库约束 有时候…

cesium雷达扫描(消逝圆效果)

cesium雷达扫描(消逝圆效果) 以下为源码直接复制可用 1、实现思路 通过修改“material”材质来实现轨迹球效果 2、示例代码 1、index.html <!DOCTYPE html> <html lang="en"><head><!

小猪APP分发:重塑应用分发市场的创新力量

在移动互联网蓬勃发展的今天&#xff0c;应用分发平台作为连接开发者与用户的桥梁&#xff0c;扮演着至关重要的角色。然而&#xff0c;随着市场的饱和&#xff0c;如何在众多平台中脱颖而出&#xff0c;为开发者提供更宽广的舞台&#xff0c;同时确保用户能够便捷、安全地获取…

【linux】dmesg工具

dmesg介绍 dmesg工具用途&#xff1a; dmesg - print or control the kernel ring buffer kernel ring buffer, 内核环形缓冲区&#xff0c;也叫环形队列&#xff0c;Linux内核日志就存储在一个环形队列中&#xff0c;环形队列满的时候&#xff0c;新的消息会覆盖掉旧的消息。…

小程序支付的款项流转与到账时间

商家做小程序&#xff0c;最关心的是客户通过小程序下单支付的钱&#xff0c;是怎么样的流转状态以及最终到哪里。因此&#xff0c;本文将详细解析款项最终流向何处以及多久能够到账。 一、小程序支付的款项流向 当用户在小程序内完成支付后&#xff0c;款项并不会直接到达商…

CSRF漏洞简介

csrf简介 CSRF 全称为跨站请求伪造&#xff08; Cross-site request forgery &#xff09;&#xff0c;是一种网络攻击方式&#xff0c;在 CSRF 的攻击场景中攻击者会伪造一个请求&#xff08;这个请求一般是一个链接&#xff09;&#xff0c;然后欺骗目标用户进行点击&#xf…

C51版本Keil + STC-ISP 实现第一盏灯,从创建到实现

创建项目 1. 新建项目 Project -> New uVision Project 2.1 新建文件夹 2.2 输入文件名称, 并保存 3.1 选择当前位STC芯片的开发板&#xff0c;选择STC MCU Database 搜素具体芯片型号&#xff0c;进行配置&#xff1a; 3.2 选择通过搜索框搜索到stc相关芯片信息 如果st…

linux数据备份与恢复

目录 前言 1、数据备份和恢复中的两个关键性指标 2、linux系统的定时任务 1&#xff09;本地定时任务crontab 在实验测试过程中&#xff0c;遇到多次crontab任务不执行问题 &#xff0c;总结下来主要有几个方面原因&#xff1a; 2)分布式定时任务系统Jenkins 3、备份存储…

机房——蓝桥杯十三届2022国赛大学B组真题

问题分析 这题用深搜广搜都能做&#xff0c;不过我更倾向于用广搜&#xff0c;因为广搜能更容易找到目标点。那么是采用结构体存储边还是采用二维数组存储临接矩阵呢&#xff1f;我们注意到n的取值范围为1e5,用二维数组哪怕是bool类型就需要至少1e10Byte的连续空间,这个空间太大…

为软件教学文档增加实践能力

为了更方便软件教学&#xff0c;我们在凌鲨(OpenLinkSaas)上增加了公共资源引用的功能。 目前可以被引用的公共资源: 微应用常用软件公共知识库Docker模板 引用公共资源 引用微应用 目前微应用包含了主流数据库&#xff0c;终端等工具&#xff0c;可以方便的进行各种相关实…

【25届秋招备战C++】23种设计模式

【25届秋招备战C】23种设计模式 一、简介程序员的两种思维8大设计原则 二、具体23种设计模式2.1 创建型模式2.2 结构性模式2.3 行为型模式 三、常考模式的实现四、参考 一、简介 从面向对象谈起&#xff0c; 程序员的两种思维 底层思维:向下 封装&#xff1a;隐藏内部实现 多…

ASP.NET小型证券术语解释及翻译系统的设计与开发

摘 要 在系统设计上&#xff0c;综合各种翻译类型网站优缺点&#xff0c;设计出具有任何使用者都可添加术语信息的且只有管理员能够实现术语修改及删除等独特方式的术语查看管理系统。此方式能够使术语量快速增大&#xff0c;并且便于使用者及管理员操作&#xff0c;满足相互…

软件设计师-应用技术-面向对象程序设计题5

考题形式&#xff1a; 代码填空&#xff0c;5 - 6空&#xff0c;每空3分。 基础知识及技巧&#xff1a; 1. 类的定义&#xff1a; 2. 接口的定义&#xff1a; 给实现类具体代码&#xff0c;填写接口中方法。 3. 类、抽象类、继承类、抽象方法的定义&#xff1a; 抽象类&…

【管理咨询宝藏95】SRM采购平台建设内部培训方案

本报告首发于公号“管理咨询宝藏”&#xff0c;如需阅读完整版报告内容&#xff0c;请查阅公号“管理咨询宝藏”。 【管理咨询宝藏95】SRM采购平台建设内部培训方案 【格式】PDF版本 【关键词】SRM采购、制造型企业转型、数字化转型 【核心观点】 - 重点是建设一个适应战略采…

20240508请问GTX2080TI的300和300A核心的差异?

20240508请问GTX2080TI的300和300A核心的差异&#xff1f; 在拼多多/淘宝上&#xff0c;GTX2080TI的300A核心的会比300核心的贵100&#xffe5;左右。 但是怎么区分呢&#xff1f; 300a核心和300请问怎么区分呢&#xff1f;[嘻嘻] devicr ID diviceid 1e07是300a 1e04是300 Gp…
最新文章