本章參考資料:《STM32F76xxx參考手冊》、《STM32F7xx規格書》、庫幫助文檔《STM32F779xx_User_Manual.chm》及《I2C總線協議》。
若對I2C通訊協議不了解,可先閱讀《I2C總線協議》文檔的內容學習。若想了解SMBUS,可閱讀《smbus20》文檔。
關於EEPROM存儲器,請參考“常用存儲器介紹”章節,實驗中的EEPROM,請參考其規格書《AT24C02》來了解。
23.1 I2C協議簡介
I2C 通訊協議(Inter-Integrated Circuit)是由Phiilps公司開發的,由於它引腳少,硬件實現簡單,可擴展性強,不需要USART、CAN等通訊協議的外部收發設備,現在被廣泛地使用在系統內多個集成電路(IC)間的通訊。
下面我們分別對I2C協議的物理層及協議層進行講解。
23.1.1 I2C物理層
I2C通訊設備之間的常用連接方式見圖 23-1。
它的物理層有如下特點:
(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 主機由從機中讀數
圖例: 數據由主機傳輸至從機 S : 傳輸開始信號
SLAVE_ADDRESS: 從機地址
數據由從機傳輸至主機 R/W(——): 傳輸方向選擇位,1為讀,0為寫
A/A(——) : 應答(ACK)或非應答(NACK)信號
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的區別);在第二次的傳輸中,對該地址的內容進行讀或寫。也就是說,第一次通訊是告訴從機讀寫地址,第二次則是讀寫的實際內容。
以上通訊流程中包含的各個信號分解如下:
1. 通訊的起始和停止信號
前文中提到的起始(S)和停止(P)信號是兩種特殊的狀態,見圖 23-5。當 SCL 線是高電平時 SDA 線從高電平向低電平切換,這個情況表示通訊的起始。當 SCL 是高電平時 SDA 線由低電平向高電平切換,表示通訊的停止。起始和停止信號一般由主機產生。
2. 數據有效性
I2C使用SDA信號線來傳輸數據,使用SCL信號線進行數據同步。見圖 236。SDA數據線在SCL的每個時鍾周期傳輸一位數據。傳輸時,SCL為高電平的時候SDA表示的數據有效,即此時的SDA為高電平時表示數據“1”,為低電平時表示數據“0”。當SCL為低電平時,SDA的數據無效,一般在這個時候SDA進行電平切換,為下一次表示數據做好准備。
每次數據傳輸都以字節為單位,每次傳輸的字節數不受限制。
3. 地址及數據方向
I2C總線上的每個設備都有自己的獨立地址,主機發起通訊時,通過SDA信號線發送設備地址(SLAVE_ADDRESS)來查找從機。I2C協議規定設備地址可以是7位或10位,實際中7位的地址應用比較廣泛。緊跟設備地址的一個數據位用來表示數據傳輸方向,它是數據方向位(R/W(——)),第8位或第11位。數據方向位為“1”時表示主機由從機讀數據,該位為“0”時表示主機向從機寫數據。見圖 23-7。
讀數據方向時,主機會釋放對SDA信號線的控制,由從機控制SDA信號線,主機接收信號,寫數據方向時,SDA由主機控制,從機接收信號。
4. 響應
I2C的數據和地址傳輸都帶響應。響應包括“應答(ACK)”和“非應答(NACK)”兩種信號。作為數據接收端時,當設備(無論主從機)接收到I2C傳輸的一個字節數據或地址后,若希望對方繼續發送數據,則需要向對方發送“應答(ACK)”信號,發送方會繼續發送下一個數據;若接收端希望結束數據傳輸,則向對方發送“非應答(NACK)”信號,發送方接收到該信號后會產生一個停止信號,結束信號傳輸。見圖 23-8。
傳輸時主機產生時鍾,在第9個時鍾時,數據發送端會釋放SDA的控制權,由數據接收端控制SDA,若SDA為高電平,表示非應答信號(NACK),低電平表示應答信號(ACK)。
23.2 STM32的I2C特性及架構
如果我們直接控制STM32的兩個GPIO引腳,分別用作SCL及SDA,按照上述信號的時序要求,直接像控制LED燈那樣控制引腳的輸出(若是接收數據時則讀取SDA電平),就可以實現I2C通訊。同樣,假如我們按照USART的要求去控制引腳,也能實現USART通訊。所以只要遵守協議,就是標准的通訊,不管您如何實現它,不管是ST生產的控制器還是ATMEL生產的存儲器, 都能按通訊標准交互。
由於直接控制GPIO引腳電平產生通訊時序時,需要由CPU控制每個時刻的引腳狀態,所以稱之為“軟件模擬協議”方式。
相對地,還有“硬件協議”方式,STM32的I2C片上外設專門負責實現I2C通訊協議,只要配置好該外設,它就會自動根據協議要求產生通訊信號,收發數據並緩存起來,CPU只要檢測該外設的狀態和訪問數據寄存器,就能完成數據收發。這種由硬件外設處理I2C協議的方式減輕了CPU的工作,且使軟件設計更加簡單。
23.2.1 STM32的I2C外設簡介
STM32的I2C外設可用作通訊的主機及從機,支持標准速度模式(高達100Kbit/s)、快速模式(高達400Kbit/s)、超快速模式(高達1Mbit/s),支持7位、10位設備地址,支持DMA數據傳輸,並具有數據校驗功能。它的I2C外設還支持SMBus2.0協議,SMBus協議與I2C類似,主要應用於筆記本電腦的電池管理中,本教程不展開,感興趣的讀者可參考《SMBus2.0》文檔了解。
23.2.2 STM32的I2C架構剖
圖 23-9 I2C架構圖
1. 通訊引腳
I2C的所有硬件架構都是根據圖中左側SCL線和SDA線展開的(其中的SMBA線用於SMBUS的警告信號,I2C通訊沒有使用)。STM32芯片有多個I2C外設,它們的I2C通訊信號引出到不同的GPIO引腳上,使用時必須配置到這些指定的引腳,見表 231。關於GPIO引腳的復用功能,可查閱《STM32F7xx規格書》,以它為准。
表 231 STM32F7xx的I2C引腳(整理自《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. 噪聲濾波器
模擬噪聲濾波器,集成於SDA和SCL的輸入上,默認情況下是打開的,該模擬濾波器符合I2C規范,此規范要求在快速模式和超快速模式下對脈寬50ns以下的脈沖都要抑制。可以空過將寄存器I2C_CR1的ANFOFF位置1,注意該位只能在I2C禁止時(PE=0)時編程。
數字噪聲濾波器,從框圖可以看出它是SDA和SCL經過模擬噪聲濾波器再進來的,通過配置 I2C_CR1 寄存器中的 DNF[3:0] 位來使能數字濾波器使能數字濾波器,數字濾波器可濾除脈寬 DNF[3:0] *以下的尖峰,可濾除的噪聲尖峰脈寬從 1 到 15 個 I2CCLK 周期可編程。如果模擬濾波器已使能,數字濾波將疊加在模擬濾波之上。
3. 時鍾源及要求
I2C 的時鍾由獨立時鍾源提供,這使得 I2C 能夠獨立於 PCLK 頻率工作。該獨立時鍾源可從以下三種時鍾源中任選其一:
q PCLK1:APB1時鍾(默認值)
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 寄存器的值,可以在我們參考工具文件夾找到。例如我們要產生標准的100KHz的I2C主設備時序,在序號1的框中依次填入Device Mode Master:Master,I2C Speed Mode:Standard Mode,I2C Speed Frequency(KHz):100,I2C Clock Source Frequency(KHz):54000,Analog Filter Delay:ON,Coefficient of Digital Filt:0,Rise Time(ns):100,Fall Time(ns):10,最后在右側序號2的框中找到Run按鈕即可生成TIMINGR 寄存器的值:0x60201E2B,雙擊即可復制,最后粘貼在MDK的I2C初始化源碼中就可以完成初始化。這樣非常方便,避免頭痛的計算。
圖 23-10 I2C時序計算工具
下面我們來講解初始化I2C時鍾的計算方法,為了支持多主環境和從時鍾延長, I2C 實現了時鍾同步機制。為了實現時鍾同步,需執行以下操作:
使用 SCLL 計數器從 SCL 低電平內部檢測開始對時鍾的低電平進行計數。
使用 SCLH 計數器從 SCL 高電平內部檢測開始對時鍾的高電平進行計數。
5. 數據控制邏輯
I2C的SDA信號主要連接到數據移位寄存器上,數據移位寄存器的數據來源及目標是數據寄存器(DR)、地址寄存器(OAR)、PEC寄存器以及SDA數據線。當向外發送數據的時候,數據移位寄存器以“數據寄存器”為數據源,把數據一位一位地通過SDA信號線發送出去;當從外部接收數據的時候,數據移位寄存器把SDA信號線采樣到的數據一位一位地存儲到“數據寄存器”中。若使能了數據校驗,接收到的數據會經過PCE計算器運算,運算結果存儲在“PEC寄存器”中。當STM32的I2C工作在從機模式的時候,接收到設備地址信號時,數據移位寄存器會把接收到的地址與STM32的自身的“I2C地址寄存器”的值作比較,以便響應主機的尋址。STM32的自身I2C地址可通過修改“自身地址寄存器”修改,支持同時使用兩個I2C設備地址,兩個地址分別存儲在OAR1和OAR2中。
6. 整體控制邏輯
整體控制邏輯負責協調整個I2C外設,控制邏輯的工作模式根據我們配置的“控制寄存器(CR1/CR2)”的參數而改變。在外設工作時,控制邏輯會根據外設的工作狀態修改“狀態寄存器(SR1和SR2)”,我們只要讀取這些寄存器相關的寄存器位,就可以了解I2C的工作狀態了。除此之外,控制邏輯還根據要求,負責控制產生I2C中斷信號、DMA請求及各種I2C的通訊信號(起始、停止、響應信號等)。
23.2.3 通訊過程
使用I2C外設通訊時,在通訊的不同階段它會對“狀態寄存器(SR1及SR2)”的不同數據位寫入參數,我們通過讀取這些寄存器標志來了解通訊狀態。
1. 主發送器
見圖 23-11。圖中的是“主發送器”流程,即作為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中斷服務程序后,再通過檢查寄存器位來了解是哪一個事件。
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-。
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
本成員配置的是STM32的I2C設備自身地址1,每個連接到I2C總線上的設備都要有一個自己的地址,作為主機也不例外。地址可設置為7位或10位(受下面(3) AddressingMode成員決定),只要該地址是I2C總線上唯一的即可。
STM32的I2C外設可同時使用兩個地址,即同時對兩個地址作出響應,這個結構成員I2C_OwnAddress1配置的是默認的、OAR1寄存器存儲的地址,若需要設置第二個地址寄存器OAR2,可使用DualAddressMode成員使能,然后設置OwnAddress2成員即可,OAR2不支持10位地址。
(3) AddressingMode
本成員選擇I2C的尋址模式是7位還是10位地址。這需要根據實際連接到I2C總線上設備的地址進行選擇,這個成員的配置也影響到OwnAddress1成員,只有這里設置成10位模式時, OwnAddress1才支持10位地址。
(4) DualAddressMode
本成員配置的是STM32的I2C設備自己的地址,每個連接到I2C總線上的設備都要有一個自己的地址,作為主機也不例外。地址可設置為7位或10位(受下面I2C_AcknowledgeAddress成員決定),只要該地址是I2C總線上唯一的即可。
STM32的I2C外設可同時使用兩個地址,即同時對兩個地址作出響應,這個結構成員I2C_OwnAddress1配置的是默認的、OAR1寄存器存儲的地址,若需要設置第二個地址寄存器OAR2,可使用I2C_OwnAddress2Config函數來配置,OAR2不支持10位地址。
(5) OwnAddress2
本成員配置的是STM32的I2C設備自身地址2,每個連接到I2C總線上的設備都要有一個自己的地址,作為主機也不例外。地址可設置為7位,只要該地址是I2C總線上唯一的即可。
(6) OwnAddress2Masks
本成員指定I2C的雙地址模式時的掩碼。
(7) GeneralCallMode
本成員是關於I2C從模式時的廣播呼叫模式設置。
(8) NoStretchMode
本成員是關於I2C禁止時鍾延長模式設置,用於在從模式下禁止時鍾延長。它在主模式下必須保持關閉。
配置完這些結構體成員值,調用庫函數HAL_I2C_Init即可把結構體的配置寫入到寄存器中。
23.4 I2C—讀寫EEPROM實驗
EEPROM是一種掉電后數據不丟失的存儲器,常用來存儲一些配置信息,以便系統重新上電的時候加載之。EEPOM芯片最常用的通訊方式就是I2C協議,本小節以EEPROM的讀寫實驗為大家講解STM32的I2C使用方法。實驗中STM32的I2C外設采用主模式,分別用作主發送器和主接收器,通過查詢事件的方式來確保正常通訊。
23.4.1 硬件設計
圖 23-13 EEPROM硬件連接圖
本實驗板中的EEPROM芯片(型號:AT24C02)的SCL及SDA引腳連接到了STM32對應的I2C引腳中,結合上拉電阻,構成了I2C通訊總線,它們通過I2C總線交互。EEPROM芯片的設備地址一共有7位,其中高4位固定為:1010 b,低3位則由A0/A1/A2信號線的電平決定,見圖 2314,圖中的R/W是讀寫方向位,與地址無關。
圖 23-14 EEPROM設備地址(摘自《AT24C02》規格書)
按照我們此處的連接,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的型號、設備地址或控制引腳不一樣,只需根據我們的工程修改即可,程序的控制原理相同。
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。
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地址及通訊速率,以便配置模式的時候使用。
初始化I2C的 GPIO
利用上面的宏,編寫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 /*使能I2C的IO口時鍾*/
16 EEPROM_I2C_SCL_GPIO_CLK_ENABLE();
17 EEPROM_I2C_SDA_GPIO_CLK_ENABLE();
18
19 /*配置I2C的SCL口*/
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 /*配置I2C的SDA口*/
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。
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結構的話,這段初始化程序就十分好理解了,指定連接EEPROM的I2C為EEPROM_I2C這里是I2C4,時序配置為上面用工具計算出來的值,自身地址為0,地址設置為7bit模式,關閉雙地址模式,自身地址2也為0,自身地址2掩碼設置為無掩碼,禁止通用廣播模式,禁止時鍾延長模式。最后調用庫函數HAL_I2C_Init把這些配置寫入寄存器。
為方便調用,我們把I2C的GPIO及模式配置都用I2C_EE_Init函數封裝起來。
向EEPROM寫入一個字節的數據
初始化好I2C外設后,就可以使用I2C通訊了,我們看看如何向EEPROM寫入一個字節的數據,見代碼清單 23-5。
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實際上通過I2C向EEPROM發送了兩個數據,但為何第一個數據被解釋為EEPROM的內存地址?這是由EEPROM的自己定義的單字節寫入時序,見圖 23-15。
圖 23-15 EEPROM單字節寫入時序(摘自《AT24C02》規格書)
EEPROM的單字節時序規定,向它寫入數據的時候,第一個字節為內存地址,第二個字節是要寫入的數據內容。所以我們需要理解:命令、地址的本質都是數據,對數據的解釋不同,它就有了不同的功能。
EEPROM的頁寫入
在以上的數據通訊中,每寫入一個數據都需要向EEPROM發送寫入的地址,我們希望向連續地址寫入多個數據的時候,只要告訴EEPROM第一個內存地址address1,后面的數據按次序寫入到address2、address3… 這樣可以節省通訊的內容,加快速度。為應對這種需求,EEPROM定義了一種頁寫入時序,見圖 23-16。
圖 23-16 EEPROM頁寫入時序(摘自《AT24C02》規格書)
根據頁寫入時序,第一個數據被解釋為要寫入的內存地址address1,后續可連續發送n個數據,這些數據會依次寫入到內存中。其中AT24C02型號的芯片頁寫入時序最多可以一次發送8個數據(即n = 8 ),該值也稱為頁大小,某些型號的芯片每個頁寫入時序最多可傳輸16個數據。EEPROM的頁寫入代碼實現見代碼清單 23-6。
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。
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),再重復上述求出NumOPage及NumOfSingle的過程,按頁傳輸到EEPROM。
1. 若writeAddress=16,計算得Addr=16%8= 0 ,count=8-0= 8;
2. 同時,若NumOfPage=22,計算得NumOfPage=22/8= 2,NumOfSingle=22%8= 6。
3. 數據傳輸情況如表 23-2
不影響 |
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= 1,count=8-1= 7;
5. 同時,若NumOfPage=22,
6. 先把count去掉,特殊處理,計算得新的NumOfPage=22-7= 15
7. 計算得NumOfPage=15/8= 1,NumOfSingle=15%8= 7。
8. 數據傳輸情況如表 23-3
不影響 |
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寫入的,例如每個Block為512或4096字節,數據寫入的最小單位是Block,寫入前都需要擦除整個Block;NOR FLASH則是寫入前必須以Sector/Block為單位擦除,然后才可以按字節寫入。而我們的EEPROM數據寫入和擦除的最小單位是“字節”而不是“頁”,數據寫入前不需要擦除整頁。
從EEPROM讀取數據
從EEPROM讀取數據是一個復合的I2C時序,它實際上包含一個寫過程和一個讀過程,見圖 23-17。
圖 23-17 EEPROM數據讀取時序
讀時序的第一個通訊過程中,使用I2C發送設備地址尋址(寫方向),接着發送要讀取的“內存地址”;第二個通訊過程中,再次使用I2C發送設備地址尋址,但這個時候的數據方向是讀方向;在這個過程之后,EEPROM會向主機返回從“內存地址”開始的數據,一個字節一個字節地傳輸,只要主機的響應為“應答信號”,它就會一直傳輸下去,主機想結束傳輸時,就發送“非應答信號”,並以“停止信號”結束通訊,作為從機的EEPROM也會停止傳輸。HAL庫已經幫我們實現了這一個過程,我們只是簡單封裝一下就可以直接使用,實現代碼見代碼清單 23-8。
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。
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,3至N,接着把這個數組的內容寫入到EEPROM中,寫入時采用頁寫入的方式。寫入完畢后再從EEPROM的地址中讀取數據,把讀取得到的與寫入的數據進行校驗,若一致說明讀寫正常,否則讀寫過程有問題或者EEPROM芯片不正常。其中代碼用到的EEPROM_INFO跟EEPROM_ERROR宏類似,都是對printf函數的封裝,使用和閱讀代碼時把它直接當成printf函數就好。具體的宏定義在“bsp_i2c_ee.h文件中”,在以后的代碼我們常常會用類似的宏來輸出調試信息。
main函數
最后編寫main函數,函數中初始化了系統時鍾、LED、串口、I2C外設,然后調用上面的I2C_Test函數進行讀寫測試,見代碼清單 23-10。
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測試的調試信息。