07.立创梁山派GD32移植OV2640摄像头

  • 此系列文章源于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.hov2640.c文件,添加至工程,添加include路径,完成后如图
在这里插入图片描述

2.DCI移植

(1)引脚连线

我们查看GD32F450的手册,如图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
根据手册,我们发现DCI的引脚复用功能都是GPIO的AF13复用功能,由于摄像头的数据只有8位,因此,我们只需要用D0-D7引脚和PIXCLK引脚、VSYNC引脚、HSYNC引脚即可
同时我们选用PA12引脚来连接RST,PA2引脚来控制PA12,所以摄像头和GD32的连线如下表

摄像头引脚开发板引脚
CLKPA6(PIXCLK)
VSYNCPG9
HREFPA4(HSYNC)
D0PC6
D1PC7
D2PC8
D3PC9
D4PC11
D5PD3
D6PB8
D7PB9
RSTPA12
PWDNPA2
SCLPB6
SDAPB7

(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

如果觉得文章有帮助,请点赞收藏关注


  • 16
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值