IIC通信實(shí)驗(yàn)
IIC簡(jiǎn)介
I2C(Inter-Integrated Circuit)字面上的意思是集成電路之間,它其實(shí)是I2C Bus簡(jiǎn)稱,所以中文應(yīng)該叫集成電路總線,它是一種串行通信總線,使用多主從架構(gòu),由飛利浦公司在1980年代為了讓主板、嵌入式系統(tǒng)或手機(jī)用以連接低速周邊設(shè)備而發(fā)展。I2C的正確讀法為“I平方C”("I-squared-C"),而“I二C”("I-two-C")則是另一種錯(cuò)誤但被廣泛使用的讀法。自2006年11月1日起,使用I2C協(xié)議已經(jīng)不需要支付專利費(fèi),但制造商仍然需要付費(fèi)以獲取I2C從屬設(shè)備地址。
為使用串行數(shù)據(jù)線(SDA)和串行時(shí)鐘線(SCL)、擁有7bit尋址空間的總線。 總線上有兩種類型角色的節(jié)點(diǎn):
- 主節(jié)點(diǎn) - 產(chǎn)生時(shí)鐘并發(fā)起與從節(jié)點(diǎn)的通信
- 從節(jié)點(diǎn) - 接收時(shí)鐘并響應(yīng)主節(jié)點(diǎn)的尋址
該總線是一種多主控總線,即可以在總線上放置任意多主節(jié)點(diǎn)。此外,在停止位(STOP)發(fā)出后,一個(gè)主節(jié)點(diǎn)也可以成為從節(jié)點(diǎn),反之亦然。
總線上有四種不同的操作模式,雖然大部分設(shè)備只作為一種角色和使用其中兩種操作模式:
- 主節(jié)點(diǎn)發(fā)送 - 主節(jié)點(diǎn)發(fā)送數(shù)據(jù)給從節(jié)點(diǎn)
- 主節(jié)點(diǎn)接收 - 主節(jié)點(diǎn)接收從節(jié)點(diǎn)數(shù)據(jù)
- 從節(jié)點(diǎn)發(fā)送 - 從節(jié)點(diǎn)發(fā)送數(shù)據(jù)給主節(jié)點(diǎn)
- 從節(jié)點(diǎn)接收 - 從節(jié)點(diǎn)接收主節(jié)點(diǎn)數(shù)據(jù)
一開始,主節(jié)點(diǎn)處于主節(jié)點(diǎn)發(fā)送模式,發(fā)送起始位(START),跟著發(fā)送希望與之通信的從節(jié)點(diǎn)的7bit位地址,最后再發(fā)送一個(gè)bit讀寫位,該數(shù)據(jù)位表示主節(jié)點(diǎn)想要與從節(jié)點(diǎn)進(jìn)行讀(1)還是寫(0)操作。
如果從節(jié)點(diǎn)在總線上,它將以ACK字符比特位應(yīng)答(低有效)該地址。主節(jié)點(diǎn)收到應(yīng)答后,根據(jù)它發(fā)送的讀寫位,處于發(fā)送模式或者接收模式,從節(jié)點(diǎn)則處于對(duì)應(yīng)的相反模式(接收或發(fā)送)。
地址和數(shù)據(jù)首先發(fā)送最高有效位。 起始位在SCL位高時(shí),由SDA上電平從高變低表示;停止位在SCL為高時(shí),由SDA上電平從低變高表示。其他SDA上的電平變化在SCL為低時(shí)發(fā)生。
如果主節(jié)點(diǎn)想要向從節(jié)點(diǎn)寫數(shù)據(jù),它將發(fā)送一個(gè)字節(jié),然后從節(jié)點(diǎn)以ACK位應(yīng)答,如此重復(fù)。此時(shí),主節(jié)點(diǎn)處于主節(jié)點(diǎn)發(fā)送模式,從節(jié)點(diǎn)處于從節(jié)點(diǎn)接收模式。
如果主節(jié)點(diǎn)想要讀取從節(jié)點(diǎn)數(shù)據(jù),它將不斷接收從節(jié)點(diǎn)發(fā)送的一個(gè)個(gè)字節(jié),在收到每個(gè)字節(jié)后發(fā)送ACK進(jìn)行應(yīng)答,除了接收到的最后一個(gè)字節(jié)。此時(shí),主節(jié)點(diǎn)處于主節(jié)點(diǎn)接收模式,從節(jié)點(diǎn)處于從節(jié)點(diǎn)發(fā)送模式。
此后,主節(jié)點(diǎn)要么發(fā)送停止位終止傳輸,要么發(fā)送另一個(gè)START比特以發(fā)起另一次傳輸(即“組合消息”)。
拓展
原始的I2C系統(tǒng)是在1980年代所創(chuàng)建的一種簡(jiǎn)單的內(nèi)部總線系統(tǒng),當(dāng)時(shí)主要的用途在于控制由飛利浦所生產(chǎn)的芯片。
- 1992年完成了最初的標(biāo)準(zhǔn)版本發(fā)布,新增了傳輸速率為400 kbit/s的快速模式及長度為10比特的地址模式可容納最多1008個(gè)節(jié)點(diǎn)。
- 1998年發(fā)布了2.0版,新增了傳輸速率為3.4Mbit/s的高速模式并為了節(jié)省能源而減少了電壓及電流的需求。
- 2.1版則在2001年完成,這是一個(gè)對(duì)2.0版做一些小修正,
- 3.0版于2007年發(fā)布。
- 2012年2月13日發(fā)布Specification Rev. 新增 5-MHz的超快速模式(UFM)。
- 2012年,第4版增加5 MHz的超快速模式(UFM),使用推挽式邏輯沒有上拉電阻新的USDA和USCS線,并增加了制造商指定的ID表。
- 2012年,第5版修正錯(cuò)誤。
- 在2014年,第6版糾正了兩個(gè)圖。這是目前最新的標(biāo)準(zhǔn)。
實(shí)驗(yàn)
信號(hào)類型及實(shí)驗(yàn)
I2C總線在傳送數(shù)據(jù)過程中共有三種類型的信號(hào),他們分別是:
開始信號(hào):SCL為高電平時(shí),SDA由高電平向低電平跳變,開始傳輸數(shù)據(jù)。
結(jié)束信號(hào):SCL為高電平時(shí),SDA由低電平向高電平跳變,結(jié)束傳輸數(shù)據(jù)。
應(yīng)答信號(hào):接受數(shù)據(jù)的IC在接收到8bit數(shù)據(jù)后,向發(fā)送數(shù)據(jù)的IC發(fā)出特定的低電平脈沖,表示已經(jīng)接受到數(shù)據(jù)。CPU向受控單元發(fā)出一個(gè)信號(hào)后,等待受控單元發(fā)出一個(gè)應(yīng)答信號(hào),CPU接收到應(yīng)答信號(hào)后,根據(jù)實(shí)際情況作出是否繼續(xù)傳遞信號(hào)的判斷。若未收到應(yīng)答信號(hào),由判斷為受控單元出現(xiàn)故障。
這些信號(hào)中,起始信號(hào)是必需的,結(jié)束信號(hào)和應(yīng)答信號(hào),都可以不要。I2C總線時(shí)序如下圖:
STM32F767上面板載的EEPROM(電子抹除式可復(fù)寫只讀存儲(chǔ)器)芯片型號(hào)為24C02。該芯片的總?cè)萘繛?56個(gè)字節(jié),該芯片通過I2C總線與外部連接,我們本實(shí)驗(yàn)就通過I2C來實(shí)現(xiàn)24C02的讀寫。
目前大部分MCU都帶有I2C總線接口,STM32F767不例外。但是,我們這里不使用STM32F767的硬件I2C來讀寫24C02,而是通過軟件模擬。ST為了規(guī)避飛利浦I2C的專利問題,將STM32的硬件I2C設(shè)計(jì)的比較復(fù)雜,而且穩(wěn)定性極差,給開發(fā)帶來非常多的不便,所以這里我們并不推薦使用,有興趣的可以下來自己查資料,來研究下STM32F767的硬件I2C。
我們?cè)谶@里使用了軟件來模擬I2C協(xié)議,這樣做的好處是,同一個(gè)代碼兼容所有的MCU,任何一個(gè)單片機(jī)只要有IO口,就可以很快的移植過去,而且不需要特定的IO口,只需要簡(jiǎn)單的更改IO口的定義,就可以快速使用。而硬件I2C,則換一次MCU,基本上等于重新搞一次I2C驅(qū)動(dòng),非常之麻煩。
I2C的實(shí)驗(yàn)功能簡(jiǎn)介:開機(jī)的時(shí)候先檢測(cè)24C02是否存在,然后在主循環(huán)里面檢測(cè)兩個(gè)按鍵,其中1個(gè)按鍵(KEY1)用來執(zhí)行寫入24C02操作,另外一個(gè)按鍵(KEY0)用來執(zhí)行讀出操作,在LCD模塊上顯示相關(guān)信息,同時(shí)DS0閃爍,提示程序運(yùn)行正常。
硬件部分
實(shí)驗(yàn)需要用到指示燈DS2,以及按鍵KEY0,1和LCD顯示屏,24C02。
前面的硬件咱們都已經(jīng)基本介紹過了,這里我們只簡(jiǎn)單介紹以下24C02與STM32F767的連接,24C02的SCL與SDA分別連接在STM32F767的PH4和PH5上的,連接關(guān)系如下圖:
軟件部分
首先來看I2C的初始化,我們要使用軟件來模擬,就要讓硬件也做出I2C硬件協(xié)議相關(guān)的工作,所以我們來操作兩個(gè)IO口來模擬I2C的SCL和SDA就行了,具體方法如下:
I2C初始化
void I2C_Init(void)
{
GPIO_InitTypeDef I2C_Initure;
__HAL_RCC_GPIOH_CLK_ENABLE(); //使能GPIOH時(shí)鐘
//PH4,5初始化設(shè)置
I2C_Initure.Pin = GPIO_PIN_4 | GPIO_PIN_5;
I2C_Initure.Mode = GPIO_MODE_OUTPUT_PP; //推挽輸出
I2C_Initure.Pull = GPIO_PULLUP; //上拉
I2C_Initure.Speed = GPIO_SPEED_FAST; //快速
HAL_GPIO_Init(GPIOH, &IC2_Initure);
I2C_SDA(1); //SDA線拉高
I2C_SCL(1); //SCL線拉高
}
我們?cè)诔跏蓟校瑢H4,5兩個(gè)IO口設(shè)置為推挽輸出,然后拉上,并設(shè)為快速,然后調(diào)用HAL_GOIO_Init初始化函數(shù),并且將兩條IO先的輸出電平先拉高,符合I2C協(xié)議的靜默狀態(tài)。至于后兩行代碼 I2C_SDA(),I2C_SCL
我們?cè)趯?duì)應(yīng)的頭文件里面用宏函數(shù)來定于,具體如下:
#define I2C_SDA(n) (n?HAL_GPIO_WritePin(GPIOH, GPIO_PIN_4, GPIO_PIN_SET):HAL_GPIO_WritePin(GPIOH, GPIO_PIN_4, GPIO_PIN_RESET))
#define I2C_SCL(n) (n?HAL_GPIO_WritePin(GPIOH, GPIO_PIN_5, GPIO_PIN_SET):HAL_GPIO_WritePin(GPIOH, GPIO_PIN_5, GPIO_PIN_RESET))
那么這樣SDA,SCL線都已經(jīng)準(zhǔn)備好了,那么要開始發(fā)送信號(hào)吧,代碼如下:
產(chǎn)生I2C起始信號(hào)
軟件模擬起始信號(hào)的代碼如下:
void I2C_Strat(void)
{
SDA_OUT(); //SDA線輸出
I2C_SDA(1);
I2C_SCL(1);
delay_us(4);
I2C_SDA(0); //在SCL線為高電平時(shí),SDA線拉低為起始信號(hào)
delay_us(4);
I2C_SCL(0); //拉低SCL線,準(zhǔn)備開始發(fā)送或者接收數(shù)據(jù)
}
其中函數(shù) SDA_OUT()
同樣是一個(gè)宏函數(shù),定義在頭文件中,具體如下:
#define SDA_OUT() {GPIOH->MODER &= ~(0x3 << (10));GPIOH->MODER |= 0x0 << 10;}
通過函數(shù) I2C_Start()
就可以發(fā)送一個(gè)開始信號(hào),來發(fā)送或者接受數(shù)據(jù)了,本質(zhì)上來說,就是我們使用了IO操作來模擬了I2C的開始階段的電壓跳變,非常簡(jiǎn)單。
產(chǎn)生I2C停止信號(hào)
有起始后,需要來停止,代碼如下:
void I2C_Stop(void)
{
SDA_OUT();
I2C_SCL(0);
I2C_SDA(0);
delay_us(4);
I2C_SCL(1);
I2C_SDA(1);
}
依然遵從I2C的時(shí)序圖,在停止信號(hào)處,先讓SDA線輸出,然后將SCL和SDA線拉低,待一段時(shí)間后,再將SCL和SDA線全部拉高,回到靜默狀態(tài)。
等待應(yīng)答信號(hào)
在起始信號(hào)發(fā)送了后,需要等待應(yīng)答,代碼如下:
u8 I2C_Wait_Ack(void)
{
u8 ucErrTime = 0;
SDA_IN(); //SDA線切換為輸入模式
I2C_SDA(1); delay_us(1);
I2C_SCL(1); delay_us(1);
while(READ_SDA) {
ucErrTime++;
if (ucErrTime > 250) {
IC_Stop();
return 1;
}
}
I2C_SLC(0); //時(shí)鐘線拉低
return 0;
}
這里用到了兩個(gè)宏函數(shù),仍然定義在頭文件當(dāng)中,代碼如下:
#define SDA_IN() {GPIOH->MODER &= ~(0x3 << 10); GPIOH->MODER |= 0x0 << 10}
#define READ_SDA HAL_GPIO_ReadPin(GPIOH, GPIO_PIN_5) //輸入SDA信號(hào)
這個(gè)函數(shù)也很容易理解,參照I2C的時(shí)序圖,將SDA線設(shè)置為了輸入模式,并拉高SDA線和SCL線,使用輪詢讀取PH5的電平值,但SDA線出現(xiàn)低電平,表示應(yīng)答信號(hào)來到,拉低SCL線,return 0,表示接收應(yīng)答成功。
產(chǎn)生應(yīng)答信號(hào)
在作為接收方時(shí),需要產(chǎn)生應(yīng)答信號(hào),代碼如下:
void I2C_Ack(void)
{
I2C_SCL(0);
SDA_OUT();
I2C_SDA(0);
delay_us(2);
I2C_SCL(1);
delay_us(2);
I2C_SCL(0);
}
這個(gè)函數(shù)根據(jù)I2C的時(shí)序圖,將應(yīng)答信號(hào)就可以發(fā)送出去了,代碼很好理解。
不產(chǎn)生應(yīng)答信號(hào)
如果不產(chǎn)生應(yīng)答信號(hào),代碼如下:
void I2C_NAck(void)
{
I2C_SCL(0);
SDA_OUT();
I2C_SDA(1);
delay_us(2);
I2C_SCL(1);
delay_us(2);
I2C_SCL(0);
}
和上邊的代碼反過來就行了,在SCL線拉低后,SDA繼續(xù)輸出高電平,那么就不會(huì)產(chǎn)生應(yīng)答信號(hào)了。
I2C發(fā)送一個(gè)字節(jié)
void I2C_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
I2C_SCL(0); //拉低時(shí)鐘開始數(shù)據(jù)發(fā)送
for(t = 0; t < 8; t++) {
I2C_SDA((txd & 0x80) >> 7);
txd <<= 1;
delay_us(2);
I2C_SCL(1);
delay_us(2);
I2C_SCL(0);
delay_us(2);
}
}
這個(gè)函數(shù)的設(shè)計(jì)也是相當(dāng)?shù)暮?jiǎn)單了,一個(gè)字節(jié)是8位,用for循環(huán),每次發(fā)送他的第8位,然后整體向左移動(dòng)一位,每次發(fā)送一位后,通過調(diào)整SCL線電平來確定時(shí)序。
I2C讀取一個(gè)字節(jié)
有了發(fā)送,就相應(yīng)的來接收就行,代碼如下:
u8 I2C_Read_Byte(u8 ack)
{
u8 i,receive = 0;
SDA_IN(); //SDA線切換為輸入,來接收數(shù)據(jù)
for(i = 0; i < 8; i++) {
I2C_SCL(0);
delay_us(2);
I2C_SCL(1);
receive <<= 1;
if (READ_SDA) receive++;
delay_us(1);
}
if (!ack) {
I2C_NAck(); //不發(fā)送應(yīng)答信號(hào)
} else {
I2C_Ack(); //發(fā)送ACK信號(hào)
}
return receive;
}
這個(gè)函數(shù)和發(fā)送字節(jié)其實(shí)沒有什么區(qū)別,就是反過來讀,然后return就行了,區(qū)別在于和用參數(shù)來確定要不要發(fā)送ack應(yīng)答信號(hào)。
I2C的處理函數(shù),就介紹完了,代碼非常簡(jiǎn)單,就是通過IO操作來設(shè)置I2C_SDA及SCL。接下來來看下24C02的處理函數(shù)。
初始化I2C接口
void 24CXX_Init(void)
{
I2C_Init(); //直接調(diào)用I2C初始化就行
}
在24CXX指定地址讀取一個(gè)數(shù)據(jù)
讀操作的時(shí)候,要先確定讀的地址,所以:
寫模式-->寫讀的地址-->讀模式-->讀數(shù)據(jù)
代碼實(shí)現(xiàn)如下:
u8 24CXX_ReadOneByte(u16 ReadAdder)
{
u8 temp = 0;
I2C_Start();
I2C_Send_Byte(0xa0 + ((ReadAdder / 256) << 1)); //發(fā)送器件地址0xa0,寫數(shù)據(jù)
I2C_Wait_Ack();
I2C_Send_Byte(ReadAdder % 256); //發(fā)送低地址
I2C_Wait_Ack();
I2C_Start();
I2C_Send_Byte(0xa1); //進(jìn)入接收模式
I2C_Wait_Ack();
temp = I2C_Read_Byte(0);
I2C_Stop(); //產(chǎn)生停止信號(hào)
return temp;
}
在開始的時(shí)候,首先發(fā)送起始信號(hào),然后將要讀取數(shù)據(jù)的地址寫入,并發(fā)送到E2PROM,分兩次,首先發(fā)送高8位,然后發(fā)送低8位,然后等待ack后,恢復(fù)到起始狀態(tài),進(jìn)入接收模式,再一個(gè)ack后,就可以讀取數(shù)據(jù)。
在24CXX指定地址寫一個(gè)數(shù)據(jù)
寫操作的時(shí)候,同樣先確定寫的地址,所以要寫模式-->寫地址-->寫數(shù)據(jù)代碼實(shí)現(xiàn)如下:
void 24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{
I2C_Start();
I2C_Send_Byte(0xa0 + ((WriteAddr / 256) << 1)); //發(fā)送器件地址OXA0,寫數(shù)據(jù)
I2C_Wait_Ack();
I2C_Send_Byte(WriteAddr % 256); //發(fā)送低地址
I2C_Wait_Ack();
I2C_Send_Byte(DataToWrite);
I2C_Wait_Ack();
I2C_Stop(); //產(chǎn)生停止信號(hào)
delay_ms(10);
}
這樣單字節(jié)的寫或者讀非常繁瑣,那么再給他封裝一層,來個(gè)多字節(jié)讀寫,代碼如下:
void 24CXX_WriteLneByte(u16 WriteAddr, u32 DataToWrite, u8 Len)
{
u8 t;
for(t = 0; t < Len; t++) {
24CXX_WriteOneByte(WriteAddr + t, (DataToWrite >> (8 * t)) & 0xff);
}
}
u32 24CXX_ReadLenByte(u16 ReadAddr, u8 Len)
{
u8 t;
u32 temp = 0;
for (t = 0; t < Len; t++) {
temp <= 8;
temp += 24CXX_ReadOneByte(ReadAddr + Len -t - 1);
}
return temp;
}
這2個(gè)函數(shù)非常好理解,就是用for循環(huán)來調(diào)用單字節(jié)讀寫函數(shù)即可。
這里最好還需要一個(gè)函數(shù)來檢測(cè)24C02的狀態(tài),當(dāng)IC出錯(cuò)時(shí)能夠反饋錯(cuò)誤,代碼如下:
u8 24CXX_Check(void)
{
u8 temp;
temp = 24CXX_ReadOneByte(255); //避免每次開機(jī)都寫24CXX
if (temp == 0x55) return 0;
else { //排除第一次初始化
24CXX_WriteOneByte(255, 0x55);
temp = 24CXX_ReadOneByte(255);
if (temp == 0x55) return 0;
}
return 1;
}
這個(gè)函數(shù)就是使用24XX的最后一個(gè)地址(255)來存儲(chǔ)標(biāo)志字0x55,通過判斷0x55來看是不是24C02設(shè)備,如果這里使用的其他24C系列,需要更改這個(gè)地址。
再定義兩個(gè)在指定地址讀寫指定長度的數(shù)據(jù)的函數(shù),代碼如下:
void 24CXX_Read(u16 ReadAddr, u8 *pBuffer, u16 NumToRead)
{
while(NumToRead) {
*pBuffer++ = 24Cxx_ReadOneByte(ReadAddr++);
NumToRead--;
}
}
void 24CXX_Write(u16 WriteAddr, u8 *pBuffer, u16 NumToWrite)
{
while(NumToWrite--) {
24CXX_WriteOneByte(WriteAddr, *pBuffer);
WriteAddr++;
pBuffer++;
}
}
以上的代碼基本可以支持24C02了,我們的正點(diǎn)原子的開發(fā)板,把24C02地址引腳都設(shè)置為0。