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 寫函數對攝像頭進行配置,並獲取攝像頭返回的數據。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章