一、 存儲器介紹
存儲器分類圖

1. RAM
這類存儲器中的數據都是掉電即失的,例如計算機中的內存就是DRAM,但它們數據讀寫速度都是要比ROM要快得多的。
- SRAM:本質是電路,使用電路構成的觸發器來存儲數據(如JK觸發器),因此這種存儲器讀寫數據是最快的,而它們的成本也比較高,一般用作計算機的高速存儲器,寄存器等
- DRAM:使用電容來存儲數據,因為電容存在漏電現象,因此需要每隔一段時間進行掃描重新充電。它們一般用來構成計算機的內存,手機的閃存等。
2. ROM
這類存儲器中的數據有着掉電不丟失的特性,但它們讀寫數據的速度遠小於RAM
- Mask ROM:僅由電路構成,只讀的ROM,就是只可讀不可寫
- PROM:可以寫入數據,但只能寫入一次數據
- EPROM:可讀可寫
- ...
二、AT24C02簡介
AT24C02
是一種可以實現掉電不丟失的存儲器,可用於保存單片機運行時想要永久保存的數據信息
- 存儲介質:E2PROM
- 通訊接口:I2C總線
- 容量:256字節
電路連接

三、I2C總線和AT24C02數據幀
I2C總線(Inter IC BUS)是由Philips
公司開發的一種通用數據總線(通信協議)
- 兩根通信線:SCL(Serial Clock)、SDA(Serial Data)
- 同步、半雙工,帶數據應答
- 通用的I2C總線,可以使各種設備的通信標准統一,對於廠家來說,使用成熟的方案可以縮短芯片設計周期、提高穩定性,對於應用者來說,使用通用的通信協議可以避免學習各種各樣的自定義協議,降低了學習和應用的難度
1. 電路規范
- 所有I2C設備的SCL連在一起,SDA連在一起
- 設備的SCL和SDA均要配置成開漏輸出模式
- SCL和SDA各添加一個上拉電阻,阻值一般為4.7KΩ左右
- 開漏輸出和上拉電阻的共同作用實現了“線與”的功能,此設計主要是為了解決多機通信互相干擾的問題
2. I2C的時序結構
我們可以將通過I2C協議實現主機與從機通信的過程分為以下六個部分,這六個部分可以像拼圖一樣拼湊出所有的通信過程:
① 發送起始信息:
起始條件:SCL
高電平期間,SDA
從高電平切換到低電平,如下圖所示:

同時為了使這塊拼圖可以和其他的部分連接上,我們在發送起始信息(
start
)之后,也將SCL拉低。
② 發送終止信息:
終止條件:SCL
高電平期間,SDA
從低電平切換到高電平

同理,為了和其他拼圖拼接上,我們得到的SCL原來是低電平的(這是因為后面的4塊拼圖結束時SCL都是低電平,因此來到終止信息時也是低電平),我們先拉高SCL,然后拉高SDA即可發送終止信號了。
③ 發送一個byte的信息:
發送一個byte
(字節)信息可以分解為循環發送8個bit 的信息,因此我們只需要知道如何發送一個bit的信息即可。
發送一個bit
信息的操作:(1)SCL低電平期間,主機將數據位依次放到SDA線上(高位在前),(2)然后拉高SCL,從機將在SCL高電平期間讀取數據位,所以SCL高電平期間SDA不允許有數據變化
即如果我們希望發送0,則:
- 在SCL在低電平時,將SDA的電平拉低(置零)
- 再將SCL的電平拉高,提醒從機讀取信息
- 過一段時間(等待從機把信息讀取完成)后再次將SCL拉低
具體的過程如圖所示:

例如在B7時間內,如果SDA為低電平,則發送的數據為0,如果為高電平則發送的數據為1
④ 接收一個byte的信息:
和發送信息類似,我們只需要知道如何接收一個bit的信息,然后只需要循環進行8次即可接收一個字節的信息了。
接收一個bit
信息的操作:(1)SCL低電平期間,從機將數據位依次放到SDA線上(高位在前),(2)然后拉高SCL(相當於通知從機主機正在讀取這個bit的數據),主機將在SCL高電平期間讀取數據位,所以SCL高電平期間SDA不允許有數據變化。(主機在接收之前,需要釋放SDA)
具體的過程如圖所示:

這個過程我個人的理解是:
- 釋放SDA(拉高電平),將寫入數據的主動權交給從機
- 從機寫入下一位bit到SDA中
- 主機拉高SCL,提示從機我正在讀取這個bit的數據,你先不要變化
- 主機讀取完數據,將SCL拉低,實際上這個過程是在告訴從機我已經讀完這個bit了,你給我下一個bit的數據吧,然后如果還未滿8位則轉到第2步繼續接受數據,如果滿了一個字節則接收結束
對比發送數據和接收數據可以發現,這兩個過程非常的相似,只不過(1)發送信息時寫入信息的一方是主機,而接收信息時寫入數據的是從機,(2)發送信息時拉高和拉低SCL電平是通知從機讀取信息,而在接收信息時則是通知從機主機當前正在讀取信息。
可以發現單從開始時的SCL
和SDA
的狀態是無法區分主機是想發送還是接收信息的,其實接收數據還是發送數據是由后面的時序過程(數據幀)所決定的,不需要起始狀態進行區分。
⑤ 發送應答:
在接收完一個字節之后,主機在下一個時鍾發送一位數據,數據0表示應答,數據1表示非應答
其實發送應答的操作就是發送一個bit的操作而已:

⑥ 接收應答:
在發送完一個字節之后,主機在下一個時鍾接收一位數據,判斷從機是否應答,數據0表示應答,數據1表示非應答(主機在接收之前,需要釋放SDA)
和發送應答類似,接收應答的操作也就是接收一個bit數據的操作。

3. I2C數據幀
① 發送一幀數據:
其過程是(過程中的接收應答省略不寫了):
- 發送一個開始信號
- 發送從機地址(加上寫入標記,最后一位為0)
- 循環發送字節數據
- 發送完后發送結束信號

② 接收一幀數據:

過程和發送一幀數據的過程非常類似,只是中間的發送字節數據變成了接收字節數據,此外還有地址部分的最后一位為1,代表讀取數據。
③ 先發送后接收數據:

4. AT24C02數據幀
AT24C02是使用I2C通訊協議進行通訊的,I2C數據幀相當於是一輛卡車,而AT24C02數據幀則是在原來卡車的基礎上裝上了特定貨物的卡車。
① 字節寫:
在WORD ADDRESS
處寫入數據DATA

② 隨機讀:
讀出在WORD ADDRESS
處的數據DATA

AT24C02的固定地址為1010,可配置地址本開發板上為000,所以
SLAVE ADDRESS+W
為0xA0
,SLAVE ADDRESS+R
為0xA1
四、代碼實現
1. I2C模塊
實現I2C模塊,即實現上面介紹的6塊拼圖。
首先定義出SCL
和SDA
連接的引腳:
sbit I2C_SCL = P2 ^ 1;
sbit I2C_SDA = P2 ^ 0;
① 發送起始信息和發送終止信息:


void I2C_Start() {
I2C_SDA = 1;
I2C_SCL = 1;
// 在SCL為高電平拉低SDA
I2C_SDA = 0;
I2C_SCL = 0;
}
void I2C_Stop() {
I2C_SDA = 0;
// 在SCL為高電平時拉高SDA
I2C_SCL = 1;
I2C_SDA = 1;
}
② 發送字節信息和接收字節信息:


void I2C_SendByte(unsigned char byte) {
unsigned char i = 0;
for (i = 0; i < 8; i++) {
// 先將一個bit的數據寫入到SDA中
I2C_SDA = byte & (0x80 >> i);
// SCL先拉高,后拉低,通知從機接收數據
I2C_SCL = 1;
I2C_SCL = 0;
}
}
unsigned char I2C_ReceiveByte() {
unsigned char byte = 0x00;
unsigned char i;
// 先釋放SDA(將主動權交給從機)
I2C_SDA = 1;
for (i = 0; i < 8; i++) {
// 通知從機主機正在讀取數據
I2C_SCL = 1;
// 如果是1則置一,否則默認為0
if (I2C_SDA) {
byte |= (0x80 >> i);
}
// 通知從機這個bit已經讀取完畢,可以發送下一個bit
I2C_SCL = 0;
}
return byte;
}
③ 發送應答和接收應答:
void I2C_SendAck(unsigned char AckBit) {
I2C_SDA = AckBit;
// SCL先拉高,后拉低,通知從機接收數據
I2C_SCL = 1;
I2C_SCL = 0;
}
unsigned char I2C_ReceiveAck() {
unsigned char AckBit;
// 先釋放SDA(將主動權交給從機)
I2C_SDA = 1;
// 通知從機主機正在讀取數據
I2C_SCL = 1;
AckBit = I2C_SDA;
// 通知從機這個bit已經讀取完畢
I2C_SCL = 0;
return AckBit;
}
2. AT24C02模塊
這個模塊依賴於I2C模塊,即利用I2C發送和接收數據。
首先定義從機地址:
// 最后一位為0代表寫,即發送數據,為1代表讀,即接受數據
#define AT24C02_ADDRESS 0xA0
① 字節寫:
前面已經提到,AT24C02可以存儲256個字節的數據,因此我們的數據可以任意0~255號地址的空間進行存儲:

void AT24C02_WriteByte(unsigned char WordAddress, unsigned char Byte) {
I2C_Start();
// 從機地址
I2C_SendByte(AT24C02_ADDRESS);
I2C_ReceiveAck();
// 字地址
I2C_SendByte(WordAddress);
I2C_ReceiveAck();
// 發送真正的數據
I2C_SendByte(Byte);
I2C_ReceiveAck();
I2C_Stop();
}
② 隨機讀:
讀取WordAddress
地址中存儲的字節信息:

unsigned char AT24C02_ReadByte(unsigned char WordAddress) {
unsigned char Byte;
I2C_Start();
// 從機地址
I2C_SendByte(AT24C02_ADDRESS);
I2C_ReceiveAck();
// 字地址
I2C_SendByte(WordAddress);
I2C_ReceiveAck();
I2C_Start();
// 從機地址,read模式
I2C_SendByte(AT24C02_ADDRESS|0x01);
I2C_ReceiveAck();
// 讀取信息
Byte = I2C_ReceiveByte();
I2C_SendAck(1);
I2C_Stop();
return Byte;
}
3. 使用AT24C02進行數據存儲
我們使用LCD_1602
進行顯示,第二行顯示num數字,當我們單擊按鈕時:
- 點擊k1,
num--
- 點擊k2,
num++
- 點擊k3,將
num
的數據存儲到AT24C02
中地址為1的空間中
void main() {
unsigned char key, num;
unsigned char storageData;
LCD_Init();
LCD_ShowString(1, 1, "Hello world");
LCD_ShowString(2, 1, "num:");
storageData = AT24C02_ReadByte(1);
num = storageData;
LCD_ShowNum(2, 5, storageData, 3);
Timer0_Init();
while (1) {
key = Key();
if (key) {
switch (key) {
case 1:
num--;
break;
case 2:
num++;
break;
case 3:
AT24C02_WriteByte(1, num);
break;
}
}
LCD_ShowNum(2, 5, num, 3);
}
}
void Timer0_Routine() interrupt 1 {
static unsigned int T0Count..;
TL0 = 0x18;//設置定時初值
TH0 = 0xFC;//設置定時初值
T0Count++;
if (T0Count >= 20) {
T0Count = 0;
Key_Loop();//每20ms調用一次按鍵驅動函數
}
}
這樣我們每次重啟時就可以看到上次存儲的數字了。