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

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


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


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


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

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

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


五、I2C初始化結構體
初始化結構體及函數定義在庫文件“stm32f10x_i2c.h”及“stm32f10x_i2c.c”中。

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

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


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

2、I2C 硬件相關宏定義
我們把 I2C 硬件相關的配置都以宏的形式定義到 “bsp_i2c_ee.h”文件中。

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

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

3、初始化 I2C 的 GPIO
利用上面的宏,編寫 I2C GPIO 引腳的初始化函數。

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

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

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

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

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

5、向 EEPROM 寫入一個字節的數據
初始化好 I2C 外設後,就可以使用 I2C 通訊,向 EEPROM 寫入一個字節

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

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

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

這段代碼比較簡單,直接使用 for 循環調用前面定義的 I2C_EE_ByteWrite 函數一個字節一個字節地向 EEPROM 發送要寫入的數據 。在每次數據寫入通訊前調用了I2C_EE_WaitEepromStandbyState 函數等待 EEPROM 內部擦寫完畢,

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

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

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

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

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

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

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

9、從EEPROM讀取數據
從 EEPROM 讀取數據是一個 複合的 I2C 時序 ,它實際上包含一個寫過程和一個讀過程。
這裏寫圖片描述
讀時序的第一個通訊過程中,使用 I2C發送設備地址尋址(寫方向),接着發送要讀取的“內存地址”;第二個通訊過程中,再次使用 I2C 發送設備地址尋址,但這個時候的數據方向是讀方向;在這個過程之後,EEPROM 會向主機返回從“內存地址”開始的數據,一個字節一個字節地傳輸,只要主機的響應爲“應答信號”,它就會一直傳輸下去,主機想結束傳輸時,就發送“非應答信號”,並以“停止信號”結束通訊,作爲從機的 EEPROM也會停止傳輸。

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

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

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

代碼中先填充一個數組,數組的內容爲 1,2,3 至 N,接着把這個數組的內容寫入到EEPROM 中,寫入時可以採用單字節寫入的方式或頁寫入的方式。寫入完畢後再從EEPROM 的地址中讀取數據,把讀取得到的與寫入的數據進行校驗,若一致說明讀寫正常,否則讀寫過程有問題或者 EEPROM 芯片不正常。其中代碼用到的 EEPROM_INFO 跟EEPROM_ERROR 宏類似,都是對 printf 函數的封裝,使用和閱讀代碼時把它直接當成
printf 函數就好。具體的宏定義在“bsp_i2c_ee.h 文件中”,在以後的代碼我們常常會用類似的宏來輸出調試信息。
11、main函數
編寫 main 函數,函數中初始化串口、I2C 外設,然後調用上面的 I2C_Test 函數進行讀寫測試,

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

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

發佈了57 篇原創文章 · 獲贊 65 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章