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