1. 綜述
I2C(IIC,Inter-Integrated Circuit),兩線式串行總線,由PHILIPS公司開發用於連接微控制器及其外圍設備。
它是由數據線SDA和時鍾SCL構成的串行總線,可發送和接收數據。在CPU和被控IC之間、IC與IC之間進行雙向傳送,高速IIC總線一般可達400kbps以上。但在STM8中,400kHZ已經是最快速度了。
2.關於STM8S103手冊的I2C簡介


芯片手冊中只對I2C的特點進行了簡單的講解,但並未深入解析其中的過程。
3. I2C詳細解析
I2C總共由五個核心函數,分別為:①起始信號②停止信號③應答信號④發送數據⑤接收數據,通過這五個核心基本函數就能於大多數的傳感進行通信了。
3.1 起始信號
當SCL為高電平期間,SDA由高電平到低電平的跳變過程;起始信號是一種電平跳變時序信號,而不是一個電平信號,如圖虛線框所示。
3.2 停止信號
當SCL為高電平期間,SDA由低電平到高電平的跳變過程;停止信號也是一種電平跳變時序信號,而不是一個電平信號,如圖虛線框所示。

3.3 應答信號
I2C的數據字節定義為8位長,對於發送端每發送1個字節后,需要將數據線(SDA)釋放,由接收端反饋一個應答信號(ACK)。應答信號為低電平時,則將其規定為有效信號(ACK簡稱應答位),表示接收端已經成功接收了該字節;應答位為高電平時,規定為非應答位(NACK),一般表示接收端沒有成功接收該字節。
對於反饋有效應答位ACK的要求是,接收端在第9個時鍾脈沖之前的低電平期間將SDA線拉低,並且確保在該時鍾的高電平期間為穩定的低電平。如果接收端是主機,則在它接收到最后一個字節后,發送一個NACK信號,以通知發送端結束數據發送,並釋放SDA線,以便主機接收端發送一個停止信號。

3.4 發送數據
在發送起始信號后開始通信,主機發送一個8位數據。然后,主機釋放SDA線並等待從從機發出得確認信號(ACK)。詳細過程請看4.3.7代碼示例。
3.5 接收數據
在發送起始信號后開始通信,主機發送一個8位數據。然后,從機收到數據返回一個確認信號(ACK)給主機,這時候主機才開始接收數據,待主機接收數據完成后,發送一個NACK信號給從機,以通知接收端結束數據接收。詳細過程請看4.3.8代碼示例。
3.6 數據有效性
I2C總線進行數據傳送時,時鍾信號為高電平期間,數據線上的數據必須保持穩定,只有在時鍾線上的信號為低電平期間,數據線上的高電平或低電平狀態才允許變化。
3.7 I2C通信總過程

4. 例程
4.1 編譯環境:
我的編譯環境是IAR,這款軟件是現在STM8的主流平台,比較推薦。不過我打算等到STCubeMX更新出比較方便的版本后再去使用Keil5,因為我在用STM32的時候就是利用Keil5,的確很方便,你們也可以學着用一下。
4.2 主芯片:
我的主芯片是STM8S系列中的103,其中STM8S的003、005、和103、105,配置一樣(外設和CPU頻率,FLASH),在代碼相同的情況下均可進行燒寫。
4.3 代碼&解析
I2C的基本函數代碼我已經和傳感的代碼區隔開來,可以移植,幾乎適用於市面上使用I2C驅動的傳感器。
4.3.1 SDA、SCL引角初始化
1 //IIC引腳 2 GPIO_Init(IIC_SCL_GPIO_Port, IIC_SCL_Pin, GPIO_MODE_OUT_PP_HIGH_FAST); 3 GPIO_Init(IIC_SDA_GPIO_Port, IIC_SDA_Pin, GPIO_MODE_OUT_PP_HIGH_FAST);
在引角的控制上面,我選擇了直接操作GPIO的寄存器,這樣操作比較快,雖然我們感覺不出來,但是省出來的時間越來越多了,也就能夠體現出這樣寫的好處了,不過不理解怎么用的話,也可以使用庫函數進行寫高、低電平。
我在SDA引角初始化的時候,選擇了推挽輸出_高電平_高速,這里就有人會有疑問了,SDA是會進行接收ACK信號的,需要接收即為輸入模式,怎么這里改成輸出模式,看過我STM8_GPIO介紹的博客的小伙伴應該會想到,怎么不使用開漏輸出,這個模式既能接收也能發送。沒錯,開漏輸出模式的確可以,但我在那篇博客中也有說到,開漏輸出模式不穩定,通過示波器觀察到是斜三角的,而推挽輸出是完整的矩形。圖我就懶得去弄了 -。- ,而如何解決推挽輸出能夠接收ACK的操作看我下一小節。
4.3.1 I2C結構體和引角配置
這里的結構體是方便I2C多線程,以后需要用到多個I2C接口時候,只需要再定義多一個該結構變量,賦予其他引角便可,省去了再次編寫代碼的時間和空間。
我在26和26行編寫了兩行代碼,分別是將SDA模式改成輸出和輸入模式,直接更改寄存器里的值就能完成實現模式的更換,想知道為什么這樣寫可以改變模式的話,可以自行百度,也可以察看相對應芯片的寄存器手冊。STM8S103中的則在6.2小節中就有介紹。因為講解起來比較麻煩,這里就不進行更深入的說明了。
1 /* Struct --------------------------------------------------------------------*/ 2 3 typedef struct iic 4 { 5 //具體信息:引腳 讀寫判定 6 GPIO_TypeDef * pSCL_Port; //SCL Gpio 7 uint8_t uSCL_Pin; //SCL Pin 8 GPIO_TypeDef * pSDA_Port; //SDA Gpio 9 uint8_t uSDA_Pin; //SDA Pin 10 11 uint8_t uSDA_Mode_Pin_Position;//SDA Mode 12 13 }IIC_HandleTypedef; 14 15 16 17 /* Define --------------------------------------------------------------------*/ 18 19 #define IIC_SCL_1(_HANDLE_) ( (_HANDLE_)->pSCL_Port->ODR |= ( (uint8_t)(_HANDLE_)->uSCL_Pin)) 20 #define IIC_SCL_0(_HANDLE_) ( (_HANDLE_)->pSCL_Port->ODR &= (~(uint8_t)(_HANDLE_)->uSCL_Pin)) 21 22 #define IIC_SDA_1(_HANDLE_) ( (_HANDLE_)->pSDA_Port->ODR |= ( (uint8_t)(_HANDLE_)->uSDA_Pin)) 23 #define IIC_SDA_0(_HANDLE_) ( (_HANDLE_)->pSDA_Port->ODR &= (~(uint8_t)(_HANDLE_)->uSDA_Pin)) 24 #define IIC_SDA_R(_HANDLE_) ( (BitStatus)(_HANDLE_)->pSDA_Port->IDR & (_HANDLE_)->uSDA_Pin) 25 26 #define IIC_GPIO_SDA_MODE_Opt(_HANDLE_) (_HANDLE_)->pSDA_Port->ODR |= (uint8_t)1<<(_HANDLE_)->uSDA_Mode_Pin_Position 27 #define IIC_GPIO_SDA_MODE_Ipt(_HANDLE_) (_HANDLE_)->pSDA_Port->ODR &= ~((uint8_t)1<<(_HANDLE_)->uSDA_Mode_Pin_Position)
4.3.2 延時函數
延時函數顧名思義,就單純的延時,延時時間可以根據芯片的速率調整,具體時間通過示波器或者可以觀察到脈沖的儀器進行測量即可。
這里定義了兩個延時函數目的是在SCL低電平期間先提前改變SDA的電平,待到SDA電平穩定時,再將SCL電平改變進行讀取。
1 void vIIC_Delay_4us(void) 2 { 3 uint8_t i=3; 4 while(i--) 5 { 6 asm(" NOP");asm(" NOP");asm(" NOP");asm(" NOP"); 7 } 8 9 } 10 11 void vIIC_Delay_2us(void) 12 { 13 asm(" NOP");asm(" NOP");asm(" NOP"); 14 }
4.3.3 IIC引角賦值&結構體參數初始化
每次調用I2C接口時都需要對IIC的句柄進行初始化。
1 void vIIC_Handle_Init(IIC_HandleTypedef * hIICx, GPIO_TypeDef * pSCL_Port, uint8_t uSCL_Pin, GPIO_TypeDef * pSDA_Port, uint8_t uSDA_Pin) 2 { 3 //GPIO 4 hIICx->pSCL_Port = pSCL_Port; 5 hIICx->uSCL_Pin = uSCL_Pin ; 6 hIICx->pSDA_Port = pSDA_Port; 7 hIICx->uSDA_Pin = uSDA_Pin ; 8 9 10 switch(uSDA_Pin) 11 { 12 case GPIO_PIN_0 : hIICx->uSDA_Mode_Pin_Position = 0 ;break; 13 case GPIO_PIN_1 : hIICx->uSDA_Mode_Pin_Position = 2 ;break; 14 case GPIO_PIN_2 : hIICx->uSDA_Mode_Pin_Position = 4 ;break; 15 case GPIO_PIN_3 : hIICx->uSDA_Mode_Pin_Position = 6 ;break; 16 case GPIO_PIN_4 : hIICx->uSDA_Mode_Pin_Position = 8 ;break; 17 case GPIO_PIN_5 : hIICx->uSDA_Mode_Pin_Position = 10;break; 18 case GPIO_PIN_6 : hIICx->uSDA_Mode_Pin_Position = 12;break; 19 case GPIO_PIN_7 : hIICx->uSDA_Mode_Pin_Position = 14;break; 20 21 } 22 }
4.3.4 起始信號
這里與3.1講解的操作有點不同,就是3.1中最后沒有將SCL拉低包括在內,而為了發送數據的方便,我也將SCL在此函數中拉低了。
1 void vIIC_Start_Signal(IIC_HandleTypedef * hIICx) 2 { 3 4 IIC_SDA_1 (hIICx); //拉高數據線 5 IIC_SCL_1 (hIICx); //拉高時鍾線 6 vIIC_Delay_4us ( ); //延時 7 IIC_SDA_0 (hIICx); //拉低數據線 8 vIIC_Delay_4us ( ); //延時 9 IIC_SCL_0 (hIICx); //拉低時鍾線 10 vIIC_Delay_4us ( ); //延時 11 12 }

4.3.5 結束信號
這里與3.2講解的操作也有所不同,因為在數據接收完或者是發送完成后,SDA的電平不能確定,有可能是高也有可能是低電平,但在結束信號的時候,SDA需要是低電平時候拉低SCL才能作為結束信號的開始。
1 void vIIC_Stop_Signal(IIC_HandleTypedef * hIICx) 2 { 3 4 IIC_SDA_0 (hIICx); //拉低數據線 5 vIIC_Delay_4us ( ); //延時 6 IIC_SCL_1 (hIICx); //拉高時鍾線 7 vIIC_Delay_4us ( ); //延時 8 IIC_SDA_1 (hIICx); //拉高數據線 9 vIIC_Delay_4us ( ); //延時 10 11 }


4.3.6 應答信號(ACK)
由於因為發送端和操作的不同,這里需要將ACK分成三種,①Ack(主動拉低SDA形成應答信號) ②NAck(主動不拉低SDA不形成應答信號) ③ReadAck(等待應答信號)。
①Ack(主動拉低SDA形成應答信號)
該信號在你沒有讀取到最后一個數據時由主機發送,使從機繼續發送數據。
1 void vIIC_Ack(IIC_HandleTypedef * hIICx) 2 { 3 4 IIC_SDA_0 (hIICx); //拉低數據位 5 vIIC_Delay_2us ( ); //延時 6 IIC_SCL_1 (hIICx); //拉高時鍾位 7 vIIC_Delay_4us ( ); //延時 8 IIC_SCL_0 (hIICx); //拉低時鍾位 9 vIIC_Delay_2us ( ); //延時 10 11 }
②NAck(主動不拉低SDA不形成應答信號)
該信號在你讀取完最后一個數據時由主機發送,使從機停止發送數據。
1 void vIIC_NAck(IIC_HandleTypedef * hIICx) 2 { 3 4 IIC_SDA_1 (hIICx); //SDA拉高 不應答對方 5 vIIC_Delay_2us ( ); 6 IIC_SCL_1 (hIICx); 7 vIIC_Delay_4us ( ); 8 IIC_SCL_0 (hIICx); 9 vIIC_Delay_2us ( ); 10 11 }
③ReadAck(等待應答信號)
該信號在主機發送完數據后等待從機應答時候使用。
1 bool bIIC_ReadACK(IIC_HandleTypedef * hIICx) //返回為:=1有ACK,=0無ACK 2 { 3 IIC_GPIO_SDA_MODE_Ipt (hIICx); //將SDA的模式改成輸入模式 4 IIC_SDA_1 (hIICx); //拉高數據線 5 vIIC_Delay_2us ( ); //延時 6 IIC_SCL_1 (hIICx); //拉高時鍾線 7 vIIC_Delay_2us ( ); //延時 8 9 if(IIC_SDA_R(hIICx)) //判斷是否成功接收應答,如‘有’返回0,‘沒有’則返回1 10 { 11 IIC_SCL_0 (hIICx); //拉低時鍾線 12 vIIC_Delay_2us ( ); //延時 13 IIC_GPIO_SDA_MODE_Opt(hIICx); //接收完應答后,將SDA的模式改回輸出模式 14 return FALSE; //沒有應答 15 } 16 else 17 { 18 IIC_SCL_0 (hIICx); //拉低時鍾線 19 vIIC_Delay_2us ( ); //延時 20 IIC_GPIO_SDA_MODE_Opt(hIICx); //接收完應答后,將SDA的模式改回輸出模式 21 return TRUE; //產生應答 22 } 23 24 }


4.3.7 發送數據
所要發送的數據為8位,學過串口協議的應該知道按位發送,我們這里將要發送的數據進行由高到低位的一個順序發送,具體操作如下,不懂的朋友可以將以下代碼通過畫圖畫出來,以方便理解。
1 void vIIC_SendByte(IIC_HandleTypedef * hIICx, uint8_t uSendByte) 2 { 3 4 uint8_t i; 5 6 for (i=0; i<8; i++) //循環8次 7 { 8 if(uSendByte & 0X80) //將發送的數據最高位與1相與,若發送的數據最高位為1,則將SDA拉高,否則拉低 9 IIC_SDA_1 (hIICx); 10 else 11 IIC_SDA_0 (hIICx); 12 uSendByte <<= 1; //數據左移1位 13 vIIC_Delay_2us ( ); //延時 14 IIC_SCL_1 (hIICx); //時鍾線拉高 15 vIIC_Delay_4us ( ); //延時 16 IIC_SCL_0 (hIICx); //時鍾線拉低 17 vIIC_Delay_2us ( ); //延時 18 19 } 20 21 }
4.3.8 數據接收
具體操作都寫在注釋部分,在SCL高電平時候去讀取SDA的電平。
1 uint8_t uIIC_RecvByte(IIC_HandleTypedef * hIICx) 2 { 3 uint8_t i,uReceiveByte = 0; 4 5 IIC_GPIO_SDA_MODE_Ipt(hIICx); //將SDA的模式設置為輸入模式 6 IIC_SDA_1 (hIICx); //拉高數據線 7 for(i=0;i<8;i++) //進行8次的循環 8 { 9 uReceiveByte <<= 1; //將接收到的數據左移 10 11 vIIC_Delay_2us ( ); //延時 12 IIC_SCL_1 (hIICx); //拉高時鍾線 13 vIIC_Delay_2us ( ); //延時 14 15 if(IIC_SDA_R (hIICx)) //讀取SDA電平 16 { 17 uReceiveByte |=0x01; //若SDA電平為高則將數據的最低位或上1,即為加1;若SDA電平為低,不進行該操作,則數據最低位為0 18 } 19 20 vIIC_Delay_2us ( ); //延時 21 IIC_SCL_0 (hIICx); //拉低時鍾線 22 vIIC_Delay_2us ( ); //延時 23 } 24 IIC_GPIO_SDA_MODE_Opt(hIICx); //將SDA的模式設置為輸出模式 25 26 return uReceiveByte; 27 28 }
5.結尾
I2C協議核心基本函數為以上,將所有的核心函數結合起來便可與傳感器設備進行通信了,但本博客只是單純講解了I2C協議,並未與傳感器進行通信,若理解完I2C協議后可前往下一章博客進行與傳感器通信的實踐。
對STM8的I2C協議講解到這里結束,感謝各位看官的點擊。
如果覺得有所收獲請點下推薦,若認為該博客中存在錯誤的說明或者對博客中某方面有疑問請留言。
作 者:浩宇99✌ 出 處:https://www.cnblogs.com/zhenghaoyu/p/10719233.html
版權聲明:本文原創發表於 博客園,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則視為侵權。
