STM32 驱动OV7670 详解(二)- IO 资源配置和 SCCB

以下内容基于 STM32F103C8T6 Blue Pill 板子 + OV7670 摄像头(带 AL422B FIFO 模块)。

目前我用的 STM32 IO 口资源如下所示:
PA9, PA10 用于 USART1;
PB10, PB11 用于 SCCB(I2C)
PA0~PA7 用于 D0-D7 数据传输;
PB0 用于 PWDN 管脚配置(设置 Normal 或 Power Down Mode);
PB1 用于 RESET 管脚配置;
PB6 用于 VSYNC 管脚捕获(即捕获帧同步信号);
PB7 用于 HREF 管脚捕获(即捕获行同步信号);
PB8 用于 FIFO WR 管脚配置(即控制向 FIFO 的写使能);
PB9 用于 FIFO WRST 管脚配置(即控制向 FIFO 的写复位);
PC13 用于 FIFO RCK 管脚配置(即提供从 FIFO 的读时钟);
PC14 用于 FIFO OE 管脚配置(即控制从 FIFO 的读使能);
PC15 用于 FIFO RRST 管脚配置(即控制从 FIFO 的读复位);
注意在 STM32F103C8T6 系列的 IO 口中,PB3/PB4/PA13/PA14/PA15 都是默认为 JTAG 的功能,建议最好就不使用了如果要使用的话需要重新映射后才可以正常拉低/拉高。可以参考以下博客的介绍:关于STM32的PB3/PB4/PA13/PA14/PA15的引脚不能控制输出的问题

我做这个驱动开发的流程是这样的:先验证与摄像头的 SCCB 通信是否正常 -> 通过 8-color bar 输出验证从 FIFO 写/读的操作是否正确 -> 验证摄像头图像传输功能。以下对代码的解释也将按照这个顺序来进行。

代码首先是各种外设的 RCC 配置,这块儿应该没有太多需要解释的,只要是需要用到的外设都需要开启对应的 RCC 时钟,需要注意的是不要忘了对 AFIO 的 RCC 时钟进行初始化。这个是当 IO口 需要作为某些外设的特定管脚时就需要用到的。

void RCC_Config(void){
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
}

接下来是对 GPIO 的配置,每种外设的不同接口都需要对 GPIO 做不同的输入/输出配置,具体的可以参考 STM32 Reference Manual 9.1.11 章节,有很详细的配置说明。后续讲到每个模块时会再对 GPIO 口配置做进一步说明。

void GPIO_Config(void){
	// for USART1; PA9-TX, PA10-RX
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// for I2C2; PB10-I2C_SCL, PB11-I2C_SDA
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	// for PWDN, RESET; PB0 - PWDN, PB5 - RESET
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = OV_PWDN;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = OV_RESET;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	// for HREF, VSYNC; PB6 - VSYNC, PB7 - HREF
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin = OV_VSYNC;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource6);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin = OV_HREF;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	// for WR, WRRST; PB8 - WR ENABLE, PB9 - WR RST
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = FIFO_WR;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = FIFO_WRST;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	//for RCK, OE and RRST; PC13 - RCK, PC14 - OE, PC15 - RRST
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = FIFO_RCK;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = FIFO_OE;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = FIFO_RRST;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC,&GPIO_InitStructure);
	
	// for Camera Read Port; PA0~7 - D0~D7
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
}

把这些配置做好后,我们就可以来看如何使用 SCCB 与摄像头模块进行通信:最简单的尝试就是通过 SCCB 读取设备的 ID,再通过串口打印出返回的 ID 值就可以判断 SCCB 通信的读写操作是否正确了。

首先我们需要对 USART 串口进行初始化配置,相应的 GPIO 配置在 GPIO_Config 函数中已经完成了,注意的是 USART TX 需要配置成 Alternate function push-pull,而不是 push pull output;RX 配置成 Input Floating 即可。

对 USART 串口的配置如下所示,配置成 115200 波特率,stop Bits = 1。

void USART1_Config(void){
	USART_InitStructure.USART_BaudRate = 115200;
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
	USART_InitStructure.USART_Parity = USART_Parity_No;
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	USART_Init(USART1,&USART_InitStructure);
	USART_Cmd(USART1,ENABLE);
}

为了能方便的打印出串口信息,我们再对 printf 函数进行重写,这样就可以像 C 一样直接使用 printf 这个函数了,注意还需要在编译菜单 Target 中勾选 “Use Micro LIB” 选项。

另外注意的是需要通过检查 USART_FLAG_TXE 标志位来确保 USART_SendData 这一字节是发送完了,然后再发下一字节。否则打印出来的信息会不完整。

int fputc(int ch, FILE *f){
	USART_SendData(USART1,(uint8_t) ch);
	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET){};
	return (ch);
}

这样,我们串口打印这块儿就配置完了,可以通过 printf 任何一段文字来进行验证。

接下来就是对 SCCB 的通信初始化配置了,SCCB 通信协议和 I2C 基本是一样的,所以我们直接用 I2C 外设来做 SCCB 通信就可以了。相应的 GPIO 配置在 GPIO_Config 函数中已经完成了,注意在 GPIO_Config 中需要把 I2C 的两个 PIN (PB10 和 PB11)配置成 Alternate function open-drain,这样的话我们也就需要在 PB10 和 PB11 两个管脚增加上拉 3.3V;因为开漏模式本身是没法输出高电平的,上拉的话一般用 4.7kohm 或 2.2kohm 都可以。

对 SCCB (I2C)的初始化配置如下,I2C 时钟速率配置为 100khz,其余的也没有太多可说的,都是标准配置:

void I2C2_Config(void){
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_10bit;
	I2C_InitStructure.I2C_ClockSpeed = 100000;
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
	I2C_InitStructure.I2C_OwnAddress1 = 0x01;
	I2C_Init(I2C2,&I2C_InitStructure);
	I2C_Cmd(I2C2, ENABLE);
}

对 SCCB(I2C)配置完成后,我们来做 SCCB 的读写函数,这部分是相当重要的,建议参考 《OV Serial Camera Control Bus Functional Specification》文档来进行深入理解。

首先是 SCCB 的写函数,对于 SCCB 的写操作实际就是参考文档中的 3-Phase Write Transmission Cycle:先写 ID address,再写 Register Addresss,最后写 Data,每个 phase 都传输的 1字节。所以 SCCB 写函数是这样的:

第一步检查 SCCB Bus 是否空闲,通过检查 I2C_FLAG_BUSY 来实现,本质就是检测总线上的电平,因为空闲时总线电平是被上拉到 3.3V。第二步 SCCB 发送 START 标志位,实际就是选择 Master/Slave 模式,通过检查 MASTER_MODE_SELECT 事件来确认,如果超时(SCCB_TIME_OUT) 该事件还没有 SET,就会返回错误。第三步是发送 ID address,注意 SCCB 定义的读写状态下 ID address 是不一样的,通过最后1位来进行区分读写操作;通过检查 MASTER_TRANSMITTER_MODE_SELECT 事件来确认。第四步是发送寄存器地址,注意根据文档描述,只有在写状态的 ID address 下,发送的寄存器地址才会有效;通过检查 MASTER_BYTE_TRANSMITTED 事件来确认。第五步是发送需要写入该寄存器的数据,也是通过检查 MASTER_BYTE_TRANSMITTED 事件来确认。第六步就是发送 STOP 标志位,注意 SCCB 通信协议写操作只支持一次写入一个字节,因此在写完一个字节数据后需要发送停止位,如果还需要再写,就需要重复这个 3-Phase Write Transmission Cycle。这就是整个 SCCB 写函数了。

void SCCB_WRITE(uint8_t Reg_Addr, uint8_t Write_Data){
	
	// Detect the bus is busy or not
	while(I2C_GetFlagStatus(I2C2,I2C_FLAG_BUSY)){};
	
	// SCCB Generate START
	I2C_GenerateSTART(I2C2,ENABLE);
	SCCB_TIME_OUT = TIME_OUT;
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) == RESET){
		if((SCCB_TIME_OUT--) == 0){
			printf("\n Start_Event Check Fail");
		}
	}
	
	// SCCB Send 7-bits Write Address
	I2C_Send7bitAddress(I2C2,SCCB_WRITE_ADDR,I2C_Direction_Transmitter);
	SCCB_TIME_OUT = TIME_OUT;
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == RESET){
		if((SCCB_TIME_OUT--) == 0){
			printf("\n Transmit Address set Fail");
		}
	}
	
	// SCCB Send Reg Data. Can only be done at WRITE Phase !!!
	I2C_SendData(I2C2,Reg_Addr);
	SCCB_TIME_OUT = TIME_OUT;
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED) == RESET){
		if((SCCB_TIME_OUT--) == 0){
			printf("\n Register Address set Fail");
		}
	}
	
	// SCCB Send Write Data. 
	I2C_SendData(I2C2,Write_Data);
	SCCB_TIME_OUT = TIME_OUT;
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED) == RESET){
		if((SCCB_TIME_OUT--) == 0){
			printf("\n Write Data Fail");
		}
	}
	
	// SCCB Generate STOP to end the 3-phase write
	I2C_GenerateSTOP(I2C2,ENABLE);	
}

接下来是 SCCB 读函数,根据参考文档描述,读操作就是 2-phase Write transmission cycle + 2-phase read transmission cycle:先写 ID address(写操作的),再写需要读的寄存器地址,再写 ID address(读操作的),最后读出数据。注意的是写需要读的寄存器地址操作一定是跟在 写操作的 ID address 后面

从检查总线是否 busy 开始,到第一次发送 STOP 标志位的过程与前面的 SCCB写函数是一样的,就不做赘述了。在第一次 STOP 标志位后,第一步是发送 START 标志位。第二步发送读操作对应的 ID address,通过检查 MASTER_RECEIVER_MODE_SELECTED 事件来确认。第三步是我们停止 Ack 并发送 STOP标志位,这一点是十分重要的!!! 因为 SCCB 的读只返回一个字节,所以根据 STM32 Reference Manual page763 的描述,当只剩一个字节需要读取时,应该先 Clear ACK,再 STOP,再等待 RXNE flag,最后再进行读取操作。我实测过如果不按照这个顺序进行,SCCB 读会有失败。第四步即读取数据,当 RXNE flag 立起后就可以进行读操作了。第五步需要重新使能 Ack,为下一次读写做准备。这就是整个 SCCB 的读函数了,第三步是最重要的,一定不能忽视。

uint8_t SCCB_READ(uint8_t Reg_Addr){
	uint8_t DATA_REC;
	
	// Detect the bus is busy or not
	while(I2C_GetFlagStatus(I2C2,I2C_FLAG_BUSY)){};
	
	// SCCB Generate START
	I2C_GenerateSTART(I2C2,ENABLE);
	SCCB_TIME_OUT = TIME_OUT;
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) == RESET){
		if((SCCB_TIME_OUT--) == 0){
			printf("\n Start_Event Check Fail");
		}
	}
	
	// SCCB Send 7-bits Write Address
	I2C_Send7bitAddress(I2C2,SCCB_WRITE_ADDR,I2C_Direction_Transmitter);
	SCCB_TIME_OUT = TIME_OUT;
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == RESET){
		if((SCCB_TIME_OUT--) == 0){
			printf("\n Read T Address set Fail");
		}
	}
	
	// SCCB Send Reg Data. Can only be done at WRITE Phase !!!
	I2C_SendData(I2C2,Reg_Addr);
	SCCB_TIME_OUT = TIME_OUT;
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED) == RESET){
		if((SCCB_TIME_OUT--) == 0){
			printf("\n Register Address set Fail");
		}
	}
	
	// SCCB Generate STOP to end the 2-phase write
	I2C_GenerateSTOP(I2C2,ENABLE);
	
	// SCCB Generate START Again
	I2C_GenerateSTART(I2C2,ENABLE);
	SCCB_TIME_OUT = TIME_OUT;
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) == RESET){
		if((SCCB_TIME_OUT--) == 0){
			printf("\n Start_Event Check Fail");
		}
	}
	
	// SCCB Send 7-bits Read Address
	I2C_Send7bitAddress(I2C2,SCCB_READ_ADDR,I2C_Direction_Receiver);
	SCCB_TIME_OUT = TIME_OUT;
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) == RESET){
		if((SCCB_TIME_OUT--) == 0){
			printf("\n R Address set Fail");
		}
	}
	
	// VERY IMPORTANT !!! We need to STOP before the last byte
	I2C_AcknowledgeConfig(I2C2,DISABLE);
	I2C_GenerateSTOP(I2C2,ENABLE);
	
	// SCCB RECEVIE DATA
	SCCB_TIME_OUT = TIME_OUT;
	while(I2C_GetFlagStatus(I2C2,I2C_FLAG_RXNE) == RESET){
		if((SCCB_TIME_OUT--)==0){
			printf("\n Receive Fail");
		}
	}
	DATA_REC = I2C_ReceiveData(I2C2);
	
	// SCCB enable ACK for next transmission
	I2C_AcknowledgeConfig(I2C2,ENABLE);
	
	return DATA_REC;
}

现在,我们已经做好了 SCCB 的读写函数,我们就通过读摄像头的 ID 来验证 SCCB 通信是否正常。在此之前,有一个坑需要注意,即我们在对 STM32 和摄像头上电后,因为上电时序我们没有控制,所以一定要在 STM32 跑起来后对摄像头先进行一个复位操作,并且复位后等待至少 1s 再进行 SCCB 读写操作,这样才能保证 SCCB 的通信正常;如果不进行复位,很可能 STM32 发起读写操作时摄像头还没有 ready。

所以我们先通过 PB1 管脚的拉低再拉高进行复位,然后将 PB0(PWDN)置低使摄像头进入 Normal Mode。

    // Reset Camera
	GPIO_ResetBits(GPIOB,OV_RESET);
	Delay_ms(50);
	GPIO_SetBits(GPIOB, OV_RESET);
	Delay_ms(5000);
	
	// Set Device into Normal Mode
	GPIO_ResetBits(GPIOB,OV_PWDN);

接下来读取 0A,0B,1C,1D 寄存器来获取摄像头的 Product ID 和 Manufacturer ID,并通过 Printf 函数打印出来。

	// Read Device ID Information 
	Pro_ID_MSB = SCCB_READ(SCCB_READ_PRO_ID_MSB);
	Pro_ID_LSB = SCCB_READ(SCCB_READ_PRO_ID_LSB);
	Manu_ID_MSB = SCCB_READ(SCCB_READ_MANU_ID_MSB);
	Manu_ID_LSB = SCCB_READ(SCCB_READ_MANU_ID_LSB);
	
	printf("\n The Product ID is: %d %d ", Pro_ID_MSB, Pro_ID_LSB);
	printf("\n The Manufactuer ID is: %d %d ", Manu_ID_MSB, Manu_ID_LSB);

以上操作后,可以在串口上看到打印的信息:在这里插入图片描述
因为打印出的是十进制格式,转为16进制格式就分别为:
Product ID:76 73
Manufactuer ID:7F A2
与 OV7670 Datasheet 所描述的一致。证明我们与摄像头之间的 SCCB 通信是没问题的。

到此,我们完成了 SCCB 通信,下一步就可以通过 SCCB 写函数对摄像头进行配置,并获取摄像头返回的数据。

你可能感兴趣的:(STM32)