I2C總線簡單方便,是我們經常使用的一種總線。但有時候我們的MCU沒有足夠多的I2C控制器來實現我們的應用,所幸我可以使用普通的GPIO引腳來模擬低速的I2C總線通信。這一節我們就來實現使用軟件通過普通GPIO操作I2C設備的驅動。
1、功能概述
I2C總線使用兩條線:串行數據(SDA)和串行時鍾(SCL)。所有I2C主設備和從設備僅與這兩條線連接。每個設備可以是發射器,接收器或兩者。有些設備是主設備,它們生成總線時鍾並在總線上啟動通信,其他設備是從設備並響應總線上的命令。為了與特定設備通信,每個從設備必須具有總線上唯一的地址。I2C主設備(通常是微控制器)不需要地址,因為沒有其他設備向主設備發送命令。總線設備連接示意圖如下:
1.1、I2C的傳輸過程
I2C總線有標准、快速和高速多種速度模式;也有7位地址和10位地址多種地址格式,但不管什么樣的模式其數據傳輸格式都可以划分為3個階段:起始階段、數據傳輸階段和終止階段。如下圖:
1.1.1、起始階段
在I2C總線不工作的情況下,SDA(數據線)和SCL(時鍾線)上的信號均為高電平。如果此時主機需要發起新的通信請求,那么需要首先通過SDA和SCL發出起始標志。當SCL為高電平時,SDA電平從高變低,這一變化表示完成了通信的起始條件。
在起始條件和數據通信之間,通常會有延時要求,具體的指標會在設備廠商的規格說明書中給出。
1.1.2、數據傳輸階段
I2C總線的數據通信是以字節(8位)作為基本單位在SDA上進行串行傳輸的。一個字節的傳輸需要9個時鍾周期。其中,字節中每一位的傳輸都需要一個時鍾周期,當新的SCL到來時,SCL為低電平,此時數據發送方根據當前傳輸的數據位控制SDA的電平信號。如果傳輸的數據位為"1",就將SDA電平拉高;如果傳輸的數據位為"0",就將SDA的電平拉低。當SDA上的數據准備好之后,SCL由低變高,此時數據接收方將會在下一次SCL信號變低之前完成數據的接收。當8位數據發送完成后,數據接收方需要一個時鍾周期以使用SDA發送ACK信號,表明數據是否接收成功。當ACK信號為"0"時,說明接收成功;為"1"時,說明接收失敗。每個字節的傳輸都是由高位(MSB)到低位(LSB)依次進行傳輸。
I2C總線協議中規定,數據通信的第一個字節必須由主機發出,內容為此次通信的目標設備地址和數據通信的方向(讀/寫)。在這個字節中,第1~7位為目標設備地址,第0位為通信方向,當第0位為"1"時表示讀,即后續的數據由目標設備發出主機進行接收;當第0位為"0"時表示寫,即后續的數據由主機發出目標設備進行接收。在數據通信過程中,總是由數據接收方發出ACK信號。
1.1.3、終止階段
當主機完成數據通信,並終止本次傳輸時會發出終止信號。當SCL 是高電平時,SDA電平由低變高,這個變化意味着傳輸終止。
1.2、I2C的傳輸格式
根據I2C總線的技術標准,I2C總線上的數據傳輸方式有3種:主站向從站寫數據方式;主站從從站讀數據方式;讀寫組合的方式。下面將就這幾種方式簡單說明。
1.2.1、寫數據格式
主站向從站寫數據方式是主棧發送數據給從站。傳輸方向沒有改變,從站接收主站發過來的每一個字節。具體格式如下圖:
1.2.2、讀數據格式
主站從從站讀數據方式,主站在發送第一個字節之后,立即接收從站數據。也就是說在第一次確認的時刻,主發送器變成了主接收器,從屬接收器變成了從屬發送器。第一個確認仍然由從站生成。主站則生成后續的確認。停止條件由主主站生成,它在停止條件之前發送一個非確認應答。具體格式如下圖:
1.2.3、讀寫組合格式
組合格式就是讀和寫是接連完成的。在傳輸中改變方向時,啟動條件和從地址都要重復,但R/W位要倒過來。如果主接收器發送一個重復啟動條件,它在重復啟動條件之前發送一個非確認應答,但不會有停止條件。具體格式如下圖:
2、驅動設計與實現
我們已經了解了I2C協議的基本內容,接下來我們需要考慮如何實現這一協議。實現了這一協議也就完成通過GPIO模擬I2C的驅動。
2.1、對象定義
我們們依然采用基於對象的操作來實現。所以在使用對象之前,我們需要得到對象。接下來我們就考慮GPIO模擬I2C的對象問題。
2.1.1、對象的抽象
一般的,作為一個對象肯定包括屬性和操作。所以我們考慮GPIO模擬I2C的對象也要從這兩方面來進行。
首先來考慮GPIO模擬I2C對象的屬性。作為屬性應該是必要的且能標識對象特點的參數。我們模擬的I2C其實是主站,作為主站沒有地址,所以地址不需要作為屬性。但通訊速度卻是主站需要控制的,所以我們將速度設置為GPIO模擬I2C的一個屬性。除此之外,作為主站沒有必須要記錄的參數了。
還需要考慮GPIO模擬I2C對象的操作。既然是使用GPIO模擬I2C,那么I2C的兩根總線SCL和SDA都需要主站操作GPIO來實現,所以控制SCL和控制SDA的行為都是對象的操作。除了控制總線我們還需要從總線讀取數據,所以從SDA讀取數據也是對象的一個操作。還有如延時等操作與具體的平台關系很大,我們也將其作為操作以便在具體的平台初始化。
根據上述的分析,我們可以抽象得到GPIO模擬I2C的對象類型如下:
1 typedef struct SimuI2CObject{ 2 uint32_t period; //確定速度為大於0K小於等於400K的整數,默認為100K 3 void (*SetSCLPin)(SimuI2CPinValue op); //設置SCL引腳 4 void (*SetSDAPin)(SimuI2CPinValue op); //設置SDA引腳 5 uint8_t (*ReadSDAPin)(void); //讀取SDA引腳位 6 void (*Delayus)(volatile uint32_t period); //速度延時函數 7 }SimuI2CObjectType;
2.1.2、對象的初始化
我們已經得到了GPIO模擬I2C的對象,但對象必須要初始化之后才可以操作,所以這里就需要考慮如何對對象進行初始化。一般來說,初始化函數需要處理幾個方面的問題。一是檢查輸入參數是否合理;二是為對象的屬性賦初值;三是對對象作必要的初始化配置。據此我們設計GPIO模擬I2C對象的初始化函數如下:
1 /* GPIO模擬I2C通訊初始化 */ 2 void SimuI2CInitialization(SimuI2CObjectType *simuI2CInstance, 3 uint32_t speed, 4 SimuI2CSetPin setSCL, 5 SimuI2CSetPin setSDA, 6 SimuI2CReadSDAPin readSDA, 7 SimuI2CDelayus delayus) 8 { 9 if((simuI2CInstance==NULL)||(setSCL==NULL)||(setSDA==NULL)||(readSDA==NULL)||(delayus==NULL)) 10 { 11 return; 12 } 13 14 simuI2CInstance->SetSCLPin=setSCL; 15 simuI2CInstance->SetSDAPin=setSDA; 16 simuI2CInstance->ReadSDAPin=readSDA; 17 simuI2CInstance->Delayus=delayus; 18 19 /*初始化速度,默認100K*/ 20 if((speed>0)&&(speed<=400)) 21 { 22 simuI2CInstance->period=500/speed; 23 } 24 else 25 { 26 simuI2CInstance->period=5; 27 } 28 29 /*拉高總線,使處於空閑狀態*/ 30 simuI2CInstance->SetSDAPin(Set); 31 simuI2CInstance->SetSCLPin(Set); 32 }
2.2、對象操作
我們已經定義了對象類型,也實現了對象的初始化函數,接下來我們就需要考慮封裝對象的操作了。根據前面我們對I2C協議的了解,需要實現的操作主要有:向從站寫數據、從從站讀數據、先向從站寫而后接着讀數據以及基於這三種模式的組合操作。
2.2.1、向從站寫數據操作
向從站寫數據包括向從站寫命令、地址以及設定數據等。如向一個或多個存儲地址寫數據,需要先寫存儲起始地址再寫需要保存的數據。所有的數據都是從主站發往從站,包括啟動通訊、下發數據、停止通訊這一過程。具體的實現如下:
1 /* 通過模擬I2C向從站寫數據 */ 2 SimuI2CStatus WriteDataBySimuI2C(SimuI2CObjectType *simuI2CInstance,uint8_t deviceAddress,uint8_t *wData,uint16_t wSize) 3 { 4 //啟動通訊 5 SimuI2CStart(simuI2CInstance); 6 //發送從站地址(寫) 7 SendByteBySimuI2C(simuI2CInstance,deviceAddress); 8 if(SimuI2CWaitAck(simuI2CInstance,5000)) 9 { 10 return I2C_ERROR; 11 } 12 13 while(wSize--) 14 { 15 SendByteBySimuI2C(simuI2CInstance,*wData); 16 if(SimuI2CWaitAck(simuI2CInstance,5000)) 17 { 18 return I2C_ERROR; 19 } 20 wData++; 21 simuI2CInstance->Delayus(10); 22 } 23 24 SimuI2CStop(simuI2CInstance); 25 return I2C_OK; 26 }
2.2.2、自從站讀數據操作
讀從站數據操作其實就是先向從站發送站地址(讀),然后接收數據。一般存儲器不會使用到這種模式,而對於向一些設備獲取數據會有這種模式,如MS5803壓力觸感器。其過程是先啟動通訊,再從主站發送包含讀的從站地址,然后主站接收自從站返回的數據,然后停止通訊。具體的實現過程如下:
1 /* 通過模擬I2C自從站讀數據 */ 2 SimuI2CStatus ReadDataBySimuI2C(SimuI2CObjectType *simuI2CInstance,uint8_t deviceAddress,uint8_t *rData, uint16_t rSize) 3 { 4 //啟動通訊 5 SimuI2CStart(simuI2CInstance); 6 7 //發送從站地址(讀) 8 SendByteBySimuI2C(simuI2CInstance,deviceAddress+1); 9 if(SimuI2CWaitAck(simuI2CInstance,5000)) 10 { 11 return I2C_ERROR; 12 } 13 14 simuI2CInstance->Delayus(1000); 15 16 while(rSize--) 17 { 18 *rData=RecieveByteBySimuI2C(simuI2CInstance); 19 rData++; 20 if(rData==0) 21 { 22 IIC_NAck(simuI2CInstance); 23 } 24 else 25 { 26 IIC_Ack(simuI2CInstance); 27 simuI2CInstance->Delayus(1000); 28 } 29 } 30 31 //結束通訊 32 SimuI2CStop(simuI2CInstance); 33 return I2C_OK; 34 }
2.2.3、先寫后讀組合操作
對於組合操作則是寫數據並讀數據連續進行。這就像從某一存儲地址讀數據一樣,先發送要讀的其實地址,然后接收讀出來的數據。其一般過程是:先啟動通訊,然后寫數據,接着重啟通訊,然后讀數據,最后停止通訊。具體的實現過程如下:
1 /* 通過模擬I2C實現對從站先寫數據緊接讀數據組合操作 */ 2 SimuI2CStatus WriteReadDataBySimuI2C(SimuI2CObjectType *simuI2CInstance,uint8_t deviceAddress, uint8_t *wData,uint16_t wSize,uint8_t *rData, uint16_t rSize) 3 { 4 //啟動通訊 5 SimuI2CStart(simuI2CInstance); 6 //發送從站地址(寫) 7 SendByteBySimuI2C(simuI2CInstance,deviceAddress); 8 if(SimuI2CWaitAck(simuI2CInstance,5000)) 9 { 10 return I2C_ERROR; 11 } 12 13 while(wSize--) 14 { 15 SendByteBySimuI2C(simuI2CInstance,*wData); 16 if(SimuI2CWaitAck(simuI2CInstance,5000)) 17 { 18 return I2C_ERROR; 19 } 20 wData++; 21 simuI2CInstance->Delayus(10); 22 } 23 24 //再啟動 25 SimuI2CStart(simuI2CInstance); 26 //發送從站地址(讀) 27 SendByteBySimuI2C(simuI2CInstance,deviceAddress+1); 28 if(SimuI2CWaitAck(simuI2CInstance,5000)) 29 { 30 return I2C_ERROR; 31 } 32 33 while(rSize--) 34 { 35 *rData=RecieveByteBySimuI2C(simuI2CInstance); 36 rData++; 37 if(rSize==0) 38 { 39 IIC_NAck(simuI2CInstance); 40 } 41 else 42 { 43 IIC_Ack(simuI2CInstance); 44 } 45 } 46 //結束通訊 47 SimuI2CStop(simuI2CInstance); 48 return I2C_OK; 49 }
3、驅動的使用前面
前面已經設計並實現了GPIO模擬I2C通訊的驅動,下面我們還需要使用此驅動設計一個簡單的應用以驗證驅動設計的是否合理。
3.1、聲明並初始化對象
在應用一個對象前,我們需要先得到這個對象。前面我們已經抽象了GPIO模擬I2C通訊的對象類型,這里我們將使用此對象類型聲明一個對象變量。具體形式如下:
SimuI2CObjectType simuI2C;
聲明了這個對象變量並不能立即使用,我們還需要使用驅動中定義的初始化函數對這個變量進行初始化。這個初始化函數所需要的輸入參數如下:
SimuI2CObjectType *simuI2CInstance,
uint32_t speed,
SimuI2CSetPin setSCL,
SimuI2CSetPin setSDA,
SimuI2CReadSDAPin readSDA,
SimuI2CDelayus delayus,
對於這些參數,對象變量我們已經定義了。而通訊速度根據實際情況選擇就好了,最大不超過500K,默認是100K。主要的是我們需要定義幾個函數,並將函數指針作為參數。這幾個函數的類型如下:
typedef void (*SimuI2CSetPin)(SimuI2CPinValue op); //設置SDA引腳
typedef uint8_t (*SimuI2CReadSDAPin)(void); //讀取SDA引腳位
typedef void (*SimuI2CDelayus)(volatile uint32_t period); //速度延時函數
對於這幾個函數我們根據樣式定義就可以了,具體的操作可能與使用的硬件平台有關系。具體函數定義如下:
1 //設置SCL引腳 2 static void SetSCLPin(SimuI2CPinValue op) 3 { 4 if(op==Set) 5 { 6 HAL_GPIO_WritePin(GPIOB,GPIO_PIN_8,GPIO_PIN_SET); 7 } 8 else 9 { 10 HAL_GPIO_WritePin(GPIOB,GPIO_PIN_8,GPIO_PIN_RESET); 11 } 12 } 13 14 //設置SDA引腳 15 static void SetSDAPin(SimuI2CPinValue op) 16 { 17 if(op==Set) 18 { 19 HAL_GPIO_WritePin(GPIOB,GPIO_PIN_7,GPIO_PIN_SET); 20 } 21 else 22 { 23 HAL_GPIO_WritePin(GPIOB,GPIO_PIN_7,GPIO_PIN_RESET); 24 } 25 } 26 27 //讀取SDA引腳位 28 static uint8_t ReadSDAPin(void) 29 { 30 return (uint8_t)HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_7); 31 }
對於延時函數我們可以采用各種方法實現。我們采用的STM32平台和HAL庫則可以直接使用HAL_Delay()函數。於是我們可以調用初始化函數如下:
SimuI2CInitialization(&simuI2C,100,SetSCLPin,SetSDAPin,ReadSDAPin,HAL_Delay);
這里我們將其設為100的I2C通訊接口。
3.2、基於對象進行操作
我們定義了對象變量並使用初始化函數給其作了初始化。接着我們就來考慮操作這一對象獲取我們想要的數據。我們在驅動中已經封裝了讀從站、寫從站以及讀寫混合操作,接下來我們使用這一驅動開發我們的應用實例。
這里我們考慮使用驅動讀寫一個I2C接口的存儲器,我們向某一個地址寫入數據和讀出數據,我們假定存儲器較小地址是8位的。
1 //從Memery中讀取數據 2 void ReadDataFromMem(uint8_t deviceAddress, uint8_t memAdd,uint8_t *rData, uint16_t rSize) 3 { 4 WriteReadDataBySimuI2C(&simuI2C,deviceAddress,&memAdd,1,rData,rSize); 5 } 6 7 //向Memery中寫數據 8 void WriteDataToMem(uint8_t deviceAddress,uint8_t memAdd,uint8_t *wData,uint16_t wSize) 9 { 10 uint8_t data[10]; 11 uint16_t size=0; 12 13 data[size++]=memAdd; 14 15 for(int i=0;i<wSize;i++) 16 { 17 data[size++]=wData[i]; 18 } 19 20 WriteDataBySimuI2C(&simuI2C,deviceAddress,wData,size); 21 }
在這一例中,我們實現了對8位地址的存儲器的數據寫入和讀出操作,根據封裝的驅動函數很容易實現。
4、應用總結
我們使用GPIO模擬的I2C協議在STM32平台上與多個設備進行通訊,如SHT20溫濕度傳感器、TSEV01CL55紅外溫度傳感器、MLX90614紅外溫度傳感器等,等到的結果非常好,即使在長達1米的通訊線路上都沒有問題。
使用本驅動是需要注意一點,因為在I2C總線中SDA是雙向的,所以在模擬式需要將模擬SDA的引腳配置為開漏模式,否則就需要控制其方向。
說到I2C總線有幾個相關的總線不能不提,系統管理總線SMBus、電源系統管理總線PMBus以及TWI Bus。這些總線與I2C總線有很多的共同點,在通訊速率一致的情況下是可以通用的。
完整的源代碼可在GitHub下載:https://github.com/foxclever/ExPeriphDriver
歡迎關注: