正点原子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; //保留字段 1unsigned short reserved2; //保留字段 2unsigned 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;elsemin_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;elsemin_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://xiahunao.cn/news/3019773.html

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

相关文章

在 Linux 中删除文件和文件夹

目录 ⛳️推荐 前言 删除文件 &#x1f3cb;️练习文件删除 小心删除 删除目录 &#x1f3cb;️练习文件夹删除 测试你的知识 ⛳️推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到…

使用.NET8实现Web API

目录 1、环境准备1.1、从官网下载 及安装VS2022社区版1.2、下载及安装asp.net core的运行时及IIS Module 2、WebAPI工程创建2.1 创建API服务2.2 推荐的库2.2.1 数据库篇2.2.1.1、 SQLSugar2.2.1.2、 OracleAccess 2.2.2、IOC篇2.2.2.1、autofac2.2.2.2、 2.2.3、日志记录篇2.2.…

Django Admin后台管理:高效开发与实践

title: Django Admin后台管理&#xff1a;高效开发与实践 date: 2024/5/8 14:24:15 updated: 2024/5/8 14:24:15 categories: 后端开发 tags: DjangoAdmin模型管理用户认证数据优化自定义扩展实战案例性能安全 第1章&#xff1a;Django Admin基础 1.1 Django Admin简介 Dj…

【SpringBoot】使用MockMvc+Mockito进行单元测试像德芙一样纵享丝滑!

文章目录 前言&#xff1a;Java常见的单元测试框架一.Junit5基础二.SpringBoot项目单元测试1.添加依赖2.SpringBoot单元测试标准结构3.SpringBoot单元测试常用注解 三.单元测试中如何注入依赖对象1.真实注入&#xff08;AutoWired、 Resource&#xff09;2.Mock注入2.1.前言2.2…

test我说话撒机房环境

testhfsjafjdsbzvbcxn.ztesthfsjafjdsbzvbcxn.ztesthfsjafjdsbzvbcxn.ztesthfsjafjdsbzvbcxn.ztesthfsjafjdsbzvbcxn.ztesthfsjafjdsbzvbcxn.ztesthfsjafjdsbzvbcxn.ztesthfsjafjdsbzvbcxn.z

海淘美国礼品卡测评:AE/TT/香草卡与国内卡商、亚马逊测评工作室如何变现?(下)

上回分析的四种变现模式&#xff0c;相信大家已经了解清楚。 塔吉特礼品卡&#xff0c;香草礼品卡&#xff0c;AE礼品卡&#xff0c;百思买礼品卡&#xff0c;亚马逊礼品卡&#xff0c;沃尔玛礼品卡&#xff0c;丝芙兰礼品卡&#xff0c;雷蛇礼品卡&#xff0c;谷歌礼品卡&…

CSS定位(如果想知道CSS有关定位的知识点,那么只看这一篇就足够了!)

前言&#xff1a;在网页布局的时候&#xff0c;我们需要将想要的元素放到指定的位置上&#xff0c;这个时候我们就可以使用CSS中的定位操作。 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客 先让我们看一下本篇文章的大致内容&…

在Unity中制作和使用图集

文章目录 使用Unity内置Sprite Packer使用图集NGUI图集制作&#xff08;如果使用NGUI&#xff09;TextMeshPro中文支持 应用案例&#xff1a;在Unity中创建一个使用图集的UI界面场景设定步骤概览1. 准备UI元素2. 创建Sprite Atlas3. 使用图集中的Sprite4. 调整与布局5. 动态加载…

鸿蒙内核源码分析(特殊进程篇)

三个进程 鸿蒙有三个特殊的进程&#xff0c;创建顺序如下: 2号进程&#xff0c;KProcess&#xff0c;为内核态根进程.启动过程中创建.0号进程&#xff0c;KIdle为内核态第二个进程&#xff0c;它是通过KProcess fork 而来的.这有点难理解.1号进程&#xff0c;init&#xff0c…

kubeflow简单记录

kubeflow 13.7k star 1、Training Operator 包括PytorchJob和XGboostJob&#xff0c;支持部署pytorch的分布式训练 2、KFServing快捷的部署推理服务 3、Jupyter Notebook 基于Web的交互式工具 4、Katib做超参数优化 5、Pipeline 基于Argo Workflow提供机器学习流程的创建、编排…

还有谁不想薅云渲染的羊毛?五种云渲染优惠知道就是省到

不管你是效果图设计师还是动画设计师&#xff0c;在面对紧急或大量的渲染任务时&#xff0c;总会有云渲染的需要。然而&#xff0c;现在的云渲染越来越贵&#xff0c;我们该如何尽可能地节约成本完成渲染任务呢&#xff1f;本文将为你介绍云渲染的五种优惠形式&#xff0c;看完…

IP 地址追踪工具促进有效的 IP 管理

网络 IP 地址空间的结构、扫描和管理方式因组织的规模和网络需求而异&#xff0c;网络越大&#xff0c;需要管理的 IP 就越多&#xff0c;IP 地址层次结构就越复杂。因此&#xff0c;如果没有 IP 地址管理&#xff08;IPAM&#xff09;解决方案&#xff0c;IP 资源过度使用和地…

VSCode-vue3.0-安装与配置-export default简单例子

文章目录 1.下载VSCode2.修改语言为中文3.辅助插件列表4.vue3模板文件简单例子5.总结 1.下载VSCode 从官网下载VSCode&#xff0c;并按下一步安装成功。 2.修改语言为中文 点击确认修改&#xff0c;如下图所示&#xff1a; 或者打开命令面板&#xff1a;输入Configure Displ…

浅析扩散模型与图像生成【应用篇】(二十二)——DreamBooth

21. DreamBooth: Fine Tuning Text-to-Image Diffusion Models for Subject-Driven Generation 本文提出一种根据少量样例图片来对文生图模型进行微调的方法&#xff0c;从而可以生成包含样例物体&#xff0c;但风格、姿态、背景都可以任意修改的图片。现有的文生图模型都是需要…

作为新型锂离子电池正极材料 磷酸锰铁锂(LMFP)行业发展空间有望扩展

作为新型锂离子电池正极材料 磷酸锰铁锂&#xff08;LMFP&#xff09;行业发展空间有望扩展 磷酸锰铁锂&#xff08;LMFP&#xff09;指在磷酸铁锂基础上添加锰元素而制成的新型磷酸盐类锂离子电池正极材料。磷酸锰铁锂含有橄榄石型结构&#xff0c;生产成本低、能量密度高、绿…

操作系统之管程

目录 一. 为什么要引入管程二. 管程的定义与基本特征三. 扩展1:用管程来解决生产者和消费者问题四. 扩展2: Java中类似于管程的机制 \quad 一. 为什么要引入管程 \quad \quad 二. 管程的定义与基本特征 \quad \quad 三. 扩展1:用管程来解决生产者和消费者问题 \quad 很智能 \qu…

VM 安装Ubuntu20

1、VM 新建虚拟机 类型配置 - 典型 安装源选择 &#xff08;安装包获取&#xff1a;Ubuntu桌面系统 | Ubuntu&#xff09; 设置计算机名与用户账号密码 为虚拟机命一个名&#xff0c;设置虚拟机文件保存的位置 设置磁盘相关信息 最后一步&#xff0c;确定虚拟机的相关参数 设置…

WDW-10B微机控制电子万能试验机技术方案

一&#xff0e;设备外观照片&#xff1a; 项目简介&#xff1a; 微机控制电子式万能试验机是专门针对高等院校、各种金属、非金属科研厂家及国家级质检单位而设计的高端微机控制电子式万能试验机、计算机系统通过全数字控制器&#xff0c;经调速系统控制伺服电机转动&#xff…

【UE+MQTT】Mqtt Client插件使用记录

步骤 1. 在虚幻商城中下载“Mqtt Client”插件 插件地址&#xff1a;https://www.unrealengine.com/marketplace/zh-CN/product/34cbcaef7a664451a886dba37b4769bc?sessionInvalidatedtrue 文档地址&#xff1a;[虚幻引擎] DT Mqtt 插件详细说明 – DT 2. 在虚幻编辑器中确…

input对接二维码条形码扫描仪输入,检测扫描完成后按下回车事件

二维码和条形码扫描仪其实是模拟键盘输入的操作&#xff0c;只是操作比键盘要快很多&#xff0c;其实想要检测扫描仪输入完成的操作&#xff0c;可以有多种方式&#xff0c;一个是比较笨的&#xff0c;就是设置防抖操作&#xff0c;等间隔超过50ms就算输入完成&#xff0c;还有…