標題: 第14章 I2C總線與EEPROM [打印本頁]
作者: admin 時間: 2013-9-28 15:25
標題: 第14章 I2C總線與EEPROM
本教材現以連載的方式由網絡發布,并將于2014年由清華大學出版社出版最終完整版,版權歸作者和清華大學出版社所有。本著開源、分享的理念,本教材可以自由傳播及學習使用,但是務必請注明出處來自金沙灘工作室
前幾章我們學了一種通信協議叫做UART異步串口通信,這節課我們要來學習第二種常用的通信協議I2C。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兩個上拉電阻。
1.JPG (7.68 KB, 下載次數: 242)
下載附件
2013-9-28 15:13 上傳
圖14-1 I2C總線的上拉電阻
雖然說任何一個設備都可以作為主機,但絕大多數情況下我們都是用微處理器,也就是我們的單片機來做主機,而總線上掛的多個器件,每一個都像電話機一樣有自己唯一的地址,在信息傳輸的過程中,通過這唯一的地址可以正常識別到屬于自己的信息,在我們的KST-51開發板上,就掛接了2個I2C設備,一個是24C02,一個是PCF8591。
我們在學習UART串行通信的時候,知道了我們的通信流程分為起始位、數據位、停止位這三部分,同理在I2C中也有起始信號、數據傳輸和停止信號,如圖14-2所示。
2.JPG (24.46 KB, 下載次數: 237)
下載附件
2013-9-28 15:13 上傳
圖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。
3.JPG (28.18 KB, 下載次數: 225)
下載附件
2013-9-28 15:13 上傳
圖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‘0’,如果不存在,就沒“人”回應ACK。
那我們寫一個簡單的程序,訪問一下我們板子上的EEPROM的地址,另外在寫一個不存在的地址,看看他們是否能回一個ACK,來了解和確認一下這個問題。
我們板子上的EEPROM器件型號是24C02,在24C02的數據手冊3.6部分說明了,24C02的7位地址中,其中高4位是固定的1010,而低3位的地址取決于我們電路的設計,由芯片上的A2、A1、A0這3個引腳的實際電平決定,來看一下我們的24C02的電路圖,如圖14-4所示。
4.JPG (20.29 KB, 下載次數: 246)
下載附件
2013-9-28 15:13 上傳
圖14-4 24C02原理圖
從圖14-4可以看出來,我們的A2、A1、A0都是接的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; //首先確保SDA、SCL都是高電平
I2C_SCL = 1;
I2CDelay();
I2C_SDA = 0; //先拉低SDA
I2CDelay();
I2C_SCL = 0; //再拉低SCL
}
void I2CStop() //產生總線停止信號
{
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總線寫操作,待寫入字節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()這個總線延時函數(它實際上就是4個NOP指令,用define在文件開頭做了定義),加上改變SCL值語句本身占用的至少一個周期,來達到這個速度限制。如果以后需要提高速度,那么只需要減小這里的總線延時時間即可。
此外我們要學習一個發送數據的技巧,就是I2C通信時如何將一個字節的數據發送出去。大家注意寫函數中,我用的那個for循環的技巧。for (mask=0x80; mask!=0; mask>>=1),由于I2C通信是從高位開始發送數據,所以我們先從最高位開始,0x80和dat進行按位與運算,從而得知dat第7位是0還是1,然后右移一位,也就是變成了用0x40和dat按位與運算,得到第6位是0還是1,一直到第0位結束,最終通過if語句,把dat的8位數據依次發送了出去。其他的邏輯大家對照前邊講到的理論知識,認真研究明白就可以了。
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個字節的存儲空間,地址從0x00到0xFF,我們想把數據存儲在哪個位置,此刻寫的就是哪個地址。
第三步,發送要存儲的數據第一個字節,第二個字節......注意在寫數據的過程中,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、讀的時候應答信號則由主機給出,表示是否繼續讀下去。
那我們下面寫一個程序,讀取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都是高電平
I2C_SCL = 1;
I2CDelay();
I2C_SDA = 0; //先拉低SDA
I2CDelay();
I2C_SCL = 0; //再拉低SCL
}
void I2CStop() //產生總線停止信號
{
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總線寫操作,待寫入字節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-9或A-F
*str = tmp + '0';
else
*str = tmp - 10 + 'A';
str++;
tmp = *array & 0x0F; //再取低4位
if (tmp <= 9) //轉換為0-9或A-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讀取函數,數據接收指針buf,E2中的起始地址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寫入函數,源數據指針buf,E2中的起始地址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分頁管理。24c01、24c02這兩個型號是8個字節一個頁,而24c04、24c08、24c16是16個字節一頁。我們板子上的型號是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讀取函數,數據接收指針buf,E2中的起始地址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寫入函數,源數據指針buf,E2中的起始地址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-9或A-F
*str = tmp + '0';
else
*str = tmp - 10 + 'A';
str++;
tmp = *array & 0x0F; //再取低4位
if (tmp <= 9) //轉換為0-9或A-F
*str = tmp + '0';
else
*str = tmp - 10 + 'A';
str++;
*str = ' '; //轉換完一個字節添加一個空格
str++;
array++;
}
}
多字節寫入和頁寫入程序都編寫出來了,而且頁寫入的程序我們還特地跨頁寫的數據,他們的寫入時間到底差別多大呢。我們用一些工具可以測量一下,比如示波器,邏輯分析儀等工具。我現在把兩次寫入時間用邏輯分析儀給抓了出來,并且用時間標簽T1和T2給標注了開始位置和結束位置,如圖14-5和圖14-6所示,右側顯示的|T1-T2|就是最終寫入5個字節所耗費的時間。多字節一個一個寫入,每次寫入后都需要再次通信檢測EEPROM是否在“忙”,因此耗費了大量的時間,同樣的寫入5個字節的數據,一個一個寫入用了8.4ms左右的時間,而使用頁寫入,只用了3.5ms左右的時間。
1.JPG (50.41 KB, 下載次數: 244)
下載附件
2013-9-28 15:15 上傳
圖14-5 多字節寫入時間
2.JPG (56.09 KB, 下載次數: 253)
下載附件
2013-9-28 15:15 上傳
圖14-6 跨頁寫入時間
14.4 I2C和EEPROM的綜合實驗學習[size=14.0000pt]電視頻道記憶功能,交通燈倒計時時間的設定,戶外LED廣告的記憶功能,都有可能有類似EEPROM這類存儲器件。這類器件的優勢是存儲的數據不僅可以改變,而且掉電后數據保存不丟失,因此大量應用在各種電子產品上。
我們這節課的例程,有點類似廣告屏。上電后,1602的第一行顯示EEPROM從0x20地址開始的16個字符,第二行顯示EERPOM從0x40開始的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做一個簡單的密碼鎖程序。
作者: lzzgg834483370 時間: 2014-9-18 15:36
我還是可以補充一點信息的
-
1.jpg
(69.94 KB, 下載次數: 331)
下載附件
2014-9-18 15:36 上傳
EEPROM
-
2.jpg
(71.03 KB, 下載次數: 320)
下載附件
2014-9-18 15:36 上傳
-
3.jpg
(66.59 KB, 下載次數: 352)
下載附件
2014-9-18 15:36 上傳
-
4.jpg
(81.37 KB, 下載次數: 343)
下載附件
2014-9-18 15:36 上傳
作者: shangxianyue 時間: 2015-8-13 17:47
樓主,,帖子里的代碼粘到 keil4里面后會成為亂碼呢,,這個有辦法解決嗎,謝謝!!
常看您的帖子,受益匪淺,謝謝!
作者: 疏影橫斜水清淺 時間: 2016-3-21 22:52
老師,最近都在看你的帖子,很受教,謝謝
作者: 單片機~+ 時間: 2016-6-1 23:51
贊!!!!
作者: zzzziqzh 時間: 2017-2-6 15:11
老師,最近都在看你的帖子,很受教,謝謝!
作者: tthxq251 時間: 2018-1-2 08:56
老師,最近都在看你的帖子,很受教,謝謝!
作者: Steven159 時間: 2018-1-25 15:41
剛剛好真是想睡覺就有人送抱枕啊,最近最新找的就是SPI通訊和IIC通訊教程了,沒想到剛注冊51黑電子論壇,就發現了這么多寶貴資源,感謝樓主的寶貴分享,剛好幫助我理解IIC通訊
作者: gavin1985bb 時間: 2018-3-5 12:32
講的真心不錯工,學習了!
作者: Jia_hu 時間: 2018-4-7 21:04
受益匪淺.
作者: liangyutong 時間: 2018-5-2 14:43
還是我們學識淺了
作者: keneng 時間: 2018-7-17 16:21
越來越難了
作者: a89588038 時間: 2018-8-6 15:43
看到后面越來越難理解了


作者: 向日葵男人 時間: 2018-8-19 01:17
學習了!!!
作者: xzf586 時間: 2018-8-20 23:23
仔細學習了一下,獲益匪淺,講的很透徹!!!
作者: xzf586 時間: 2018-8-21 09:53
版主原來是宋老師,久聞大名,怎么這么近呢?
,現在仔細地將這本書啃一啃!
-
微信圖片_20180821094640.jpg
(136.74 KB, 下載次數: 148)
下載附件
2018-8-21 09:51 上傳
作者: yasi666 時間: 2019-1-23 20:06
絕世好貼,感謝分享!
作者: mawuxi 時間: 2020-3-24 20:18
絕對給力
作者: OHHO 時間: 2020-6-10 00:44
謝謝,受益匪淺!
作者: hhh123hhh123 時間: 2021-9-10 11:38
太偉大了,等我這么吊的時候也要把經驗分享出來。
歡迎光臨 (http://www.zg4o1577.cn/bbs/) |
Powered by Discuz! X3.1 |
主站蜘蛛池模板:
欧美成年黄网站色视频
|
国产羞羞视频在线观看
|
精品视频一区二区三区
|
精品三区
|
精品视频在线免费观看
|
中文久久|
欧美日韩一二三区
|
成人av在线播放
|
国产视频亚洲视频
|
国产成人自拍一区
|
精品国产乱码久久久久久丨区2区
|
国产精品美女久久久久久免费
|
99在线免费视频
|
国产美女特级嫩嫩嫩bbb片
|
超碰av人人
|
亚洲精品九九
|
久久成人免费视频
|
日韩免费av一区二区
|
在线观看国产www
|
精品欧美乱码久久久久久
|
日一区二区
|
男女啪啪高潮无遮挡免费动态
|
一区二区三区在线观看免费视频
|
午夜视频在线免费观看
|
国产三级大片
|
网站黄色在线
|
正在播放国产精品
|
日韩欧美三区
|
国产精品久久久久久久7电影
|
亚洲久久一区
|
日韩成人中文字幕
|
亚洲国产aⅴ成人精品无吗
欧美激情欧美激情在线五月
|
欧美日韩免费视频
|
成人免费视频7777777
|
国产精品黄视频
|
美国av毛片
|
久久精品久久久久久
|
中文字幕精品一区
|
91一区二区三区在线观看
|
av网站免费
|
久久久久久久久91
|