I2C每一位信號的時(shí)序流程,而I2C通信在字節(jié)級的傳輸中,也有固定的時(shí)序要求。I2C通信的起始信號(Start)后,首先要發(fā)送一個(gè)從機(jī)的地址,這個(gè)地址一共有 7位,緊跟著的第 8 位是數(shù)據(jù)方向位(R/W),“0”表示接下來要發(fā)送數(shù)據(jù)(寫),‘“1”表示接下來是請求數(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(拉低 SDA 即輸出“0”),如果不存在,就沒“人”回應(yīng) ACK(SDA將保持高電平即“1”)。
那我們寫一個(gè)簡單的程序,訪問一下我們板子上的 EEPROM 的地址,另外再寫一個(gè)不存在的地址,看看它們是否能回一個(gè) ACK,來了解和確認(rèn)一下這個(gè)問題。
我們板子上的 EEPROM 器件型號是 24C02,在 24C02 的數(shù)據(jù)手冊 3.6 節(jié)中可查到,24C02的 7 位地址中,其中高 4 位是固定的 0b1010,而低 3 位的地址取決于具體電路的設(shè)計(jì),由芯片上的 A2、A1、A0 這 3 個(gè)引腳的實(shí)際電平?jīng)Q定,來看一下我們的 24C02 的電路圖,它和24C01 的原理圖完全一樣,如圖 14-4 所示。
圖 14-4 24C02 原理圖
從圖 14-4 可以看出來,我們的 A2、A1、A0 都是接的 GND,也就是說都是 0,因此 24C02的 7 位地址實(shí)際上是二進(jìn)制的 0b1010000,也就是 0x50。我們用 I2C 的協(xié)議來尋址 0x50,另外再尋址一個(gè)不存在的地址 0x62,尋址完畢后,把返回的 ACK 顯示到我們的 1602 液晶上,大家對比一下。
/***************************Lcd1602.c 文件程序源代碼*****************************/
#include
#define LCD1602_DB P0
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;
/* 等待液晶準(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 為止
}
/* 向 LCD1602 液晶寫入一字節(jié)命令,cmd-待寫入命令值 */
void LcdWriteCmd(unsigned char cmd){
LcdWaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}
/* 向 LCD1602 液晶寫入一字節(jié)數(shù)據(jù),dat-待寫入數(shù)據(jù)值 */
void LcdWriteDat(unsigned char dat){
LcdWaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}
/* 設(shè)置顯示 RAM 起始地址,亦即光標(biāo)位置,(x,y)-對應(yīng)屏幕上的字符坐標(biāo) */
void LcdSetCursor(unsigned char x, unsigned char y){
unsigned char addr;
if (y == 0){ //由輸入的屏幕坐標(biāo)計(jì)算顯示 RAM 的地址
addr = 0x00 + x; //第一行字符地址從 0x00 起始
}else{
addr = 0x40 + x; //第二行字符地址從 0x40 起始
}
LcdWriteCmd(addr | 0x80); //設(shè)置 RAM 地址
}
/* 在液晶上顯示字符串,(x,y)-對應(yīng)屏幕上的起始坐標(biāo),str-字符串指針 */
void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str){
LcdSetCursor(x, y);//設(shè)置起始地址
while (*str != ‘’){ //連續(xù)寫入字符串?dāng)?shù)據(jù),直到檢測到結(jié)束符
LcdWriteDat(*str++);
}
}
/* 初始化 1602 液晶 */
void InitLcd1602(){
LcdWriteCmd(0x38); //16*2 顯示,5*7 點(diǎn)陣,8 位數(shù)據(jù)接口
LcdWriteCmd(0x0C); //顯示器開,光標(biāo)關(guān)閉
LcdWriteCmd(0x06); //文字不動,地址自動+1
LcdWriteCmd(0x01); //清屏
}
/*****************************main.c 文件程序源代碼******************************/
#include
#include
#define I2CDelay() {_nop_();_nop_();_nop_();_nop_();}
sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;
bit I2CAddressing(unsigned char addr);
extern void InitLcd1602();
extern void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str);
void main(){
bit ack;
unsigned char str[10];
InitLcd1602(); //初始化液晶
ack = I2CAddressing(0x50); //查詢地址為 0x50 的器件
str[0] = ‘5’; //將地址和應(yīng)答值轉(zhuǎn)換為字符串
str[1] = ‘0’;
str[2] = ‘:’;
str[3] = (unsigned char)ack + ‘0’;
str[4] = ‘’;
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] = ‘’;
LcdShowStr(8, 0, str); //顯示到液晶上
while (1);
}
/* 產(chǎn)生總線起始信號 */
void I2CStart(){
I2C_SDA = 1; //首先確保 SDA、SCL 都是高電平
I2C_SCL = 1;
I2CDelay();
I2C_SDA = 0; //先拉低 SDA
I2CDelay();
I2C_SCL = 0; //再拉低 SCL
}
/* 產(chǎn)生總線停止信號 */
void I2CStop(){
I2C_SCL = 0; //首先確保 SDA、SCL 都是低電平
I2C_SDA = 0;
I2CDelay();
I2C_SCL = 1; //先拉高 SCL
I2CDelay();
I2C_SDA = 1; //再拉高 SDA
I2CDelay();
}
/* I2C 總線寫操作,dat-待寫入字節(jié),返回值-從機(jī)應(yīng)答位的值 */
bit I2CWrite(unsigned char dat){
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
ack = I2C_SDA; //讀取此時(shí)的 SDA 值,即為從機(jī)的應(yīng)答值
I2CDelay();
I2C_SCL = 0; //再拉低 SCL 完成應(yīng)答位,并保持住總線
return ack; //返回從機(jī)應(yīng)答值
}
/* I2C 尋址函數(shù),即檢查地址為 addr 的器件是否存在,返回值-從器件應(yīng)答值 */
bit I2CAddressing(unsigned char addr){
bit ack;
I2CStart(); //產(chǎn)生起始位,即啟動一次總線操作
//器件地址需左移一位,因?qū)ぶ访畹淖畹臀?/p>
//為讀寫位,用于表示之后的操作是讀或?qū)?/p>
ack = I2CWrite(addr《《1);
I2CStop(); //不需進(jìn)行后續(xù)讀寫,而直接停止本次總線操作
return ack;
}
我們把這個(gè)程序在 KST-51開發(fā)板上運(yùn)行完畢,會在液晶上邊顯示出來我們預(yù)想的結(jié)果,主機(jī)發(fā)送一個(gè)存在的從機(jī)地址,從機(jī)會回復(fù)一個(gè)應(yīng)答位,即應(yīng)答位為 0;主機(jī)如果發(fā)送一個(gè)不存在的從機(jī)地址,就沒有從機(jī)應(yīng)答,即應(yīng)答位為 1。
前面的章節(jié)中已經(jīng)提到利用庫函數(shù)_nop_()可以進(jìn)行精確延時(shí),一個(gè)_nop_()的時(shí)間就是一個(gè)機(jī)器周期,這個(gè)庫函數(shù)包含在 intrins.h 這個(gè)文件中,如果要使用這個(gè)庫函數(shù),只需要在程序最開始,和包含 reg52.h 一樣,include之后,程序中就可以使用這個(gè)庫函數(shù)了。
還有一點(diǎn)要提一下,I2C通信分為低速模式 100kbit/s、快速模式 400kbit/s 和高速模式3.4Mbit/s。因?yàn)樗械?I2C 器件都支持低速,但卻未必支持另外兩種速度,所以作為通用的I2C 程序我們選擇 100k 這個(gè)速率來實(shí)現(xiàn),也就是說實(shí)際程序產(chǎn)生的時(shí)序必須小于等于 100k的時(shí)序參數(shù),很明顯也就是要求 SCL 的高低電平持續(xù)時(shí)間都不短于 5us,因此我們在時(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ù) I2CWrite 中,用的那個(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ā)送了出去。其它的邏輯大家對照前邊講到的理論知識,認(rèn)真研究明白就可以了。
責(zé)任編輯;zl
評論
查看更多