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

news2024/11/19 3:27:34

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.coloradmin.cn/o/1655529.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java的BIO/NIO/AIO

1. Java中的BIO、NIO和AIO的基本概念及其主要区别 BIO (Blocking I/O): 传统的同步阻塞I/O模型。每个连接创建成功后都需要一个线程来处理&#xff0c;如果连接没有数据可读&#xff0c;则线程会阻塞在读操作上。这种模型简单易理解&#xff0c;但在高并发环境下会消耗大量系统…

【excel】数据非数值导致排序失效

场景 存在待排序列的数值列&#xff0c;但排序失效&#xff0c;提示类型有问题&#xff1a; 解决 选中该列&#xff0c;数据→分列 而后发现提示消失&#xff0c;识别为数字&#xff0c;可正常排序。

ERROR 1045 (28000) Access denied for user ‘root‘@‘IP‘(using password YES/NO)

查看权限 要查看MySQL用户的权限&#xff0c;您可以使用SHOW GRANTS语句。这将列出用户的权限&#xff0c;包括授予的权限和可以授予其他用户的权限。 以下是查看当前用户权限的SQL命令&#xff1a; SHOW GRANTS; 如果您想查看特定用户的权限&#xff0c;可以使用以下命令&…

【漏洞复现】金和OA FileDownLoad接口处存在任意文件读取漏洞

免责声明&#xff1a;文章来源互联网收集整理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该…

销售订单分析表-CX_SY_CONVERSION_NO_NUMBER异常

销售订单分析表-CX_SY_CONVERSION_NO_NUMBER异常 这里记录一次发生过的异常报错&#xff0c;和找到原因的过程&#xff1a;

Springboot+Vue项目-基于Java+MySQL的流浪动物管理系统(附源码+演示视频+LW)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;Java毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计 &…

华为OD机试 - 计算三叉搜索树的高度 - 二叉树(Java 2024 C卷 100分)

华为OD机试 2024C卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷C卷&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;每一题都有详细的答题思路、详细的代码注释、样例测试…

每日Attention学习1——Parallel Aggregation Pyramid Pooling Module

模块出处 [CVPR 23] [link] [code] PIDNet: A Real-time Semantic Segmentation Network Inspired by PID Controllers 模块名称 Parallel Aggregation Pyramid Pooling Module (PAPPM) 模块作用 多尺度特征提取&#xff0c;更大感受野 模块结构 模块代码 import torch imp…

C++聊天服务器数据库创建

创建数据库chat show databases&#xff1a;展示所有的数据库 create database chat&#xff1a;创建一个数据库chat use chat&#xff1a;使用数据库 创建表User、Friend、AllGroup、GroupUser、OfflineMessage 表User包含&#xff1a;用户id、用户名、用户密码、当前登录…

jsp驾校管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 驾校管理系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统采用serlvetdaobean mvc 模式&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发…

二叉树习题汇总

片头 嗨&#xff01;大家好&#xff0c;今天我们来练习几道二叉树的题目来巩固知识点&#xff0c;准备好了吗&#xff1f;Ready Go ! ! ! 第一题&#xff1a;二叉树的最大深度 解答这道题&#xff0c;我们采用分治思想 1. 递归子问题&#xff1a;左子树的高度和右子树的高度 …

绿盟之旅——一段安全实习结束

去年&#xff0c;因为着急找实习&#xff0c;拿着简历就开始海投&#xff0c;当时想的是有人让我去就谢天谢地了&#xff0c;第一个约我面试的就是绿盟&#xff0c;也很顺利的通过了面试&#xff0c;当时让我选择在上海还是北京&#xff0c;我选择的是上海&#xff0c;因为学校…

热红外相机图片与可见光图片配准教程

一、前言 图像配准是一种图像处理技术&#xff0c;用于将多个场景对齐到单个集成图像中。在这篇文章中&#xff0c;我将讨论如何在可见光及其相应的热图像上应用图像配准。在继续该过程之前&#xff0c;让我们看看什么是热图像及其属性。 二、热红外数据介绍 热图像本质上通…

高薪!【YesPMP】众包平台5月9日最新外包项目

【YesPMP】众包平台5月9日最新外包项目&#xff0c;感兴趣的小伙伴&#xff0c;可进入平台参与竞标&#xff0c;竞标后与项目方直接与联系&#xff0c;双方直接对接。 1.查看项目&#xff1a;go编写的协议网关成品代码(支持modbus,knx等) http:// https://www.yespmp.com/proj…

HTTPS 是如何进行安全传输的 ?

概述 现代密码学对信息的处理主要离不开以下的三种形式&#xff1a; 摘要&#xff1a;主要用于数据校验&#xff0c;例如存储密码等&#xff0c;摘要是对信息进行单向的哈希&#xff0c;改变信息的原有形态&#xff0c;因为哈希函数的特点是易变性&#xff08;即使微小的变化也…

医学四种概念:B超、X光、CT、核磁共振

辐射检测&#xff1a;CT和X光 X光&#xff1a;X光检测价格低、时间短、出片快、辐射小&#xff0c;适合前期检查。由于人体各个组织对X线所产生的影响是不同的&#xff0c;所以就有不同的反应实验就是下图 生成的图片为&#xff0c;它把三维的你压缩成二维的图片了&#xff0c…

上位机图像处理和嵌入式模块部署(树莓派4b和mcu的分工与配合)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 树莓派4b从广义上来说&#xff0c;它本身就是一个mini小电脑。我们需要什么软件&#xff0c;就可以apt install去下载即可。这和之前嵌入式linux开…

探索设计模式的魅力:权力集中,效率提升,中心化模式的优势与挑战

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 ✨欢迎加入探索中心化模式之旅✨ 大家好啊&#xff01;&#x1f44b; 这次我们要聊的是IT界一…

matlab使用教程(66)—在图中绘制多条曲线(2019b以下版本)

您可以采用多种方式合并绘图。利用子图&#xff0c;可在同一坐标区中合并多个图&#xff0c;或在一个图窗中创建多个坐标区。 1.在同一坐标区中合并绘图 默认情况下&#xff0c;新图将清除现有图&#xff0c;并重置标题等坐标区属性。但是&#xff0c;您可以使用 hold on 命令…

Netty HTTP2 示例-响应式编程-013

🤗 ApiHug {Postman|Swagger|Api...} = 快↑ 准√ 省↓ GitHub - apihug/apihug.com: All abou the Apihug apihug.com: 有爱,有温度,有质量,有信任ApiHug - API design Copilot - IntelliJ IDEs Plugin | Marketplace The Next Generation API Development Platform …