找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

搜索
查看: 41052|回復(fù): 19
打印 上一主題 下一主題
收起左側(cè)

第14章 I2C總線與EEPROM

  [復(fù)制鏈接]
跳轉(zhuǎn)到指定樓層
樓主
ID:1 發(fā)表于 2013-9-28 15:25 | 只看該作者 |只看大圖 回帖獎勵 |倒序瀏覽 |閱讀模式
  本教材現(xiàn)以連載的方式由網(wǎng)絡(luò)發(fā)布,并將于2014年由清華大學(xué)出版社出版最終完整版,版權(quán)歸作者和清華大學(xué)出版社所有。本著開源、分享的理念,本教材可以自由傳播及學(xué)習(xí)使用,但是務(wù)必請注明出處來自金沙灘工作室
                 前幾章我們學(xué)了一種通信協(xié)議叫做UART異步串口通信,這節(jié)課我們要來學(xué)習(xí)第二種常用的通信協(xié)議I2C。I2C總線是由PHILIPS公司開發(fā)的兩線式串行總線,多用于連接微處理器及其外圍設(shè)備。I2C總線的主要特點(diǎn)是接口方式簡單,兩條線可以掛多個參與通信的器件,即多機(jī)模式,而且任何一個器件都可以作為主機(jī),當(dāng)然同一時刻只能一個主機(jī)。

從原理上來講,UART屬于異步通信,比如電腦發(fā)送給單片機(jī),電腦只負(fù)責(zé)把數(shù)據(jù)通過TXD發(fā)送出來即可,接收數(shù)據(jù)是單片機(jī)自己的事情。而I2C屬于同步通信,SCL時鐘線負(fù)責(zé)收發(fā)雙方的時鐘節(jié)拍,SDA數(shù)據(jù)線負(fù)責(zé)傳輸數(shù)據(jù)。I2C的發(fā)送方和接收方都以SCL這個時鐘節(jié)拍為基準(zhǔn)進(jìn)行數(shù)據(jù)的發(fā)送和接收。
從應(yīng)用上來講,UART通信多用于板間通信,比如單片機(jī)和電腦,這個設(shè)備和另外一個設(shè)備之間的通信。而I2C多用于板內(nèi)通信,比如單片機(jī)和我們本章要學(xué)的EEPROM之間的通信。
14.1 I2C時序初步認(rèn)識
在硬件上,I2C總線是由時鐘總線SCL和數(shù)據(jù)總線SDA兩條線構(gòu)成,連接到總線上的所有的器件的SCL都連到一起,所有的SDA都連到一起。I2C總線是開漏引腳并聯(lián)的結(jié)構(gòu),因此我們外部要添加上拉電阻。對于開漏電路外部加上拉電阻的話,那就組成了線“與”的關(guān)系?偩上線“與”的關(guān)系,那所有接入的器件保持高電平,這條線才是高電平。而任意一個器件輸出一個低電平,那這條線就會保持低電平,因此可以做到任何一個器件都可以拉低電平,也就是任何一個器件都可以作為主機(jī),如圖14-1所示,我們添加了R63R64兩個上拉電阻。

14-1 I2C總線的上拉電阻
雖然說任何一個設(shè)備都可以作為主機(jī),但絕大多數(shù)情況下我們都是用微處理器,也就是我們的單片機(jī)來做主機(jī),而總線上掛的多個器件,每一個都像電話機(jī)一樣有自己唯一的地址,在信息傳輸?shù)倪^程中,通過這唯一的地址可以正常識別到屬于自己的信息,在我們的KST-51開發(fā)板上,就掛接了2I2C設(shè)備,一個是24C02,一個是PCF8591
我們在學(xué)習(xí)UART串行通信的時候,知道了我們的通信流程分為起始位、數(shù)據(jù)位、停止位這三部分,同理在I2C中也有起始信號、數(shù)據(jù)傳輸和停止信號,如圖14-2所示。
         
14-2 I2C時序流程圖
從圖上可以看出來,I2CUART時序流程有相似性,也有一定的區(qū)別。UART每個字節(jié)中,都有一個起始位,8個數(shù)據(jù)位和1位停止位。而I2C分為起始信號,數(shù)據(jù)傳輸部分,最后是停止信號。其中數(shù)據(jù)傳輸部分,可以一次通信過程傳輸很多個字節(jié),字節(jié)數(shù)是不受限制的,而每個字節(jié)的數(shù)據(jù)最后也跟了一位,這一位叫做應(yīng)答位,通常用ACK表示,有點(diǎn)類似于UART的停止位。
下面我們一部分一部分的把I2C通信時序進(jìn)行剖析。之前我們學(xué)過了UART,所以學(xué)習(xí)I2C的過程我盡量拿UART來作為對比,這樣有助于更好的理解。但是有一點(diǎn)大家要理解清楚,就是UART通信雖然我們用了TXDRXD兩根線,但是實(shí)際一次通信,1條線就可以完成,2條線是把發(fā)送和接收分開而已,而I2C每次通信,不管是發(fā)送還是接收,必須2條線都參與工作才能完成,為了更方便的看出來每一位的傳輸流程,我們把圖14-2改進(jìn)成圖14-3。
           
圖14-3 I2C通信流程解析
起始信號:UART通信是從一直持續(xù)的高電平出現(xiàn)一個低電平標(biāo)志起始位;而I2C通信的起始信號的定義是SCL為高電平期間,SDA由高電平向低電平變化產(chǎn)生一個下降沿,表示起始信號,如圖14-3中的start部分所示。
數(shù)據(jù)傳輸:首先,UART是低位在前,高位在后;而I2C通信是高位在前,低位在后。第二,UART通信數(shù)據(jù)位是固定長度,波特率分之一,一位一位固定時間發(fā)送完畢就可以了。而I2C沒有固定波特率,但是有時序的要求,要求當(dāng)SCL在低電平的時候,SDA允許變化,也就是說,發(fā)送方必須先保持SCL是低電平,才可以改變數(shù)據(jù)線SDA,輸出要發(fā)送的當(dāng)前數(shù)據(jù)的一位;而當(dāng)SCL在高電平的時候,SDA絕對不可以變化,因為這個時候,接收方要來讀取當(dāng)前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部分所示。
14.2 I2C尋址模式
上一節(jié)介紹的是I2C每一位信號的時序流程,而I2C通信在字節(jié)級的傳輸中,也有固定的時序要求。I2C通信的起始信號(Start)后,首先要發(fā)送一個從機(jī)的地址,這個地址一共有7位,緊跟著的第8位是數(shù)據(jù)方向位(R/W),‘0’表示接下來要發(fā)送數(shù)據(jù)(),‘1’表示接下來是請求數(shù)據(jù)()
我們知道,打電話的時候,當(dāng)撥通電話,接聽方撿起電話肯定要回一個“喂”,這就是告訴撥電話的人,這邊有人了。同理,這個第九位ACK實(shí)際上起到的就是這樣一個作用。當(dāng)我們發(fā)送完了這7位地址和1位方向位,如果我們發(fā)送的這個地址確實(shí)存在,那么這個地址的器件應(yīng)該回應(yīng)一個ACK0’,如果不存在,就沒“人”回應(yīng)ACK。
那我們寫一個簡單的程序,訪問一下我們板子上的EEPROM的地址,另外在寫一個不存在的地址,看看他們是否能回一個ACK,來了解和確認(rèn)一下這個問題。
我們板子上的EEPROM器件型號是24C02,在24C02的數(shù)據(jù)手冊3.6部分說明了,24C027位地址中,其中高4位是固定的1010,而低3位的地址取決于我們電路的設(shè)計,由芯片上的A2A1A03個引腳的實(shí)際電平?jīng)Q定,來看一下我們的24C02的電路圖,如圖14-4所示。
14-4 24C02原理圖
從圖14-4可以看出來,我們的A2A1、A0都是接的GND,也就是說都是0,因此我們的7位地址實(shí)際上是二進(jìn)制的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()  //等待液晶準(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)計算顯示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);  //文字不動,地址自動+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,返回值為從機(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,完成一個位周期
    }
    I2C_SDA = 1;   //8位數(shù)據(jù)發(fā)送完后,主機(jī)釋放SDA,以檢測從機(jī)應(yīng)答
    I2CDelay();
    I2C_SCL = 1;   //拉高SCL
    I2CDelay();
    ack = I2C_SDA; //讀取此時的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)生起始位,即啟動一次總線操作
    ack = I2CWrite(addr<<1);  //器件地址需左移一位,因?qū)ぶ访畹淖畹臀粸樽x寫位,用于表示之后的操作是讀或?qū)?/font>
    I2CStop();   //不需進(jìn)行后續(xù)讀寫,而直接停止本次總線操作

    return ack;
}
我們把這個程序在KST-51開發(fā)板上運(yùn)行完畢,會在液晶上邊顯示出來我們預(yù)想的結(jié)果,主機(jī)發(fā)送一個存在的從機(jī)地址,從機(jī)會回復(fù)一個應(yīng)答位;主機(jī)如果發(fā)送一個不存在的從機(jī)地址,就沒有從機(jī)應(yīng)答。
前邊我有提到過有一個利用庫函數(shù)_nop_()來進(jìn)行精確延時,一個_nop_()的時間就是一個機(jī)器周期,這個庫函數(shù)是包含在了intrins.h這個庫文件中,我們?nèi)绻褂眠@個庫函數(shù),只需要在程序最開始,和包含reg52.h一樣,include<intrins.h>之后,我們程序就可以直接使用這個庫函數(shù)了。
還有一點(diǎn)要提一下,I2C通信分為低速模式100kbit/s,快速模式400kbit/s和高速模式3.4Mbit/s。因為所有的I2C器件都支持低速,但卻未必支持另外兩種速度,所以作為通用的I2C程序我們選擇100k這個速率來實(shí)現(xiàn),也就是說實(shí)際程序產(chǎn)生的時序必須小于等于100k的時序參數(shù),很明顯也就是要求SCL的高低電平持續(xù)時間都不短于5us,因此我們在時序函數(shù)中通過插入I2CDelay()這個總線延時函數(shù)(它實(shí)際上就是4NOP指令,用define在文件開頭做了定義),加上改變SCL值語句本身占用的至少一個周期,來達(dá)到這個速度限制。如果以后需要提高速度,那么只需要減小這里的總線延時時間即可。
此外我們要學(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ù),所以我們先從最高位開始,0x80dat進(jìn)行按位與運(yùn)算,從而得知dat7位是0還是1,然后右移一位,也就是變成了用0x40dat按位與運(yùn)算,得到第6位是0還是1,一直到第0位結(jié)束,最終通過if語句,把dat8位數(shù)據(jù)依次發(fā)送了出去。其他的邏輯大家對照前邊講到的理論知識,認(rèn)真研究明白就可以了。
1.3 EEPROM的學(xué)習(xí)
在實(shí)際的應(yīng)用中,保存在單片機(jī)RAM中的數(shù)據(jù),掉電后數(shù)據(jù)就丟失了,保存在單片機(jī)的FLASH中的數(shù)據(jù),又不能隨意改變,也就是不能用它來記錄變化的數(shù)值。但是在某些場合,我們又確實(shí)需要記錄下某些數(shù)據(jù),而它們還時常需要改變或更新,掉電之后數(shù)據(jù)還不能丟失,比如我們的家用電表度數(shù),我們的電視機(jī)里邊的頻道記憶,一般都是使用EEPROM來保存數(shù)據(jù),特點(diǎn)就是掉電后不丟失。我們板子上使用的這個器件是24C02,是一個容量大小是2Kbit位,也就是256個字節(jié)的EEPROM。一般情況下,EEPROM擁有30萬到100萬次的壽命,也就是它可以反復(fù)寫入30-100萬次,而讀取次數(shù)是無限的。
24C02是一個基于I2C通信協(xié)議的器件,因此從現(xiàn)在開始,我們的I2C和我們的EEPROM就要合體了。但是大家要分清楚,I2C是一個通信協(xié)議,它擁有嚴(yán)密的通信時序邏輯要求,而EEPROM是一個器件,只是這個器件采樣了I2C協(xié)議的接口與單片機(jī)相連而已,二者并沒有必然的聯(lián)系,EEPROM可以用其他接口,I2C也可以用在其它很多器件上。
14.3.1 EEPROM單字節(jié)讀寫操作時序
1、EEPROM寫數(shù)據(jù)流程
第一步,首先是I2C的起始信號,接著跟上首字節(jié),也就是我們前邊講的I2C的器件地
(EERPOM),并且在讀寫方向上選擇“寫”操作。
第二步,發(fā)送數(shù)據(jù)的存儲地址。我們24C02一共256個字節(jié)的存儲空間,地址從0x000xFF,我們想把數(shù)據(jù)存儲在哪個位置,此刻寫的就是哪個地址。
第三步,發(fā)送要存儲的數(shù)據(jù)第一個字節(jié),第二個字節(jié)......注意在寫數(shù)據(jù)的過程中,EEPROM每個字節(jié)都會回應(yīng)一個“應(yīng)答位0”,來告訴我們寫EEPROM數(shù)據(jù)成功,如果沒有回應(yīng)答位,說明寫入不成功。
在寫數(shù)據(jù)的過程中,每成功寫入一個字節(jié),EEPROM存儲空間的地址就會自動加1,當(dāng)加到0xFF后,再寫一個字節(jié),地址會溢出又變成了0x00。
2、EEPROM讀數(shù)據(jù)流程
第一步,首先是I2C的起始信號,接著跟上首字節(jié),也就是我們前邊講的I2C的器件地
(EERPOM),并且在讀寫方向上選擇“寫”操作。這個地方可能有同學(xué)會詫異,我們明明是讀數(shù)據(jù)為何方向也要選“寫”呢?剛才說過了,我們24C02一共有256個地址,我們選擇寫操作,是為了把所要讀的數(shù)據(jù)的存儲地址先寫進(jìn)去,告訴EEPROM我們要讀取哪個地址的數(shù)據(jù)。這就如同我們打電話,先撥總機(jī)號碼(EEPROM器件地址),而后還要繼續(xù)撥分機(jī)號碼(數(shù)據(jù)地址),而撥分機(jī)號碼這個動作,主機(jī)仍然是發(fā)送方,方向依然是“寫”。
第二步,發(fā)送要讀取的數(shù)據(jù)的地址,注意是地址而非存在EEPROM中的數(shù)據(jù),通知EEPROM我要哪個分機(jī)的信息。
第三步,重新發(fā)送I2C起始信號和器件地址,并且在方向位選擇“讀”操作。
這三步當(dāng)中,每一個字節(jié)實(shí)際上都是在“寫”,所以每一個字節(jié)EEPROM都會回應(yīng)一個“應(yīng)答位0”。
第四步,讀取從器件發(fā)回的數(shù)據(jù),讀一個字節(jié),如果還想繼續(xù)讀下一個字節(jié),就發(fā)送一個“應(yīng)答位ACK(0)”,如果不想讀了,告訴EEPROM,我不想要數(shù)據(jù)了,別再發(fā)數(shù)據(jù)了,那就發(fā)送一個“非應(yīng)答位NACK(1)”。
和寫操作規(guī)則一樣,我們每讀一個字節(jié),地址會自動加1,那如果我們想繼續(xù)往下讀,給EEPROM一個ACK(0)低電平,那再繼續(xù)給SCL完整的時序,EEPROM會繼續(xù)往外送數(shù)據(jù)。如果我們不想讀了,要告訴EEPROM不要數(shù)據(jù)了,那我們直接給一個NAK(1)高電平即可。這個地方大家要從邏輯上理解透徹,不能簡單的靠死記硬背了,一定要理解明白。梳理一下幾個要點(diǎn):A、在本例中單片機(jī)是主機(jī),24C02是從機(jī);B、無論是讀是寫,SCL始終都是由主機(jī)控制的;C、寫的時候應(yīng)答信號由從機(jī)給出,表示從機(jī)是否正確接收了數(shù)據(jù);D、讀的時候應(yīng)答信號則由主機(jī)給出,表示是否繼續(xù)讀下去。
那我們下面寫一個程序,讀取EEPROM0x02這個地址上的一個數(shù)據(jù),不管這個數(shù)據(jù)之前是多少,我們都再將讀出來的數(shù)據(jù)加1,再寫到EEPROM0x02這個地址上。此外我們將I2C的程序建立一個文件,寫一個I2C.c程序文件,形成我們又一個程序模塊。大家也可以看出來,我們連續(xù)的這幾個程序,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)生總線起始信號
{
    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)答狀態(tài)
{
    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,完成一個位周期
    }
    I2C_SDA = 1;   //8位數(shù)據(jù)發(fā)送完后,主機(jī)釋放SDA,以檢測從機(jī)應(yīng)答
    I2CDelay();
    I2C_SCL = 1;   //拉高SCL
    ack = I2C_SDA; //讀取此時的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)答信號,返回值為讀到的字節(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時,dat中對應(yīng)位清零
        else
            dat |= mask;  //1時,dat中對應(yīng)位置1
        I2CDelay();
        I2C_SCL = 0;      //再拉低SCL,以使從機(jī)發(fā)送出下一位
    }
    I2C_SDA = 1;   //8位數(shù)據(jù)發(fā)送完后,拉高SDA,發(fā)送非應(yīng)答信號
    I2CDelay();
    I2C_SCL = 1;   //拉高SCL
    I2CDelay();
    I2C_SCL = 0;   //再拉低SCL完成非應(yīng)答位,并保持住總線

    return dat;
}
unsigned char I2CReadACK() //I2C總線讀操作,并發(fā)送應(yīng)答信號,返回值為讀到的字節(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時,dat中對應(yīng)位清零
        else
            dat |= mask;  //1時,dat中對應(yīng)位置1
        I2CDelay();
        I2C_SCL = 0;      //再拉低SCL,以使從機(jī)發(fā)送出下一位
    }
    I2C_SDA = 0;   //8位數(shù)據(jù)發(fā)送完后,拉低SDA,發(fā)送應(yīng)答信號
    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ù)檢測直到其等于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)計算顯示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);  //文字不動,地址自動+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);    //讀取指定地址上的一個字節(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);    //再寫回到對應(yīng)的地址上

    while(1)
    {}
}

unsigned char E2ReadByte(unsigned char addr) //讀取EEPROM中的一個字節(jié),字節(jié)地址addr
{
    unsigned char dat;

    I2CStart();
    I2CWrite(0x50<<1); //尋址器件,后續(xù)為寫操作
    I2CWrite(addr);    //寫入存儲地址
    I2CStart();        //發(fā)送重復(fù)啟動信號
    I2CWrite((0x50<<1)|0x01); //尋址器件,后續(xù)為讀操作
    dat = I2CReadNAK();       //讀取一個字節(jié)數(shù)據(jù)
    I2CStop();

    return dat;
}

void E2WriteByte(unsigned char addr, unsigned char dat) //EEPROM中寫入一個字節(jié),字節(jié)地址addr
{
    I2CStart();
    I2CWrite(0x50<<1); //尋址器件,后續(xù)為寫操作
    I2CWrite(addr);    //寫入存儲地址
    I2CWrite(dat);     //寫入一個字節(jié)數(shù)據(jù)
    I2CStop();
}
/***********************************************************************/
這個程序,以同學(xué)們現(xiàn)在的基礎(chǔ),獨(dú)立分析應(yīng)該不困難了,遇到哪個語句不懂可以及時問問別人或者搜索一下,把該解決的問題理解明白。大家把這個程序復(fù)制過去后,編譯一下會發(fā)現(xiàn)Keil軟件提示了一個警告:*** WARNING L16: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS,這個警告的意思是有我們代碼中存在沒有被調(diào)用過的變量或者函數(shù)。
大家仔細(xì)觀察一下,這個程序,我們讀取EEPROM的時候,只讀了一個字節(jié)我們就要告訴EEPROM不需要再讀數(shù)據(jù)了,因此我們讀完后直接回復(fù)一個“NAK”,因此我們只調(diào)用了I2CReadNAK()這個函數(shù),而并沒有調(diào)用I2CReadACK()這個函數(shù)。我們今后很可能讀數(shù)據(jù)的時候要連續(xù)讀幾個字節(jié),因此這個函數(shù)寫在了I2C.c文件中,作為I2C功能模塊的一部分是必要的,方便我們這個文件以后移植到其他程序中使用,因此這個警告在這里就不必管它了。
14.3.2 EEPROM多字節(jié)讀寫操作時序[size=14.0000pt]
我們讀取EEPROM的時候很簡單,EEPROM根據(jù)我們所送的時序,直接就把數(shù)據(jù)送出來了,但是寫EEPROM卻沒有這么簡單。我們?nèi)绻oEEPROM發(fā)送數(shù)據(jù)后,先保存在了EEPROM的緩存,EEPROM必須要把緩存中的數(shù)據(jù)搬移到“非易失”的區(qū)域,才能達(dá)到掉電不丟失的效果。而往非易失區(qū)域?qū)懶枰欢ǖ臅r間,每種器件不完全一樣,ATMEL公司的24C02的這個寫入時間最高不超過5ms。在往非易失區(qū)域?qū)懙倪^程,EEPROM是不會再響應(yīng)我們的訪問的,不僅接收不到我們的數(shù)據(jù),我們即使用I2C標(biāo)準(zhǔn)的尋址模式去尋址,EEPROM都不會應(yīng)答,就如同這個總線上沒有這個器件一樣。數(shù)據(jù)寫入非易失區(qū)域完畢后,EEPROM再次恢復(fù)正常,可以正常讀寫了。
細(xì)心的同學(xué),在看上一節(jié)程序的時候會發(fā)現(xiàn),我們寫數(shù)據(jù)的那段代碼,實(shí)際上我們有去讀應(yīng)答位ACK,但是讀到了應(yīng)答位我們也沒有做任何處理。這是因為我們一次只寫一個字節(jié)的數(shù)據(jù)進(jìn)去,等到下次重新上電再寫的時候,時間肯定遠(yuǎn)遠(yuǎn)超過了5ms,但是如果我們是連續(xù)寫入幾個字節(jié)的時候,我們就必須得考慮到應(yīng)答位的問題了。寫入一個字節(jié)后,再寫入下一個字節(jié)之前,我們必須要等待EEPROM再次響應(yīng)才可以,大家注意我的程序的寫法,可以學(xué)習(xí)一下。
之前我們知道編寫多.c文件移植的方便性了,本節(jié)程序和上一節(jié)的lcd1602.c文件和I2C.c文件完全是一樣的,因此這次我們只把main.c文件給大家發(fā)出來,幫大家分析明白。而同學(xué)們卻不能這樣,同學(xué)們是初學(xué),很多知識和技巧需要多練才能鞏固下來,因此每個程序還是建議大家在你的Keil軟件上一個代碼一個代碼的敲出來。

        
                /***********************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) //把一個字節(jié)數(shù)組轉(zhuǎn)換為十六進(jìn)制字符串的格式
{
    unsigned char tmp;

    while (len--)
    {
        tmp = *array >> 4;         //先取高4
        if (tmp <= 9)              //轉(zhuǎn)換為0-9A-F
            *str = tmp + '0';
        else
            *str = tmp - 10 + 'A';
        str++;
        tmp = *array & 0x0F;       //再取低4
        if (tmp <= 9)              //轉(zhuǎn)換為0-9A-F
            *str = tmp + '0';
        else
            *str = tmp - 10 + 'A';
        str++;
        *str = ' ';                //轉(zhuǎn)換完一個字節(jié)添加一個空格
        str++;
        array++;
    }
}
void E2Read(unsigned char *buf, unsigned char addr, unsigned char len) //E2讀取函數(shù),數(shù)據(jù)接收指針buf,E2中的起始地址addr,讀取長度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ù)啟動信號
    I2CWrite((0x50<<1)|0x01); //尋址器件,后續(xù)為讀操作
    while (len > 1)           //連續(xù)讀取len-1個字節(jié)
    {
        *buf = I2CReadACK();  //最后字節(jié)之前為讀取操作+應(yīng)答
        buf++;
        len--;
    }
    *buf = I2CReadNAK();      //最后一個字節(jié)為讀取操作+非應(yīng)答
    I2CStop();
}

void E2Write(unsigned char *buf, unsigned char addr, unsigned char len) //E2寫入函數(shù),源數(shù)據(jù)指針buf,E2中的起始地址addr,寫入長度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);           //寫入一個字節(jié)數(shù)據(jù)
        I2CStop();                //結(jié)束寫操作,以等待寫入完成
        buf++;                    //數(shù)據(jù)指針遞增
        addr++;                   //E2地址遞增
    }
}

函數(shù)ArrayToHexStr:這是一個把數(shù)組轉(zhuǎn)換成十六進(jìn)制字符串的形式。由于我們從EEPROM讀出來的是正常的數(shù)據(jù),而1602液晶接收的是ASCII碼字符,因此我們要通過液晶把數(shù)據(jù)顯示出來必須先通過一步轉(zhuǎn)換。算法倒是很簡單,就是把每一個字節(jié)的數(shù)據(jù)高4位和低4位分開,和9進(jìn)行比較,如果小于等于9,則通過數(shù)字加’0’轉(zhuǎn)ASCII碼發(fā)送;如果大于9,則通過加’A’轉(zhuǎn)ASCII碼發(fā)送出去。
函數(shù)E2Read:我們在讀之前,要查詢一下當(dāng)前是否可以進(jìn)行讀寫操作,EEPROM正常響應(yīng)才可以進(jìn)行。進(jìn)行后,最后一個字節(jié)之前的,全部給出ACK,而讀完了最后一個字節(jié),我們要給出一個NAK。
函數(shù)E2Write:每次寫操作之前,我們都要進(jìn)行查詢判斷當(dāng)前EEPROM是否響應(yīng),正常響應(yīng)后才可以寫數(shù)據(jù)。
14.3.3 EEPROM的頁寫入
如果每個數(shù)據(jù)都連續(xù)寫入,像我們上節(jié)課那樣寫的時候,每次都先起始位,再訪問一下這個EEPROM的地址,看看是否響應(yīng),感覺上效率太低了。因此EEPROM的廠商就想了一個辦法,把EEPROM分頁管理。24c01、24c02這兩個型號是8個字節(jié)一個頁,而24c04、24c08、24c1616個字節(jié)一頁。我們板子上的型號是24C02,一共是256個字節(jié),8個字節(jié)一頁,那么就一共有32頁。
分配好頁之后,如果我們在同一個頁內(nèi)連續(xù)寫入幾個字節(jié)后,最后再發(fā)送停止位的時序。EEPROM檢測到這個停止位后,統(tǒng)一把這一頁的數(shù)據(jù)寫到非易失區(qū)域,就不需要像上節(jié)課那樣寫一個字節(jié)檢測一次了,并且頁寫入的時間也不會超過5ms。如果我們寫入的數(shù)據(jù)跨頁了,那么寫完了一頁之后,我們要發(fā)送一個停止位,然后等待并且檢測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,讀取長度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ù)啟動信號
    I2CWrite((0x50<<1)|0x01); //尋址器件,后續(xù)為讀操作
    while (len > 1)           //連續(xù)讀取len-1個字節(jié)
    {
        *buf = I2CReadACK();  //最后字節(jié)之前為讀取操作+應(yīng)答
        buf++;
        len--;
    }
    *buf = I2CReadNAK();      //最后一個字節(jié)為讀取操作+非應(yīng)答
    I2CStop();
}

void E2Write(unsigned char *buf, unsigned char addr, unsigned char len) //E2寫入函數(shù),源數(shù)據(jù)指針bufE2中的起始地址addr,寫入長度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);       //寫入一個字節(jié)數(shù)據(jù)
            len--;                //待寫入長度計數(shù)遞減
            buf++;                //數(shù)據(jù)指針遞增
            addr++;               //E2地址遞增
            if ((addr&0x07) == 0) //檢查地址是否到達(dá)頁邊界,24C02每頁8字節(jié),所以檢測低3位是否為零即可
                break;            //到達(dá)頁邊界時,跳出循環(huán),結(jié)束本次寫操作
        }
        I2CStop();
    }
}
這個eeprom.c文件中的程序,單獨(dú)做一個文件,用來管理eeprom的訪問。其中E2Read函數(shù)和上一節(jié)是一樣的,因為讀操作和是否同一頁無關(guān)。重點(diǎn)是E2Write函數(shù),我們在寫入數(shù)據(jù)的時候,要計算下一個要寫的數(shù)據(jù)的地址是否是一個頁的起始地址,如果是的話,則必須跳出循環(huán),等待EEPROM上一頁寫入到非易失區(qū)域后,再進(jìn)行繼續(xù)寫入。
而寫了eeprom.c后,main.c文件里的程序就要變的簡單多了,大家可以自己看一下,不需要過多解釋了。
/************************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) //把一個字節(jié)數(shù)組轉(zhuǎn)換為十六進(jìn)制字符串的格式
{
    unsigned char tmp;

    while (len--)
    {
        tmp = *array >> 4;         //先取高4
        if (tmp <= 9)              //轉(zhuǎn)換為0-9A-F
            *str = tmp + '0';
        else
            *str = tmp - 10 + 'A';
        str++;
        tmp = *array & 0x0F;       //再取低4
        if (tmp <= 9)              //轉(zhuǎn)換為0-9A-F
            *str = tmp + '0';
        else
            *str = tmp - 10 + 'A';
        str++;
        *str = ' ';                //轉(zhuǎn)換完一個字節(jié)添加一個空格
        str++;
        array++;
    }
}
多字節(jié)寫入和頁寫入程序都編寫出來了,而且頁寫入的程序我們還特地跨頁寫的數(shù)據(jù),他們的寫入時間到底差別多大呢。我們用一些工具可以測量一下,比如示波器,邏輯分析儀等工具。我現(xiàn)在把兩次寫入時間用邏輯分析儀給抓了出來,并且用時間標(biāo)簽T1T2給標(biāo)注了開始位置和結(jié)束位置,如圖14-5和圖14-6所示,右側(cè)顯示的|T1-T2|就是最終寫入5個字節(jié)所耗費(fèi)的時間。多字節(jié)一個一個寫入,每次寫入后都需要再次通信檢測EEPROM是否在“忙”,因此耗費(fèi)了大量的時間,同樣的寫入5個字節(jié)的數(shù)據(jù),一個一個寫入用了8.4ms左右的時間,而使用頁寫入,只用了3.5ms左右的時間。
      
圖14-5 多字節(jié)寫入時間
   
圖14-6 跨頁寫入時間
14.4 I2C和EEPROM的綜合實(shí)驗學(xué)習(xí)[size=14.0000pt]
電視頻道記憶功能,交通燈倒計時時間的設(shè)定,戶外LED廣告的記憶功能,都有可能有類似EEPROM這類存儲器件。這類器件的優(yōu)勢是存儲的數(shù)據(jù)不僅可以改變,而且掉電后數(shù)據(jù)保存不丟失,因此大量應(yīng)用在各種電子產(chǎn)品上。
我們這節(jié)課的例程,有點(diǎn)類似廣告屏。上電后,1602的第一行顯示EEPROM0x20地址開始的16個字符,第二行顯示EERPOM0x40開始的16個字符。我們可以通過UART串口通信來改變EEPROM內(nèi)部的這個數(shù)據(jù),并且同時改變了1602顯示的內(nèi)容,下次上電的時候,直接會顯示我們更新過的內(nèi)容。
這個程序所有的相關(guān)內(nèi)容,我們之前都已經(jīng)講過了。但是這個程序體現(xiàn)在了一個綜合程序應(yīng)用能力上。這個程序用到了1602液晶、UART實(shí)用串口通信、EEPROM讀寫操作等多個功能的綜合應(yīng)用。寫個點(diǎn)亮小燈好簡單,但是我們想學(xué)會真正的單片機(jī),必須得學(xué)會這種綜合程序的應(yīng)用,實(shí)現(xiàn)多個模塊同時參與工作,這個理念在我們的全板子測試視頻里已經(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ā)送完一個字節(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;  //計算T1重載值
    TL1 = TH1;     //初值等于重載值
    ET1 = 0;       //禁止T1中斷
    ES  = 1;       //使能串口中斷
    TR1 = 1;       //啟動T1
}
unsigned char UartRead(unsigned char *buf, unsigned char len) //串口數(shù)據(jù)讀取函數(shù),數(shù)據(jù)接收指針buf,讀取數(shù)據(jù)長度len,返回值為實(shí)際讀取到的數(shù)據(jù)長度
{
    unsigned char i;

    if (len > cntRxd) //讀取長度大于接收到的數(shù)據(jù)長度時,
    {
        len = cntRxd; //讀取長度設(shè)置為實(shí)際接收到的數(shù)據(jù)長度
    }
    for (i=0; i<len; i++) //拷貝接收到的數(shù)據(jù)
    {
        *buf = bufRxd[ i];
        buf++;
    }
    cntRxd = 0;  //清零接收計數(shù)器

    return len;  //返回實(shí)際讀取長度
}
void UartWrite(unsigned char *buf, unsigned char len) //串口數(shù)據(jù)寫入函數(shù),即串口發(fā)送函數(shù),待發(fā)送數(shù)據(jù)指針buf,數(shù)據(jù)長度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) //遇到不相同字符時即刻返回0
        {
            return 0;
        }
        else //當(dāng)前字符相等時,指針遞增準(zhǔn)備比較下一字符
        {
            cmd++;
            buf++;
        }
    }
    return 1; //到命令字符串結(jié)束時字符都相等則返回1
}
void TrimString16(unsigned char *out, unsigned char *in) //將一字符串整理成16字節(jié)的固定長度字符串,不足部分補(bǔ)空格
{
    unsigned char i = 0;

    while (*in != '\0') //拷貝字符串直到輸入字符串結(jié)束
    {
        *out = *in;
        out++;
        in++;
        i++;
        if (i >= 16)   //當(dāng)拷貝長度已達(dá)到16字節(jié)時,強(qiáng)制跳出循環(huán)
            break;
    }
    for ( ; i<16; i++) //如不足16個字節(jié)則用空格補(bǔ)齊
    {
        *out = ' ';
        out++;
    }
    *out = '\0';       //最后添加結(jié)束符
}
void UartDriver() //串口驅(qū)動函數(shù),檢測接收到的命令并執(zhí)行相應(yī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á)時,讀取處理該命令
    {
        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) //檢測到相符命令時退出循環(huán),此時的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é)的固定長度字符串,不足部分補(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)時,即表示沒有相符的命令,給上機(jī)發(fā)送“錯誤命令”的提示
                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)  //接收計數(shù)器大于零時,監(jiān)控總線空閑時間
    {
        if (cntbkp != cntRxd)  //接收計數(shù)器改變,即剛接收到數(shù)據(jù)時,清零空閑計時
        {
            cntbkp = cntRxd;
            idletmr = 0;
        }
        else
        {
            if (idletmr < 30)  //接收計數(shù)器未改變,即總線空閑時,累積空閑時間
            {
                idletmr += ms;
                if (idletmr >= 30)  //空閑時間超過30ms即認(rèn)為一幀命令接收完畢
                {
                    cmdArrived = 1; //設(shè)置命令到達(dá)標(biāo)志
                }
            }
        }
    }
    else
    {
        cntbkp = 0;
    }
}
void InterruptUART() interrupt 4  //UART中斷服務(wù)函數(shù)
{
if (RI)  //接收到字節(jié)
    {
RI = 0;   //手動清零接收中斷標(biāo)志位
        if (cntRxd < sizeof(bufRxd)) //接收緩沖區(qū)尚未用完時,
        {
            bufRxd[cntRxd++] = SBUF; //保存接收字節(jié),并遞增計數(shù)器
        }
}
if (TI)  //字節(jié)發(fā)送完畢
    {
        TI = 0;   //手動清零發(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定時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ù)頻率
    tmp = (tmp * ms) / 1000;  //計算所需的計數(shù)值
    tmp = 65536 - tmp;        //計算定時器重載值
    tmp = tmp + 18;           //修正中斷響應(yīng)延時造成的誤差

    T0RH = (unsigned char)(tmp >> 8);  //定時器重載值拆分為高低字節(jié)
    T0RL = (unsigned char)tmp;
    TMOD &= 0xF0;   //清零T0的控制位
    TMOD |= 0x01;   //配置T0為模式1
    TH0 = T0RH;     //加載T0重載值
    TL0 = T0RL;
    ET0 = 1;        //使能T0中斷
    TR0 = 1;        //啟動T0
}
void InterruptTimer0() interrupt 1  //T0中斷服務(wù)函數(shù)
{
    TH0 = T0RH;  //定時器重新加載重載值
    TL0 = T0RL;
    UartRxMonitor(1);  //串口接收監(jiān)控
}
我們在學(xué)習(xí)UART通信的時候,剛開始也是用的IO口去模擬UART通信過程,最終實(shí)現(xiàn)和電腦的通信。而后我們的STC89C52RC由于內(nèi)部具備了UART硬件通信模塊,所以我們直接可以通過配置寄存器就可以很輕松的實(shí)現(xiàn)單片機(jī)的UART通信。同樣的道理,我們這個I2C通信,如果我們單片機(jī)內(nèi)部有硬件模塊的話,單片機(jī)可以直接自動實(shí)現(xiàn)I2C通信了,就不需要我們再進(jìn)行IO口模擬起始、模擬發(fā)送、模擬結(jié)束,配置好寄存器,單片機(jī)就會把這些工作全部做了。
不過我們的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的通信時序,不僅僅是記住。
2、能夠獨(dú)立完成EEPROM任意地址的單字節(jié)讀寫、多字節(jié)的跨頁連續(xù)寫入讀出。
3、將前邊學(xué)的交通燈進(jìn)行改進(jìn),使用EEPROM保存紅燈和綠燈倒計時的時間,并且可以通過UART改變紅燈和綠燈倒計時時間。
4、使用按鍵、1602液晶、EEPROM做一個簡單的密碼鎖程序。

評分

參與人數(shù) 5黑幣 +31 收起 理由
hhh123hhh123 + 10 咿呀咿呀喲
Steven159 + 5 絕世好帖!
龍龍啊 + 10 很給力!
單片機(jī)~+ + 1 絕世好帖!
henryxue + 5 絕世好帖!

查看全部評分

分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏13 分享淘帖 頂10 踩
回復(fù)

使用道具 舉報

沙發(fā)
ID:65341 發(fā)表于 2014-9-18 15:36 | 只看該作者
我還是可以補(bǔ)充一點(diǎn)信息的

1.jpg (69.94 KB, 下載次數(shù): 331)

EEPROM

EEPROM

2.jpg (71.03 KB, 下載次數(shù): 320)

2.jpg

3.jpg (66.59 KB, 下載次數(shù): 352)

3.jpg

4.jpg (81.37 KB, 下載次數(shù): 343)

4.jpg
回復(fù)

使用道具 舉報

板凳
ID:88275 發(fā)表于 2015-8-13 17:47 | 只看該作者
樓主,,帖子里的代碼粘到 keil4里面后會成為亂碼呢,,這個有辦法解決嗎,謝謝!
常看您的帖子,受益匪淺,謝謝!
回復(fù)

使用道具 舉報

地板
ID:103908 發(fā)表于 2016-3-21 22:52 | 只看該作者
老師,最近都在看你的帖子,很受教,謝謝
回復(fù)

使用道具 舉報

5#
ID:124087 發(fā)表于 2016-6-1 23:51 | 只看該作者
贊。。。
回復(fù)

使用道具 舉報

6#
ID:163537 發(fā)表于 2017-2-6 15:11 | 只看該作者
老師,最近都在看你的帖子,很受教,謝謝!
回復(fù)

使用道具 舉報

7#
ID:268884 發(fā)表于 2018-1-2 08:56 | 只看該作者

老師,最近都在看你的帖子,很受教,謝謝!
回復(fù)

使用道具 舉報

8#
ID:279601 發(fā)表于 2018-1-25 15:41 | 只看該作者
剛剛好真是想睡覺就有人送抱枕啊,最近最新找的就是SPI通訊和IIC通訊教程了,沒想到剛注冊51黑電子論壇,就發(fā)現(xiàn)了這么多寶貴資源,感謝樓主的寶貴分享,剛好幫助我理解IIC通訊
回復(fù)

使用道具 舉報

9#
ID:241700 發(fā)表于 2018-3-5 12:32 | 只看該作者
講的真心不錯工,學(xué)習(xí)了!
回復(fù)

使用道具 舉報

10#
ID:303333 發(fā)表于 2018-4-7 21:04 | 只看該作者
受益匪淺.
回復(fù)

使用道具 舉報

11#
ID:315723 發(fā)表于 2018-5-2 14:43 | 只看該作者
還是我們學(xué)識淺了
回復(fù)

使用道具 舉報

12#
ID:93625 發(fā)表于 2018-7-17 16:21 | 只看該作者
越來越難了
回復(fù)

使用道具 舉報

13#
ID:370231 發(fā)表于 2018-8-6 15:43 | 只看該作者
看到后面越來越難理解了
回復(fù)

使用道具 舉報

14#
ID:168971 發(fā)表于 2018-8-19 01:17 來自手機(jī) | 只看該作者
學(xué)習(xí)了。!
回復(fù)

使用道具 舉報

15#
ID:135253 發(fā)表于 2018-8-20 23:23 | 只看該作者
仔細(xì)學(xué)習(xí)了一下,獲益匪淺,講的很透徹。。
回復(fù)

使用道具 舉報

16#
ID:135253 發(fā)表于 2018-8-21 09:53 | 只看該作者
版主原來是宋老師,久聞大名,怎么這么近呢?,現(xiàn)在仔細(xì)地將這本書啃一啃!

微信圖片_20180821094640.jpg (136.74 KB, 下載次數(shù): 148)

微信圖片_20180821094640.jpg
回復(fù)

使用道具 舉報

17#
ID:9727 發(fā)表于 2019-1-23 20:06 | 只看該作者
絕世好貼,感謝分享!
回復(fù)

使用道具 舉報

18#
ID:702974 發(fā)表于 2020-3-24 20:18 | 只看該作者
絕對給力
回復(fù)

使用道具 舉報

19#
ID:686782 發(fā)表于 2020-6-10 00:44 | 只看該作者
謝謝,受益匪淺!
回復(fù)

使用道具 舉報

20#
ID:623405 發(fā)表于 2021-9-10 11:38 | 只看該作者
太偉大了,等我這么吊的時候也要把經(jīng)驗分享出來。
回復(fù)

使用道具 舉報

您需要登錄后才可以回帖 登錄 | 立即注冊

本版積分規(guī)則

手機(jī)版|小黑屋|51黑電子論壇 |51黑電子論壇6群 QQ 管理員QQ:125739409;技術(shù)交流QQ群281945664

Powered by 單片機(jī)教程網(wǎng)

快速回復(fù) 返回頂部 返回列表