標(biāo)題: 第14章 I2C總線與EEPROM [打印本頁]
作者: admin 時(shí)間: 2013-9-28 15:25
標(biāo)題: 第14章 I2C總線與EEPROM
本教材現(xiàn)以連載的方式由網(wǎng)絡(luò)發(fā)布,并將于2014年由清華大學(xué)出版社出版最終完整版,版權(quán)歸作者和清華大學(xué)出版社所有。本著開源、分享的理念,本教材可以自由傳播及學(xué)習(xí)使用,但是務(wù)必請(qǐng)注明出處來自金沙灘工作室
前幾章我們學(xué)了一種通信協(xié)議叫做UART異步串口通信,這節(jié)課我們要來學(xué)習(xí)第二種常用的通信協(xié)議I2C。I2C總線是由PHILIPS公司開發(fā)的兩線式串行總線,多用于連接微處理器及其外圍設(shè)備。I2C總線的主要特點(diǎn)是接口方式簡(jiǎ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之間的通信。
14.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ī),如圖14-1所示,我們添加了R63和R64兩個(gè)上拉電阻。
1.JPG (7.68 KB, 下載次數(shù): 242)
下載附件
2013-9-28 15:13 上傳
圖14-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),如圖14-2所示。
2.JPG (24.46 KB, 下載次數(shù): 237)
下載附件
2013-9-28 15:13 上傳
圖14-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條線都參與工作才能完成,為了更方便的看出來每一位的傳輸流程,我們把圖14-2改進(jìn)成圖14-3。
3.JPG (28.18 KB, 下載次數(shù): 225)
下載附件
2013-9-28 15:13 上傳
圖14-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ù)位是固定長(zhǎng)度,波特率分之一,一位一位固定時(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部分所示。
14.2 I2C尋址模式上一節(jié)介紹的是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è)簡(jiǎn)單的程序,訪問一下我們板子上的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的電路圖,如圖14-4所示。
4.JPG (20.29 KB, 下載次數(shù): 246)
下載附件
2013-9-28 15:13 上傳
圖14-4 24C02原理圖
從圖14-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ù)檢測(cè)直到其等于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ù),直到檢測(cè)到結(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; //用于探測(cè)字節(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,以檢測(cè)從機(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ū)?/font>
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è)利用庫(kù)函數(shù)_nop_()來進(jìn)行精確延時(shí),一個(gè)_nop_()的時(shí)間就是一個(gè)機(jī)器周期,這個(gè)庫(kù)函數(shù)是包含在了intrins.h這個(gè)庫(kù)文件中,我們?nèi)绻褂眠@個(gè)庫(kù)函數(shù),只需要在程序最開始,和包含reg52.h一樣,include<intrins.h>之后,我們程序就可以直接使用這個(gè)庫(kù)函數(shù)了。
還有一點(diǎn)要提一下,I2C通信分為低速模式100kbit/s,快速模式400kbit/s和高速模式3.4Mbit/s。因?yàn)樗械?/font>I2C器件都支持低速,但卻未必支持另外兩種速度,所以作為通用的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)真研究明白就可以了。
1.3 EEPROM的學(xué)習(xí)在實(shí)際的應(yīng)用中,保存在單片機(jī)RAM中的數(shù)據(jù),掉電后數(shù)據(jù)就丟失了,保存在單片機(jī)的FLASH中的數(shù)據(jù),又不能隨意改變,也就是不能用它來記錄變化的數(shù)值。但是在某些場(chǎng)合,我們又確實(shí)需要記錄下某些數(shù)據(jù),而它們還時(shí)常需要改變或更新,掉電之后數(shù)據(jù)還不能丟失,比如我們的家用電表度數(shù),我們的電視機(jī)里邊的頻道記憶,一般都是使用EEPROM來保存數(shù)據(jù),特點(diǎn)就是掉電后不丟失。我們板子上使用的這個(gè)器件是24C02,是一個(gè)容量大小是2Kbit位,也就是256個(gè)字節(jié)的EEPROM。一般情況下,EEPROM擁有30萬到100萬次的壽命,也就是它可以反復(fù)寫入30-100萬次,而讀取次數(shù)是無限的。
24C02是一個(gè)基于I2C通信協(xié)議的器件,因此從現(xiàn)在開始,我們的I2C和我們的EEPROM就要合體了。但是大家要分清楚,I2C是一個(gè)通信協(xié)議,它擁有嚴(yán)密的通信時(shí)序邏輯要求,而EEPROM是一個(gè)器件,只是這個(gè)器件采樣了I2C協(xié)議的接口與單片機(jī)相連而已,二者并沒有必然的聯(lián)系,EEPROM可以用其他接口,I2C也可以用在其它很多器件上。
14.3.1 EEPROM單字節(jié)讀寫操作時(shí)序1、EEPROM寫數(shù)據(jù)流程
第一步,首先是I2C的起始信號(hào),接著跟上首字節(jié),也就是我們前邊講的I2C的器件地
址(EERPOM),并且在讀寫方向上選擇“寫”操作。
第二步,發(fā)送數(shù)據(jù)的存儲(chǔ)地址。我們24C02一共256個(gè)字節(jié)的存儲(chǔ)空間,地址從0x00到0xFF,我們想把數(shù)據(jù)存儲(chǔ)在哪個(gè)位置,此刻寫的就是哪個(gè)地址。
第三步,發(fā)送要存儲(chǔ)的數(shù)據(jù)第一個(gè)字節(jié),第二個(gè)字節(jié)......注意在寫數(shù)據(jù)的過程中,EEPROM每個(gè)字節(jié)都會(huì)回應(yīng)一個(gè)“應(yīng)答位0”,來告訴我們寫EEPROM數(shù)據(jù)成功,如果沒有回應(yīng)答位,說明寫入不成功。
在寫數(shù)據(jù)的過程中,每成功寫入一個(gè)字節(jié),EEPROM存儲(chǔ)空間的地址就會(huì)自動(dòng)加1,當(dāng)加到0xFF后,再寫一個(gè)字節(jié),地址會(huì)溢出又變成了0x00。
2、EEPROM讀數(shù)據(jù)流程
第一步,首先是I2C的起始信號(hào),接著跟上首字節(jié),也就是我們前邊講的I2C的器件地
址(EERPOM),并且在讀寫方向上選擇“寫”操作。這個(gè)地方可能有同學(xué)會(huì)詫異,我們明明是讀數(shù)據(jù)為何方向也要選“寫”呢?剛才說過了,我們24C02一共有256個(gè)地址,我們選擇寫操作,是為了把所要讀的數(shù)據(jù)的存儲(chǔ)地址先寫進(jìn)去,告訴EEPROM我們要讀取哪個(gè)地址的數(shù)據(jù)。這就如同我們打電話,先撥總機(jī)號(hào)碼(EEPROM器件地址),而后還要繼續(xù)撥分機(jī)號(hào)碼(數(shù)據(jù)地址),而撥分機(jī)號(hào)碼這個(gè)動(dòng)作,主機(jī)仍然是發(fā)送方,方向依然是“寫”。
第二步,發(fā)送要讀取的數(shù)據(jù)的地址,注意是地址而非存在EEPROM中的數(shù)據(jù),通知EEPROM我要哪個(gè)分機(jī)的信息。
第三步,重新發(fā)送I2C起始信號(hào)和器件地址,并且在方向位選擇“讀”操作。
這三步當(dāng)中,每一個(gè)字節(jié)實(shí)際上都是在“寫”,所以每一個(gè)字節(jié)EEPROM都會(huì)回應(yīng)一個(gè)“應(yīng)答位0”。
第四步,讀取從器件發(fā)回的數(shù)據(jù),讀一個(gè)字節(jié),如果還想繼續(xù)讀下一個(gè)字節(jié),就發(fā)送一個(gè)“應(yīng)答位ACK(0)”,如果不想讀了,告訴EEPROM,我不想要數(shù)據(jù)了,別再發(fā)數(shù)據(jù)了,那就發(fā)送一個(gè)“非應(yīng)答位NACK(1)”。
和寫操作規(guī)則一樣,我們每讀一個(gè)字節(jié),地址會(huì)自動(dòng)加1,那如果我們想繼續(xù)往下讀,給EEPROM一個(gè)ACK(0)低電平,那再繼續(xù)給SCL完整的時(shí)序,EEPROM會(huì)繼續(xù)往外送數(shù)據(jù)。如果我們不想讀了,要告訴EEPROM不要數(shù)據(jù)了,那我們直接給一個(gè)NAK(1)高電平即可。這個(gè)地方大家要從邏輯上理解透徹,不能簡(jiǎn)單的靠死記硬背了,一定要理解明白。梳理一下幾個(gè)要點(diǎn):A、在本例中單片機(jī)是主機(jī),24C02是從機(jī);B、無論是讀是寫,SCL始終都是由主機(jī)控制的;C、寫的時(shí)候應(yīng)答信號(hào)由從機(jī)給出,表示從機(jī)是否正確接收了數(shù)據(jù);D、讀的時(shí)候應(yīng)答信號(hào)則由主機(jī)給出,表示是否繼續(xù)讀下去。
那我們下面寫一個(gè)程序,讀取EEPROM的0x02這個(gè)地址上的一個(gè)數(shù)據(jù),不管這個(gè)數(shù)據(jù)之前是多少,我們都再將讀出來的數(shù)據(jù)加1,再寫到EEPROM的0x02這個(gè)地址上。此外我們將I2C的程序建立一個(gè)文件,寫一個(gè)I2C.c程序文件,形成我們又一個(gè)程序模塊。大家也可以看出來,我們連續(xù)的這幾個(gè)程序,lcd1602.c文件里的程序都是一樣的,今后我們大家寫1602顯示程序也可以直接拿過去用,大大提高了程序移植的方便性。
/*************************I2C.c文件程序源代碼***************************/
#include <reg52.h>
#include <intrins.h>
#define I2CDelay() {_nop_();_nop_();_nop_();_nop_();}
sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;
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,返回值為應(yīng)答狀態(tài)
{
bit ack; //用于暫存應(yīng)答位的值
unsigned char mask; //用于探測(cè)字節(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,以檢測(cè)從機(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); //應(yīng)答值取反以符合通常的邏輯:0=不存在或忙或?qū)懭胧。?/font>1=存在且空閑或?qū)懭氤晒?/font>
}
unsigned char I2CReadNAK() //I2C總線讀操作,并發(fā)送非應(yīng)答信號(hào),返回值為讀到的字節(jié)
{
unsigned char mask;
unsigned char dat;
I2C_SDA = 1; //首先確保主機(jī)釋放SDA
for (mask=0x80; mask!=0; mask>>=1) //從高位到低位依次進(jìn)行
{
I2CDelay();
I2C_SCL = 1; //拉高SCL
if(I2C_SDA == 0) //讀取SDA的值
dat &= ~mask; //為0時(shí),dat中對(duì)應(yīng)位清零
else
dat |= mask; //為1時(shí),dat中對(duì)應(yīng)位置1
I2CDelay();
I2C_SCL = 0; //再拉低SCL,以使從機(jī)發(fā)送出下一位
}
I2C_SDA = 1; //8位數(shù)據(jù)發(fā)送完后,拉高SDA,發(fā)送非應(yīng)答信號(hào)
I2CDelay();
I2C_SCL = 1; //拉高SCL
I2CDelay();
I2C_SCL = 0; //再拉低SCL完成非應(yīng)答位,并保持住總線
return dat;
}
unsigned char I2CReadACK() //I2C總線讀操作,并發(fā)送應(yīng)答信號(hào),返回值為讀到的字節(jié)
{
unsigned char mask;
unsigned char dat;
I2C_SDA = 1; //首先確保主機(jī)釋放SDA
for (mask=0x80; mask!=0; mask>>=1) //從高位到低位依次進(jìn)行
{
I2CDelay();
I2C_SCL = 1; //拉高SCL
if(I2C_SDA == 0) //讀取SDA的值
dat &= ~mask; //為0時(shí),dat中對(duì)應(yīng)位清零
else
dat |= mask; //為1時(shí),dat中對(duì)應(yīng)位置1
I2CDelay();
I2C_SCL = 0; //再拉低SCL,以使從機(jī)發(fā)送出下一位
}
I2C_SDA = 0; //8位數(shù)據(jù)發(fā)送完后,拉低SDA,發(fā)送應(yīng)答信號(hào)
I2CDelay();
I2C_SCL = 1; //拉高SCL
I2CDelay();
I2C_SCL = 0; //再拉低SCL完成應(yīng)答位,并保持住總線
return dat;
}
/***********************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ù)檢測(cè)直到其等于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ù),直到檢測(cè)到結(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>
extern void LcdInit();
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern void I2CStart();
extern void I2CStop();
extern unsigned char I2CReadACK();
extern unsigned char I2CReadNAK();
extern bit I2CWrite(unsigned char dat);
unsigned char E2ReadByte(unsigned char addr);
void E2WriteByte(unsigned char addr, unsigned char dat);
void main ()
{
unsigned char dat;
unsigned char str[10];
LcdInit(); //初始化液晶
dat = E2ReadByte(0x02); //讀取指定地址上的一個(gè)字節(jié)
str[0] = (dat/100) + '0'; //轉(zhuǎn)換為十進(jìn)制字符串格式
str[1] = (dat/10%10) + '0';
str[2] = (dat%10) + '0';
str[3] = '\0';
LcdShowStr(0, 0, str); //顯示在液晶上
dat++; //將其數(shù)值+1
E2WriteByte(0x02, dat); //再寫回到對(duì)應(yīng)的地址上
while(1)
{}
}
unsigned char E2ReadByte(unsigned char addr) //讀取EEPROM中的一個(gè)字節(jié),字節(jié)地址addr
{
unsigned char dat;
I2CStart();
I2CWrite(0x50<<1); //尋址器件,后續(xù)為寫操作
I2CWrite(addr); //寫入存儲(chǔ)地址
I2CStart(); //發(fā)送重復(fù)啟動(dòng)信號(hào)
I2CWrite((0x50<<1)|0x01); //尋址器件,后續(xù)為讀操作
dat = I2CReadNAK(); //讀取一個(gè)字節(jié)數(shù)據(jù)
I2CStop();
return dat;
}
void E2WriteByte(unsigned char addr, unsigned char dat) //向EEPROM中寫入一個(gè)字節(jié),字節(jié)地址addr
{
I2CStart();
I2CWrite(0x50<<1); //尋址器件,后續(xù)為寫操作
I2CWrite(addr); //寫入存儲(chǔ)地址
I2CWrite(dat); //寫入一個(gè)字節(jié)數(shù)據(jù)
I2CStop();
}
/***********************************************************************/
這個(gè)程序,以同學(xué)們現(xiàn)在的基礎(chǔ),獨(dú)立分析應(yīng)該不困難了,遇到哪個(gè)語句不懂可以及時(shí)問問別人或者搜索一下,把該解決的問題理解明白。大家把這個(gè)程序復(fù)制過去后,編譯一下會(huì)發(fā)現(xiàn)Keil軟件提示了一個(gè)警告:*** WARNING L16: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS,這個(gè)警告的意思是有我們代碼中存在沒有被調(diào)用過的變量或者函數(shù)。
大家仔細(xì)觀察一下,這個(gè)程序,我們讀取EEPROM的時(shí)候,只讀了一個(gè)字節(jié)我們就要告訴EEPROM不需要再讀數(shù)據(jù)了,因此我們讀完后直接回復(fù)一個(gè)“NAK”,因此我們只調(diào)用了I2CReadNAK()這個(gè)函數(shù),而并沒有調(diào)用I2CReadACK()這個(gè)函數(shù)。我們今后很可能讀數(shù)據(jù)的時(shí)候要連續(xù)讀幾個(gè)字節(jié),因此這個(gè)函數(shù)寫在了I2C.c文件中,作為I2C功能模塊的一部分是必要的,方便我們這個(gè)文件以后移植到其他程序中使用,因此這個(gè)警告在這里就不必管它了。
14.3.2 EEPROM多字節(jié)讀寫操作時(shí)序[size=14.0000pt]我們讀取EEPROM的時(shí)候很簡(jiǎn)單,EEPROM根據(jù)我們所送的時(shí)序,直接就把數(shù)據(jù)送出來了,但是寫EEPROM卻沒有這么簡(jiǎn)單。我們?nèi)绻oEEPROM發(fā)送數(shù)據(jù)后,先保存在了EEPROM的緩存,EEPROM必須要把緩存中的數(shù)據(jù)搬移到“非易失”的區(qū)域,才能達(dá)到掉電不丟失的效果。而往非易失區(qū)域?qū)懶枰欢ǖ臅r(shí)間,每種器件不完全一樣,ATMEL公司的24C02的這個(gè)寫入時(shí)間最高不超過5ms。在往非易失區(qū)域?qū)懙倪^程,EEPROM是不會(huì)再響應(yīng)我們的訪問的,不僅接收不到我們的數(shù)據(jù),我們即使用I2C標(biāo)準(zhǔn)的尋址模式去尋址,EEPROM都不會(huì)應(yīng)答,就如同這個(gè)總線上沒有這個(gè)器件一樣。數(shù)據(jù)寫入非易失區(qū)域完畢后,EEPROM再次恢復(fù)正常,可以正常讀寫了。
細(xì)心的同學(xué),在看上一節(jié)程序的時(shí)候會(huì)發(fā)現(xiàn),我們寫數(shù)據(jù)的那段代碼,實(shí)際上我們有去讀應(yīng)答位ACK,但是讀到了應(yīng)答位我們也沒有做任何處理。這是因?yàn)槲覀円淮沃粚懸粋(gè)字節(jié)的數(shù)據(jù)進(jìn)去,等到下次重新上電再寫的時(shí)候,時(shí)間肯定遠(yuǎn)遠(yuǎn)超過了5ms,但是如果我們是連續(xù)寫入幾個(gè)字節(jié)的時(shí)候,我們就必須得考慮到應(yīng)答位的問題了。寫入一個(gè)字節(jié)后,再寫入下一個(gè)字節(jié)之前,我們必須要等待EEPROM再次響應(yīng)才可以,大家注意我的程序的寫法,可以學(xué)習(xí)一下。
之前我們知道編寫多.c文件移植的方便性了,本節(jié)程序和上一節(jié)的lcd1602.c文件和I2C.c文件完全是一樣的,因此這次我們只把main.c文件給大家發(fā)出來,幫大家分析明白。而同學(xué)們卻不能這樣,同學(xué)們是初學(xué),很多知識(shí)和技巧需要多練才能鞏固下來,因此每個(gè)程序還是建議大家在你的Keil軟件上一個(gè)代碼一個(gè)代碼的敲出來。
/***********************lcd1602.c文件程序源代碼*************************/
略
/************************I2C.c文件程序源代碼***************************/
略
/************************main.c文件程序源代碼**************************/
#include <reg52.h>
extern void LcdInit();
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern void I2CStart();
extern void I2CStop();
extern unsigned char I2CReadACK();
extern unsigned char I2CReadNAK();
extern bit I2CWrite(unsigned char dat);
void E2Read(unsigned char *buf, unsigned char addr, unsigned char len);
void E2Write(unsigned char *buf, unsigned char addr, unsigned char len);
void ArrayToHexStr(unsigned char *str, unsigned char *array, unsigned char len);
void main ()
{
unsigned char i;
unsigned char buf[5];
unsigned char str[20];
LcdInit(); //初始化液晶
E2Read(buf, 0x90, sizeof(buf)); //從E2中讀取一段數(shù)據(jù)
ArrayToHexStr(str, buf, sizeof(buf)); //轉(zhuǎn)換為十六進(jìn)制字符串
LcdShowStr(0, 0, str); //顯示到液晶上
for (i=0; i<sizeof(buf); i++) //數(shù)據(jù)依次+1,+2,+3...
{
buf[ i] = buf[ i] + 1 + i;
}
E2Write(buf, 0x90, sizeof(buf)); //再寫回到E2中
while(1)
{}
}
void ArrayToHexStr(unsigned char *str, unsigned char *array, unsigned char len) //把一個(gè)字節(jié)數(shù)組轉(zhuǎn)換為十六進(jìn)制字符串的格式
{
unsigned char tmp;
while (len--)
{
tmp = *array >> 4; //先取高4位
if (tmp <= 9) //轉(zhuǎn)換為0-9或A-F
*str = tmp + '0';
else
*str = tmp - 10 + 'A';
str++;
tmp = *array & 0x0F; //再取低4位
if (tmp <= 9) //轉(zhuǎn)換為0-9或A-F
*str = tmp + '0';
else
*str = tmp - 10 + 'A';
str++;
*str = ' '; //轉(zhuǎn)換完一個(gè)字節(jié)添加一個(gè)空格
str++;
array++;
}
}
void E2Read(unsigned char *buf, unsigned char addr, unsigned char len) //E2讀取函數(shù),數(shù)據(jù)接收指針buf,E2中的起始地址addr,讀取長(zhǎng)度len
{
do { //用尋址操作查詢當(dāng)前是否可進(jìn)行讀寫操作
I2CStart();
if (I2CWrite(0x50<<1)) //器件應(yīng)答則跳出循環(huán),繼續(xù)執(zhí)行,非應(yīng)答則進(jìn)行下一次查詢
break;
I2CStop();
} while(1);
I2CWrite(addr); //寫入起始地址
I2CStart(); //發(fā)送重復(fù)啟動(dòng)信號(hào)
I2CWrite((0x50<<1)|0x01); //尋址器件,后續(xù)為讀操作
while (len > 1) //連續(xù)讀取len-1個(gè)字節(jié)
{
*buf = I2CReadACK(); //最后字節(jié)之前為讀取操作+應(yīng)答
buf++;
len--;
}
*buf = I2CReadNAK(); //最后一個(gè)字節(jié)為讀取操作+非應(yīng)答
I2CStop();
}
void E2Write(unsigned char *buf, unsigned char addr, unsigned char len) //E2寫入函數(shù),源數(shù)據(jù)指針buf,E2中的起始地址addr,寫入長(zhǎng)度len
{
while (len--)
{
do { //用尋址操作查詢當(dāng)前是否可進(jìn)行讀寫操作,即等待上一次寫入操作完成
I2CStart();
if (I2CWrite(0x50<<1)) //器件應(yīng)答則跳出循環(huán),繼續(xù)執(zhí)行,非應(yīng)答則進(jìn)行下一次查詢
break;
I2CStop();
} while(1);
I2CWrite(addr); //寫入起始地址
I2CWrite(*buf); //寫入一個(gè)字節(jié)數(shù)據(jù)
I2CStop(); //結(jié)束寫操作,以等待寫入完成
buf++; //數(shù)據(jù)指針遞增
addr++; //E2地址遞增
}
}
函數(shù)ArrayToHexStr:這是一個(gè)把數(shù)組轉(zhuǎn)換成十六進(jìn)制字符串的形式。由于我們從EEPROM讀出來的是正常的數(shù)據(jù),而1602液晶接收的是ASCII碼字符,因此我們要通過液晶把數(shù)據(jù)顯示出來必須先通過一步轉(zhuǎn)換。算法倒是很簡(jiǎn)單,就是把每一個(gè)字節(jié)的數(shù)據(jù)高4位和低4位分開,和9進(jìn)行比較,如果小于等于9,則通過數(shù)字加’0’轉(zhuǎn)ASCII碼發(fā)送;如果大于9,則通過加’A’轉(zhuǎn)ASCII碼發(fā)送出去。
函數(shù)E2Read:我們?cè)谧x之前,要查詢一下當(dāng)前是否可以進(jìn)行讀寫操作,EEPROM正常響應(yīng)才可以進(jìn)行。進(jìn)行后,最后一個(gè)字節(jié)之前的,全部給出ACK,而讀完了最后一個(gè)字節(jié),我們要給出一個(gè)NAK。
函數(shù)E2Write:每次寫操作之前,我們都要進(jìn)行查詢判斷當(dāng)前EEPROM是否響應(yīng),正常響應(yīng)后才可以寫數(shù)據(jù)。
14.3.3 EEPROM的頁寫入如果每個(gè)數(shù)據(jù)都連續(xù)寫入,像我們上節(jié)課那樣寫的時(shí)候,每次都先起始位,再訪問一下這個(gè)EEPROM的地址,看看是否響應(yīng),感覺上效率太低了。因此EEPROM的廠商就想了一個(gè)辦法,把EEPROM分頁管理。24c01、24c02這兩個(gè)型號(hào)是8個(gè)字節(jié)一個(gè)頁,而24c04、24c08、24c16是16個(gè)字節(jié)一頁。我們板子上的型號(hào)是24C02,一共是256個(gè)字節(jié),8個(gè)字節(jié)一頁,那么就一共有32頁。
分配好頁之后,如果我們?cè)谕粋(gè)頁內(nèi)連續(xù)寫入幾個(gè)字節(jié)后,最后再發(fā)送停止位的時(shí)序。EEPROM檢測(cè)到這個(gè)停止位后,統(tǒng)一把這一頁的數(shù)據(jù)寫到非易失區(qū)域,就不需要像上節(jié)課那樣寫一個(gè)字節(jié)檢測(cè)一次了,并且頁寫入的時(shí)間也不會(huì)超過5ms。如果我們寫入的數(shù)據(jù)跨頁了,那么寫完了一頁之后,我們要發(fā)送一個(gè)停止位,然后等待并且檢測(cè)EEPROM的空閑模式,一直等到把上一頁數(shù)據(jù)完全寫到非易失區(qū)域后,再進(jìn)行下一頁的寫入,這樣就可以在一定程度上提高我們的寫入效率。
/***********************lcd1602.c文件程序源代碼*************************/
略
/************************I2C.c文件程序源代碼***************************/
略
/***********************eeprom.c文件程序源代碼*************************/
#include <reg52.h>
extern void I2CStart();
extern void I2CStop();
extern unsigned char I2CReadACK();
extern unsigned char I2CReadNAK();
extern bit I2CWrite(unsigned char dat);
void E2Read(unsigned char *buf, unsigned char addr, unsigned char len) //E2讀取函數(shù),數(shù)據(jù)接收指針buf,E2中的起始地址addr,讀取長(zhǎng)度len
{
do { //用尋址操作查詢當(dāng)前是否可進(jìn)行讀寫操作
I2CStart();
if (I2CWrite(0x50<<1)) //器件應(yīng)答則跳出循環(huán),繼續(xù)執(zhí)行,非應(yīng)答則進(jìn)行下一次查詢
break;
I2CStop();
} while(1);
I2CWrite(addr); //寫入起始地址
I2CStart(); //發(fā)送重復(fù)啟動(dòng)信號(hào)
I2CWrite((0x50<<1)|0x01); //尋址器件,后續(xù)為讀操作
while (len > 1) //連續(xù)讀取len-1個(gè)字節(jié)
{
*buf = I2CReadACK(); //最后字節(jié)之前為讀取操作+應(yīng)答
buf++;
len--;
}
*buf = I2CReadNAK(); //最后一個(gè)字節(jié)為讀取操作+非應(yīng)答
I2CStop();
}
void E2Write(unsigned char *buf, unsigned char addr, unsigned char len) //E2寫入函數(shù),源數(shù)據(jù)指針buf,E2中的起始地址addr,寫入長(zhǎng)度len
{
while (len > 0)
{
//等待上次寫入操作完成
do {
I2CStart();
if (I2CWrite(0x50<<1)) //器件應(yīng)答則跳出循環(huán),繼續(xù)執(zhí)行,非應(yīng)答則進(jìn)行下一次查詢
break;
I2CStop();
} while(1);
//按頁寫模式連續(xù)寫入字節(jié)
I2CWrite(addr); //寫入起始地址
while (len > 0)
{
I2CWrite(*buf); //寫入一個(gè)字節(jié)數(shù)據(jù)
len--; //待寫入長(zhǎng)度計(jì)數(shù)遞減
buf++; //數(shù)據(jù)指針遞增
addr++; //E2地址遞增
if ((addr&0x07) == 0) //檢查地址是否到達(dá)頁邊界,24C02每頁8字節(jié),所以檢測(cè)低3位是否為零即可
break; //到達(dá)頁邊界時(shí),跳出循環(huán),結(jié)束本次寫操作
}
I2CStop();
}
}
這個(gè)eeprom.c文件中的程序,單獨(dú)做一個(gè)文件,用來管理eeprom的訪問。其中E2Read函數(shù)和上一節(jié)是一樣的,因?yàn)樽x操作和是否同一頁無關(guān)。重點(diǎn)是E2Write函數(shù),我們?cè)趯懭霐?shù)據(jù)的時(shí)候,要計(jì)算下一個(gè)要寫的數(shù)據(jù)的地址是否是一個(gè)頁的起始地址,如果是的話,則必須跳出循環(huán),等待EEPROM上一頁寫入到非易失區(qū)域后,再進(jìn)行繼續(xù)寫入。
而寫了eeprom.c后,main.c文件里的程序就要變的簡(jiǎn)單多了,大家可以自己看一下,不需要過多解釋了。
/************************main.c文件程序源代碼**************************/
#include <reg52.h>
extern void LcdInit();
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern void E2Read(unsigned char *buf, unsigned char addr, unsigned char len);
extern void E2Write(unsigned char *buf, unsigned char addr, unsigned char len);
void ArrayToHexStr(unsigned char *str, unsigned char *array, unsigned char len);
void main ()
{
unsigned char i;
unsigned char buf[5];
unsigned char str[20];
LcdInit(); //初始化液晶
E2Read(buf, 0x8E, sizeof(buf)); //從E2中讀取一段數(shù)據(jù)
ArrayToHexStr(str, buf, sizeof(buf)); //轉(zhuǎn)換為十六進(jìn)制字符串
LcdShowStr(0, 0, str); //顯示到液晶上
for (i=0; i<sizeof(buf); i++) //數(shù)據(jù)依次+1,+2,+3...
{
buf[ i] = buf[ i] + 1 + i;
}
E2Write(buf, 0x8E, sizeof(buf)); //再寫回到E2中
while(1)
{}
}
void ArrayToHexStr(unsigned char *str, unsigned char *array, unsigned char len) //把一個(gè)字節(jié)數(shù)組轉(zhuǎn)換為十六進(jìn)制字符串的格式
{
unsigned char tmp;
while (len--)
{
tmp = *array >> 4; //先取高4位
if (tmp <= 9) //轉(zhuǎn)換為0-9或A-F
*str = tmp + '0';
else
*str = tmp - 10 + 'A';
str++;
tmp = *array & 0x0F; //再取低4位
if (tmp <= 9) //轉(zhuǎn)換為0-9或A-F
*str = tmp + '0';
else
*str = tmp - 10 + 'A';
str++;
*str = ' '; //轉(zhuǎn)換完一個(gè)字節(jié)添加一個(gè)空格
str++;
array++;
}
}
多字節(jié)寫入和頁寫入程序都編寫出來了,而且頁寫入的程序我們還特地跨頁寫的數(shù)據(jù),他們的寫入時(shí)間到底差別多大呢。我們用一些工具可以測(cè)量一下,比如示波器,邏輯分析儀等工具。我現(xiàn)在把兩次寫入時(shí)間用邏輯分析儀給抓了出來,并且用時(shí)間標(biāo)簽T1和T2給標(biāo)注了開始位置和結(jié)束位置,如圖14-5和圖14-6所示,右側(cè)顯示的|T1-T2|就是最終寫入5個(gè)字節(jié)所耗費(fèi)的時(shí)間。多字節(jié)一個(gè)一個(gè)寫入,每次寫入后都需要再次通信檢測(cè)EEPROM是否在“忙”,因此耗費(fèi)了大量的時(shí)間,同樣的寫入5個(gè)字節(jié)的數(shù)據(jù),一個(gè)一個(gè)寫入用了8.4ms左右的時(shí)間,而使用頁寫入,只用了3.5ms左右的時(shí)間。
1.JPG (50.41 KB, 下載次數(shù): 244)
下載附件
2013-9-28 15:15 上傳
圖14-5 多字節(jié)寫入時(shí)間
2.JPG (56.09 KB, 下載次數(shù): 253)
下載附件
2013-9-28 15:15 上傳
圖14-6 跨頁寫入時(shí)間
14.4 I2C和EEPROM的綜合實(shí)驗(yàn)學(xué)習(xí)[size=14.0000pt]電視頻道記憶功能,交通燈倒計(jì)時(shí)時(shí)間的設(shè)定,戶外LED廣告的記憶功能,都有可能有類似EEPROM這類存儲(chǔ)器件。這類器件的優(yōu)勢(shì)是存儲(chǔ)的數(shù)據(jù)不僅可以改變,而且掉電后數(shù)據(jù)保存不丟失,因此大量應(yīng)用在各種電子產(chǎn)品上。
我們這節(jié)課的例程,有點(diǎn)類似廣告屏。上電后,1602的第一行顯示EEPROM從0x20地址開始的16個(gè)字符,第二行顯示EERPOM從0x40開始的16個(gè)字符。我們可以通過UART串口通信來改變EEPROM內(nèi)部的這個(gè)數(shù)據(jù),并且同時(shí)改變了1602顯示的內(nèi)容,下次上電的時(shí)候,直接會(huì)顯示我們更新過的內(nèi)容。
這個(gè)程序所有的相關(guān)內(nèi)容,我們之前都已經(jīng)講過了。但是這個(gè)程序體現(xiàn)在了一個(gè)綜合程序應(yīng)用能力上。這個(gè)程序用到了1602液晶、UART實(shí)用串口通信、EEPROM讀寫操作等多個(gè)功能的綜合應(yīng)用。寫個(gè)點(diǎn)亮小燈好簡(jiǎn)單,但是我們想學(xué)會(huì)真正的單片機(jī),必須得學(xué)會(huì)這種綜合程序的應(yīng)用,實(shí)現(xiàn)多個(gè)模塊同時(shí)參與工作,這個(gè)理念在我們的全板子測(cè)試視頻里已經(jīng)有所體現(xiàn)。因此同學(xué)們,要認(rèn)認(rèn)真真的把工程建立起來,一行一行的把程序編寫起來,最終鞏固下來。
/***********************lcd1602.c文件程序源代碼*************************/
略
/************************I2C.c文件程序源代碼***************************/
略
/***********************eeprom.c文件程序源代碼*************************/
略
/************************uart.c文件程序源代碼***************************/
#include <reg52.h>
bit flagOnceTxd = 0; //單次發(fā)送完成標(biāo)志,即發(fā)送完一個(gè)字節(jié)
bit cmdArrived = 0; //命令到達(dá)標(biāo)志,即接收到上位機(jī)下發(fā)的命令
unsigned char cntRxd = 0;
unsigned char pdata bufRxd[40]; //串口接收緩沖區(qū)
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern void E2Write(unsigned char *buf, unsigned char addr, unsigned char len);
void ConfigUART(unsigned int baud) //串口配置函數(shù),baud為波特率
{
SCON = 0x50; //配置串口為模式1
TMOD &= 0x0F; //清零T1的控制位
TMOD |= 0x20; //配置T1為模式2
TH1 = 256 - (11059200/12/32) / baud; //計(jì)算T1重載值
TL1 = TH1; //初值等于重載值
ET1 = 0; //禁止T1中斷
ES = 1; //使能串口中斷
TR1 = 1; //啟動(dòng)T1
}
unsigned char UartRead(unsigned char *buf, unsigned char len) //串口數(shù)據(jù)讀取函數(shù),數(shù)據(jù)接收指針buf,讀取數(shù)據(jù)長(zhǎng)度len,返回值為實(shí)際讀取到的數(shù)據(jù)長(zhǎng)度
{
unsigned char i;
if (len > cntRxd) //讀取長(zhǎng)度大于接收到的數(shù)據(jù)長(zhǎng)度時(shí),
{
len = cntRxd; //讀取長(zhǎng)度設(shè)置為實(shí)際接收到的數(shù)據(jù)長(zhǎng)度
}
for (i=0; i<len; i++) //拷貝接收到的數(shù)據(jù)
{
*buf = bufRxd[ i];
buf++;
}
cntRxd = 0; //清零接收計(jì)數(shù)器
return len; //返回實(shí)際讀取長(zhǎng)度
}
void UartWrite(unsigned char *buf, unsigned char len) //串口數(shù)據(jù)寫入函數(shù),即串口發(fā)送函數(shù),待發(fā)送數(shù)據(jù)指針buf,數(shù)據(jù)長(zhǎng)度len
{
while (len--)
{
flagOnceTxd = 0;
SBUF = *buf;
buf++;
while (!flagOnceTxd);
}
}
bit CmdCompare(unsigned char *buf, const unsigned char *cmd) //命令比較函數(shù),緩沖區(qū)數(shù)據(jù)與指定命令比較,相同返回1,不同返回0
{
while (*cmd != '\0')
{
if (*cmd != *buf) //遇到不相同字符時(shí)即刻返回0
{
return 0;
}
else //當(dāng)前字符相等時(shí),指針遞增準(zhǔn)備比較下一字符
{
cmd++;
buf++;
}
}
return 1; //到命令字符串結(jié)束時(shí)字符都相等則返回1
}
void TrimString16(unsigned char *out, unsigned char *in) //將一字符串整理成16字節(jié)的固定長(zhǎng)度字符串,不足部分補(bǔ)空格
{
unsigned char i = 0;
while (*in != '\0') //拷貝字符串直到輸入字符串結(jié)束
{
*out = *in;
out++;
in++;
i++;
if (i >= 16) //當(dāng)拷貝長(zhǎng)度已達(dá)到16字節(jié)時(shí),強(qiáng)制跳出循環(huán)
break;
}
for ( ; i<16; i++) //如不足16個(gè)字節(jié)則用空格補(bǔ)齊
{
*out = ' ';
out++;
}
*out = '\0'; //最后添加結(jié)束符
}
void UartDriver() //串口驅(qū)動(dòng)函數(shù),檢測(cè)接收到的命令并執(zhí)行相應(yīng)動(dòng)作
{
unsigned char i;
unsigned char len;
unsigned char buf[30];
unsigned char str[17];
const unsigned char code cmd0[] = "showstr1 ";
const unsigned char code cmd1[] = "showstr2 ";
const unsigned char code *cmdList[] = {cmd0, cmd1};
if (cmdArrived) //有命令到達(dá)時(shí),讀取處理該命令
{
cmdArrived = 0;
for (i=0; i<sizeof(buf); i++) //清零命令接收緩沖區(qū)
{
buf[ i] = 0;
}
len = UartRead(buf, sizeof(buf)); //將接收到的命令讀取到緩沖區(qū)中
for (i=0; i<sizeof(cmdList)/sizeof(cmdList[0]); i++) //與所支持的命令列表逐一進(jìn)行比較
{
if (CmdCompare(buf, cmdList[ i]) == 1) //檢測(cè)到相符命令時(shí)退出循環(huán),此時(shí)的i值就是該命令在列表中的下標(biāo)值
{
break;
}
}
switch (i) //根據(jù)比較結(jié)果執(zhí)行相應(yīng)命令
{
case 0:
buf[len] = '\0'; //為接收到的字符串添加結(jié)束符
TrimString16(str, buf+sizeof(cmd0)-1); //整理成16字節(jié)的固定長(zhǎng)度字符串,不足部分補(bǔ)空格
LcdShowStr(0, 0, str); //顯示字符串1
E2Write(str, 0x20, sizeof(str)); //保存字符串1,其E2起始地址為0x20
break;
case 1:
buf[len] = '\0';
TrimString16(str, buf+sizeof(cmd1)-1);
LcdShowStr(0, 1, str);
E2Write(str, 0x40, sizeof(str)); //保存字符串2,其E2起始地址為0x40
break;
default: //i大于命令列表最大下標(biāo)時(shí),即表示沒有相符的命令,給上機(jī)發(fā)送“錯(cuò)誤命令”的提示
UartWrite("bad command.\r\n", sizeof("bad command.\r\n")-1);
return;
}
buf[len++] = '\r'; //有效命令被執(zhí)行后,在原命令幀之后添加回車換行符后返回給上位機(jī),表示已執(zhí)行
buf[len++] = '\n';
UartWrite(buf, len);
}
}
void UartRxMonitor(unsigned char ms) //串口接收監(jiān)控函數(shù)
{
static unsigned char cntbkp = 0;
static unsigned char idletmr = 0;
if (cntRxd > 0) //接收計(jì)數(shù)器大于零時(shí),監(jiān)控總線空閑時(shí)間
{
if (cntbkp != cntRxd) //接收計(jì)數(shù)器改變,即剛接收到數(shù)據(jù)時(shí),清零空閑計(jì)時(shí)
{
cntbkp = cntRxd;
idletmr = 0;
}
else
{
if (idletmr < 30) //接收計(jì)數(shù)器未改變,即總線空閑時(shí),累積空閑時(shí)間
{
idletmr += ms;
if (idletmr >= 30) //空閑時(shí)間超過30ms即認(rèn)為一幀命令接收完畢
{
cmdArrived = 1; //設(shè)置命令到達(dá)標(biāo)志
}
}
}
}
else
{
cntbkp = 0;
}
}
void InterruptUART() interrupt 4 //UART中斷服務(wù)函數(shù)
{
if (RI) //接收到字節(jié)
{
RI = 0; //手動(dòng)清零接收中斷標(biāo)志位
if (cntRxd < sizeof(bufRxd)) //接收緩沖區(qū)尚未用完時(shí),
{
bufRxd[cntRxd++] = SBUF; //保存接收字節(jié),并遞增計(jì)數(shù)器
}
}
if (TI) //字節(jié)發(fā)送完畢
{
TI = 0; //手動(dòng)清零發(fā)送中斷標(biāo)志位
flagOnceTxd = 1; //設(shè)置單次發(fā)送完成標(biāo)志
}
}
/************************main.c文件程序源代碼**************************/
#include <reg52.h>
unsigned char T0RH = 0; //T0重載值的高字節(jié)
unsigned char T0RL = 0; //T0重載值的低字節(jié)
extern void LcdInit();
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern void E2Read(unsigned char *buf, unsigned char addr, unsigned char len);
extern void ConfigUART(unsigned int baud);
extern void UartRxMonitor(unsigned char ms);
extern void UartDriver();
void ConfigTimer0(unsigned int ms);
void InitShowStr();
void main ()
{
EA = 1; //開總中斷
ConfigTimer0(1); //配置T0定時(shí)1ms
ConfigUART(9600); //配置波特率為9600
LcdInit(); //初始化液晶
InitShowStr(); //初始顯示內(nèi)容
while(1)
{
UartDriver();
}
}
void InitShowStr() //處理液晶屏初始顯示內(nèi)容
{
unsigned char str[17];
str[16] = '\0'; //在最后添加字符串結(jié)束符,確保字符串可以結(jié)束
E2Read(str, 0x20, 16); //讀取第一行字符串,其E2起始地址為0x20
LcdShowStr(0, 0, str); //顯示到液晶屏
E2Read(str, 0x40, 16); //讀取第二行字符串,其E2起始地址為0x40
LcdShowStr(0, 1, str); //顯示到液晶屏
}
void ConfigTimer0(unsigned int ms) //T0配置函數(shù)
{
unsigned long tmp;
tmp = 11059200 / 12; //定時(shí)器計(jì)數(shù)頻率
tmp = (tmp * ms) / 1000; //計(jì)算所需的計(jì)數(shù)值
tmp = 65536 - tmp; //計(jì)算定時(shí)器重載值
tmp = tmp + 18; //修正中斷響應(yīng)延時(shí)造成的誤差
T0RH = (unsigned char)(tmp >> 8); //定時(shí)器重載值拆分為高低字節(jié)
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清零T0的控制位
TMOD |= 0x01; //配置T0為模式1
TH0 = T0RH; //加載T0重載值
TL0 = T0RL;
ET0 = 1; //使能T0中斷
TR0 = 1; //啟動(dòng)T0
}
void InterruptTimer0() interrupt 1 //T0中斷服務(wù)函數(shù)
{
TH0 = T0RH; //定時(shí)器重新加載重載值
TL0 = T0RL;
UartRxMonitor(1); //串口接收監(jiān)控
}
我們?cè)趯W(xué)習(xí)UART通信的時(shí)候,剛開始也是用的IO口去模擬UART通信過程,最終實(shí)現(xiàn)和電腦的通信。而后我們的STC89C52RC由于內(nèi)部具備了UART硬件通信模塊,所以我們直接可以通過配置寄存器就可以很輕松的實(shí)現(xiàn)單片機(jī)的UART通信。同樣的道理,我們這個(gè)I2C通信,如果我們單片機(jī)內(nèi)部有硬件模塊的話,單片機(jī)可以直接自動(dòng)實(shí)現(xiàn)I2C通信了,就不需要我們?cè)龠M(jìn)行IO口模擬起始、模擬發(fā)送、模擬結(jié)束,配置好寄存器,單片機(jī)就會(huì)把這些工作全部做了。
不過我們的STC89C52RC單片機(jī)內(nèi)部不具備I2C的硬件模塊,所以我們使用STC89C52RC單片機(jī)進(jìn)行I2C通信必須用IO口來模擬。使用IO口模擬I2C,實(shí)際上更有利于我們徹底理解透徹I2C通信的實(shí)質(zhì)。當(dāng)然了,通過學(xué)習(xí)IO口模擬通信,今后我們?nèi)绻龅絻?nèi)部帶I2C模塊的單片機(jī),也應(yīng)該很輕松的搞定,使用內(nèi)部的硬件模塊,可以提高程序的執(zhí)行效率。
14.5 作業(yè)1、徹底理解I2C的通信時(shí)序,不僅僅是記住。
2、能夠獨(dú)立完成EEPROM任意地址的單字節(jié)讀寫、多字節(jié)的跨頁連續(xù)寫入讀出。
3、將前邊學(xué)的交通燈進(jìn)行改進(jìn),使用EEPROM保存紅燈和綠燈倒計(jì)時(shí)的時(shí)間,并且可以通過UART改變紅燈和綠燈倒計(jì)時(shí)時(shí)間。
4、使用按鍵、1602液晶、EEPROM做一個(gè)簡(jiǎn)單的密碼鎖程序。
作者: lzzgg834483370 時(shí)間: 2014-9-18 15:36
我還是可以補(bǔ)充一點(diǎn)信息的
-
1.jpg
(69.94 KB, 下載次數(shù): 331)
下載附件
2014-9-18 15:36 上傳
EEPROM
-
2.jpg
(71.03 KB, 下載次數(shù): 320)
下載附件
2014-9-18 15:36 上傳
-
3.jpg
(66.59 KB, 下載次數(shù): 352)
下載附件
2014-9-18 15:36 上傳
-
4.jpg
(81.37 KB, 下載次數(shù): 343)
下載附件
2014-9-18 15:36 上傳
作者: shangxianyue 時(shí)間: 2015-8-13 17:47
樓主,,帖子里的代碼粘到 keil4里面后會(huì)成為亂碼呢,,這個(gè)有辦法解決嗎,謝謝!
常看您的帖子,受益匪淺,謝謝!
作者: 疏影橫斜水清淺 時(shí)間: 2016-3-21 22:52
老師,最近都在看你的帖子,很受教,謝謝
作者: 單片機(jī)~+ 時(shí)間: 2016-6-1 23:51
贊!。!
作者: zzzziqzh 時(shí)間: 2017-2-6 15:11
老師,最近都在看你的帖子,很受教,謝謝!
作者: tthxq251 時(shí)間: 2018-1-2 08:56
老師,最近都在看你的帖子,很受教,謝謝!
作者: Steven159 時(shí)間: 2018-1-25 15:41
剛剛好真是想睡覺就有人送抱枕啊,最近最新找的就是SPI通訊和IIC通訊教程了,沒想到剛注冊(cè)51黑電子論壇,就發(fā)現(xiàn)了這么多寶貴資源,感謝樓主的寶貴分享,剛好幫助我理解IIC通訊
作者: gavin1985bb 時(shí)間: 2018-3-5 12:32
講的真心不錯(cuò)工,學(xué)習(xí)了!
作者: Jia_hu 時(shí)間: 2018-4-7 21:04
受益匪淺.
作者: liangyutong 時(shí)間: 2018-5-2 14:43
還是我們學(xué)識(shí)淺了
作者: keneng 時(shí)間: 2018-7-17 16:21
越來越難了
作者: a89588038 時(shí)間: 2018-8-6 15:43
看到后面越來越難理解了


作者: 向日葵男人 時(shí)間: 2018-8-19 01:17
學(xué)習(xí)了!!
作者: xzf586 時(shí)間: 2018-8-20 23:23
仔細(xì)學(xué)習(xí)了一下,獲益匪淺,講的很透徹。!
作者: xzf586 時(shí)間: 2018-8-21 09:53
版主原來是宋老師,久聞大名,怎么這么近呢?
,現(xiàn)在仔細(xì)地將這本書啃一啃!
-
微信圖片_20180821094640.jpg
(136.74 KB, 下載次數(shù): 148)
下載附件
2018-8-21 09:51 上傳
作者: yasi666 時(shí)間: 2019-1-23 20:06
絕世好貼,感謝分享!
作者: mawuxi 時(shí)間: 2020-3-24 20:18
絕對(duì)給力
作者: OHHO 時(shí)間: 2020-6-10 00:44
謝謝,受益匪淺!
作者: hhh123hhh123 時(shí)間: 2021-9-10 11:38
太偉大了,等我這么吊的時(shí)候也要把經(jīng)驗(yàn)分享出來。
歡迎光臨 (http://www.torrancerestoration.com/bbs/) |
Powered by Discuz! X3.1 |