天天看點

《嵌入式 – GD32開發實戰指南》第21章 I2C

開發環境:

MDK:Keil 5.30

開發闆:GD32F207I-EVAL

MCU:GD32F207IK

21.1 I2C工作原理

21.1.1 I2C串行總線概述

I2C總線是PHLIPS公司推出的一種雙線式半雙工串行總線,是具備多主機系統所需的總線裁決和高低速器件同步功能的高性能串行總線。用于連接配接微控器及外圍裝置。I2C總線隻有兩根雙向信号線。一根是資料線SDA,另一根是時鐘線SCL。

 實體層

1)它隻使用兩條總線線路 :一條雙向串行資料線(SDA),一條串行時鐘線(SCL)。見下圖。

2)每個連接配接到總線的裝置都有一個獨立的位址,主機可以利用這個位址進行不同裝置之間的通路。

3)多主機同時使用總線時,為了防止資料沖突,會利用仲裁方式決定由哪個裝置占用總線。

4)具有三種傳輸模式 :标準模式的傳輸速率為 100 Kbit/s ,快速模式為 400 Kbit/s ,高速模式下可達 3.4 Mbit/s,但目前大多12C裝置尚不支援高速模式。

5)片上的濾波器可以濾去總線資料線上的毛刺波以保證資料完整。

6)連接配接到相同總線的 IC 數量受到總線的最大電容 400 pF 限制

《嵌入式 – GD32開發實戰指南》第21章 I2C

I2C總線通過上拉電阻接正電源。當總線空閑時,兩根線均為高電平。連到總線上的任一器件輸出的低電平,都将使總線的信号變低,即各器件的SDA及SCL都是線“與”關系。

《嵌入式 – GD32開發實戰指南》第21章 I2C

每個接到I2C總線上的器件都有唯一的位址。主機與其它器件間的資料傳送可以是由主機發送資料到其它器件,這時主機即為發送器。由總線上接收資料的器件則為接收器。

在多主機系統中,可能同時有幾個主機企圖啟動總線傳送資料。為了避免混亂, I2C總線要通過總線仲裁,以決定由哪一台主機控制總線。

 協定層

I2C的協定包括起始和停止條件、資料有效性、響應、仲裁、時鐘同步和位址廣播等環節,由于我們使用的是 STM32 內建的硬體I2C接口,并不需要用軟體去模拟 SDA 和SCL 線的時序。

《嵌入式 – GD32開發實戰指南》第21章 I2C
《嵌入式 – GD32開發實戰指南》第21章 I2C

這兩幅圖表示的是主機和從機通信時 SDA 線的資料包序列。

其中 S 表示由主機的I2C接口産生的傳輸起始信号(S),這時連接配接到I2C總線上的所有從機都會接收到這個信号。

起始信号産生後,所有從機就開始等待主機緊接下來廣播的從機位址信号(SLAVE_ADDRESS),在I2C總線上,每個裝置的位址都是唯一的。當主機廣播的位址與某個裝置位址相同時,這個裝置就被選中了,沒被選中的裝置将會忽略之後的資料信号。根據I2C協定,這個從機位址可以是 7 位或 10 位。

在位址位之後,是傳輸方向的選擇位,該位為 0 時,表示後面的資料傳輸方向是由主機傳輸至從機。該位為 1 時,則相反。

從機接收到比對的位址後,主機或從機會傳回一個應答(A)或非應答信号,隻有接收到應答信号後,主機才能繼續發送或接收資料。

若配置的方向傳輸位為寫資料,廣播完位址,接收到應答信号後,主機開始正式向從機傳輸資料(DATA),資料包的大小為 8 位。主機每發送完一個資料,都要等待從機的應答信号(A),重複這個過程,可以向從機傳輸 N 個資料,這個 N 沒有大小限制。當資料傳輸結束時,主機向從機發送一個停止傳輸信号(P),表示不再傳輸資料。

若配置的方向傳輸位為讀資料,廣播完位址,接收到應答信号後,從機開始向主機傳回資料(DATA),資料包大小也為 8 位。從機每發送完一個資料,都會等待主機的應答信号(A),重複這個過程,可以傳回 N 個資料,這個 N 也沒有大小限制。當主機希望停止接收資料時,就向從機傳回一個非應答信号,則從機自動停止資料傳輸。

 I2C接口特性

1)STM32 的中等容量和大容量型号的晶片均有多達兩個的I2C總線接口。

2)能夠工作于多主模式或從模式,分别為主接收器、主發送器、從接收器及從發送器。

3)支援标準模式 100 Kbit/s 和快速模式 400 Kbit/s,不支援高速模式。

4)支援 7 位或 10 位尋址。

5)内置了硬體 CRC 發生器 / 校驗器。

6)I2 C 的接收和發送都可以使用 DMA 操作。

7)支援系統管理總線(SMBus)2.0 版。

 I 2 C 架構

I2C的所有硬體架構就是根據 SCL 線和 SDA 線展開的(其中 SMBALERT 線用于 SMBus)。SCL 線的時序即為I2C 協定中的時鐘信号,它由I2C 接口根據時鐘控制寄存器(CCR)控制,控制的參數主要為時鐘頻率。而 SDA 線的信号則通過一系列資料控制架構,在将要發送的資料的基礎上,根據協定添加各種起始信号、應答信号、位址信号,實作以 I 2 C 協定的方式發送出去。讀取資料時則從 SDA 線上的信号中取出接收到的資料值。發送和接收的資料都被儲存在資料寄存器(DR)上。

《嵌入式 – GD32開發實戰指南》第21章 I2C

21.1.2 I2C總線的資料傳送

 資料位的有效性規定

I2C總線進行資料傳送時,時鐘信号為高電平期間,資料線上的資料必須保持穩定,隻有在時鐘線上的信号為低電平期間,資料線上的高電平或低電平狀态才允許變化。

《嵌入式 – GD32開發實戰指南》第21章 I2C

 起始和終止信号

SCL線為高電平期間,SDA線由高電平向低電平的變化表示起始信号;SCL線為高電平期間,SDA線由低電平向高電平的變化表示終止信号。

《嵌入式 – GD32開發實戰指南》第21章 I2C

起始和終止信号都是由主機發出的,在起始信号産生後,總線就處于被占用的狀态;在終止信号産生後,總線就處于空閑狀态。連接配接到I2C總線上的器件,若具有I2C總線的硬體接口,則很容易檢測到起始和終止信号。每當發送器件傳輸完一個位元組的資料後,後面必須緊跟一個校驗位,這個校驗位是接收端通過控制SDA(資料線)來實作的,以提醒發送端資料我這邊已經接收完成,資料傳送可以繼續進行。

 資料傳送格式

 位元組傳送與應答

每一個位元組必須保證是8位長度。資料傳送時,先傳送最高位(MSB),每一個被傳送的位元組後面都必須跟随一位應答位(即一幀共有9位)。

《嵌入式 – GD32開發實戰指南》第21章 I2C

由于某種原因從機不對主機尋址信号應答時(如從機正在進行實時性的處理工作而無法接收總線上的資料),它必須将資料線置于高電平,而由主機産生一個終止信号以結束總線的資料傳送。

如果從機對主機進行了應答,但在資料傳送一段時間後無法繼續接收更多的資料時,從機可以通過對無法接收的第一個資料位元組的“非應答”通知主機,主機則應發出終止信号以結束資料的繼續傳送。

當主機接收資料時,它收到最後一個資料位元組後,必須向從機發出一個結束傳送的信号。這個信号是由對從機的“非應答”來實作的。然後,從機釋放SDA線,以允許主機産生終止信号。

 總線的尋址

I2C總線協定有明确的規定:采用7位的尋址位元組(尋址位元組是起始信号後的第一個位元組)。

《嵌入式 – GD32開發實戰指南》第21章 I2C

 尋址位元組的位定義

D7~D1位組成從機的位址。D0位是資料傳送方向位,為“0”時表示主機向從機寫資料,為“1”時表示主機由從機讀資料。

主機發送位址時,總線上的每個從機都将這7位位址碼與自己的位址進行比較,如果相同,則認為自己正被主機尋址,根據R/T位将自己确定為發送器或接收器。

從機的位址由固定部分和可程式設計部分組成。在一個系統中可能希望接入多個相同的從機,從機位址中可程式設計部分決定了可接入總線該類器件的最大數目。如一個從機的7位尋址位有4位是固定位,3位是可程式設計位,這時僅能尋址8個同樣的器件,即可以有8個同樣的器件接入到該I2C總線系統中。

 資料幀格式

I2C總線上傳送的資料信号是廣義的,既包括位址信号,又包括真正的資料信号。

在起始信号後必須傳送一個從機的位址(7位),第8位是資料的傳送方向位(R/T),用“0”表示主機發送資料(T),“1”表示主機接收資料(R)。每次資料傳送總是由主機産生的終止信号結束。但是,若主機希望繼續占用總線進行新的資料傳送,則可以不産生終止信号,馬上再次發出起始信号對另一從機進行尋址。

在總線的一次資料傳送過程中,可以有以下幾種組合方式:

A)主機向從機發送資料,資料傳送方向在整個過程中不變;

《嵌入式 – GD32開發實戰指南》第21章 I2C

注:有陰影部分表示資料由主機向從機傳送,無陰影部分則表示資料由從機向主機傳送。A表示應答, A表示非應答(高電平)。S表示起始信号,P表示終止信号。

B)主機在第一個位元組後,立即從從機讀資料。

《嵌入式 – GD32開發實戰指南》第21章 I2C

C)在傳送過程中,當需要改變傳送方向時,起始信号和從機位址都被重複産生一次,但兩次讀/寫方向位正好反相。

《嵌入式 – GD32開發實戰指南》第21章 I2C
《嵌入式 – GD32開發實戰指南》第21章 I2C

要想了解對I2C的主從模式詳細了解,參看STM32F10xxx參考手冊的I2C接口章節。

21.2 AT24Cxx存儲器原理

21.2.1 AT24Cxx概述

AT24C01/02/04/08/16是一個1K/2K/4K/8K/16K位串行CMOS,EEPROM内部含有128/256/512/1024/2048個8位位元組CATALYST公司的先進CMOS技術實質上減少了器件的功耗,AT24C01/02有一個8位元組頁寫緩沖器AT24C04/08/16有一個16位元組頁寫緩沖器,該器件通過I2C總線接口進行操作有一個專門的寫保護功能。AT24C01/02每頁有8個位元組,分别為16/32頁;AT24C04/08/16每頁有16個位元組,分别為32/64/128頁。

工作特點

 與400KHz I2C總線相容

 1.8到6.0伏工作電壓範圍

 低功耗CMOS技術

 寫保護功能當WP為高電平時進入寫保護狀态

 頁寫緩沖器

 自定時擦寫周期

 100萬次程式設計/擦除周期

 可儲存資料100年

 8腳DIP SOIC或TSSOP封裝

 溫度範圍商業級和工業級

AT24Cxx的引腳定義如下:

《嵌入式 – GD32開發實戰指南》第21章 I2C

Note: For use of 5-lead SOT23, the software A2, A1, and A0 bits in the device address word must be set to zero toproperly communicate.

《嵌入式 – GD32開發實戰指南》第21章 I2C

21.2.2總線時序

I2C總線時序如下:

《嵌入式 – GD32開發實戰指南》第21章 I2C

其讀寫周期的的電壓範圍如下:

《嵌入式 – GD32開發實戰指南》第21章 I2C

寫周期時間是指從一個寫時序的有效停止信号到内部程式設計/擦除周期結束的這一段時間。在寫周期期間,總線接口電路禁能,SDA保持為高電平,器件不響應外部操作。

21.2.3器件尋址

主器件通過發送一個起始信号啟動發送過程,然後發送它所要尋址的從器件的位址。8位從器件位址的高4位固定為(1010)。接下來的3位(A2、A1、A0)為器件的位址位,用來定義哪個器件以及器件的哪個部分被主器件通路,上述8個AT24C01/02,4個AT24C04,2個AT24C08,1個AT24C16可單獨被系統尋址。從器件8位位址的最低位,作為讀寫控制位。“1”表示對從器件進行讀操作,“0”表示對從器件進行寫操作。在主器件發送起始信号和從器件位址位元組後,AT24C01/02/04/08/16監視總線并當其位址與發送的從位址相符時響應一個應答信号(通過SDA線)。AT24C01/02/04/08/16再根據讀寫控制位(R/W)的狀态進行讀或寫操作。

《嵌入式 – GD32開發實戰指南》第21章 I2C

 位元組寫

在位元組寫模式下,主器件發送起始指令和從器件位址資訊(R/W)位置發給從器件,在從器件産生應答信号後,主器件發送AT24Cxx的位元組位址,主器件在收到從器件的另一個應答信号後,再發送資料到被尋址的存儲單元。AT24Cxx再次應答,并在主器件産生停止信号後開始内部資料的擦寫,在内部擦寫過程中,AT24Cxx不再應答主器件的任何請求。

《嵌入式 – GD32開發實戰指南》第21章 I2C

 頁寫

用頁寫,AT24C01/02可一次寫入8 個位元組資料,AT24C04/08/16可以一次寫入16個位元組的資料。頁寫操作的啟動和位元組寫一樣,不同在于傳送了一位元組資料後并不産生停止信号。主器件被允許發送P(AT24C01:P=7;AT24C02/04/08/16:P=15)個額外的位元組。每發送一個位元組資料後AT24Cxx産生一個應答位并将位元組位址低位加1,高位保持不變。

如果在發送停止信号之前主器件發送超過P+1個位元組,位址計數器将自動翻轉,先前寫入的資料被覆寫。

接收到P+1位元組資料和主器件發送的停止信号後,AT24Cxx啟動内部寫周期将資料寫到資料區。所有接收的資料在一個寫周期内寫入AT24Cxx。

《嵌入式 – GD32開發實戰指南》第21章 I2C

 讀位元組

讀操作允許主器件對寄存器的任意位元組進行讀操作,主器件首先通過發送起始信号、從器件位址和它想讀取的位元組資料的位址執行一個寫操作。在AT24Cxx應答之後,主器件重新發送起始信号和從器件位址,此時R/W位置1,AT24Cxx響應并發送應答信号,然後輸出所要求的一個8位位元組資料,主器件不發送應答信号但産生一個停止信号。

《嵌入式 – GD32開發實戰指南》第21章 I2C

 順序讀

在AT24Cxx發送完一個8位位元組資料後,主器件産生一個應答信号來響應,告知AT24Cxx主器件要求更多的資料,對應每個主機産生的應答信号AT24Cxx将發送一個8位資料位元組。當主器件不發送應答信号而發送停止位時結束此操作。

從AT24Cxx輸出的資料按順序由N到N+1輸出。讀操作時位址計數器在AT24Cxx整個位址内增加,這樣整個寄存器區域可在一個讀操作内全部讀出,當讀取的位元組超過E(對于24WC01,E=127;對24C02,E=255;對24C04,E=511;對24C08,E=1023;對24C16,E=2047)計數器将翻轉到零并繼續輸出資料位元組。

《嵌入式 – GD32開發實戰指南》第21章 I2C

 典型應用

ATC02的典型電路如下:

《嵌入式 – GD32開發實戰指南》第21章 I2C

根據AT24C02的晶片資料,我們會發現AT24C02有三個位址A0,A1,A2。同時,我們會在資料的Device Address介紹發現I2C器件一共有七位位址碼,還有一位是讀/寫(R/W)操作位,而在AT24C02的前四位已經固定為1010。R/W為1則為 讀操作,為0則為寫操作。R/W位我們要設定為0(寫操作)。

規則為:1010(A0)(A1)(A2)(R/W)

例子1:

那麼對應的A0,A1,A2都是接的VCC,是以為A0=1,A1=1,A2=1;可以知道AT24C02的從裝置寫位址為10101110(0xae),讀裝置位址為10101111(0xaf)。

例子2:

那麼對應的A0,A1,A2都是接的GND,是以為A0=0,A1=0,A2=0;可以知道AT24C02的從裝置寫位址為10100000(0xa0),讀裝置位址為10100001(0xa1)。

21.3 I2C寄存器描述

I2C有6類寄存器,詳細的介紹請參考GD32F2XXX參考手冊的I2C寄存器描述部分。在這裡筆者隻講最重要的2個寄存器。

 資料寄存器

資料寄存器的較長的描述如下所示。

《嵌入式 – GD32開發實戰指南》第21章 I2C

 時鐘寄存器

時鐘寄存器是I2C中比較重要的一個寄存器,時鐘信号的信号的穩定是I2C正常工作的前提。

《嵌入式 – GD32開發實戰指南》第21章 I2C

21.4硬體設計及連接配接

本文是使用I2C協定對EEPROM進行讀寫操作,具體的硬體連接配接如下。

《嵌入式 – GD32開發實戰指南》第21章 I2C

從硬體連結可以得到AT24C02的位址是0xA0,I2C的接口是I2C0。

21.5硬體I2C

21.5.1具體代碼實作

首先看看I2C的初始化。這有兩部分。

一部分是I2C的GPIO初始化。

/*
    brief      configure the GPIO ports
    param[in]  i2c_typedef_enum i2c_id
    param[out] none
    retval     none
*/
void i2c_gpio_config(i2c_typedef_enum i2c_id)
{
    /* enable GPIO clock */
    rcu_periph_clock_enable(I2C_BUS_SCL_GPIO_CLK[i2c_id]);
    rcu_periph_clock_enable(I2C_BUS_SDA_GPIO_CLK[i2c_id]);

    /* Config I2C_SCL */
    gpio_init(I2C_BUS_SCL_GPIO_PORT[i2c_id], GPIO_MODE_AF_OD, GPIO_OSPEED_50MHZ, I2C_BUS_SCL_PIN[i2c_id]);

    /* Config I2C_SDA */
    gpio_init(I2C_BUS_SDA_GPIO_PORT[i2c_id], GPIO_MODE_AF_OD, GPIO_OSPEED_50MHZ, I2C_BUS_SDA_PIN[i2c_id]);
}

/*
    brief      configure the I2C0 interfaces
    param[in]  i2c_typedef_enum i2c_id
    param[out] none
    retval     none
*/
void i2c_mode_config(i2c_typedef_enum i2c_id)
{
    /* enable I2C clock */
    rcu_periph_clock_enable(I2C_BUS_CLK[i2c_id]);
    /* configure I2C clock */
    i2c_clock_config(I2C_BUS[i2c_id], I2C_SPEED, I2C_DTCY_2);
    /* configure I2C address */
    i2c_mode_addr_config(I2C_BUS[i2c_id], I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, I2C_OWN_ADDRESS7);
    /* enable I2C0 */
    i2c_enable(I2C_BUS[i2c_id]);
    /* enable acknowledge */
    i2c_ack_config(I2C_BUS[i2c_id], I2C_ACK_ENABLE);
}      

主要配置I2C模式、低電平占空比、I2C尋址模式以及通信速率,最後使能I2C裝置。

初始化完成後就是對AT24C02的讀寫操作,嚴格按照相應的時序操作就行。

 位元組寫

在位元組寫模式下,向AT24C02中寫資料時序如下:

《嵌入式 – GD32開發實戰指南》第21章 I2C

操作時序如下:

1.MCU先發送一個開始信号(START)啟動總線

2.接着跟上首位元組,發送器件寫操作位址(DEVICE ADDRESS)+寫資料(0xA0)

3.等待應答信号(ACK)

4.發送資料的存儲位址。24C02一共有256個位元組的存儲空間,位址從0x00~0xFF,想把資料存儲在哪個位置,此刻寫的就是哪個位址。

5.發送要存儲的資料,在寫資料的過程中,AT24C02會回應一個“應答位0”,則表明寫AT24C02資料成功,如果沒有回應答位,說明寫入不成功。

6.發送結束信号(STOP)停止總線。

代碼很簡單,跟着時序來就行。

/*
    brief      write one byte to the I2C EEPROM
    param[in]  i2c_typedef_enum i2c_id
    p_buffer: pointer to the buffer containing the data to be written to the EEPROM
    param[in]  write_address: EEPROM's internal address to write to
    param[out] none
    retval     none
*/
void eeprom_byte_write(i2c_typedef_enum i2c_id, uint8_t *p_buffer, uint8_t write_address)
{
    /* wait until I2C bus is idle */
    while(i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_I2CBSY));

    /* send a start condition to I2C bus */
    i2c_start_on_bus(I2C_BUS[i2c_id]);

    /* wait until SBSEND bit is set */
    while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_SBSEND));

    /* send slave address to I2C bus */
    i2c_master_addressing(I2C_BUS[i2c_id], eeprom_address, I2C_TRANSMITTER);

    /* wait until ADDSEND bit is set */
    while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_ADDSEND));

    /* clear the ADDSEND bit */
    i2c_flag_clear(I2C_BUS[i2c_id], I2C_FLAG_ADDSEND);

    /* wait until the transmit data buffer is empty */
    while(SET != i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_TBE));

    /* send the EEPROM's internal address to write to : only one byte address */
    i2c_data_transmit(I2C_BUS[i2c_id], write_address);

    /* wait until BTC bit is set */
    while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_BTC));

    /* send the byte to be written */
    i2c_data_transmit(I2C_BUS[i2c_id], *p_buffer);

    /* wait until BTC bit is set */
    while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_BTC));

    /* send a stop condition to I2C bus */
    i2c_stop_on_bus(I2C_BUS[i2c_id]);

    /* wait until the stop condition is finished */
    while(I2C_CTL0(I2C_BUS[i2c_id]) & 0x0200);
}      

 頁寫

用頁寫,AT24C01可一次寫入8 個位元組資料,AT24C02/04/08/16可以一次寫入16個位元組的資料。頁寫操作的啟動和位元組寫一樣,不同在于傳送了一位元組資料後并不産生停止信号。每發送一個位元組資料後AT24Cxx産生一個應答位并将位元組位址低位加1,高位保持不變。

如果在發送停止信号之前主器件發送超過P+1個位元組,位址計數器将自動翻轉,先前寫入的資料被覆寫。

接收到P+1位元組資料和主器件發送的停止信号後,AT24Cxx啟動内部寫周期将資料寫到資料區。所有接收的資料在一個寫周期内寫入AT24Cxx。

《嵌入式 – GD32開發實戰指南》第21章 I2C

代碼很簡單,和位元組寫不同的是,資料會一直發,直到主機發送停止信号。

/*
    brief      write more than one byte to the EEPROM with a single write cycle
    param[in]  i2c_typedef_enum i2c_id
p_buffer: pointer to the buffer containing the data to be written to the EEPROM
    param[in]  write_address: EEPROM's internal address to write to
    param[in]  number_of_byte: number of bytes to write to the EEPROM
    param[out] none
    retval     none
*/
void eeprom_page_write(i2c_typedef_enum i2c_id, uint8_t *p_buffer, uint8_t write_address, uint8_t number_of_byte)
{
    /* wait until I2C bus is idle */
    while(i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_I2CBSY));

    /* send a start condition to I2C bus */
    i2c_start_on_bus(I2C_BUS[i2c_id]);

    /* wait until SBSEND bit is set */
    while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_SBSEND));

    /* send slave address to I2C bus */
    i2c_master_addressing(I2C_BUS[i2c_id], eeprom_address, I2C_TRANSMITTER);

    /* wait until ADDSEND bit is set */
    while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_ADDSEND));

    /* clear the ADDSEND bit */
    i2c_flag_clear(I2C_BUS[i2c_id], I2C_FLAG_ADDSEND);

    /* wait until the transmit data buffer is empty */
    while(SET != i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_TBE));

    /* send the EEPROM's internal address to write to : only one byte address */
    i2c_data_transmit(I2C_BUS[i2c_id], write_address);

    /* wait until BTC bit is set */
    while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_BTC));

    /* while there is data to be written */
    while(number_of_byte--)
    {
        i2c_data_transmit(I2C_BUS[i2c_id], *p_buffer);

        /* point to the next byte to be written */
        p_buffer++;

        /* wait until BTC bit is set */
        while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_BTC));
    }
    /* send a stop condition to I2C bus */
    i2c_stop_on_bus(I2C_BUS[i2c_id]);

    /* wait until the stop condition is finished */
    while(I2C_CTL0(I2C_BUS[i2c_id]) & 0x0200);
}      

 任意寫

在實際過程中,我們經常需要任意寫資料,這裡就調用頁寫的操作,來實作任意位元組的寫操作。

/*
    brief      write buffer of data to the I2C EEPROM
    param[in]  i2c_typedef_enum i2c_id
    p_buffer: pointer to the buffer  containing the data to be written to the EEPROM
    param[in]  write_address: EEPROM's internal address to write to
    param[in]  number_of_byte: number of bytes to write to the EEPROM
    param[out] none
    retval     none
*/
void eeprom_buffer_write(i2c_typedef_enum i2c_id, uint8_t *p_buffer, uint8_t write_address, uint16_t number_of_byte)
{
    uint8_t number_of_page = 0, number_of_single = 0, address = 0, count = 0;

    address = write_address % I2C_PAGE_SIZE;
    count = I2C_PAGE_SIZE - address;
    number_of_page =  number_of_byte / I2C_PAGE_SIZE;
    number_of_single = number_of_byte % I2C_PAGE_SIZE;

    /* if write_address is I2C_PAGE_SIZE aligned  */
    if(0 == address)
    {
        while(number_of_page--)
        {
            eeprom_page_write(i2c_id, p_buffer, write_address, I2C_PAGE_SIZE);
            eeprom_wait_standby_state(i2c_id);
            write_address +=  I2C_PAGE_SIZE;
            p_buffer += I2C_PAGE_SIZE;
        }
        if(0 != number_of_single)
        {
            eeprom_page_write(i2c_id, p_buffer, write_address, number_of_single);
            eeprom_wait_standby_state(i2c_id);
        }
    }
    else
    {
        /* if write_address is not I2C_PAGE_SIZE aligned */
        if(number_of_byte < count)
        {
            eeprom_page_write(i2c_id, p_buffer, write_address, number_of_byte);
            eeprom_wait_standby_state(i2c_id);
        }
        else
        {
            number_of_byte -= count;
            number_of_page =  number_of_byte / I2C_PAGE_SIZE;
            number_of_single = number_of_byte % I2C_PAGE_SIZE;

            if(0 != count)
            {
                eeprom_page_write(i2c_id, p_buffer, write_address, count);
                eeprom_wait_standby_state(i2c_id);
                write_address += count;
                p_buffer += count;
            }
            /* write page */
            while(number_of_page--)
            {
                eeprom_page_write(i2c_id, p_buffer, write_address, I2C_PAGE_SIZE);
                eeprom_wait_standby_state(i2c_id);
                write_address +=  I2C_PAGE_SIZE;
                p_buffer += I2C_PAGE_SIZE;
            }
            /* write single */
            if(0 != number_of_single)
            {
                eeprom_page_write(i2c_id, p_buffer, write_address, number_of_single);
                eeprom_wait_standby_state(i2c_id);
            }
        }
    }
}      

主要分為兩種情況,寫的位址正好是一頁的開始,另外一種是在一頁的中間。不管如何,始終遵循的原則就是最大智能寫一頁,可以從一頁的中間開始。

 讀位元組

讀操作允許主器件對寄存器的任意位元組進行讀操作,主器件首先通過發送起始信号、從器件位址和它想讀取的位元組資料的位址執行一個寫操作。在AT24Cxx應答之後,主器件重新發送起始信号和從器件位址,此時R/W位置1,AT24Cxx響應并發送應答信号,然後輸出所要求的一個8位位元組資料,主器件不發送應答信号但産生一個停止信号。

《嵌入式 – GD32開發實戰指南》第21章 I2C

讀取位元組的時序如下:

1.MCU先發送一個開始信号(START)啟動總線

2.接着跟上首位元組,發送器件寫操作位址(DEVICE ADDRESS)+寫資料(0xA0)

注意:這裡寫操作是為了要把所要讀的資料的存儲位址先寫進去,告訴AT24Cxx要讀取哪個位址的資料。

3.發送要讀取記憶體的位址(WORD ADDRESS),通知AT24Cxx讀取要哪個位址的資訊。

4.重新發送開始信号(START)。

5.發送裝置讀操作位址(DEVICE ADDRESS)對AT24Cxx進行讀操作 (0xA1)。

6.AT24Cxx會自動向主機發送資料,主機讀取從器件發回的資料,在讀一個位元組後,MCU會回應一個應答信号(ACK)。

7.發送一個“非應答位NAK(1)”。發送結束信号(STOP)停止總線。

 順序讀

在AT24Cxx發送完一個8位位元組資料後,主器件産生一個應答信号來響應,告知AT24Cxx主器件要求更多的資料,對應每個主機産生的應答信号AT24Cxx将發送一個8位資料位元組。當主器件不發送應答信号而發送停止位時結束此操作。

從AT24Cxx輸出的資料按順序由N到N+1輸出。讀操作時位址計數器在AT24Cxx整個位址内增加,這樣整個寄存器區域可在一個讀操作内全部讀出,當讀取的位元組超過E(對于24WC01,E=127;對24C02,E=255;對24C04,E=511;對24C08,E=1023;對24C16,E=2047)計數器将翻轉到零并繼續輸出資料位元組。

《嵌入式 – GD32開發實戰指南》第21章 I2C

我們常用的方式就是連續讀取,代碼很簡單。

/*
    brief      read data from the EEPROM
    param[in]  i2c_typedef_enum i2c_id
p_buffer: pointer to the buffer that receives the data read from the EEPROM
    param[in]  read_address: EEPROM's internal address to start reading from
    param[in]  number_of_byte: number of bytes to reads from the EEPROM
    param[out] none
    retval     none
*/
void eeprom_buffer_read(i2c_typedef_enum i2c_id, uint8_t *p_buffer, uint8_t read_address, uint16_t number_of_byte)
{
    /* wait until I2C bus is idle */
    while(i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_I2CBSY));

    if(2 == number_of_byte)
    {
        i2c_ackpos_config(I2C_BUS[i2c_id], I2C_ACKPOS_NEXT);
    }

    /* send a start condition to I2C bus */
    i2c_start_on_bus(I2C_BUS[i2c_id]);

    /* wait until SBSEND bit is set */
    while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_SBSEND));

    /* send slave address to I2C bus */
    i2c_master_addressing(I2C_BUS[i2c_id], eeprom_address, I2C_TRANSMITTER);

    /* wait until ADDSEND bit is set */
    while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_ADDSEND));

    /* clear the ADDSEND bit */
    i2c_flag_clear(I2C_BUS[i2c_id], I2C_FLAG_ADDSEND);

    /* wait until the transmit data buffer is empty */
    while(SET != i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_TBE));

    /* enable I2C*/
    i2c_enable(I2C_BUS[i2c_id]);

    /* send the EEPROM's internal address to write to */
    i2c_data_transmit(I2C_BUS[i2c_id], read_address);

    /* wait until BTC bit is set */
    while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_BTC));

    /* send a start condition to I2C bus */
    i2c_start_on_bus(I2C_BUS[i2c_id]);

    /* wait until SBSEND bit is set */
    while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_SBSEND));

    /* send slave address to I2C bus */
    i2c_master_addressing(I2C_BUS[i2c_id], eeprom_address, I2C_RECEIVER);

    if(number_of_byte < 3)
    {
        /* disable acknowledge */
        i2c_ack_config(I2C_BUS[i2c_id], I2C_ACK_DISABLE);
    }

    /* wait until ADDSEND bit is set */
    while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_ADDSEND));

    /* clear the ADDSEND bit */
    i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);

    if(1 == number_of_byte)
    {
        /* send a stop condition to I2C bus */
        i2c_stop_on_bus(I2C_BUS[i2c_id]);
    }

    /* while there is data to be read */
    while(number_of_byte)
    {
        if(3 == number_of_byte)
        {
            /* wait until BTC bit is set */
            while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_BTC));

            /* disable acknowledge */
            i2c_ack_config(I2C_BUS[i2c_id], I2C_ACK_DISABLE);
        }
        if(2 == number_of_byte)
        {
            /* wait until BTC bit is set */
            while(!i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_BTC));

            /* send a stop condition to I2C bus */
            i2c_stop_on_bus(I2C_BUS[i2c_id]);
        }

        /* wait until the RBNE bit is set and clear it */
        if(i2c_flag_get(I2C_BUS[i2c_id], I2C_FLAG_RBNE))
        {
            /* read a byte from the EEPROM */
            *p_buffer = i2c_data_receive(I2C_BUS[i2c_id]);

            /* point to the next location where the byte read will be saved */
            p_buffer++;

            /* decrement the read bytes counter */
            number_of_byte--;
        }
    }

    /* wait until the stop condition is finished */
    while(I2C_CTL0(I2C_BUS[i2c_id]) & 0x0200);

    /* enable acknowledge */
    i2c_ack_config(I2C_BUS[i2c_id], I2C_ACK_ENABLE);
    i2c_ackpos_config(I2C_BUS[i2c_id], I2C_ACKPOS_CURRENT);
}      

最後看下主函數吧。

/*
    brief      main function
    param[in]  none
    param[out] none
    retval     none
*/
int main(void)
{
    //systick init
    sysTick_init();

    // led init
    led_init(LED1);

    //usart init 115200 8-N-1
    com_init(COM1, 115200, 0, 1);

    // DMA config
    usart0_dma_init();

    /* USART DMA 發送使能 */
    usart_dma_enable(USART0, USART_DMA_TRANSMIT);
    /* USART DMA接收使能 */
    usart_dma_enable(USART0, USART_DMA_RECEIVE);
    /* initialize EEPROM  */
    I2C_EE_Init(IIC0);
    i2c_24c02_test(IIC0);

    while(1)
    {
        led_toggle(LED1);
        delay_ms(1000);
    }
}      

很簡單,往AT24C02中寫入資料,然後再讀取資料,讀寫測試的函數如下:

/*
    brief      I2C read and write functions
    param[in]  i2c_typedef_enum i2c_id
    param[out] none
    retval     I2C_OK or I2C_FAIL
*/
uint8_t i2c_24c02_test(i2c_typedef_enum i2c_id)
{
    uint16_t i;
    uint8_t i2c_buffer_write[BUFFER_SIZE];
    uint8_t i2c_buffer_read[BUFFER_SIZE];

    printf("\r\nAT24C02 writing...\r\n");

    /* initialize i2c_buffer_write */
    for(i = 0; i < BUFFER_SIZE; i++)
    {
        i2c_buffer_write[i] = i;
        printf("0x%02X ", i2c_buffer_write[i]);
        if(15 == i % 16)
        {
            printf("\r\n");
        }
    }
    /* EEPROM data write */
    eeprom_buffer_write(i2c_id, i2c_buffer_write, EEP_FIRST_PAGE, BUFFER_SIZE);
    printf("AT24C02 reading...\r\n");
    /* EEPROM data read */
    eeprom_buffer_read(i2c_id, i2c_buffer_read, EEP_FIRST_PAGE, BUFFER_SIZE);
    /* compare the read buffer and write buffer */
    for(i = 0; i < BUFFER_SIZE; i++)
    {
        if(i2c_buffer_read[i] != i2c_buffer_write[i])
        {
            printf("0x%02X ", i2c_buffer_read[i]);
            printf("Err:data read and write aren't matching.\n\r");
            return I2C_FAIL;
        }
        printf("0x%02X ", i2c_buffer_read[i]);
        if(15 == i % 16)
        {
            printf("\r\n");
        }
    }
    printf("I2C-AT24C02 test passed!\n\r");
    return I2C_OK;
}      

當然在讀寫測試之前應該對AT24C02進行初始化操作。

/**
  * @brief  I2C 外設(EEPROM)初始化
  * @param  i2c_typedef_enum i2c_id
  * @retval 無
  */
void I2C_EE_Init(i2c_typedef_enum i2c_id)
{
    /* 選擇EEPROM要寫入的位址 */
#ifdef EEPROM_BLOCK0_ADDRESS
    /* 選擇 EEPROM Block0 來寫入 */
    eeprom_address = EEPROM_BLOCK0_ADDRESS;
#endif

#ifdef EEPROM_BLOCK1_ADDRESS  
    /* 選擇 EEPROM Block1 來寫入 */
    eeprom_address = EEPROM_BLOCK2_ADDRESS;
#endif

#ifdef EEPROM_BLOCK2_ADDRESS  
    /* 選擇 EEPROM Block2 來寫入 */
    eeprom_address = EEPROM_BLOCK2_ADDRESS;
#endif

#ifdef EEPROM_BLOCK3_ADDRESS  
    /* 選擇 EEPROM Block3 來寫入 */
    eeprom_address = EEPROM_BLOCK3_ADDRESS;
#endif
    i2c_gpio_config(i2c_id); 
 
    i2c_mode_config(i2c_id);
}      

21.5.2實驗現象

下載下傳好程式後,打開序列槽助手,可以看到如下資訊。

《嵌入式 – GD32開發實戰指南》第21章 I2C

最後,我們使用邏輯分析來檢視資料。

《嵌入式 – GD32開發實戰指南》第21章 I2C

使用的400kHz的速率,可以看到資料的寫操作和前面分析的時序是一樣的,完全吻合。

21.6軟體I2C

21.6.1具體代碼實作

首先實作I2C的協定。

/**
  * @brief  I2C_Delay, I2C總線位延遲,最快400KHz
  * @param  None
  * @retval None
  */
static void I2C_Delay(void)
{
    uint8_t i;
    //I2C_Delay_us(2);
    for (i = 0; i < 15; i++);
}

/**
  * @brief  I2C_Start, CPU發起I2C總線啟動信号
  * @param  None
  * @retval None
  */
void I2C_Start(void)
{
    I2C_SDA_OUT();
    /* 當SCL高電平時,SDA出現一個下跳沿表示I2C總線啟動信号 */
    I2C_SDA_1();
    I2C_SCL_1();
    I2C_Delay();
    I2C_SDA_0();//START:when CLK is high,DATA change form high to low 
    I2C_Delay();
    I2C_SCL_0();
    I2C_Delay();
}

/**
  * @brief  I2C_Stop, CPU發起I2C總線停止信号
  * @param  None
  * @retval None
  */
void I2C_Stop(void)
{
    I2C_SDA_OUT();
    /* 當SCL高電平時,SDA出現一個上跳沿表示I2C總線停止信号 */
    I2C_SDA_0();//STOP:when CLK is high DATA change form low to high
    I2C_SCL_1();
    I2C_Delay();
    I2C_SDA_1();
}

/**
  * @brief  I2C_SendByte, CPU向I2C總線裝置發送8bit資料
  * @param  ucByte : 等待發送的位元組
  * @retval None
  */
void I2C_SendByte(uint8_t ucByte)
{
    uint8_t i;
    I2C_SDA_OUT();
    /* 先發送位元組的高位bit7 */
    for (i = 0; i < 8; i++)
    {    
        if (ucByte & 0x80)
        {
            I2C_SDA_1();
        }
        else
        {
            I2C_SDA_0();
        }
        I2C_Delay();
        I2C_SCL_1();
        I2C_Delay();  
        I2C_SCL_0();
        if (i == 7)
        {
            I2C_SDA_1(); // 釋放總線
        }
        ucByte <<= 1;  /* 左移一個bit */
        I2C_Delay();
    }
}


/**
  * @brief  I2C_ReadByte, CPU從I2C總線裝置讀取8bit資料
  * @param  None
  * @retval 讀到的資料
  */
uint8_t I2C_ReadByte(void)
{
    uint8_t i;
    uint8_t value;

    I2C_SDA_IN();
    /* 讀到第1個bit為資料的bit7 */
    value = 0;
    for (i = 0; i < 8; i++)
    {
        value <<= 1;
        I2C_SCL_1();
        I2C_Delay();
        if (I2C_SDA_READ())
        {
            value |= 1;
        }
        I2C_SCL_0();
        I2C_Delay();
    }
    return value;
}

/**
  * @brief  I2C_WaitAck, CPU産生一個時鐘,并讀取器件的ACK應答信号
  * @param  None
  * @retval 傳回0表示正确應答,1表示無器件響應
  */
uint8_t I2C_WaitAck(void)
{
    uint8_t re;
    I2C_SDA_IN();
    I2C_SDA_1();  /* CPU釋放SDA總線 */
    I2C_Delay();
    I2C_SCL_1();  /* CPU驅動SCL = 1, 此時器件會傳回ACK應答 */
    I2C_Delay();
    if (I2C_SDA_READ())  /* CPU讀取SDA口線狀态 */
    {
        re = 1;
    }
    else
    {
        re = 0;
    }
    I2C_SCL_0();
    I2C_Delay();
    return re;
}

/**
  * @brief  I2C_Ack, CPU産生一個ACK信号
  * @param  None
  * @retval None
  */
void I2C_Ack(void)
{
    I2C_SDA_OUT();
    I2C_SDA_0();  /* CPU驅動SDA = 0 */
    I2C_Delay();
    I2C_SCL_1();  /* CPU産生1個時鐘 */
    I2C_Delay();
    I2C_SCL_0();
    I2C_Delay();
    I2C_SDA_1();  /* CPU釋放SDA總線 */
}

/**
  * @brief  iI2C_NAck, CPU産生1個NACK信号
  * @param  None
  * @retval None
  */
void I2C_NAck(void)
{
    I2C_SDA_OUT();
    I2C_SDA_1();  /* CPU驅動SDA = 1 */
    I2C_Delay();
    I2C_SCL_1();  /* CPU産生1個時鐘 */
    I2C_Delay();
    I2C_SCL_0();
    I2C_Delay();  
}

/**
  * @brief  I2C_Cfg_GPIO, 配置I2C總線的GPIO,采用模拟IO的方式實作
  * @param  None
  * @retval None
  */
static void I2C_Cfg_GPIO(void)
{
    /* enable the led clock */
    rcu_periph_clock_enable(I2C_RCC_CLK);

    /* configure GPIO port */
  /* 配置 IIC_SCL 引腳為推挽輸出 */ 
    gpio_init(I2C_GPIO_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, I2C_SCL_PIN);
  /* 配置 IIC_SDA 引腳為推挽輸出 */ 
    gpio_init(I2C_GPIO_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, I2C_SDA_PIN);

    //gpio_bit_set(I2C_GPIO_PORT, I2C_SCL_PIN);
  //gpio_bit_set(I2C_GPIO_PORT, I2C_SDA_PIN);

    /* 給一個停止信号, 複位I2C總線上的所有裝置到待機模式 */
    I2C_Stop();
}

/**
  * @brief  i2c_CheckDevice, 檢測I2C總線裝置,CPU向發送裝置位址,然後讀取裝置應答來判斷該裝置是否存在
  * @param address:裝置的I2C總線位址
  * @retval 傳回值 0 表示正确, 傳回1表示未探測到
  */
uint8_t I2C_CheckDevice(uint8_t address)
{
    uint8_t ucAck;

    I2C_Cfg_GPIO();    /* 配置GPIO */

    I2C_Start();    /* 發送啟動信号 */

    /* 發送裝置位址+讀寫控制bit(0 = w, 1 = r) bit7 先傳 */
    I2C_SendByte(address | I2C_WR);
    ucAck = I2C_WaitAck();  /* 檢測裝置的ACK應答 */

    I2C_Stop();      /* 發送停止信号 */

    return ucAck;
}      

注釋很清楚,對照I2C的協定看就行。

接着就是實作AT2C02的讀寫操作。

/**
  * @brief  EEPROM_CheckOk, 判斷串行EERPOM是否正常
  * @param  None
  * @retval 1 表示正常, 0 表示不正常
  */
uint8_t EEPROM_CheckOk(void)
{
    if (I2C_CheckDevice(EEPROM_DEV_ADDR) == 0)
    {
        return 1;
    }
    else
    {
        /* 失敗後,切記發送I2C總線停止信号 */
        I2C_Stop();    
        return 0;
    }
}

/**
  * @brief  EEPROM_ReadBytes, 從串行EEPROM指定位址處開始讀取若幹資料
  * @param  _usAddress : 起始位址
  *         _usSize : 資料長度,機關為位元組
  *         _pReadBuf : 存放讀到的資料的緩沖區指針
  * @retval 0 表示失敗,1表示成功
  */
uint8_t EEPROM_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize)
{
  uint16_t i;
  
  /* 采用串行EEPROM随即讀取指令序列,連續讀取若幹位元組 */
  
  /* 第1步:發起I2C總線啟動信号 */
  I2C_Start();
  
  /* 第2步:發起控制位元組,高7bit是位址,bit0是讀寫控制位,0表示寫,1表示讀 */
  I2C_SendByte(EEPROM_DEV_ADDR | I2C_WR);  /* 此處是寫指令 */
  
  /* 第3步:發送ACK */
  if (I2C_WaitAck() != 0)
  {
    goto cmd_fail;  /* EEPROM器件無應答 */
  }

  /* 第4步:發送位元組位址,24C02隻有256位元組,是以1個位元組就夠了,如果是24C04以上,那麼此處需要連發多個位址 */
  I2C_SendByte((uint8_t)_usAddress);
  
  /* 第5步:發送ACK */
  if (I2C_WaitAck() != 0)
  {
    goto cmd_fail;  /* EEPROM器件無應答 */
  }
  
  /* 第6步:重新啟動I2C總線。前面的代碼的目的向EEPROM傳送位址,下面開始讀取資料 */
  I2C_Start();
  
  /* 第7步:發起控制位元組,高7bit是位址,bit0是讀寫控制位,0表示寫,1表示讀 */
  I2C_SendByte(EEPROM_DEV_ADDR | I2C_RD);  /* 此處是讀指令 */
  
  /* 第8步:發送ACK */
  if (I2C_WaitAck() != 0)
  {
    goto cmd_fail;  /* EEPROM器件無應答 */
  }  
  
  /* 第9步:循環讀取資料 */
  for (i = 0; i < _usSize; i++)
  {
    _pReadBuf[i] = I2C_ReadByte();  /* 讀1個位元組 */
    
    /* 每讀完1個位元組後,需要發送Ack, 最後一個位元組不需要Ack,發Nack */
    if (i != _usSize - 1)
    {
      I2C_Ack();  /* 中間位元組讀完後,CPU産生ACK信号(驅動SDA = 0) */
    }
    else
    {
      I2C_NAck();  /* 最後1個位元組讀完後,CPU産生NACK信号(驅動SDA = 1) */
    }
  }
  /* 發送I2C總線停止信号 */
  I2C_Stop();
  return 1;  /* 執行成功 */

cmd_fail: /* 指令執行失敗後,切記發送停止信号,避免影響I2C總線上其他裝置 */
  /* 發送I2C總線停止信号 */
  I2C_Stop();
  return 0;
}

/**
  * @brief  EEPROM_WriteBytes, 向串行EEPROM指定位址寫入若幹資料,采用頁寫操作提高寫入效率
  * @param  _usAddress : 起始位址
  *         _usSize : 資料長度,機關為位元組
  *         _pWriteBuf : 存放讀到的資料的緩沖區指針
  * @retval 0 表示失敗,1表示成功
  */
uint8_t EEPROM_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize)
{
  uint16_t i,m;
  uint16_t usAddr;
  
  /* 
    寫串行EEPROM不像讀操作可以連續讀取很多位元組,每次寫操作隻能在同一個page。
    對于24xx02,page size = 8
    簡單的處理方法為:按位元組寫操作模式,沒寫1個位元組,都發送位址
    為了提高連續寫的效率: 本函數采用page wirte操作。
  */

  usAddr = _usAddress;  
  for (i = 0; i < _usSize; i++)
  {
    /* 當發送第1個位元組或是頁面首位址時,需要重新發起啟動信号和位址 */
    if ((i == 0) || (usAddr & (EEPROM_PAGE_SIZE - 1)) == 0)
    {
      /* 第0步:發停止信号,啟動内部寫操作 */
      I2C_Stop();
      
      /* 通過檢查器件應答的方式,判斷内部寫操作是否完成, 一般小于 10ms       
        CLK頻率為200KHz時,查詢次數為30次左右
      */
      for (m = 0; m < 100; m++)
      {        
        /* 第1步:發起I2C總線啟動信号 */
        I2C_Start();
        
        /* 第2步:發起控制位元組,高7bit是位址,bit0是讀寫控制位,0表示寫,1表示讀 */
        I2C_SendByte(EEPROM_DEV_ADDR | I2C_WR);  /* 此處是寫指令 */
        
        /* 第3步:發送一個時鐘,判斷器件是否正确應答 */
        if (I2C_WaitAck() == 0)
        {
          break;
        }
      }
      if (m  == 1000)
      {
        goto cmd_fail;  /* EEPROM器件寫逾時 */
      }
    
      /* 第4步:發送位元組位址,24C02隻有256位元組,是以1個位元組就夠了,如果是24C04以上,那麼此處需要連發多個位址 */
      I2C_SendByte((uint8_t)usAddr);
      
      /* 第5步:發送ACK */
      if (I2C_WaitAck() != 0)
      {
        goto cmd_fail;  /* EEPROM器件無應答 */
      }
    }
  
    /* 第6步:開始寫入資料 */
    I2C_SendByte(_pWriteBuf[i]);
  
    /* 第7步:發送ACK */
    if (I2C_WaitAck() != 0)
    {
      goto cmd_fail;  /* EEPROM器件無應答 */
    }

    usAddr++;  /* 位址增1 */    
  }
  
  /* 指令執行成功,發送I2C總線停止信号 */
  I2C_Stop();
  return 1;

cmd_fail: /* 指令執行失敗後,切記發送停止信号,避免影響I2C總線上其他裝置 */
  /* 發送I2C總線停止信号 */
  I2C_Stop();
  return 0;
}

/**
  * @brief  EEPROM_Erase
  * @param  None
  * @retval None
  */
void EEPROM_Erase(void)
{
  uint16_t i;
  uint8_t buf[EEPROM_SIZE];
  
  /* 填充緩沖區 */
  for (i = 0; i < EEPROM_SIZE; i++)
  {
    buf[i] = 0xFF;
  }
  
  /* 寫EEPROM, 起始位址 = 0,資料長度為 256 */
  if (EEPROM_WriteBytes(buf, 0, EEPROM_SIZE) == 0)
  {
    printf("擦除eeprom出錯!\r\n");
    return;
  }
  else
  {
    printf("擦除eeprom成功!\r\n");
  }
}

/**
  * @brief  EE_Delay
  * @param  nCount
  * @retval None
  */
static void EEPROM_Delay(__IO uint32_t nCount)   //簡單的延時函數
{
  for(; nCount != 0; nCount--);
}
/**
  * @brief  AT24C02 初始化
  * @param  None
  * @retval None
  */
void EEPROM_Init(void)
{
    /*-----------------------------------------------------------------------------------*/  
    if (EEPROM_CheckOk() == 0)
    {
        /* 沒有檢測到EEPROM */
        printf("沒有檢測到串行EEPROM!\r\n");
    }
}

/**
  * @brief  AT24C02 讀寫測試
  * @param  None
  * @retval None
  */
void EEPROM_Test(void)
{
    uint16_t i;
    uint8_t write_buf[EEPROM_SIZE];
    uint8_t read_buf[EEPROM_SIZE];

    /*------------------------------------------------------------------------------------*/  
    /* 填充測試緩沖區 */
    for (i = 0; i < EEPROM_SIZE; i++)
    {    
        write_buf[i] = i;
    }
    /*------------------------------------------------------------------------------------*/  
    if (EEPROM_WriteBytes(write_buf, 0, EEPROM_SIZE) == 0)
    {
        printf("寫eeprom出錯!\r\n");
        return;
    }
    else
    {    
        printf("寫eeprom成功!\r\n");
    }
    /*寫完之後需要适當的延時再去讀,不然會出錯*/
    EEPROM_Delay(0x0FFFFF);
    /*-----------------------------------------------------------------------------------*/
    if (EEPROM_ReadBytes(read_buf, 0, EEPROM_SIZE) == 0)
    {
        printf("讀eeprom出錯!\r\n");
        return;
    }
    else
    {    
        printf("讀eeprom成功,資料如下:\r\n");
    }
    /*-----------------------------------------------------------------------------------*/  
    for (i = 0; i < EEPROM_SIZE; i++)
    {
        if(read_buf[i] != write_buf[i])
        {
            printf("0x%02X ", read_buf[i]);
            printf("錯誤:EEPROM讀出與寫入的資料不一緻");
            return;
        }
        printf(" %02X", read_buf[i]);
        if ((i & 15) == 15)
        {
            printf("\r\n");  
        }  
    }
    printf("eeprom讀寫測試成功\r\n");
}      

代碼很簡單,和使用硬體I2C的邏輯是一樣的。

最後看下主函數吧。

/*
    brief      main function
    param[in]  none
    param[out] none
    retval     none
*/
int main(void)
{
    //systick init
    sysTick_init();

    // led init
    led_init(LED1);
    //usart init 115200 8-N-1
    com_init(COM1, 115200, 0, 1);

    // DMA config
    usart0_dma_init();
    /* USART DMA 發送使能 */
    usart_dma_enable(USART0, USART_DMA_TRANSMIT);
    /* USART DMA接收使能 */
    usart_dma_enable(USART0, USART_DMA_RECEIVE);

    printf("eeprom 軟體模拟i2c測試例程 \r\n");
    EEPROM_Init();
    EEPROM_Test();
    while(1)
    {
        led_toggle(LED1);
        delay_ms(1000);
    }
}      

主函數中重點關注EEPROM_Test()函數,這就是對AT24C02的讀寫操作。

21.6.2實驗現象

下載下傳程式,連接配接序列槽列印資訊如下。

《嵌入式 – GD32開發實戰指南》第21章 I2C

最後,我們使用邏輯分析來檢視資料

《嵌入式 – GD32開發實戰指南》第21章 I2C

歡迎通路我的網站

資源擷取方式