I2C協議簡介
I2C 通訊協議(Inter-Integrated Circuit)是由 Phiilps 公司開發的,由於它引腳少,硬件實現簡單,可擴展性強,不需要 USART、 CAN 等通訊協議的外部收發設備,現在被廣泛地使用在系統內多個集成電路(IC)間的通訊。
下面我們分別對 I2C 協議的物理層及協議層進行講解。
I2C物理層
I2C 通訊設備之間的常用連接方式見圖 23-1。
它的物理層有如下特點:
(1) 它是一個支持多設備的總線。“總線”指多個設備共用的信號線。在一個 I2C 通訊總線中,可連接多個 I2C 通訊設備,支持多個通訊主機及多個通訊從機。
(2) 一個 I2C 總線只使用兩條總線線路,一條雙向串行數據線(SDA) ,一條串行時鍾線(SCL)。數據線即用來表示數據,時鍾線用於數據收發同步。
(3) 每個連接到總線的設備都有一個獨立的地址,主機可以利用這個地址進行不同設備之間的訪問。
(4) 總線通過上拉電阻接到電源。當 I2C 設備空閑時,會輸出高阻態,而當所有設備都空閑,都輸出高阻態時,由上拉電阻把總線拉成高電平。
(5) 多個主機同時使用總線時,為了防止數據沖突,會利用仲裁方式決定由哪個設備占用總線。
(6) 具有三種傳輸模式:標准模式傳輸速率為 100kbit/s ,快速模式為 400kbit/s ,高速模式下可達 3.4Mbit/s,但目前大多 I2C 設備尚不支持高速模式。
(7) 連接到相同總線的 IC 數量受到總線的最大電容 400pF 限制 。
協議層
I2C 的協議定義了通訊的起始和停止信號、數據有效性、響應、仲裁、時鍾同步和地址廣播等環節。
I2C基本讀寫過程
先看看 I2C 通訊過程的基本結構,它的通訊過程見圖 23-2、 圖 23-3 及圖 23-4。
P : 停止傳輸信號
這些圖表示的是主機和從機通訊時, SDA 線的數據包序列。
其中 S 表示由主機的 I2C 接口產生的傳輸起始信號(S),這時連接到 I2C 總線上的所有從機都會接收到這個信號。
起始信號產生后,所有從機就開始等待主機緊接下來廣播 的從機地址信號(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 的區別);在第二次的傳輸中,對該地址的內容進行讀或寫。也就是說,第一次通訊是告訴從機讀寫地址,第二次則是讀寫的實際內容。
以上通訊流程中包含的各個信號分解如下:
通訊的起始和停止信號
前文中提到的起始(S)和停止(P)信號是兩種特殊的狀態,見圖 23-5。當 SCL 線是高電平時 SDA 線從高電平向低電平切換,這個情況表示通訊的起始。當 SCL 是高電平時 SDA線由低電平向高電平切換,表示通訊的停止。起始和停止信號一般由主機產生。
數據的有效性
I2C 使用 SDA 信號線來傳輸數據,使用 SCL 信號線進行數據同步。見圖 23-6。 SDA數據線在 SCL 的每個時鍾周期傳輸一位數據。傳輸時, SCL 為高電平的時候 SDA 表示的數據有效,即此時的 SDA 為高電平時表示數據“1”,為低電平時表示數據“0”。當 SCL為低電平時, SDA 的數據無效,一般在這個時候 SDA 進行電平切換,為下一次表示數據做好准備。
每次數據傳輸都以字節為單位,每次傳輸的字節數不受限制。
地址及數據方向
I2C 總線上的每個設備都有自己的獨立地址,主機發起通訊時,通過 SDA 信號線發送設備地址(SLAVE_ADDRESS)來查找從機。 I2C 協議規定設備地址可以是 7 位或 10 位,實際中 7 位的地址應用比較廣泛。緊跟設備地址的一個數據位用來表示數據傳輸方向,它是數據方向位(R/W-),第 8 位或第 11 位。數據方向位為“1”時表示主機由從機讀數據,該位為“0”時表示主機向從機寫數據。見圖 23-7。
讀數據方向時,主機會釋放對 SDA 信號線的控制,由從機控制 SDA 信號線,主機接收信號,寫數據方向時, SDA 由主機控制, 從機接收信號。
響應
I2C 的數據和地址傳輸都帶響應。響應包括“應答(ACK)”和“非應答(NACK)”兩種信號。作為數據接收端時,當設備(無論主從機)接收到 I2C 傳輸的一個字節數據或地址后,若希望對方繼續發送數據,則需要向對方發送“應答(ACK)”信號,發送方會繼續發送下一個數據;若接收端希望結束數據傳輸,則向對方發送“非應答(NACK)”信號,發送方接收到該信號后會產生一個停止信號,結束信號傳輸。見圖 23-8。
傳輸時主機產生時鍾,在第 9 個時鍾時,數據發送端會釋放 SDA 的控制權,由數據接收端控制 SDA,若 SDA 為高電平,表示非應答信號(NACK),低電平表示應答信號(ACK)。
STM32的I2C特性及架構
如果我們直接控制 STM32 的兩個 GPIO 引腳,分別用作 SCL 及 SDA,按照上述信號的時序要求,直接像控制 LED 燈那樣控制引腳的輸出(若是接收數據時則讀取 SDA 電平),就可以實現 I2C 通訊。同樣,假如我們按照 USART 的要求去控制引腳,也能實現 USART通訊。所以只要遵守協議,就是標准的通訊,不管您如何實現它,不管是 ST 生產的控制器還是 ATMEL 生產的存儲器, 都能按通訊標准交互。
由於直接控制 GPIO 引腳電平產生通訊時序時,需要由 CPU 控制每個時刻的引腳狀態,所以稱之為“軟件模擬協議”方式。
相對地,還有“硬件協議”方式, STM32 的 I2C 片上外設專門負責實現 I2C 通訊協議,只要配置好該外設,它就會自動根據協議要求產生通訊信號,收發數據並緩存起來, CPU只要檢測該外設的狀態和訪問數據寄存器,就能完成數據收發。這種由硬件外設處理 I2C協議的方式減輕了 CPU 的工作,且使軟件設計更加簡單。
STM32的I2C外設簡介
STM32 的 I2C 外設可用作通訊的主機及從機,支持 100Kbit/s 和 400Kbit/s 的速率,支持 7 位、 10 位設備地址,支持 DMA 數據傳輸,並具有數據校驗功能。它的 I2C 外設還支持 SMBus2.0 協議, SMBus 協議與 I2C 類似,主要應用於筆記本電腦的電池管理中,本教程不展開,感興趣的讀者可參考《SMBus20》文檔了解。
STM32的I2C架構剖析
1、通訊引腳
I2C 的所有硬件架構都是根據圖中左側 SCL 線和 SDA 線展開的(其中的 SMBA 線用於SMBUS 的警告信號, I2C 通訊沒有使用)。 STM32 芯片有多個 I2C 外設,它們的 I2C 通訊信號引出到不同的 GPIO 引腳上,使用時必須配置到這些指定的引腳,見表 23-1。關於GPIO 引腳的復用功能,可查閱《STM32F4xx 規格書》,以它為准。
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 信號線的輸出時鍾公式如下:
標准模式:
Thigh=CCRTPCKL1 Tlow = CCRTPCLK1
快速模式中 Tlow/Thigh=2 時:
Thigh = CCRTPCKL1 Tlow = 2CCR*TPCKL1
快速模式中 Tlow/Thigh=16/9 時:
Thigh = 9CCRTPCKL1 Tlow = 16CCRTPCKL1
例如,我們的 PCLK1=45MHz,想要配置 400Kbit/s 的速率,計算方式如下:
PCLK 時鍾周期:TPCLK1 = 1/45000000
目標 SCL 時鍾周期:TSCL = 1/400000
SCL 時鍾周期內的高電平時間: THIGH = TSCL/3
SCL 時鍾周期內的低電平時間: TLOW = 2*TSCL/3
計算 CCR 的值: CCR = THIGH/TPCLK1 = 37.5
計算結果為小數,而 CCR 寄存器是無法配置小數參數的,所以我們只能把 CCR 取值為 38,這樣 I2C 的 SCL 實際頻率無法達到 400KHz (約為 394736Hz)。要想它實際頻率達到400KHz,需要修改 STM32 的系統時鍾,把 PCLK1 時鍾頻率改成 10 的倍數才可以,但修改 PCKL 時鍾影響很多外設,所以一般我們不會修改它。 SCL 的實際頻率不達到 400KHz,除了通訊稍慢一點以外,不會對 I2C 的標准通訊造成其它影響。
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)”的不同數據位寫入參數,我們通過讀取這些寄存器標志來了解通訊狀態。
主發送器
見圖 23-10。圖中的是“主發送器”流程,即作為 I2C 通訊的主機端時,向外發送數據時的過程。
主發送器發送流程及事件說明如下:
(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),這個時候會產生EV2 事件, SR1 的 TXE 位及 BTF 位都被置 1,表示通訊結束。
假如使能I2C中斷,以上所有事件產生時,都會產生I2C中斷信號,進入同一個中斷服務函數,到I2C中斷服務函數程序后,再通過檢查寄存器位來了解是哪一個事件。
主接收器
再來分析主接收器過程,即作為 I2C 通訊的主機端時,從外部接收數據的過程,見圖23-11。
主接收器接收流程及事件說明如下:
(1) 同主發送流程,起始信號(S)是由主機端產生的,控制發生起始信號后,它產生事件“EV5”,並會對 SR1 寄存器的“SB”位置 1,表示起始信號已經發送;
(2) 緊接着發送設備地址並等待應答信號,若有從機應答,則產生事件“EV6”這時SR1 寄存器的“ADDR”位被置 1,表示地址已經發送。
(3) 從機端接收到地址后,開始向主機端發送數據。當主機接收到這些數據后,會產生“EV7”事件, SR1 寄存器的 RXNE 被置 1,表示接收數據寄存器非空,我們讀取該寄存器后,可對數據寄存器清空,以便接收下一次數據。此時我們可以控制 I2C 發送應答信號(ACK)或非應答信號(NACK),若應答,則重復以上步驟接收數據,若非應答,則停止傳輸;
(4) 發送非應答信號后,產生停止信號(P),結束傳輸。
在發送和接收過程中,有的事件不只是標志了我們上面提到的狀態位,還可能同時標志主機狀態之類的狀態位,而且讀了之后還需要清除標志位,比較復雜。我們可使用STM32 標准庫函數來直接檢測這些事件的復合標志,降低編程難度。
I2C初始化結構體詳解
跟其它外設一樣, STM32 標准庫提供了 I2C 初始化結構體及初始化函數來配置 I2C 外設。初始化結構體及函數定義在庫文件“stm32f4xx_i2c.h”及“stm32f4xx_i2c.c”中,編程時我們可以結合這兩個文件內的注釋使用或參考庫幫助文檔。了解初始化結構體后我們就能對 I2C 外設運用自如了,見代碼清單 23-1。
代碼清單 23-1 I2C 初始化結構體
typedef struct {
uint32_t I2C_ClockSpeed; /*!< 設置 SCL 時鍾頻率,此值要低於 40 0000*/
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;
這些結構體成員說明如下,其中括號內的文字是對應參數在 STM32 標准庫中定義的宏:
(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
本成員設置的是 I2C 的 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 位地址。
(5) I2C_Ack_Enable
本成員是關於 I2C 應答設置,設置為使能則可以發送響應信號。 該成員值一般配置為允許應答(I2C_Ack_Enable),這是絕大多數遵循 I2C 標准的設備的通訊要求,改為禁止應答(I2C_Ack_Disable)往往會導致通訊錯誤。
(6) I2C_AcknowledgeAddress
本成員選擇 I2C 的尋址模式是 7 位還是 10 位地址。這需要根據實際連接到 I2C 總線上設備的地址進行選擇,這個成員的配置也影響到 I2C_OwnAddress1 成員,只有這里設置成10 位模式時, I2C_OwnAddress1 才支持 10 位地址。
配置完這些結構體成員值,調用庫函數 I2C_Init 即可把結構體的配置寫入到寄存器中。
I2C讀寫實驗
GPIO 初始化結構體賦值,把引腳初始化成復用開漏模式,要注意 I2C 的引腳必須使用這種模式
向 EEPROM 寫入一個字節的數據
初始化好 I2C 外設后,就可以使用 I2C 通訊了,我們看看如何向 EEPROM 寫入一個字節的數據,見代碼清單 23-5。
代碼清單 23-5 向 EEPROM 寫入一個字節的數據
/***************************************************************/
/*通訊等待超時時間*/
#define I2CT_FLAG_TIMEOUT ((uint32_t)0x1000)
#define I2CT_LONG_TIMEOUT ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))
/**
* @brief I2C 等待事件超時的情況下會調用這個函數來處理
* @param errorCode:錯誤代碼,可以用來定位是哪個環節出錯.
* @retval 返回 0,表示 IIC 讀取失敗.
*/
static uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode)
{
/* 使用串口 printf 輸出錯誤信息,方便調試 */
EEPROM_ERROR("I2C 等待超時!errorCode = %d",errorCode);
return 0;
}
/**
* @brief 寫一個字節到 I2C EEPROM 中
* @param pBuffer:緩沖區指針
* @param WriteAddr:寫地址
* @retval 正常返回 1,異常返回 0
*/
uint32_t I2C_EE_ByteWrite(u8* pBuffer, u8 WriteAddr)
{
/* 產生 I2C 起始信號 */
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
/*設置超時等待時間*/
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 檢測 EV5 事件並清除標志*/
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(0);
}
/* 發送 EEPROM 設備地址 */
I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDRESS, I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 檢測 EV6 事件並清除標志*/
while (!I2C_CheckEvent(EEPROM_I2C,
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(1);
}
/* 發送要寫入的 EEPROM 內部地址(即 EEPROM 內部存儲器的地址) */
I2C_SendData(EEPROM_I2C, WriteAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 檢測 EV8 事件並清除標志*/
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(2);
}
/* 發送一字節要寫入的數據 */
I2C_SendData(EEPROM_I2C, *pBuffer);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 檢測 EV8 事件並清除標志*/
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(3);
}
/* 發送停止信號 */
I2C_GenerateSTOP(EEPROM_I2C, ENABLE);
return 1;
}
先來分析 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 的自己定義的單字節寫入時序,見圖 23-14。
EEPROM 的單字節時序規定,向它寫入數據的時候,第一個字節為內存地址,第二個字節是要寫入的數據內容。所以我們需要理解:命令、地址的本質都是數據,對數據的解釋不同,它就有了不同的功能。
多字節寫入及狀態等待
單字節寫入通訊結束后, EEPROM 芯片會根據這個通訊結果擦寫該內存地址的內容,這需要一段時間,所以我們在多次寫入數據時,要先等待 EEPROM 內部擦寫完畢。多個數據寫入過程見代碼清單 23-6。
代碼清單 23-6 多字節寫入
/**
* @brief 將緩沖區中的數據寫到 I2C EEPROM 中,采用單字節寫入的方式,
速度比頁寫入慢
* @param pBuffer:緩沖區指針
* @param WriteAddr:寫地址
* @param NumByteToWrite:寫的字節數
* @retval 無
*/
uint8_t I2C_EE_ByetsWrite(uint8_t* pBuffer,uint8_t WriteAddr, uint16_t NumByteToWrite)
{
uint16_t i;
uint8_t res;
/*每寫一個字節調用一次 I2C_EE_ByteWrite 函數*/
for (i=0; i<NumByteToWrite; i++)
{
/*等待 EEPROM 准備完畢*/
I2C_EE_WaitEepromStandbyState();
/*按字節寫入數據*/
res = I2C_EE_ByteWrite(pBuffer++,WriteAddr++);
}
return res;
}
這段代碼比較簡單,直接使用 for 循環調用前面定義的 I2C_EE_ByteWrite 函數一個字節 一 個 字 節 地 向 EEPROM 發 送 要 寫 入 的 數 據 。 在 每 次 數 據 寫 入 通 訊 前 調 用 了I2C_EE_WaitEepromStandbyState 函數等待 EEPROM 內部擦寫完畢,該函數的定義見代碼
清單 23-7。
代碼清單 23-7 等待 EEPROM 處於准備狀態
//等待 Standby 狀態的最大次數
#define MAX_TRIAL_NUMBER 300
/**
* @brief 等待 EEPROM 到准備狀態
* @param 無
* @retval 正常返回 1,異常返回 0
*/
static uint8_t I2C_EE_WaitEepromStandbyState(void)
{
__IO uint16_t tmpSR1 = 0;
__IO uint32_t EETrials = 0;
/*總線忙時等待 */
I2CTimeout = I2CT_LONG_TIMEOUT;
while (I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(20);
}
/* 等待從機應答,最多等待 300 次 */
while (1)
{
/*開始信號 */
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
/* 檢測 EV5 事件並清除標志*/
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(21);
}
/* 發送 EEPROM 設備地址 */
I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDRESS,I2C_Direction_Transmitter);
/* 等待 ADDR 標志 */
I2CTimeout = I2CT_LONG_TIMEOUT;
do
{
/* 獲取 SR1 寄存器狀態 */
tmpSR1 = EEPROM_I2C->SR1;
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(22);
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(22);
}
/* 一直等待直到 addr 及 af 標志為 1 */
while ((tmpSR1 & (I2C_SR1_ADDR | I2C_SR1_AF)) == 0);
/*檢查 addr 標志是否為 1 */
if (tmpSR1 & I2C_SR1_ADDR)
{
/* 清除 addr 標志該標志通過讀 SR1 及 SR2 清除 */
(void)EEPROM_I2C->SR2;
/*產生停止信號 */
I2C_GenerateSTOP(EEPROM_I2C, ENABLE);
/* 退出函數 */
return 1;
}
else
{
/*清除 af 標志 */
I2C_ClearFlag(EEPROM_I2C, I2C_FLAG_AF);
}
/*檢查等待次數*/
if (EETrials++ == MAX_TRIAL_NUMBER)
{
/* 等待 MAX_TRIAL_NUMBER 次都還沒准備好,退出等待 */
return I2C_TIMEOUT_UserCallback(23);
}
}
}
這個函數主要實現是向 EEPROM 發送它設備地址,檢測 EEPROM 的響應,若EEPROM 接收到地址后返回應答信號,則表示 EEPROM 已經准備好,可以開始下一次通訊。函數中檢測響應是通過讀取 STM32 的 SR1 寄存器的 ADDR 位及 AF 位來實現的,當I2C 設備響應了地址的時候, ADDR 會置 1,若應答失敗, AF 位會置 1。
EEPROM 的頁寫入
在以上的數據通訊中,每寫入一個數據都需要向 EEPROM 發送寫入的地址,我們希望向連續地址寫入多個數據的時候,只要告訴 EEPROM 第一個內存地址 address1,后面的數據按次序寫入到 address2、 address3… 這樣可以節省通訊的內容,加快速度。為應對這種需求, EEPROM 定義了一種頁寫入時序,見圖 23-15。
根據頁寫入時序,第一個數據被解釋為要寫入的內存地址 address1,后續可連續發送 n 個數據,這些數據會依次寫入到內存中。其中 AT24C02 型號的芯片頁寫入時序最多可以一次發送 8 個數據(即 n = 8 ),該值也稱為頁大小,某些型號的芯片每個頁寫入時序最多可傳輸16 個數據。 EEPROM 的頁寫入代碼實現見代碼清單 23-8。
代碼清單 23-8 EEPROM 的頁寫入
/**
* @brief 在 EEPROM 的一個寫循環中可以寫多個字節,但一次寫入的字節數
* 不能超過 EEPROM 頁的大小, AT24C02 每頁有 8 個字節
* @param
* @param pBuffer:緩沖區指針
* @param WriteAddr:寫地址
* @param NumByteToWrite:要寫的字節數要求 NumByToWrite 小於頁大小
* @retval 正常返回 1,異常返回 0
*/
uint8_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint8_t NumByteToWrite)
{
I2CTimeout = I2CT_LONG_TIMEOUT;
while (I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(4);
}
/* 產生 I2C 起始信號 */
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 檢測 EV5 事件並清除標志 */
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(5);
}
/* 發送 EEPROM 設備地址 */
I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDRESS,I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 檢測 EV6 事件並清除標志*/
while (!I2C_CheckEvent(EEPROM_I2C, 39 I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(6);
}
/* 發送要寫入的 EEPROM 內部地址(即 EEPROM 內部存儲器的地址) */
I2C_SendData(EEPROM_I2C, WriteAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 檢測 EV8 事件並清除標志*/
while (! I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(7);
}
/* 循環發送 NumByteToWrite 個數據 */
while (NumByteToWrite--)
{
/* 發送緩沖區中的數據 */
I2C_SendData(EEPROM_I2C, *pBuffer);
/* 指向緩沖區中的下一個數據 */
/* 指向緩沖區中的下一個數據 */
pBuffer++;
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 檢測 EV8 事件並清除標志*/
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(8);
}
}
/* 發送停止信號 */
I2C_GenerateSTOP(EEPROM_I2C, ENABLE);
return 1;
}
這段頁寫入函數主體跟單字節寫入函數是一樣的,只是它在發送數據的時候,使用 for循環控制發送多個數據,發送完多個數據后才產生 I2C 停止信號,只要每次傳輸的數據小於等於 EEPROM 時序規定的頁大小,就能正常傳輸。
快速寫入多字節
利用 EEPROM 的頁寫入方式,可以改進前面的“多字節寫入”函數,加快傳輸速度,見代碼清單 23-9
代碼清單 23-9 快速寫入多字節函數
/* AT24C01/02 每頁有 8 個字節 */
#define I2C_PageSize 8
/**
* @brief 將緩沖區中的數據寫到 I2C EEPROM 中,采用頁寫入的方式,加快寫入速度
* @param pBuffer:緩沖區指針
* @param WriteAddr:寫地址
* @param NumByteToWrite:寫的字節數* @retval 無
*/
void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr, u16 NumByteToWrite)
{
uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;
/*mod 運算求余,若 writeAddr 是 I2C_PageSize 整數倍,運算結果 Addr 值為 0*/
Addr = WriteAddr % I2C_PageSize;
/*差 count 個數據,剛好可以對齊到頁地址*/
count = I2C_PageSize - Addr;
/*計算出要寫多少整數頁*/
NumOfPage = NumByteToWrite / I2C_PageSize;
/*mod 運算求余,計算出剩余不滿一頁的字節數*/
NumOfSingle = NumByteToWrite % I2C_PageSize;
/* Addr=0,則 WriteAddr 剛好按頁對齊 aligned */
if (Addr == 0)
{
/* 如果 NumByteToWrite < I2C_PageSize */
if (NumOfPage == 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
/* 如果 NumByteToWrite > I2C_PageSize */
else
{
/*先把整數頁都寫了*/
while (NumOfPage--)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
I2C_EE_WaitEepromStandbyState();
WriteAddr += I2C_PageSize;
pBuffer += I2C_PageSize;
}
/*若有多余的不滿一頁的數據,把它寫完*/
if (NumOfSingle!=0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
}
}
/* 如果 WriteAddr 不是按 I2C_PageSize 對齊 */
else
{
/* 如果 NumByteToWrite < I2C_PageSize */
if (NumOfPage== 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
/* 如果 NumByteToWrite > I2C_PageSize */
else
{
/*地址不對齊多出的 count 分開處理,不加入這個運算*/
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / I2C_PageSize;
NumOfSingle = NumByteToWrite % I2C_PageSize;
/*先把 WriteAddr 所在頁的剩余字節寫了*/
if (count != 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, count);
I2C_EE_WaitEepromStandbyState();
/*WriteAddr 加上 count 后,地址就對齊到頁了*/
WriteAddr += count;
pBuffer += count;
}
/*把整數頁都寫了*/
while (NumOfPage--)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
I2C_EE_WaitEepromStandbyState();
WriteAddr += I2C_PageSize;
pBuffer += I2C_PageSize;
}
/*若有多余的不滿一頁的數據,把它寫完*/
if (NumOfSingle != 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
}
}
}
很多讀者覺得這段代碼的運算很復雜,看不懂,其實它的主旨就是對輸入的數據進行分頁(本型號芯片每頁 8 個字節),見表 23-2。通過“整除”計算要寫入的數據NumByteToWrite 能寫滿多少“完整的頁”,計算得的值存儲在 NumOfPage 中,但有時數據不是剛好能寫滿完整頁的,會多一點出來,通過“求余”計算得出“不滿一頁的數據個數”就存儲在 NumOfSingle 中。計算后通過按頁傳輸 NumOfPage 次整頁數據及最后的NumOfSingle 個數據,使用頁傳輸,比之前的單個字節數據傳輸要快很多。
除了基本的分頁傳輸,還要考慮首地址的問題,見表 23-3。若首地址不是剛好對齊到頁的首地址,會需要一個 count 值,用於存儲從該首地址開始寫滿該地址所在的頁,還能寫多少個數據。實際傳輸時,先把這部分 count 個數據先寫入,填滿該頁,然后把剩余的數據(NumByteToWrite-count),再重復上述求出 NumOPage 及 NumOfSingle 的過程,按頁傳輸到 EEPROM。
-
若 writeAddress=16,計算得 Addr=16%8= 0 , count=8-0= 8;
-
同時,若 NumByteToWrite =22,計算得 NumOfPage=22/8= 2, NumOfSingle=22%8= 6
-
數據傳輸情況如表 23-2
-
若 writeAddress=17,計算得 Addr=17%8= 1, count=8-1= 7;
-
同時,若 NumByteToWrite =22,
-
先把 count 去掉,特殊處理,計算得新的 NumByteToWrite =22-7= 15
-
計算得 NumOfPage=15/8= 1, NumOfSingle=15%8= 7。
-
數據傳輸情況如表 23-3
最后,強調一下, EEPROM 支持的頁寫入只是一種加速的 I2C 的傳輸時序,實際上並不要求每次都以頁為單位進行讀寫, EEPROM 是支持隨機訪問的(直接讀寫任意一個地址),如前面的單個字節寫入。在某些存儲器,如 NAND FLASH,它是必須按照 Block 寫入的,例如每個 Block 為 512 或 4096 字節,數據寫入的最小單位是 Block,寫入前都需要擦除整個 Block; NOR FLASH 則是寫入前必須以 Sector/Block 為單位擦除,然后才可以按字節寫入。 而我們的 EEPROM 數據寫入和擦除的最小單位是“字節”而不是“頁”,數據寫入前不需要擦除整頁。
從 EEPROM 讀取數據
從 EEPROM 讀取數據是一個復合的 I2C 時序,它實際上包含一個寫過程和一個讀過程,見圖 23-16。
讀時序的第一個通訊過程中,使用 I2C 發送設備地址尋址(寫方向),接着發送要讀取的“內存地址”;第二個通訊過程中,再次使用 I2C 發送設備地址尋址,但這個時候的數據方向是讀方向;在這個過程之后, EEPROM 會向主機返回從“內存地址”開始的數據,一個字節一個字節地傳輸,只要主機的響應為“應答信號”,它就會一直傳輸下去,主機想結束傳輸時,就發送“非應答信號”,並以“停止信號”結束通訊,作為從機的EEPROM 也會停止傳輸。實現代碼見代碼清單 23-10。
代碼清單 23-10 從 EEPROM 讀取數據
/**
* @brief 從 EEPROM 里面讀取一塊數據
* @param pBuffer:存放從 EEPROM 讀取的數據的緩沖區指針
* @param ReadAddr:接收數據的 EEPROM 的地址
* @param NumByteToRead:要從 EEPROM 讀取的字節數
* @retval 正常返回 1,異常返回 0
*/
uint8_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr,u16 NumByteToRead)
{
I2CTimeout = I2CT_LONG_TIMEOUT;
while (I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(9);
}
/* 產生 I2C 起始信號 */
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 檢測 EV5 事件並清除標志*/
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(10);
}
/* 發送 EEPROM 設備地址 */
I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDRESS,I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 檢測 EV6 事件並清除標志*/
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(11);
}
/*通過重新設置 PE 位清除 EV6 事件 */
I2C_Cmd(EEPROM_I2C, ENABLE);
/* 發送要讀取的 EEPROM 內部地址(即 EEPROM 內部存儲器的地址) */
I2C_SendData(EEPROM_I2C, ReadAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 檢測 EV8 事件並清除標志*/
while(!I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(12);
}
/* 產生第二次 I2C 起始信號 */
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 檢測 EV5 事件並清除標志*/
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(13);
}
/* 發送 EEPROM 設備地址 */
I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDRESS, I2C_Direction_Receiver);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 檢測 EV6 事件並清除標志*/
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(14);
}
/* 讀取 NumByteToRead 個數據*/
while (NumByteToRead)
{
/*若 NumByteToRead=1,表示已經接收到最后一個數據了,發送非應答信號,結束傳輸*/
if (NumByteToRead == 1)
{
/* 發送非應答信號 */
I2C_AcknowledgeConfig(EEPROM_I2C, DISABLE);
/* 發送停止信號 */
I2C_GenerateSTOP(EEPROM_I2C, ENABLE);
}
I2CTimeout = I2CT_LONG_TIMEOUT;
while (I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_RECEIVED)==0)
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(3);
}
{
/*通過 I2C,從設備中讀取一個字節的數據 */
*pBuffer = I2C_ReceiveData(EEPROM_I2C);
/* 存儲數據的指針指向下一個地址 */
pBuffer++;
/* 接收數據自減 */
NumByteToRead--;
}
}
/* 使能應答,方便下一次 I2C 傳輸 */
I2C_AcknowledgeConfig(EEPROM_I2C, ENABLE);
return 1;
}
參考引用:
- 野火---《零死角玩轉STM32-F429挑戰者》
- 《STM32F4xx中文參考手冊》
- 《Cortex-M4內核編程手冊》