第23章 I2C—讀寫EEPR


本章參考資料:《STM32F76xxx參考手冊》、《STM32F7xx規格書》、庫幫助文檔《STM32F779xx_User_Manual.chm》及《I2C總線協議》。

若對I2C通訊協議不了解,可先閱讀《I2C總線協議》文檔的內容學習。若想了解SMBUS,可閱讀《smbus20》文檔。

關於EEPROM存儲器,請參考“常用存儲器介紹”章節,實驗中的EEPROM,請參考其規格書《AT24C02》來了解。

23.1  I2C協議簡介

I2C 通訊協議(InterIntegrated Circuit)是由Phiilps公司開發的,由於它引腳少,硬件實現簡單,可擴展性強,不需要USARTCAN等通訊協議的外部收發設備,現在被廣泛地使用在系統內多個集成電路(IC)間的通訊。

下面我們分別對I2C協議的物理層及協議層進行講解。

23.1.1  I2C物理層

I2C通訊設備之間的常用連接方式見 23-1

23-1 常見的I2C通訊系統

它的物理層有如下特點:

(1) 它是一個支持多設備的總線。“總線”指多個設備共用的信號線。在一個I2C通訊總線中,可連接多個I2C通訊設備,支持多個通訊主機及多個通訊從機。

(2) 一個I2C總線只使用兩條總線線路,一條雙向串行數據線(SDA) ,一條串行時鍾線 (SCL)。數據線即用來表示數據,時鍾線用於數據收發同步。

(3) 每個連接到總線的設備都有一個獨立的地址,主機可以利用這個地址進行不同設備之間的訪問。

(4) 總線通過上拉電阻接到電源。當I2C設備空閑時,會輸出高阻態,而當所有設備都空閑,都輸出高阻態時,由上拉電阻把總線拉成高電平。

(5) 多個主機同時使用總線時,為了防止數據沖突,會利用仲裁方式決定由哪個設備占用總線。

(6) 具有三種傳輸模式:標准模式傳輸速率為100kbit/s ,快速模式為400kbit/s ,高速模式下可達1Mbit/s,但目前大多I2C設備尚不支持高速模式。

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

1.1.2  協議層

I2C的協議定義了通訊的起始和停止信號、數據有效性、響應、仲裁、時鍾同步和地址廣播等環節

1. I2C基本讀寫過程

先看看I2C通訊過程的基本結構,它的通訊過程見 23-2 23-3 23-4

23-2 主機寫數據到從機

23-3 主機由從機中讀數

23-4 I2C通訊復合格式

圖例:  數據由主機傳輸至從機     S : 傳輸開始信號

   SLAVE_ADDRESS: 從機地址

       數據由從機傳輸至主機    R/W(——) 傳輸方向選擇位,1為讀,0為寫

        A/A(——)  應答(ACK)或非應答(NACK)信號

         P   : 停止傳輸信號

這些圖表示的是主機和從機通訊時,SDA線的數據包序列。

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

起始信號產生后,所有從機就開始等待主機緊接下來廣播 從機地址信號 (SLAVE_ADDRESS)。在I2C總線上,每個設備的地址都是唯一的,當主機廣播的地址與某個設備地址相同時,這個設備就被選中了,沒被選中的設備將會忽略之后的數據信號。根據I2C協議,這個從機地址可以是710

在地址位之后,是傳輸方向的選擇位,該位為0時,表示后面的數據傳輸方向是由主機傳輸至從機,即主機向從機寫數據。該位為1時,則相反,即主機由從機讀數據。

從機接收到匹配的地址后,主機或從機會返回一個應答(ACK)或非應答(NACK)信號,只有接收到應答信號后,主機才能繼續發送或接收數據。

 若配置的方向傳輸位為寫數據”方向,即第一幅圖的情況,廣播完地址,接收到應答信號后,主機開始正式向從機傳輸數據(DATA),數據包的大小為8,主機每發送完一個字節數據,都要等待從機的應答信號(ACK),重復這個過程,可以向從機傳輸N個數據,這個N沒有大小限制。當數據傳輸結束時,主機向從機發送一個停止傳輸信號(P),表示不再傳輸數據。

若配置的方向傳輸位為讀數據”方向,即第二幅圖的情況,廣播完地址,接收到應答信號后,從機開始向主機返回數據(DATA),數據包大小也為8,從機每發送完一個數據,都會等待主機的應答信號(ACK),重復這個過程,可以返回N個數據,這個N也沒有大小限制。當主機希望停止接收數據時,就向從機返回一個非應答信號(NACK),則從機自動停止數據傳輸。

除了基本的讀寫,I2C通訊更常用的是復合格式,即第三幅圖的情況,該傳輸過程有兩次起始信號(S)。一般在第一次傳輸中,主機通過 SLAVE_ADDRESS尋找到從設備后,發送一段“數據”,這段數據通常用於表示從設備內部的寄存器或存儲器地址(注意區分它與SLAVE_ADDRESS的區別);在第二次的傳輸中,對該地址的內容進行讀或寫。也就是說,第一次通訊是告訴從機讀寫地址,第二次則是讀寫的實際內容。

以上通訊流程中包含的各個信號分解如下:

1. 通訊的起始和停止信號

前文中提到的起始(S)和停止(P)信號是兩種特殊的狀態,見 23-5。當 SCL 線是高電平時 SDA 線從高電平向低電平切換,這個情況表示通訊的起始。當 SCL 是高電平時 SDA 線由低電平向高電平切換,表示通訊的停止。起始和停止信號一般由主機產生。

23-5 起始和停止信號

2. 數據有效性

I2C使用SDA信號線來傳輸數據,使用SCL信號線進行數據同步。見 236SDA數據線在SCL的每個時鍾周期傳輸一位數據。傳輸時,SCL為高電平的時候SDA表示的數據有效,即此時的SDA為高電平時表示數據“1”,為低電平時表示數據“0”。當SCL為低電平時,SDA的數據無效,一般在這個時候SDA進行電平切換,為下一次表示數據做好准備。

23-6 數據有效性

每次數據傳輸都以字節為單位,每次傳輸的字節數不受限制。

3. 地址及數據方向

I2C總線上的每個設備都有自己的獨立地址,主機發起通訊時,通過SDA信號線發送設備地址(SLAVE_ADDRESS)來查找從機。I2C協議規定設備地址可以是7位或10位,實際中7位的地址應用比較廣泛。緊跟設備地址的一個數據位用來表示數據傳輸方向,它是數據方向位(R/W(——)),第8位或第11位。數據方向位為“1”時表示主機由從機讀數據,該位為“0”時表示主機向從機寫數據。見 23-7

23-7 設備地址(7)及數據傳輸方向

讀數據方向時,主機會釋放對SDA信號線的控制,由從機控制SDA信號線,主機接收信號,寫數據方向時,SDA由主機控制,從機接收信號。

4. 響應

I2C的數據和地址傳輸都帶響應。響應包括“應答(ACK)”和“非應答(NACK)”兩種信號。作為數據接收端時,當設備(無論主從機)接收到I2C傳輸的一個字節數據或地址后,若希望對方繼續發送數據,則需要向對方發送“應答(ACK)”信號,發送方會繼續發送下一個數據;若接收端希望結束數據傳輸,則向對方發送“非應答(NACK)”信號,發送方接收到該信號后會產生一個停止信號,結束信號傳輸。見 23-8

23-8 響應與非響應信號

傳輸時主機產生時鍾,在第9個時鍾時,數據發送端會釋放SDA的控制權,由數據接收端控制SDA,若SDA為高電平,表示非應答信號(NACK),低電平表示應答信號(ACK)

23.2  STM32I2C特性及架構

如果我們直接控制STM32的兩個GPIO引腳,分別用作SCLSDA,按照上述信號的時序要求,直接像控制LED燈那樣控制引腳的輸出(若是接收數據時則讀取SDA電平),就可以實現I2C通訊。同樣,假如我們按照USART的要求去控制引腳,也能實現USART通訊。所以只要遵守協議,就是標准的通訊,不管您如何實現它,不管是ST生產的控制器還是ATMEL生產的存儲器, 都能按通訊標准交互。

由於直接控制GPIO引腳電平產生通訊時序時,需要由CPU控制每個時刻的引腳狀態,所以稱之為“軟件模擬協議”方式。

相對地,還有“硬件協議”方式,STM32I2C片上外設專門負責實現I2C通訊協議,只要配置好該外設,它就會自動根據協議要求產生通訊信號,收發數據並緩存起來,CPU只要檢測該外設的狀態和訪問數據寄存器,就能完成數據收發。這種由硬件外設處理I2C協議的方式減輕了CPU的工作,且使軟件設計更加簡單。

23.2.1  STM32I2C外設簡介

STM32I2C外設可用作通訊的主機及從機,支持標准速度模式(高達100Kbit/s)、快速模式(高達400Kbit/s)、超快速模式(高達1Mbit/s),支持7位、10位設備地址,支持DMA數據傳輸,並具有數據校驗功能。它的I2C外設還支持SMBus2.0協議,SMBus協議與I2C類似,主要應用於筆記本電腦的電池管理中,本教程不展開,感興趣的讀者可參考《SMBus2.0》文檔了解。

23.2.2  STM32I2C架構剖

23-9 I2C架構圖

1. 通訊引腳

I2C的所有硬件架構都是根據圖中左側SCL線和SDA線展開的(其中的SMBA線用於SMBUS的警告信號,I2C通訊沒有使用)STM32芯片有多個I2C外設,它們的I2C通訊信號引出到不同的GPIO引腳上,使用時必須配置到這些指定的引腳,見 231。關於GPIO引腳的復用功能,可查閱《STM32F7xx規格書》,以它為准。

231 STM32F7xxI2C引腳(整理自《STM32F7xx規格書》)

引腳

I2C編號

I2C1

I2C2

I2C3

I2C4

SCL

PB6/PB8

PH4/PF1/PB10

PH7/PA8

PD12/PF14/PH11

SDA

PB7/PB9

PH5/PF0/PB11

PH8/PC9

PD13/PF15/PH12

2. 噪聲濾波器

模擬噪聲濾波器,集成於SDASCL的輸入上,默認情況下是打開的,該模擬濾波器符合I2C規范,此規范要求在快速模式和超快速模式下對脈寬50ns以下的脈沖都要抑制。可以空過將寄存器I2C_CR1ANFOFF位置1,注意該位只能在I2C禁止時(PE=0)時編程。

數字噪聲濾波器,從框圖可以看出它是SDASCL經過模擬噪聲濾波器再進來的,通過配置 I2C_CR1 寄存器中的 DNF[3:0] 位來使能數字濾波器使能數字濾波器,數字濾波器可濾除脈寬 DNF[3:0] *以下的尖峰,可濾除的噪聲尖峰脈寬從 1 15 I2CCLK 周期可編程。如果模擬濾波器已使能,數字濾波將疊加在模擬濾波之上。

 

3. 時鍾源及要求

I2C 的時鍾由獨立時鍾源提供,這使得 I2C 能夠獨立於 PCLK 頻率工作。該獨立時鍾源可從以下三種時鍾源中任選其一:

q PCLK1APB1時鍾(默認值)

q HIS:高速內部振盪器

q  SYSCLK:系統時鍾。

I2C 內核的時鍾由 I2CCLK 提供。I2CCLK 周期 必須遵循以下條件:

4. I2C時鍾控制

使用I2C必須配置時序,以便保證主模式和從模式下使用正確的數據保持和建立時間。通過設置 I2C_TIMINGR 寄存器中的 SCLH SCLL 位來配置 I2C 主時鍾。具體是指 I2C_TIMINGR 寄存器中的 PRESC[3:0]SCLDEL[3:0] SDADEL[3:0] 位。ST已經專用做了一款工具來計算I2C_TIMINGR 寄存器的值,可以在我們參考工具文件夾找到。例如我們要產生標准的100KHzI2C主設備時序,在序號1的框中依次填入Device Mode MasterMasterI2C Speed Mode:Standard ModeI2C Speed Frequency(KHz)100I2C Clock Source Frequency(KHz)54000Analog Filter DelayONCoefficient of Digital Filt0Rise Time(ns)100Fall Time(ns)10,最后在右側序號2的框中找到Run按鈕即可生成TIMINGR 寄存器的值:0x60201E2B,雙擊即可復制,最后粘貼在MDK的I2C初始化源碼中就可以完成初始化。這樣非常方便,避免頭痛的計算。

 

23-10 I2C時序計算工具

 

下面我們來講解初始化I2C時鍾的計算方法,為了支持多主環境和從時鍾延長, I2C 實現了時鍾同步機制。為了實現時鍾同步,需執行以下操作:

 

使用 SCLL 計數器從 SCL 低電平內部檢測開始對時鍾的低電平進行計數。

 

使用 SCLH 計數器從 SCL 高電平內部檢測開始對時鍾的高電平進行計數。

 

5. 數據控制邏輯

I2CSDA信號主要連接到數據移位寄存器上,數據移位寄存器的數據來源及目標是數據寄存器(DR)、地址寄存器(OAR)PEC寄存器以及SDA數據線。當向外發送數據的時候,數據移位寄存器以“數據寄存器”為數據源,把數據一位一位地通過SDA信號線發送出去;當從外部接收數據的時候,數據移位寄存器把SDA信號線采樣到的數據一位一位地存儲到“數據寄存器”中。若使能了數據校驗,接收到的數據會經過PCE計算器運算,運算結果存儲在“PEC寄存器”中。當STM32I2C工作在從機模式的時候,接收到設備地址信號時,數據移位寄存器會把接收到的地址與STM32的自身的“I2C地址寄存器”的值作比較,以便響應主機的尋址。STM32的自身I2C地址可通過修改“自身地址寄存器”修改,支持同時使用兩個I2C設備地址,兩個地址分別存儲在OAR1OAR2中。

6. 整體控制邏輯

整體控制邏輯負責協調整個I2C外設,控制邏輯的工作模式根據我們配置的“控制寄存器(CR1/CR2)”的參數而改變。在外設工作時,控制邏輯會根據外設的工作狀態修改“狀態寄存器(SR1SR2)”,我們只要讀取這些寄存器相關的寄存器位,就可以了解I2C的工作狀態了。除此之外,控制邏輯還根據要求,負責控制產生I2C中斷信號、DMA請求及各種I2C的通訊信號(起始、停止、響應信號等)

23.2.3  通訊過程

使用I2C外設通訊時,在通訊的不同階段它會對“狀態寄存器(SR1SR2)”的不同數據位寫入參數,我們通過讀取這些寄存器標志來了解通訊狀態。

1. 主發送器

23-11。圖中的是“主發送器”流程,即作為I2C通訊的主機端時,向外發送數據時的過程。

 

23-11 主發送器通訊過程

主發送器發送流程及事件說明如下:

(1) 控制產生起始信號(S),當發生起始信號后,它產生事件“EV5”,並會對SR1寄存器的“SB”位置1,表示起始信號已經發送;

(2) 緊接着發送設備地址並等待應答信號,若有從機應答,則產生事件EV6”及“EV8”,這時SR1寄存器的“ADDR”位及“TXE”位被置1ADDR 1表示地址已經發送,TXE1表示數據寄存器為空;

(3) 以上步驟正常執行並對ADDR位清零后,我們往I2C的“數據寄存器DR”寫入要發送的數據,這時TXE位會被重置0,表示數據寄存器非空,I2C外設通過SDA信號線一位位把數據發送出去后,又會產生“EV8”事件,即TXE位被置1,重復這個過程,就可以發送多個字節數據了;

(4) 當我們發送數據完成后,控制I2C設備產生一個停止信號(P),這個時候會產生EV2事件,SR1TXE位及BTF位都被置1,表示通訊結束。

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

2. 主接收器

再來分析主接收器過程,即作為I2C通訊的主機端時,從外部接收數據的過程,見 23-12

23-12 主接收器過程

主接收器接收流程及事件說明如下:

(1) 同主發送流程,起始信號(S)是由主機端產生的,控制發生起始信號后,它產生事件“EV5”,並會對SR1寄存器的“SB”位置1,表示起始信號已經發送;

(2) 緊接着發送設備地址並等待應答信號,若有從機應答,則產生事件EV6”這時SR1寄存器的“ADDR”位被置1,表示地址已經發送。

(3) 從機端接收到地址后,開始向主機端發送數據。當主機接收到這些數據后,會產生EV7”事件,SR1寄存器的RXNE被置1,表示接收數據寄存器非空,我們讀取該寄存器后,可對數據寄存器清空,以便接收下一次數據。此時我們可以控制I2C發送應答信號(ACK)或非應答信號(NACK),若應答,則重復以上步驟接收數據,若非應答,則停止傳輸;

(4) 發送非應答信號后,產生停止信號(P),結束傳輸。

在發送和接收過程中,有的事件不只是標志了我們上面提到的狀態位,還可能同時標志主機狀態之類的狀態位,而且讀了之后還需要清除標志位,比較復雜。我們可使用STM32 HAL庫函數來直接檢測這些事件的復合標志,降低編程難度。

23.3  I2C初始化結構體詳解

跟其它外設一樣,STM32 HAL庫提供了I2C初始化結構體及初始化函數來配置I2C外設。初始化結構體及函數定義在庫文件“stm32f7xx_hal_i2c.h”及“stm32f7xx_hal_i2c.c”中,編程時我們可以結合這兩個文件內的注釋使用或參考庫幫助文檔。了解初始化結構體后我們就能對I2C外設運用自如了,見代碼清單 231-

代碼清單 23-1 I2C初始化結構體

1 typedef struct {

 2     uint32_t Timing;

 3     /*指定I2C_TIMINGR寄存器的值,可以通過I2C_TIMING_CONFIGURARION工具計算*/

 4

 5     uint32_t OwnAddress1;  /*指定自身的I2C設備地址1,可以是 7-bit或者10-bit*/

 6

 7     uint32_t AddressingMode; /*指定地址的長度模式,可以是7bit模式或者10bit模式 */

 8

 9     uint32_t DualAddressMode;  /*設置雙地址模式 */

10

11     uint32_t OwnAddress2; /*指定自身的I2C設備地址2只能 7-bit */

12

13     uint32_t OwnAddress2Masks;    /*指定當雙地址模式時的掩碼 */

14

15     uint32_t GeneralCallMode;     /*指定廣播呼叫模式 */

16

17     uint32_t NoStretchMode;       /*指定禁止時鍾延長模*/

18

19 } I2C_InitTypeDef;

這些結構體成員說明如下,其中括號內的文字是對應參數在STM32 HAL庫中定義的宏:

(1) Timing

本成員設置的是I2C的傳輸速率,在調用初始化函數時,函數會根據我們輸入的數值寫入到I2C的時鍾控制寄存器CCR。這個數值的計算上一節已經說明。

(2) OwnAddress1

本成員配置的是STM32I2C設備自身地址1,每個連接到I2C總線上的設備都要有一個自己的地址,作為主機也不例外。地址可設置為7位或10(受下面(3) AddressingMode成員決定),只要該地址是I2C總線上唯一的即可。

STM32I2C外設可同時使用兩個地址,即同時對兩個地址作出響應,這個結構成員I2C_OwnAddress1配置的是默認的、OAR1寄存器存儲的地址,若需要設置第二個地址寄存器OAR2,可使用DualAddressMode成員使能,然后設置OwnAddress2成員即可,OAR2不支持10位地址。

(3) AddressingMode

本成員選擇I2C的尋址模式是7位還是10位地址。這需要根據實際連接到I2C總線上設備的地址進行選擇,這個成員的配置也影響到OwnAddress1成員,只有這里設置成10位模式時, OwnAddress1才支持10位地址。

(4) DualAddressMode

本成員配置的是STM32I2C設備自己的地址,每個連接到I2C總線上的設備都要有一個自己的地址,作為主機也不例外。地址可設置為7位或10(受下面I2C_AcknowledgeAddress成員決定),只要該地址是I2C總線上唯一的即可。

STM32I2C外設可同時使用兩個地址,即同時對兩個地址作出響應,這個結構成員I2C_OwnAddress1配置的是默認的、OAR1寄存器存儲的地址,若需要設置第二個地址寄存器OAR2,可使用I2C_OwnAddress2Config函數來配置,OAR2不支持10位地址。

(5) OwnAddress2

本成員配置的是STM32I2C設備自身地址2,每個連接到I2C總線上的設備都要有一個自己的地址,作為主機也不例外。地址可設置為7位,只要該地址是I2C總線上唯一的即可。

(6) OwnAddress2Masks

本成員指定I2C的雙地址模式時的掩碼。

(7) GeneralCallMode

本成員是關於I2C從模式時的廣播呼叫模式設置。

(8) NoStretchMode

本成員是關於I2C禁止時鍾延長模式設置,用於在從模式下禁止時鍾延長。它在主模式下必須保持關閉。

 

配置完這些結構體成員值,調用庫函數HAL_I2C_Init即可把結構體的配置寫入到寄存器中。

23.4  I2C—讀寫EEPROM實驗

EEPROM是一種掉電后數據不丟失的存儲器,常用來存儲一些配置信息,以便系統重新上電的時候加載之。EEPOM芯片最常用的通訊方式就是I2C協議,本小節以EEPROM的讀寫實驗為大家講解STM32I2C使用方法。實驗中STM32I2C外設采用主模式,分別用作主發送器和主接收器,通過查詢事件的方式來確保正常通訊。

23.4.1  硬件設計

 

23-13 EEPROM硬件連接圖

本實驗板中的EEPROM芯片(型號:AT24C02)SCLSDA引腳連接到了STM32對應的I2C引腳中,結合上拉電阻,構成了I2C通訊總線,它們通過I2C總線交互。EEPROM芯片的設備地址一共有7位,其中高4位固定為:1010 b,低3位則由A0/A1/A2信號線的電平決定,見 2314,圖中的R/W是讀寫方向位,與地址無關。

23-14 EEPROM設備地址(摘自《AT24C02》規格書)

按照我們此處的連接,A0/A1/A2均為0,所以EEPROM7位設備地址是:101 0000b ,即0x50。由於I2C通訊時常常是地址跟讀寫方向連在一起構成一個8位數,且當R/W位為0時,表示寫方向,所以加上7位地址,其值為“0xA0”,常稱該值為I2C設備的“寫地址”;當R/W位為1時,表示讀方向,加上7位地址,其值為“0xA1”,常稱該值為“讀地址”。

EEPROM芯片中還有一個WP引腳,具有寫保護功能,當該引腳電平為高時,禁止寫入數據,當引腳為低電平時,可寫入數據,我們直接接地,不使用寫保護功能。

關於EEPROM的更多信息,可參考其數據手冊《AT24C02》來了解。若您使用的實驗板EEPROM的型號、設備地址或控制引腳不一樣,只需根據我們的工程修改即可,程序的控制原理相同。

23.4.2  軟件設計

為了使工程更加有條理,我們把讀寫EEPROM相關的代碼獨立分開存儲,方便以后移植。在“工程模板”之上新建“bsp_i2c_ee.c”及“bsp_i2c_ee.h”文件,這些文件也可根據您的喜好命名,它們不屬於STM32 HAL庫的內容,是由我們自己根據應用需要編寫的。

1. 編程要點

(1) 配置通訊使用的目標引腳為開漏模式;

(2) 使能I2C外設的時鍾;

(3) 配置I2C外設的模式、地址、速率等參數並使能I2C外設;

(4) 編寫基本I2C按字節收發的函數;

(5) 編寫讀寫EEPROM存儲內容的函數;

(6) 編寫測試程序,對讀寫數據進行校驗。

2. 代碼分析
I2C硬件相關宏定義

我們把I2C硬件相關的配置都以宏的形式定義到 “bsp_i2c_ee.h”文件中,見代碼清單 23-2

代碼清單 23-2  I2C硬件配置相關的宏

1 /* 這個地址只要與STM32外掛的I2C器件地址不一樣即可 */

 2 #define I2C_OWN_ADDRESS7      0X0A

 3

 4 /*I2C接口*/

 5 #define EEPROM_I2C                          I2C4

 6 #define EEPROM_I2C_CLK_ENABLE()             __HAL_RCC_I2C4_CLK_ENABLE()

 7 #define RCC_PERIPHCLK_I2Cx                  RCC_PERIPHCLK_I2C4

 8

 9 #define EEPROM_I2C_SCL_PIN                  GPIO_PIN_12

10 #define EEPROM_I2C_SCL_GPIO_PORT            GPIOD

11 #define EEPROM_I2C_SCL_GPIO_CLK_ENABLE()    __GPIOD_CLK_ENABLE()

12 #define EEPROM_I2C_SCL_AF                   GPIO_AF4_I2C4

13

14 #define EEPROM_I2C_SDA_PIN                  GPIO_PIN_13

15 #define EEPROM_I2C_SDA_GPIO_PORT            GPIOD

16 #define EEPROM_I2C_SDA_GPIO_CLK_ENABLE()    __GPIOD_CLK_ENABLE()

17 #define EEPROM_I2C_SDA_AF                   GPIO_AF4_I2C4 #define

以上代碼根據硬件連接,把與EEPROM通訊使用的I2C號 、引腳號、引腳源以及復用功能映射都以宏封裝起來,並且定義了自身的I2C地址及通訊速率,以便配置模式的時候使用。

初始化I2CGPIO 

利用上面的宏,編寫I2C  GPIO引腳的初始化函數,見代碼清單 13-2

代碼清單 23-3  I2C初始化函數

1 /**

 2   * @brief  I2C1 I/O配置

 3   * @param  

 4   * @retval

 5   */

 6 static void I2C_GPIO_Config(void)

 7 {

 8

 9     GPIO_InitTypeDef  GPIO_InitStructure;

10     RCC_PeriphCLKInitTypeDef RCC_PeriphClkInit;

11

12     /*使能I2C時鍾*/

13     EEPROM_I2C_CLK_ENABLE();

14

15     /*使能I2CIO口時鍾*/

16     EEPROM_I2C_SCL_GPIO_CLK_ENABLE();

17     EEPROM_I2C_SDA_GPIO_CLK_ENABLE();

18

19     /*配置I2CSCL*/

20     GPIO_InitStructure.Pin = EEPROM_I2C_SCL_PIN;

21     GPIO_InitStructure.Mode = GPIO_MODE_AF_OD;

22     GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;

23     GPIO_InitStructure.Pull  = GPIO_NOPULL;

24     GPIO_InitStructure.Alternate = EEPROM_I2C_SCL_AF;

25     HAL_GPIO_Init(EEPROM_I2C_SCL_GPIO_PORT, &GPIO_InitStructure);

26

27     /*配置I2CSDA*/

28     GPIO_InitStructure.Pin = EEPROM_I2C_SDA_PIN;

29     HAL_GPIO_Init(EEPROM_I2C_SDA_GPIO_PORT, &GPIO_InitStructure);

30

31     /* Force the I2C peripheral clock reset */

32     EEPROM_I2C_FORCE_RESET();

33

34     /* Release the I2C peripheral clock reset */

35     EEPROM_I2C_RELEASE_RESET();

36

37 } 

同為外設使用的GPIO引腳初始化,初始化的流程與“串口初始化函數”章節中的類似,主要區別是引腳的模式。函數執行流程如下:

(1) 使用GPIO_InitTypeDef定義GPIO初始化結構體變量,以便下面用於存儲GPIO配置;

(2) 調用宏EEPROM_I2C_CLK_ENABLE()使能I2C外設時鍾,調用宏定義EEPROM_I2C_SCL_GPIO_CLK_ENABLE()EEPROM_I2C_SDA_GPIO_CLK_ENABLE()來使能I2C引腳使用的GPIO端口時鍾。

(3) GPIO初始化結構體賦值,把引腳初始化成復用開漏模式,要注意I2C的引腳必須使用這種模式。

(4) 使用以上初始化結構體的配置,調用HAL_GPIO_Init函數向寄存器寫入參數,完成GPIO的初始化。

配置I2C的模式

以上只是配置了I2C使用的引腳,還不算對I2C模式的配置,見代碼清單 23-4

代碼清單 23-4 配置I2C模式

1 /**

 2   * @brief  I2C 工作模式配置

 3   * @param  

 4   * @retval

 5   */

 6 static void I2C_Mode_Config(void)

 7 {

 8     /* I2C 配置 */

 9     I2C_Handle.Instance = EEPROM_I2C;

10     I2C_Handle.Init.Timing           = 0x60201E2B;//100KHz

11     I2C_Handle.Init.OwnAddress1      = 0;

12     I2C_Handle.Init.AddressingMode   = I2C_ADDRESSINGMODE_7BIT;

13     I2C_Handle.Init.DualAddressMode  = I2C_DUALADDRESS_DISABLE;

14     I2C_Handle.Init.OwnAddress2      = 0;

15     I2C_Handle.Init.OwnAddress2Masks = I2C_OA2_NOMASK;

16     I2C_Handle.Init.GeneralCallMode  = I2C_GENERALCALL_DISABLE;

17     I2C_Handle.Init.NoStretchMode    = I2C_NOSTRETCH_DISABLE;

18

19     /* Init the I2C */

20     HAL_I2C_Init(&I2C_Handle);

21

22     HAL_I2CEx_AnalogFilter_Config(&I2C_Handle, I2C_ANALOGFILTER_ENABLE);

23 }

24

25 /**

26   * @brief  I2C 外設(EEPROM)初始化

27   * @param  

28   * @retval

29   */

30 void I2C_EE_Init(void)

31 {

32

33     I2C_GPIO_Config();

34     I2C_Mode_Config();

35

36 }

熟悉STM32  I2C結構的話,這段初始化程序就十分好理解了,指定連接EEPROMI2CEEPROM_I2C這里是I2C4,時序配置為上面用工具計算出來的值,自身地址為0,地址設置為7bit模式,關閉雙地址模式,自身地址2也為0,自身地址2掩碼設置為無掩碼,禁止通用廣播模式,禁止時鍾延長模式。最后調用庫函數HAL_I2C_Init把這些配置寫入寄存器。

為方便調用,我們把I2CGPIO及模式配置都用I2C_EE_Init函數封裝起來。

EEPROM寫入一個字節的數據

初始化好I2C外設后,就可以使用I2C通訊了,我們看看如何向EEPROM寫入一個字節的數據,見代碼清單 23-5

代碼清單 23-5 EEPROM寫入一個字節的數據

1 /**

 2   * @brief   寫一個字節到I2C EEPROM

 3   * @param

 4   *   @arg pBuffer:緩沖區指針

 5   *   @arg WriteAddr:寫地址

 6   * @retval  

 7   */

 8 uint32_t I2C_EE_ByteWrite(uint8_t* pBuffer, uint8_t WriteAddr)

 9 {

10     HAL_StatusTypeDef status = HAL_OK;

11

12     status = HAL_I2C_Mem_Write(&I2C_Handle, EEPROM_ADDRESS, (uint16_t)WriteAddr,

13 I2C_MEMADD_SIZE_8BIT, pBuffer, 1, 100);

14     /* Check the communication status */

15     if (status != HAL_OK) {

16         /* Execute user timeout callback */

17         //I2Cx_Error(Addr);

18     }

19     while (HAL_I2C_GetState(&I2C_Handle) != HAL_I2C_STATE_READY) {

20

21     }

22

23     /* Check if the EEPROM is ready for a new operation */

24     while (HAL_I2C_IsDeviceReady(&I2C_Handle, EEPROM_ADDRESS,

25 EEPROM_MAX_TRIALS, I2Cx_TIMEOUT_MAX) == HAL_TIMEOUT);

26     /* Wait for the end of the transfer */

27     while (HAL_I2C_GetState(&I2C_Handle) != HAL_I2C_STATE_READY) {

28

29     }

30     return status;

31 }

這里我們只是簡單調用庫函數HAL_I2C_Mem_Write就可以實現,通過封裝一次使用更方便。

在這個通訊過程中,STM32實際上通過I2CEEPROM發送了兩個數據,但為何第一個數據被解釋為EEPROM的內存地址?這是由EEPROM的自己定義的單字節寫入時序,見 23-15

 

23-15 EEPROM單字節寫入時序(摘自《AT24C02》規格書)

EEPROM的單字節時序規定,向它寫入數據的時候,第一個字節為內存地址,第二個字節是要寫入的數據內容。所以我們需要理解:命令、地址的本質都是數據,對數據的解釋不同,它就有了不同的功能。

EEPROM的頁寫入

在以上的數據通訊中,每寫入一個數據都需要向EEPROM發送寫入的地址,我們希望向連續地址寫入多個數據的時候,只要告訴EEPROM第一個內存地址address1,后面的數據按次序寫入到address2address3…  這樣可以節省通訊的內容,加快速度。為應對這種需求,EEPROM定義了一種頁寫入時序,見 23-16

23-16  EEPROM頁寫入時序(摘自《AT24C02》規格書)

根據頁寫入時序,第一個數據被解釋為要寫入的內存地址address1,后續可連續發送n個數據,這些數據會依次寫入到內存中。其中AT24C02型號的芯片頁寫入時序最多可以一次發送8個數據(n = 8 ),該值也稱為頁大小,某些型號的芯片每個頁寫入時序最多可傳輸16個數據。EEPROM的頁寫入代碼實現見代碼清單 23-6

代碼清單 23-6 EEPROM的頁寫入

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_I2C, I2C_FLAG_BUSY))

17     {

18         if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(4);

19     }

20

21     /* 產生I2C起始信號 */

22     I2C_GenerateSTART(EEPROM_I2C, ENABLE);

23

24     I2CTimeout = I2CT_FLAG_TIMEOUT;

25

26     /* 檢測 EV5 事件並清除標志 */

27     while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))

28     {

29         if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(5);

30     }

31

32     /* 發送EEPROM設備地址  */

33 I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDRESS,I2C_Direction_Transmitter);

34

35     I2CTimeout = I2CT_FLAG_TIMEOUT;

36

37     /* 檢測 EV6 事件並清除標志*/

38     while (!I2C_CheckEvent(EEPROM_I2C,

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_I2C, WriteAddr);

45

46     I2CTimeout = I2CT_FLAG_TIMEOUT;

47

48     /* 檢測 EV8 事件並清除標志*/

49   while (! I2C_CheckEvent(EEPROM_I2C, 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_I2C, *pBuffer);

58

59         /* 指向緩沖區中的下一個數據 */

60         pBuffer++;

61

62         I2CTimeout = I2CT_FLAG_TIMEOUT;

63

64         /* 檢測 EV8 事件並清除標志*/

65     while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))

66         {

67             if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(8);

68         }

69     }

70     /* 發送停止信號 */

71     I2C_GenerateSTOP(EEPROM_I2C, ENABLE);

72     return 1;

73 }

這段頁寫入函數主體跟單字節寫入函數是一樣的,只是它在發送數據的時候,使用for循環控制發送多個數據,發送完多個數據后才產生I2C停止信號,只要每次傳輸的數據小於等於EEPROM時序規定的頁大小,就能正常傳輸。

多字節寫入

多次寫入數據時,利用EEPROM的頁寫入方式,避免單字節讀寫時候的等待。多個數據寫入過程見代碼清單 23-7

代碼清單 23-7 多字節寫入

1 /**

 2   * @brief   將緩沖區中的數據寫到I2C EEPROM

 3   * @param

 4   *   @arg pBuffer:緩沖區指針

 5   *   @arg WriteAddr:寫地址

 6   *     @arg NumByteToWrite:寫的字節數

 7   * @retval  

 8   */

 9 void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint16_t NumByteToWrite)

10 {

11     uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;

12

13     Addr = WriteAddr % EEPROM_PAGESIZE;

14     count = EEPROM_PAGESIZE - Addr;

15     NumOfPage =  NumByteToWrite / EEPROM_PAGESIZE;

16     NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;

17

18     /* If WriteAddr is I2C_PageSize aligned  */

19     if (Addr == 0) {

20         /* If NumByteToWrite < I2C_PageSize */

21         if (NumOfPage == 0) {

22             I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);

23         }

24         /* If NumByteToWrite > I2C_PageSize */

25         else {

26             while (NumOfPage--) {

27                 I2C_EE_PageWrite(pBuffer, WriteAddr, EEPROM_PAGESIZE);

28                 WriteAddr +=  EEPROM_PAGESIZE;

29                 pBuffer += EEPROM_PAGESIZE;

30             }

31

32             if (NumOfSingle!=0) {

33                 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);

34             }

35         }

36     }

37     /* If WriteAddr is not I2C_PageSize aligned  */

38     else {

39         /* If NumByteToWrite < I2C_PageSize */

40         if (NumOfPage== 0) {

41             I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);

42         }

43         /* If NumByteToWrite > I2C_PageSize */

44         else {

45             NumByteToWrite -= count;

46             NumOfPage =  NumByteToWrite / EEPROM_PAGESIZE;

47             NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;

48

49             if (count != 0) {

50                 I2C_EE_PageWrite(pBuffer, WriteAddr, count);

51                 WriteAddr += count;

52                 pBuffer += count;

53             }

54

55             while (NumOfPage--) {

56                 I2C_EE_PageWrite(pBuffer, WriteAddr, EEPROM_PAGESIZE);

57                 WriteAddr +=  EEPROM_PAGESIZE;

58                 pBuffer += EEPROM_PAGESIZE;

59             }

60             if (NumOfSingle != 0) {

61                 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);

62             }

63         }

64     }

65}

很多讀者覺得這段代碼的運算很復雜,看不懂,其實它的主旨就是對輸入的數據進行分頁(本型號芯片每頁8個字節),見 232。通過“整除”計算要寫入的數據NumByteToWrite能寫滿多少“完整的頁”,計算得的值存儲在NumOfPage中,但有時數據不是剛好能寫滿完整頁的,會多一點出來,通過“求余”計算得出“不滿一頁的數據個數”就存儲在NumOfSingle中。計算后通過按頁傳輸NumOfPage次整頁數據及最后的NumOfSing個數據,使用頁傳輸,比之前的單個字節數據傳輸要快很多。

除了基本的分頁傳輸,還要考慮首地址的問題,見 23-3。若首地址不是剛好對齊到頁的首地址,會需要一個count值,用於存儲從該首地址開始寫滿該地址所在的頁,還能寫多少個數據。實際傳輸時,先把這部分count個數據先寫入,填滿該頁,然后把剩余的數據(NumByteToWrite-count),再重復上述求出NumOPageNumOfSingle的過程,按頁傳輸到EEPROM

1. writeAddress=16,計算得Addr=16%8= 0 count=8-0= 8

2. 同時,若NumOfPage=22,計算得NumOfPage=22/8= 2NumOfSingle=22%8= 6

3. 數據傳輸情況如 23-2

232 首地址對齊到頁時的情況

不影響

0

1

2

3

4

5

6

7

不影響

8

9

10

11

12

13

14

15

第1頁

16

17

18

19

20

21

22

23

第2頁

24

25

26

27

28

29

30

31

NumOfSingle=6

32

33

34

35

36

37

38

39

 

4. writeAddress=17,計算得Addr=17%8= 1count=8-1= 7

5. 同時,若NumOfPage=22

6. 先把count去掉,特殊處理,計算得新的NumOfPage=22-7= 15

7. 計算得NumOfPage=15/8= 1NumOfSingle=15%8= 7

8. 數據傳輸情況如 23-3

233 首地址未對齊到頁時的情況

不影響

0

1

2

3

4

5

6

7

不影響

8

9

10

11

12

13

14

15

count=7

16

17

18

19

20

21

22

23

第1頁

24

25

26

27

28

29

30

31

NumOfSingle=7

32

33

34

35

36

37

38

39

 

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

EEPROM讀取數據

EEPROM讀取數據是一個復合的I2C時序,它實際上包含一個寫過程和一個讀過程,見 23-17

23-17 EEPROM數據讀取時序

讀時序的第一個通訊過程中,使用I2C發送設備地址尋址(寫方向),接着發送要讀取的“內存地址”;第二個通訊過程中,再次使用I2C發送設備地址尋址,但這個時候的數據方向是讀方向;在這個過程之后,EEPROM會向主機返回從“內存地址”開始的數據,一個字節一個字節地傳輸,只要主機的響應為“應答信號”,它就會一直傳輸下去,主機想結束傳輸時,就發送“非應答信號”,並以“停止信號”結束通訊,作為從機的EEPROM也會停止傳輸。HAL庫已經幫我們實現了這一個過程,我們只是簡單封裝一下就可以直接使用,實現代碼見代碼清單 23-8

代碼清單 23-8 EEPROM讀取數據

1 /**

 2   * @brief   EEPROM里面讀取一塊數據

 3   * @param

 4   *   @arg pBuffer:存放從EEPROM讀取的數據的緩沖區指針

 5   *   @arg WriteAddr:接收數據的EEPROM的地址

 6   *     @arg NumByteToWrite:要從EEPROM讀取的字節數

 7   * @retval  

 8   */

 9 uint32_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr, uint16_t NumByteToRead)

10 {

11     HAL_StatusTypeDef status = HAL_OK;

12

13     status=HAL_I2C_Mem_Read(&I2C_Handle,EEPROM_ADDRESS,ReadAddr,

14 I2C_MEMADD_SIZE_8BIT, (uint8_t *)pBuffer, NumByteToRead, 1000);

15

16     return status;

17 } 

這里代碼非常簡單,我們只需要確定I2C的地址,數據格式,數據存儲指針,數據大小,超時設置就可以把想要的數據讀回來。

3. main文件
EEPROM讀寫測試函數

完成基本的讀寫函數后,接下來我們編寫一個讀寫測試函數來檢驗驅動程序,見代碼清單 23-9

代碼清單 23-9 EEPROM讀寫測試函數

1 /**

 2   * @brief  I2C(AT24C02)讀寫測試

 3   * @param  

 4   * @retval 正常返回1 ,不正常返回0

 5   */

 6 uint8_t I2C_Test(void)

 7 {

 8     uint16_t i;

 9

10     EEPROM_INFO("寫入的數據");

11

12     for ( i=0; i<DATA_Size; i++ ) { //填充緩沖

13         I2c_Buf_Write[i] =i;

14         printf("0x%02X ", I2c_Buf_Write[i]);

15         if (i%16 == 15)

16             printf("\n\r");

17     }

18

19     //I2c_Buf_Write中順序遞增的數據寫入EERPOM

20     I2C_EE_BufferWrite( I2c_Buf_Write, EEP_Firstpage, DATA_Size);

21

22     EEPROM_INFO("讀出的數據");

23     //EEPROM讀出數據順序保持到I2c_Buf_Read

24     I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, DATA_Size);

25     //I2c_Buf_Read中的數據通過串口打印

26     for (i=0; i<DATA_Size; i++) {

27         if (I2c_Buf_Read[i] != I2c_Buf_Write[i]) {

28             printf("0x%02X ", I2c_Buf_Read[i]);

29             EEPROM_ERROR("錯誤:I2C EEPROM寫入與讀出的數據不一致");

30             return 0;

31         }

32         printf("0x%02X ", I2c_Buf_Read[i]);

33         if (i%16 == 15)

34             printf("\n\r");

35

36     }

37     EEPROM_INFO("I2C(AT24C02)讀寫測試成功");

38     return 1;

39 } 

代碼中先填充一個數組,數組的內容為1,2,3N,接着把這個數組的內容寫入到EEPROM中,寫入時采用頁寫入的方式。寫入完畢后再從EEPROM的地址中讀取數據,把讀取得到的與寫入的數據進行校驗,若一致說明讀寫正常,否則讀寫過程有問題或者EEPROM芯片不正常。其中代碼用到的EEPROM_INFOEEPROM_ERROR宏類似,都是對printf函數的封裝,使用和閱讀代碼時把它直接當成printf函數就好。具體的宏定義在“bsp_i2c_ee.h文件中”,在以后的代碼我們常常會用類似的宏來輸出調試信息。

main函數

最后編寫main函數,函數中初始化了系統時鍾、LED、串口、I2C外設,然后調用上面的I2C_Test函數進行讀寫測試,見代碼清單 23-10

代碼清單 23-10 main函數

1 /**

 2   * @brief  主函數

 3   * @param  

 4   * @retval

 5   */

 6 int main(void)

 7 {

 8     /* 配置系統時鍾為216 MHz */

 9     SystemClock_Config();

10

11     /* 初始化RGB彩燈 */

12     LED_GPIO_Config();

13

14     LED_BLUE;

15     /*初始化USART1*/

16     UARTx_Config();

17

18     printf("\r\n 歡迎使用秉火  STM32 F429 開發板。\r\n");

19

20     printf("\r\n 這是一個I2C外設(AT24C02)讀寫測試例程 \r\n");

21

22     /* I2C 外設初(AT24C02)始化 */

23     I2C_EE_Init();

24

25     if (I2C_Test() ==1) {

26         LED_GREEN;

27     } else {

28         LED_RED;

29     }

30

31     while (1) {

32

33     }

34 } 

23.1.1  下載驗證

USB線連接開發板“USB TO UART”接口跟電腦,在電腦端打開串口調試助手,把編譯好的程序下載到開發板。在串口調試助手可看到EEPROM測試的調試信息。

 


免責聲明!

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



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