SPI協議簡介
SPI協議是由摩托羅拉公司提出的通訊協議(Serial Peripheral Interface),即串行外圍設備接口,是一種高速全雙工的通信總線。它被廣泛地使用在 ADC、 LCD 等設備與 MCU 間,要求通訊速率較高的場合。
學習本章時,可與 I2C 章節對比閱讀,體會兩種通訊總線的差異以及 EEPROM 存儲器與 FLASH 存儲器的區別。下面我們分別對 SPI 協議的物理層及協議層進行講解。
SPI物理層
SPI 通訊設備之間的常用連接方式見圖 24-1。
SPI 通訊使用 3 條總線及片選線, 3 條總線分別為 SCK、 MOSI、 MISO,片選線為SS,它們的作用介紹如下:
(1) ss( Slave Select):從設備選擇信號線,常稱為片選信號線,也稱為 NSS、 CS,以下用 NSS 表示。 當有多個 SPI 從設備與 SPI 主機相連時,設備的其它信號線 SCK、MOSI 及 MISO 同時並聯到相同的 SPI 總線上,即無論有多少個從設備,都共同只使用這 3 條總線;而每個從設備都有獨立的這一條 NSS 信號線,本信號線獨占主機的一個引腳,即有多少個從設備,就有多少條片選信號線。 I2C 協議中通過設備地址來尋址、選中總線上的某個設備並與其進行通訊;而 SPI 協議中沒有設備地址,它使用 NSS 信號線來尋址,當主機要選擇從設備時,把該從設備的 NSS 信號線設置為低電平,該從設備即被選中,即片選有效,接着主機開始與被選中的從設備進行 SPI 通訊。所以SPI 通訊以 NSS 線置低電平為開始信號,以 NSS 線被拉高作為結束信號。
(2) SCK (Serial Clock): 時鍾信號線,用於通訊數據同步。它由通訊主機產生,決定了通訊的速率,不同的設備支持的最高時鍾頻率不一樣,如 STM32 的 SPI 時鍾頻率最大為fpclk/2,兩個設備之間通訊時,通訊速率受限於低速設備。
(3) MOSI (Master Output, Slave Input): 主設備輸出/從設備輸入引腳。主機的數據從這條信號線輸出,從機由這條信號線讀入主機發送的數據,即這條線上數據的方向為主機到從機。
(4) MISO(Master Input,, Slave Output): 主設備輸入/從設備輸出引腳。主機從這條信號線讀入數據,從機的數據由這條信號線輸出到主機,即在這條線上數據的方向為從機到主機
協議層
- SPI基本通訊過程
先看看 SPI 通訊的通訊時序,見圖 24-2。
這是一個主機的通訊時序。 NSS、 SCK、 MOSI 信號都由主機控制產生,而 MISO 的信號由從機產生,主機通過該信號線讀取從機的數據。 MOSI 與 MISO 的信號只在 NSS 為低電平的時候才有效,在 SCK 的每個時鍾周期 MOSI 和 MISO 傳輸一位數據。
以上通訊流程中包含的各個信號分解如下:
- 通訊的起始和停止信號
在圖 24-2 中的標號1處, NSS 信號線由高變低,是 SPI 通訊的起始信號。 NSS 是每個從機各自獨占的信號線,當從機檢在自己的 NSS 線檢測到起始信號后,就知道自己被主機選中了,開始准備與主機通訊。在圖中的標號6處, NSS 信號由低變高,是 SPI 通訊的停止信號,表示本次通訊結束,從機的選中狀態被取消。
- 數據有效性
SPI 使用 MOSI 及 MISO 信號線來傳輸數據,使用 SCK 信號線進行數據同步。 MOSI及 MISO 數據線在 SCK 的每個時鍾周期傳輸一位數據,且數據輸入輸出是同時進行的。數據傳輸時, MSB 先行或 LSB 先行並沒有作硬性規定,但要保證兩個 SPI 通訊設備之間使用同樣的協定,一般都會采用圖 24-2 中的 MSB 先行模式。
觀察圖中的2、3、4、5標號處, MOSI 及 MISO 的數據在 SCK 的上升沿期間變化輸出,在 SCK 的下降沿時被采樣。即在 SCK 的下降沿時刻, MOSI 及 MISO 的數據有效,高電平時表示數據“1”,為低電平時表示數據“0”。在其它時刻,數據無效, MOSI 及 MISO為下一次表示數據做准備。
SPI 每次數據傳輸可以 8 位或 16 位為單位,每次傳輸的單位數不受限制。
- CPOL/CPHA 及通訊模式
上面講述的圖 24-2 中的時序只是 SPI 中的其中一種通訊模式, SPI 一共有四種通訊模式,它們的主要區別是總線空閑時 SCK 的時鍾狀態以及數據采樣時刻。為方便說明,在此引入“時鍾極性CPOL”和“時鍾相位 CPHA”的概念。
時鍾極性 CPOL 是指 SPI 通訊設備處於空閑狀態時, SCK 信號線的電平信號(即 SPI 通訊開始前、 NSS 線為高電平時 SCK 的狀態)。 CPOL=0 時, SCK 在空閑狀態時為低電平,CPOL=1 時,則相反。
時鍾相位 CPHA 是指數據的采樣的時刻,當 CPHA=0 時, MOSI 或 MISO 數據線上的信號將會在 SCK 時鍾線的“奇數邊沿” 被采樣。當 CPHA=1 時,數據線在 SCK 的“偶數邊沿” 采樣。見圖 24-3 及圖 24-4。
我們來分析這個 CPHA=0 的時序圖。首先,根據 SCK 在空閑狀態時的電平,分為兩種情況。 SCK 信號線在空閑狀態為低電平時, CPOL=0;空閑狀態為高電平時, CPOL=1。
無論 CPOL=0 還是=1,因為我們配置的時鍾相位 CPHA=0,在圖中可以看到, 采樣時刻都是在 SCK 的奇數邊沿。注意當 CPOL=0 的時候,時鍾的奇數邊沿是上升沿,而CPOL=1 的時候,時鍾的奇數邊沿是下降沿。所以 SPI 的采樣時刻不是由上升/下降沿決定的。 MOSI 和 MISO 數據線的有效信號在 SCK 的奇數邊沿保持不變,數據信號將在 SCK 奇數邊沿時被采樣,在非采樣時刻, MOSI 和 MISO 的有效信號才發生切換。類似地,當 CPHA=1 時,不受 CPOL 的影響,數據信號在 SCK 的偶數邊沿被采樣,見圖 24-4。
由 CPOL 及 CPHA 的不同狀態, SPI 分成了四種模式,見表 24-1,主機與從機需要工作在相同的模式下才可以正常通訊,實際中采用較多的是“模式 0”與“模式 3”。
STM32的SPI特性及架構
與 I2C 外設一樣, STM32 芯片也集成了專門用於 SPI 協議通訊的外設。
STM32的SPI外設簡介
STM32 的 SPI 外設可用作通訊的主機及從機,支持最高的 SCK 時鍾頻率為 fpclk/2(STM32F429 型號的芯片默認 fpclk1為 90MHz, fpclk2 為 45MHz),完全支持 SPI 協議的 4 種模式,數據幀長度可設置為 8 位或 16 位,可設置數據 MSB 先行或 LSB 先行。它還支持雙線全雙工(前面小節說明的都是這種模式)、雙線單向以及單線模式。其中雙線單向模式可以同時使用 MOSI 及 MISO 數據線向一個方向傳輸數據,可以加快一倍的傳輸速度。而單線模式則可以減少硬件接線,當然這樣速率會受到影響。我們只講解雙線全雙工模式。
STM32 的 SPI 外設還支持 I2S 功能, I2S 功能是一種音頻串行通訊協議,在我們以后講解 MP3 播放器的章節中會進行介紹。
STM32 的SPI架構剖析
2. 通訊引腳
SPI 的所有硬件架構都從圖 24-5 中左側 MOSI、 MISO、 SCK 及 NSS 線展開的。STM32 芯片有多個 SPI 外設,它們的 SPI 通訊信號引出到不同的 GPIO 引腳上,使用時必須配置到這些指定的引腳,見表 24-2。關於 GPIO 引腳的復用功能,可查閱《STM32F4xx規格書》,以它為准。
其中 SPI1、 SPI4、 SPI5、 SPI6 是 APB2 上的設備,最高通信速率達 45Mbtis/s, SPI2、SPI3 是 APB1 上的設備,最高通信速率為 22.5Mbits/s。其它功能上沒有差異。
- 時鍾控制邏輯
SCK 線的時鍾信號,由波特率發生器根據“控制寄存器CR1”中的 BR[0:2]位控制,該位是對 fpclk時鍾的分頻因子,對 fpclk 的分頻結果就是 SCK 引腳的輸出時鍾頻率,計算方法見表 24-3。
其中的 fpclk頻率是指 SPI 所在的 APB 總線頻率, APB1 為 fpclk1, APB2 為 fpckl2。通過配置“控制寄存器 CR”的“CPOL 位”及“CPHA”位可以把 SPI 設置成前面分析的 4 種 SPI 模式。
- 數據控制邏輯
SPI 的 MOSI 及 MISO 都連接到數據移位寄存器上,數據移位寄存器的內容來源於接收緩沖區及發送緩沖區以及 MISO、 MOSI 線。當向外發送數據的時候,數據移位寄存器以“發送緩沖區”為數據源,把數據一位一位地通過數據線發送出去;當從外部接收數據的時候,數據移位寄存器把數據線采樣到的數據一位一位地存儲到“接收緩沖區”中。通過寫 SPI 的“數據寄存器 DR”把數據填充到發送緩沖區中,通過 “數據寄存器 DR”,可以獲取接收緩沖區中的內容。其中數據幀長度可以通過“控制寄存器 CR1”的“DFF 位”配置成 8 位及 16 位模式;配置“LSBFIRST 位”可選擇 MSB 先行還是 LSB 先行。
- 整體控制邏輯
整體控制邏輯負責協調整個 SPI 外設,控制邏輯的工作模式根據我們配置的“控制寄存器(CR1/CR2)”的參數而改變,基本的控制參數包括前面提到的 SPI 模式、波特率、 LSB先行、主從模式、單雙向模式等等。在外設工作時,控制邏輯會根據外設的工作狀態修改“狀態寄存器(SR)”,我們只要讀取狀態寄存器相關的寄存器位,就可以了解 SPI 的工作狀態了。除此之外,控制邏輯還根據要求,負責控制產生 SPI 中斷信號、 DMA 請求及控制NSS 信號線。
實際應用中,我們一般不使用STM32 SPI 外設的標准 NSS 信號線,而是更簡單地使用普通的 GPIO,軟件控制它的電平輸出,從而產生通訊起始和停止信號。
通訊過程
STM32 使用 SPI 外設通訊時,在通訊的不同階段它會對“狀態寄存器 SR”的不同數據位寫入參數,我們通過讀取這些寄存器標志來了解通訊狀態。
圖 24-6 中的是“主模式”流程,即 STM32 作為 SPI 通訊的主機端時的數據收發過程。
主模式收發流程及事件說明如下:
(1) 控制 NSS 信號線,產生起始信號(圖中沒有畫出);
(2) 把要發送的數據寫入到“數據寄存器 DR”中,該數據會被存儲到發送緩沖區;
(3) 通訊開始, SCK 時鍾開始運行。 MOSI 把發送緩沖區中的數據一位一位地傳輸出去; MISO 則把數據一位一位地存儲進接收緩沖區中;
(4) 當發送完一幀數據的時候,“狀態寄存器 SR”中的“TXE 標志位”會被置 1,表示傳輸完一幀,發送緩沖區已空;類似地,當接收完一幀數據的時候,“RXNE標志位”會被置 1,表示傳輸完一幀,接收緩沖區非空;
(5) 等待到“TXE 標志位”為 1 時,若還要繼續發送數據,則再次往“數據寄存器DR”寫入數據即可;等待到“RXNE 標志位”為 1 時,通過讀取“數據寄存器DR”可以獲取接收緩沖區中的內容。
假如我們使能了 TXE 或 RXNE 中斷, TXE 或 RXNE 置 1 時會產生 SPI 中斷信號,進入同一個中斷服務函數,到 SPI 中斷服務程序后,可通過檢查寄存器位來了解是哪一個事件,再分別進行處理。也可以使用 DMA 方式來收發“數據寄存器 DR”中的數據。
SPI初始化結構體詳解
跟其它外設一樣, STM32 標准庫提供了 SPI 初始化結構體及初始化函數來配置 SPI 外設。初始化結構體及函數定義在庫文件“stm32f4xx_spi.h”及“stm32f4xx_spi.c”中,編程時我們可以結合這兩個文件內的注釋使用或參考庫幫助文檔。了解初始化結構體后我們就能對 SPI 外設運用自如了, 見代碼清單 24-1。
代碼清單 24-1 SPI 初始化結構體
typedef struct
{
uint16_t SPI_Direction; /*設置 SPI 的單雙向模式 */
uint16_t SPI_Mode; /*設置 SPI 的主/從機端模式 */
uint16_t SPI_DataSize; /*設置 SPI 的數據幀長度,可選 8/16 位 */
uint16_t SPI_CPOL; /*設置時鍾極性 CPOL,可選高/低電平*/
uint16_t SPI_CPHA; /*設置時鍾相位,可選奇/偶數邊沿采樣 */
uint16_t SPI_NSS; /*設置 NSS 引腳由 SPI 硬件控制還是軟件控制*/
uint16_t SPI_BaudRatePrescaler; /*設置時鍾分頻因子, fpclk/分頻數=fSCK */
uint16_t SPI_FirstBit; /*設置 MSB/LSB 先行 */
uint16_t SPI_CRCPolynomial; /*設置 CRC 校驗的表達式 */
} SPI_InitTypeDef;
這些結構體成員說明如下,其中括號內的文字是對應參數在 STM32 標准庫中定義的宏:
(1) SPI_Direction
本成員設置 SPI 的通訊方向,可設置為雙線全雙工(SPI_Direction_2Lines_FullDuplex),雙線只接收(SPI_Direction_2Lines_RxOnly),單線只接收(SPI_Direction_1Line_Rx)、單線只發送模式(SPI_Direction_1Line_Tx)。
(2) SPI_Mode
本成員設置 SPI 工作在主機模式(SPI_Mode_Master)或從機模式(SPI_Mode_Slave ),這兩個模式的最大區別為 SPI 的 SCK 信號線的時序, SCK 的時序是由通訊中的主機產生的。若被配置為從機模式, STM32 的 SPI 外設將接受外來的 SCK 信號。
(3) SPI_DataSize
本成員可以選擇 SPI 通訊的數據幀大小是為 8 位(SPI_DataSize_8b)還是 16 位(SPI_DataSize_16b)。
(4) SPI_CPOL 和 SPI_CPHA
這兩個成員配置 SPI 的時鍾極性 CPOL 和時鍾相位 CPHA,這兩個配置影響到 SPI 的通訊模式,關於 CPOL 和 CPHA 的說明參考前面“通訊模式”小節。
時鍾極性 CPOL 成員,可設置為高電平(SPI_CPOL_High)或低電平(SPI_CPOL_Low )。
時鍾相位 CPHA 則可以設置為 SPI_CPHA_1Edge(在 SCK 的奇數邊沿采集數據) 或SPI_CPHA_2Edge (在 SCK 的偶數邊沿采集數據) 。
(5) SPI_NSS
本成員配置 NSS 引腳的使用模式,可以選擇為硬件模式(SPI_NSS_Hard )與軟件模式(SPI_NSS_Soft ),在硬件模式中的 SPI 片選信號由 SPI 硬件自動產生,而軟件模式則需要我們親自把相應的 GPIO 端口拉高或置低產生非片選和片選信號。實際中軟件模式應用比較多。
(6) SPI_BaudRatePrescaler
本成員設置波特率分頻因子,分頻后的時鍾即為 SPI 的 SCK 信號線的時鍾頻率。這個成員參數可設置為 fpclk 的 2、 4、 6、 8、 16、 32、 64、 128、 256 分頻。
(7) SPI_FirstBit
所有串行的通訊協議都會有 MSB 先行(高位數據在前)還是 LSB 先行(低位數據在前)的問題,而 STM32 的 SPI 模塊可以通過這個結構體成員,對這個特性編程控制。
(8) SPI_CRCPolynomial
這是 SPI 的 CRC 校驗中的多項式,若我們使用 CRC 校驗時,就使用這個成員的參數(多項式),來計算 CRC 的值。
配置完這些結構體成員后,我們要調用 SPI_Init 函數把這些參數寫入到寄存器中,實現 SPI 的初始化,然后調用 SPI_Cmd 來使能 SPI 外設。
本實驗板中的 FLASH 芯片(型號: W25Q128)是一種使用 SPI 通訊協議的 NOR FLASH存儲器,它的 CS/CLK/DIO/DO 引腳分別連接到了 STM32 對應的 SDI 引腳NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引腳是一個普通的 GPIO,不是 SPI 的專用NSS 引腳,所以程序中我們要使用軟件控制的方式。
FLASH 芯片中還有 WP 和 HOLD 引腳。 WP 引腳可控制寫保護功能,當該引腳為低電平時,禁止寫入數據。我們直接接電源,不使用寫保護功能。 HOLD 引腳可用於暫停通訊,該引腳為低電平時,通訊暫停,數據輸出引腳輸出高阻抗狀態,時鍾和數據輸入引腳無效。我們直接接電源,不使用通訊暫停功能。
關於 FLASH 芯片的更多信息,可參考其數據手冊《W25Q128》來了解。若您使用的實驗板 FLASH 的型號或控制引腳不一樣, 只需根據我們的工程修改即可,程序的控制原理相同。
軟件設計
SPI 的接收過程和發送過程實質是一樣的,收發同步進行,關鍵在於我們的上層應用中,關注的是發送還是接收的數據。
控制 FLASH 的指令
搞定 SPI 的基本收發單元后,還需要了解如何對 FLASH 芯片進行讀寫。 FLASH 芯片自定義了很多指令,我們通過控制 STM32 利用 SPI 總線向 FLASH 芯片發送指令, FLASH芯片收到后就會執行相應的操作。
而這些指令,對主機端(STM32)來說,只是它遵守最基本的 SPI 通訊協議發送出的數據,但在設備端(FLASH 芯片)把這些數據解釋成不同的意義,所以才成為指令。查看FLASH 芯片的數據手冊《W25Q128》,可了解各種它定義的各種指令的功能及指令格式,見表 24-4。
該表中的第一列為指令名,第二列為指令編碼,第三至第 N 列的具體內容根據指令的不同而有不同的含義。其中帶括號的字節參數,方向為 FLASH 向主機傳輸,即命令響應,不帶括號的則為主機向 FLASH 傳輸。表中“A0~A23” 指 FLASH 芯片內部存儲器組織的地址; “M0~M7” 為廠商號(MANUFACTURER ID); “ID0-ID15”為 FLASH 芯片的ID;“dummy”指該處可為任意數據;“D0~D7” 為 FLASH 內部存儲矩陣的內容。
在 FLSAH 芯片內部,存儲有固定的廠商編號(M7-M0)和不同類型 FLASH 芯片獨有的編號(ID15-ID0),見表 24-5。
通過指令表中的讀 ID 指令“JEDEC ID”可以獲取這兩個編號, 該指令編碼為“9Fh”,其中“9Fh”是指 16 進制數“9F” (相當於 C 語言中的 0x9F)。緊跟指令編碼的三個字節分別為 FLASH 芯片輸出的“(M7-M0)”、“(ID15-ID8)”及“(ID7-ID0)” 。
此處我們以該指令為例,配合其指令時序圖進行講解,見圖 24-8。
主機首先通過 MOSI 線向 FLASH 芯片發送第一個字節數據為“9F h” ,當 FLASH 芯片收到該數據后,它會解讀成主機向它發送了“JEDEC 指令”,然后它就作出該命令的響應: 通過 MISO 線把它的廠商 ID(M7-M0)及芯片類型(ID15-0)發送給主機,主機接收到指令響應后可進行校驗。 常見的應用是主機端通過讀取設備 ID 來測試硬件是否連接正常,或用於識別設備。
對於 FLASH 芯片的其它指令,都是類似的,只是有的指令包含多個字節,或者響應包含更多的數據。
實際上,編寫設備驅動都是有一定的規律可循的。首先我們要確定設備使用的是什么通訊協議。如上一章的 EEPROM 使用的是 I2C,本章的 FLASH 使用的是 SPI。那么我們就先根據它的通訊協議,選擇好 STM32 的硬件模塊,並進行相應的 I2C 或 SPI 模塊初始化。接着,我們要了解目標設備的相關指令,因為不同的設備,都會有相應的不同的指令。如EEPROM 中會把第一個數據解釋為內部存儲矩陣的地址(實質就是指令)。而 FLASH 則定義了更多的指令,有寫指令,讀指令,讀 ID 指令等等。最后,我們根據這些指令的格式要求,使用通訊協議向設備發送指令,達到控制設備的目標。
FLASH 扇區擦除
由於 FLASH 存儲器的特性決定了它只能把原來為“1”的數據位改寫成“0”,而原來為“0”的數據位不能直接改寫為“1”。所以這里涉及到數據“擦除”的概念,在寫入前,必須要對目標存儲矩陣進行擦除操作,把矩陣中的數據位擦除為“1”,在數據寫入的時候,如果要存儲數據“1”,那就不修改存儲矩陣 ,在要存儲數據“0”時,才更改該位。
通常,對存儲矩陣擦除的基本操作單位都是多個字節進行,如本例子中的 FLASH 芯片支持“扇區擦除”、“塊擦除”以及“整片擦除”,見表 24-6。
FLASH 芯片的最小擦除單位為扇區(Sector),而一個塊(Block)包含 16 個扇區,其內部存儲矩陣分布見圖 24-11。
使用扇區擦除指令“Sector Erase”可控制 FLASH 芯片開始擦寫,其指令時序見圖24-14。
扇區擦除指令的第一個字節為指令編碼,緊接着發送的 3 個字節用於表示要擦除的存儲矩陣地址。要注意的是在扇區擦除指令前,還需要先發送“寫使能”指令,發送扇區擦除指令后,通過讀取寄存器狀態等待扇區擦除操作完畢,代碼實現見代碼清單 24-10。
代碼清單 24-10 擦除扇區
/**
* @brief 擦除 FLASH 扇區
* @param SectorAddr:要擦除的扇區地址
* @retval 無
*/
void SPI_FLASH_SectorErase(u32 SectorAddr)
{
/* 發送 FLASH 寫使能命令 */
SPI_FLASH_WriteEnable();
SPI_FLASH_WaitForWriteEnd();
/* 擦除扇區 */
/* 選擇 FLASH: CS 低電平 */
SPI_FLASH_CS_LOW();
/* 發送扇區擦除指令*/
SPI_FLASH_SendByte(W25X_SectorErase);
/*發送擦除扇區地址的高位*/
SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);
/* 發送擦除扇區地址的中位 */
SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);
/* 發送擦除扇區地址的低位 */
SPI_FLASH_SendByte(SectorAddr & 0xFF);
/* 停止信號 FLASH: CS 高電平 */
SPI_FLASH_CS_HIGH();
/* 等待擦除完畢*/
SPI_FLASH_WaitForWriteEnd();
}
這段代碼調用的函數在前面都已講解,只要注意發送擦除地址時高位在前即可。 調用扇區擦除指令時注意輸入的地址要對齊到 4KB。
FLASH 的頁寫入
目標扇區被擦除完畢后,就可以向它寫入數據了。與 EEPROM 類似, FLASH 芯片也有頁寫入命令,使用頁寫入命令最多可以一次向 FLASH 傳輸 256 個字節的數據,我們把這個單位為頁大小。 FLASH 頁寫入的時序見圖 24-13。
從時序圖可知,第 1 個字節為“頁寫入指令”編碼, 2-4 字節為要寫入的“地址 A”,接着的是要寫入的內容,最多個可以發送 256 字節數據,這些數據將會從“地址 A”開始,按順序寫入到 FLASH 的存儲矩陣。若發送的數據超出 256 個,則會覆蓋前面發送的數據。
與擦除指令不一樣,頁寫入指令的地址並不要求按 256 字節對齊,只要確認目標存儲單元是擦除狀態即可(即被擦除后沒有被寫入過)。所以,若對“地址 x”執行頁寫入指令后,發送了 200 個字節數據后終止通訊,下一次再執行頁寫入指令,從“地址(x+200)”開始寫入 200 個字節也是沒有問題的(小於 256 均可)。 只是在實際應用中由於基本擦除單元是4KB,一般都以扇區為單位進行讀寫,想深入了解,可學習我們的“FLASH 文件系統”相關的例子。
把頁寫入時序封裝成函數,其實現見代碼清單 24-11。
代碼清單 24-11 FLASH 的頁寫入
/**
* @brief 對 FLASH 按頁寫入數據,調用本函數寫入數據前需要先擦除扇區
* @param pBuffer,要寫入數據的指針
* @param WriteAddr,寫入地址
* @param NumByteToWrite,寫入數據長度,必須小於等於頁大小
* @retval 無
*/
void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
/* 發送 FLASH 寫使能命令 */
SPI_FLASH_WriteEnable();
/* 選擇 FLASH: CS 低電平 */
SPI_FLASH_CS_LOW();
/* 寫送寫指令*/
SPI_FLASH_SendByte(W25X_PageProgram);
/*發送寫地址的高位*/
SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
/*發送寫地址的中位*/
SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
/*發送寫地址的低位*/
SPI_FLASH_SendByte(WriteAddr & 0xFF);
if (NumByteToWrite > SPI_FLASH_PerWritePageSize)
{
NumByteToWrite = SPI_FLASH_PerWritePageSize;
FLASH_ERROR("SPI_FLASH_PageWrite too large!");
}
/* 寫入數據*/
while (NumByteToWrite--)
{
/* 發送當前要寫入的字節數據 */
SPI_FLASH_SendByte(*pBuffer);
/* 指向下一字節數據 */
pBuffer++;
}
/* 停止信號 FLASH: CS 高電平 */
SPI_FLASH_CS_HIGH();
/* 等待寫入完畢*/
SPI_FLASH_WaitForWriteEnd();
}
這段代碼的內容為: 先發送“寫使能”命令,接着才開始頁寫入時序, 然后發送指令編碼、地址, 再把要寫入的數據一個接一個地發送出去,發送完后結束通訊,檢查 FLASH狀態寄存器,等待 FLASH 內部寫入結束。
不定量數據寫入
應用的時候我們常常要寫入不定量的數據,直接調用“頁寫入”函數並不是特別方便,所以我們在它的基礎上編寫了“不定量數據寫入”的函數,基實現見代碼清單 24-12。
代碼清單 24-12 不定量數據寫入
/**
* @brief 對 FLASH 寫入數據,調用本函數寫入數據前需要先擦除扇區
* @param pBuffer,要寫入數據的指針
* @param WriteAddr,寫入地址
* @param NumByteToWrite,寫入數據長度
* @retval 無
*/
void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
/*mod 運算求余,若 writeAddr 是 SPI_FLASH_PageSize 整數倍,運算結果 Addr 值為
*/
Addr = WriteAddr % SPI_FLASH_PageSize;
/*差 count 個數據值,剛好可以對齊到頁地址*/
count = SPI_FLASH_PageSize - Addr;
/*計算出要寫多少整數頁*/
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
/*mod 運算求余,計算出剩余不滿一頁的字節數*/
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
/* Addr=0,則 WriteAddr 剛好按頁對齊 aligned */
if (Addr == 0)
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if (NumOfPage == 0)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{
/*先把整數頁都寫了*/
while (NumOfPage--)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
WriteAddr += SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
/*若有多余的不滿一頁的數據,把它寫完*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
/* 若地址與 SPI_FLASH_PageSize 不對齊 */
else
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if (NumOfPage == 0)
{
/*當前頁剩余的 count 個位置比 NumOfSingle 小,寫不完*/
if (NumOfSingle > count)
{
temp = NumOfSingle - count;
/*先寫滿當前頁*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
WriteAddr += count;
pBuffer += count;
/*再寫剩余的數據*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);
}
else /*當前頁剩余的 count 個位置能寫完 NumOfSingle 個數據*/
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
}
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{
/*地址不對齊多出的 count 分開處理,不加入這個運算*/
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
WriteAddr += count;
pBuffer += count;
/*把整數頁都寫了*/
while (NumOfPage--)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
WriteAddr += SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
/*若有多余的不滿一頁的數據,把它寫完*/
if (NumOfSingle != 0)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
}
}
這段代碼與 EEPROM 章節中的“快速寫入多字節”函數原理是一樣的,運算過程在此不再贅述。區別是頁的大小以及實際數據寫入的時候,使用的是針對 FLASH 芯片的頁寫入函數,且在實際調用這個“不定量數據寫入”函數時,還要注意確保目標扇區處於擦除狀態。
從 FLASH 讀取數據
相對於寫入, FLASH 芯片的數據讀取要簡單得多,使用讀取指令“Read Data”即可,其指令時序見圖 24-14。
發送了指令編碼及要讀的起始地址后, FLASH 芯片就會按地址遞增的方式返回存儲矩陣的內容,讀取的數據量沒有限制,只要沒有停止通訊, FLASH 芯片就會一直返回數據。代碼實現見代碼清單 24-13。
代碼清單 24-13 從 FLASH 讀取數據
/**
* @brief 讀取 FLASH 數據
* @param pBuffer,存儲讀出數據的指針
* @param ReadAddr,讀取地址
* @param NumByteToRead,讀取數據長度
* @retval 無
*/
void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)
{
/* 選擇FLASH: CS 低電平 */
SPI_FLASH_CS_LOW();
/* 發送讀 指令 */
SPI_FLASH_SendByte(W25X_ReadData);
/* 發送讀 地址高位 */
SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
/* 發送讀 地址中位 */
SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);
/* 發送讀 地址低位 */
SPI_FLASH_SendByte(ReadAddr & 0xFF);
/* 讀取數據 */
while (NumByteToRead--)
{
/* 讀取一個字節*/
*pBuffer = SPI_FLASH_SendByte(Dummy_Byte);
/* 指向下一個字節緩沖區 */
pBuffer++;
}
/* 停止信號 FLASH: CS 高電平 */
SPI_FLASH_CS_HIGH();
}
由於讀取的數據量沒有限制,所以發送讀命令后一直接收 NumByteToRead 個數據到結束即可。
參考引用:
- 野火---《零死角玩轉STM32-F429挑戰者》
- 《STM32F4xx中文參考手冊》
- 《Cortex-M4內核編程手冊》