一、概述
1. 背景介紹
在微機發展的早期,出現了BIOS(Basic Input Output System),它是個人電腦啟動時加載的第一個軟件,用來完成對系統的加電自檢、各功能模塊的初始化、基本輸入輸出的驅動程序及引導操作系統。人們希望掉電之后 BIOS 數據不能丟失,於是將 BIOS 燒錄到 ROM。事實上它是一組固化到計算機內主板上一個 ROM 芯片上的程序。
最初的最初,BIOS 都是通過一種特殊的燒錄方法燒入 ROM 中的,但是一旦燒進去,你只能讀 ROM,里面的內容是無法更改的。萬一發現錯誤,只能丟棄,換另一塊ROM,重做一遍,這就非常麻煩了。
后來的后來,一位以色列工程師發明了EPROM(Erasable Programmable Read-Only Memory),這種芯片比較奇葩,封裝頂部開了一個透明小窗口,如果要往芯片內部錄入程序,就需要使用強紫外線進行擦除(當然你放到太陽底下也是可以的)。因此,一般情況下為了不破壞內部的數據,就需要用紙來蓋住窗口。好景不長,程序員和硬件工程師還是覺得這樣太麻煩了。
最后的最后,EEPROM(Electrically Erasable Programmable Read Only Memory) 登場了,它是一種帶電可擦可編程只讀存儲器 EEPROM,你可以在電腦上或專用設備上擦除已有信息,重新編程,這為許多開發人員提供了便利。
如今,40多年過去了,ROM 早已失去當年的完整意思,不再是曾經的只讀存儲器了,它不僅能讀,還能寫,然而它的名字卻像人類的傳統風俗一樣被保留了下來,成為硬件發展史上的一顆璀璨的明珠。
2. EEPROM簡介
EEPROM(帶電可擦可編程只讀存儲器,Electrically Erasable Programmable Read Only Memory) 是用戶可更改的只讀存儲器,其可通過高於普通電壓的作用來擦除和重編程(重寫)。不像EPROM芯片,EEPROM不需從計算機中取出即可修改。
在一個EEPROM中,當計算機在使用的時候可頻繁地反復編程,因此EEPROM的壽命是一個很重要的設計考慮參數。EEPROM是一種特殊形式的閃存,其應用通常是個人電腦中的電壓來擦寫和重編程。一般情況下,EEPROM擁有30萬到100萬次的壽命,也就是它可以反復寫入30-100萬次,而讀取次數是無限的。
二、AT24C02——常用的EEPROM
AT24C02是一個常用的基於 I2C 通信協議的 EEPROM 元件,例如ATMEL公司的AT24C02、CATALYST公司的 CAT24C02 和ST公司的 ST24C02 等芯片。我們實驗板使用的是ATMEL公司的AT24C02(Automotive
Temperature
Serial EEPROM)。
ATMEL公司曾經推出不同型號的EEPROM:AT24C01A、AT24C02、AT24C04、AT24C08A、AT24C16A,分別對應不同容量:1K (128 x 8)、2K (256 x 8)、4K (512 x 8)、8K (1024 x 8)、16K (2048 x 8)。
AT24C02的容量描述如下:
AT24C02, 2K SERIAL EEPROM: Internally organized with 32 pages of 8 bytes each,
the 2K requires an 8-bit data word address for random word addressing.
1. 電路原理圖
首先需要注意一點:AT24C02采用了 I2C 協議的接口,但這不意味着 EEPROM 就一定要用 I2C 接口,EEPROM 也可以用其它接口。I2C 和 EEPROM 沒有任何聯系。
我們截取了正點原子精英版的電路原理圖:
從圖中可以知道,AT24C02 有8個接口,每個接口的功能如下表所示:
引腳名 | 功能 |
---|---|
A0-A2 | 地址輸入 |
SDA | I2C數據總線 |
SCL | I2C時鍾總線 |
WP | 寫保護(Write Protect) |
NC | 無連接(No Connect) |
這里需要特別說明一下 AT24C02 的設備地址的組成。設備地址一共有八位,高四位已經固化為1010
,用戶不能修改;低四位中,后三位為可配置的地址位,最低一位為讀寫位(還記得 I2C 的協議層吧,0為寫,1為讀)。如下表所示:
1 | 0 | 1 | 0 | A2 | A1 | A0 | R/W |
---|---|---|---|---|---|---|---|
MSB | LSB |
所以,一般情況下,我們要進行寫操作,設備地址就為1010 0000
(十六進制:0xA0
);我們要進行讀操作,設備地址就為1010 0001
(十六進制:0xA1
)。
我們的電路圖顯示,WP 接地,A2-A0 都接地,SDA 和 SCL 與 MCU 主機相連。
2. 寫操作
(1)按字節寫操作(Byte Write)
與我們之前講的 I2C 協議類似,字節寫入的通訊過程如下:
- 主機產生起始信號和 EEPROM 地址,並且讀寫方向為寫方向(0)。
- 主機發送要寫入數據的地址,EEPROM 收到后會將其存入緩存中,同時發送應答信號。
- 主機發送8位數據至 EEPROM ,EEPROM 將數據存入緩存后,開始往非易失區寫入數據。注意,這個過程需要一定時間,根據官方手冊可知一次寫入過程的最大時間為 5ms,此時 EEPROM 不會響應主機任何的請求,相當於 EEPROM 從 I2C 總線斷開了。
AT24Cxx官方手冊: At this time the EEPROM enters an internally timed write cycle, tWR (Write Cycle Time, Max = 5 ms) , to the nonvolatile memory. All inputs are disabled during this write cycle and the EEPROM will not respond until the
write is complete (see Figure 8 on page 10).
- 由於 EEPROM 的寫入時間比 STM32 的運行速度要慢得多,因此 STM32 會誤以為沒有收到應答信號。應答輪詢(ACKNOWLEDGE POLLING) 前來解決這個問題:STM32 再次發送一個起始信號和 EEPROM 地址,當 EEPROM 完成一次寫入周期后,它會發送應答信號至主機。
AT24Cxx官方手冊: ACKNOWLEDGE POLLING: Once the internally timed write cycle has started and the
EEPROM inputs are disabled, acknowledge polling can be initiated. This involves sending a start condition followed by the device address word. The read/write bit is representative of the operation desired. Only if the internal write cycle has completed will the EEPROM respond with a “0”, allowing the read or write sequence to continue.
- 最后,主機發送停止信號,一次通訊過程完成。
(2)按頁寫操作(Page Write)
由於按字節寫入多個數據的時候,每次都要發起起始條件和設備地址,十分耗費時間,於是就有了按頁寫入的操作。和字節寫入有區別的是,頁寫入是多個數據寫入,在此期間不需要起始信號,如上圖所示,每成功寫入一次數據,地址加一。
這里需要說明一點,對於 AT24C02 的分頁管理是每8個字節為一頁,它一共有256個字節,所以總頁數為 256 / 8 = 16頁,因此每一次頁寫入操作的是8個字節。例如,下面的地址 0-7 為一頁:
數據 | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
這里還有一個坑,如果你想從地址2到地址9寫入數據(a-h),正好是8個地址,你想通過頁寫將數據寫入,會出現這樣的情況:
(你以為的結果)
數據 | a | b | c | d | e | f | g | h | ||
---|---|---|---|---|---|---|---|---|---|---|
地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
(實際的結果:由於地址8和9是下一頁,且只能對一整塊頁進行操作,不能跨頁寫,所以回到0地址繼續寫,有可能會把原來數據覆蓋掉,這種情況叫 回滾到頁首 。每頁的首地址是與8對齊的,即 addr mod 8 = 0)
數據 | g | h | a | b | c | d | e | f | ||
---|---|---|---|---|---|---|---|---|---|---|
地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
另外,當寫到 EEPROM 的最后一個地址還要往下寫時,會產生溢出,它回到0地址繼續寫。
3. 讀操作
(1)隨機讀操作
注意,這里的隨機指的是可任意讀取一個地址的數據。
和之前寫操作很類似,不過有一個地方不同:在讀取數據之前,STM32 需要將准備讀取的數據的地址寫入到 EEPROM 的緩存區。接下來 STM32 還要發送一次起始信號和設備地址,選擇讀方向(1)。
另外,由於是讀取不是寫入,EEPROM的一次讀取周期比寫入周期快得多,不需要像寫入操作那樣應答輪詢。
(2)順序讀操作
與頁寫不同,順序讀不受頁的限制,你想讀多少就多少。與頁寫類似,不再贅述。
請注意,以上這4種操作請務必熟練掌握!
三、實戰:讀寫EEPROM(單字節操作)
1. 單字節寫入
按照之前的講解,我們可以寫出編寫的思路:
- 產生起始信號
- 發送從機地址
- 發送要寫入數據的地址
- 發送要寫入的數據
- 產生停止信號
/**
* @brief 軟件模擬EEPROM字節寫入
* @param addr:要寫入數據的地址
data:要寫入的數據
* @retval 無
*/
void EEPROM_ByteWrite(uint8_t addr, uint8_t data)
{
/* 產生起始信號 */
I2C_GenerateSTART(I2Cx, ENABLE);
/* 檢測EV5事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS );
/* 發送7位從機地址 */
I2C_Send7bitAddress(I2Cx, EEPROM_ADDR, I2C_Direction_Transmitter);
/* 檢測EV6事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS );
/* 發送要寫入數據的地址 */
I2C_SendData(I2Cx, addr);
/* 檢測EV8事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS );
/* 發送要寫入的數據 */
I2C_SendData(I2Cx, data);
/* 檢測EV8_2事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS );
/* 產生停止信號 */
I2C_GenerateSTOP(I2Cx, ENABLE);
/* 重新使能ACK */
I2C_AcknowledgeConfig(I2Cx, ENABLE);
}
2. 單字節讀取
按照之前的講解,我們可以寫出編寫的思路:
- 產生起始信號
- 發送從機地址
- 發送要讀取數據的地址
- 再產生一次起始信號
- 發送從機地址
- 讀取數據
- 產生停止信號
/**
* @brief 軟件模擬EEPROM隨機讀取
* @param addr:要讀取數據的地址
*data:要接收數據的容器
* @retval 無
*/
void EEPROM_RandomRead(uint8_t addr, uint8_t* data)
{
/* 產生第一次起始信號 */
I2C_GenerateSTART(I2Cx, ENABLE);
/* 檢測EV5事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS );
/* 發送7位從機地址 */
I2C_Send7bitAddress(I2Cx, EEPROM_ADDR, I2C_Direction_Transmitter);
/* 檢測EV6事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED ) != SUCCESS );
/* 發送要讀取數據的地址 */
I2C_SendData(I2Cx, addr);
/* 檢測EV8事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS );
/* 產生第二次起始信號 */
I2C_GenerateSTART(I2Cx, ENABLE);
/* 檢測EV5事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS );
/* 發送7位從機地址 */
I2C_Send7bitAddress(I2Cx, EEPROM_ADDR, I2C_Direction_Receiver);
/* 檢測EV6事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS );
/* 檢測EV7事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS );
/* 讀取數據寄存器中的數據 */
*data = I2C_ReceiveData(I2Cx);
/* 產生停止信號 */
I2C_GenerateSTOP(I2Cx, ENABLE);
/* 重新使能ACK */
I2C_AcknowledgeConfig(I2Cx, ENABLE);
}
3. 需要注意的問題
我們在 main 函數調用以上兩個函數進行實驗:
#include "stm32f10x.h"
#include "usart.h"
#include "i2c.h"
#include "led.h"
uint8_t data = 45;
uint8_t data_rec = 0;
uint8_t addr = 11;
int main(void)
{
LED_Init();
USART_Config();
I2C_Config();
LED0_ON;
LED1_OFF;
printf("這是一個IIC通訊實驗\n");
EEPROM_ByteWrite(addr, data);
EEPROM_RandomRead(addr, &data_rec);
printf("發送和接收成功!數據為:%d\n", data_rec);
LED0_OFF;
LED1_ON;
while(1)
{
}
}
我們預期的結果是兩條 printf 語句都被執行,然而實際結果是只輸出了“這是一個IIC通訊實驗”,后面的內容並沒有輸出,這是怎么一回事呢?通過 debug 進行程序調試,我們發現程序卡死在了這條語句上(EEPROM_RandomRead
函數內第一次檢測 EV6 事件時發生死循環):
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED ) != SUCCESS );
這是為什么呢?原來是 EEPROM 寫入周期較長,人家還沒寫完呢,STM32 就要開始讀取數據了,發送過去的起始信號和從機地址人家收不到,不會產生應答,接着開始檢測 EV5 和 EV6 事件了,就會一直產生 ERROR,就發生死循環了。
所以,大家可以試一下,如果在以下兩個語句加上斷點進行調試的話,輸出結果是正確的。因為運行到斷點時程序會暫停,給了 EEPROM 充足的時間寫入數據,而且也將 STM32 的操作也暫停了。
EEPROM_ByteWrite(addr, data);
EEPROM_RandomRead(addr, &data_rec);
因此我們要做的是:必須確保 EEPROM 寫完后,STM32 才能發起讀取操作。這里有兩個辦法:一是直接在寫操作和讀操作之間加個延時函數,等待 5ms 再去讀取數據;二是檢測 EEPROM 是否可以產生應答,如果 STM32 收到應答,那么說明 EEPROM 完成了寫入,可以進行讀取操作了。這里我們直接給出第二種辦法的代碼:
/**
* @brief 等待EEPROM寫入完成
* @param 無
* @retval 無
*/
void EEPROM_WaitForWriteEnd(void)
{
do{
I2C_GenerateSTART(I2Cx, ENABLE);
while( I2C_GetFlagStatus(I2Cx, I2C_FLAG_SB) == RESET );
/* EV5事件被檢測到,發送設備地址 */
I2C_Send7bitAddress(I2Cx, EEPROM_ADDR, I2C_Direction_Transmitter);
}while( I2C_GetFlagStatus(I2Cx, I2C_FLAG_ADDR) == RESET );
/* EEPROM內部時序完成傳輸完成 */
I2C_GenerateSTOP(I2Cx, ENABLE);
}
這里說明一下,如果用I2C_CheckEvent
函數進行檢測,那么也會出現卡死的情況。這個涉及到 event 的清除原理,本人還不是很懂,就沒去深究了。
在 main 函數加入等待函數,問題解決:
EEPROM_ByteWrite(addr, data);
EEPROM_WaitForWriteEnd();
EEPROM_RandomRead(addr, &data_rec);
四、實戰:讀寫EEPROM(多字節操作)
和上面的思路類似,這里不再贅述。程序均已通過驗證,沒有問題。
1. 頁寫入
這個函數我在調用時曾經出現一次卡死的情況,不太清楚什么原因,因為后來又沒發生這種情況,可能這種簡單的寫法不太穩定吧。
/**
* @brief 軟件模擬EEPROM頁寫入
* @param addr:要寫入數據的地址
*data:數組的首地址
num:數據個數
* @retval 無
*/
void EEPROM_PageWrite(uint8_t addr, uint8_t *data, uint8_t num)
{
/* 產生起始信號 */
I2C_GenerateSTART(I2Cx, ENABLE);
/* 檢測EV5事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS );
/* 發送7位從機地址 */
I2C_Send7bitAddress(I2Cx, EEPROM_ADDR, I2C_Direction_Transmitter);
/* 檢測EV6事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS );
/* 發送要寫入數據的地址 */
I2C_SendData(I2Cx, addr);
/* 檢測EV8事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS );
while(num--)
{
/* 發送要寫入的數據 */
I2C_SendData(I2Cx, *data);
/* 檢測EV8_2事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS );
data++;
}
/* 產生停止信號 */
I2C_GenerateSTOP(I2Cx, ENABLE);
/* 重新使能ACK */
I2C_AcknowledgeConfig(I2Cx, ENABLE);
}
2. 連續讀取
/**
* @brief 軟件模擬EEPROM連續讀取
* @param addr:要讀取數據的地址
*data:要接收數據的容器
num:數據個數
* @retval 無
*/
void EEPROM_SeqRead(uint8_t addr, uint8_t* data, uint8_t num)
{
/* 產生第一次起始信號 */
I2C_GenerateSTART(I2Cx, ENABLE);
/* 檢測EV5事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS );
/* 發送7位從機地址 */
I2C_Send7bitAddress(I2Cx, EEPROM_ADDR, I2C_Direction_Transmitter);
/* 檢測EV6事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED ) != SUCCESS );
/* 發送要讀取數據的地址 */
I2C_SendData(I2Cx, addr);
/* 檢測EV8事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS );
/* 產生第二次起始信號 */
I2C_GenerateSTART(I2Cx, ENABLE);
/* 檢測EV5事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS );
/* 發送7位從機地址 */
I2C_Send7bitAddress(I2Cx, EEPROM_ADDR, I2C_Direction_Receiver);
/* 檢測EV6事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS );
while(num--)
{
/* 檢測EV7事件 */
while( I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS );
/* 讀取數據寄存器中的數據 */
*data = I2C_ReceiveData(I2Cx);
data++;
}
/* 產生停止信號 */
I2C_GenerateSTOP(I2Cx, ENABLE);
/* 重新使能ACK */
I2C_AcknowledgeConfig(I2Cx, ENABLE);
}
暫時先寫到這,鑒於程序還有可改進之處,以后可能會有補充。