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