50. QR-Decoder-OV5640二维码识别

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

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

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

50.1. 二维码简介

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

50.2. 二维条形码类型

50.2.1. 矩阵式二维条码

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

矩阵式二维码

50.2.2. 行排列式二维条码

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

行排列式二维条码

50.3. 二维条形码的优点

  1. 可靠性强,条形码的读取准确率远远超过人工记录,平均每15000个字符才会出现一个错误。

  2. 效率高,条形码的读取速度很快,相当于每秒40个字符。

  3. 成本低,与其它自动化识别技术相比较,条形码技术仅仅需要一小张贴纸和相对构造简单的光学扫描仪,成本相当低廉。

  4. 易于制作,条形码制作:条形码的编写很简单,制作也仅仅需要印刷,被称作为“可印刷的计算机语言”。

  5. 构造简单,条形码识别设备的构造简单,使用方便。

  6. 灵活实用,条形码符号可以手工键盘输入,也可以和有关设备组成识别系统实现自动化识别,还可和其他控制设备联系起来实现整个系统的自动化管理。

  7. 高密度,二维条码通过利用垂直方向的堆积来提高条码的信息密度,而且采用高密度图形表示,因此不需事先建立数据库,真正实现了用条码对信息的直接描述。

  8. 纠错功能,二维条形码不仅能防止错误,而且能纠正错误,即使条形码部分损坏,也能将正确的信息还原出来。

  9. 多语言形式、可表示图像,二维条码具有字节表示模式,即提供了一种表示字节流的机制。不论何种语言文字它们在计算机中存储时以机内码的形式表现,而内部码都是字节码,可识别多种语言文字的条码。

  10. 具有加密机制,可以先用一定的加密算法将信息加密,再用二维条码表示。在识别二维条码时,再加以一定的解密算法,便可以恢复所表示的信息。

50.4. QR二维码的编码及识别

50.4.1. QR码基本结构

QR码基本结构,见QR码基本结构

  1. 位置探测图形、位置探测图形分隔符、定位图形:用于对二维码的定位,对每个QR码来说,位置都是固定存在的,只是大小规格会有所差异。

  2. 校正图形:规格确定,校正图形的数量和位置也就确定了。

  3. 格式信息:表示改二维码的纠错级别,分为L、M、Q、H。

  4. 版本信息:即二维码的规格,QR码符号共有40种规格的矩阵(一般为黑白色),从21x21(版本1),到177x177(版本40),每一版本符号比前一版本 每边增加4个模块。

  5. 数据和纠错码字:实际保存的二维码信息,和纠错码字(用于修正二维码损坏带来的错误)。

QR码基本结构

50.4.2. QR码编码过程

1. 数据分析:确定编码的字符类型,按相应的字符集转换成符号字符; 选择纠错等级,在规格一定的条件下,纠错等级越高其真实数据的容量越小。

2. 数据编码:将数据字符转换为位流,每8位一个码字,整体构成一个数据的码字序列。其实知道这个数据码字序列就知道了二维码的数据内容。 见表 QR码数据容量 和表 QR数据模式指示符

QR码数据容量 QR数据模式指示符

3. 编码过程:数据可以按照一种模式进行编码,以便进行更高效的解码, 例如:对数据:01234567编码(版本1-H)。

  1. 分组:012 345 67

  2. 转成二进制:

012 → 0000001100

345 → 0101011001

67 → 1000011

  1. 转成序列:0000001100 0101011001 1000011

  2. 字符数转成二进制:8 → 0000001000

  3. 加入模式指示符:

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. 构造矩阵:将探测图形、分隔符、定位图形、 校正图形和码字模块放入矩阵中。把上面的完整序列填充到相应规格的二维码矩阵的区域中,见图 构造矩阵 构造矩阵。

构造矩阵

7. 掩摸:将掩摸图形用于符号的编码区域, 使得二维码图形中的深色和浅色(黑色和白色)区域能够比率最优的分布。见图 构造矩阵 构造矩阵。

8. 格式和版本信息:生成格式和版本信息放入相应区域内。版本7-40都包含了版本信息,没有版本信息的全为0。 二维码上两个位置包含了版本信息,它们是冗余的。版本信息共18位,6X3的矩阵,其中6位是数据位,如版本号8,数据位的信息时 001000,后面的12位是纠错位。

50.4.3. QR码识别过程

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

  1. 条码的定位就是找到条码符号的图像区域,对有明显条码特征的区域进行定位。然后根据不同条码的定位图形结构特征对不同的条码符号进行下一步的处理。

  2. 实现条码的定位,采用以下步骤:

  1. 利用点运算的阈值理论将采集到的图象变为二值图像, 即对图像进行二值化处理;

  2. 得到二值化图像后,对其进行膨胀运算;

  3. 对膨胀后的图象进行边缘检测得到条码区域的轮廓;

图图像处理 是经过上述处理后得到的一系列图像。

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

../_images/QR007.png

其中,f(x,y)是点(x,y)处像素的灰度值,T为阈值(自适应门限)。找到条码区域后, 我们还要进一步区分到底是哪种矩阵式条码。下面图形是几种常见的矩阵式条码:

  1. 位于左上角、左下角、右上角的三个定位图形

  2. 位于符号中央的三个等间距同心圆环(或称公牛眼定位图形)

  3. 位于左边和下边的两条垂直的实线段

图像处理
  1. 条码的分割

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

  1. 译码

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

50.5. QR-Decoder-OV564摄像头实验

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

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

OV5640识别二维码过程

50.5.1. QR-Code解码库特点

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

  • 条码种类: 支持常用QR-Code、EAN、UPC

  • 扫描速度: 400 毫秒

  • 扫描英文: 250 个字符

  • 扫描中文: 90中文字符,UTF-8编码格式(需上位机支持)

  • 多码扫描: 支持多个二维码同时解码,同时输出结果

50.5.2. 软件设计

50.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二维数组的数据,通过串口发送到电脑上位机。

50.5.2.2. 代码分析

QR-Code解码库相关宏定义

我们把QR-Code解码库相关的配置都以宏的形式定义到 “qr_decoder_user.h”文件中,其中包括数据缓冲基地址、扫描窗大小、扫描框线条大小、 解码结果二维数组、扫描二维码的函数,见 代码清单:QR_Code-1

代码清单:QR_Code-1 QR-Code解码库配置相关的宏
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#ifndef  __QR_DECODER_USER_H
#define  __QR_DECODER_USER_H

#include "qr_decoder.h"
#include <stdio.h>

// 开辟SDRAM的3M字节作为数据缓存,这里使用显存以外的空间,
// 0xD0800000-0x300000 = 0xD0500000
#define  QR_FRAME_BUFFER  ((uint32_t)0xD0500000)

/*扫描窗口参数*/
#define  Frame_width           ((uint16_t)320)//扫描窗口边长(正方形)

/*扫描框线条参数*/
#define  Frame_line_length     ((uint16_t)30) //扫描框线条长度
#define  Frame_line_size       ((uint16_t)3)  //扫描框线条宽度

#define  QR_SYMBOL_NUM  5    //识别二维码的最大个数
#define  QR_SYMBOL_SIZE 512  //每组二维码的的最大容量

//解码数据封装为二维数组decoded_buf,格式为:
// (第一组:解码类型长度(8bit)+解码类型名称+解码数据长度(16bit,高位在前低位在后)+ 解码数据)

// (第二组:解码类型长度(8bit)+解码类型名称+解码数据长度(16bit,高位在前低位在后)+ 解码数据)

//  。。。
//以此类推
extern char decoded_buf[QR_SYMBOL_NUM][QR_SYMBOL_SIZE];

//解码函数,返回值为识别条码的个数
char QR_decoder(void);

//获取一帧图像
void get_image(uint32_t src_addr,uint16_t img_width,uint16_t img_height);

#endif /* __QR_DECODER_USER_H */

以上代码首先定义一个3M字节的空间用作解码库的数据的缓冲,只需要定义SDRAM的空闲空间的基地址;然后定义扫描二维码的窗口及框体大小, 范围由100~480(图像不能太小,否则图像很难识别);定义decoded_buf[QR_SYMBOL_NUM][QR_SYMBOL_SIZE]二维数组存放解码的结果, 存放解码的最大个数由QR_SYMBOL_NUM决定,存放解码的最大数据量由QR_SYMBOL_SIZE决定,没有特殊要求就不需要做变动; 存放数据的格式介绍如下表 二维数组数据格式

二维数组数据格式

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

图像采集

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

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

}

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

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

液晶驱动

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

代码清单:QR_Code-3 配置DMA数据传输(bsp_ov5640.c文件)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/*扫描窗口参数*/
#define  Frame_width           ((uint16_t)320)//扫描窗口边长(正方形)

/*扫描框线条参数*/
#define  Frame_line_length     ((uint16_t)30) //扫描框线条长度
#define  Frame_line_size       ((uint16_t)3)  //扫描框线条宽度

//指定扫描窗口里边扫描线的初始位置
int pos=(LCD_PIXEL_HEIGHT-Frame_width)/2+5*Frame_line_size;
/**
* @brief  清屏
* @param  Color: 清屏颜色
* @retval None
*/
void LCD_Clear_ARGB8888(uint32_t Color)
{
    DMA2D_InitTypeDef      DMA2D_InitStruct;

    uint16_t Alpha_Value=0,Red_Value = 0, Green_Value = 0, Blue_Value = 0;

    Alpha_Value = (0xFF000000&Color)>>24;
    Red_Value = (0x00FF0000 & Color) >> 16;
    Blue_Value = 0x000000FF & Color;
    Green_Value = (0x0000FF00 & Color) >> 8;

    /* configure DMA2D */
    DMA2D_DeInit();
    DMA2D_InitStruct.DMA2D_Mode = DMA2D_R2M;
    DMA2D_InitStruct.DMA2D_CMode = DMA2D_ARGB8888;
    DMA2D_InitStruct.DMA2D_OutputGreen = Green_Value;
    DMA2D_InitStruct.DMA2D_OutputBlue = Blue_Value;
    DMA2D_InitStruct.DMA2D_OutputRed = Red_Value;
    DMA2D_InitStruct.DMA2D_OutputAlpha = Alpha_Value; //设置透明度
    DMA2D_InitStruct.DMA2D_OutputMemoryAdd = CurrentFrameBuffer;
    DMA2D_InitStruct.DMA2D_OutputOffset = 0;
    DMA2D_InitStruct.DMA2D_NumberOfLine = LCD_PIXEL_HEIGHT;
    DMA2D_InitStruct.DMA2D_PixelPerLine = LCD_PIXEL_WIDTH;
    DMA2D_Init(&DMA2D_InitStruct);

    /* Start Transfer */
    DMA2D_StartTransfer();

    /* Wait for CTC Flag activation */
    while (DMA2D_GetFlagStatus(DMA2D_FLAG_TC) == RESET) {
    }
}
/**
* @brief 绘制一条线条
* @param Xpos: 起点X轴坐标,范围0 到800
* @param Ypos: 起点Y轴坐标,范围0 到480
* @param Length: 线条长度
* @param Line_width: 线条宽度
* @param Direction:  线条方向(水平或者垂直).
* @retval None
*/
void LCD_DrawLine_ARGB8888(
    uint16_t Xpos,
    uint16_t Ypos,
    uint16_t Length,
    uint8_t Line_width,
    uint8_t Direction)
{
    DMA2D_InitTypeDef      DMA2D_InitStruct;

    uint32_t  Xaddress = 0;
    uint16_t Alpha_Value=0,Red_Value = 0, Green_Value = 0, Blue_Value = 0;
    //提取各通道的颜色值
    Alpha_Value = (0xFF000000&CurrentTextColor_ARGB8888)>>24;
    Red_Value = (0x00FF0000 & CurrentTextColor_ARGB8888) >> 16;
    Blue_Value = 0x000000FF & CurrentTextColor_ARGB8888;
    Green_Value = (0x0000FF00 & CurrentTextColor_ARGB8888) >> 8;
    //指定绘制的首地址
    Xaddress = CurrentFrameBuffer + 4*(LCD_PIXEL_WIDTH*Ypos + Xpos);

    //配置 DMA2D
    DMA2D_DeInit();
    DMA2D_InitStruct.DMA2D_Mode = DMA2D_R2M;
    DMA2D_InitStruct.DMA2D_CMode = DMA2D_ARGB8888;
    DMA2D_InitStruct.DMA2D_OutputGreen = Green_Value;
    DMA2D_InitStruct.DMA2D_OutputBlue = Blue_Value;
    DMA2D_InitStruct.DMA2D_OutputRed = Red_Value;
    DMA2D_InitStruct.DMA2D_OutputAlpha = Alpha_Value;
    DMA2D_InitStruct.DMA2D_OutputMemoryAdd = Xaddress;
    //水平方向
    if (Direction == LCD_DIR_HORIZONTAL) {
        DMA2D_InitStruct.DMA2D_OutputOffset = LCD_PIXEL_WIDTH-Length;
        DMA2D_InitStruct.DMA2D_NumberOfLine = Line_width;
        DMA2D_InitStruct.DMA2D_PixelPerLine = Length;
    } else { //垂直方向
        DMA2D_InitStruct.DMA2D_OutputOffset = LCD_PIXEL_WIDTH - Line_width;
        DMA2D_InitStruct.DMA2D_NumberOfLine = Length;
        DMA2D_InitStruct.DMA2D_PixelPerLine = Line_width;
    }

    DMA2D_Init(&DMA2D_InitStruct);
    // 开始传输
    DMA2D_StartTransfer();
    //等待传输完成
    while (DMA2D_GetFlagStatus(DMA2D_FLAG_TC) == RESET) {
    }

}
/**
* @brief  绘制一个矩形线条框.
* @param  Xpos:  X起始位置, 范围 0 —— 800
* @param  Ypos:  Y起始位置,范围 0 —— 480
* @param  Width: 显示线条的宽度度, 范围 0 —— 800
* @param  Height:显示线条的高度, 范围 0 —— 480
* @param  Line_width:显示线条的宽度
* @retval None
*/
void LCD_DrawRect_ARGB8888(
    uint16_t Xpos,
    uint16_t Ypos,
    uint16_t Width,
    uint16_t Height,
    uint8_t Line_width)
{
    //绘制水平方向的线条
    LCD_DrawLine_ARGB8888(Xpos, Ypos, Width,Line_width ,LCD_DIR_HORIZONTAL);
    LCD_DrawLine_ARGB8888(Xpos, (Ypos+ Height), Width+Line_width,
                        Line_width ,LCD_DIR_HORIZONTAL);

    //绘制垂直方向的线条
    LCD_DrawLine_ARGB8888(Xpos, Ypos, Height, Line_width ,LCD_DIR_VERTICAL);
    LCD_DrawLine_ARGB8888((Xpos + Width), Ypos, Height, Line_width ,LCD_DIR_VERTICAL);
}


/**
* @brief  在显示区域中心绘制一个矩形.
* @param  Width: 显示图像的宽度, 范围 0 —— 800
* @param  Height:显示图像的高度, 范围 0 —— 480
* @retval None
*/
void LCD_DrawFullRect_ARGB8888(uint16_t Width, uint16_t Height)
{
    DMA2D_InitTypeDef      DMA2D_InitStruct;

    uint32_t  Xaddress = 0;
    uint16_t Alpha_Value=0,Red_Value = 0, Green_Value = 0, Blue_Value = 0;

    //提取各通道的颜色值
    Alpha_Value = (0xFF000000&CurrentTextColor_ARGB8888)>>24;
    Red_Value = (0x00FF0000 & CurrentTextColor_ARGB8888) >> 16;
    Blue_Value = 0x000000FF & CurrentTextColor_ARGB8888;
    Green_Value = (0x0000FF00 & CurrentTextColor_ARGB8888) >> 8;

    //指定绘制的首地址
    Xaddress = CurrentFrameBuffer +
                4*(LCD_PIXEL_WIDTH*(LCD_PIXEL_HEIGHT-Height)/2 +
                (LCD_PIXEL_WIDTH-Width)/2);

    //配置 DMA2D
    DMA2D_DeInit();
    DMA2D_InitStruct.DMA2D_Mode = DMA2D_R2M;
    DMA2D_InitStruct.DMA2D_CMode = DMA2D_ARGB8888;
    DMA2D_InitStruct.DMA2D_OutputGreen = Green_Value;
    DMA2D_InitStruct.DMA2D_OutputBlue = Blue_Value;
    DMA2D_InitStruct.DMA2D_OutputRed = Red_Value;
    DMA2D_InitStruct.DMA2D_OutputAlpha = Alpha_Value;
    DMA2D_InitStruct.DMA2D_OutputMemoryAdd = Xaddress;
    DMA2D_InitStruct.DMA2D_OutputOffset = (LCD_PIXEL_WIDTH - Width);
    DMA2D_InitStruct.DMA2D_NumberOfLine = Height;
    DMA2D_InitStruct.DMA2D_PixelPerLine = Width;
    DMA2D_Init(&DMA2D_InitStruct);

    //开始传输
    DMA2D_StartTransfer();

    //等待传输完成
    while (DMA2D_GetFlagStatus(DMA2D_FLAG_TC) == RESET) {
    }

    LCD_SetTextColor(CurrentTextColor);
}

/**
* @brief  绘制一个扫描窗口.
* @param  Width: 正方形的边长.
* @param  Length:边框的长度.
* @param  size: 边框的线宽.
* @param  color:扫描框的颜色.
* @retval None
*/
void LCD_View_Finder_ARGB8888(
    uint16_t Width,
    uint16_t Length,
    uint16_t size ,
    uint32_t color)
{
    //设置当前颜色
    LCD_SetTextColor_ARGB8888(color);
    //绘制矩形框
    LCD_DrawRect_ARGB8888((LCD_PIXEL_WIDTH-Width)/2,
                (LCD_PIXEL_HEIGHT-Width)/2,Width,Width-size,size);
    //设置当前颜色为透明
    LCD_SetTextColor_ARGB8888(TRANSPARENCY_ARGB8888);
    //绘制线条
    LCD_DrawLine_ARGB8888((LCD_PIXEL_WIDTH-Width)/2+Length,
        (LCD_PIXEL_HEIGHT-Width)/2,Width-2*Length,size, LCD_DIR_HORIZONTAL);

    LCD_DrawLine_ARGB8888((LCD_PIXEL_WIDTH-Width)/2+Length,
    (LCD_PIXEL_HEIGHT+Width)/2-size,Width-2*Length,size, LCD_DIR_HORIZONTAL);

    LCD_DrawLine_ARGB8888((LCD_PIXEL_WIDTH-Width)/2,
    (LCD_PIXEL_HEIGHT-Width)/2+Length,Width-2*Length,size, LCD_DIR_VERTICAL);

    LCD_DrawLine_ARGB8888((LCD_PIXEL_WIDTH+Width)/2,
    (LCD_PIXEL_HEIGHT-Width)/2+Length, Width-2*Length,size, LCD_DIR_VERTICAL);

}

/**
* @brief  在扫描框里循环显示扫描线条.
* @param  None
* @retval None
*/
void LCD_Line_Scan_ARGB8888(void)
{
    //切换为前景层
    LCD_SetLayer(LCD_FOREGROUND_LAYER);
    //设置图形颜色为透明
    LCD_SetTextColor_ARGB8888(TRANSPARENCY_ARGB8888);
    //画一条透明颜色的线条,即清除上一次绘制的线条
    LCD_DrawLine_ARGB8888((LCD_PIXEL_WIDTH-Frame_width+8*Frame_line_size)/2,pos,
        Frame_width-8*Frame_line_size,Frame_line_size, LCD_DIR_HORIZONTAL);
    //改变线条位置
    pos=pos+Frame_line_size;
    //判断线条是否越界
    if (pos>=((LCD_PIXEL_HEIGHT+Frame_width)/2-5*Frame_line_size)) {
        pos = (LCD_PIXEL_HEIGHT-Frame_width)/2+5*Frame_line_size;
    }
    //设置图形颜色为红色
    LCD_SetTextColor_ARGB8888(0xD0FF0000);
    //绘制一条线线条
    LCD_DrawLine_ARGB8888((LCD_PIXEL_WIDTH-Frame_width+8*Frame_line_size)/2,pos,
            Frame_width-8*Frame_line_size,Frame_line_size, LCD_DIR_HORIZONTAL);
}

通过宏定义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函数完成该功能,见 代码清单:QR_Code-4

代码清单:QR_Code-4 配置串口中断发送模式(bsp_debug_usart.c文件)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#include "./usart/bsp_debug_usart.h"

unsigned int  uart_data_len = 0;     //串口待发送数据长度
unsigned int  uart_data_index = 0;   //串口已发送数据个数
unsigned char uart_send_state= 0; //串口状态,1表示正在发送,0表示空闲
unsigned char uart_tx_buf[UART_MAX_BUF_SIZE] = {0};//串口发送数据缓冲区

/**
* @brief  DEBUG_USART GPIO 配置,工作模式配置。115200 8-N-1
* @param  无
* @retval 无
*/
void Debug_USART_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_AHB1PeriphClockCmd( DEBUG_USART_RX_GPIO_CLK|DEBUG_USART_TX_GPIO_CLK, ENABLE);

    /* 使能 UART 时钟 */
    RCC_APB2PeriphClockCmd(DEBUG_USART_CLK, ENABLE);

    /* 连接 PXx 到 USARTx_Tx*/
    GPIO_PinAFConfig(DEBUG_USART_RX_GPIO_PORT,DEBUG_USART_RX_SOURCE, DEBUG_USART_RX_AF);

    /*  连接 PXx 到 USARTx__Rx*/
    GPIO_PinAFConfig(DEBUG_USART_TX_GPIO_PORT,DEBUG_USART_TX_SOURCE,DEBUG_USART_TX_AF);

    /* 配置Tx引脚为复用功能  */
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;

    GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_PIN  ;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);

    /* 配置Rx引脚为复用功能 */
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_PIN;
    GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);

    /* 配置串DEBUG_USART 模式 */
    USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No ;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    USART_Init(DEBUG_USART, &USART_InitStructure);
    USART_Cmd(DEBUG_USART, ENABLE);

    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

    //配置USART1中断优先级
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

}

/**
* @brief  获取串口发送状态
* @param  无
* @retval 1表示正在发送,0表示空闲
*/
uint8_t get_send_sta()
{
    if (uart_send_state)
        return 1;
    return 0;
}
/**
* @brief  将数据写入USART1发送缓冲区
* @param  dat数据指针,len数据长度
* @retval 0表示写入成功,1表示写入失败
*/
uint8_t uart_send_buf(unsigned char *dat, unsigned int len)
{
    unsigned char addr = 0;

    if (uart_send_state)
        return 1;

    uart_data_len = len;
    uart_data_index = 0;
    uart_send_state = 1;

    for (; len > 0; len--)
        uart_tx_buf[addr++] = *(dat++);

    USART_ITConfig(USART1, USART_IT_TXE, ENABLE);

    return 0;
}
/**
* @brief  USART1发送中断响应函数
* @param
* @retval
*/
void USART1_IRQ(void)
{
    //发送中断
    if (USART_GetITStatus(USART1, USART_IT_TXE) != RESET) {
        if (uart_data_index < uart_data_len) {
            USART_SendData(USART1, uart_tx_buf[uart_data_index++]);
        } else {
            uart_send_state = 0;
            USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
        }

        USART_ClearITPendingBit(USART1, USART_IT_TXE);
    }
}

串口的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的初始化,见 代码清单:QR_Code-5

代码清单:QR_Code-5 使用TIM2定时器延时
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* @brief  TIM2产生10ms时基初始化函数
* @param
* @param
* @note
*/
void Time2_init()
{
    NVIC_InitTypeDef NVIC_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    TIM_TimeBaseStructure.TIM_Period = 10000;       //10000us=10ms
    TIM_TimeBaseStructure.TIM_Prescaler = 90-1;
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    //中断使能
    TIM_ITConfig(TIM2, TIM_IT_Update , ENABLE);
    TIM_Cmd(TIM2, ENABLE);
}
/**
* @brief  TIM4_IRQHandler:10ms时基中断函数
* @param
* @param
* @note
*/
void Time2_IRQ()
{
    static u32 BeepTime=8;

    if (beep_on_flag) {
        BEEP_ON;
        if ((--BeepTime) == 0) {
            BeepTime=8;
            beep_on_flag =0;
            BEEP_OFF;
        }
    }
}

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函数,利用前面讲解的函数,扫描二维码并输出结果,见 代码清单:QR_Code-6

代码清单:QR_Code-6 main函数
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
* @brief  主函数
* @param  无
* @retval 无
*/
int main(void)
{
    char  qr_type_len=0;
    short qr_data_len=0;
    char qr_type_buf[10];
    char qr_data_buf[512];
    int addr=0;
    int i=0,j=0;
    char qr_num=0;
    /*摄像头与RGBLED灯共用引脚,不要同时使用LED和摄像头*/

    Debug_USART_Config();

    /* 配置SysTick 为10us中断一次,时间到后触发定时中断,*进入stm32fxx_it.
    c文件的SysTick_Handler处理,通过数中断次数计时*/

    SysTick_Init();

    BEEP_GPIO_Config();
    /*初始化液晶屏*/
    LCD_Init();
    LCD_LayerInit();
    LTDC_Cmd(ENABLE);

    /*把背景层刷黑色*/
    LCD_SetLayer(LCD_BACKGROUND_LAYER);
    LCD_SetTransparency(0xFF);
    LCD_Clear(LCD_COLOR_BLACK);

    /*初始化后默认使用前景层*/
    LCD_SetLayer(LCD_FOREGROUND_LAYER);
    /*默认设置不透明       ,该函数参数为不透明度,范围0-0xff ,0为全透明,0xff为不透明*/
    LCD_SetTransparency(0xFF);
    LCD_Clear_ARGB8888(LCD_COLOR_BLACK_ARGB8888);
    //绘制透明框
    LCD_SetTextColor_ARGB8888(TRANSPARENCY_ARGB8888);
    LCD_DrawFullRect_ARGB8888(Frame_width,Frame_width);
    //绘制扫描框
    LCD_View_Finder_ARGB8888(Frame_width,Frame_line_length,
                            Frame_line_size,LCD_COLOR_GREEN_ARGB8888);

    CAMERA_DEBUG("STM32F429 二维码解码例程");


    /* 初始化摄像头GPIO及IIC */
    OV5640_HW_Init();

    /* 读取摄像头芯片ID,确定摄像头正常连接 */
    OV5640_ReadID(&OV5640_Camera_ID);

    if (OV5640_Camera_ID.PIDH  == 0x56) {
    //sprintf((char*)dispBuf, "OV5640 摄像头,ID:0x%x",OV5640_Camera_ID.PIDH);
    //LCD_DisplayStringLine_EN_CH(LINE(0),(uint8_t*)dispBuf);
        CAMERA_DEBUG("%x %x",OV5640_Camera_ID.PIDH ,OV5640_Camera_ID.PIDL);

    } else {
        LCD_SetTextColor(LCD_COLOR_RED);
        LCD_DisplayStringLine_EN_CH(LINE(0),(uint8_t*) "没有检测到OV5640,请重新检查连接。");
        CAMERA_DEBUG("没有检测到OV5640摄像头,请重新检查连接。");

        while (1);
    }


    OV5640_Init();

    OV5640_RGB565Config();
    OV5640_AUTO_FOCUS();

    //使能DCMI采集数据
    DCMI_Cmd(ENABLE);
    DCMI_CaptureCmd(ENABLE);

    Time2_init();

    /*DMA直接传输摄像头数据到LCD屏幕显示*/
    while (1) {
        //二维码识别,返回识别条码的个数
        qr_num = QR_decoder();

        if (qr_num) {
            //识别成功,蜂鸣器响标志
            beep_on_flag =1;

            //解码的数据是按照识别条码的个数封装好的二维数组,这些数据需要
            //根据识别条码的个数,按组解包并通过串口发送到上位机串口终端
            for (i=0; i < qr_num; i++) {
                qr_type_len = qr_result_buf[i][addr++];
                            //获取解码类型长度

                for (j=0; j < qr_type_len; j++)
                    qr_type_buf[j]=qr_result_buf[i][addr++];
                                    //获取解码类型名称

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

                for (j=0; j < qr_data_len; j++)
                    qr_data_buf[j]=qr_result_buf[i][addr++];
                                    //获取解码数据

                uart_send_buf((unsigned char *)qr_type_buf,
                            qr_type_len);//串口发送解码类型
                while (get_send_sta()); //等待串口发送完毕
                uart_send_buf((unsigned char *)":", 1);
                            //串口发送分隔符
                while (get_send_sta()); //等待串口发送完毕
                uart_send_buf((unsigned char *)qr_data_buf,
                            qr_data_len);//串口发送解码数据
                while (get_send_sta()); //等待串口发送完毕
                uart_send_buf((unsigned char *)"\r\n", 2);
                            //串口发送分隔符
                while (get_send_sta()); //等待串口发送完毕
                addr =0;//清零
            }

        }

    }

}

在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编码,否则输出结果会乱码。

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

50.5.2.3. 下载验证

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