您的位置:首页 > 其它

第47章 QR-Decoder-OV5640二维码识别—零死角玩转STM32-F429系列

2017-08-24 17:50 931 查看

第47章 QR-Decoder-OV5640二维码识别

全套200集视频教程和1000页PDF教程请到秉火论坛下载:www.firebbs.cn

野火视频教程优酷观看网址:http://i.youku.com/firege

本章参考资料:《STM32F4xx中文参考手册》、《STM32F4xx规格书》、库帮助文档《stm32f4xx_dsp_stdperiph_lib_um.chm》。

关于开发板配套的OV5640摄像头参数可查阅《ov5640datasheet》配套资料获知。

STM32F4芯片具有浮点运算单元,适合对图像信息使用DSP进行基本的图像处理,其处理速度比传统的8、16位机快得多,而且它还具有与摄像头通讯的专用DCMI接口,所以使用它驱动摄像头采集图像信息并进行基本的加工处理非常适合。本章讲解如何使用二维码识别库进行二维码的识别。

47.1 二维码简介

二维码,又称二维条码或二维条形码,二维条码是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的;在代码编制上巧妙地利用构成计算机内部逻辑基础的"0"、"1"比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息,通过图象输入设备或光电扫描设备自动识读以实现信息自动处理:它具有条码技术的一些共性:每种码制有其特定的字符集;每个字符占有一定的宽度;具有一定的校验功能等。同时还具有对不同行的信息自动识别功能、及处理图形旋转变化等特点。二维条码/二维码能够在横向和纵向两个方位同时表达信息,因此能在很小的面积内表达大量的信息。

47.2 二维条形码类型

47.2.1矩阵式二维条码

矩阵式二维条码(2D MATRIX BAR CODE)又称:棋盘式二维条码。有代表性的矩阵式二维条码有:QR
Code 、Data Matrix、Maxi Code、Code
one 等,目前最流行的是QR CODE。见图 471。



图 471矩阵式二维码

47.2.2行排列式二维条码

行排列式二维条码(2D STACKED BAR CODE)又称:堆积式二维条码或层排式二维条码,其编码原理是建立在一维条码基础之上,按需要堆积成二行或多行。有代表性的行排式二维条码有:PDF417、CODE49、CODE
16K等。见图 472。



图 472行排列式二维条码

47.3 二维条形码的优点

1. 可靠性强,条形码的读取准确率远远超过人工记录,平均每15000个字符才会出现一个错误。
2. 效率高,条形码的读取速度很快,相当于每秒40个字符。
3. 成本低,与其它自动化识别技术相比较,条形码技术仅仅需要一小张贴纸和相对构造简单的光学扫描仪,成本相当低廉。
4. 易于制作,条形码制作:条形码的编写很简单,制作也仅仅需要印刷,被称作为"可印刷的计算机语言"。
5. 构造简单,条形码识别设备的构造简单,使用方便。
6. 灵活实用,条形码符号可以手工键盘输入,也可以和有关设备组成识别系统实现自动化识别,还可和其他控制设备联系起来实现整个系统的自动化管理。
7. 高密度,二维条码通过利用垂直方向的堆积来提高条码的信息密度,而且采用高密度图形表示,因此不需事先建立数据库,真正实现了用条码对信息的直接描述。
8. 纠错功能,二维条形码不仅能防止错误,而且能纠正错误,即使条形码部分损坏,也能将正确的信息还原出来。
9. 多语言形式、可表示图像,二维条码具有字节表示模式,即提供了一种表示字节流的机制。不论何种语言文字它们在计算机中存储时以机内码的形式表现,而内部码都是字节码,可识别多种语言文字的条码。
10. 具有加密机制,可以先用一定的加密算法将信息加密,再用二维条码表示。在识别二维条码时,再加以一定的解密算法,便可以恢复所表示的信息。

47.4 QR二维码的编码及识别

47.4.1QR码基本结构

QR码基本结构,见图 473。

1. 位置探测图形、位置探测图形分隔符、定位图形:用于对二维码的定位,对每个QR码来说,位置都是固定存在的,只是大小规格会有所差异。
2. 校正图形:规格确定,校正图形的数量和位置也就确定了。
3. 格式信息:表示改二维码的纠错级别,分为L、M、Q、H。
4. 版本信息:即二维码的规格,QR码符号共有40种规格的矩阵(一般为黑白色),从21x21(版本1),到177x177(版本40),每一版本符号比前一版本 每边增加4个模块。
5. 数据和纠错码字:实际保存的二维码信息,和纠错码字(用于修正二维码损坏带来的错误)。


图 473 QR码基本结构

47.4.2QR码编码过程

1. 数据分析:确定编码的字符类型,按相应的字符集转换成符号字符; 选择纠错等级,在规格一定的条件下,纠错等级越高其真实数据的容量越小。
2. 数据编码:将数据字符转换为位流,每8位一个码字,整体构成一个数据的码字序列。其实知道这个数据码字序列就知道了二维码的数据内容。见表 471和表 472。
表 471 QR码数据容量

QR码数据容量
数字

最多7,089字符

字母

最多4,296字符

二进制数(8 bit)

最多2,593字节

日本汉字/片假名

最多1,817字符(采用Shift JIS)

中文汉字

最多984字符(采用UTF-8)

中文汉字

最多1,800字符(采用BIG5)

表 472 QR数据模式指示符

模式

指示符

ECI

0111

数字

0001

字母数字

0010

8位字节

0100

日本汉字

1000

中国汉字

1101

结构链接

0011

FNC1

0101(第一位置)

1001(第二位置)

终止符(信息结尾)

0000

3. 编码过程:数据可以按照一种模式进行编码,以便进行更高效的解码,例如:对数据:01234567编码(版本1-H)。
a) 分组:012 345 67
b) 转成二进制:
012 → 0000001100
345 → 0101011001
67 → 1000011
c) 转成序列:0000001100 0101011001 1000011
d) 字符数转成二进制:8 → 0000001000
e) 加入模式指示符:
0001:0001 0000001000 0000001100 0101011001 1000011
对于字母、中文、日文等只是分组的方式、模式等内容有所区别。基本方法是一致的。
4. 纠错编码:按需要将上面的码字序列分块,并根据纠错等级和分块的码字,产生纠错码字,并把纠错码字加入到数据码字序列后面,成为一个新的序列。
错误修正容量, L水平有7%的字码可被修正;
M水平有15%的字码可被修正;Q水平有25%的字码可被修正;H水平有30%的字码可被修正。

二维码规格和纠错等级确定的情况下,其实它所能容纳的码字总数和纠错码字数也就确定了,比如:版本10,纠错等级时H时,总共能容纳346个码字,其中224个纠错码字。

就是说二维码区域中大约1/3的码字时冗余的。对于这224个纠错码字,它能够纠正112个替代错误(如黑白颠倒)或者224个据读错误(无法读到或者无法译码),这样纠错容量为:112/346=32.4%。

5. 构造最终数据信息:在规格确定的条件下,将上面产生的序列按次序放如分块中,按规定把数据分块,然后对每一块进行计算,得出相应的纠错码字区块,把纠错码字区块按顺序构成一个序列,添加到原先的数据码字序列后面。
例如:D1, D12, D23, D35, D2, D13, D24, D36, ... D11, D22, D33, D45, D34,
D46, E1, E23,E45, E67, E2, E24, E46, E68,...
6. 构造矩阵:将探测图形、分隔符、定位图形、校正图形和码字模块放入矩阵中。把上面的完整序列填充到相应规格的二维码矩阵的区域中,见图 474 构造矩阵。



图 474构造矩阵
7. 掩摸:将掩摸图形用于符号的编码区域,使得二维码图形中的深色和浅色(黑色和白色)区域能够比率最优的分布。见图 474 构造矩阵。
8. 格式和版本信息:生成格式和版本信息放入相应区域内。版本7-40都包含了版本信息,没有版本信息的全为0。二维码上两个位置包含了版本信息,它们是冗余的。版本信息共18位,6X3的矩阵,其中6位是数据位,如版本号8,数据位的信息时 001000,后面的12位是纠错位。

47.4.3QR码识别过程

通过图像的采集设备(激光扫描器、面阵CCD、数码相机等成像设备),我们得到含有条码的图像,此后主要经过条码定位(预处理,定位,角度纠正和特征值提取)分割解码三个步骤实现条码的识别。

1. 条码的定位就是找到条码符号的图像区域,对有明显条码特征的区域进行定位。然后根据不同条码的定位图形结构特征对不同的条码符号进行下一步的处理。
2. 实现条码的定位,采用以下步骤:
a) 利用点运算的阈值理论将采集到的图象变为二值图像, 即对图像进行二值化处理;
b) 得到二值化图像后,对其进行膨胀运算;
c) 对膨胀后的图象进行边缘检测得到条码区域的轮廓;
下图 475是经过上述处理后得到的一系列图像。



图 475图像处理
3. 对图像进行二值化处理,按下式进行





其中,f(x,y)是点(x,y)处像素的灰度值,T为阈值(自适应门限)。找到条码区域后,我们还要进一步区分到底是哪种矩阵式条码。下面图形是几种常见的矩阵式条码:
a) 位于左上角、左下角、右上角的三个定位图形
b) 位于符号中央的三个等间距同心圆环(或称公牛眼定位图形)
c) 位于左边和下边的两条垂直的实线段



图 476图像处理
4. 条码的分割
边缘检测后条码区域的边界不是很完整,所以需要进一步的修正边界,然后分割出一个完整的条码区域。首先采用区域增长的方法对符号进行分割,以此修正条码边界。其基本思想是从符号内的一个小区域(种子)开始,通过区域增长来修正条码边界,把符号内的所有点都包括在这个边界内。然后通过凸壳计算准确分割出整个符号。之后区域增长和凸壳计算交替进行,通常对那些密度比较大的条码重复两次就足够了,而对于那些模块组合比较稀疏的条码至少要重复四次。

5. 译码
得到一幅标准的条码图像后,对该符号进行网格采样,对网格每一个交点上的图像像素取样,并根据阈值确定是深色块还是浅色块。构造一个位图,用二进制的"1"表示深色像素, "0"表示浅色像素,从而得到条码的原始二进制序列值,然后对这些数据进行纠错和译码,最后根据条码的逻辑编码规则把这些原始的数据位流转换成数据码字,即将码字图像符号换成ASCII码字符串。

47.5 QR-Decoder-OV564摄像头实验

本小节讲解如何使用QR-Code库在DCMI—OV5640摄像头实验基础上进行二维码解码的过程,建议学习之前先把DCMI—OV5640摄像头实验弄明白。

学习本小节内容时,请打开配套的"QR-Decoder-OV5640"工程配合阅读。由于硬件设计方面跟DCMI—OV5640摄像头实验的是一样的,这里不再重复。下面直接介绍如何使用QR-Code库进行二维码识别。OV5640识别二维码的过程包括以下几个重要部分:图像采集,液晶驱动,图像处理,数据解码,串口打印输出结果。见图
477。



图 477 OV5640识别二维码过程

47.5.1QR-Code解码库特点

QR-Code解码库是秉火专门针对STM32F429移植的一个的条码解码库,因为其结构复杂,移植过程繁琐,所以打包为一个解码库,提供接口方便用户直接调用,提高开发的效率。其主要特点如下:

 条码种类: 支持常用QR-Code、EAN、UPC
 扫描速度: 400 毫秒
 扫描英文: 250 个字符
 扫描中文: 90中文字符,UTF-8编码格式(需上位机支持)
 多码扫描: 支持多个二维码同时解码,同时输出结果

47.5.2软件设计

1. 编程要点
根据OV5640识别二维码的过程,软件设计可以根据以下几个模块分别进行:

(1) 图像采集,通过STM32F429的DCMI接口驱动OV5640,采集适合液晶屏分辨率的图像。OV5640支持自动对焦功能,因此很容易采集到高清度的图像。
(2) 液晶驱动,通过STM32F429的LTDC接口驱动液晶屏,使用外部SDRAM作为液晶屏的显存,通过DMA2D来刷屏;同时LTDC支持双层叠加显示,可以在液晶屏上实现半透明的扫描窗并且支持绘制扫描线的动画效果。
(3) 图像处理,使用外部SDRAM作为缓存为图像处理提供足够的空间,通过调用QR-Code解码库的get_image函数获取一帧图像。通过图像处理将图像的数据流转变为一个二进制的码流再进行数据解码。
(4) 数据解码,直接通过QR_decoder函数来解码。返回值为解码的条码个数。并将解码结果保存到decoded_buf的二维数组当中。
(5) 串口发送,根据解码结果的个数及decoded_buf二维数组的数据,通过串口发送到电脑上位机。
2. 代码分析
QR-Code解码库相关宏定义
我们把QR-Code解码库相关的配置都以宏的形式定义到"qr_decoder_user.h"文件中,其中包括数据缓冲基地址、扫描窗大小、扫描框线条大小、解码结果二维数组、扫描二维码的函数,见代码清单
242。

代码清单 471 QR-Code解码库配置相关的宏
1 #ifndef  __QR_DECODER_USER_H

2#define __QR_DECODER_USER_H
3
4#include
"qr_decoder.h"
5#include <stdio.h>
6
7//
开辟SDRAM的3M字节作为数据缓存,这里使用显存以外的空间,
8// 0xD0800000-0x300000 = 0xD0500000
9#define QR_FRAME_BUFFER ((uint32_t)0xD0500000)
10
11/*扫描窗口参数*/
12#define Frame_width ((uint16_t)320)//扫描窗口边长(正方形)
13
14/*扫描框线条参数*/
15#define Frame_line_length ((uint16_t)30)//扫描框线条长度
16#define Frame_line_size ((uint16_t)3)//扫描框线条宽度
17
18#define QR_SYMBOL_NUM 5
//识别二维码的最大个数
19#define QR_SYMBOL_SIZE 512
//每组二维码的的最大容量
20
21//解码数据封装为二维数组decoded_buf,格式为:
22//
(第一组:解码类型长度(8bit)+解码类型名称+解码数据长度(16bit,高位在前低位在后)+解码数据)
23
24//
(第二组:解码类型长度(8bit)+解码类型名称+解码数据长度(16bit,高位在前低位在后)+解码数据)
25
26//
。。。
27//以此类推
28 externchar decoded_buf[QR_SYMBOL_NUM][QR_SYMBOL_SIZE];
29
30//解码函数,返回值为识别条码的个数
31char QR_decoder(void);
32
33//获取一帧图像
34void get_image(uint32_t src_addr,uint16_t
img_width,uint16_t img_height);
35
36#endif
/* __QR_DECODER_USER_H */
以上代码首先定义一个3M字节的空间用作解码库的数据的缓冲,只需要定义SDRAM的空闲空间的基地址;然后定义扫描二维码的窗口及框体大小,范围由100~480(图像不能太小,否则图像很难识别);定义decoded_buf[QR_SYMBOL_NUM][QR_SYMBOL_SIZE]二维数组存放解码的结果,存放解码的最大个数由QR_SYMBOL_NUM决定,存放解码的最大数据量由QR_SYMBOL_SIZE决定,没有特殊要求就不需要做变动;存放数据的格式介绍如下表
473。

表 473二维数组数据格式

数组

十六进制
字符
含义
decoded_buf[0][0]

0x07
第一组解码类型名字的长度
decoded_buf[0][1]

0x51
Q
第一组解码类型名字:QR-Code
decoded_buf[0][2]

0x52
R
decoded_buf[0][3]

0x2d
-
decoded_buf[0][4]

0x43
C
decoded_buf[0][5]

0x6f
o
decoded_buf[0][6]

0x64
d
decoded_buf[0][7]

0x65
e
decoded_buf[0][8]

0x00
第一组解码数据长度的高八位
decoded_buf[0][9]

0x15
第一组解码数据长度的低八位
decoded_buf[0][10]

0x68
h
第一组解码数据:http://www.firebbs.cn
decoded_buf[0][11]

0x74
t
decoded_buf[0][12]

0x74
t
decoded_buf[0][13]

0x70
p
decoded_buf[0][14]

0x3a
:
decoded_buf[0][15]

0x2f
/
decoded_buf[0][16]

0x2f
/
decoded_buf[0][17]

0x77
w
decoded_buf[0][18]

0x77
w
decoded_buf[0][19]

0x77
w
decoded_buf[0][20]

0x2e
.
decoded_buf[0][21]

0x66
f
decoded_buf[0][22]

0x69
i
decoded_buf[0][23]

0x72
r
decoded_buf[0][24]

0x65
e
decoded_buf[0][25]

0x62
b
decoded_buf[0][26]

0x62
b
decoded_buf[0][27]

0x73
s
decoded_buf[0][28]

0x2e
.
decoded_buf[0][29]

0x63
c
decoded_buf[0][30]

0x6e
n
decoded_buf[1][0]

0x07
第二组解码类型名字的长度
decoded_buf[1][1]

0x51
Q
第二组解码类型名字:QR-Code
decoded_buf[1][2]

0x52
R
decoded_buf[1][3]

0x2d
-
decoded_buf[1][4]

0x43
C
decoded_buf[1][5]

0x6f
o
decoded_buf[1][6]

0x64
d
decoded_buf[1][7]

0x65
e
decoded_buf[1][8]

0x00
第二组解码数据长度的高八位
decoded_buf[1][9]

0x03
第二组解码数据长度的低八位
decoded_buf[1][10]

0x31
1
第二组解码数据:123
decoded_buf[1][11]

0x32
2
decoded_buf[1][12]

0x33
3
decoded_buf[2][0]

第三组解码类型名字的长度





QR_decoder为解码函数,用户可以直接调用这个函数,返回值为解码成功的个数。get_image函数为获取图片的函数,通过指定存放图片的首地址,图片的分辨率来获取图片。

图像采集
我们需要通过OV5640摄像头采集的图像数据传递到解码库解码,在帧中断提取一帧图片用来解码,见代码清单 243。

代码清单 472 DCMI的中断响应函数(stm32f4xx.it)
1//使用帧中断重置line_num,可防止有时掉数据的时候DMA传送行数出现偏移
2void DCMI_IRQHandler(void)
3{
4/*判断帧中断标志位是否被置位*/
5if ( DCMI_GetITStatus (DCMI_IT_FRAME) == SET ) {
6/*传输完一帧,计数复位*/
7line_num=0;
8/*停止采集*/
9DCMI_CaptureCmd(DISABLE);
10/*获取一帧图片,FSMC_LCD_ADDRESS为存放图片的首地址*/
11/*LCD_PIXEL_WIDTH为图片宽度,LCD_PIXEL_HEIGHT为图片高度*/
12get_image(FSMC_LCD_ADDRESS,LCD_PIXEL_WIDTH,LCD_PIXEL_HEIGHT);
13/*绘制扫描窗口里边的扫描线,放在这里主要是避免屏幕闪烁*/
14LCD_Line_Scan_ARGB8888();
15/*重新开始采集*/
16DCMI_CaptureCmd(ENABLE);
17/*清除帧中断标志位*/
18DCMI_ClearITPendingBit(DCMI_IT_FRAME);
19}
20
21}

在DCMI中断函数中增加获取图片函数,先停止摄像头的采集,然后通过get_image函数获取一帧图片,这个函数传递的第一个参数FSMC_LCD_ADDRESS是图片存放的首地址,第二个参数LCD_PIXEL_WIDTH为图片宽度,第三个参数是LCD_PIXEL_HEIGHT为图片高度,图片通过这个函数传递给解码函数进行解码,主函数将介绍如何调用解码函数。

通过LCD_Line_Scan_ARGB8888函数来绘制扫描线,绘制完后再启动摄像头的采集。LCD_Line_Scan_ARGB8888函数放在这个位置解决了当同时操作液晶的前景层和背景层时闪烁的问题。

液晶驱动
F429的LTDC支持双层叠加显示功能,具体可以参考我们LTDC部分章节的详细介绍。现在主要介绍如何绘制扫描窗口。我们定义背景层为显示摄像头图像层,前景层为扫描框显示层,代码清单
455。

代码清单 473配置DMA数据传输(bsp_ov5640.c文件)
1/*扫描窗口参数*/
2#define Frame_width ((uint16_t)320)//扫描窗口边长(正方形)
3
4/*扫描框线条参数*/
5#define Frame_line_length ((uint16_t)30)//扫描框线条长度
6#define Frame_line_size ((uint16_t)3)//扫描框线条宽度
7
8//指定扫描窗口里边扫描线的初始位置
9int pos=(LCD_PIXEL_HEIGHT-Frame_width)/2+5*Frame_line_size;
10/**
11* @brief
清屏
12* @param Color:
清屏颜色
13* @retval None
14*/
15void LCD_Clear_ARGB8888(uint32_t Color)
16{
17DMA2D_InitTypeDef DMA2D_InitStruct;
18
19uint16_t Alpha_Value=0,Red_Value = 0, Green_Value = 0, Blue_Value = 0;
20
21Alpha_Value = (0xFF000000&Color)>>24;
22Red_Value = (0x00FF0000 & Color) >> 16;
23Blue_Value = 0x000000FF & Color;
24Green_Value = (0x0000FF00 & Color) >> 8;
25
26/* configure DMA2D */
27DMA2D_DeInit();
28DMA2D_InitStruct.DMA2D_Mode = DMA2D_R2M;
29DMA2D_InitStruct.DMA2D_CMode = DMA2D_ARGB8888;
30DMA2D_InitStruct.DMA2D_OutputGreen = Green_Value;
31DMA2D_InitStruct.DMA2D_OutputBlue = Blue_Value;
32DMA2D_InitStruct.DMA2D_OutputRed = Red_Value;
33DMA2D_InitStruct.DMA2D_OutputAlpha = Alpha_Value; //设置透明度
34DMA2D_InitStruct.DMA2D_OutputMemoryAdd = CurrentFrameBuffer;
35DMA2D_InitStruct.DMA2D_OutputOffset = 0;
36DMA2D_InitStruct.DMA2D_NumberOfLine = LCD_PIXEL_HEIGHT;
37DMA2D_InitStruct.DMA2D_PixelPerLine = LCD_PIXEL_WIDTH;
38DMA2D_Init(&DMA2D_InitStruct);
39
40/* Start Transfer */
41DMA2D_StartTransfer();
42
43/* Wait for CTC Flag activation */
44while (DMA2D_GetFlagStatus(DMA2D_FLAG_TC) == RESET) {
45}
46}
47/**
48* @brief
绘制一条线条
49* @param Xpos:
起点X轴坐标,范围0到800
50* @param Ypos:
起点Y轴坐标,范围0到480
51* @param Length:
线条长度
52* @param Line_width:
线条宽度
53* @param Direction:
线条方向(水平或者垂直).
54* @retval None
55*/
56void LCD_DrawLine_ARGB8888(
57uint16_t Xpos,
58uint16_t Ypos,
59uint16_t Length,
60uint8_t Line_width,
61uint8_t Direction)
62{
63DMA2D_InitTypeDef DMA2D_InitStruct;
64
65uint32_t Xaddress = 0;
66uint16_t Alpha_Value=0,Red_Value = 0, Green_Value = 0, Blue_Value = 0;
67//提取各通道的颜色值
68Alpha_Value = (0xFF000000&CurrentTextColor_ARGB8888)>>24;
69Red_Value = (0x00FF0000 & CurrentTextColor_ARGB8888) >> 16;
70Blue_Value = 0x000000FF & CurrentTextColor_ARGB8888;
71Green_Value = (0x0000FF00 & CurrentTextColor_ARGB8888) >> 8;
72//指定绘制的首地址
73Xaddress = CurrentFrameBuffer + 4*(LCD_PIXEL_WIDTH*Ypos + Xpos);
74
75//配置 DMA2D
76DMA2D_DeInit();
77DMA2D_InitStruct.DMA2D_Mode = DMA2D_R2M;
78DMA2D_InitStruct.DMA2D_CMode = DMA2D_ARGB8888;
79DMA2D_InitStruct.DMA2D_OutputGreen = Green_Value;
80DMA2D_InitStruct.DMA2D_OutputBlue = Blue_Value;
81DMA2D_InitStruct.DMA2D_OutputRed = Red_Value;
82DMA2D_InitStruct.DMA2D_OutputAlpha = Alpha_Value;
83DMA2D_InitStruct.DMA2D_OutputMemoryAdd = Xaddress;
84//水平方向
85if (Direction == LCD_DIR_HORIZONTAL) {
86DMA2D_InitStruct.DMA2D_OutputOffset = LCD_PIXEL_WIDTH-Length;
87DMA2D_InitStruct.DMA2D_NumberOfLine = Line_width;
88DMA2D_InitStruct.DMA2D_PixelPerLine = Length;
89}
else {//垂直方向
90DMA2D_InitStruct.DMA2D_OutputOffset = LCD_PIXEL_WIDTH - Line_width;
91DMA2D_InitStruct.DMA2D_NumberOfLine = Length;
92DMA2D_InitStruct.DMA2D_PixelPerLine = Line_width;
93}
94
95DMA2D_Init(&DMA2D_InitStruct);
96//
开始传输
97DMA2D_StartTransfer();
98//等待传输完成
99while (DMA2D_GetFlagStatus(DMA2D_FLAG_TC) == RESET) {
100}
101
102}
103/**
104* @brief
绘制一个矩形线条框.
105* @param Xpos: X起始位置,范围
0 —— 800
106* @param Ypos: Y起始位置,范围
0 —— 480
107* @param Width:
显示线条的宽度度,范围 0 —— 800
108* @param Height:显示线条的高度,范围
0 —— 480
109* @param Line_width:显示线条的宽度
110* @retval None
111*/
112void LCD_DrawRect_ARGB8888(
113uint16_t Xpos,
114uint16_t Ypos,
115uint16_t Width,
116uint16_t Height,
117uint8_t Line_width)
118{
119//绘制水平方向的线条
120LCD_DrawLine_ARGB8888(Xpos, Ypos, Width,Line_width,LCD_DIR_HORIZONTAL);
121LCD_DrawLine_ARGB8888(Xpos, (Ypos+ Height), Width+Line_width,
122Line_width ,LCD_DIR_HORIZONTAL);
123
124//绘制垂直方向的线条
125LCD_DrawLine_ARGB8888(Xpos, Ypos, Height, Line_width ,LCD_DIR_VERTICAL);
126LCD_DrawLine_ARGB8888((Xpos + Width), Ypos, Height, Line_width ,LCD_DIR_VERTICAL);
127}
128
129
130/**
131* @brief
在显示区域中心绘制一个矩形.
132* @param Width:
显示图像的宽度,范围 0 —— 800
133* @param Height:显示图像的高度,范围
0 —— 480
134* @retval None
135*/
136void LCD_DrawFullRect_ARGB8888(uint16_t Width,uint16_t
Height)
137{
138DMA2D_InitTypeDef DMA2D_InitStruct;
139
140uint32_t Xaddress = 0;
141uint16_t Alpha_Value=0,Red_Value = 0, Green_Value = 0, Blue_Value =
0;
142
143//提取各通道的颜色值
144Alpha_Value = (0xFF000000&CurrentTextColor_ARGB8888)>>24;
145Red_Value = (0x00FF0000 & CurrentTextColor_ARGB8888) >> 16;
146Blue_Value = 0x000000FF & CurrentTextColor_ARGB8888;
147Green_Value = (0x0000FF00 & CurrentTextColor_ARGB8888) >> 8;
148
149//指定绘制的首地址
150Xaddress = CurrentFrameBuffer +
1514*(LCD_PIXEL_WIDTH*(LCD_PIXEL_HEIGHT-Height)/2 +
152(LCD_PIXEL_WIDTH-Width)/2);
153
154//配置
DMA2D
155DMA2D_DeInit();
156DMA2D_InitStruct.DMA2D_Mode = DMA2D_R2M;
157DMA2D_InitStruct.DMA2D_CMode = DMA2D_ARGB8888;
158DMA2D_InitStruct.DMA2D_OutputGreen = Green_Value;
159DMA2D_InitStruct.DMA2D_OutputBlue = Blue_Value;
160DMA2D_InitStruct.DMA2D_OutputRed = Red_Value;
161DMA2D_InitStruct.DMA2D_OutputAlpha = Alpha_Value;
162DMA2D_InitStruct.DMA2D_OutputMemoryAdd = Xaddress;
163DMA2D_InitStruct.DMA2D_OutputOffset = (LCD_PIXEL_WIDTH - Width);
164DMA2D_InitStruct.DMA2D_NumberOfLine = Height;
165DMA2D_InitStruct.DMA2D_PixelPerLine = Width;
166DMA2D_Init(&DMA2D_InitStruct);
167
168//开始传输
169DMA2D_StartTransfer();
170
171//等待传输完成
172while (DMA2D_GetFlagStatus(DMA2D_FLAG_TC) == RESET) {
173}
174
175LCD_SetTextColor(CurrentTextColor);
176}
177
178/**
179* @brief
绘制一个扫描窗口.
180* @param Width:
正方形的边长.
181* @param Length:边框的长度.
182* @param size:
边框的线宽.
183* @param color:扫描框的颜色.
184* @retval None
185*/
186void LCD_View_Finder_ARGB8888(
187uint16_t Width,
188uint16_t Length,
189uint16_t size ,
190uint32_t color)
191{
192//设置当前颜色
193LCD_SetTextColor_ARGB8888(color);
194//绘制矩形框
195LCD_DrawRect_ARGB8888((LCD_PIXEL_WIDTH-Width)/2,
196(LCD_PIXEL_HEIGHT-Width)/2,Width,Width-size,size);
197//设置当前颜色为透明
198LCD_SetTextColor_ARGB8888(TRANSPARENCY_ARGB8888);
199//绘制线条
200LCD_DrawLine_ARGB8888((LCD_PIXEL_WIDTH-Width)/2+Length,
201(LCD_PIXEL_HEIGHT-Width)/2,Width-2*Length,size, LCD_DIR_HORIZONTAL);
202
203LCD_DrawLine_ARGB8888((LCD_PIXEL_WIDTH-Width)/2+Length,
204(LCD_PIXEL_HEIGHT+Width)/2-size,Width-2*Length,size, LCD_DIR_HORIZONTAL);
205
206LCD_DrawLine_ARGB8888((LCD_PIXEL_WIDTH-Width)/2,
207(LCD_PIXEL_HEIGHT-Width)/2+Length,Width-2*Length,size, LCD_DIR_VERTICAL);
208
209LCD_DrawLine_ARGB8888((LCD_PIXEL_WIDTH+Width)/2,
210(LCD_PIXEL_HEIGHT-Width)/2+Length, Width-2*Length,size, LCD_DIR_VERTICAL);
211
212}
213
214/**
215* @brief
在扫描框里循环显示扫描线条.
216* @param None
217* @retval None
218*/
219void LCD_Line_Scan_ARGB8888(void)
220{
221//切换为前景层
222LCD_SetLayer(LCD_FOREGROUND_LAYER);
223//设置图形颜色为透明
224LCD_SetTextColor_ARGB8888(TRANSPARENCY_ARGB8888);
225//画一条透明颜色的线条,即清除上一次绘制的线条
226LCD_DrawLine_ARGB8888((LCD_PIXEL_WIDTH-Frame_width+8*Frame_line_size)/2,pos,
227Frame_width-8*Frame_line_size,Frame_line_size, LCD_DIR_HORIZONTAL);
228//改变线条位置
229pos=pos+Frame_line_size;
230//判断线条是否越界
231if (pos>=((LCD_PIXEL_HEIGHT+Frame_width)/2-5*Frame_line_size)) {
232pos = (LCD_PIXEL_HEIGHT-Frame_width)/2+5*Frame_line_size;
233}
234//设置图形颜色为红色
235LCD_SetTextColor_ARGB8888(0xD0FF0000);
236//绘制一条线线条
237LCD_DrawLine_ARGB8888((LCD_PIXEL_WIDTH-Frame_width+8*Frame_line_size)/2,pos,
238Frame_width-8*Frame_line_size,Frame_line_size, LCD_DIR_HORIZONTAL);
239}

通过宏定义Frame_width为扫描窗口的宽度,定义Frame_line_length为扫描线的长度,

Frame_line_size的线条的宽度。

LCD_Clear_ARGB8888为清屏函数,配置DMA2D模式为R2M,意思是寄存器到存储器,颜色为ARGB8888模式,Alpha_Value为透明度的设置参数,范围是0~255,0为全透明,255为不透明,半透明取中间值127即可。

LCD_DrawLine_ARGB8888为绘制线条函数,通过配置DMA2D来绘制,跟RGB565模式类似,主要注意每个像素的大小为4个字节。

LCD_DrawRect_ARGB8888为绘制矩形框函数,实际上就是画线,画四条线条组成一个矩形。

LCD_View_Finder_ARGB8888是绘制扫描框的函数,首先是绘制一个矩形,然后将各个边上的线的中间部分刷一遍透明色,就成了扫描框。

LCD_Line_Scan_ARGB8888为扫描线条函数,目的是为了在扫描窗口里边的从上往下画线形成扫描线的效果。需要注意的是每次画线之前先清掉上一次画的线条。

图像处理
图像处理部分已经封装到解码库里边,并预留了与之相关的接口,通过宏定义QR_FRAME_BUFFER确保图像处理的数据缓冲区有3M字节的空间。同时摄像头需要采集到图像并传递到解码库即可。其他图像数据的处理全部在解码库里边完成。

数据解码
数据解码部分已经封装到解码库里边,并预留了与之相关的接口,通过调用QR_decoder解码函数对经过图像处理的数据进行解码,返回解码成功的条码个数。并将解码结果存进decoded_buf二维数组。

串口发送结果
接下来需要配置USART1的工作模式,我们通过编写Debug_USART_Config函数完成该功能,见代码清单
244。

代码清单 474配置串口中断发送模式(bsp_debug_usart.c文件)
1#include
"./usart/bsp_debug_usart.h"
2
3unsigned int uart_data_len = 0;//串口待发送数据长度
4unsigned int uart_data_index = 0;//串口已发送数据个数
5unsigned char uart_send_state= 0;//串口状态,1表示正在发送,0表示空闲
6unsigned char uart_tx_buf[UART_MAX_BUF_SIZE] = {0};//串口发送数据缓冲区
7
8/**
9* @brief DEBUG_USART GPIO
配置,工作模式配置。115200 8-N-1
10* @param

11* @retval

12*/
13void Debug_USART_Config(void)
14{
15GPIO_InitTypeDef GPIO_InitStructure;
16USART_InitTypeDef USART_InitStructure;
17NVIC_InitTypeDef NVIC_InitStructure;
18
19RCC_AHB1PeriphClockCmd( DEBUG_USART_RX_GPIO_CLK|DEBUG_USART_TX_GPIO_CLK, ENABLE);
20
21/*
使能 UART时钟 */
22RCC_APB2PeriphClockCmd(DEBUG_USART_CLK, ENABLE);
23
24/*
连接 PXx到 USARTx_Tx*/
25GPIO_PinAFConfig(DEBUG_USART_RX_GPIO_PORT,DEBUG_USART_RX_SOURCE, DEBUG_USART_RX_AF);
26
27/*
连接 PXx到 USARTx__Rx*/
28GPIO_PinAFConfig(DEBUG_USART_TX_GPIO_PORT,DEBUG_USART_TX_SOURCE,DEBUG_USART_TX_AF);
29
30/*
配置Tx引脚为复用功能 */
31GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
32GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
33GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
34
35GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_PIN ;
36GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
37GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
38
39/*
配置Rx引脚为复用功能 */
40GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
41GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_PIN;
42GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
43
44/*
配置串DEBUG_USART模式 */
45USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
46USART_InitStructure.USART_WordLength = USART_WordLength_8b;
47USART_InitStructure.USART_StopBits = USART_StopBits_1;
48USART_InitStructure.USART_Parity = USART_Parity_No ;
49USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
50USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
51USART_Init(DEBUG_USART, &USART_InitStructure);
52USART_Cmd(DEBUG_USART, ENABLE);
53
54USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
55
56//配置USART1中断优先级
57NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
58NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
59NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
60NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
61NVIC_Init(&NVIC_InitStructure);
62
63}
64
65/**
66* @brief
获取串口发送状态
67* @param

68* @retval 1表示正在发送,0表示空闲
69*/
70uint8_t get_send_sta()
71{
72if (uart_send_state)
73return 1;
74return 0;
75}
76/**
77* @brief
将数据写入USART1发送缓冲区
78* @param dat数据指针,len数据长度
79* @retval 0表示写入成功,1表示写入失败
80*/
81uint8_t uart_send_buf(unsigned char *dat,unsigned
int len)
82{
83unsigned char addr = 0;
84
85if (uart_send_state)
86return 1;
87
88uart_data_len = len;
89uart_data_index = 0;
90uart_send_state = 1;
91
92for (; len > 0; len--)
93uart_tx_buf[addr++] = *(dat++);
94
95USART_ITConfig(USART1, USART_IT_TXE, ENABLE);
96
97return 0;
98}
99/**
100* @brief USART1发送中断响应函数
101* @param
102* @retval
103*/
104void USART1_IRQ(void)
105{
106//发送中断
107if (USART_GetITStatus(USART1, USART_IT_TXE) != RESET) {
108if (uart_data_index < uart_data_len) {
109USART_SendData(USART1, uart_tx_buf[uart_data_index++]);
110}
else {
111uart_send_state = 0;
112USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
113}
114
115USART_ClearITPendingBit(USART1, USART_IT_TXE);
116}
117}
串口的IO的配置跟之前的串口实验是一样的,这里说一下串口中断优先级的配置,首先要声明NVIC_InitStructure中断向量初始化的结构体,然后依次填入串口1的中断通道USART1_IRQn,串口1的中断抢占式优先级0,响应优先级0,并使能USART1中断通道,最后初始化这个结构体即可完串口中断优先级的配置。定义全局变量uart_send_state为串口发送状态的标志,通过get_send_sta函数获取当前的串口发送状态。uart_send_buf函数将待发送的数据写入待发送缓冲区,然后使能串口1发送中断,开始发送数据。USART1_IRQ函数是串口1的中断响应函数的回调函数,当发送数据的缓冲区非空就一直会进入中断发送数据,直到发送完毕,才将串口发送状态的标志清零,等待发送数据。

使用TIM2定时器延时
扫描二维码的时候我们需要用到蜂鸣器作为提示,蜂鸣器是有源的,给电就响掉电就不响,我们通过定时器2的计时来为蜂鸣器响的持续时间延时,TIM2的初始化,见代码清单
475。

代码清单 475使用TIM2定时器延时
1/**
2* @brief TIM2产生10ms时基初始化函数
3* @param
4* @param
5* @note
6*/
7void Time2_init()
8{
9NVIC_InitTypeDef NVIC_InitStructure;
10TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
11TIM_OCInitTypeDef TIM_OCInitStructure;
12
13RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
14NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
15NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
16NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
17NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
18NVIC_Init(&NVIC_InitStructure);
19
20TIM_TimeBaseStructure.TIM_Period = 10000; //10000us=10ms
21TIM_TimeBaseStructure.TIM_Prescaler = 90-1;
22TIM_TimeBaseStructure.TIM_ClockDivision = 0;
23TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
24TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
25
26//中断使能
27TIM_ITConfig(TIM2, TIM_IT_Update , ENABLE);
28TIM_Cmd(TIM2, ENABLE);
29}
30/**
31* @brief TIM4_IRQHandler:10ms时基中断函数
32* @param
33* @param
34* @note
35*/
36void Time2_IRQ()
37{
38static u32 BeepTime=8;
39
40if (beep_on_flag) {
41BEEP_ON;
42if ((--BeepTime) == 0) {
43BeepTime=8;
44beep_on_flag =0;
45BEEP_OFF;
46}
47}
48
}

Time2_init函数直接初始化定时器2,分频系数为90,计数周期为10000,总线频率为90MHz,中断周期为90MHz/90*10000=10000us=10ms,每10ms进入TIM2_IRQHandler中断一次,
TIM2_IRQHandler中断函数调用Time2_IRQ函数,设定延时8*10ms=80ms初始值,如果解码成功,beep_on_flag被置为1,蜂鸣器通过宏BEEP_ON来触发响声,BeepTime开始倒计时,直到BeepTime为0,重新设定延时初始值80ms,复位蜂鸣器状态标志位,关闭蜂鸣器。蜂鸣器的使用可以参考蜂鸣器的相关章节介绍。

main函数
最后我们来编写main函数,利用前面讲解的函数,扫描二维码并输出结果,见代码清单 2414。

代码清单 476 main函数
1/**
2* @brief
主函数
3* @param

4* @retval

5*/
6int main(void)
7{
8char qr_type_len=0;
9short qr_data_len=0;
10char qr_type_buf[10];
11char qr_data_buf[512];
12int addr=0;
13int i=0,j=0;
14char qr_num=0;
15/*摄像头与RGB
16LED灯共用引脚,不要同时使用LED和摄像头*/
17
18Debug_USART_Config();
19
20/*
配置SysTick为10us中断一次,
21时间到后触发定时中断,
22*进入stm32fxx_it.
23c文件的SysTick_Handler处理,通过数中断次数计时
24*/
25SysTick_Init();
26
27BEEP_GPIO_Config();
28/*初始化液晶屏*/
29LCD_Init();
30LCD_LayerInit();
31LTDC_Cmd(ENABLE);
32
33/*把背景层刷黑色*/
34LCD_SetLayer(LCD_BACKGROUND_LAYER);
35LCD_SetTransparency(0xFF);
36LCD_Clear(LCD_COLOR_BLACK);
37
38/*初始化后默认使用前景层*/
39LCD_SetLayer(LCD_FOREGROUND_LAYER);
40/*默认设置不透明 ,该函数参数为不透明度,范围
410-0xff
,0为全透明,0xff为不透明*/
42LCD_SetTransparency(0xFF);
43LCD_Clear_ARGB8888(LCD_COLOR_BLACK_ARGB8888);
44//绘制透明框
45LCD_SetTextColor_ARGB8888(TRANSPARENCY_ARGB8888);
46LCD_DrawFullRect_ARGB8888(Frame_width,Frame_width);
47//绘制扫描框
48LCD_View_Finder_ARGB8888(Frame_width,Frame_line_length,
49Frame_line_size,LCD_COLOR_GREEN_ARGB8888);
50
51CAMERA_DEBUG("STM32F429
二维码解码例程");
52
53
54/*
初始化摄像头GPIO及IIC */
55OV5640_HW_Init();
56
57/*
读取摄像头芯片ID,确定摄像头正常连接 */
58OV5640_ReadID(&OV5640_Camera_ID);
59
60if (OV5640_Camera_ID.PIDH == 0x56) {
61// sprintf((char*)dispBuf, " OV5640
摄像头,ID:0x%x",
62OV5640_Camera_ID.PIDH);
63// LCD_DisplayStringLine_EN_CH(LINE(0),(uint8_t*)dispBuf);
64CAMERA_DEBUG("%x %x",OV5640_Camera_ID.PIDH ,OV5640_Camera_ID.
65PIDL);
66
67}
else {
68LCD_SetTextColor(LCD_COLOR_RED);
69LCD_DisplayStringLine_EN_CH(LINE(0),(uint8_t*)"

70没有检测到OV5640,请重新检查
71查连接。");
72CAMERA_DEBUG("没有检测到OV5640摄像头,请重新检查连
73接。");
74
75while (1);
76}
77
78
79OV5640_Init();
80
81OV5640_RGB565Config();
82OV5640_AUTO_FOCUS();
83
84//使能DCMI采集数据
85DCMI_Cmd(ENABLE);
86DCMI_CaptureCmd(ENABLE);
87
88Time2_init();
89
90/*DMA直接传输摄像头数据到LCD屏幕显示*/
91while (1) {
92//二维码识别,返回识别条码的个数
93qr_num = QR_decoder();
94
95if (qr_num) {
96//识别成功,蜂鸣器响标志
97beep_on_flag =1;
98
99//解码的数据是按照识别条码的个数封装好的
100二维数组,这些数据需要
101//根据识别条码的个数,按组解包并通过串口
102发送到上位机串口终端
103for (i=0; i < qr_num; i++) {
104qr_type_len = qr_result_buf[i][addr++];
105//获取解码类型长度
106
107for (j=0; j < qr_type_len; j++)
108qr_type_buf[j]=qr_result_buf[i][addr++];
109//获取解码类型名称
110
111qr_data_len = qr_result_buf[i][addr++]<<8;

112//获取解码数据长度高8位
113qr_data_len |= qr_result_buf[i][addr++];

114//获取解码数据长度低8位
115
116for (j=0; j < qr_data_len; j++)
117qr_data_buf[j]=qr_result_buf[i][addr++];
118//获取解码数据
119
120uart_send_buf((unsigned char *)qr_type_buf,
121qr_type_len);//串口发送解码类型
122while (get_send_sta());//等待串口发送完毕
123uart_send_buf((unsigned char *)":",
1);
124//串口发送分隔符
125while (get_send_sta());//等待串口发送完毕
126uart_send_buf((unsigned char *)qr_data_buf,
127qr_data_len);//串口发送解码数据
128while (get_send_sta());//等待串口发送完毕
129uart_send_buf((unsigned char *)"\r\n",
2);
130//串口发送分隔符
131while (get_send_sta());//等待串口发送完毕
132addr =0;//清零
133}
134
135}
136
137}
138
139
}

在main函数中,首先初始化了串口,然后初始化系统滴答定时器,再初始化液晶屏,注意它是把摄像头使用的液晶层初始化成RGB565格式。第二层为半透明的扫描窗口,先是通过LCD_Clear_ARGB8888函数整屏填充一个半透明的矩形,然后通过LCD_DrawFullRect_ARGB8888函数在液晶的中心位置再画一个全透明的矩形,这样就显示一个扫描窗口,再用LCD_View_Finder_ARGB8888函数将二维码的扫描框画出来。扫描框的大小和颜色都是可以通过宏定义来定义。

摄像头控制部分,首先调用了OV5640_HW_Init函数初始化DCMI及I2C,然后调用OV5640_ReadID函数检测摄像头与实验板是否正常连接,若连接正常则调用OV5640_Init函数初始化DCMI的工作模式及配置DMA,再调用OV5640_RGB565Config函数向OV5640写入寄存器配置,再调用OV5640_AUTO_FOCUS函数初始化OV5640自动对焦功能,最后,一定要记住调用库函数DCMI_Cmd及DCMI_CaptureCmd函数使能DCMI开始捕获数据,这样才能正常开始工作。

使用蜂鸣器时需要初始化定时器2,用作解码成功时蜂鸣器动作持续的延时。

大循环里边直接调用QR_decoder函数来对二维码数据进行解码,返回值为解码成功的条码个数,通过二维数组保存解码结果。然后将解码结果拆包,发送解码类型和解码的数据。扫描中文二维码的时候特别注意上位机一定要支持UTF-8编码,否则输出结果会乱码。

最后特别注意,这个解码库消耗的堆栈比较大,我们需要调大堆栈的大小保证程序能正常稳定运行。

3. 下载验证
把OV5640接到实验板的摄像头接口中,用USB线连接开发板,编译程序下载到实验板,并上电复位,打开串口终端助手,液晶屏会显示摄像头扫描框,对准二维码扫描即可把扫描结果发送到串口终端。

47.6 每课一问

1. 为什么液晶屏扫描框里的循环扫描的线条一定要放在场中断里边进行?
2. 尝试多个二维码放在一起扫描,观察实验现象。
3. 尝试修改例程中的Frame_width,Frame_line_length和Frame_line_size变量,观察实验现象。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  stm32 QRCODE