久久久久久久999_99精品久久精品一区二区爱城_成人欧美一区二区三区在线播放_国产精品日本一区二区不卡视频_国产午夜视频_欧美精品在线观看免费

 找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

搜索
查看: 40922|回復: 19
收起左側

第14章 I2C總線與EEPROM

  [復制鏈接]
ID:1 發表于 2013-9-28 15:25 | 顯示全部樓層 |閱讀模式
  本教材現以連載的方式由網絡發布,并將于2014年由清華大學出版社出版最終完整版,版權歸作者和清華大學出版社所有。本著開源、分享的理念,本教材可以自由傳播及學習使用,但是務必請注明出處來自金沙灘工作室
                 前幾章我們學了一種通信協議叫做UART異步串口通信,這節課我們要來學習第二種常用的通信協議I2CI2C總線是由PHILIPS公司開發的兩線式串行總線,多用于連接微處理器及其外圍設備。I2C總線的主要特點是接口方式簡單,兩條線可以掛多個參與通信的器件,即多機模式,而且任何一個器件都可以作為主機,當然同一時刻只能一個主機。

從原理上來講,UART屬于異步通信,比如電腦發送給單片機,電腦只負責把數據通過TXD發送出來即可,接收數據是單片機自己的事情。而I2C屬于同步通信,SCL時鐘線負責收發雙方的時鐘節拍,SDA數據線負責傳輸數據。I2C的發送方和接收方都以SCL這個時鐘節拍為基準進行數據的發送和接收。
從應用上來講,UART通信多用于板間通信,比如單片機和電腦,這個設備和另外一個設備之間的通信。而I2C多用于板內通信,比如單片機和我們本章要學的EEPROM之間的通信。
14.1 I2C時序初步認識
在硬件上,I2C總線是由時鐘總線SCL和數據總線SDA兩條線構成,連接到總線上的所有的器件的SCL都連到一起,所有的SDA都連到一起。I2C總線是開漏引腳并聯的結構,因此我們外部要添加上拉電阻。對于開漏電路外部加上拉電阻的話,那就組成了線“與”的關系。總線上線“與”的關系,那所有接入的器件保持高電平,這條線才是高電平。而任意一個器件輸出一個低電平,那這條線就會保持低電平,因此可以做到任何一個器件都可以拉低電平,也就是任何一個器件都可以作為主機,如圖14-1所示,我們添加了R63R64兩個上拉電阻。
1.JPG
14-1 I2C總線的上拉電阻
雖然說任何一個設備都可以作為主機,但絕大多數情況下我們都是用微處理器,也就是我們的單片機來做主機,而總線上掛的多個器件,每一個都像電話機一樣有自己唯一的地址,在信息傳輸的過程中,通過這唯一的地址可以正常識別到屬于自己的信息,在我們的KST-51開發板上,就掛接了2I2C設備,一個是24C02,一個是PCF8591
我們在學習UART串行通信的時候,知道了我們的通信流程分為起始位、數據位、停止位這三部分,同理在I2C中也有起始信號、數據傳輸和停止信號,如圖14-2所示。
          2.JPG
14-2 I2C時序流程圖
從圖上可以看出來,I2CUART時序流程有相似性,也有一定的區別。UART每個字節中,都有一個起始位,8個數據位和1位停止位。而I2C分為起始信號,數據傳輸部分,最后是停止信號。其中數據傳輸部分,可以一次通信過程傳輸很多個字節,字節數是不受限制的,而每個字節的數據最后也跟了一位,這一位叫做應答位,通常用ACK表示,有點類似于UART的停止位。
下面我們一部分一部分的把I2C通信時序進行剖析。之前我們學過了UART,所以學習I2C的過程我盡量拿UART來作為對比,這樣有助于更好的理解。但是有一點大家要理解清楚,就是UART通信雖然我們用了TXDRXD兩根線,但是實際一次通信,1條線就可以完成,2條線是把發送和接收分開而已,而I2C每次通信,不管是發送還是接收,必須2條線都參與工作才能完成,為了更方便的看出來每一位的傳輸流程,我們把圖14-2改進成圖14-3
            3.JPG
圖14-3 I2C通信流程解析
起始信號:UART通信是從一直持續的高電平出現一個低電平標志起始位;而I2C通信的起始信號的定義是SCL為高電平期間,SDA由高電平向低電平變化產生一個下降沿,表示起始信號,如圖14-3中的start部分所示。
數據傳輸:首先,UART是低位在前,高位在后;而I2C通信是高位在前,低位在后。第二,UART通信數據位是固定長度,波特率分之一,一位一位固定時間發送完畢就可以了。而I2C沒有固定波特率,但是有時序的要求,要求當SCL在低電平的時候,SDA允許變化,也就是說,發送方必須先保持SCL是低電平,才可以改變數據線SDA,輸出要發送的當前數據的一位;而當SCL在高電平的時候,SDA絕對不可以變化,因為這個時候,接收方要來讀取當前SDA的電平信號是0還是1,因此要保證SDA的穩定不變化,如圖14-3中的每一位數據的變化,都是在SCL的低電平位置。8為數據位后邊跟著的是一位響應位,響應位我們后邊還要具體介紹。
停止信號:UART通信的停止位是一位固定的高電平信號;而I2C通信停止信號的定義是SCL為高電平期間,SDA由低電平向高電平變化產生一個上升沿,表示結束信號,如圖14-3中的stop部分所示。
14.2 I2C尋址模式
上一節介紹的是I2C每一位信號的時序流程,而I2C通信在字節級的傳輸中,也有固定的時序要求。I2C通信的起始信號(Start)后,首先要發送一個從機的地址,這個地址一共有7位,緊跟著的第8位是數據方向位(R/W),‘0’表示接下來要發送數據(),‘1’表示接下來是請求數據()
我們知道,打電話的時候,當撥通電話,接聽方撿起電話肯定要回一個“喂”,這就是告訴撥電話的人,這邊有人了。同理,這個第九位ACK實際上起到的就是這樣一個作用。當我們發送完了這7位地址和1位方向位,如果我們發送的這個地址確實存在,那么這個地址的器件應該回應一個ACK0’,如果不存在,就沒“人”回應ACK
那我們寫一個簡單的程序,訪問一下我們板子上的EEPROM的地址,另外在寫一個不存在的地址,看看他們是否能回一個ACK,來了解和確認一下這個問題。
我們板子上的EEPROM器件型號是24C02,在24C02的數據手冊3.6部分說明了,24C027位地址中,其中高4位是固定的1010,而低3位的地址取決于我們電路的設計,由芯片上的A2A1A03個引腳的實際電平決定,來看一下我們的24C02的電路圖,如圖14-4所示。
4.JPG
14-4 24C02原理圖
從圖14-4可以看出來,我們的A2A1A0都是接的GND,也就是說都是0,因此我們的7位地址實際上是二進制的1010000,也就是0x50。我們用I2C的協議來尋址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; //讀取狀態字
        LCD1602_E = 0;
    } while (sta & 0x80); //bit7等于1表示液晶正忙,重復檢測直到其等于0為止
}
void LcdWriteCmd(unsigned char cmd)  //寫入命令函數
{
    LcdWaitReady();
    LCD1602_RS = 0;
    LCD1602_RW = 0;
    LCD1602_DB = cmd;
    LCD1602_E  = 1;
    LCD1602_E  = 0;
}
void LcdWriteDat(unsigned char dat)  //寫入數據函數
{
    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地址連續寫入字符串
    LcdWriteCmd(addr | 0x80); //寫入起始地址
    while (*str != '\0')      //連續寫入字符串數據,直到檢測到結束符
    {
        LcdWriteDat(*str);
        str++;
    }
}
void LcdInit()  //液晶初始化函數
{
    LcdWriteCmd(0x38);  //16*2顯示,5*7點陣,8位數據接口
    LcdWriteCmd(0x0C);  //顯示器開,光標關閉
    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';              //將地址和應答值轉換為字符串
    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';              //將地址和應答值轉換為字符串
    str[1] = '2';
    str[2] = ':';
    str[3] = (unsigned char)ack + '0';
    str[4] = '\0';
    LcdShowStr(8, 0, str);     //顯示到液晶上

    while(1)
    {}
}

void I2CStart()  //產生總線起始信號
{
    I2C_SDA = 1; //首先確保SDASCL都是高電平
    I2C_SCL = 1;
    I2CDelay();
    I2C_SDA = 0; //先拉低SDA
    I2CDelay();
    I2C_SCL = 0; //再拉低SCL
}
void I2CStop()   //產生總線停止信號
{
    I2C_SCL = 0; //首先確保SDASCL都是低電平
    I2C_SDA = 0;
    I2CDelay();
    I2C_SCL = 1; //先拉高SCL
    I2CDelay();
    I2C_SDA = 1; //再拉高SDA
    I2CDelay();
}
bit I2CWrite(unsigned char dat) //I2C總線寫操作,待寫入字節dat,返回值為從機應答位的值
{
    bit ack;  //用于暫存應答位的值
    unsigned char mask;  //用于探測字節內某一位值的掩碼變量

    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位數據發送完后,主機釋放SDA,以檢測從機應答
    I2CDelay();
    I2C_SCL = 1;   //拉高SCL
    I2CDelay();
    ack = I2C_SDA; //讀取此時的SDA值,即為從機的應答值
    I2C_SCL = 0;   //再拉低SCL完成應答位,并保持住總線

    return ack;    //返回從機應答值
}
bit I2CAddressing(unsigned char addr) //I2C尋址函數,即檢查地址為addr的器件是否存在,返回值為其應答值,即應答則表示存在,非應答則表示不存在
{
    bit ack;

    I2CStart();  //產生起始位,即啟動一次總線操作
    ack = I2CWrite(addr<<1);  //器件地址需左移一位,因尋址命令的最低位為讀寫位,用于表示之后的操作是讀或寫
    I2CStop();   //不需進行后續讀寫,而直接停止本次總線操作

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

    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位數據發送完后,主機釋放SDA,以檢測從機應答
    I2CDelay();
    I2C_SCL = 1;   //拉高SCL
    ack = I2C_SDA; //讀取此時的SDA值,即為從機的應答值
    I2CDelay();
    I2C_SCL = 0;   //再拉低SCL完成應答位,并保持住總線

    return (~ack); //應答值取反以符合通常的邏輯:0=不存在或忙或寫入失敗,1=存在且空閑或寫入成功
}
unsigned char I2CReadNAK() //I2C總線讀操作,并發送非應答信號,返回值為讀到的字節
{
    unsigned char mask;
    unsigned char dat;

    I2C_SDA = 1;  //首先確保主機釋放SDA
    for (mask=0x80; mask!=0; mask>>=1) //從高位到低位依次進行
    {
        I2CDelay();
        I2C_SCL = 1;      //拉高SCL
        if(I2C_SDA == 0)  //讀取SDA的值
            dat &= ~mask; //0時,dat中對應位清零
        else
            dat |= mask;  //1時,dat中對應位置1
        I2CDelay();
        I2C_SCL = 0;      //再拉低SCL,以使從機發送出下一位
    }
    I2C_SDA = 1;   //8位數據發送完后,拉高SDA,發送非應答信號
    I2CDelay();
    I2C_SCL = 1;   //拉高SCL
    I2CDelay();
    I2C_SCL = 0;   //再拉低SCL完成非應答位,并保持住總線

    return dat;
}
unsigned char I2CReadACK() //I2C總線讀操作,并發送應答信號,返回值為讀到的字節
{
    unsigned char mask;
    unsigned char dat;

    I2C_SDA = 1;  //首先確保主機釋放SDA
    for (mask=0x80; mask!=0; mask>>=1) //從高位到低位依次進行
    {
        I2CDelay();
        I2C_SCL = 1;      //拉高SCL
        if(I2C_SDA == 0)  //讀取SDA的值
            dat &= ~mask; //0時,dat中對應位清零
        else
            dat |= mask;  //1時,dat中對應位置1
        I2CDelay();
        I2C_SCL = 0;      //再拉低SCL,以使從機發送出下一位
    }
    I2C_SDA = 0;   //8位數據發送完后,拉低SDA,發送應答信號
    I2CDelay();
    I2C_SCL = 1;   //拉高SCL
    I2CDelay();
    I2C_SCL = 0;   //再拉低SCL完成應答位,并保持住總線

    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()  //等待液晶準備好
{
    unsigned char sta;

    LCD1602_DB = 0xFF;
    LCD1602_RS = 0;
    LCD1602_RW = 1;
    do
    {
        LCD1602_E = 1;
        sta = LCD1602_DB; //讀取狀態字
        LCD1602_E = 0;
    } while (sta & 0x80); //bit7等于1表示液晶正忙,重復檢測直到其等于0為止
}
void LcdWriteCmd(unsigned char cmd)  //寫入命令函數
{
    LcdWaitReady();
    LCD1602_RS = 0;
    LCD1602_RW = 0;
    LCD1602_DB = cmd;
    LCD1602_E  = 1;
    LCD1602_E  = 0;
}
void LcdWriteDat(unsigned char dat)  //寫入數據函數
{
    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地址連續寫入字符串
    LcdWriteCmd(addr | 0x80); //寫入起始地址
    while (*str != '\0')      //連續寫入字符串數據,直到檢測到結束符
    {
        LcdWriteDat(*str);
        str++;
    }
}
void LcdInit()  //液晶初始化函數
{
    LcdWriteCmd(0x38);  //16*2顯示,5*7點陣,8位數據接口
    LcdWriteCmd(0x0C);  //顯示器開,光標關閉
    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);    //讀取指定地址上的一個字節
    str[0] = (dat/100) + '0';  //轉換為十進制字符串格式
    str[1] = (dat/10%10) + '0';
    str[2] = (dat%10) + '0';
    str[3] = '\0';
    LcdShowStr(0, 0, str);     //顯示在液晶上
    dat++;                     //將其數值+1
    E2WriteByte(0x02, dat);    //再寫回到對應的地址上

    while(1)
    {}
}

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

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

    return dat;
}

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

    while (len--)
    {
        tmp = *array >> 4;         //先取高4
        if (tmp <= 9)              //轉換為0-9A-F
            *str = tmp + '0';
        else
            *str = tmp - 10 + 'A';
        str++;
        tmp = *array & 0x0F;       //再取低4
        if (tmp <= 9)              //轉換為0-9A-F
            *str = tmp + '0';
        else
            *str = tmp - 10 + 'A';
        str++;
        *str = ' ';                //轉換完一個字節添加一個空格
        str++;
        array++;
    }
}
void E2Read(unsigned char *buf, unsigned char addr, unsigned char len) //E2讀取函數,數據接收指針bufE2中的起始地址addr,讀取長度len
{
    do {                       //用尋址操作查詢當前是否可進行讀寫操作
        I2CStart();
        if (I2CWrite(0x50<<1)) //器件應答則跳出循環,繼續執行,非應答則進行下一次查詢
            break;
        I2CStop();
    } while(1);
    I2CWrite(addr);           //寫入起始地址
    I2CStart();               //發送重復啟動信號
    I2CWrite((0x50<<1)|0x01); //尋址器件,后續為讀操作
    while (len > 1)           //連續讀取len-1個字節
    {
        *buf = I2CReadACK();  //最后字節之前為讀取操作+應答
        buf++;
        len--;
    }
    *buf = I2CReadNAK();      //最后一個字節為讀取操作+非應答
    I2CStop();
}

void E2Write(unsigned char *buf, unsigned char addr, unsigned char len) //E2寫入函數,源數據指針bufE2中的起始地址addr寫入長度len
{
    while (len--)
    {
        do {                       //用尋址操作查詢當前是否可進行讀寫操作,即等待上一次寫入操作完成
            I2CStart();
            if (I2CWrite(0x50<<1)) //器件應答則跳出循環,繼續執行,非應答則進行下一次查詢
                break;
            I2CStop();
        } while(1);
        I2CWrite(addr);           //寫入起始地址
        I2CWrite(*buf);           //寫入一個字節數據
        I2CStop();                //結束寫操作,以等待寫入完成
        buf++;                    //數據指針遞增
        addr++;                   //E2地址遞增
    }
}

函數ArrayToHexStr:這是一個把數組轉換成十六進制字符串的形式。由于我們從EEPROM讀出來的是正常的數據,而1602液晶接收的是ASCII碼字符,因此我們要通過液晶把數據顯示出來必須先通過一步轉換。算法倒是很簡單,就是把每一個字節的數據高4位和低4位分開,和9進行比較,如果小于等于9,則通過數字加’0’轉ASCII碼發送;如果大于9,則通過加’A’轉ASCII碼發送出去。
函數E2Read:我們在讀之前,要查詢一下當前是否可以進行讀寫操作,EEPROM正常響應才可以進行。進行后,最后一個字節之前的,全部給出ACK,而讀完了最后一個字節,我們要給出一個NAK
函數E2Write:每次寫操作之前,我們都要進行查詢判斷當前EEPROM是否響應,正常響應后才可以寫數據。
14.3.3 EEPROM的頁寫入
如果每個數據都連續寫入,像我們上節課那樣寫的時候,每次都先起始位,再訪問一下這個EEPROM的地址,看看是否響應,感覺上效率太低了。因此EEPROM的廠商就想了一個辦法,把EEPROM分頁管理。24c0124c02這兩個型號是8個字節一個頁,而24c0424c0824c1616個字節一頁。我們板子上的型號是24C02,一共是256個字節,8個字節一頁,那么就一共有32頁。
分配好頁之后,如果我們在同一個頁內連續寫入幾個字節后,最后再發送停止位的時序。EEPROM檢測到這個停止位后,統一把這一頁的數據寫到非易失區域,就不需要像上節課那樣寫一個字節檢測一次了,并且頁寫入的時間也不會超過5ms。如果我們寫入的數據跨頁了,那么寫完了一頁之后,我們要發送一個停止位,然后等待并且檢測EEPROM的空閑模式,一直等到把上一頁數據完全寫到非易失區域后,再進行下一頁的寫入,這樣就可以在一定程度上提高我們的寫入效率。

        
                /***********************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讀取函數,數據接收指針bufE2中的起始地址addr,讀取長度len
{
    do {                       //用尋址操作查詢當前是否可進行讀寫操作
        I2CStart();
        if (I2CWrite(0x50<<1)) //器件應答則跳出循環,繼續執行,非應答則進行下一次查詢
            break;
        I2CStop();
    } while(1);
    I2CWrite(addr);           //寫入起始地址
    I2CStart();               //發送重復啟動信號
    I2CWrite((0x50<<1)|0x01); //尋址器件,后續為讀操作
    while (len > 1)           //連續讀取len-1個字節
    {
        *buf = I2CReadACK();  //最后字節之前為讀取操作+應答
        buf++;
        len--;
    }
    *buf = I2CReadNAK();      //最后一個字節為讀取操作+非應答
    I2CStop();
}

void E2Write(unsigned char *buf, unsigned char addr, unsigned char len) //E2寫入函數,源數據指針bufE2中的起始地址addr,寫入長度len
{
    while (len > 0)
    {
        //等待上次寫入操作完成
        do {
            I2CStart();
            if (I2CWrite(0x50<<1)) //器件應答則跳出循環,繼續執行,非應答則進行下一次查詢
                break;
            I2CStop();
        } while(1);
        //按頁寫模式連續寫入字節
        I2CWrite(addr);           //寫入起始地址
        while (len > 0)
        {
            I2CWrite(*buf);       //寫入一個字節數據
            len--;                //待寫入長度計數遞減
            buf++;                //數據指針遞增
            addr++;               //E2地址遞增
            if ((addr&0x07) == 0) //檢查地址是否到達頁邊界,24C02每頁8字節,所以檢測低3位是否為零即可
                break;            //到達頁邊界時,跳出循環,結束本次寫操作
        }
        I2CStop();
    }
}
這個eeprom.c文件中的程序,單獨做一個文件,用來管理eeprom的訪問。其中E2Read函數和上一節是一樣的,因為讀操作和是否同一頁無關。重點是E2Write函數,我們在寫入數據的時候,要計算下一個要寫的數據的地址是否是一個頁的起始地址,如果是的話,則必須跳出循環,等待EEPROM上一頁寫入到非易失區域后,再進行繼續寫入。
而寫了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中讀取一段數據
    ArrayToHexStr(str, buf, sizeof(buf)); //轉換為十六進制字符串
    LcdShowStr(0, 0, str);                //顯示到液晶上
    for (i=0; i<sizeof(buf); i++)         //數據依次+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) //把一個字節數組轉換為十六進制字符串的格式
{
    unsigned char tmp;

    while (len--)
    {
        tmp = *array >> 4;         //先取高4
        if (tmp <= 9)              //轉換為0-9A-F
            *str = tmp + '0';
        else
            *str = tmp - 10 + 'A';
        str++;
        tmp = *array & 0x0F;       //再取低4
        if (tmp <= 9)              //轉換為0-9A-F
            *str = tmp + '0';
        else
            *str = tmp - 10 + 'A';
        str++;
        *str = ' ';                //轉換完一個字節添加一個空格
        str++;
        array++;
    }
}
多字節寫入和頁寫入程序都編寫出來了,而且頁寫入的程序我們還特地跨頁寫的數據,他們的寫入時間到底差別多大呢。我們用一些工具可以測量一下,比如示波器,邏輯分析儀等工具。我現在把兩次寫入時間用邏輯分析儀給抓了出來,并且用時間標簽T1T2給標注了開始位置和結束位置,如圖14-5和圖14-6所示,右側顯示的|T1-T2|就是最終寫入5個字節所耗費的時間。多字節一個一個寫入,每次寫入后都需要再次通信檢測EEPROM是否在“忙”,因此耗費了大量的時間,同樣的寫入5個字節的數據,一個一個寫入用了8.4ms左右的時間,而使用頁寫入,只用了3.5ms左右的時間。
      
1.JPG
圖14-5 多字節寫入時間
   
2.JPG
圖14-6 跨頁寫入時間
14.4 I2C和EEPROM的綜合實驗學習[size=14.0000pt]
電視頻道記憶功能,交通燈倒計時時間的設定,戶外LED廣告的記憶功能,都有可能有類似EEPROM這類存儲器件。這類器件的優勢是存儲的數據不僅可以改變,而且掉電后數據保存不丟失,因此大量應用在各種電子產品上。
我們這節課的例程,有點類似廣告屏。上電后,1602的第一行顯示EEPROM0x20地址開始的16個字符,第二行顯示EERPOM0x40開始的16個字符。我們可以通過UART串口通信來改變EEPROM內部的這個數據,并且同時改變了1602顯示的內容,下次上電的時候,直接會顯示我們更新過的內容。
這個程序所有的相關內容,我們之前都已經講過了。但是這個程序體現在了一個綜合程序應用能力上。這個程序用到了1602液晶、UART實用串口通信、EEPROM讀寫操作等多個功能的綜合應用。寫個點亮小燈好簡單,但是我們想學會真正的單片機,必須得學會這種綜合程序的應用,實現多個模塊同時參與工作,這個理念在我們的全板子測試視頻里已經有所體現。因此同學們,要認認真真的把工程建立起來,一行一行的把程序編寫起來,最終鞏固下來。

        
                /***********************lcd1602.c文件程序源代碼*************************/
        
                                                       略
        
                /************************I2C.c文件程序源代碼***************************/
        
        略
        
                /***********************eeprom.c文件程序源代碼*************************/
        
        略
        
                /************************uart.c文件程序源代碼***************************/


#include <reg52.h>

bit flagOnceTxd = 0;  //單次發送完成標志,即發送完一個字節
bit cmdArrived = 0;   //命令到達標志,即接收到上位機下發的命令
unsigned char cntRxd = 0;
unsigned char pdata bufRxd[40]; //串口接收緩沖區

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)  //串口配置函數,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) //串口數據讀取函數,數據接收指針buf,讀取數據長度len,返回值為實際讀取到的數據長度
{
    unsigned char i;

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

    return len;  //返回實際讀取長度
}
void UartWrite(unsigned char *buf, unsigned char len) //串口數據寫入函數,即串口發送函數,待發送數據指針buf,數據長度len
{
    while (len--)
    {
        flagOnceTxd = 0;
        SBUF = *buf;
        buf++;
        while (!flagOnceTxd);
    }
}

bit CmdCompare(unsigned char *buf, const unsigned char *cmd) //命令比較函數,緩沖區數據與指定命令比較,相同返回1,不同返回0
{
    while (*cmd != '\0')
    {
        if (*cmd != *buf) //遇到不相同字符時即刻返回0
        {
            return 0;
        }
        else //當前字符相等時,指針遞增準備比較下一字符
        {
            cmd++;
            buf++;
        }
    }
    return 1; //到命令字符串結束時字符都相等則返回1
}
void TrimString16(unsigned char *out, unsigned char *in) //將一字符串整理成16字節的固定長度字符串,不足部分補空格
{
    unsigned char i = 0;

    while (*in != '\0') //拷貝字符串直到輸入字符串結束
    {
        *out = *in;
        out++;
        in++;
        i++;
        if (i >= 16)   //當拷貝長度已達到16字節時,強制跳出循環
            break;
    }
    for ( ; i<16; i++) //如不足16個字節則用空格補齊
    {
        *out = ' ';
        out++;
    }
    *out = '\0';       //最后添加結束符
}
void UartDriver() //串口驅動函數,檢測接收到的命令并執行相應動作
{
    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) //有命令到達時,讀取處理該命令
    {
        cmdArrived = 0;
        for (i=0; i<sizeof(buf); i++) //清零命令接收緩沖區
        {
            buf[ i] = 0;
        }
        len = UartRead(buf, sizeof(buf)); //將接收到的命令讀取到緩沖區中
        for (i=0; i<sizeof(cmdList)/sizeof(cmdList[0]); i++) //與所支持的命令列表逐一進行比較
        {
            if (CmdCompare(buf, cmdList[ i]) == 1) //檢測到相符命令時退出循環,此時的i值就是該命令在列表中的下標值
            {
                break;
            }
        }
        switch (i) //根據比較結果執行相應命令
        {
            case 0:
                buf[len] = '\0';                       //為接收到的字符串添加結束符
                TrimString16(str, buf+sizeof(cmd0)-1); //整理成16字節的固定長度字符串,不足部分補空格
                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大于命令列表最大下標時,即表示沒有相符的命令,給上機發送“錯誤命令”的提示
                UartWrite("bad command.\r\n", sizeof("bad command.\r\n")-1);
                return;
        }
        buf[len++] = '\r';  //有效命令被執行后,在原命令幀之后添加回車換行符后返回給上位機,表示已執行
        buf[len++] = '\n';
        UartWrite(buf, len);
    }
}

void UartRxMonitor(unsigned char ms)  //串口接收監控函數
{
    static unsigned char cntbkp = 0;
    static unsigned char idletmr = 0;

    if (cntRxd > 0)  //接收計數器大于零時,監控總線空閑時間
    {
        if (cntbkp != cntRxd)  //接收計數器改變,即剛接收到數據時,清零空閑計時
        {
            cntbkp = cntRxd;
            idletmr = 0;
        }
        else
        {
            if (idletmr < 30)  //接收計數器未改變,即總線空閑時,累積空閑時間
            {
                idletmr += ms;
                if (idletmr >= 30)  //空閑時間超過30ms即認為一幀命令接收完畢
                {
                    cmdArrived = 1; //設置命令到達標志
                }
            }
        }
    }
    else
    {
        cntbkp = 0;
    }
}
void InterruptUART() interrupt 4  //UART中斷服務函數
{
if (RI)  //接收到字節
    {
RI = 0;   //手動清零接收中斷標志位
        if (cntRxd < sizeof(bufRxd)) //接收緩沖區尚未用完時,
        {
            bufRxd[cntRxd++] = SBUF; //保存接收字節,并遞增計數器
        }
}
if (TI)  //字節發送完畢
    {
        TI = 0;   //手動清零發送中斷標志位
        flagOnceTxd = 1;  //設置單次發送完成標志
     }
}
/************************main.c文件程序源代碼**************************/

#include <reg52.h>

unsigned char T0RH = 0;  //T0重載值的高字節
unsigned char T0RL = 0;  //T0重載值的低字節

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();    //初始顯示內容

    while(1)
    {
        UartDriver();
    }
}

void InitShowStr()  //處理液晶屏初始顯示內容
{
    unsigned char str[17];

    str[16] = '\0';         //在最后添加字符串結束符,確保字符串可以結束
    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配置函數
{
    unsigned long tmp;

    tmp = 11059200 / 12;      //定時器計數頻率
    tmp = (tmp * ms) / 1000;  //計算所需的計數值
    tmp = 65536 - tmp;        //計算定時器重載值
    tmp = tmp + 18;           //修正中斷響應延時造成的誤差

    T0RH = (unsigned char)(tmp >> 8);  //定時器重載值拆分為高低字節
    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中斷服務函數
{
    TH0 = T0RH;  //定時器重新加載重載值
    TL0 = T0RL;
    UartRxMonitor(1);  //串口接收監控
}
我們在學習UART通信的時候,剛開始也是用的IO口去模擬UART通信過程,最終實現和電腦的通信。而后我們的STC89C52RC由于內部具備了UART硬件通信模塊,所以我們直接可以通過配置寄存器就可以很輕松的實現單片機的UART通信。同樣的道理,我們這個I2C通信,如果我們單片機內部有硬件模塊的話,單片機可以直接自動實現I2C通信了,就不需要我們再進行IO口模擬起始、模擬發送、模擬結束,配置好寄存器,單片機就會把這些工作全部做了。
不過我們的STC89C52RC單片機內部不具備I2C的硬件模塊,所以我們使用STC89C52RC單片機進行I2C通信必須用IO口來模擬。使用IO口模擬I2C,實際上更有利于我們徹底理解透徹I2C通信的實質。當然了,通過學習IO口模擬通信,今后我們如果遇到內部帶I2C模塊的單片機,也應該很輕松的搞定,使用內部的硬件模塊,可以提高程序的執行效率。
14.5 作業
1、徹底理解I2C的通信時序,不僅僅是記住。
2、能夠獨立完成EEPROM任意地址的單字節讀寫、多字節的跨頁連續寫入讀出。
3、將前邊學的交通燈進行改進,使用EEPROM保存紅燈和綠燈倒計時的時間,并且可以通過UART改變紅燈和綠燈倒計時時間。
4、使用按鍵、1602液晶、EEPROM做一個簡單的密碼鎖程序。

評分

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

查看全部評分

回復

使用道具 舉報

ID:65341 發表于 2014-9-18 15:36 | 顯示全部樓層
我還是可以補充一點信息的

EEPROM

EEPROM
2.jpg
3.jpg
4.jpg
回復

使用道具 舉報

ID:88275 發表于 2015-8-13 17:47 | 顯示全部樓層
樓主,,帖子里的代碼粘到 keil4里面后會成為亂碼呢,,這個有辦法解決嗎,謝謝!!
常看您的帖子,受益匪淺,謝謝!
回復

使用道具 舉報

ID:103908 發表于 2016-3-21 22:52 | 顯示全部樓層
老師,最近都在看你的帖子,很受教,謝謝
回復

使用道具 舉報

ID:124087 發表于 2016-6-1 23:51 | 顯示全部樓層
贊!!!!
回復

使用道具 舉報

ID:163537 發表于 2017-2-6 15:11 | 顯示全部樓層
老師,最近都在看你的帖子,很受教,謝謝!
回復

使用道具 舉報

ID:268884 發表于 2018-1-2 08:56 | 顯示全部樓層

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

使用道具 舉報

ID:279601 發表于 2018-1-25 15:41 | 顯示全部樓層
剛剛好真是想睡覺就有人送抱枕啊,最近最新找的就是SPI通訊和IIC通訊教程了,沒想到剛注冊51黑電子論壇,就發現了這么多寶貴資源,感謝樓主的寶貴分享,剛好幫助我理解IIC通訊
回復

使用道具 舉報

ID:241700 發表于 2018-3-5 12:32 | 顯示全部樓層
講的真心不錯工,學習了!
回復

使用道具 舉報

ID:303333 發表于 2018-4-7 21:04 | 顯示全部樓層
受益匪淺.
回復

使用道具 舉報

ID:315723 發表于 2018-5-2 14:43 | 顯示全部樓層
還是我們學識淺了
回復

使用道具 舉報

ID:93625 發表于 2018-7-17 16:21 | 顯示全部樓層
越來越難了
回復

使用道具 舉報

ID:370231 發表于 2018-8-6 15:43 | 顯示全部樓層
看到后面越來越難理解了
回復

使用道具 舉報

ID:168971 發表于 2018-8-19 01:17 來自手機 | 顯示全部樓層
學習了!!!
回復

使用道具 舉報

ID:135253 發表于 2018-8-20 23:23 | 顯示全部樓層
仔細學習了一下,獲益匪淺,講的很透徹!!!
回復

使用道具 舉報

ID:135253 發表于 2018-8-21 09:53 | 顯示全部樓層
版主原來是宋老師,久聞大名,怎么這么近呢?,現在仔細地將這本書啃一啃!
微信圖片_20180821094640.jpg
回復

使用道具 舉報

ID:9727 發表于 2019-1-23 20:06 | 顯示全部樓層
絕世好貼,感謝分享!
回復

使用道具 舉報

ID:702974 發表于 2020-3-24 20:18 | 顯示全部樓層
絕對給力
回復

使用道具 舉報

ID:686782 發表于 2020-6-10 00:44 | 顯示全部樓層
謝謝,受益匪淺!
回復

使用道具 舉報

ID:623405 發表于 2021-9-10 11:38 | 顯示全部樓層
太偉大了,等我這么吊的時候也要把經驗分享出來。
回復

使用道具 舉報

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

本版積分規則

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

Powered by 單片機教程網

快速回復 返回頂部 返回列表
主站蜘蛛池模板: 久久久久久久久毛片 | 日韩在线一区二区三区 | 成人18亚洲xxoo | 免费在线黄 | 精品av| 国产日韩一区二区三免费高清 | 激情欧美日韩一区二区 | 成人中文字幕av | 91精品国产美女在线观看 | 久久精品青青大伊人av | 国产日韩欧美电影 | 翔田千里一区二区 | 天天亚洲 | 亚洲高清视频在线观看 | 国产无人区一区二区三区 | 日韩视频精品 | 成av在线 | 日韩欧美国产精品一区 | 99精品国产一区二区三区 | 欧美一区二区在线看 | 四虎永久免费影院 | 91大神在线资源观看无广告 | 亚洲精品久久久蜜桃网站 | 日韩激情视频一区 | 丝袜美腿av | 在线观看国产视频 | 91久久精品一区二区二区 | 天天操网| 精品国产乱码久久久久久a丨 | 国产精品二区三区在线观看 | 一级毛片免费 | 欧美理论片在线 | 欧一区二区 | 91p在线观看 | 日韩在线免费观看视频 | 美国一级片在线观看 | 香蕉国产在线视频 | 粉色午夜视频 | 精品九九 | 91日b| 欧美成人视屏 |