單片機:第14章 I2C 總線與 EEPROM

第14章 I2C 總線與 EEPROM

I2C 總線是由 PHILIPS 公司開發的兩線式串行總線,多用於連接微處理器及其外圍芯片。I2C 總線的主要特點是接口方式簡單,兩條線可以掛多個參與通信的器件,即多機模式,而且任何一個器件都可以作爲主機,當然同一時刻只能有一個主機。

從原理上來講,UART 屬於異步通信,比如電腦發送給單片機,電腦只負責把數據通過TXD 發送出來即可,接收數據是單片機自己的事情。而I2C 屬於同步通信,SCL 時鐘線負責收發雙方的時鐘節拍,SDA 數據線負責傳輸數據。I2C 的發送方和接收方都以 SCL 這個時鐘節拍爲基準進行數據的發送和接收。

從應用上來講,UART 通信多用於板間通信,比如單片機和電腦,這個設備和另外一個設備之間的通信。而 I2C 多用於板內通信,比如單片機和我們本章要學的 EEPROM 之間的通信。

14.1 I2C 時序初步認識

在硬件上,I2C 總線是由時鐘總線 SCL 和數據總線 SDA 兩條線構成,連接到總線上的所有器件的 SCL 都連到一起,所有 SDA 都連到一起。I2C 總線是開漏引腳並聯的結構,因此我們外部要添加上拉電阻。對於開漏電路外部加上拉電阻,就組成了線“與”的關係。總線上線“與”的關係就是說,所有接入的器件保持高電平,這條線是高電平,而任何一個器件輸出一個低電平,那這條線就會保持低電平,因此可以做到任何一個器件都可以拉低電平,也就是任何一個器件都可以作爲主機,如圖 14-1 所示,我們添加了 R63 和 R64 兩個上拉電阻。
在這裏插入圖片描述
圖 14-1

雖然說任何一個設備都可以作爲主機,但絕大多數情況下我們都是用單片機來做主機,而總線上掛的多個器件,每一個都像電話機一樣有自己唯一的地址,在信息傳輸的過程中,通過這唯一的地址就可以正常識別到屬於自己的信息,在 KST-51 開發板上,就掛接了 2 個I2C 設備,一個是 24C02,一個是 PCF8591。

我們在學習 UART 串行通信的時候,知道了通信流程分爲起始位、數據位、停止位這三部分,同理在 I2C 中也有起始信號、數據傳輸和停止信號,如圖 14-2 所示。

在這裏插入圖片描述

圖 14-2 I2C 時序流程圖
從圖上可以看出來,I2C 和 UART 時序流程有相似性,也有一定的區別。UART 每個字節中,都有一個起始位、8 個數據位、1 位停止位。而 I2C 分爲起始信號、數據傳輸部分、停止信號。其中數據傳輸部分,可以一次通信過程傳輸很多個字節,字節數是不受限制的,而每個字節的數據最後也跟了一位,這一位叫做應答位,通常用 ACK 表示,有點類似於 UART的停止位。

下面我們一部分一部分的把 I2C 通信時序進行剖析。之前我們已經學過了 UART,所以學習 I2C 的過程我儘量拿 UART 來作爲對比,這樣有助於更好的理解。但是有一點大家要理解清楚,就是 UART 通信雖然用了 TXD 和 RXD 兩根線,但是實際一次通信中,1 條線就可以完成,2 條線是把發送和接收分開而已,而 I2C 每次通信,不管是發送還是接收,必須 2條線都參與工作才能完成,爲了更方便的看出來每一位的傳輸流程,我們把圖 14-2 改進成圖14-3。
在這裏插入圖片描述
圖 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 位方向後,如果發送的這個地址確實存在,那麼這個地址的器件應該回應一個 ACK(拉低 SDA 即輸出“0”),如果不存在,就沒“人”迴應 ACK(SDA將保持高電平即“1”)。

那我們寫一個簡單的程序,訪問一下我們板子上的 EEPROM 的地址,另外再寫一個不存在的地址,看看它們是否能回一個 ACK,來了解和確認一下這個問題。

我們板子上的EEPROM 器件型號是24C02,在24C02 的數據手冊3.6 節中可查到,24C02的 7 位地址中,其中高 4 位是固定的 0b1010,而低 3 位的地址取決於具體電路的設計,由芯片上的 A2、A1、A0 這 3 個引腳的實際電平決定,來看一下我們的 24C02 的電路圖,它和24C01 的原理圖完全一樣,如圖 14-4 所示。

在這裏插入圖片描述
圖 14-4 24C02 原理圖
從圖 14-4 可以看出來,我們的 A2、A1、A0 都是接的 GND,也就是說都是 0,因此 24C02的 7 位地址實際上是二進制的 0b1010000,也就是 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 爲止 
} 
/* 向 LCD1602 液晶寫入一字節命令,cmd-待寫入命令值 */ 
void LcdWriteCmd(unsigned char cmd) 
{ 
    LcdWaitReady(); 
    LCD1602_RS = 0; 
    LCD1602_RW = 0; 
    LCD1602_DB = cmd; 
    LCD1602_E  = 1; 
    LCD1602_E  = 0; 
} 
/* 向 LCD1602 液晶寫入一字節數據,dat-待寫入數據值 */ 
void LcdWriteDat(unsigned char dat) 
{ 
    LcdWaitReady(); 
    LCD1602_RS = 1; 
    LCD1602_RW = 0; 
    LCD1602_DB = dat; 
    LCD1602_E  = 1; 
    LCD1602_E  = 0; 
} 
/* 設置顯示 RAM 起始地址,亦即光標位置,(x,y)-對應屏幕上的字符座標 */ 
void LcdSetCursor(unsigned char x, unsigned char y) 
{ 
    unsigned char addr; 
     
    if (y == 0) //由輸入的屏幕座標計算顯示 RAM 的地址 
        addr = 0x00 + x; //第一行字符地址從 0x00 起始 
    else 
        addr = 0x40 + x; //第二行字符地址從 0x40 起始 
    LcdWriteCmd(addr | 0x80); //設置 RAM 地址 
} 
/* 在液晶上顯示字符串,(x,y)-對應屏幕上的起始座標,str-字符串指針 */ 
void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str) 
{ 
    LcdSetCursor(x, y);   //設置起始地址 
    while (*str != '\0')  //連續寫入字符串數據,直到檢測到結束符 
    { 
        LcdWriteDat(*str++); 
    } 
} 
/* 初始化 1602 液晶 */ 
void InitLcd1602() 
{ 
    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 InitLcd1602(); 
extern void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str); 
 
void main() 
{ 
    bit ack; 
    unsigned char str[10]; 
 
    InitLcd1602();   //初始化液晶 
     
    ack = I2CAddressing(0x50); //查詢地址爲 0x50 的器件 
    str[0] = '5';                //將地址和應答值轉換爲字符串 
    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; //首先確保 SDA、SCL 都是高電平,
    			// 這兩句與停止信號的代碼設置順序正好相反
    			//此處先SDA,再SCL,如果順序相反會產生什麼後果?
    I2C_SCL = 1; 
    I2CDelay(); 
    I2C_SDA = 0; //先拉低 SDA 
    I2CDelay(); 
    I2C_SCL = 0; //再拉低 SCL 
} 
/* 產生總線停止信號 */ 
void I2CStop() 
{ 
    I2C_SCL = 0; //首先確保 SDA、SCL 都是低電平 ,
    			// 如果SCL和SDA是高電平,下面拉低SDA將產生起始信號,干擾其它設備。
    			//此處先SCL,再SDA,如果順序相反會產生什麼後果?
    I2C_SDA = 0; 
    I2CDelay(); 
    I2C_SCL = 1; //先拉高 SCL 
    I2CDelay(); 
    I2C_SDA = 1; //再拉高 SDA 
    I2CDelay(); 
} 
/* I2C 總線寫操作,dat-待寫入字節,返回值-從機應答位的值 */ 
bit I2CWrite(unsigned char 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,以檢測從機應答,
    			//如果這裏不釋放,一旦SDA是低電平,
    			//那麼,整條總線將總是低電平,無法得到正確的反饋
    I2CDelay(); 
    I2C_SCL = 1; //拉高 SCL 
    ack = I2C_SDA; //讀取此時的 SDA 值,即爲從機的應答值 
    I2CDelay(); 
    I2C_SCL = 0; //再拉低 SCL 完成應答位,並保持住總線 
    return ack;    //返回從機應答值 
} 
/* I2C 尋址函數,即檢查地址爲 addr(7位數據,不包括讀寫位) 的器件是否存在,
返回值-從器件應答值,0:從機存在,1:從機不存在 */ 
bit I2CAddressing(unsigned char addr) 
{ 
    bit ack; 
 
    I2CStart();  //產生起始位,即啓動一次總線操作 
    ack = I2CWrite(addr << 1);  //器件地址需左移一位,因尋址命令的最低位 
                              //爲讀寫位,用於表示之後的操作是讀或寫 
    I2CStop();   //不需進行後續讀寫,而直接停止本次總線操作 
     
    return ack; 
} 

我們把這個程序在KST-51 開發板上運行完畢,會在液晶上邊顯示出來我們預想的結果,主機發送一個存在的從機地址,從機會回覆一個應答位,即應答位爲 0;主機如果發送一個不存在的從機地址,就沒有從機應答,即應答位爲 1。

前面的章節中已經提到利用庫函數_nop_()可以進行精確延時,一個_nop_()的時間就是一個機器週期,這個庫函數包含在 intrins.h 這個文件中,如果要使用這個庫函數,只需要在程序最開始,和包含 reg52.h 一樣,include<intrins.h>之後,程序中就可以使用這個庫函數了。

還有一點要提一下,I2C 通信分爲低速模式 100kbit/s、快速模式 400kbit/s 和高速模式3.4Mbit/s。因爲所有的 I2C 器件都支持低速,但卻未必支持另外兩種速度,所以作爲通用的I2C 程序我們選擇 100k 這個速率來實現,也就是說實際程序產生的時序必須小於等於 100k的時序參數,很明顯也就是要求 SCL 的高低電平持續時間都不短於 5us,因此我們在時序函數中通過插入 I2CDelay()這個總線延時函數(它實際上就是 4 個NOP 指令,用 define 在文件開頭做了定義),加上改變 SCL 值語句本身佔用的至少一個週期,來達到這個速度限制。如果以後需要提高速度,那麼只需要減小這裏的總線延時時間即可。

此外我們要學習一個發送數據的技巧,就是 I2C 通信時如何將一個字節的數據發送出去。大家注意函數 I2CWrite 中,用的那個 for 循環的技巧。for (mask=0x80; mask!=0; mask>>=1),由於 I2C 通信是從高位開始發送數據,所以我們先從最高位開始,0x80 和 dat 進行按位與運算,從而得知 dat 第 7 位是 0 還是 1,然後右移一位,也就是變成了用 0x40 和 dat 按位與運算,得到第 6 位是 0 還是 1,一直到第 0 位結束,最終通過 if 語句,把 dat 的 8 位數據依次發送了出去。其它的邏輯大家對照前邊講到的理論知識,認真研究明白就可以了。

14.3 EEPROM 的學習

在實際的應用中,保存在單片機 RAM 中的數據,掉電後就丟失了,保存在單片機的FLASH 中的數據,又不能隨意改變,也就是不能用它來記錄變化的數值。但是在某些場合,我們又確實需要記錄下某些數據,而它們還時常需要改變或更新,掉電之後數據還不能丟失,比如我們的家用電錶度數,電視機裏邊的頻道記憶,一般都是使用 EEPROM 來保存數據,特點就是掉電後不丟失。我們板子上使用的這個器件是 24C02,是一個容量大小是 2Kbits,也就是 256 個字節的 EEPROM。一般情況下,EEPROM 擁有 30 萬到 100 萬次的壽命,也就是它可以反覆寫入 30-100 萬次,而讀取次數是無限的。

24C02 是一個基於 I2C 通信協議的器件,因此從現在開始,我們的 I2C 和我們的 EEPROM就要合體了。但是大家要分清楚,I2C 是一個通信協議,它擁有嚴密的通信時序邏輯要求,而 EEPROM 是一個器件,只是這個器件採樣了 I2C 協議的接口與單片機相連而已,二者並沒有必然的聯繫,EEPROM 可以用其它接口,I2C 也可以用在其它很多器件上。

14.3.1 EEPROM 單字節讀寫操作時序

1、EEPROM 寫數據流程

  • 第一步,首先是 I2C 的起始信號,接着跟上首字節,也就是我們前邊講的 I2C 的器件地址,並且在讀寫方向上選擇“寫”操作。
  • 第二步,發送數據的存儲地址。24C02 一共 256 個字節的存儲空間,地址從 0x00~0xFF,我們想把數據存儲在哪個位置,此刻寫的就是哪個地址。
  • 第三步,發送要存儲的數據第一個字節、第二個字節……注意在寫數據的過程中,EEPROM 每個字節都會迴應一個“應答位 0”,來告訴我們寫 EEPROM 數據成功,如果沒有迴應答位,說明寫入不成功。
    寫數據的過程中,每成功寫入一個字節,EEPROM 存儲空間的地址就會自動加 1,當加到 0xFF 後,再寫一個字節,地址會溢出又變成了 0x00。

2、EEPROM 讀數據流程

  • 第一步,首先是 I2C 的起始信號,接着跟上首字節,也就是我們前邊講的 I2C 的器件地址,並且在讀寫方向上選擇“寫”操作。這個地方可能有同學會詫異,我們明明是讀數據爲何方向也要選“寫”呢?剛纔說過了,24C02 一共有 256 個地址,我們選擇寫操作,是爲了把所要讀的數據的存儲地址先寫進去,告訴 EEPROM 我們要讀取哪個地址的數據。這就如同我們打電話,先撥總機號碼(EEPROM 器件地址),而後還要繼續撥分機號碼(數據地址),而撥分機號碼這個動作,主機仍然是發送方,方向依然是“寫”。
  • 第二步,發送要讀取的數據的地址,注意是地址而非存在 EEPROM 中的數據,通知EEPROM 我要哪個分機的信息。
  • 第三步,重新發送 I2C 起始信號和器件地址,並且在方向位選擇“讀”操作。
    這三步當中,每一個字節實際上都是在“寫”,所以每一個字節 EEPROM 都會迴應一個“應答位 0”。
  • 第四步,讀取從器件發回的數據,讀一個字節,如果還想繼續讀下一個字節,就發送一個“應答位 ACK(0)”,如果不想讀了,告訴 EEPROM,我不想要數據了,別再發數據了,那就發送一個“非應答位 NAK(1)”。

和寫操作規則一樣,我們每讀一個字節,地址會自動加 1,那如果我們想繼續往下讀,給 EEPROM 一個 ACK(0)低電平,那再繼續給 SCL 完整的時序,EEPROM 會繼續往外送數據。如果我們不想讀了,要告訴 EEPROM 不要數據了,那我們直接給一個 NAK(1)高電平即可。這個地方大家要從邏輯上理解透徹,不能簡單的靠死記硬背了,一定要理解明白。梳理一下幾個要點:

  1. 本例中單片機是主機,24C02 是從機;
  2. 無論是讀是寫,SCL 始終都是由主機控制的;
  3. 寫的時候應答信號由從機給出,表示從機是否正確接收了數據;D、讀的時候應答信號則由主機給出,表示是否繼續讀下去。

那我們下面寫一個程序,讀取 EEPROM 的 0x02 這個地址上的一個數據,不管這個數據之前是多少,我們都將讀出來的數據加 1,再寫到 EEPROM 的 0x02 這個地址上。此外我們將 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; //首先確保 SDA、SCL 都是高電平;
    			//思考:SCL=1;SDA=1可能發生什麼?(終止信號)
    I2C_SCL = 1; 
    I2CDelay(); 
    I2C_SDA = 0; //先拉低 SDA 
    I2CDelay(); 
    I2C_SCL = 0; //再拉低 SCL 
} 

//終止信號

/* 產生總線停止信號 */ 
void I2CStop() 
{ 
    I2C_SCL = 0; //首先確保 SDA、SCL 都是低電平 ;
    			
    I2C_SDA = 0; //思考:若 無上一行代碼,會發生什麼?(起始信號),
    			//因爲:原來SCL=1;SDA=1;就產生起始信號,當然還有其它可能
    			//這樣2行代碼將保證程序無歧義。
    I2CDelay(); 
    I2C_SCL = 1; //先拉高 SCL 
    I2CDelay(); 
    I2C_SDA = 1; //再拉高 SDA 
    I2CDelay(); 
} 
/* I2C 總線寫操作,dat-待寫入字節,返回值-從機應答位的值,0-無應答,1-應答 */ 
bit I2CWrite(unsigned char 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=存在且空閒或寫入成功 
} 

//非應答

/* I2C 總線讀操作,併發送非應答信號,返回值-讀到的字節 */ 
unsigned char I2CReadNAK() 
{ 
    unsigned char mask; 
    unsigned char dat; 
 
    I2C_SDA = 1; //首先確保主機釋放 SDA ,因爲,一旦SDA=0,將無法讀取從機數據。
    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; 
} 

//應答信號

/* I2C 總線讀操作,併發送應答信號,返回值-讀到的字節 */ 
unsigned char I2CReadACK() 
{ 
    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; 
} 

I2C.c 文件提供了 I2C 總線所有的底層操作函數,包括起始、停止、字節寫、字節讀+應答、字節讀+非應答。

/***************************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 爲止 
} 
/* 向 LCD1602 液晶寫入一字節命令,cmd-待寫入命令值 */ 
void LcdWriteCmd(unsigned char cmd) 
{ 
    LcdWaitReady(); 
    LCD1602_RS = 0; 
    LCD1602_RW = 0; 
    LCD1602_DB = cmd; 
    LCD1602_E  = 1; 
    LCD1602_E  = 0; 
} 
/* 向 LCD1602 液晶寫入一字節數據,dat-待寫入數據值 */ 
void LcdWriteDat(unsigned char dat) 
{ 
    LcdWaitReady(); 
    LCD1602_RS = 1; 
    LCD1602_RW = 0; 
    LCD1602_DB = dat; 
    LCD1602_E  = 1; 
    LCD1602_E  = 0; 
} 
/* 設置顯示 RAM 起始地址,亦即光標位置,(x,y)-對應屏幕上的字符座標 */ 
void LcdSetCursor(unsigned char x, unsigned char y) 
{ 
    unsigned char addr; 
     
    if (y == 0) //由輸入的屏幕座標計算顯示 RAM 的地址 
        addr = 0x00 + x; //第一行字符地址從 0x00 起始 
    else 
        addr = 0x40 + x; //第二行字符地址從 0x40 起始 
    LcdWriteCmd(addr | 0x80); //設置 RAM 地址 
} 
/* 在液晶上顯示字符串,(x,y)-對應屏幕上的起始座標,str-字符串指針 */ 
void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str) 
{ 
    LcdSetCursor(x, y);   //設置起始地址 
    while (*str != '\0')  //連續寫入字符串數據,直到檢測到結束符 
    {
        LcdWriteDat(*str++); 
    } 
} 
/* 初始化 1602 液晶 */ 
void InitLcd1602() 
{ 
    LcdWriteCmd(0x38); //16*2 顯示,5*7 點陣,8 位數據接口 
    LcdWriteCmd(0x0C);  //顯示器開,光標關閉 
    LcdWriteCmd(0x06);  //文字不動,地址自動+1 
    LcdWriteCmd(0x01);  //清屏 
} 
/*****************************main.c 文件程序源代碼******************************/ 
#include <reg52.h> 
 
extern void InitLcd1602(); 
extern void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str); 
extern void I2CStart(); 
extern void I2CStop(); 
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]; 
 
    InitLcd1602();   //初始化液晶 
    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); 
} 
 
/* 讀取 EEPROM 中的一個字節,addr-字節地址 */ 
unsigned char E2ReadByte(unsigned char addr) 
{ 
    unsigned char dat; 
     
    I2CStart(); 
    I2CWrite(0x50<<1); //尋址器件,後續爲寫操作 
    I2CWrite(addr);    //寫入存儲地址 
    I2CStart();         //發送重複啓動信號 
    I2CWrite((0x50<<1)|0x01); //尋址器件,後續爲讀操作 
    dat = I2CReadNAK();        //讀取一個字節數據 
    I2CStop(); 
     
    return dat; 
} 
/* 向 EEPROM 中寫入一個字節,addr-字節地址 */ 
void E2WriteByte(unsigned char addr, unsigned char dat) 
{ 
    I2CStart(); 
    I2CWrite(0x50<<1); //尋址器件,後續爲寫操作 
    I2CWrite(addr);    //寫入存儲地址 
    I2CWrite(dat);     //寫入一個字節數據 
    I2CStop(); 
} 

這個程序,以同學們現在的基礎,獨立分析應該不困難了,遇到哪個語句不懂可以及時問問別人或者搜索一下,把該解決的問題理解明白。大家把這個程序複製過去後,編譯一下會發現 Keil 軟件提示了一個警告:*** WARNING L16: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS,這個警告的意思是在代碼中存在沒有被調用過的變量或者函數,即 I2C.c 文件中的 I2CReadACK()這個函數在本例中沒有用到。

大家仔細觀察一下這個程序,我們讀取 EEPROM 的時候,只讀了一個字節就要告訴EEPROM 不需要再讀數據了,讀完後直接發送一個“NAK”,因此只調用了 I2CReadNAK()這個函數,而並沒有調用 I2CReadACK()這個函數。我們今後很可能讀數據的時候要連續讀幾個字節,因此這個函數寫在了 I2C.c 文件中,作爲 I2C 功能模塊的一部分是必要的,方便我們這個文件以後移植到其他程序中使用,因此這個警告在這裏就不必管它了。

14.3.2 EEPROM 多字節讀寫操作時序

我們讀取 EEPROM 的時候很簡單,EEPROM 根據我們所送的時序,直接就把數據送出來了,但是寫 EEPROM 卻沒有這麼簡單了。給 EEPROM 發送數據後,先保存在了 EEPROM的緩存,EEPROM 必須要把緩存中的數據搬移到“非易失”的區域,才能達到掉電不丟失的效果。而往非易失區域寫需要一定的時間,每種器件不完全一樣,ATMEL 公司的 24C02 的這個寫入時間最高不超過 5ms。在往非易失區域寫的過程,EEPROM 是不會再響應我們的訪問的,不僅接收不到我們的數據,我們即使用 I2C 標準的尋址模式去尋址,EEPROM 都不會應答,就如同這個總線上沒有這個器件一樣。數據寫入非易失區域完畢後,EEPROM 再次恢復正常,可以正常讀寫了。

細心的同學,在看上一節程序的時候會發現,我們寫數據的那段代碼,實際上我們有去讀應答位 ACK,但是讀到了應答位我們也沒有做任何處理。這是因爲我們一次只寫一個字節的數據進去,等到下次重新上電再寫的時候,時間肯定遠遠超過了 5ms,但是如果我們是連續寫入幾個字節的時候,就必須得考慮到應答位的問題了。寫入一個字節後,再寫入下一個字節之前,我們必須要等待 EEPROM 再次響應纔可以,大家注意我們程序的寫法,可以學習一下。

之前我們知道編寫多.c 文件移植的方便性了,本節程序和上一節的 Lcd1602.c 文件和I2C.c 文件完全是一樣的,因此這次我們只把 main.c 文件給大家發出來,幫大家分析明白。

而同學們卻不能這樣,同學們是初學,很多知識和技巧需要多練才能鞏固下來,因此每個程序還是建議大家在你的 Keil 軟件上一個代碼一個代碼的敲出來。

/*****************************I2C.c 文件程序源代碼*******************************/ 
(此處省略,可參考之前章節的代碼) 
/***************************Lcd1602.c 文件程序源代碼*****************************/ 
(此處省略,可參考之前章節的代碼) 
/*****************************main.c 文件程序源代碼******************************/ 
#include <reg52.h> 
 
extern void InitLcd1602(); 
extern void LcdShowStr(unsigned char x, unsigned char y, 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 MemToStr(unsigned char *str, unsigned char *src, unsigned char len); 
 
void main() 
{ 
    unsigned char i; 
    unsigned char buf[5]; 
    unsigned char str[20]; 
 
    InitLcd1602();   //初始化液晶 
    E2Read(buf, 0x90, sizeof(buf)); //從 E2 中讀取一段數據 
    MemToStr(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); 
} 
/* 將一段內存數據轉換爲十六進制格式的字符串, 
   str-字符串指針,src-源數據地址,len-數據長度 */ 
void MemToStr(unsigned char *str, unsigned char *src, unsigned char len) 
{ 
    unsigned char tmp; 
 
    while (len--) 
    { 
        tmp = *src >> 4; //先取高 4 位 
        if (tmp <= 9) //轉換爲 0-9 或 A-F 
            *str++ = tmp + '0'; 
        else 
            *str++ = tmp - 10 + 'A'; 
        tmp = *src & 0x0F; //再取低 4 位 
        if (tmp <= 9) //轉換爲 0-9 或 A-F 
            *str++ = tmp + '0'; 
        else 
            *str++ = tmp - 10 + 'A'; 
        *str++ = ' ';              //轉換完一個字節添加一個空格 
        src++; 
    } 
} 
/* E2 讀取函數,buf-數據接收指針,addr-E2 中的起始地址,len-讀取長度 */ 
void E2Read(unsigned char *buf, unsigned char addr, unsigned char 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(); //最後字節之前爲讀取操作+應答 
        len--; 
    } 
    *buf = I2CReadNAK();       //最後一個字節爲讀取操作+非應答 
    I2CStop(); 
} 
/* E2 寫入函數,buf-源數據指針,addr-E2 中的起始地址,len-寫入長度 */ 
void E2Write(unsigned char *buf, unsigned char addr, unsigned char len) 
{ 
    while (len--) 
    { 
        do {                           //用尋址操作查詢當前是否可進行讀寫操作 
            I2CStart(); 
            if (I2CWrite(0x50<<1)) //應答則跳出循環,非應答則進行下一次查詢 
            { 
                break; 
            } 
            I2CStop(); 
        } while(1); 
        I2CWrite(addr++);  //寫入起始地址 
        I2CWrite(*buf++);  //寫入一個字節數據 
        I2CStop();          //結束寫操作,以等待寫入完成 
    } 
} 

函數 MemToStr:可以把一段內存數據轉換成十六進制字符串的形式。由於我們從EEPROM 讀出來的是正常的數據,而 1602 液晶接收的是 ASCII 碼字符,因此我們要通過液晶把數據顯示出來必須先通過一步轉換。算法倒是很簡單,就是把每一個字節的數據高 4 位和低 4 位分開,和 9 進行比較,如果小於等於 9,則直接加‘0’轉爲 0~9 的 ASCII 碼;如果大於 9,則先減掉 10 再加‘A’即可轉爲 A~F 的 ASCII 碼。

函數 E2Read:我們在讀之前,要查詢一下當前是否可以進行讀寫操作,EEPROM 正常響應纔可以進行。進行後,讀最後一個字節之前的,全部給出 ACK,而讀完了最後一個字節,我們要給出一個 NAK。

函數 E2Write:每次寫操作之前,我們都要進行查詢判斷當前 EEPROM 是否響應,正常響應後纔可以寫數據。

14.3.3 EEPROM 的頁寫入

在向 EEPROM 連續寫入多個字節的數據時,如果每寫一個字節都要等待幾 ms 的話,整體上的寫入效率就太低了。因此 EEPROM 的廠商就想了一個辦法,把 EEPROM 分頁管理。

24C01、24C02 這兩個型號是 8 個字節一個頁,而 24C04、24C08、24C16 是 16 個字節一頁。我們開發板上用的型號是 24C02,一共是 256 個字節,8 個字節一頁,那麼就一共有 32 頁。分配好頁之後,如果我們在同一個頁內連續寫入幾個字節後,最後再發送停止位的時序。

EEPROM 檢測到這個停止位後,就會一次性把這一頁的數據寫到非易失區域,就不需要像上節課那樣寫一個字節檢測一次了,並且頁寫入的時間也不會超過 5ms。如果我們寫入的數據跨頁了,那麼寫完了一頁之後,我們要發送一個停止位,然後等待並且檢測 EEPROM 的空閒模式,一直等到把上一頁數據完全寫到非易失區域後,再進行下一頁的寫入,這樣就可以在很大程度上提高數據的寫入效率。

/*****************************I2C.c 文件程序源代碼*******************************/ 
(此處省略,可參考之前章節的代碼) 
/***************************Lcd1602.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); 
 
/* E2 讀取函數,buf-數據接收指針,addr-E2 中的起始地址,len-讀取長度 */ 
void E2Read(unsigned char *buf, unsigned char addr, unsigned char 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(); //最後字節之前爲讀取操作+應答 
        len--; 
    } 
    *buf = I2CReadNAK();       //最後一個字節爲讀取操作+非應答 
    I2CStop(); 
} 
/* E2 寫入函數,buf-源數據指針,addr-E2 中的起始地址,len-寫入長度 */ 
void E2Write(unsigned char *buf, unsigned char addr, unsigned char len) 
{ 
    while (len > 0) 
    { 
        //等待上次寫入操作完成 
        do {                           //用尋址操作查詢當前是否可進行讀寫操作 
            I2CStart(); 
            if (I2CWrite(0x50<<1)) //應答則跳出循環,非應答則進行下一次查詢 
            { 
                break; 
            } 
            I2CStop(); 
        } while(1); 
        //按頁寫模式連續寫入字節 
        I2CWrite(addr);            //寫入起始地址 
        while (len > 0) 
        { 
            I2CWrite(*buf++);      //寫入一個字節數據 
            len--;                   //待寫入長度計數遞減 
            addr++; //E2 地址遞增 
            if ((addr&0x07) == 0) //檢查地址是否到達頁邊界,24C02 每頁 8 字節, 
            { //所以檢測低 3 位是否爲零即可 
                break;               //到達頁邊界時,跳出循環,結束本次寫操作 
            } 
        } 
        I2CStop(); 
    } 
} 

遵循模塊化的原則,我們把 EEPROM 的讀寫函數也單獨寫成一個 eeprom.c 文件。其中E2Read 函數和上一節是一樣的,因爲讀操作與分頁無關。重點是 E2Write 函數,我們在寫入數據的時候,要計算下一個要寫的數據的地址是否是一個頁的起始地址,如果是的話,則必須跳出循環,等待 EEPROM 把當前這一頁寫入到非易失區域後,再進行後續頁的寫入。

/*****************************main.c 文件程序源代碼******************************/ 
#include <reg52.h> 
 
extern void InitLcd1602(); 
extern void LcdShowStr(unsigned char x, unsigned char y, 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 MemToStr(unsigned char *str, unsigned char *src, unsigned char len); 
 
void main() 
{ 
    unsigned char i; 
    unsigned char buf[5]; 
    unsigned char str[20]; 
 
    InitLcd1602();   //初始化液晶 
    E2Read(buf, 0x8E, sizeof(buf)); //從 E2 中讀取一段數據 
    MemToStr(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); 
} 
/* 將一段內存數據轉換爲十六進制格式的字符串, 
   str-字符串指針,src-源數據地址,len-數據長度 */ 
void MemToStr(unsigned char *str, unsigned char *src, unsigned char len) 
{ 
    unsigned char tmp; 
 
    while (len--) 
    { 
        tmp = *src >> 4; //先取高 4 位 
        if (tmp <= 9) //轉換爲 0-9 或 A-F 
            *str++ = tmp + '0'; 
        else 
            *str++ = tmp - 10 + 'A'; 
        tmp = *src & 0x0F; //再取低 4 位 
        if (tmp <= 9) //轉換爲 0-9 或 A-F 
            *str++ = tmp + '0'; 
        else 
            *str++ = tmp - 10 + 'A'; 
        *str++ = ' ';              //轉換完一個字節添加一個空格 
        src++; 
    } 
} 

多字節寫入和頁寫入程序都編寫出來了,而且頁寫入的程序我們還特地跨頁寫的數據,它們的寫入時間到底差別多大呢。我們用一些工具可以測量一下,比如示波器,邏輯分析儀等工具。我現在把兩次寫入時間用邏輯分析儀給抓了出來,並且用時間標籤 T1 和 T2 標註了開始位置和結束位置,如圖 14-5 和圖 14-6 所示,右側顯示的|T1-T2|就是最終寫入 5 個字節所耗費的時間。多字節一個一個寫入,每次寫入後都需要再次通信檢測EEPROM 是否在“忙”,因此耗費了大量的時間,同樣的寫入 5 個字節的數據,一個一個寫入用了 8.4ms 左右的時間,而使用頁寫入,只用了 3.5ms 左右的時間。
在這裏插入圖片描述
圖 14-5 多字節寫入時間

在這裏插入圖片描述

圖 14-6 跨頁寫入時間

14.4 I2C 和 EEPROM 的綜合實驗學習

電視頻道記憶功能,交通燈倒計時時間的設定,戶外 LED 廣告的記憶功能,都有可能用到 EEPROM 這類存儲器件。這類器件的優勢是存儲的數據不僅可以改變,而且掉電後數據保存不丟失,因此大量應用在各種電子產品上。

我們這節課的例程,有點類似廣告屏。上電後,1602 的第一行顯示 EEPROM 從 0x20 地址開始的 16 個字符,第二行顯示 EERPOM 從 0x40 開始的 16 個字符。我們可以通過 UART串口通信來改變 EEPROM 內部的這個數據,並且同時也改變了 1602 顯示的內容,下次上電的時候,直接會顯示我們更新過的內容。

這個程序所有的相關內容,前面都已經講過了。但是這個程序體現在了一個綜合應用能力上。這個程序用到了 1602 液晶、UART 串口通信、EEPROM 讀寫操作等多個功能的綜合應用。寫個點亮小燈好簡單,但是我們想真正學好單片機,必須得學會這種綜合程序的應用,實現多個模塊同時參與工作。因此同學們,要認認真真的把工程建立起來,一行一行的把程序編寫起來,最終鞏固下來。

/*****************************I2C.c 文件程序源代碼*******************************/ 
(此處省略,可參考之前章節的代碼) 
/***************************Lcd1602.c 文件程序源代碼*****************************/ 
(此處省略,可參考之前章節的代碼) 
/****************************eeprom.c 文件程序源代碼*****************************/ 
(此處省略,可參考之前章節的代碼) 
/*****************************Uart.c 文件程序源代碼*****************************/ 
(此處省略,可參考之前章節的代碼) 
/*****************************main.c 文件程序源代碼******************************/ 
#include <reg52.h> 
unsigned char T0RH = 0; //T0 重載值的高字節 
unsigned char T0RL = 0; //T0 重載值的低字節 
 
void InitShowStr(); 
void ConfigTimer0(unsigned int ms); 
extern void InitLcd1602(); 
extern void LcdShowStr(unsigned char x, unsigned char y, 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); 
extern void UartDriver(); 
extern void ConfigUART(unsigned int baud); 
extern void UartRxMonitor(unsigned char ms); 
extern void UartWrite(unsigned char *buf, unsigned char len); 
 
void main() 
{ 
    EA = 1;              //開總中斷 
    ConfigTimer0(1); //配置 T0 定時 1ms 
    ConfigUART(9600); //配置波特率爲 9600 
    InitLcd1602();     //初始化液晶 
    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);  //顯示到液晶屏 
} 
/* 內存比較函數,比較兩個指針所指向的內存數據是否相同, 
 ptr1-待比較指針 1,ptr2-待比較指針 2,len-待比較長度 
 返回值-兩段內存數據完全相同時返回 1,不同返回 0 */ 
bit CmpMemory(unsigned char *ptr1, unsigned char *ptr2, unsigned char len) 
{ 
    while (len--) 
    { 
        if (*ptr1++ != *ptr2++) //遇到不相等數據時即刻返回 0 
        { 
            return 0; 
        } 
    } 
 return 1; //比較完全部長度數據都相等則返回 1 
} 
/* 將一字符串整理成 16 字節的固定長度字符串,不足部分補空格 
   out-整理後的字符串輸出指針,in-待整理字符串指針 */ 
void TrimString16(unsigned char *out, unsigned char *in) 
{ 
    unsigned char i = 0; 
     
    while (*in != '\0')  //拷貝字符串直到輸入字符串結束 
    { 
        *out++ = *in++; 
        i++; 
        if (i >= 16) //當拷貝長度已達到 16 字節時,強制跳出循環 
        { 
            break; 
        } 
    } 
    for ( ; i<16; i++) //如不足 16 個字節則用空格補齊 
    { 
        *out++ = ' '; 
    } 
    *out = '\0';        //最後添加結束符 
} 
/* 串口動作函數,根據接收到的命令幀執行響應的動作 
   buf-接收到的命令幀指針,len-命令幀長度 */ 
void UartAction(unsigned char *buf, unsigned char len) 
{ 
    unsigned char i; 
    unsigned char str[17]; 
    unsigned char code cmd0[] = "showstr1 ";  //第一行字符顯示命令 
    unsigned char code cmd1[] = "showstr2 ";  //第二行字符顯示命令 
    unsigned char code cmdLen[] = {            //命令長度彙總表 
        sizeof(cmd0)-1, sizeof(cmd1)-1, 
    }; 
    unsigned char code *cmdPtr[] = {           //命令指針彙總表 
        &cmd0[0],  &cmd1[0], 
    }; 
    for (i=0; i<sizeof(cmdLen); i++)  //遍歷命令列表,查找相同命令 
    { 
        if (len >= cmdLen[i])  //首先接收到的數據長度要不小於命令長度 
        { 
            if (CmpMemory(buf, cmdPtr[i], cmdLen[i]))  //比較相同時退出循環 
            { 
                break; 
            } 
        } 
    } 
    switch (i) //根據比較結果執行相應命令 
    { 
        case 0: 
            buf[len] = '\0';                      //爲接收到的字符串添加結束符 
            TrimString16(str, buf+cmdLen[0]); //整理成 16 字節固定長度字符串 
            LcdShowStr(0, 0, str); //顯示字符串 1 
            E2Write(str, 0x20, sizeof(str)); //保存字符串 1,起始地址爲 0x20 
            break; 
        case 1: 
            buf[len] = '\0';                      //爲接收到的字符串添加結束符 
            TrimString16(str, buf+cmdLen[1]); //整理成 16 字節固定長度字符串 
            LcdShowStr(0, 1, str); //顯示字符串 1 
            E2Write(str, 0x40, sizeof(str)); //保存字符串 2,起始地址爲 0x40 
            break; 
        default:   //未找到相符命令時,給上機發送“錯誤命令”的提示 
            UartWrite("bad command.\r\n", sizeof("bad command.\r\n")-1); 
            return; 
    } 
    buf[len++] = '\r';  //有效命令被執行後,在原命令幀之後添加 
    buf[len++] = '\n';  //回車換行符後返回給上位機,表示已執行 
    UartWrite(buf, len); 
} 
/* 配置並啓動 T0,ms-T0 定時時間 */ 
void ConfigTimer0(unsigned int ms) 
{ 
    unsigned long tmp;  //臨時變量 
     
    tmp = 11059200 / 12;       //定時器計數頻率 
    tmp = (tmp * ms) / 1000;  //計算所需的計數值 
    tmp = 65536 - tmp;         //計算定時器重載值 
    tmp = tmp + 33;            //補償中斷響應延時造成的誤差 
    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 
} 
/* T0 中斷服務函數,執行串口接收監控和蜂鳴器驅動 */ 
void InterruptTimer0() interrupt 1 
{ 
    TH0 = T0RH;  //重新加載重載值 
    TL0 = T0RL; 
    UartRxMonitor(1);  //串口接收監控 
} 

我們在學習 UART 通信的時候,剛開始也是用的 IO 口去模擬 UART 通信過程,最終實現和電腦的通信,而後因爲 STC89C52 內部具備 UART 硬件通信模塊,所以我們直接可以通過配置寄存器就可以很輕鬆的實現單片機的 UART 通信。同樣的道理,這個 I2C 通信,如果單片機內部有硬件模塊的話,單片機可以直接自動實現 I2C 通信了,就不需要我們再進行 IO口模擬起始、模擬發送、模擬結束,配置好寄存器,單片機就會把這些工作全部做了。

不過我們的 STC89C52 單片機內部不具備 I2C 的硬件模塊,所以我們使用 STC89C52 進行 I2C 通信的話必須用 IO 口來模擬。使用 IO 口模擬 I2C,實際上更有利於我們徹底理解透徹 I2C 通信的實質。當然了,通過學習 IO 口模擬通信,今後如果遇到內部帶 I2C 模塊的單片機,也應該很輕鬆的搞定,使用內部的硬件模塊,可以提高程序的執行效率。

14.5 練習題

  1. 徹底理解 I2C 的通信時序,不僅僅是記住。
  2. 能夠獨立完成 EEPROM 任意地址的單字節讀寫、多字節的跨頁連續寫入讀出。
  3. 將前邊學的交通燈進行改進,使用 EEPROM 保存紅燈和綠燈倒計時的時間,並且可以通過 UART 改變紅燈和綠燈倒計時時間。
  4. 使用按鍵、1602 液晶、EEPROM 做一個簡單的密碼鎖程序。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章