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