微控制器學習(十一)I2C匯流排和AT24C02的使用

CodeReaper發表於2021-09-08

一、 儲存器介紹

儲存器分類圖

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協議實現主機與從機通訊的過程分為以下六個部分這六個部分可以像拼圖一樣拼湊出所有的通訊過程

2.1 傳送起始資訊

起始條件:SCL高電平期間SDA從高電平切換到低電平,如下圖所示:

image-20210908152835490

同時為了使這塊拼圖可以和其他的部分連線上,我們在傳送起始資訊(start)之後,也將SCL拉低。

2.2 傳送終止資訊

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

image-20210908153046203

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

2.3 傳送一個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

2.4 接收一個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的狀態是無法區分主機是想傳送還是接收資訊的,其實接收資料還是傳送資料是由後面的時序過程(資料幀)所決定的,不需要起始狀態進行區分

2.5 傳送應答

在接收完一個位元組之後,主機在下一個時鐘傳送一位資料資料0表示應答資料1表示非應答

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

image-20210908160852670

2.6 接收應答

在傳送完一個位元組之後,主機在下一個時鐘接收一位資料,判斷從機是否應答,資料0表示應答資料1表示非應答(主機在接收之前,需要釋放SDA)

和傳送應答類似,接收應答的操作也就是接收一個bit資料的操作。

image-20210908161026090

3. I2C資料幀

3.1 傳送一幀資料

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

  1. 傳送一個開始訊號
  2. 傳送從機地址(加上寫入標記,最後一位為0)
  3. 迴圈傳送位元組資料
  4. 傳送完後傳送結束訊號
image-20210908161143468

3.2 接收一幀資料

image-20210908161500947

過程和傳送一幀資料的過程非常類似,只是中間的傳送位元組資料變成了接收位元組資料,此外還有地址部分的最後一位為1,代表讀取資料。

3.3 先傳送後接收資料

image-20210908161722293

4. AT24C02資料幀

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

4.1 位元組寫

WORD ADDRESS處寫入資料DATA

image-20210908162104316

4.2 隨機讀

讀出在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;

1.1 傳送起始資訊和傳送終止資訊

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;
}

1.2 傳送位元組資訊和接收位元組資訊

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;
}

1.3 傳送應答和接收應答

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

2.1 位元組寫

前面已經提到,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();
}

2.2 隨機讀

讀取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呼叫一次按鍵驅動函式
    }
}

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

相關文章