第23章 I2C—讀寫EEPROM—零死角玩轉STM32-F429系列


第23章     I2C—讀寫EEPROM

全套200集視頻教程和1000PDF教程請到秉火論壇下載:www.firebbs.cn

野火視頻教程優酷觀看網址:http://i.youku.com/firege

 

 

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

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

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

23.1 I2C協議簡介

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

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

23.1.1 I2C物理層

I2C通訊設備之間的常用連接方式見圖 231

231 常見的I2C通訊系統

它的物理層有如下特點:

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

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

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

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

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

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

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

23.1.2 協議層

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

1.    I2C基本讀寫過程

先看看I2C通訊過程的基本結構,它的通訊過程見圖 232、圖 233及圖 234

232 主機寫數據到從機

233 主機由從機中讀數據

234 I2C通訊復合格式

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

                     SLAVE_ADDRESS: 從機地址

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

                     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)信號是兩種特殊的狀態,見圖 235。當 SCL 線是高電平時 SDA 線從高電平向低電平切換,這個情況表示通訊的起始。當 SCL 是高電平時 SDA 線由低電平向高電平切換,表示通訊的停止。起始和停止信號一般由主機產生。

235 起始和停止信號

2.    數據有效性

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

236 數據有效性

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

3.    地址及數據方向

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

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

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

4.    響應

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

238 響應與非響應信號

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

23.2 STM32的I2C特性及架構

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

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

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

23.2.1 STM32的I2C外設簡介

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

23.2.2 STM32的I2C架構剖析

239 I2C架構圖

1.    通訊引腳

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

231 STM32F4xxI2C引腳(整理自《STM32F4xx規格書》)

引腳

I2C編號

I2C1

I2C2

I2C3

SCL

PB6/PB10

PH4/PF1/PB10

PH7/PA8

SDA

PB7/PB9

PH5/PF0/PB11

PH8/PC9

2.    時鍾控制邏輯

SCL線的時鍾信號,由I2C接口根據時鍾控制寄存器(CCR)控制,控制的參數主要為時鍾頻率。配置I2CCCR寄存器可修改通訊速率相關的參數:

    可選擇I2C通訊的"標准/快速"模式,這兩個模式分別I2C對應100/400Kbit/s的通訊速率。

    在快速模式下可選擇SCL時鍾的占空比,可選Tlow/Thigh=2Tlow/Thigh=16/9模式,我們知道I2C協議在SCL高電平時對SDA信號采樣,SCL低電平時SDA准備下一個數據,修改SCL的高低電平比會影響數據采樣,但其實這兩個模式的比例差別並不大,若不是要求非常嚴格,這里隨便選就可以了。

    CCR寄存器中還有一個12位的配置因子CCR,它與I2C外設的輸入時鍾源共同作用,產生SCL時鍾,STM32I2C外設都掛載在APB1總線上,使用APB1的時鍾源PCLK1SCL信號線的輸出時鍾公式如下:

標准模式:

Thigh=CCR*TPCKL1        Tlow = CCR*TPCLK1

快速模式中Tlow/Thigh=2時:

Thigh = CCR*TPCKL1        Tlow = 2*CCR*TPCKL1

快速模式中Tlow/Thigh=16/9時:

Thigh = 9*CCR*TPCKL1        Tlow = 16*CCR*TPCKL1

 

例如,我們的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,這樣I2CSCL實際頻率無法達到400KHz (約為394736Hz)。要想它實際頻率達到400KHz,需要修改STM32的系統時鍾,把PCLK1時鍾頻率改成10的倍數才可以,但修改PCKL時鍾影響很多外設,所以一般我們不會修改它。SCL的實際頻率不達到400KHz,除了通訊稍慢一點以外,不會對I2C的標准通訊造成其它影響

3.    數據控制邏輯

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

4.    整體控制邏輯

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

23.2.3 通訊過程

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

1.    主發送器

見圖 2310。圖中的是"主發送器"流程,即作為I2C通訊的主機端時,向外發送數據時的過程。

2310 主發送器通訊過程

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

(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通訊的主機端時,從外部接收數據的過程,見圖 2311

2311 主接收器過程

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

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

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

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

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

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

23.3 I2C初始化結構體詳解

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

代碼清單 231 I2C初始化結構體

1 typedef struct {

2 uint32_t I2C_ClockSpeed; /*!< 設置SCL時鍾頻率,此值要低於40 0000*/

3 uint16_t I2C_Mode; /*!< 指定工作模式,可I2C模式及SMBUS模式 */

4 uint16_t I2C_DutyCycle; /*指定時鍾占空比,可選low/high = 2:116:9模式*/

5 uint16_t I2C_OwnAddress1; /*!< 指定自身的I2C設備地址 */

6 uint16_t I2C_Ack; /*!< 使能或關閉響應(一般都要使能) */

7 uint16_t I2C_AcknowledgedAddress; /*!< 指定地址的長度,可為7位及10 */

8 } 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

本成員設置的是I2CSCL線時鍾的占空比。該配置有兩個選擇,分別為低電平時間比高電平時間為21 ( I2C_DutyCycle_2)169 (I2C_DutyCycle_16_9)。其實這兩個模式的比例差別並不大,一般要求都不會如此嚴格,這里隨便選就可以了。

(4)    I2C_OwnAddress1

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

STM32I2C外設可同時使用兩個地址,即同時對兩個地址作出響應,這個結構成員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即可把結構體的配置寫入到寄存器中。

23.4 I2C—讀寫EEPROM實驗

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

23.4.1 硬件設計

2312 EEPROM硬件連接圖

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

2313 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標准庫的內容,是由我們自己根據應用需要編寫的。

1.    編程要點

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

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

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

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

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

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

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

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

代碼清單 232 I2C硬件配置相關的宏

2 /* STM32 I2C 速率 */

3 #define I2C_Speed 400000

4

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

6 #define I2C_OWN_ADDRESS7 0X0A

7

8 /*I2C接口*/

9 #define EEPROM_I2C I2C1

10 #define EEPROM_I2C_CLK RCC_APB1Periph_I2C1

11

12 #define EEPROM_I2C_SCL_PIN GPIO_Pin_6

13 #define EEPROM_I2C_SCL_GPIO_PORT GPIOB

14 #define EEPROM_I2C_SCL_GPIO_CLK RCC_AHB1Periph_GPIOB

15 #define EEPROM_I2C_SCL_SOURCE GPIO_PinSource6

16 #define EEPROM_I2C_SCL_AF GPIO_AF_I2C1

17

18 #define EEPROM_I2C_SDA_PIN GPIO_Pin_7

19 #define EEPROM_I2C_SDA_GPIO_PORT GPIOB

20 #define EEPROM_I2C_SDA_GPIO_CLK RCC_AHB1Periph_GPIOB

21 #define EEPROM_I2C_SDA_SOURCE GPIO_PinSource7

22 #define EEPROM_I2C_SDA_AF GPIO_AF_I2C1

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

初始化I2C的 GPIO

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

代碼清單 233 I2C初始化函數

2 /**

3 * @brief I2C1 I/O配置

4 * @param

5 * @retval

6 */

7 static void I2C_GPIO_Config(void)

8 {

9 GPIO_InitTypeDef GPIO_InitStructure;

10

11 /*使能I2C外設時鍾 */

12 RCC_APB1PeriphClockCmd(EEPROM_I2C_CLK, ENABLE);

13

14 /*使能I2C引腳的GPIO時鍾*/

15 RCC_AHB1PeriphClockCmd(EEPROM_I2C_SCL_GPIO_CLK |

16 EEPROM_I2C_SDA_GPIO_CLK, ENABLE);

17

18 /* 連接引腳源 PXx I2C_SCL*/

19 GPIO_PinAFConfig(EEPROM_I2C_SCL_GPIO_PORT, EEPROM_I2C_SCL_SOURCE,

20 EEPROM_I2C_SCL_AF);

21 /* 連接引腳源 PXx to I2C_SDA*/

22 GPIO_PinAFConfig(EEPROM_I2C_SDA_GPIO_PORT, EEPROM_I2C_SDA_SOURCE,

23 EEPROM_I2C_SDA_AF);

24

25 /*配置 SCL引腳 */

26 GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN;

27 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;

28 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

29 GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;

30 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;

31 GPIO_Init(EEPROM_I2C_SCL_GPIO_PORT, &GPIO_InitStructure);

32

33 /*配置 SDA引腳 */

34 GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SDA_PIN;

35 GPIO_Init(EEPROM_I2C_SDA_GPIO_PORT, &GPIO_InitStructure);

36 }

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

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

(2)    調用庫函數RCC_APB1PeriphClockCmd使能I2C外設時鍾,調用RCC_AHB1PeriphClockCmd來使能I2C引腳使用的GPIO端口時鍾,調用時我們使用"|"操作同時配置兩個引腳。

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

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

配置I2C的模式

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

代碼清單 234 配置I2C模式

2 /**

3 * @brief I2C 工作模式配置

4 * @param

5 * @retval

6 */

7 static void I2C_Mode_Config(void)

8 {

9 I2C_InitTypeDef I2C_InitStructure;

10

11 /* I2C 配置 */

12 /*I2C模式*/

13 I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;

14 /*占空比*/

15 I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;

16 /*I2C自身地址*/

17 I2C_InitStructure.I2C_OwnAddress1 =I2C_OWN_ADDRESS7;

18 /*使能響應*/

19 I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;

20 /* I2C的尋址模式 */

21 I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;

22 /* 通信速率 */

23 I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;

24 /*寫入配置*/

25 I2C_Init(EEPROM_I2C, &I2C_InitStructure);

26 /* 使能 I2C */

27 I2C_Cmd(EEPROM_I2C, ENABLE);

28 }

29

30 /**

31 * @brief I2C 外設初始化

32 * @param

33 * @retval

34 */

35 void I2C_EE_Init(void)

36 {

37 I2C_GPIO_Config();

38

39 I2C_Mode_Config();

40 }

熟悉STM32 I2C結構的話,這段初始化程序就十分好理解了,它把I2C外設通訊時鍾SCL的低/高電平比設置為2,使能響應功能,使用7位地址I2C_OWN_ADDRESS7以及速率配置為I2C_Speed(前面在bsp_i2c_ee.h定義的宏)。最后調用庫函數I2C_Init把這些配置寫入寄存器,並調用I2C_Cmd函數使能外設。

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

向EEPROM寫入一個字節的數據

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

代碼清單 235 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_I2C, ENABLE);

28

29 /*設置超時等待時間*/

30 I2CTimeout = I2CT_FLAG_TIMEOUT;

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

32 while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))

33 {

34 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);

35 }

36

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

38 I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDRESS,

39 I2C_Direction_Transmitter);

40

41 I2CTimeout = I2CT_FLAG_TIMEOUT;

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

43 while (!I2C_CheckEvent(EEPROM_I2C,

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

51

52 I2CTimeout = I2CT_FLAG_TIMEOUT;

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

54 while (!I2C_CheckEvent(EEPROM_I2C,

55 I2C_EVENT_MASTER_BYTE_TRANSMITTED))

56 {

57 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);

58 }

59 /* 發送一字節要寫入的數據 */

60 I2C_SendData(EEPROM_I2C, *pBuffer);

61

62 I2CTimeout = I2CT_FLAG_TIMEOUT;

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

64 while (!I2C_CheckEvent(EEPROM_I2C,

65 I2C_EVENT_MASTER_BYTE_TRANSMITTED))

66 {

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

68 }

69

70 /* 發送停止信號 */

71 I2C_GenerateSTOP(EEPROM_I2C, ENABLE);

72

73 return 1;

74 }

先來分析I2C_TIMEOUT_UserCallback函數,它的函數體里只調用了宏EEPROM_ERROR,這個宏封裝了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_SendDataEEPROM發送要寫入的內部地址,該地址是I2C_EE_ByteWrite函數的輸入參數,發送完畢后等待EV8事件。要注意這個內部地址跟上面的EEPROM地址不一樣,上面的是指I2C總線設備的獨立地址,而此處的內部地址是指EEPROM內數據組織的地址,也可理解為EEPROM內存的地址或I2C設備的寄存器地址;

(5)    調用庫函數I2C_SendDataEEPROM發送要寫入的數據,該數據是I2C_EE_ByteWrite函數的輸入參數,發送完畢后等待EV8事件;

(6)    一個I2C通訊過程完畢,調用I2C_GenerateSTOP發送停止信號。

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

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

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

多字節寫入及狀態等待

單字節寫入通訊結束后,EEPROM芯片會根據這個通訊結果擦寫該內存地址的內容,這需要一段時間,所以我們在多次寫入數據時,要先等待EEPROM內部擦寫完畢。多個數據寫入過程見代碼清單 236

代碼清單 236 多字節寫入

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內部擦寫完畢,該函數的定義見代碼清單 237

代碼清單 237 等待EEPROM處於准備狀態

1 //等待Standby狀態的最大次數

2 #define MAX_TRIAL_NUMBER 300

3 /**

4 * @brief 等待EEPROM到准備狀態

5 * @param

6 * @retval 正常返回1,異常返回0

7 */

8 static uint8_t I2C_EE_WaitEepromStandbyState(void)

9 {

10 __IO uint16_t tmpSR1 = 0;

11 __IO uint32_t EETrials = 0;

12

13 /*總線忙時等待 */

14 I2CTimeout = I2CT_LONG_TIMEOUT;

15 while (I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY))

16 {

17 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(20);

18 }

19

20 /* 等待從機應答,最多等待300 */

21 while (1)

22 {

23 /*開始信號 */

24 I2C_GenerateSTART(EEPROM_I2C, ENABLE);

25

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

27 I2CTimeout = I2CT_FLAG_TIMEOUT;

28 while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))

29 {

30 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(21);

31 }

32

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

34 I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDRESS,I2C_Direction_Transmitter);

35

36 /* 等待ADDR標志 */

37 I2CTimeout = I2CT_LONG_TIMEOUT;

38 do

39 {

40 /* 獲取SR1寄存器狀態 */

41 tmpSR1 = EEPROM_I2C->SR1;

42

43 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(22);

44 }

45 /* 一直等待直到addraf標志為1 */

46 while ((tmpSR1 & (I2C_SR1_ADDR | I2C_SR1_AF)) == 0);

47

48 /*檢查addr標志是否為1 */

49 if (tmpSR1 & I2C_SR1_ADDR)

50 {

51 /* 清除addr標志該標志通過讀SR1SR2清除 */

52 (void)EEPROM_I2C->SR2;

53

54 /*產生停止信號 */

55 I2C_GenerateSTOP(EEPROM_I2C, ENABLE);

56

57 /* 退出函數 */

58 return 1;

59 }

60 else

61 {

62 /*清除af標志 */

63 I2C_ClearFlag(EEPROM_I2C, I2C_FLAG_AF);

64 }

65

66 /*檢查等待次數*/

67 if (EETrials++ == MAX_TRIAL_NUMBER)

68 {

69 /* 等待MAX_TRIAL_NUMBER次都還沒准備好,退出等待 */

70 return I2C_TIMEOUT_UserCallback(23);

71 }

72 }

73 }

這個函數主要實現是向EEPROM發送它設備地址,檢測EEPROM的響應,若EEPROM接收到地址后返回應答信號,則表示EEPROM已經准備好,可以開始下一次通訊。函數中檢測響應是通過讀取STM32SR1寄存器的ADDR位及AF位來實現的,當I2C設備響應了地址的時候,ADDR會置1,若應答失敗,AF位會置1

EEPROM的頁寫入

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

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

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

代碼清單 238 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的頁寫入方式,可以改進前面的"多字節寫入"函數,加快傳輸速度,見代碼清單 239

代碼清單 239 快速寫入多字節函數

1

2 /* AT24C01/02每頁有8個字節 */

3 #define I2C_PageSize 8

4

5 /**

6 * @brief 將緩沖區中的數據寫到I2C EEPROM中,采用頁寫入的方式,加快寫入速度

7 * @param pBuffer:緩沖區指針

8 * @param WriteAddr:寫地址

9 * @param NumByteToWrite:寫的字節數

10 * @retval

11 */

12 void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr,

13 u16 NumByteToWrite)

14 {

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

16

17 /*mod運算求余,若writeAddrI2C_PageSize整數倍,運算結果Addr值為0*/

18 Addr = WriteAddr % I2C_PageSize;

19

20 /*count個數據,剛好可以對齊到頁地址*/

21 count = I2C_PageSize - Addr;

22 /*計算出要寫多少整數頁*/

23 NumOfPage = NumByteToWrite / I2C_PageSize;

24 /*mod運算求余,計算出剩余不滿一頁的字節數*/

25 NumOfSingle = NumByteToWrite % I2C_PageSize;

26

27 /* Addr=0,WriteAddr 剛好按頁對齊 aligned */

28 if (Addr == 0)

29 {

30 /* 如果 NumByteToWrite < I2C_PageSize */

31 if (NumOfPage == 0)

32 {

33 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);

34 I2C_EE_WaitEepromStandbyState();

35 }

36 /* 如果 NumByteToWrite > I2C_PageSize */

37 else

38 {

39 /*先把整數頁都寫了*/

40 while (NumOfPage--)

41 {

42 I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);

43 I2C_EE_WaitEepromStandbyState();

44 WriteAddr += I2C_PageSize;

45 pBuffer += I2C_PageSize;

46 }

47

48 /*若有多余的不滿一頁的數據,把它寫完*/

49 if (NumOfSingle!=0)

50 {

51 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);

52 I2C_EE_WaitEepromStandbyState();

53 }

54 }

55 }

56 /* 如果 WriteAddr 不是按 I2C_PageSize 對齊 */

57 else

58 {

59 /* 如果 NumByteToWrite < I2C_PageSize */

60 if (NumOfPage== 0)

61 {

62 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);

63 I2C_EE_WaitEepromStandbyState();

64 }

65 /* 如果 NumByteToWrite > I2C_PageSize */

66 else

67 {

68 /*地址不對齊多出的count分開處理,不加入這個運算*/

69 NumByteToWrite -= count;

70 NumOfPage = NumByteToWrite / I2C_PageSize;

71 NumOfSingle = NumByteToWrite % I2C_PageSize;

72

73 /*先把WriteAddr所在頁的剩余字節寫了*/

74 if (count != 0)

75 {

76 I2C_EE_PageWrite(pBuffer, WriteAddr, count);

77 I2C_EE_WaitEepromStandbyState();

78

79 /*WriteAddr加上count后,地址就對齊到頁了*/

80 WriteAddr += count;

81 pBuffer += count;

82 }

83 /*把整數頁都寫了*/

84 while (NumOfPage--)

85 {

86 I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);

87 I2C_EE_WaitEepromStandbyState();

88 WriteAddr += I2C_PageSize;

89 pBuffer += I2C_PageSize;

90 }

91 /*若有多余的不滿一頁的數據,把它寫完*/

92 if (NumOfSingle != 0)

93 {

94 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);

95 I2C_EE_WaitEepromStandbyState();

96 }

97 }

98 }

99 }

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

除了基本的分頁傳輸,還要考慮首地址的問題,見表 233。若首地址不是剛好對齊到頁的首地址,會需要一個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.    數據傳輸情況如表 232

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.    數據傳輸情況如表 233

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時序,它實際上包含一個寫過程和一個讀過程,見圖 2316

2316 EEPROM數據讀取時序

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

代碼清單 2310 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_I2C, I2C_FLAG_BUSY))

15 {

16 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9);

17 }

18

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

20 I2C_GenerateSTART(EEPROM_I2C, ENABLE);

21

22 I2CTimeout = I2CT_FLAG_TIMEOUT;

23

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

25 while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))

26 {

27 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);

28 }

29

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

31 I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDRESS,I2C_Direction_Transmitter);

32

33 I2CTimeout = I2CT_FLAG_TIMEOUT;

34

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

36 while (!I2C_CheckEvent(EEPROM_I2C,

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

43

44 /* 發送要讀取的EEPROM內部地址(EEPROM內部存儲器的地址) */

45 I2C_SendData(EEPROM_I2C, ReadAddr);

46

47 I2CTimeout = I2CT_FLAG_TIMEOUT;

48

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

50 while (!I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTED))

51 {

52 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(12);

53 }

54 /* 產生第二次I2C起始信號 */

55 I2C_GenerateSTART(EEPROM_I2C, ENABLE);

56

57 I2CTimeout = I2CT_FLAG_TIMEOUT;

58

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

60 while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))

61 {

62 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(13);

63 }

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

65 I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDRESS, I2C_Direction_Receiver);

66

67 I2CTimeout = I2CT_FLAG_TIMEOUT;

68

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

70 while (!I2C_CheckEvent(EEPROM_I2C,

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

84

85 /* 發送停止信號 */

86 I2C_GenerateSTOP(EEPROM_I2C, ENABLE);

87 }

88

89 I2CTimeout = I2CT_LONG_TIMEOUT;

90 while (I2C_CheckEvent(EEPROM_I2C, 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_I2C);

97

98 /* 存儲數據的指針指向下一個地址 */

99 pBuffer++;

100

101 /* 接收數據自減 */

102 NumByteToRead--;

103 }

104 }

105

106 /* 使能應答,方便下一次I2C傳輸 */

107 I2C_AcknowledgeConfig(EEPROM_I2C, ENABLE);

108 return 1;

109 }

這段中的寫過程跟前面的寫字節函數類似,而讀過程中接收數據時,需要使用庫函數I2C_ReceiveData來讀取。響應信號則通過庫函數I2C_AcknowledgeConfig來發送,DISABLE時為非響應信號,ENABLE為響應信號。

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

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

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

main函數

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

代碼清單 2312 main函數

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 F429 開發板。\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

23.4.3 下載驗證

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

23.5 每課一問

1.    在EEPROM測試程序中,分別使用單字節寫入及頁寫入函數寫入數據,對比它們消耗的時間。

2.    嘗試使用EEPROM存儲int整型變量,float型浮點變量,編寫程序寫入數據,並讀出校驗。

3.    嘗試把I2C通訊引腳的模式改成非開漏模式,測試是否還能正常通訊,為什么?

4.    查看"bsp_i2c_ee.h"文件中EEPROM_ERROR、EEPROM_INFO、EEPROM_DEBUG宏,解釋為何要使用這樣的宏輸出調試信息,而不直接使用printf函數。

 


免責聲明!

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



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