|
12.3 EEPROM的學習 在實際應用中,保存在單片機RAM中的數據,掉電后就丟失了,使用code關鍵字保存在單片機的FLASH中的數據,又不能隨意改變,也就是不能用它來記錄變化的數值。但是在某些場合,又確實需要記錄下某些數據,而它們還時常需要改變或更新,掉電之后數據還不能丟失,比如家用電表度數,電視機里邊的頻道記憶,一般都是使用EEPROM來保存數據,特點就是掉電后不丟失。Kingst51開發板上使用的這個器件是24C02,是一個容量大小是2Kbits,也就是256個字節的EEPROM。一般情況下,EEPROM擁有30萬到100萬次的壽命,也就是它可以反復寫入30-100萬次,而讀取次數是無限的。 24C02是一個基于I2C通信協議的器件,但要分清楚,I2C是一個通信協議,它擁有嚴密的通信時序邏輯要求,而EEPROM是一個器件,只是這個器件采用了I2C協議的接口與單片機相連而已,二者并沒有必然的聯系,EEPROM可以用其它接口,I2C也可以用在其它很多器件上。 12.3.1 EEPROM單字節讀寫操作時序1、EEPROM寫數據流程 (1)首先是I2C的起始信號,接著跟上首字節,也就是I2C的器件地址,并且在讀寫方向上選擇“寫”操作。 (2)發送數據的存儲地址。24C02一共256個字節的存儲空間,地址從0x00~0xFF,想把數據存儲在哪個位置,此刻寫的就是哪個地址。 (3)發送要存儲的數據第一個字節、第二個字節……注意在寫數據的過程中,EEPROM每個字節都會回應一個“應答位0”,來通知用戶寫EEPROM數據成功,如果沒有回應答位,說明寫入不成功。 在寫數據的過程中,每成功寫入一個字節,EEPROM存儲空間的地址就會自動加1,當加到0xFF后,再寫一個字節,地址會溢出又變成了0x00。 2、EEPROM讀數據流程 (1)首先是I2C的起始信號,接著跟上首字節,也就是I2C的器件地址,并且在讀寫方向上選擇“寫”操作。明明是讀數據為何方向也要選“寫”呢?24C02一共有256個地址,選擇寫操作,是為了把所要讀的數據的存儲地址先寫進去,告訴EEPROM要讀取哪個地址的數據。這就如同打電話,先撥總機號碼(EEPROM器件地址),而后還要繼續撥分機號碼(數據地址),而撥分機號碼這個動作,主機仍然是發送方,方向依然是“寫”。 (2)發送要讀取的數據的地址,注意是地址而非存在EEPROM中的數據,通知EEPROM要哪個分機的信息。 (3)重新發送I2C起始信號和器件地址,并且在方向位選擇“讀”操作。 這3步當中,每一個字節實際上都是在“寫”,所以每一個字節EEPROM都會回應一個“應答位0”。 (4)讀取從器件發回的數據,讀一個字節后,如果還想繼續讀下一個字節,就發送一個“應答位ACK(0)”,如果不想讀了,通知EEPROM不想要數據了,那就發送一個“非應答位NAK(1)”。 和寫操作規則一樣,每讀一個字節地址會自動加1。如果想繼續往下讀,給EEPROM一個ACK(0)低電平后,再繼續給SCL完整的時序,EEPROM會繼續往外送數據。如果不想讀了,直接給一個NAK(1)高電平。 梳理一下幾個要點: 1、在本例中單片機是主機,24C02是從機; 2、無論是讀是寫,SCL始終都是由主機控制的; 3、寫的時候應答信號由從機給出,表示從機是否正確接收了數據; 4、讀的時候應答信號則由主機給出,表示是否繼續讀下去。 下面寫一個程序,讀取EEPROM的0x02這個地址上的一個數據,不管這個數據之前是多少都將讀出來的數據加1,再寫到EEPROM的0x02這個地址上。此外將I2C的程序建立一個文件,寫一個I2C.c程序文件,形成又一個程序模塊。 /*****************************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(); } /* 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,以檢測從機應答 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 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; } /****************************main.c文件程序源代碼*****************************/ #include <reg52.h> 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; dat = E2ReadByte(0x02); //讀取指定地址上的一個字節 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(); } I2C.c文件提供了I2C總線底層函數,包括起始、停止、字節寫、字節讀+應答、字節讀+非應答。將這個程序復編譯會發現Keil軟件提示一個警告:*** WARNING L16: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS,這個警告的意思是在代碼中存在沒有被調用過的變量或者函數,I2C.c文件中的I2CReadACK()這個函數在本例中沒有用到。 讀取EEPROM的時候,由于只讀了一個字節就要告訴EEPROM不需要再讀數據了,讀完后直接發送一個“NAK”,因此只調用了I2CReadNAK()這個函數,而并沒有調用I2CReadACK()這個函數。今后很可能讀數據的時候要連續讀幾個字節,因此這個函數寫在了I2C.c文件中,作為I2C功能模塊的一部分是必要的,方便這個文件以后移植到其他程序中使用,因此這個警告在這里就不必管它了。 將這個程序中,I2C的讀寫EEPROM操作用邏輯分析儀抓出來,并且用I2C-EEPROM協議解析出來,如圖12-7所示。
12-7.png (10.61 KB, 下載次數: 0)
下載附件
2026-4-29 11:45 上傳
圖12-7 I2C-EEPROM解析結果圖 從圖12-7能看出,第一個字節是器件地址0x50+ACK,第二個字節是數據地址0x02+ACK,第三個字節是器件地址0x50+ACK,第四個是讀取到了0x04+NAK數據,第五個字節是器件地址0x50+ACK,第6個字節是數據地址0x02+ACK,第七個字節是寫入數據0x05+ACK。 12.3.2 EEPROM多字節讀寫操作時序讀取EEPROM的時候很簡單,EEPROM根據主機的時序,直接就把數據送出來了,但是寫EEPROM卻沒有這么簡單了。給EEPROM發送數據后,先保存在了EEPROM的緩存,EEPROM必須要把緩存中的數據搬移到“非易失”的區域,才能達到掉電不丟失的效果。而往非易失區域寫需要一定的時間,每種器件不完全一樣,ATMEL公司的24C02的這個寫入時間最高不超過5ms。在往非易失區域寫的過程,EEPROM是不會再響應訪問的,不僅接收不到數據,即使用I2C標準的尋址模式去尋址,EEPROM都不會應答,就如同這個總線上沒有這個器件一樣。數據寫入非易失區域完畢后,EEPROM再次恢復正常。 12.2節程序中寫數據的代碼,程序上有讀取應答ACK,但是讀取完畢后沒有做任何處理。這是因為一次只寫一個字節的數據進去,等到下次重新再寫的時候,時間肯定遠遠超過了5ms,但是如果是連續寫入幾個字節的時候,就必須得考慮到應答位的問題了。寫入一個字節后,再寫入下一個字節之前,必須要等待EEPROM再次響應才可以。 先從EEPROM的0x90這個地址連續讀出4個字節,然后把這4個數據分別加1,加2,加3, 加4后重新寫入到這四個地址中去。I2C.c文件和之前是完全一樣的,因此只把main.c文件給發出來。 /****************************I2C.c文件程序源代碼******************************/ (此處省略,可參考之前章節的代碼) /****************************main.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); void E2Write(unsigned char *buf, unsigned char addr, unsigned char len); void main() { unsigned char i; unsigned char buf[5]; E2Read(buf, 0x90, sizeof(buf)); //從E2中讀取一段數據 for (i=0; i<sizeof(buf); i++) //數據依次+1,+2,+3... { buf = buf + 1 + i; } E2Write(buf, 0x90, sizeof(buf)); //再寫回到E2中 while(1); } /* 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(); //結束寫操作,以等待寫入完成 } } 函數E2Read:讀數據前,要查詢當前是否允許讀寫操作,EEPROM正常響應才表示允許。讀最后一個字節之前的,全部給ACK,而讀完最后一個字節,要給出一個NAK。 函數E2Write:寫操作前,要查詢當前EEPROM是否響應,正常響應后才可以寫數據。 將I2C多字節讀寫EEPROM的時序部分用邏輯分析儀抓取,由于此次的讀寫數據量特別大,因此用邏輯分析儀抓取后,直接將解析后的數據導出到excel表格中,如圖12-8所示。
12-8.png (15.89 KB, 下載次數: 0)
下載附件
2026-4-29 11:45 上傳
圖12-8 連續讀寫解析后數據示意圖 從圖12-8表格看出,第一行為讀到的4個字節的數據,下面只有紅框內為寫入EEPROM的數據,而紅框外的為檢測0x50是否響應。由于EEPROM正在將前次寫入的數據搬移到非易失區,因此一直檢測一直等待到EEPROM響應才能再次往里邊寫數據。 12.3.3 EEPROM的頁寫入在向EEPROM連續寫入多個字節的數據時,如果每寫一個字節都要等待幾ms的話,整體上的寫入效率就太低了。因此EEPROM的廠商就想了一個辦法,把EEPROM分頁管理。24C01、24C02這兩個型號是8個字節一個頁,而24C04、24C08、24C16是16個字節一頁。Kingst51開發板上用的型號是24C02,一共是256個字節,8個字節一頁,一共有32頁。 分配好頁之后,同一個頁內連續寫入幾個字節后再發送停止位,EEPROM檢測到停止位后,就會一次性把這一頁的數據寫到非易失區域,不需要寫一個字節檢測一次了,并且頁寫入的時間也不會超過5ms。如果寫入的數據跨頁了,寫完了一頁之后,要發送一個停止位,然后等待并且檢測EEPROM的空閑模式,一直等到把上一頁數據完全寫到非易失區域后,再進行下一頁的寫入,這樣就可以在很大程度上提高數據的寫入效率,程序如下。 /****************************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); /* 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 E2Read(unsigned char *buf, unsigned char addr, unsigned char len); extern void E2Write(unsigned char *buf, unsigned char addr, unsigned char len); void main() { unsigned char i; unsigned char buf[5]; E2Read(buf, 0x8E, sizeof(buf)); //從E2中讀取一段數據 for (i=0; i<sizeof(buf); i++) //數據依次+1,+2,+3... { buf = buf + 1 + i; } E2Write(buf, 0x8E, sizeof(buf)); //再寫回到E2中 while(1); } 同樣數量的多字節寫入時間和頁寫入的時間到底差別多大呢?現在把兩次寫入時間用邏輯分析儀給抓了出來,并且用時間標簽A1和A2標注了開始位置和結束位置,如圖12-9和圖12-10所示,右側顯示的|A1-A2|就是最終寫入5個字節所耗費的時間。多字節一個一個寫入,每次寫入后都需要再次通信檢測EEPROM是否在“忙”,因此耗費了大量的時間,同樣的寫入5個字節的數據,一個一個寫入用了8.4ms左右的時間,而使用頁寫入,并且還跨頁操作,只用了3.5ms左右的時間。
12-9.png (26.32 KB, 下載次數: 0)
下載附件
2026-4-29 11:45 上傳
圖12-9 多字節寫入時間
12-10.png (24.9 KB, 下載次數: 0)
下載附件
2026-4-29 11:44 上傳
圖12-10 跨頁寫入時間 12.4練習題1、徹底理解I2C的通信時序。 2、能夠獨立完成EEPROM任意地址的單字節讀寫、多字節的跨頁連續寫入讀出。 3、將前邊學的交通燈進行改進,使用EEPROM保存紅燈和綠燈倒計時的時間,并且可以通過UART改變紅燈和綠燈倒計時時間。
|