I2C總線與EEPROM
I2C總線是由PHILIPS公司開發(fā)的兩線式串行總線,多用于連接微處理器及其外圍設(shè)備。I2C總線的主要特點(diǎn)是接口方式簡單,兩條線可以掛多個(gè)參與通信的器件,即多機(jī)模式,而且任何一個(gè)器件都可以作為主機(jī),當(dāng)然同一時(shí)刻只能一個(gè)主機(jī)。
從原理上來講,UART屬于異步通信,比如電腦發(fā)送給單片機(jī),電腦只負(fù)責(zé)把數(shù)據(jù)通過TXD發(fā)送出來即可,接收數(shù)據(jù)是單片機(jī)自己的事情。而I2C屬于同步通信,SCL時(shí)鐘線負(fù)責(zé)收發(fā)雙方的時(shí)鐘節(jié)拍,SDA數(shù)據(jù)線負(fù)責(zé)傳輸數(shù)據(jù)。I2C的發(fā)送方和接收方都以SCL這個(gè)時(shí)鐘節(jié)拍為基準(zhǔn)進(jìn)行數(shù)據(jù)的發(fā)送和接收。
從應(yīng)用上來講,UART通信多用于板間通信,比如單片機(jī)和電腦,這個(gè)設(shè)備和另外一個(gè)設(shè)備之間的通信。而I2C多用于板內(nèi)通信,比如單片機(jī)和我們本章要學(xué)的EEPROM之間的通信。
1、I2C時(shí)序初步認(rèn)識(shí)
在硬件上,I2C總線是由時(shí)鐘總線SCL和數(shù)據(jù)總線SDA兩條線構(gòu)成,連接到總線上的所有的器件的SCL都連到一起,所有的SDA都連到一起。I2C總線是開漏引腳并聯(lián)的結(jié)構(gòu),因此我們外部要添加上拉電阻。對(duì)于開漏電路外部加上拉電阻的話,那就組成了線“與”的關(guān)系?偩上線“與”的關(guān)系,那所有接入的器件保持高電平,這條線才是高電平。而任意一個(gè)器件輸出一個(gè)低電平,那這條線就會(huì)保持低電平,因此可以做到任何一個(gè)器件都可以拉低電平,也就是任何一個(gè)器件都可以作為主機(jī),如圖1所示,我們添加了R63和R64兩個(gè)上拉電阻。
圖1 I2C總線的上拉電阻
雖然說任何一個(gè)設(shè)備都可以作為主機(jī),但絕大多數(shù)情況下我們都是用微處理器,也就是我們的單片機(jī)來做主機(jī),而總線上掛的多個(gè)器件,每一個(gè)都像電話機(jī)一樣有自己唯一的地址,在信息傳輸?shù)倪^程中,通過這唯一的地址可以正常識(shí)別到屬于自己的信息,在我們的KST-51開發(fā)板上,就掛接了2個(gè)I2C設(shè)備,一個(gè)是24C02,一個(gè)是PCF8591。
我們?cè)趯W(xué)習(xí)UART串行通信的時(shí)候,知道了我們的通信流程分為起始位、數(shù)據(jù)位、停止位這三部分,同理在I2C中也有起始信號(hào)、數(shù)據(jù)傳輸和停止信號(hào),如圖2所示。
圖2 I2C時(shí)序流程圖
從圖上可以看出來,I2C和UART時(shí)序流程有相似性,也有一定的區(qū)別。UART每個(gè)字節(jié)中,都有一個(gè)起始位,8個(gè)數(shù)據(jù)位和1位停止位。而I2C分為起始信號(hào),數(shù)據(jù)傳輸部分,最后是停止信號(hào)。其中數(shù)據(jù)傳輸部分,可以一次通信過程傳輸很多個(gè)字節(jié),字節(jié)數(shù)是不受限制的,而每個(gè)字節(jié)的數(shù)據(jù)最后也跟了一位,這一位叫做應(yīng)答位,通常用ACK表示,有點(diǎn)類似于UART的停止位。
下面我們一部分一部分的把I2C通信時(shí)序進(jìn)行剖析。之前我們學(xué)過了UART,所以學(xué)習(xí)I2C的過程我盡量拿UART來作為對(duì)比,這樣有助于更好的理解。但是有一點(diǎn)大家要理解清楚,就是UART通信雖然我們用了TXD和RXD兩根線,但是實(shí)際一次通信,1條線就可以完成,2條線是把發(fā)送和接收分開而已,而I2C每次通信,不管是發(fā)送還是接收,必須2條線都參與工作才能完成,為了更方便的看出來每一位的傳輸流程,我們把圖2改進(jìn)成圖3。
圖3 I2C通信流程解析
起始信號(hào):UART通信是從一直持續(xù)的高電平出現(xiàn)一個(gè)低電平標(biāo)志起始位;而I2C通信的起始信號(hào)的定義是SCL為高電平期間,SDA由高電平向低電平變化產(chǎn)生一個(gè)下降沿,表示起始信號(hào),如圖14-3中的start部分所示。
數(shù)據(jù)傳輸:首先,UART是低位在前,高位在后;而I2C通信是高位在前,低位在后。第二,UART通信數(shù)據(jù)位是固定長度,波特率分之一,一位一位固定時(shí)間發(fā)送完畢就可以了。而I2C沒有固定波特率,但是有時(shí)序的要求,要求當(dāng)SCL在低電平的時(shí)候,SDA允許變化,也就是說,發(fā)送方必須先保持SCL是低電平,才可以改變數(shù)據(jù)線SDA,輸出要發(fā)送的當(dāng)前數(shù)據(jù)的一位;而當(dāng)SCL在高電平的時(shí)候,SDA絕對(duì)不可以變化,因?yàn)檫@個(gè)時(shí)候,接收方要來讀取當(dāng)前SDA的電平信號(hào)是0還是1,因此要保證SDA的穩(wěn)定不變化,如圖14-3中的每一位數(shù)據(jù)的變化,都是在SCL的低電平位置。8為數(shù)據(jù)位后邊跟著的是一位響應(yīng)位,響應(yīng)位我們后邊還要具體介紹。
停止信號(hào):UART通信的停止位是一位固定的高電平信號(hào);而I2C通信停止信號(hào)的定義是SCL為高電平期間,SDA由低電平向高電平變化產(chǎn)生一個(gè)上升沿,表示結(jié)束信號(hào),如圖14-3中的stop部分所示。
2、I2C尋址模式
上面介紹的是I2C每一位信號(hào)的時(shí)序流程,而I2C通信在字節(jié)級(jí)的傳輸中,也有固定的時(shí)序要求。I2C通信的起始信號(hào)(Start)后,首先要發(fā)送一個(gè)從機(jī)的地址,這個(gè)地址一共有7位,緊跟著的第8位是數(shù)據(jù)方向位(R/W),‘0’表示接下來要發(fā)送數(shù)據(jù)(寫),‘1’表示接下來是請(qǐng)求數(shù)據(jù)(讀)。
我們知道,打電話的時(shí)候,當(dāng)撥通電話,接聽方撿起電話肯定要回一個(gè)“喂”,這就是告訴撥電話的人,這邊有人了。同理,這個(gè)第九位ACK實(shí)際上起到的就是這樣一個(gè)作用。當(dāng)我們發(fā)送完了這7位地址和1位方向位,如果我們發(fā)送的這個(gè)地址確實(shí)存在,那么這個(gè)地址的器件應(yīng)該回應(yīng)一個(gè)ACK‘0’,如果不存在,就沒“人”回應(yīng)ACK。
那我們寫一個(gè)簡單的程序,訪問一下我們板子上的EEPROM的地址,另外在寫一個(gè)不存在的地址,看看他們是否能回一個(gè)ACK,來了解和確認(rèn)一下這個(gè)問題。
我們板子上的EEPROM器件型號(hào)是24C02,在24C02的數(shù)據(jù)手冊(cè)3.6部分說明了,24C02的7位地址中,其中高4位是固定的1010,而低3位的地址取決于我們電路的設(shè)計(jì),由芯片上的A2、A1、A0這3個(gè)引腳的實(shí)際電平?jīng)Q定,來看一下我們的24C02的電路圖,如圖4所示。
圖4 24C02原理圖
從圖4可以看出來,我們的A2、A1、A0都是接的GND,也就是說都是0,因此我們的7位地址實(shí)際上是二進(jìn)制的1010000,也就是0x50。我們用I2C的協(xié)議來尋址0x50,另外再尋址一個(gè)不存在的地址0x62,尋址完畢后,把返回的ACK顯示到我們的1602液晶上,大家對(duì)比一下。
/***********************lcd1602.c文件程序源代碼*************************/
#include <reg52.h>
#define LCD1602_DB P0
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;
void LcdWaitReady() //等待液晶準(zhǔn)備好
{
unsigned char sta;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do
{
LCD1602_E = 1;
sta = LCD1602_DB; //讀取狀態(tài)字
LCD1602_E = 0;
} while (sta & 0x80); //bit7等于1表示液晶正忙,重復(fù)檢測直到其等于0為止
}
void LcdWriteCmd(unsigned char cmd) //寫入命令函數(shù)
{
LcdWaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}
void LcdWriteDat(unsigned char dat) //寫入數(shù)據(jù)函數(shù)
{
LcdWaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}
void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str) //顯示字符串,屏幕起始坐標(biāo)(x,y),字符串指針str
{
unsigned char addr;
//由輸入的顯示坐標(biāo)計(jì)算顯示RAM的地址
if (y == 0)
addr = 0x00 + x; //第一行字符地址從0x00起始
else
addr = 0x40 + x; //第二行字符地址從0x40起始
//由起始顯示RAM地址連續(xù)寫入字符串
LcdWriteCmd(addr | 0x80); //寫入起始地址
while (*str != '\0') //連續(xù)寫入字符串?dāng)?shù)據(jù),直到檢測到結(jié)束符
{
LcdWriteDat(*str);
str++;
}
}
void LcdInit() //液晶初始化函數(shù)
{
LcdWriteCmd(0x38); //16*2顯示,5*7點(diǎn)陣,8位數(shù)據(jù)接口
LcdWriteCmd(0x0C); //顯示器開,光標(biāo)關(guān)閉
LcdWriteCmd(0x06); //文字不動(dòng),地址自動(dòng)+1
LcdWriteCmd(0x01); //清屏
}
/*************************main.c文件程序源代碼**************************/
#include <reg52.h>
#include <intrins.h>
#define I2CDelay() {_nop_();_nop_();_nop_();_nop_();}
sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;
bit I2CAddressing(unsigned char addr);
extern void LcdInit();
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
void main ()
{
bit ack;
unsigned char str[10];
LcdInit(); //初始化液晶
ack = I2CAddressing(0x50); //查詢地址為0x50的器件
str[0] = '5'; //將地址和應(yīng)答值轉(zhuǎn)換為字符串
str[1] = '0';
str[2] = ':';
str[3] = (unsigned char)ack + '0';
str[4] = '\0';
LcdShowStr(0, 0, str); //顯示到液晶上
ack = I2CAddressing(0x62); //查詢地址為0x62的器件
str[0] = '6'; //將地址和應(yīng)答值轉(zhuǎn)換為字符串
str[1] = '2';
str[2] = ':';
str[3] = (unsigned char)ack + '0';
str[4] = '\0';
LcdShowStr(8, 0, str); //顯示到液晶上
while(1)
{}
}
void I2CStart() //產(chǎn)生總線起始信號(hào)
{
I2C_SDA = 1; //首先確保SDA、SCL都是高電平
I2C_SCL = 1;
I2CDelay();
I2C_SDA = 0; //先拉低SDA
I2CDelay();
I2C_SCL = 0; //再拉低SCL
}
void I2CStop() //產(chǎn)生總線停止信號(hào)
{
I2C_SCL = 0; //首先確保SDA、SCL都是低電平
I2C_SDA = 0;
I2CDelay();
I2C_SCL = 1; //先拉高SCL
I2CDelay();
I2C_SDA = 1; //再拉高SDA
I2CDelay();
}
bit I2CWrite(unsigned char dat) //I2C總線寫操作,待寫入字節(jié)dat,返回值為從機(jī)應(yīng)答位的值
{
bit ack; //用于暫存應(yīng)答位的值
unsigned char mask; //用于探測字節(jié)內(nèi)某一位值的掩碼變量
for (mask=0x80; mask!=0; mask>>=1) //從高位到低位依次進(jìn)行
{
if ((mask&dat) == 0) //該位的值輸出到SDA上
I2C_SDA = 0;
else
I2C_SDA = 1;
I2CDelay();
I2C_SCL = 1; //拉高SCL
I2CDelay();
I2C_SCL = 0; //再拉低SCL,完成一個(gè)位周期
}
I2C_SDA = 1; //8位數(shù)據(jù)發(fā)送完后,主機(jī)釋放SDA,以檢測從機(jī)應(yīng)答
I2CDelay();
I2C_SCL = 1; //拉高SCL
I2CDelay();
ack = I2C_SDA; //讀取此時(shí)的SDA值,即為從機(jī)的應(yīng)答值
I2C_SCL = 0; //再拉低SCL完成應(yīng)答位,并保持住總線
return ack; //返回從機(jī)應(yīng)答值
}
bit I2CAddressing(unsigned char addr) //I2C尋址函數(shù),即檢查地址為addr的器件是否存在,返回值為其應(yīng)答值,即應(yīng)答則表示存在,非應(yīng)答則表示不存在
{
bit ack;
I2CStart(); //產(chǎn)生起始位,即啟動(dòng)一次總線操作
ack = I2CWrite(addr<<1); //器件地址需左移一位,因?qū)ぶ访畹淖畹臀粸樽x寫位,用于表示之后的操作是讀或?qū)?/P>
I2CStop(); //不需進(jìn)行后續(xù)讀寫,而直接停止本次總線操作
return ack;
}
我們把這個(gè)程序在KST-51開發(fā)板上運(yùn)行完畢,會(huì)在液晶上邊顯示出來我們預(yù)想的結(jié)果,主機(jī)發(fā)送一個(gè)存在的從機(jī)地址,從機(jī)會(huì)回復(fù)一個(gè)應(yīng)答位;主機(jī)如果發(fā)送一個(gè)不存在的從機(jī)地址,就沒有從機(jī)應(yīng)答。
前邊我有提到過有一個(gè)利用庫函數(shù)_nop_()來進(jìn)行精確延時(shí),一個(gè)_nop_()的時(shí)間就是一個(gè)機(jī)器周期,這個(gè)庫函數(shù)是包含在了intrins.h這個(gè)庫文件中,我們?nèi)绻褂眠@個(gè)庫函數(shù),只需要在程序最開始,和包含reg52.h一樣,include<intrins.h>之后,我們程序就可以直接使用這個(gè)庫函數(shù)了。
還有一點(diǎn)要提一下,I2C通信分為低速模式100kbit/s,快速模式400kbit/s和高速模式3.4Mbit/s。因?yàn)樗械腎2C器件都支持低速,但卻未必支持另外兩種速度,所以作為通用的I2C程序我們選擇100k這個(gè)速率來實(shí)現(xiàn),也就是說實(shí)際程序產(chǎn)生的時(shí)序必須小于等于100k的時(shí)序參數(shù),很明顯也就是要求SCL的高低電平持續(xù)時(shí)間都不短于5us,因此我們?cè)跁r(shí)序函數(shù)中通過插入I2CDelay()這個(gè)總線延時(shí)函數(shù)(它實(shí)際上就是4個(gè)NOP指令,用define在文件開頭做了定義),加上改變SCL值語句本身占用的至少一個(gè)周期,來達(dá)到這個(gè)速度限制。如果以后需要提高速度,那么只需要減小這里的總線延時(shí)時(shí)間即可。
此外我們要學(xué)習(xí)一個(gè)發(fā)送數(shù)據(jù)的技巧,就是I2C通信時(shí)如何將一個(gè)字節(jié)的數(shù)據(jù)發(fā)送出去。大家注意寫函數(shù)中,我用的那個(gè)for循環(huán)的技巧。for (mask=0x80; mask!=0; mask>>=1),由于I2C通信是從高位開始發(fā)送數(shù)據(jù),所以我們先從最高位開始,0x80和dat進(jìn)行按位與運(yùn)算,從而得知dat第7位是0還是1,然后右移一位,也就是變成了用0x40和dat按位與運(yùn)算,得到第6位是0還是1,一直到第0位結(jié)束,最終通過if語句,把dat的8位數(shù)據(jù)依次發(fā)送了出去。其他的邏輯大家對(duì)照前邊講到的理論知識(shí),認(rèn)真研究明白就可以了。
編輯:admin 最后修改時(shí)間:2018-05-08