STM32系統學習——I2C (讀寫EEPROM)

Yuk丶Han發表於2017-12-08

I2C 通訊協議(Inter-Integrated Circuit)引腳少,硬體實現簡單,可擴充套件性強,不需要 USART、CAN 等通訊協議的外部收發裝置,現在被廣泛地使用在系統內多個積體電路(IC)間的通訊。
在電腦科學裡,大部分複雜的問題都可以通過分層來簡化。如晶片被分為核心層和片上外設;STM32 標準庫則是在暫存器與使用者程式碼之間的軟體層。對於通訊協議,我們也以分層的方式來理解,最基本的是把它分為物理層和協議層。
物理層規定通訊系統中具有機械、電子功能部分的特性,確保原始資料在物理媒體的傳輸。協議層主要規定通訊邏輯,統一收發雙方的資料打包、解包標準。簡單來說物理層規定我們用嘴巴還是用肢體來交流,
協議層則規定我們用中文還是英文來交流。
一、I2C物理層
這裡寫圖片描述
它的物理層有如下特點:
(1) 它是一個支援裝置的匯流排。“匯流排”指多個裝置共用的訊號線。在一個 I2C 通訊匯流排中,可連線多個 I2C 通訊裝置,支援多個通訊主機及多個通訊從機。
(2) 一個 I2C 匯流排只使用兩條匯流排線路,一條雙向序列資料線(SDA) ,一條序列時鐘線(SCL)。資料線即用來表示資料,時鐘線用於資料收發同步。
(3) 每個連線到匯流排的裝置都有一個獨立的地址,主機可以利用這個地址進行不同裝置之間的訪問。
(4) 匯流排通過上拉電阻接到電源。當 I2C 裝置空閒時,會輸出高阻態,而當所有裝置都空閒,都輸出高阻態時,由上拉電阻把匯流排拉成高電平。
(5) 多個主機同時使用匯流排時,為了防止資料衝突,會利用仲裁方式決定由哪個裝置佔用匯流排。
(6) 具有三種傳輸模式:標準模式傳輸速率為 100kbit/s ,快速模式為 400kbit/s ,高速模式下可達 3.4Mbit/s,但目前大多 I2C 裝置尚不支援高速模式。
(7) 連線到相同匯流排的 IC 數量受到匯流排的最大電容 400pF 限制 。


二、協議層
I2C 的協議定義了通訊的起始和停止訊號、資料有效性、響應、仲裁、時鐘同步和地址廣播等環節。
1、基本讀寫過程
這裡寫圖片描述
起始訊號產生後,所有從機就開始等待主機緊接下來廣播的從機地址訊號(SLAVE_ADDRESS)。在 I2C 匯流排上,每個裝置的地址都是唯一的,當主機廣播的地址與某個裝置地址相同時,這個裝置就被選中了,沒被選中的裝置將會忽略之後的資料訊號。
根據 I2C協議,這個從機地址可以是 7位或 10位。
在地址位之後,是傳輸方向的選擇位,該位為 0 時,表示後面的資料傳輸方向是由主機傳輸至從機,即主機向從機寫資料。該位為 1時,則相反。
從機接收到匹配的地址後,主機或從機會返回一個應答(ACK)或非應答(NACK)訊號,只有接收到應答訊號後,主機才能繼續傳送或接收資料。
寫資料
若配置的方向傳輸位為“寫資料”方向,即第一幅圖,廣播完地址,接收到應答訊號後,主機開始正式向從機傳輸資料(DATA),資料包的大小為 8 位,主機每傳送完一個位元組資料,都要等待從機的應答訊號(ACK),重複,可以向從機傳輸 N 個資料,這個 N 沒有大小限制。當資料傳輸結束時,主機向從機傳送一個停止傳輸訊號(P),表示不再傳輸資料。
讀資料
若配置的方向傳輸位為“讀資料”方向,即第二幅圖,廣播完地址,接收到應答訊號後,從機開始向主機返回資料(DATA),資料包大小也為 8 位,從機每傳送完一個資料,都會等待主機的應答訊號(ACK),重複這個過程,可以返回 N 個資料,這個 N 也沒有大小限制。當主機希望停止接收資料時,就向從機返回一個非應答訊號(NACK),則從機自動停止資料傳輸。
讀和寫資料
除了基本的讀寫,I2C 通訊更常用的是複合格式,即第三幅圖,該傳輸過程有兩次起始訊號(S)。一般在第一次傳輸中,主機通過 SLAVE_ADDRESS 尋找到從裝置後,傳送一段“資料”,這段資料通常用於表示從裝置內部的暫存器或儲存器地址(注意區分它與 SLAVE_ADDRESS 的區別);在第二次的傳輸中,對該地址的內容進行讀或寫。也就是說,第一次通訊是告訴從機讀寫地址,第二次則是讀寫的實際內容。
以上通訊流程中包含的各個訊號分解如下:
2、通訊的起始和停止訊號
起始(S)和停止(P)訊號是兩種特殊的狀態。
當 SCL 線是高電平時 SDA 線從高電平向低電平切換,這個情況表示通訊的起始。當 SCL 是高電平時 SDA
線由低電平向高電平切換,表示通訊的停止。起始和停止訊號一般由主機產生
這裡寫圖片描述
3、 資料有效性
I2C使用 SDA訊號線來傳輸資料,使用 SCL訊號線進行資料同步。SDA資料線在 SCL的每個時鐘週期傳輸一位資料。傳輸時,SCL為高電平的時候 SDA表示的資料有效,即此時的SDA為高電平時表示資料“1”,為低電平時表示資料“0”。當SCL為低電平時,SDA的資料無效,一般在這個時候 SDA進行電平切換,為下一次表示資料做好準備。
這裡寫圖片描述
4、 地址及資料方向
I2C 匯流排上的每個裝置都有自己的獨立地址,主機發起通訊時,通過 SDA 訊號線傳送裝置地址(SLAVE_ADDRESS)來查詢從機。I2C 協議規定裝置地址可以是 7 或 10 位,實際中 7 位的地址應用比較廣泛。緊跟裝置地址的一個資料位用來表示資料傳輸方向,它是資料方向位(R/W),第 8位或第 11位。資料方向位為“1”時表示主機由從機讀資料,該位為“0”時表示主機向從機寫資料。
5、 響應
I2C 的資料和地址傳輸都帶響應。響應包括“應答(ACK)”和“非應答(NACK)”兩種訊號。作為資料接收端時,當裝置(無論主從機)接收到 I2C傳輸的一個位元組資料或地址後,若希望對方繼續傳送資料,則需要向對方傳送“應答(ACK)”訊號,傳送方會繼續傳送下一個資料;若接收端希望結束資料傳輸,則向對方傳送“非應答(NACK)”訊號,傳送方接收到該訊號後會產生一個停止訊號,結束訊號傳輸。


三、STM32的I2C結構與特性
如果我們直接控制 STM32的兩個GPIO引腳,分別用作 SCL及 SDA,按照上述訊號的時序要求,直接像控制 LED 燈那樣控制引腳的輸出(若是接收資料時則讀取 SDA 電平),就可以實現 I2C 通訊。同樣,假如我們按照 USART 的要求去控制引腳,也能實現 USART 通訊。所以只要遵守協議,就是標準的通訊,不管您如何實現它,不管是 ST生產的控制器還是 ATMEL生產的儲存器, 都能按通訊標準互動。由於直接控制 GPIO 引腳電平產生通訊時序時,需要由 CPU 控制每個時刻的引腳狀態,所以稱之為“軟體模擬協議”方式。
相對地,還有“硬體協議”方式,STM32 的 I2C 片上外設專門負責實現 I2C 通訊協議,只要配置好該外設,它就會自動根據協議要求產生通訊訊號,收發資料並快取起來,CPU只要檢測該外設的狀態和訪問資料暫存器,就能完成資料收發。這種由硬體外設處理 I2C協議的方式減輕了 CPU 的工作。
1、外設簡介
STM32 的 I2C 外設可用作通訊的主機及從機,支援 100Kbit/s 和 400Kbit/s 的速率,支援 7 位、10 位裝置地址,支援 DMA 資料傳輸,並具有資料校驗功能。它的 I2C 外設還支援 SMBus2.0 協議,SMBus 協議與 I2C 類似,主要應用於膝上型電腦的電池管理中。
2.框架解析*
這裡寫圖片描述
1)通訊引腳
I2C的所有硬體架構都是根據圖中左側 SCL 線和 SDA 線展開的(其中的 SMBA 線用於SMBUS的警告訊號,I2C通訊沒有使用)。
STM32晶片有多個 I2C外設,它們的 I2C通訊訊號引出到不同的 GPIO 引腳上,使用時必須配置到這些指定的引腳。
I2C1 I2C2
SCL I2C1 :PB5 / PB8(重對映) I2C2: PB10
SDA I2C1:PB6 / PB9(重對映) I2C2:PB11
2)時鐘控制邏輯
SCL線的時鐘訊號,由 I2C 介面根據時鐘控制暫存器(CCR)控制,控制的引數主要為時脈頻率。配置 I2C的 CCR 暫存器可修改通訊速率相關的引數:
 可選擇 I2C 通訊的“標準/快速”模式,這兩個模式分別 I2C 對應 100/400Kbit/s 的通訊速率。
 在快速模式下可選擇 SCL 時鐘的佔空比,可選 Tlow/Thigh=2 或 Tlow/Thigh=16/9模式,我們知道 I2C 協議在 SCL 高電平時對 SDA 訊號取樣,SCL 低電平時 SDA準備下一個資料,修改 SCL 的高低電平比會影響資料取樣,但其實這兩個模式的比例差別並不大,若不是要求非常嚴格,隨便選就可以了。
 CCR 暫存器中還有一個 12 位的配置因子 CCR,它與 I2C 外設的輸入時鐘源共同作用,產生 SCL 時鐘,STM32 的I2C 外設都掛載在 APB1 匯流排上,使用 APB1 的時鐘源 PCLK1,SCL訊號線的輸出時鐘公式如下:
這裡寫圖片描述
例如,我們的 PCLK1=36MHz,想要配置 400Kbit/s 的速率,計算方式如下:
PCLK時鐘週期: TPCLK1 = 1/36000000
目標 SCL時鐘週期: TSCL = 1/400000
SCL時鐘週期內的高電平時間: THIGH = TSCL/3
SCL時鐘週期內的低電平時間: TLOW = 2*TSCL/3
計算 CCR的值: CCR = THIGH/TPCLK1 = 30
計算結果得出CCR為30,向該暫存器位寫入此值則可以控制IIC的通訊速率為400KHz,其實即使配置出來的 SCL 時鐘不完全等於標準的 400KHz,IIC 通訊的正確性也不會受到影響,因為所有資料通訊都是由 SCL協調的,只要它的時脈頻率不遠高於標準即可。
3)資料控制邏輯
I2C 的 SDA 訊號主要連到資料移位暫存器上,資料移位暫存器的資料來源及目標是資料暫存器(DR)、地址暫存器(OAR)、PEC 暫存器以及 SDA 資料線。當向外傳送資料的時候,資料移位暫存器以“資料暫存器”為資料來源,把資料一位一位地通過 SDA 訊號線傳送出去;當從外部接收資料的時候,資料移位暫存器把 SDA 訊號線取樣到的資料一位位地儲存到“資料暫存器”中。若使能了資料校驗,接收到的資料會經過 PCE 計算器運算,運算結果儲存在“PEC 暫存器”中。當 STM32 的 I2C 工作在從機模式的時候,接收到裝置地址訊號時,資料移位暫存器會把接收到的地址與 STM32 的自身的“I2C 地址暫存器”的值作比較,以便響應主機的定址。STM32 的自身 I2C 地址可通過修改“自身地址暫存器”修改,支援同時使用兩個 I2C裝置地址,兩個地址分別儲存在 OAR1和 OAR2中。
4)整體控制邏輯
整體控制邏輯負責協調整個 I2C 外設,控制邏輯的工作模式根據我們配置的“控制暫存器(CR1/CR2)”的引數而改變。在外設工作時,控制邏輯會根據外設的工作狀態修改“狀態暫存器(SR1 和 SR2)”,我們只要讀取這些暫存器相關的暫存器位,就可以瞭解 I2C的工作狀態。除此之外,控制邏輯還根據要求,負責控制產生 I2C 中斷訊號、DMA 請求及各種 I2C的通訊訊號(起始、停止、響應訊號等)。


四、通訊過程
使用 I2C 外設通訊時,在通訊的不同階段它會對“狀態暫存器(SR1及 SR2)”的不同資料位寫入引數,我們通過讀取這些暫存器標誌來了解通訊狀態。
1、主傳送器
這裡寫圖片描述
主傳送器傳送流程及事件說明如下:
(1) 控制產生起始訊號(S),當發生起始訊號後,它產生事件“EV5”,並會對 SR1 暫存器的“SB”位置 1,表示起始訊號已傳送;
(2) 接著傳送裝置地址並等待應答訊號,若有從機應答,則產生事件“EV6”及“EV8”,這時 SR1 暫存器的“ADDR”位及“TXE”位被置 1,ADDR 為 1表示地址已經傳送,TXE 為 1表示資料暫存器為空;
(3) 以上步驟正常執行並對 ADDR 位清零後,我們往 I2C 的“資料暫存器 DR”寫入要傳送的資料,這時TXE位會被重置0,表示資料暫存器非空,I2C外設通過SDA訊號線一位位把資料傳送出去後,又會產生“EV8”事件,即 TXE 位被置 1,重複這個過程,就可以傳送多個位元組資料了;
(4) 當我們傳送資料完成後,控制 I2C 裝置產生一個停止訊號(P),這個時候會產生EV8_2 事件,SR1 的 TXE位及 BTF位都被置 1,表示通訊結束。

假如我們使能了 I2C 中斷,以上所有事件產生時,都會產生 I2C 中斷訊號,進入同一個中斷服務函式,到 I2C中斷服務程式後,再通過檢查暫存器位來判斷是哪一個事件。

2、主接收器
這裡寫圖片描述
主接收器接收流程及事件說明如下:
(1) 同主傳送流程,起始訊號(S)是由主機端產生的,控制發生起始訊號後,它產生事件“EV5”,並會對 SR1暫存器的“SB”位置 1,表示起始訊號已經傳送;
(2) 緊接著傳送裝置地址並等待應答訊號,若有從機應答,則產生事件“EV6”這時SR1 寄存的“ADDR”位被置 1,表示地址已經傳送。
(3) 從機端接收到地址後,開始向主機端傳送資料。當主機接收到這些資料後,會產生“EV7”事件,SR1暫存器的 RXNE被置 1,表示接收資料暫存器非空,我們讀取該暫存器後,可對資料暫存器清空,以便接收下一次資料。此時我們可以控制I2C 傳送應答訊號(ACK)或非應答訊號(NACK),若應答,則重複以上步驟接收資料,若非應答,則停止傳輸;
(4) 傳送非應答訊號後,產生停止訊號(P),結束傳輸。
在傳送和接收過程中,有的事件不只是標誌了我們上面提到的狀態位,還可能同時標誌主機狀態之類的狀態位,而且讀了之後還需要清除標誌位,比較複雜。我們可使用STM32標準庫函式來直接檢測這些事件的複合標誌,降低程式設計難度。


五、I2C初始化結構體
初始化結構體及函式定義在庫檔案“stm32f10x_i2c.h”及“stm32f10x_i2c.c”中。

                 I2C 初始化結構體
 typedef struct {
 uint32_t I2C_ClockSpeed; /*!< 設定 SCL 時脈頻率,此值要低於 400000*/
 uint16_t I2C_Mode; /*!< 指定工作模式,可選 I2C 模式及 SMBUS 模式 */
  uint16_t I2C_DutyCycle; /*指定時鐘佔空比,可選 low/high = 2:1 及 16:9 模式*/
 uint16_t I2C_OwnAddress1; /*!< 指定自身的 I2C 裝置地址 */
 uint16_t I2C_Ack; /*!< 使能或關閉響應(一般都要使能) */
 uint16_t I2C_AcknowledgedAddress; /*!< 指定地址的長度,可為 7 位及 10 位 */
 } I2C_InitTypeDef;

(1) I2C_ClockSpeed
本成員設定的是 I2C 的傳輸速率,在呼叫初始化函式時,函式會根據我們輸入的數值經過運算後把時鐘因子寫入到 I2C 的時鐘控制暫存器 CCR。寫入的這個引數值不得高於 400KHz。實際上由於 CCR 暫存器不能寫入小數型別的時鐘因子,影響到 SCL 的實際頻率可能會低於本成員設定的引數值,這時除了通訊稍慢一點以外,不會對 I2C 的標準通訊造成其它影響。
(2) I2C_Mode
本成員是選擇I2C的使用方式,有I2C模式(I2C_Mode_I2C)和SMBus主、從模式(I2C_Mode_SMBusHost、 I2C_Mode_SMBusDevice ) 。I2C 不需要在此處區分主從模式,直接設定 I2C_Mode_I2C 即可。
(3) I2C_DutyCycle
本成員設定的是 I 2 C 的 SCL 線時鐘的佔空比。該配置有兩個選擇,分別為低電平時間比高電平時間為 2:1 ( I2C_DutyCycle_2)和 16:9 (I2C_DutyCycle_16_9)。其實這兩個模式的比例差別並不大,一般要求都不會如此嚴格,這裡隨便選就可以。
(4) I2C_OwnAddress1
本成員配置的是 STM32 的 I2C 裝置自己的地址,每個連線到 I2C 匯流排上的裝置都要有一 個 自 己 的 地 址 , 作 為 主 機 也 不 例 外 。 地 址 可 設 置 為 7 位 或 10 位 ( 受 下 面I2C_AcknowledgeAddress成員決定),只要該地址是 I2C 匯流排上唯一的即可。
STM32 的 I2C 外設可同時使用兩個地址,即同時對兩個地址作出響應,這個結構成員I2C_OwnAddress1配置的是預設的、OAR1暫存器儲存的地址,若需要設定第二個地址暫存器 OAR2,可使用I2C_OwnAddress2Config 函式來配置,OAR2 不支援 10 位地址,只有 7位。
(5) I2C_Ack_Enable
本成員是關於 I 2 C 應答設定,設定為使能則可以傳送響應訊號。本實驗配置為允許應答(I2C_Ack_Enable),這是絕大多數遵循 I 2 C 標準的裝置的通訊要求,改為禁止應答(I2C_Ack_Disable)往往會導致通訊錯誤。
(6) I2C_AcknowledgeAddress
本成員選擇 I2C 的定址模式是 7 位還是 10 位地址。這需要根據實際連線到 I2C 匯流排上裝置的地址進行選擇,這個成員的配置也影響到 I2C_OwnAddress1 成員,只有這裡設定成10 位模式時,I2C_OwnAddress1 才支援 10位地址。
配置完這些結構體成員值,呼叫庫函式 I2C_Init 即可把結構體的配置寫入到暫存器中。


六、讀寫EEPROM實驗
EEPROM 是一種掉電後資料不丟失的儲存器,常用來儲存一些配置資訊,以便系統重新上電的時候載入之。EEPOM晶片最常用的通訊方式就是I 2 C協議,本小節以EEPROM的讀寫實驗為大家講解 STM32的 I 2 C使用方法。實驗中 STM32的 I2C外設採用主模式,分別用作主傳送器和主接收器,通過查詢事件的方式來確保正常通訊。
(本實驗板中的 EEPROM 晶片(型號:AT24C02)的 SCL及 SDA 引腳連線到了 STM32 對應的I2C引腳中,結合上拉電阻,構成了I2C通訊匯流排,它們通過I2C匯流排互動。EEPROM晶片的裝置地址一共有 7 位,其中高 4 位固定為:1010 b,低 3 位則由 A0/A1/A2 訊號線的電平決定,圖中的 R/W是讀寫方向位,與地址無關。)
這裡寫圖片描述
按照我們此處的連線,A0/A1/A2均為0,所以EEPROM的7位裝置地址是:101 0000b ,即 0x50。由於 I2C 通訊時常常是地址跟讀寫方向連在一起構成一個 8 位數,且當 R/W 位為0 時,表示寫方向,所以加上 7 位地址,其值為“0xA0”,常稱該值為 I2C 裝置的“寫地址”;當 R/W 位為 1 時,表示讀方向,加上 7 位地址,其值為“0xA1”,常稱該值為“讀地址”。
EEPROM 晶片中還有一個 WP 引腳,具有防寫功能,當該引腳電平為高時,禁止寫入資料,當引腳為低電平時,可寫入資料,我們直接接地,不使用防寫功能。
關於 EEPROM 的更多資訊,可參考其資料手冊《AT24C02》來了解。若您使用的實驗板 EEPROM 的型號、裝置地址或控制引腳不一樣,只需根據我們的工程修改即可,程式的控制原理相同。
1、程式設計要點
為了使工程更加有條理,我們把讀寫 EEPROM 相關的程式碼獨立分開儲存,方便以後移植。在“工程模板”之上新建“bsp_i2c_ee.c”及“bsp_i2c_ee.h”檔案,這些檔案也可根據您的喜好命名,它們不屬於 STM32 標準庫的內容,是由我們自己根據應用需要編寫的。
(1) 配置通訊使用的目標引腳為開漏模式;
(2) 使能 I2C外設的時鐘;
(3) 配置 I2C外設的模式、地址、速率等引數並使能 I2C外設;
(4) 編寫基本 I2C按位元組收發的函式;
(5) 編寫讀寫 EEPROM 儲存內容的函式;
(6) 編寫測試程式,對讀寫資料進行校驗。

2、I2C 硬體相關巨集定義
我們把 I2C 硬體相關的配置都以巨集的形式定義到 “bsp_i2c_ee.h”檔案中。

1 /**************************I2C 引數定義,I2C1 或 I2C2*********************/
2 #define EEPROM_I2Cx I2C1
3 #define EEPROM_I2C_APBxClock_FUN RCC_APB1PeriphClockCmd
4 #define EEPROM_I2C_CLK RCC_APB1Periph_I2C1
5 #define EEPROM_I2C_GPIO_APBxClock_FUN RCC_APB2PeriphClockCmd
6 #define EEPROM_I2C_GPIO_CLK RCC_APB2Periph_GPIOB
7 #define EEPROM_I2C_SCL_PORT GPIOB
8 #define EEPROM_I2C_SCL_PIN GPIO_Pin_6
9 #define EEPROM_I2C_SDA_PORT GPIOB
10 #define EEPROM_I2C_SDA_PIN GPIO_Pin_7
11 
12 /* STM32 I2C 快速模式 */
13 #define I2C_Speed 400000 //*
14
15 /* 這個地址只要與 STM32 外掛的 I2C 器件地址不一樣即可 */
16 #define I2Cx_OWN_ADDRESS7 0X0A
17 
18 /* AT24C01/02 每頁有 8 個位元組 */
19 #define I2C_PageSize 8

以上程式碼根據硬體連線,把與 EEPROM通訊使用的 I2C號 、引腳號都以巨集封裝起來,
並且定義了自身的 I2C地址及通訊速率,以便配置模式的時候使用。

3、初始化 I2C 的 GPIO
利用上面的巨集,編寫 I2C GPIO 引腳的初始化函式。

1 static void I2C_GPIO_Config(void)
2 {
3 GPIO_InitTypeDef GPIO_InitStructure;
4 
5 /* 使能與 I2C 有關的時鐘 */
6 EEPROM_I2C_APBxClock_FUN ( EEPROM_I2C_CLK, ENABLE );
7 EEPROM_I2C_GPIO_APBxClock_FUN ( EEPROM_I2C_GPIO_CLK, ENABLE );
8 
9 /* I2C_SCL、I2C_SDA*/
10 GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN;
11 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
12 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 開漏輸出
13 GPIO_Init(EEPROM_I2C_SCL_PORT, &GPIO_InitStructure);
14 
15 GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SDA_PIN;
16 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
17 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 開漏輸出
18 GPIO_Init(EEPROM_I2C_SDA_PORT, &GPIO_InitStructure);
19 }

開啟相關的時鐘並初始化 GPIO引腳,函式執行流程如下:
(1) 使用GPIO_InitTypeDef定義 GPIO初始化結構體變數,以便下面用於儲存GPIO配置;
(2) 呼叫庫函式 RCC_APB1PeriphClockCmd(程式碼中為巨集 EEPROM_I2C_APBxClock_FUN)使 能 I2C 外 設 時 鍾 , 調 用 RCC_APB2PeriphClockCmd ( 代 碼 中 為 巨集EEPROM_I2C_GPIO_APBxClock_FUN)來使能 I2C 引腳使用的 GPIO 埠時鐘,呼叫時我們使用“|”操作同時配置兩個引腳。
(3) 向GPIO初始化結構體賦值,把引腳初始化成複用開漏模式,要注意I2C的引腳必須使用這種模式。
(4) 使用以上初始化結構體的配置,呼叫 GPIO_Init 函式向暫存器寫入引數,完成 GPIO 的初始化。

4、配置 I2C 的模式
以上只是配置了 I2C 使用的引腳,還不算對 I2C模式的配置。

1 /**
2 * @brief I2C 工作模式配置
3 * @param 無
4 * @retval 無
5 */
6 static void I2C_Mode_Configu(void)
7 {
8 I2C_InitTypeDef I2C_InitStructure;
9 
10 /* I2C 配置 */
11 I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
12 
13 /* 高電平資料穩定,低電平資料變化 SCL 時鐘線的佔空比 */
14 I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
15 
16 I2C_InitStructure.I2C_OwnAddress1 =I2Cx_OWN_ADDRESS7;
17 I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;
18 
19 /* I2C 的定址模式 */
20 I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
21 
22 /* 通訊速率 */
23 I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;
24 
25 /* I2C 初始化 */
26 I2C_Init(EEPROM_I2Cx, &I2C_InitStructure);
27 
28 /* 使能 I2C */
29 I2C_Cmd(EEPROM_I2Cx, ENABLE);
30 }
31 
32 
33 /**
34 * @brief I2C 外設(EEPROM)初始化
35 * @param 無
36 * @retval 無
37 */
38 void I2C_EE_Init(void)
39 {
40 I2C_GPIO_Config();
41 
42 I2C_Mode_Configu();
43 
44 /* 根據標頭檔案 i2c_ee.h 中的定義來選擇 EEPROM 要寫入的裝置地址 */
45 /* 選擇 EEPROM Block0 來寫入 */
46 EEPROM_ADDRESS = EEPROM_Block0_ADDRESS;
47 }

熟悉 STM32 I2C 結構的話,這段初始化程式就十分好理解,它把 I2C 外設通訊時鐘SCL的低/高電平比設定為 2,使能響應功能,使用 7 位地址 I2C_OWN_ADDRESS7 以及速率配置為 I2C_Speed(前面在 bsp_i2c_ee.h 定義的巨集)。最後呼叫庫函式 I2C_Init 把這些配置寫入暫存器,並呼叫 I2C_Cmd 函式使能外設。
為方便呼叫,我們把 I2C的 GPIO 及模式配置都用 I2C_EE_Init 函式封裝起來。

5、向 EEPROM 寫入一個位元組的資料
初始化好 I2C 外設後,就可以使用 I2C 通訊,向 EEPROM 寫入一個位元組

1 
2 /***************************************************************/
3 /*通訊等待超時時間*/
4 #define I2CT_FLAG_TIMEOUT ((uint32_t)0x1000)
5 #define I2CT_LONG_TIMEOUT ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))
6 
7 /**
8 * @brief I2C 等待事件超時的情況下會呼叫這個函式來處理
9 * @param errorCode:錯誤程式碼,可以用來定位是哪個環節出錯.
10 * @retval 返回 0,表示 IIC 讀取失敗.
11 */
12 static uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode)
13 {
14 /* 使用串列埠 printf 輸出錯誤資訊,方便除錯 */
15 EEPROM_ERROR("I2C 等待超時!errorCode = %d",errorCode);
16 return 0;
17 }
18 /**
19 * @brief 寫一個位元組到 I2C EEPROM 中
20 * @param pBuffer:緩衝區指標
21 * @param WriteAddr:寫地址
22 * @retval 正常返回 1,異常返回 0
23 */
24 uint32_t I2C_EE_ByteWrite(u8* pBuffer, u8 WriteAddr)
25 {
26 /* 產生 I2C 起始訊號 */
27 I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
28 
29 /*設定超時等待時間*/
30 I2CTimeout = I2CT_FLAG_TIMEOUT;
31 /* 檢測 EV5 事件並清除標誌*/
32 while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
33 {
34 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);
35 }
36 
37 /* 傳送 EEPROM 裝置地址 */
38 I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS,
39 I2C_Direction_Transmitter);
40 
41 I2CTimeout = I2CT_FLAG_TIMEOUT;
42 /* 檢測 EV6 事件並清除標誌*/
43 while (!I2C_CheckEvent(EEPROM_I2Cx,
44 I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
45 {
46 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);
47 }
48 
49 /* 傳送要寫入的 EEPROM 內部地址(即 EEPROM 內部儲存器的地址) */
50 I2C_SendData(EEPROM_I2Cx, WriteAddr);
51 
52 I2CTimeout = I2CT_FLAG_TIMEOUT;
53 /* 檢測 EV8 事件並清除標誌*/
54 while (!I2C_CheckEvent(EEPROM_I2Cx,
55 I2C_EVENT_MASTER_BYTE_TRANSMITTED))
56 {
57 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
58 }
59 /* 傳送一位元組要寫入的資料 */
60 I2C_SendData(EEPROM_I2Cx, *pBuffer);
61 
62 I2CTimeout = I2CT_FLAG_TIMEOUT;
63 /* 檢測 EV8 事件並清除標誌*/
64 while (!I2C_CheckEvent(EEPROM_I2Cx,
65 I2C_EVENT_MASTER_BYTE_TRANSMITTED))
66 {
67 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
68 }
69 
70 /* 傳送停止訊號 */
71 I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
72 
73 return 1;
74 }

先 來 分 析 I2C_TIMEOUT_UserCallback 函 數 , 它 的 函 數 體 裡 只 調 用 了 巨集EEPROM_ERROR,這個巨集封裝了 printf函式,方便使用串列埠向上位機列印除錯資訊,閱讀程式碼時把它當成 printf函式即可。在 I2C通訊的很多過程,都需要檢測事件,當檢測到某事件後才能繼續下一步的操作,但有時通訊錯誤或者 I2C 匯流排被佔用,我們不能無休止地等待下去,所以我們設定每個事件檢測都有等待的時間上限,若超過這個時間,我們就呼叫I2C_TIMEOUT_UserCallback 函式輸出除錯資訊(或可以自己加其它操作),並終止 I2C 通訊。
瞭解了這個機制,再來分析 I2C_EE_ByteWrite 函式,這個函式實現了前面講的 I2C 主傳送器通訊流程:
(1) 使用庫函式 I2C_GenerateSTART產生 I2C起始訊號,其中的 EEPROM_I2C巨集是前面硬體定義相關的 I2C編號;
(2) 對 I2CTimeout 變數賦值為巨集 I2CT_FLAG_TIMEOUT,這個 I2CTimeout 變數在下面的 while 迴圈中每次迴圈減 1,該迴圈通過呼叫庫函式 I2C_CheckEvent 檢測事件,若檢測到事件,則進入通訊的下一階段,若未檢測到事件則停留在此處一直檢測,當檢測I2CT_FLAG_TIMEOUT次都還沒等待到事件則認為通訊失敗,呼叫前面的 I2C_TIMEOUT_UserCallback輸出除錯資訊,並退出通訊;
(3) 呼叫庫函式I2C_Send7bitAddress傳送EEPROM的裝置地址,並把資料傳輸方向設定為 I2C_Direction_Transmitter(即傳送方向),這個資料傳輸方向就是通過設定 I2C通訊中緊跟地址後面的 R/W位實現的。傳送地址後以同樣的方式檢測 EV6標誌;
(4) 呼叫庫函式 I2C_SendData 向 EEPROM 傳送要寫入的內部地址,該地址是I2C_EE_ByteWrite 函式的輸入引數,傳送完畢後等待 EV8 事件。要注意這個內部地址跟上面的 EEPROM 地址不一樣,上面的是指 I2C 匯流排裝置的獨立地址,而此處的內部地址是指 EEPROM 內資料組織的地址,也可理解為 EEPROM 記憶體的地址或 I2C裝置的暫存器地址;
(5) 調 用 庫 函 數 I2C_SendData 向 EEPROM 發 送 要 寫 入 的 數 據 , 該 數 據 是I2C_EE_ByteWrite 函式的輸入引數,傳送完畢後等待 EV8 事件;
(6) 一個 I2C通訊過程完畢,呼叫 I2C_GenerateSTOP 傳送停止訊號。
在這個通訊過程中,STM32實際上通過 I2C向 EEPROM傳送了兩個資料,但為何第一個資料被解釋為 EEPROM 的記憶體地址?這是由 EEPROM 的自己定義的單位元組寫入時序,
這裡寫圖片描述
EEPROM 的單位元組時序規定,向它寫入資料的時候,第一個位元組為記憶體地址,第二個位元組是要寫入的資料內容。所以我們需要理解:命令、地址的本質都是資料,對資料的解釋不同,它就有了不同的功能。
6、多位元組寫入及狀態等待
單位元組寫入通訊結束後,EEPROM 晶片會根據這個通訊結果擦寫該記憶體地址的內容,這需要一段時間,所以我們在多次寫入資料時,要先等待 EEPROM 內部擦寫完畢。

1 /**
2 * @brief 將緩衝區中的資料寫到 I2C EEPROM 中,採用單位元組寫入的方式,速度比頁寫入慢
3 
4 * @param pBuffer:緩衝區指標
5 * @param WriteAddr:寫地址
6 * @param NumByteToWrite:寫的位元組數
7 * @retval 無
8 */
9 uint8_t I2C_EE_ByetsWrite(uint8_t* pBuffer,uint8_t WriteAddr,
10 uint16_t NumByteToWrite)
11 {
12 uint16_t i;
13 uint8_t res;
14 
15 /*每寫一個位元組呼叫一次 I2C_EE_ByteWrite 函式*/
16 for (i=0; i<NumByteToWrite; i++)
17 {
18 /*等待 EEPROM 準備完畢*/
19 I2C_EE_WaitEepromStandbyState();
20 /*按位元組寫入資料*/
21 res = I2C_EE_ByteWrite(pBuffer++,WriteAddr++);
22 }
23 return res;
24 }

這段程式碼比較簡單,直接使用 for 迴圈呼叫前面定義的 I2C_EE_ByteWrite 函式一個位元組一個位元組地向 EEPROM 傳送要寫入的資料 。在每次資料寫入通訊前呼叫了I2C_EE_WaitEepromStandbyState 函式等待 EEPROM 內部擦寫完畢,

1 /**
2 * @brief 等待 EEPROM 到準備狀態
3 * @param 無
4 * @retval 無
5 */
6 void I2C_EE_WaitEepromStandbyState(void)
7 {
8 vu16 SR1_Tmp = 0;
9 
10 do {
11 /* 傳送起始訊號 */
12 I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
13 
14 /* 讀 I2C1 SR1 暫存器 */
15 SR1_Tmp = I2C_ReadRegister(EEPROM_I2Cx, I2C_Register_SR1);
16 
17 /* 傳送 EEPROM 地址 + 寫方向 */
18 I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS,
19 I2C_Direction_Transmitter);
20 }
21 // SR1 位 1 ADDR:1 表示地址傳送成功,0 表示地址傳送沒有結束
22 // 等待地址傳送成功
23 while (!(I2C_ReadRegister(EEPROM_I2Cx, I2C_Register_SR1) & 0x0002));
24 
25 /* 清除 AF 位 */
26 I2C_ClearFlag(EEPROM_I2Cx, I2C_FLAG_AF);
27 /* 傳送停止訊號 */
28 I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
29 }

這個函式主要實現是向 EEPROM 傳送它裝置地址,檢測 EEPROM 的響應,若EEPROM 接收到地址後返回應答訊號,則表示 EEPROM 已經準備好,可以開始下一次通訊。函式中檢測響應是通過讀取 STM32 的 SR1 暫存器的 ADDR 位及 AF 位來實現的,當I2C 裝置響應了地址的時候,ADDR會置 1,若應答失敗,AF位會置 1。
7.頁寫入
在以上的資料通訊中,每寫入一個資料都需要向 EEPROM 傳送寫入的地址,我們希望
向連續地址寫入多個資料的時候,只要告訴 EEPROM 第一個記憶體地址 address1,後面的數
據按次序寫入到address2、address3… 這樣可以節省通訊的時間,加快速度。為應對這種需
求,EEPROM 定義了一種頁寫入時序
這裡寫圖片描述
根據頁寫入時序,第一個資料被解釋為要寫入的記憶體地址 address1,後續可連續傳送 n個資料,這些資料會依次寫入到記憶體中。其中 AT24C02 型號的晶片頁寫入時序最多可以一次傳送 8個資料(即 n = 8 ),該值也稱為頁大小,某些型號的晶片每個頁寫入時序最多可傳輸 16 個資料。

1
2 /**
3 * @brief 在 EEPROM 的一個寫迴圈中可以寫多個位元組,但一次寫入的位元組數
4 * 不能超過 EEPROM 頁的大小,AT24C02 每頁有 8 個位元組
5 * @param
6 * @param pBuffer:緩衝區指標
7 * @param WriteAddr:寫地址
8 * @param NumByteToWrite:要寫的位元組數要求 NumByToWrite 小於頁大小
9 * @retval 正常返回 1,異常返回 0
10 */
11 uint8_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr,
12 uint8_t NumByteToWrite)
13 {
14 I2CTimeout = I2CT_LONG_TIMEOUT;
15 
16 while (I2C_GetFlagStatus(EEPROM_I2Cx, I2C_FLAG_BUSY))
17 {
18 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(4);
19 }
20 
21 /* 產生 I2C 起始訊號 */
22 I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
23 
24 I2CTimeout = I2CT_FLAG_TIMEOUT;
25 
26 /* 檢測 EV5 事件並清除標誌 */
27 while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
28 {
29 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(5);
30 }
31 
32 /* 傳送 EEPROM 裝置地址 */
33 I2C_Send7bitAddress(EEPROM_I2Cx,EEPROM_ADDRESS,I2C_Direction_Transmitter);
34 
35 I2CTimeout = I2CT_FLAG_TIMEOUT;
36 
37 /* 檢測 EV6 事件並清除標誌*/
38 while (!I2C_CheckEvent(EEPROM_I2Cx,
39 I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
40 {
41 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(6);
42 }
43 /* 傳送要寫入的 EEPROM 內部地址(即 EEPROM 內部儲存器的地址) */
44 I2C_SendData(EEPROM_I2Cx, WriteAddr);
45 
46 I2CTimeout = I2CT_FLAG_TIMEOUT;
47 
48 /* 檢測 EV8 事件並清除標誌*/
49 while (! I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
50 {
51 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(7);
52 }
53 /* 迴圈傳送 NumByteToWrite 個資料 */
54 while (NumByteToWrite--)
55 {
56 /* 傳送緩衝區中的資料 */
57 I2C_SendData(EEPROM_I2Cx, *pBuffer);
58 
59 /* 指向緩衝區中的下一個資料 */
60 pBuffer++;
61 
62 I2CTimeout = I2CT_FLAG_TIMEOUT;
63 
64 /* 檢測 EV8 事件並清除標誌*/
65 while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
66 {
67 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(8);
68 }
69 }
70 /* 傳送停止訊號 */
71 I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
72 return 1;
73 }

這段頁寫入函式主體跟單位元組寫入函式是一樣的,只是它在傳送資料的時候,使用 for迴圈控制傳送多個資料,傳送完多個資料後才產生 I2C 停止訊號,只要每次傳輸的資料小於等於 EEPROM時序規定的頁大小,就能正常傳輸。
8、快速寫入多位元組
利用 EEPROM 的頁寫入方式,可以改進前面的“多位元組寫入”函式,加快傳輸速度

1 // AT24C01/02 每頁有 8 個位元組
2 #define I2C_PageSize 8
3 
4 /**
5 * @brief 將緩衝區中的資料寫到 I2C EEPROM 中
6 * @param
7 * @arg pBuffer:緩衝區指標
8 * @arg WriteAddr:寫地址
9 * @arg NumByteToWrite:寫的位元組數
10 * @retval 無
11 */
12 void I2C_EE_BufferWrite(u8* pBuffer, u8 WriteAddr,
13 u16 NumByteToWrite)
14 {
15 u8 NumOfPage=0,NumOfSingle=0,Addr =0,count=0,temp =0;
16 
17 /*mod 運算求餘,若 writeAddr 是 I2C_PageSize 整數倍,
18 運算結果 Addr 值為 0*/
19 Addr = WriteAddr % I2C_PageSize;
20 
21 /*差 count 個資料值,剛好可以對齊到頁地址*/
22 count = I2C_PageSize - Addr;
23 
24 /*計算出要寫多少整數頁*/
25 NumOfPage = NumByteToWrite / I2C_PageSize;
26 
27 /*mod 運算求餘,計算出剩餘不滿一頁的位元組數*/
28 NumOfSingle = NumByteToWrite % I2C_PageSize;
29 
30 // Addr=0,則 WriteAddr 剛好按頁對齊 aligned
31 // 這樣就很簡單了,直接寫就可以,寫完整頁後
32 // 把剩下的不滿一頁的寫完即可
33 if (Addr == 0) {
34 /* 如果 NumByteToWrite < I2C_PageSize */
35 if (NumOfPage == 0) {
36 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
37 I2C_EE_WaitEepromStandbyState();
38 }
39 /* 如果 NumByteToWrite > I2C_PageSize */
40 else {
41 /*先把整數頁都寫了*/
42 while (NumOfPage--) {
43 I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
44 I2C_EE_WaitEepromStandbyState();
45 WriteAddr += I2C_PageSize;
46 pBuffer += I2C_PageSize;
47 }
48 /*若有多餘的不滿一頁的資料,把它寫完*/
49 if (NumOfSingle!=0) {
50 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
51 I2C_EE_WaitEepromStandbyState();
52 }
53 }
54 }
55 // 如果 WriteAddr 不是按 I2C_PageSize 對齊
56 // 那就算出對齊到頁地址還需要多少個資料,然後
57 // 先把這幾個資料寫完,剩下開始的地址就已經對齊
58 // 到頁地址了,程式碼重複上面的即可
59 else {
60 /* 如果 NumByteToWrite < I2C_PageSize */
61 if (NumOfPage== 0) {
62 /*若 NumOfSingle>count,當前面寫不完,要寫到下一頁*/
63 if (NumOfSingle > count) {
64 // temp 的資料要寫到寫一頁
65 temp = NumOfSingle - count;
66 
67 I2C_EE_PageWrite(pBuffer, WriteAddr, count);
68 I2C_EE_WaitEepromStandbyState();
69 WriteAddr += count;
70 pBuffer += count;
71 
72 I2C_EE_PageWrite(pBuffer, WriteAddr, temp);
73 I2C_EE_WaitEepromStandbyState();
74 } else { /*若 count 比 NumOfSingle 大*/
75 I2C_EE_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
76 I2C_EE_WaitEepromStandbyState();
77 }
78 }
79 /* 如果 NumByteToWrite > I2C_PageSize */
80 else {
81 /*地址不對齊多出的 count 分開處理,不加入這個運算*/
82 NumByteToWrite -= count;
83 NumOfPage = NumByteToWrite / I2C_PageSize;
84 NumOfSingle = NumByteToWrite % I2C_PageSize;
85 
86 /*先把 WriteAddr 所在頁的剩餘位元組寫了*/
87 if (count != 0) {
88 I2C_EE_PageWrite(pBuffer, WriteAddr, count);
89 I2C_EE_WaitEepromStandbyState();
90 
91 /*WriteAddr 加上 count 後,地址就對齊到頁了*/
92 WriteAddr += count;
93 pBuffer += count;
94 }
95 /*把整數頁都寫了*/
96 while (NumOfPage--) {
97 I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
98 I2C_EE_WaitEepromStandbyState();
99 WriteAddr += I2C_PageSize;
100 pBuffer += I2C_PageSize;
101 }
102 /*若有多餘的不滿一頁的資料,把它寫完*/
103 if (NumOfSingle != 0) {
104 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
105 I2C_EE_WaitEepromStandbyState();
106 }
107 }
108 }
109 }

它的主旨就是對輸入的資料進行分頁(本型號晶片每頁 8 個位元組),通過“整除”計算要寫入的資料NumByteToWrite 能寫滿多少“完整的頁”,計算得的值儲存在 NumOfPage 中,但有時資料不是剛好能寫滿完整頁的,會多一點出來,通過“求餘”計算得出“不滿一頁的資料個數”就儲存在 NumOfSingle 中。計算後通過按頁傳輸 NumOfPage 次整頁資料及最後的NumOfSing 個資料,使用頁傳輸,比之前的單個位元組資料傳輸要快很多。
除了基本的分頁傳輸,還要考慮首地址的問題。若首地址不是剛好對齊到頁的首地址,會需要一個count值,用於儲存從該首地址開始寫滿該地址所在的頁,還能寫多少個資料。實際傳輸時,先把這部分count個資料先寫入,填滿該頁,然後把剩餘的資料(NumByteToWrite-count),再重複上述求出 NumOPage及 NumOfSingle的過程,按頁傳輸到EEPROM。

最後,強調一下,EEPROM 支援的頁寫入只是一種加速的 I2C 的傳輸時序,實際上並不要求每次都以頁為單位進行讀寫,EEPROM 是支援隨機訪問的(直接讀寫任意一個地址),如前面的單個位元組寫入。在某些儲存器,如 NAND FLASH,它是必須按照 Block 寫入的,例如每個 Block 為 512 或 4096 位元組,資料寫入的最小單位是 Block,寫入前都需要擦除整個 Block;NOR FLASH 則是寫入前必須以 Sector/Block 為單位擦除,然後才可以按位元組寫入。而我們的 EEPROM 資料寫入和擦除的最小單位是“位元組”而不是“頁”,資料寫入前不需要擦除整頁。

9、從EEPROM讀取資料
從 EEPROM 讀取資料是一個 複合的 I2C 時序 ,它實際上包含一個寫過程和一個讀過程。
這裡寫圖片描述
讀時序的第一個通訊過程中,使用 I2C傳送裝置地址定址(寫方向),接著傳送要讀取的“記憶體地址”;第二個通訊過程中,再次使用 I2C 傳送裝置地址定址,但這個時候的資料方向是讀方向;在這個過程之後,EEPROM 會向主機返回從“記憶體地址”開始的資料,一個位元組一個位元組地傳輸,只要主機的響應為“應答訊號”,它就會一直傳輸下去,主機想結束傳輸時,就傳送“非應答訊號”,並以“停止訊號”結束通訊,作為從機的 EEPROM也會停止傳輸。

1 
2 /**
3 * @brief 從 EEPROM 裡面讀取一塊資料
4 * @param pBuffer:存放從 EEPROM 讀取的資料的緩衝區指標
5 * @param ReadAddr:接收資料的 EEPROM 的地址
6 * @param NumByteToRead:要從 EEPROM 讀取的位元組數
7 * @retval 正常返回 1,異常返回 0
8 */
9 uint8_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr,
10 u16 NumByteToRead)
11 {
12 I2CTimeout = I2CT_LONG_TIMEOUT;
13 
14 while (I2C_GetFlagStatus(EEPROM_I2Cx, I2C_FLAG_BUSY))
15 {
16 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9);
17 }
18 
19 /* 產生 I2C 起始訊號 */
20 I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
21 
22 I2CTimeout = I2CT_FLAG_TIMEOUT;
23 
24 /* 檢測 EV5 事件並清除標誌*/
25 while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
26 {
27 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);
28 }
29 
30 /* 傳送 EEPROM 裝置地址 */
31 I2C_Send7bitAddress(EEPROM_I2Cx,EEPROM_ADDRESS,I2C_Direction_Transmitter);
32 
33 I2CTimeout = I2CT_FLAG_TIMEOUT;
34 
35 /* 檢測 EV6 事件並清除標誌*/
36 while (!I2C_CheckEvent(EEPROM_I2Cx,
37 I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
38 {
39 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(11);
40 }
41 /*通過重新設定 PE 位清除 EV6 事件 */
42 I2C_Cmd(EEPROM_I2Cx, ENABLE);
43 
44 /* 傳送要讀取的 EEPROM 內部地址(即 EEPROM 內部儲存器的地址) */
45 I2C_SendData(EEPROM_I2Cx, ReadAddr);
46 
47 I2CTimeout = I2CT_FLAG_TIMEOUT;
48 
49 /* 檢測 EV8 事件並清除標誌*/
50 while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTED))
51 {
52 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(12);
53 }
54 /* 產生第二次 I2C 起始訊號 */
55 I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
56 
57 I2CTimeout = I2CT_FLAG_TIMEOUT;
58 
59 /* 檢測 EV5 事件並清除標誌*/
60 while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
61 {
62 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(13);
63 }
64 /* 傳送 EEPROM 裝置地址 */
65 I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Receiver);
66 
67 I2CTimeout = I2CT_FLAG_TIMEOUT;
68 
69 /* 檢測 EV6 事件並清除標誌*/
70 while (!I2C_CheckEvent(EEPROM_I2Cx,
71 I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
72 {
73 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(14);
74 }
75 /* 讀取 NumByteToRead 個資料*/
76 while (NumByteToRead)
77 {
78 /*若 NumByteToRead=1,表示已經接收到最後一個資料了,
79 傳送非應答訊號,結束傳輸*/
80 if (NumByteToRead == 1)
81 {
82 /* 傳送非應答訊號 */
83 I2C_AcknowledgeConfig(EEPROM_I2Cx, DISABLE);
84 
85 /* 傳送停止訊號 */
86 I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
87 }
88 
89 I2CTimeout = I2CT_LONG_TIMEOUT;
90 while (I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED)==0)
91 {
92 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
93 }
94 {
95 /*通過 I2C,從裝置中讀取一個位元組的資料 */
96 *pBuffer = I2C_ReceiveData(EEPROM_I2Cx);
97 
98 /* 儲存資料的指標指向下一個地址 */
99 pBuffer++;
100 
101 /* 接收資料自減 */
102 NumByteToRead--;
103 }
104 }
105 
106 /* 使能應答,方便下一次 I2C 傳輸 */
107 I2C_AcknowledgeConfig(EEPROM_I2Cx, ENABLE);
108 return 1;
109 }

這段中的寫過程跟前面的寫位元組函式類似,而讀過程中接收資料時,需要使用庫函式I2C_ReceiveData 來讀取。響應訊號則通過庫函式 I2C_AcknowledgeConfig 來傳送,DISABLE 時為非響應訊號,ENABLE 為響應訊號。
10、EEPROM讀寫測試函式

1 /**
2 * @brief I2C(AT24C02)讀寫測試
3 * @param 無
4 * @retval 正常返回 1 ,不正常返回 0
5 */
6 uint8_t I2C_Test(void)
7 {
8 u16 i;
9 EEPROM_INFO("寫入的資料");
10 
11 for ( i=0; i<=255; i++ ) //填充緩衝
12 {
13 I2c_Buf_Write[i] = i;
14 
15 printf("0x%02X ", I2c_Buf_Write[i]);
16 if (i%16 == 15)
17 printf("\n\r");
18 }
19 
20 //將 I2c_Buf_Write 中順序遞增的資料寫入 EERPOM 中
21 //頁寫入方式
22 // I2C_EE_BufferWrite( I2c_Buf_Write, EEP_Firstpage, 256);
23 //位元組寫入方式
24 I2C_EE_ByetsWrite( I2c_Buf_Write, EEP_Firstpage, 256);
25 
26 EEPROM_INFO("寫結束");
27 
28 EEPROM_INFO("讀出的資料");
29 //將 EEPROM 讀出資料順序保持到 I2c_Buf_Read 中
30 I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, 256);
31 
32 //將 I2c_Buf_Read 中的資料通過串列埠列印
33 for (i=0; i<256; i++)
34 {
35 if (I2c_Buf_Read[i] != I2c_Buf_Write[i])
36 {
37 printf("0x%02X ", I2c_Buf_Read[i]);
38 EEPROM_ERROR("錯誤:I2C EEPROM 寫入與讀出的資料不一致");
39 return 0;
40 }
41 printf("0x%02X ", I2c_Buf_Read[i]);
42 if (i%16 == 15)
43 printf("\n\r");
44 
45 }
46 EEPROM_INFO("I2C(AT24C02)讀寫測試成功");
47 return 1;
48 }

程式碼中先填充一個陣列,陣列的內容為 1,2,3 至 N,接著把這個陣列的內容寫入到EEPROM 中,寫入時可以採用單位元組寫入的方式或頁寫入的方式。寫入完畢後再從EEPROM 的地址中讀取資料,把讀取得到的與寫入的資料進行校驗,若一致說明讀寫正常,否則讀寫過程有問題或者 EEPROM 晶片不正常。其中程式碼用到的 EEPROM_INFO 跟EEPROM_ERROR 巨集類似,都是對 printf 函式的封裝,使用和閱讀程式碼時把它直接當成
printf 函式就好。具體的巨集定義在“bsp_i2c_ee.h 檔案中”,在以後的程式碼我們常常會用類似的巨集來輸出除錯資訊。
11、main函式
編寫 main 函式,函式中初始化串列埠、I2C 外設,然後呼叫上面的 I2C_Test 函式進行讀寫測試,

1 
2 /**
3 * @brief 主函式
4 * @param 無
5 * @retval 無
6 */
7 int main(void)
8 {
9 LED_GPIO_Config();
10 
11 LED_BLUE;
12 /*初始化 USART1*/
13 Debug_USART_Config();
14 
15 printf("\r\n 歡迎使用 STM32 F103型號\r\n");
16 
17 printf("\r\n 這是一個 I2C 外設(AT24C02)讀寫測試例程 \r\n");
18 
19 /* I2C 外設(AT24C02)初始化 */
20 I2C_EE_Init();
21 
22 if (I2C_Test() ==1)
23 {
24 LED_GREEN;
25 }
26 else
27 {
28 LED_RED;
29 }
30 
31 while (1)
32 {
33 }
34 
35 }
36 

文章引用《STM32庫開發實戰指南》

相關文章