- 此系列文章源于SYSU 2024电信学院通信工程专业工训课的训练题目,同时也是2024电子设计校内赛的题目。题目的要求描述如下:
无线手持二维码识别器是一个通过图像识别技术,对二维码图片进行识别,并解析出二维码的内容数据,并通过无线传输给手机或电脑的一种设备,通过这个设备,可以快速对物品进行扫描并在电脑端进行归档。要求能够对二维码图片进行扫描,二维码可自行生成,源信息包含字母和数字,能够支持将扫描数据上传到电脑,并在电脑端设计上位机进行数据显示,能够支持识别特殊二维码时,进行报警鸣叫处理,同时要使用3D建模软件对识别器进行建模并制作,大小符合手持。- 项目使用的主控为立创梁山派GD32F470ZGT6,项目源码存放地址为https://github.com/liangbm3/GD32_QR_Decoder
- 此系列文章更多的是对开发过程的记录和思路的呈现,文章分则可以作为教程实现单个功能,合则可以作为复现整个项目的教程。
- 文章的撰写部分参考了立创官方文档
- 文章未经作者许可,不得转载。
文章目录
前言
由于项目需要通过摄像头获取图像输入,因此需要连接摄像头。这里我们选用的是OV2640,参考商家给出的资料,将其移植到梁山派中。
一、OV2640介绍
1.简介
OV2640 是 OV(OmniVision)公司生产的一颗 1/4 寸的 CMOS UXGA(1632*1232)图像传感器。该传感器体积小、工作电压低,提供单片 UXGA 摄像头和影像处理器的所有功能。通过 SCCB 总线控制,可以输出整帧、子采样、缩放和取窗口等方式的各种分辨率 8/10位影像数据。该产品 UXGA 图像最高达到 15 帧/秒(SVGA 可达 30 帧, CIF 可达 60 帧)。
用户可以完全控制图像质量、数据格式和传输方式。所有图像处理功能过程包括伽玛曲线、白平衡、对比度、色度等都可以通过 SCCB 接口编程。 OmmiVision 图像传感器应用独有的传感器技术,通过减少或消除光学或电子缺陷如固定图案噪声、拖尾、浮散等,提高图像质量,得到清晰的稳定的彩色图像。
OV2640 的特点有:
- 高灵敏度、低电压适合嵌入式应用
- 标准的 SCCB 接口,兼容 IIC 接口
- 支持 RawRGB、 RGB(RGB565/RGB555)、 GRB422、 YUV(422/420)和 YCbCr(422)
输出格式 - 支持 UXGA、 SXGA、 SVGA 以及按比例缩小到从 SXGA 到 40*30 的任何尺寸
- 支持自动曝光控制、自动增益控制、自动白平衡、自动消除灯光条纹、自动黑电平
校准等自动控制功能。同时支持色饱和度、色相、伽马、锐度等设置。 - 支持闪光灯
- 支持图像缩放、平移和窗口设置
- 支持图像压缩,即可输出 JPEG 图像数据
- 自带嵌入式微处理器
2.引脚定义
选用的OV2640摄像头模板如图
OV2640摄像头模板的引脚定义如下图
3.串行摄像头控制总线(SCCB)简介
ATK-OV2640 摄像头模块的所有配置,都是通过 SCCB 总线来实现的, SCCB 全称是:
Seril Camera Control Bus 即串行摄像头控制总线, 它由两条数据线组成:一个是用于传输时钟信号的 SIO_C(即 OV_SCL),另一个是用于传输数据信号的 SIO_D(即 OV_SDA)。
SCCB 的传输协议与 IIC 协议极其相似,只不过 IIC 在每传输完一个字节后,接收数
据的一方要发送一位的确认数据,而 SCCB 一次要传输 9 位数据,前 8 位为有用数据,而第9 位数据在写周期中是 don’t care 位(即不必关心位),在读周期中是 NA 位。 SCCB 定义数据传输的基本单元为相(phase),即一个相传输一个字节数据。
SCCB 只包括三种传输周期,即 3 相写传输周期(三个相依次为设备从地址,内存地址,
所写数据), 2 相写传输周期(两个相依次为设备从地址,内存地址)和 2 相读传输周期(两个相依次为设备从地址,所读数据)。当需要写操作时,应用 3 相写传输周期,当需要读操作时,依次应用 2 相写传输周期和 2 相读传输周期。
3相写传输周期示意图如下
2相写传输周期示意图如下
2相读传输周期如下
SCCB传输信号的时序图
其中SCCB_E没有引出,这个信号为1的时候,说明设备时空闲的。
SIO_C相当于IIC中的SCL
SIO_D相当于IIC中的SDA
1.传输起始信号:SCL在高电平的状态下,SDA的电平由高转低,表示开始一次通信。
2.传输停止信号:SCL在高电平的状态下,SDA的电平由低转高,表示结束这次通信。主设备在发送停止信号后不能再向从设备发送任何数据,除非再次发送起始信号。
4.OV2640的时序介绍
(1)行输出时序
从上图可以看出,图像数据在 HREF 为高的时候输出,当 HREF 变高后,每一个 PCLK
时钟,输出一个 8 位/10 位数据。我们采用 8 位接口,所以每个 PCLK 输出 1 个字节,且在
RGB/YUV 输出格式下,每个 tp=2 个 Tpclk,如果是 Raw 格式,则一个 tp=1 个 Tpclk。比如我们采用 UXGA 时序, RGB565 格式输出,每 2 个字节组成一个像素的颜色(高低字节顺序可通过 0XDA 寄存器设置),这样每行输出总共有 16002 个 PCLK 周期,输出 16002 个字节。
(2)帧输出时序(UXGA模式)
上图清楚的表示了 OV2640 在 UXGA 模式下的数据输出。我们按照这个时序去读取
OV2640 的数据,就可以得到图像数据。其他分辨率的输出时序大同小异。
注意:选用的摄像头模板并没有引出VSYNC行同步信号,直接用HREF信号做同步即可
二、DCI介绍
1.简介
DCI是GD32中数组摄像头接口的简称,相当于stm32中的DCIM。数字摄像头接口是一个同步并行接口,可以从数字摄像头捕获视频和图像信息。它支持不同的颜色空间图像,例如YUV/RGB,另外支持压缩数据的JPEG格式图像。
2.结构框图
由于摄像头时钟信号和MCU的时钟频率并不相同,通过DCI外设可以很好地缓冲数据,并且可以借助DMA进行数据搬运。
三、SCCB的移植
由于嘉立创官方移植手册中已经做出移植,这里给出移植过程
1.建立工程,添加文件、include路径
1.在HardWare中建立OV2640文件夹,添加如下文件
2.将文件添加至工程
3.添加include路径
3.更改相关函数
1.我们使用的是软件IIC,因此无需考虑IO口的选择,这里我们选择PB7作为SDA,SCL作为PB6
#define RCU_OV2640_SDA RCU_GPIOB
#define PORT_OV2640_SDA GPIOB
#define GPIO_OV2640_SDA GPIO_PIN_7
#define RCU_OV2640_SCL RCU_GPIOB
#define PORT_OV2640_SCL GPIOB
#define GPIO_OV2640_SCL GPIO_PIN_6
2.原stm32代码中设置SDA的输入输出模式和IO操作函数如图
我们把他改为
//SDA输出模式
#define OV2640_SDA_MODE_OUT() gpio_mode_set(PORT_OV2640_SDA, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_OV2640_SDA)
#define OV2640_SDA_MODE_IN() gpio_mode_set(PORT_OV2640_SDA, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_OV2640_SDA)
//获取SDA引脚变化
#define SDA_GET() gpio_input_bit_get(PORT_OV2640_SDA,GPIO_OV2640_SDA)
//SDA和SCL输出
#define SDA(x) gpio_bit_write(PORT_OV2640_SDA,GPIO_OV2640_SDA, (x?SET:RESET))
#define SCL(x) gpio_bit_write(PORT_OV2640_SCL,GPIO_OV2640_SCL, (x?SET:RESET))
3.原stm32代码中的SCCB接口初始化函数如图
我们把他改为
void OV2640_IIC_Init(void)
{
rcu_periph_clock_enable(RCU_OV2640_SCL);
rcu_periph_clock_enable(RCU_OV2640_SDA);
//初始化SCL
gpio_mode_set(PORT_OV2640_SCL, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_OV2640_SCL);
gpio_output_options_set(PORT_OV2640_SCL, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_OV2640_SCL);
//初始化SDA
gpio_mode_set(PORT_OV2640_SDA, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_OV2640_SDA);
gpio_output_options_set(PORT_OV2640_SDA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_OV2640_SDA);
//SCL和SDA输出高电平
gpio_bit_write(PORT_OV2640_SCL, GPIO_OV2640_SCL, SET);
gpio_bit_write(PORT_OV2640_SDA, GPIO_OV2640_SDA, SET);
OV2640_SDA_MODE_OUT(); //设置SDA为输出模式
}
4.接下来用GD32函数和我们定义的函数重写stm32函数,例如
我们把他重写成
void IIC_Start(void)
{
OV2640_SDA_MODE_OUT();
SDA(1);
SCL(1);
delay_1us(50);
SDA(0);
delay_1us(50);
SCL(0);
}
四、OV2640其它接口的移植
1.添加文件
与前面的操作类似,创建ov2640.h
和ov2640.c
文件,添加至工程,添加include路径,完成后如图
2.DCI移植
(1)引脚连线
我们查看GD32F450的手册,如图
根据手册,我们发现DCI的引脚复用功能都是GPIO的AF13复用功能,由于摄像头的数据只有8位,因此,我们只需要用D0-D7引脚和PIXCLK引脚、VSYNC引脚、HSYNC引脚即可
同时我们选用PA12引脚来连接RST,PA2引脚来控制PA12,所以摄像头和GD32的连线如下表
摄像头引脚 | 开发板引脚 |
---|---|
CLK | PA6(PIXCLK) |
VSYNC | PG9 |
HREF | PA4(HSYNC) |
D0 | PC6 |
D1 | PC7 |
D2 | PC8 |
D3 | PC9 |
D4 | PC11 |
D5 | PD3 |
D6 | PB8 |
D7 | PB9 |
RST | PA12 |
PWDN | PA2 |
SCL | PB6 |
SDA | PB7 |
(2)配置GD32的DCI
先开启对应GPIO的时钟,开启DCI的时钟,将对应的DCI引脚设置为引脚复用,配置相应的GPIO,配置DCI参数结构体。
代码如下
void DCI_OV2640_Init(void)
{
/*开启GPIO时钟和DCI的时钟*/
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_GPIOB);
rcu_periph_clock_enable(RCU_GPIOC);
rcu_periph_clock_enable(RCU_GPIOD);
rcu_periph_clock_enable(RCU_GPIOG);
rcu_periph_clock_enable(RCU_DCI);
/*配置引脚复用,配置为DCI模式 */
gpio_af_set(GPIOA, GPIO_AF_13, GPIO_PIN_4);
gpio_af_set(GPIOA, GPIO_AF_13, GPIO_PIN_6);
gpio_af_set(GPIOC, GPIO_AF_13, GPIO_PIN_6);
gpio_af_set(GPIOC, GPIO_AF_13, GPIO_PIN_7);
gpio_af_set(GPIOC, GPIO_AF_13, GPIO_PIN_8);
gpio_af_set(GPIOC, GPIO_AF_13, GPIO_PIN_9);
gpio_af_set(GPIOC, GPIO_AF_13, GPIO_PIN_11);
gpio_af_set(GPIOB, GPIO_AF_13, GPIO_PIN_8);
gpio_af_set(GPIOB, GPIO_AF_13, GPIO_PIN_9);
gpio_af_set(GPIOD, GPIO_AF_13, GPIO_PIN_3);
gpio_af_set(GPIOG, GPIO_AF_13, GPIO_PIN_9);
/* configure DCI_PIXCLK(PA6), DCI_VSYNC(PG9), DCI_HSYNC(PA4), DCI_D0(PC6), DCI_D1(PC7)
DCI_D2(PC8), DCI_D3(PC9), DCI_D4(PC11), DCI_D5(PD3), DCI_D6(PB8), DCI_D7(PB9) */
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_4);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_4);
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_6);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_6);
gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_6);
gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_6);
gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_7);
gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_7);
gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_8);
gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_8);
gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_9);
gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_9);
gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_11);
gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_11);
gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_8);
gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_8);
gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_9);
gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_9);
gpio_mode_set(GPIOD, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_3);
gpio_output_options_set(GPIOD, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_3);
gpio_mode_set(GPIOG, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_9);
gpio_output_options_set(GPIOG, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_9);
/**DCI参数结构体*/
dci_parameter_struct dci_struct = {0};
dci_deinit(); // DCI复位
/* DCI configuration */
dci_struct.capture_mode = DCI_CAPTURE_MODE_CONTINUOUS; // 配置为连续捕获
dci_struct.clock_polarity = DCI_CK_POLARITY_RISING; // 时钟极性PCLK上升沿有效
dci_struct.hsync_polarity = DCI_HSYNC_POLARITY_LOW; // 水平参考信号低电平有效
dci_struct.vsync_polarity = DCI_VSYNC_POLARITY_LOW; // 垂直参考信号低电平有效
dci_struct.frame_rate = DCI_FRAME_RATE_ALL; // 全帧捕获
dci_struct.interface_format = DCI_INTERFACE_FORMAT_8BITS; // 8位数据
dci_init(&dci_struct);
}
3.DMA配置
通过查表,DCI的外设请求为DMA1,CH7,如图
因此宏定义如下
#define DMA_DCI_RCU RCU_DMA1
#define DMA_DCI DMA1
#define DMA_DCI_CH DMA_CH7
编写DMA初始化函数,这里选择单数据传输模式,外设和存储器的位宽都是32bit。外设为DCI的地址,因此设置为固定模式;存储器为数组的地址,不断自增。不设置循环模式,手动使能和失能。
void DCItoLCD_DMA_Init()
{
dma_single_data_parameter_struct dma_single_struct; //定义DMA参数结构体
rcu_periph_clock_enable(RCU_DMA1);
dma_deinit(DMA_DCI, DMA_DCI_CH);
dma_single_struct.periph_addr = (uint32_t)DCI_DR_ADDRESS;//外设基地址
dma_single_struct.memory0_addr = (uint32_t)photo_buff; //存储器基地址,定义的数组的地址
dma_single_struct.direction = DMA_PERIPH_TO_MEMORY;//DMA数据传输方向,外设到存储器
dma_single_struct.number =65535;//65535; // 38400 x 4 = 153600B
dma_single_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;//外设地址生成算法,串口地址不变,设置为固定模式
dma_single_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;//存储器基地址,每次存放的地址不同,要配置为增量模式
dma_single_struct.periph_memory_width = DMA_PERIPH_WIDTH_32BIT;//DMA_PERIPH_WIDTH_32BIT
dma_single_struct.priority = DMA_PRIORITY_HIGH;//DMA的软件优先级,这里配置为高优先级
dma_single_struct.circular_mode = DMA_CIRCULAR_MODE_DISABLE; //DMA循环模式,关闭循环模式
dma_single_data_mode_init(DMA_DCI, DMA_DCI_CH, &dma_single_struct);
dma_channel_subperipheral_select(DMA_DCI, DMA_DCI_CH, DMA_SUBPERI1);//DMAͨµÀÍâÉèÑ¡Ôñ
dma_channel_enable(DMA_DCI, DMA_DCI_CH);
dma_interrupt_enable(DMA_DCI, DMA_DCI_CH, DMA_CHXCTL_FTFIE);//通道传输完成中断
dma_interrupt_enable(DMA_DCI,DMA_DCI_CH,DMA_CHXCTL_TAEIE);
nvic_irq_enable(DMA1_Channel7_IRQn, 2, 1);//配置中断优先级
}
编写中断函数,中断函数在这里没有实际作用,只是用于调试,在实际工程中可以不使能中断
void DMA1_Channel7_IRQHandler(void)
{
if (dma_interrupt_flag_get(DMA_DCI, DMA_DCI_CH,DMA_INT_FLAG_FTF ) == SET)
{
dma_interrupt_flag_clear(DMA_DCI, DMA_DCI_CH, DMA_INT_FLAG_FTF);
printf("ok");
}
if (dma_interrupt_flag_get(DMA_DCI, DMA_DCI_CH, DMA_INT_FLAG_TAE) == SET)
{
printf("no");
}
printf("enter\n");
}
其余的函数只需根据厂家资料稍作修改即可
4.进行测试
在main.c
文件中,添加如下测试函数
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "stdlib.h"
#include "string.h"
#include "main.h"
#include "dma.h"
#include "usart.h"
#include "lcd.h"
#include "lcd_init.h"
#include "pic.h"
#include "sccb.h"
#include "ov2640.h"
void show_photo();
int main()
{
float t = 0;
usart_gpio_config(115200);
dma_config();
systick_config();
LCD_Init();
LCD_Fill(0, 0, LCD_W, LCD_H, WHITE);
SCCB_OV2640_init();
DCI_OV2640_Init();
OV2640_ImageSize_Set(1000, 1000);
OV2640_Outsize_Set(100, 100);
nvic_irq_enable(DCI_IRQn, 0U, 0U);
dci_interrupt_enable(DCI_INT_EF);
dci_enable();
while (1)
{
if (g_recv_complete_flag)
{
g_recv_complete_flag = 0;
printf("字节长度:%d ", g_recv_length);
printf("内容:%s\r\n", g_recv_buff);
memset(g_recv_buff, 0, g_recv_length);
g_recv_length = 0;
}
if (dci_flag_get(DCI_FLAG_OVR))
{
printf("溢出!\n");
}
delay_1ms(200);
DCItoLCD_DMA_Init();
dci_capture_enable();
delay_1ms(150);
dma_deinit(DMA_DCI,DMA_DCI_CH);
dci_capture_disable();
show_photo();
}
}
void show_photo()
{
int index = 0;
for (uint16_t i = 0; i < 100; i++)
{
for (uint16_t j = 0; j < 50; j++)
{
uint32_t tmp = photo_buff[index];
LCD_DrawPoint(j * 2, i, (uint16_t)(tmp & 0xFFFF));
LCD_DrawPoint(j * 2 + 1, i, (uint16_t)(photo_buff[index] >> 16));
index += 1;
}
}
}
在上面的测试函数中,我们配置摄像头输出100*100的RGB565格式的图片,同时定义了一个函数,用来遍历数组,显示像素数据,在遍历的过程中,要把32bit的像素数据分别取高位和低位,因为根据手册
DCI将四个字节填充成32位数据进行存储,而我们配置的DMA则是原封不动地搬运到指定的存储器中,由于在RGB565格式下,每个时钟输出半个像素,两个8bit的半像素再合成一个像素,因此只需要分别取这32位的高16位和低16位,即可得到正确的像素值。
除了这种方法,还可以使用DMA多数据传输模式,在多数据传输模式中,DMA可以自动地将数据打包和解包,如图
这里没有给出多数据传输模式的DMA配置,具体使用可以参考手册
最后的测试结果如下
这里给出的示例代码有许多改进的地方,比如拍出来的图片比较模糊,这可以通过配置ov2640寄存器,设置白平衡,曝光等设置来调整;还有就是这里没有使用多数据传输模式,还要进行多一次的运算,这可以进行改进等等
本次代码在我的二维码识别项目的OV2640分支下,地址:https://github.com/liangbm3/GD32_QR_Decoder/releases/tag/OV2640V2.0
如果觉得文章有帮助,请点赞收藏关注