本文介紹如何使用STM32標准外設庫驅動EEPROM,本例程驅動的EEPROM為AT24C02,通訊協議為IIC,使用IO口模擬方式。
本文適合對單片機及C語言有一定基礎的開發人員閱讀,MCU使用STM32F103VE系列。
1. EEPROM簡介
EEPROM全稱為EEPROM(Electrically Erasable Programmable Read Only Memory)是電可擦除可編程只讀存儲器。雖然名稱為只讀存儲器,但是擦除和寫入都是直接使用電路控制,不需要再使用外部設備來擦寫,即設備在運行過程中即可隨時擦除和寫入。可以按字節為單位修改數據,無需整個芯片擦除,且掉電后數據不丟失,一般用來存儲一些配置信息,以便系統重新上電的時候加載。
2. 常用EEPROM
一般常用的EEPROM為ATMEL公司(已被Microchip收購)的AT24Cxx系列,常用容量從1K到64Kbit不等,換算成字節為128到8K Bytes,可以根據項目需求和價格綜合考慮選型。
3. EEPROM操作說明
AT24C02容量為2Kbit,即256Byte,地址范圍為0~255,即0~0xFF,使用1個字節即可表示,因此地址長度為1字節。
3.1. 通訊方式I
AT24C02使用IIC協議跟MCU通訊。
3.2. 設備地址
如果僅接入一個AT24C02,可以將設備的A0、A1、A2引腳全部接入低電平,那么此時該設備的地位為0x50,再增加一位讀寫標志位,最終讀取操作時地址為0xA1,寫入操作時地址為0xA0。
3.3. 讀取數據
- 讀取當前字節:MCU直接發起讀操作,設備返回當前字節,當前字節自動加1,該操作較少使用。
- 讀取指定地址一個字節:MCU先向AT24C02寫入一個地址,然后再發起一個讀操作,AT24C02返回該地址存儲的字節。
- 連續讀取:MCU發起讀當前字節,或者讀指定地址字節,設備返回數據,MCU發送ACK,設備繼續返回后續地址數據,直到MCU發送NACK,設備不再返回數據。
1 uint8_t EE_Read(uint8_t addr, uint8_t *pBuffer, uint8_t numToRead) 2 { 3 uint8_t i; 4 5 if(EE_WaitReady()){ 6 goto iic_fail; 7 } 8 9 IIC_Start(); 10 IIC_WriteByte(EEPROM_IIC_ADDR | IIC_WR); 11 if(IIC_WaitAck()){ 12 goto iic_fail; 13 } 14 IIC_WriteByte(addr); 15 if(IIC_WaitAck()){ 16 goto iic_fail; 17 } 18 IIC_Start(); 19 IIC_WriteByte(EEPROM_IIC_ADDR | IIC_RD); 20 if(IIC_WaitAck()){ 21 goto iic_fail; 22 } 23 24 for(i = numToRead; i > 0; i--) 25 { 26 *pBuffer++ = ((i == 1) ? IIC_ReadByte(0) : IIC_ReadByte(1)); 27 } 28 29 IIC_Stop(); 30 return 0; 31 32 iic_fail: 33 IIC_Stop(); 34 return 1; 35 }
3.4. 寫入數據
- 寫入一個字節:MCU先向AT24C02寫入一個地址,然后再寫入數據。
- 寫入一頁:MCU先向AT24C02寫入一個地址,然后再依次寫入數據,注意AT24C02一頁有8個字節,每頁開始地址均是8的整數倍,一次頁寫入操作地址不能超過當前頁的尾地址。
- 連續寫入:AT24C02本身沒有提供連續寫入的操作,因此必須先將數據按頁地址分為若干頁,然后再依次調用頁寫入操作進行寫入。數據分頁函數為EE_GetWritePages(),詳細介紹見后面。
1 //AT24C02頁寫入函數,不得超過當前頁的最后地址,每頁可寫入8個字節 2 //addr: 寫入數據的目的地址 3 //pBuffer: 要寫入的數據 4 //numToWrite: 要寫入數據的長度 5 //返回值: 1,寫入失敗 6 // 0,寫入成功 7 uint8_t EE_PageWrite(uint8_t addr, uint8_t *pBuffer, uint8_t numToWrite) 8 { 9 uint8_t i; 10 11 if(EE_WaitReady()){ 12 goto iic_fail; 13 } 14 15 IIC_Start(); 16 IIC_WriteByte(EEPROM_IIC_ADDR | IIC_WR); 17 if(IIC_WaitAck()){ 18 goto iic_fail; 19 } 20 IIC_WriteByte(addr); 21 if(IIC_WaitAck()){ 22 goto iic_fail; 23 } 24 for(i = 0; i < numToWrite; i++){ 25 IIC_WriteByte(*pBuffer++); 26 if(IIC_WaitAck()){ 27 goto iic_fail; 28 } 29 } 30 31 IIC_Stop(); 32 return 0; 33 34 iic_fail: 35 IIC_Stop(); 36 return 1; 37 } 38 39 //在AT24C02里面的指定地址開始寫入指定個數的數據 40 //addr: 寫入數據的目的地址 41 //pBuffer: 要寫入的數據 42 //numToWrite: 要寫入數據的長度 43 //返回值: 1,讀取失敗 44 // 0,讀取成功 45 uint8_t EE_Write(uint8_t addr, uint8_t *pBuffer, uint8_t numToWrite) 46 { 47 uint8_t i; 48 uint8_t firstPageLen, lastPageLen, pageNum; 49 50 EE_GetWritePages(addr, numToWrite, EEPROM_PAGE_SIZE, 51 &firstPageLen, &lastPageLen, &pageNum); 52 53 for(i = 0; i < pageNum; i++) 54 { 55 if(i == 0){ //首頁寫入長度為firstPageLen 56 if(EE_PageWrite(addr, pBuffer, firstPageLen)){ 57 goto iic_fail; 58 } 59 addr += firstPageLen; 60 pBuffer += firstPageLen; 61 }else if(i == pageNum - 1){ //尾頁寫入長度為lastPageLen 62 if(EE_PageWrite(addr, pBuffer, lastPageLen)){ 63 goto iic_fail; 64 } 65 addr += lastPageLen; 66 pBuffer += lastPageLen; 67 }else{ //除首頁和尾頁外寫入長度為EEPROM_PAGE_SIZE 68 if(EE_PageWrite(addr, pBuffer, EEPROM_PAGE_SIZE)){ 69 goto iic_fail; 70 } 71 addr += EEPROM_PAGE_SIZE; 72 pBuffer += EEPROM_PAGE_SIZE; 73 } 74 } 75 76 return 0; 77 78 iic_fail: 79 IIC_Stop(); 80 return 1; 81 }
3.5. 狀態等待
EEPROM調用寫入操作后,需要一段時間才能將數據更新,通過查看AT24C02的datasheet,可以看到寫入循環最大為5ms,數據更新期間對設備無法進行讀寫操作,程序具體實現為向AT24C02發送START+設備地址(寫命令),如果AT24C02返回ACK信號,表示AT24C02目前已經可以正常讀寫,否則需要繼續等待,等待過程中可以繼續發送START+設備地址(寫命令),直到返回ACK信號。
為防止設備故障導致一直無法返回ACK信號,需要設定一個最大檢測次數,因為AT24C02最大寫入循環為5ms,因此可以將每次檢測時間間隔設定為100us,循環次數為50次,如果檢測50次均沒有返回ACK信號,此時可以放棄讀寫操作,返回錯誤。
1 //查看AT24C02是否空閑 2 //返回值: 1,EEPROM忙,無法讀寫 3 // 0,EEPROM空閑,可以讀寫 4 uint8_t EE_WaitReady(void) 5 { 6 uint8_t i; 7 uint8_t ret = 1; 8 9 for(i = 0; i < 50; i++){ 10 IIC_Start(); 11 IIC_WriteByte(EEPROM_IIC_ADDR | IIC_WR); //發送寫命令 12 if(!IIC_WaitAck()){ 13 ret = 0; 14 break; 15 } 16 delay_us(100); 17 } 18 IIC_Stop(); 19 return ret; 20 }
3.6. 數據存儲如何按Page對齊
AT24C02存儲數據時把整個存儲區會分成若干頁,每頁8個字節,一次寫操作不能跨頁寫入,因此如果連續寫入時必須要使用一定的算法將數據按頁將數據分割成若干塊,然后按塊進行寫入。
如果需要連續寫入,那么需要分成若干次頁寫入,除首頁和尾頁可能不是整頁寫入之外,中間頁均可以整頁寫入,那么只要能夠計算出首頁需要寫入的字節數、尾頁需要寫入的字節數以及需要寫入的總頁數n,然后調用n次頁寫入函數,即可實現數據的全部寫入,第一次調用時寫入長度為首頁字節數,最后一次調用時寫入長度為尾頁字節數,中間的寫入長度為整頁長度,每次調用完畢后地址增加實際寫入的長度。
具體計算過程:
首塊偏移 = 起始地址 % 頁大小,
首塊寫入長度 = 頁大小 - 首塊偏移,
如果寫入長度 < 首塊寫入長度,首塊寫入長度 = 寫入長度,
剩余長度 = 寫入長度 - 首塊寫入長度,
剩余整數塊數 = 剩余長度 / 頁大小,
尾塊長度 = 剩余長度 % 頁大小,
總塊數 = 1 + 剩余整數塊數,
如果尾塊長度不為0,總塊數加1。
舉例:寫入起始地址為2,寫入長度為18,那么通過計算,可以得出首頁寫入長度為6,尾頁寫入長度為4,寫入頁數為3,需要調用3次頁寫入函數EE_PageWrite()。該函數第一個參數為寫入的地址,第二個參數為寫入數據指針,第三個參數為寫入的字節數。每次調用完畢后數據指針(pBuffer)也需要增加寫入的長度。
1 EE_PageWrite(2, pBuffer, 6); 2 pBuffer += 6; 3 EE_PageWrite(8, pBuffer, 8); 4 pBuffer += 8; 5 EE_PageWrite(16, pBuffer, 4); 6 pBuffer += 4;
1 /* 2 根據要寫入的地址、長度、頁大小計算如何分頁 3 輸入參數:addr: 寫入起始地址 4 len: 寫入數據長度 5 pageSize:每頁存儲的數據,對於AT24C02來說,該值為8 6 要寫入參數:pFirstPageLen: 首頁要寫入的字節 7 pLastPageLen: 尾頁要寫入的字節 8 pPageNum: 總共要寫入的頁數 9 */ 10 void EE_GetWritePages(uint8_t addr, uint8_t len, uint8_t pageSize, 11 uint8_t * pFirstPageLen, uint8_t * pLastPageLen, uint8_t * pPageNum) 12 { 13 uint8_t firstPageOffset; //首頁偏移 14 uint8_t otherLen; //去除首頁之后剩余長度 15 uint8_t otherPageNum; //去除首頁之后剩余整數頁數量 16 17 firstPageOffset = addr % pageSize; 18 *pFirstPageLen = pageSize - firstPageOffset; 19 20 if(len < *pFirstPageLen){ 21 *pFirstPageLen = len; 22 } 23 24 otherLen = len - *pFirstPageLen; 25 otherPageNum = otherLen / pageSize; 26 *pLastPageLen = otherLen % pageSize; 27 28 *pPageNum = otherPageNum + 1; 29 30 if(*pLastPageLen){ 31 (*pPageNum)++; 32 } 33 }