目录
1. 硬件设计
摄像头与 STM32 连接关系中主要分为 SCCB 控制、VGA 时序控制、FIFO 数据读取部分,介绍如下:
1.1 SCCB 控制相关
摄像头中的 SIO_C 和 SIO_D 引脚直接连接到 STM32 普通的 GPIO,它们不具有硬件I2C 的功能,所以在后面的代码中采用模拟 I2C 时序,实际上直接使用硬件 I2C 是完全可以实现 SCCB 协议的,本设计采用模拟 I2C 是芯片资源分配妥协的结果。
1.2 VGA 时序相关
检测 VGA 时序的 HREF、VSYNC 引脚,它们与 STM32 连接的 GPIO 均设置为输入模式,其中 HREF 在本实验中并没有使用,它已经通过摄像头内部的与非门控制了 FIFO 的写使能;VSYNC 与 STM32 连接的 GPIO 引脚会在程序中配置成中断模式,STM32 利用该中断信号获知新的图像是否采集完成,从而控制 FIFO 是否写使能。
1.3 FIFO 相关
与 FIFO 控制相关的 RCLK、RRST、WRST、WEN 及 OE 与 STM32 连接的引脚均直接配置成推挽输出,STM32 根据图像的采集情况利用这些引脚控制 FIFO;读取 FIFO 数据内容使用的数据引脚 DO[0:7]均连接到 STM32 同一个 GPIO 端口连续的高 8 位引脚 PB[8:15],这些引脚使用时均配置成输入,程序设计中直接读取 GPIO 端口的高 8 位状态直接获取一个字节的 FIFO 内容,建议在连接这部分数据信号时,参考本设计采用同一个 GPIO 端口连续的 8 位(高 8 位或低 8 位均可),否则会导致读取数据的程序非常复杂。
1.4 XCLK 信号
本设计中 STM32 的摄像头接口还预留了 PA8 引脚用于与摄像头的 XCLK 连接,STM32的 PA8 可以对外输出时钟信号,所以在使用不带晶振的摄像头时,可以通过该引脚给摄像头提供时钟,秉火摄像头内部已自带晶振,在程序中没有使用 PA8 引脚。
2. 代码设计
2.1 SCCB总线软件实现
2.1.1 宏定义
#ifndef __SCCB_H
#define __SCCB_H
#include "stm32f10x.h"
/************************** OV7725 连接引脚定义********************************/
#define OV7725_SIO_C_SCK_APBxClock_FUN RCC_APB2PeriphClockCmd
#define OV7725_SIO_C_GPIO_CLK RCC_APB2Periph_GPIOC
#define OV7725_SIO_C_GPIO_PORT GPIOC
#define OV7725_SIO_C_GPIO_PIN GPIO_Pin_6
#define OV7725_SIO_D_SCK_APBxClock_FUN RCC_APB2PeriphClockCmd
#define OV7725_SIO_D_GPIO_CLK RCC_APB2Periph_GPIOC
#define OV7725_SIO_D_GPIO_PORT GPIOC
#define OV7725_SIO_D_GPIO_PIN GPIO_Pin_7
#define SCL_H GPIO_SetBits(OV7725_SIO_C_GPIO_PORT , OV7725_SIO_C_GPIO_PIN)
#define SCL_L GPIO_ResetBits(OV7725_SIO_C_GPIO_PORT , OV7725_SIO_C_GPIO_PIN)
#define SDA_H GPIO_SetBits(OV7725_SIO_D_GPIO_PORT , OV7725_SIO_D_GPIO_PIN)
#define SDA_L GPIO_ResetBits(OV7725_SIO_D_GPIO_PORT , OV7725_SIO_D_GPIO_PIN)
#define SCL_read GPIO_ReadInputDataBit(OV7725_SIO_C_GPIO_PORT , OV7725_SIO_C_GPIO_PIN)
#define SDA_read GPIO_ReadInputDataBit(OV7725_SIO_D_GPIO_PORT , OV7725_SIO_D_GPIO_PIN)
#define ADDR_OV7725 0x42
void SCCB_GPIO_Config(void);
int SCCB_WriteByte( u16 WriteAddress , u8 SendByte);
int SCCB_ReadByte(u8* pBuffer, u16 length, u8 ReadAddress);
#endif
2.1.2 SCCB管脚配置
前面我们说过,SCCB的引脚配置类似于IIC的引脚配置,这里我们对SCCB的引脚进行初始化,使用的类似软件IIC的协议,可以不用规定必须是IIC的引脚,进行配置为开漏输出模式:
void SCCB_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* SCL(PC6)、SDA(PC7)管脚配置 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE );
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_Init(GPIOC, &GPIO_InitStructure);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE );
GPIO_InitStructure.GPIO_Pin =GPIO_Pin_7;
GPIO_Init(GPIOC, &GPIO_InitStructure);
}
按照宏定义进行转换:
void SCCB_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* SCL(PC6)、SDA(PC7)管脚配置 */
OV7725_SIO_C_SCK_APBxClock_FUN ( OV7725_SIO_C_GPIO_CLK, ENABLE );
GPIO_InitStructure.GPIO_Pin = OV7725_SIO_C_GPIO_PIN ;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_Init(OV7725_SIO_C_GPIO_PORT, &GPIO_InitStructure);
OV7725_SIO_D_SCK_APBxClock_FUN ( OV7725_SIO_D_GPIO_CLK, ENABLE );
GPIO_InitStructure.GPIO_Pin = OV7725_SIO_D_GPIO_PIN ;
GPIO_Init(OV7725_SIO_D_GPIO_PORT, &GPIO_InitStructure);
}
2.1.3 延时函数
一个简单的循环等于0时跳出循环,起到延时的作用:
static void SCCB_delay(void)
{
uint16_t i = 400;
while(i)
{
i--;
}
}
2.1.4 SCCB起始信号
类比IIC协议,起始条件下,SCL高电平期间,SDA从高电平切换到低电平。
static int SCCB_Start(void)
{
GPIO_SetBits(GPIOC, GPIO_Pin_7);
SCCB_delay();
GPIO_SetBits(GPIOC, GPIO_Pin_6);
SCCB_delay();
GPIO_ResetBits(GPIOC, GPIO_Pin_7);
SCCB_delay();
GPIO_ResetBits(GPIOC, GPIO_Pin_6);
SCCB_delay();
}
为了提高程序的健壮性,可以加入:GPIO_ReadInputDataBit 函数,其作用是读取指定GPIO端口的指定引脚的输入状态,并返回该引脚的输入值(0 或 1) ,进行检测SDA线是否忙碌是否正常。
static int SCCB_Start(void)
{
GPIO_SetBits(GPIOC, GPIO_Pin_7);
GPIO_SetBits(GPIOC, GPIO_Pin_6);
SCCB_delay();
if(!GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_7))
return DISABLE; /* SDA线为低电平则总线忙,退出 */
GPIO_ResetBits(GPIOC, GPIO_Pin_7);
SCCB_delay();
if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_7))
return DISABLE; /* SDA线为高电平则总线出错,退出 */
GPIO_ResetBits(GPIOC, GPIO_Pin_7);
SCCB_delay();
return ENABLE;
}
按照宏定义进行转换:
static int SCCB_Start(void)
{
SDA_H;
SCL_H;
SCCB_delay();
if(!SDA_read)
return DISABLE; /* SDA线为低电平则总线忙,退出 */
SDA_L;
SCCB_delay();
if(SDA_read)
return DISABLE; /* SDA线为高电平则总线出错,退出 */
SDA_L;
SCCB_delay();
return ENABLE;
}
2.1.5 SCCB终止信号
终止条件下,SCL高电平期间,SDA从低电平切换到高电平。
static void SCCB_Stop(void)
{
GPIO_ResetBits(GPIOC, GPIO_Pin_6);
SCCB_delay();
GPIO_ResetBits(GPIOC, GPIO_Pin_7);
SCCB_delay();
GPIO_SetBits(GPIOC, GPIO_Pin_6);
SCCB_delay();
GPIO_SetBits(GPIOC, GPIO_Pin_7);
SCCB_delay();
}
按照宏定义进行转换:
static void SCCB_Stop(void)
{
SCL_L;
SCCB_delay();
SDA_L;
SCCB_delay();
SCL_H;
SCCB_delay();
SDA_H;
SCCB_delay();
}
2.1.6 SCCB应答信号
与 I2C 时序类似,在 SCCB 时序也使用自由位(Don’t care bit )和非应答(NA)信号来保证正常通讯。自由位和非应答信号位于 SCCB 每个传输阶段中的第九位。
在写数据的第一个传输阶段中,第 9 位为自由位,在一般的正常通讯中,第 9 位时,主机的 SDA 线输出高电平,而从机把 SDA 线拉低作为响应,只是传输的内容分别为目的寄存器地址和要写入的数据。
应答信号的发送是 SCCB 协议中的一个重要步骤,用于确认数据传输的成功。
static void SCCB_Ack(void)
{
GPIO_ResetBits(GPIOC, GPIO_Pin_6);
SCCB_delay();
GPIO_ResetBits(GPIOC, GPIO_Pin_7);
SCCB_delay();
GPIO_SetBits(GPIOC, GPIO_Pin_6);
SCCB_delay();
GPIO_ResetBits(GPIOC, GPIO_Pin_6);
SCCB_delay();
}
按照宏定义进行转换:
static void SCCB_Ack(void)
{
SCL_L;
SCCB_delay();
SDA_L;
SCCB_delay();
SCL_H;
SCCB_delay();
SCL_L;
SCCB_delay();
}
2.1.7 SCCB非应答信号
非应答信号的发送也是 SCCB 协议中的一部分,用于处理数据传输失败或其他错误情况。在某些情况下,如果从设备无法正确接收数据,主设备可能会发送非应答信号并采取相应的错误处理措施。
static void SCCB_NoAck(void)
{
GPIO_ResetBits(GPIOC, GPIO_Pin_6);
SCCB_delay();
GPIO_SetBits(GPIOC, GPIO_Pin_7);
SCCB_delay();
GPIO_SetBits(GPIOC, GPIO_Pin_6);
SCCB_delay();
GPIO_ResetBits(GPIOC, GPIO_Pin_6);
SCCB_delay();
}
按照宏定义进行转换:
static void SCCB_NoAck(void)
{
SCL_L;
SCCB_delay();
SDA_H;
SCCB_delay();
SCL_H;
SCCB_delay();
SCL_L;
SCCB_delay();
}
2.1.8 SCCB等待应答信号
让主机把 SDA 线设为高电平,延时一段时间后再检测 SDA 线的电平,若为低则返回 ENABLE 表示接收到从机的应答,反之返回 DISABLE。(L代表低,H代表高,以下直接使用SCL和SDA的宏定义表示)
static int SCCB_WaitAck(void)
{
SCL_L;
SCCB_delay();
SDA_H;
SCCB_delay();
SCL_H;
SCCB_delay();
if(SDA_read)
{
SCL_L;
return DISABLE;
}
SCL_L;
return ENABLE;
}
2.1.9 发送数据
首先,我们先创建一个用于接收数据的参数SendByt,它接受一个 8 位的字节数据作为输入参数,使用一个循环逐位发送这个字节的数据。循环从最高位开始,依次发送每一位,直到最低位。在每一次循环中,首先将 SCL(时钟线)拉低,然后通过 SCCB_delay 函数产生一定延迟,接着,根据 SendByte 的当前最高位,控制 SDA(数据线)为高电平或低电平,以此来发送数据的当前位,然后将 SendByte 左移一位,准备发送下一位数据。通过循环8次来完成一位数据的发送。
static void SCCB_SendByte(uint8_t SendByte)
{
uint8_t i=8;
while(i--)
{
SCL_L;
SCCB_delay();
if(SendByte&0x80)
SDA_H;
else
SDA_L;
SendByte<<=1;
SCCB_delay();
SCL_H;
SCCB_delay();
}
SCL_L;
}
2.1.10 SCCB总线返回的数据
首先声明了一个变量 i 并初始化为 8,以及再声明了一个变量 ReceiveByte 并初始化为 0,用于存储接收到的字节数据,然后,将 SDA(数据线)拉高,准备接收数据,通过一个循环逐位接收一个字节的数据。循环从最高位开始,依次接收每一位,直到最低位,读取 SDA 线上的数据,并根据其值决定是否将 ReceiveByte 的当前最低位设置为 1,将 ReceiveByte 左移一位,准备接收下一位数据。
static int SCCB_ReceiveByte(void)
{
uint8_t i=8;
uint8_t ReceiveByte=0;
SDA_H;
while(i--)
{
ReceiveByte<<=1;
SCL_L;
SCCB_delay();
SCL_H;
SCCB_delay();
if(SDA_read)
{
ReceiveByte|=0x01;
}
}
SCL_L;
return ReceiveByte;
}
2.1.11 SCCB写一个字节数据
首先,明确两个宏,其实OV7725的设备地址:
#define ADDR_OV7725 0x42
#define DEV_ADR ADDR_OV7725
为了确保SCCB总线通信正常启动,我们可以先进行判断SCCB是否发送起始信号,若是发送继续,若是失败则返回DISABLE。
if(!SCCB_Start())
{
return DISABLE;
}
然后,通过 SCCB_SendByte 函数发送设备地址 DEV_ADR,用于指定要通信的设备,此时指定要通信的设备会发送应答,通过判断本机时候接收到应答,来确定两个设备间是否能进行正常通信,若是不能则终止信号,并返回DISABLE。
SCCB_SendByte( DEV_ADR );
if( !SCCB_WaitAck() )
{
SCCB_Stop();
return DISABLE;
}
若是能则将要写入的寄存器地址的低八位(WriteAddress 的低八位)发送给设备,以确定写入数据的目标寄存器,再次等待指定要通信的设备的应答,然后发送要写入的数据 SendByte 给设备,再次等待设备的应答,,停止 SCCB 通信,并返回 ENABLE 表示写入操作成功。
SCCB_SendByte((uint8_t)(WriteAddress & 0x00FF));
SCCB_SendByte(SendByte);
SCCB_WaitAck();
SCCB_Stop();
return ENABLE;
完整代码:
#define ADDR_OV7725 0x42
#define DEV_ADR ADDR_OV7725
int SCCB_WriteByte( uint16_t WriteAddress , uint8_t SendByte )
{
if(!SCCB_Start())
{
return DISABLE;
}
SCCB_SendByte( DEV_ADR ); /* Æ÷¼þµØÖ· */
if( !SCCB_WaitAck() )
{
SCCB_Stop();
return DISABLE;
}
SCCB_SendByte((uint8_t)(WriteAddress & 0x00FF)); /* ÉèÖõÍÆðʼµØÖ· */
SCCB_WaitAck();
SCCB_SendByte(SendByte);
SCCB_WaitAck();
SCCB_Stop();
return ENABLE;
}
2.1.12 SCCB读一串数据
int SCCB_ReadByte(uint8_t* pBuffer, uint16_t length, uint8_t ReadAddress)
{
if(!SCCB_Start())
{
return DISABLE;
}
SCCB_SendByte( DEV_ADR ); /* Æ÷¼þµØÖ· */
if( !SCCB_WaitAck() )
{
SCCB_Stop();
return DISABLE;
}
SCCB_SendByte( ReadAddress ); /* ÉèÖõÍÆðʼµØÖ· */
SCCB_WaitAck();
SCCB_Stop();
if(!SCCB_Start())
{
return DISABLE;
}
SCCB_SendByte( DEV_ADR + 1 ); /* Æ÷¼þµØÖ· */
if(!SCCB_WaitAck())
{
SCCB_Stop();
return DISABLE;
}
while(length)
{
*pBuffer = SCCB_ReceiveByte();
if(length == 1)
{
SCCB_NoAck();
}
else
{
SCCB_Ack();
}
pBuffer++;
length--;
}
SCCB_Stop();
return ENABLE;
}