單片機學習(十一)I2C總線和AT24C02的使用


一、 存儲器介紹

存儲器分類圖

image-20210905154725844

1. RAM

這類存儲器中的數據都是掉電即失的,例如計算機中的內存就是DRAM,但它們數據讀寫速度都是要比ROM要快得多的。

  • SRAM:本質是電路,使用電路構成的觸發器來存儲數據(如JK觸發器),因此這種存儲器讀寫數據是最快的,而它們的成本也比較高,一般用作計算機的高速存儲器寄存器
  • DRAM:使用電容來存儲數據,因為電容存在漏電現象,因此需要每隔一段時間進行掃描重新充電。它們一般用來構成計算機的內存,手機的閃存等。

2. ROM

這類存儲器中的數據有着掉電不丟失的特性,但它們讀寫數據的速度遠小於RAM

  • Mask ROM:僅由電路構成,只讀的ROM,就是只可讀不可寫
  • PROM:可以寫入數據,但只能寫入一次數據
  • EPROM:可讀可寫
  • ...

二、AT24C02簡介

AT24C02是一種可以實現掉電不丟失的存儲器,可用於保存單片機運行時想要永久保存的數據信息

  • 存儲介質:E2PROM
  • 通訊接口:I2C總線
  • 容量:256字節

電路連接

image-20210905160534839

三、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從高電平切換到低電平,如下圖所示:

image-20210908152835490

同時為了使這塊拼圖可以和其他的部分連接上,我們在發送起始信息(start)之后,也將SCL拉低。

② 發送終止信息

終止條件:SCL高電平期間SDA從低電平切換到高電平

image-20210908153046203

同理,為了和其他拼圖拼接上,我們得到的SCL原來是低電平的(這是因為后面的4塊拼圖結束時SCL都是低電平,因此來到終止信息時也是低電平),我們先拉高SCL然后拉高SDA即可發送終止信號了。

③ 發送一個byte的信息

發送一個byte(字節)信息可以分解為循環發送8個bit 的信息,因此我們只需要知道如何發送一個bit的信息即可。

發送一個bit信息的操作:(1)SCL低電平期間,主機將數據位依次放到SDA線上(高位在前),(2)然后拉高SCL從機將在SCL高電平期間讀取數據位,所以SCL高電平期間SDA不允許有數據變化

即如果我們希望發送0,則:

  1. 在SCL在低電平時,將SDA的電平拉低(置零
  2. 再將SCL的電平拉高,提醒從機讀取信息
  3. 過一段時間(等待從機把信息讀取完成)后再次將SCL拉低

具體的過程如圖所示:

image-20210908153940500

例如在B7時間內,如果SDA為低電平,則發送的數據為0,如果為高電平則發送的數據為1

④ 接收一個byte的信息

和發送信息類似,我們只需要知道如何接收一個bit的信息,然后只需要循環進行8次即可接收一個字節的信息了。

接收一個bit信息的操作:(1)SCL低電平期間從機將數據位依次放到SDA線上(高位在前),(2)然后拉高SCL(相當於通知從機主機正在讀取這個bit的數據),主機將在SCL高電平期間讀取數據位,所以SCL高電平期間SDA不允許有數據變化。(主機在接收之前,需要釋放SDA

具體的過程如圖所示:

image-20210908155949532

這個過程我個人的理解是:

  1. 釋放SDA(拉高電平),將寫入數據的主動權交給從機
  2. 從機寫入下一位bit到SDA中
  3. 主機拉高SCL,提示從機我正在讀取這個bit的數據,你先不要變化
  4. 主機讀取完數據,將SCL拉低,實際上這個過程是在告訴從機我已經讀完這個bit了,你給我下一個bit的數據吧,然后如果還未滿8位則轉到第2步繼續接受數據,如果滿了一個字節則接收結束

對比發送數據和接收數據可以發現,這兩個過程非常的相似,只不過(1)發送信息時寫入信息的一方是主機,而接收信息時寫入數據的是從機,(2)發送信息時拉高和拉低SCL電平是通知從機讀取信息,而在接收信息時則是通知從機主機當前正在讀取信息。

可以發現單從開始時的SCLSDA的狀態是無法區分主機是想發送還是接收信息的,其實接收數據還是發送數據是由后面的時序過程(數據幀)所決定的,不需要起始狀態進行區分

⑤ 發送應答

在接收完一個字節之后,主機在下一個時鍾發送一位數據數據0表示應答數據1表示非應答

其實發送應答的操作就是發送一個bit的操作而已:

image-20210908160852670

⑥ 接收應答

在發送完一個字節之后,主機在下一個時鍾接收一位數據,判斷從機是否應答,數據0表示應答數據1表示非應答(主機在接收之前,需要釋放SDA)

和發送應答類似,接收應答的操作也就是接收一個bit數據的操作。

image-20210908161026090

3. I2C數據幀

① 發送一幀數據

其過程是(過程中的接收應答省略不寫了):

  1. 發送一個開始信號
  2. 發送從機地址(加上寫入標記,最后一位為0)
  3. 循環發送字節數據
  4. 發送完后發送結束信號
image-20210908161143468

② 接收一幀數據

image-20210908161500947

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

③ 先發送后接收數據

image-20210908161722293

4. AT24C02數據幀

AT24C02是使用I2C通訊協議進行通訊的,I2C數據幀相當於是一輛卡車,而AT24C02數據幀則是在原來卡車的基礎上裝上了特定貨物的卡車。

① 字節寫

WORD ADDRESS處寫入數據DATA

image-20210908162104316

② 隨機讀

讀出在WORD ADDRESS處的數據DATA

image-20210908162140492

AT24C02的固定地址為1010,可配置地址本開發板上為000,所以SLAVE ADDRESS+W0xA0SLAVE ADDRESS+R0xA1

四、代碼實現

1. I2C模塊

實現I2C模塊,即實現上面介紹的6塊拼圖。

首先定義出SCLSDA連接的引腳:

sbit I2C_SCL = P2 ^ 1;
sbit I2C_SDA = P2 ^ 0;

① 發送起始信息和發送終止信息

image-20210908152835490 image-20210908153046203
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;
}

② 發送字節信息和接收字節信息

image-20210908153940500 image-20210908155949532
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號地址的空間進行存儲:

image-20210908162104316
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地址中存儲的字節信息:

image-20210908162140492
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調用一次按鍵驅動函數
    }
}

這樣我們每次重啟時就可以看到上次存儲的數字了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM