第24章 SPI—讀寫串行FLASH—零死角玩轉STM32-F429系列


24     SPI—讀寫串行FLASH

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

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

 

 

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

若對SPI通訊協議不了解,可先閱讀《SPI總線協議介紹》文檔的內容學習。

關於FLASH存儲器,請參考"常用存儲器介紹"章節,實驗中FLASH芯片的具體參數,請參考其規格書《W25Q128》來了解。

24.1 SPI協議簡介

SPI協議是由摩托羅拉公司提出的通訊協議(Serial Peripheral Interface),即串行外圍設備接口,是一種高速全雙工的通信總線。它被廣泛地使用在ADCLCD等設備與MCU間,要求通訊速率較高的場合。

學習本章時,可與I2C章節對比閱讀,體會兩種通訊總線的差異以及EEPROM存儲器與FLASH存儲器的區別。下面我們分別對SPI協議的物理層及協議層進行講解。

24.1.1 SPI物理層

SPI通訊設備之間的常用連接方式見圖 241

241 常見的SPI通訊系統

SPI通訊使用3條總線及片選線,3條總線分別為SCKMOSIMISO,片選線為,它們的作用介紹如下:

(1)     ( 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):主設備輸入/從設備輸出引腳。主機從這條信號線讀入數據,從機的數據由這條信號線輸出到主機,即在這條線上數據的方向為從機到主機。

24.1.2 協議層

I2C的類似,SPI協議定義了通訊的起始和停止信號、數據有效性、時鍾同步等環節。

1.    SPI基本通訊過程

先看看SPI通訊的通訊時序,見圖 242

242 SPI通訊時序

這是一個主機的通訊時序。NSSSCKMOSI信號都由主機控制產生,而MISO的信號由從機產生,主機通過該信號線讀取從機的數據。MOSIMISO的信號只在NSS為低電平的時候才有效,在SCK的每個時鍾周期MOSIMISO傳輸一位數據。

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

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

在圖 242中的標號處,NSS信號線由高變低,是SPI通訊的起始信號。NSS是每個從機各自獨占的信號線,當從機檢在自己的NSS線檢測到起始信號后,就知道自己被主機選中了,開始准備與主機通訊。在圖中的標號†處,NSS信號由低變高,是SPI通訊的停止信號,表示本次通訊結束,從機的選中狀態被取消。

3.    數據有效性

SPI使用MOSIMISO信號線來傳輸數據,使用SCK信號線進行數據同步。MOSIMISO數據線在SCK的每個時鍾周期傳輸一位數據,且數據輸入輸出是同時進行的。數據傳輸時,MSB先行或LSB先行並沒有作硬性規定,但要保證兩個SPI通訊設備之間使用同樣的協定,一般都會采用圖 242中的MSB先行模式。

觀察圖中的‚ƒ„…標號處,MOSIMISO的數據在SCK的上升沿期間變化輸出,在SCK的下降沿時被采樣。即在SCK的下降沿時刻,MOSIMISO的數據有效,高電平時表示數據"1",為低電平時表示數據"0"。在其它時刻,數據無效,MOSIMISO為下一次表示數據做准備。

SPI每次數據傳輸可以8位或16位為單位,每次傳輸的單位數不受限制。

4.    CPOL/CPHA及通訊模式

上面講述的圖 242中的時序只是SPI中的其中一種通訊模式,SPI一共有四種通訊模式,它們的主要區別是總線空閑時SCK的時鍾狀態以及數據采樣時刻。為方便說明,在此引入"時鍾極性CPOL"和"時鍾相位CPHA"的概念。

時鍾極性CPOL是指SPI通訊設備處於空閑狀態時,SCK信號線的電平信號(SPI通訊開始前、 NSS線為高電平時SCK的狀態)CPOL=0時, SCK在空閑狀態時為低電平,CPOL=1時,則相反。

時鍾相位CPHA是指數據的采樣的時刻,當CPHA=0時,MOSIMISO數據線上的信號將會在SCK時鍾線的"奇數邊沿"被采樣。當CPHA=1時,數據線在SCK的"偶數邊沿"采樣。見圖 243及圖 244

243 CPHA=0時的SPI通訊模式

我們來分析這個CPHA=0的時序圖。首先,根據SCK在空閑狀態時的電平,分為兩種情況。SCK信號線在空閑狀態為低電平時,CPOL=0;空閑狀態為高電平時,CPOL=1

無論CPOL=0還是=1,因為我們配置的時鍾相位CPHA=0,在圖中可以看到,采樣時刻都是在SCK的奇數邊沿。注意當CPOL=0的時候,時鍾的奇數邊沿是上升沿,而CPOL=1的時候,時鍾的奇數邊沿是下降沿。所以SPI的采樣時刻不是由上升/下降沿決定的。MOSIMISO數據線的有效信號在SCK的奇數邊沿保持不變,數據信號將在SCK奇數邊沿時被采樣,在非采樣時刻,MOSIMISO的有效信號才發生切換。

類似地,當CPHA=1時,不受CPOL的影響,數據信號在SCK的偶數邊沿被采樣,見圖 244

244 CPHA=1時的SPI通訊模式

CPOLCPHA的不同狀態,SPI分成了四種模式,見表 241,主機與從機需要工作在相同的模式下才可以正常通訊,實際中采用較多的是"模式0"與"模式3"。

241 SPI的四種模式

SPI模式

CPOL

CPHA

空閑時SCK時鍾

采樣時刻

0

0

0

低電平

奇數邊沿

1

0

1

低電平

偶數邊沿

2

1

0

高電平

奇數邊沿

3

1

1

高電平

偶數邊沿

24.2 STM32的SPI特性及架構

I2C外設一樣,STM32芯片也集成了專門用於SPI協議通訊的外設。

24.2.1 STM32的SPI外設簡介

STM32SPI外設可用作通訊的主機及從機,支持最高的SCK時鍾頻率為fpclk/2 (STM32F429型號的芯片默認fpclk190MHzfpclk245MHz),完全支持SPI協議的4種模式,數據幀長度可設置為8位或16位,可設置數據MSB先行或LSB先行。它還支持雙線全雙工(前面小節說明的都是這種模式)、雙線單向以及單線模式。其中雙線單向模式可以同時使用MOSIMISO數據線向一個方向傳輸數據,可以加快一倍的傳輸速度。而單線模式則可以減少硬件接線,當然這樣速率會受到影響。我們只講解雙線全雙工模式。

STM32SPI外設還支持I2S功能,I2S功能是一種音頻串行通訊協議,在我們以后講解MP3播放器的章節中會進行介紹。

24.2.2 STM32的SPI架構剖析

245 SPI架構圖

1.    通訊引腳

SPI的所有硬件架構都從圖 245中左側MOSIMISOSCKNSS線展開的。STM32芯片有多個SPI外設,它們的SPI通訊信號引出到不同的GPIO引腳上,使用時必須配置到這些指定的引腳,見表 242。關於GPIO引腳的復用功能,可查閱《STM32F4xx規格書》,以它為准。

242 STM32F4xxSPI引腳(整理自《STM32F4xx規格書》)

引腳

SPI編號

SPI1

SPI2

SPI3

SPI4

SPI5

SPI6

MOSI

PA7/PB5

PB15/PC3/PI3

PB5/PC12/PD6

PE6/PE14

PF9/PF11

PG14

MISO

PA6/PB4

PB14/PC2/PI2

PB4/PC11

PE5/PE13

PF8/PH7

PG12

SCK

PA5/PB3

PB10/PB13/PD3

PB3/PC10

PE2/PE12

PF7/PH6

PG13

NSS

PA4/PA15

PB9/PB12/PI0

PA4/PA15

PE4/PE11

PF6/PH5

PG8

其中SPI1SPI4SPI5SPI6APB2上的設備,最高通信速率達45Mbtis/sSPI2SPI3APB1上的設備,最高通信速率為22.5Mbits/s。除了通訊速率,在其它功能上沒有差異。

2.    時鍾控制邏輯

SCK線的時鍾信號,由波特率發生器根據"控制寄存器CR1"中的BR[0:2]位控制,該位是對fpclk時鍾的分頻因子,對fpclk的分頻結果就是SCK引腳的輸出時鍾頻率,計算方法見表 243

243 BR位對fpclk的分頻

BR[0:2]

分頻結果(SCK頻率)

 

BR[0:2]

分頻結果(SCK頻率)

000

fpclk/2

 

100

fpclk/32

001

fpclk/4

 

101

fpclk/64

010

fpclk/8

 

110

fpclk/128

011

fpclk/16

 

111

fpclk/256

其中的fpclk頻率是指SPI所在的APB總線頻率,APB1fpclk1APB2fpckl2

通過配置"控制寄存器CR"的"CPOL位"及"CPHA"位可以把SPI設置成前面分析的4SPI模式。

3.    數據控制邏輯

SPIMOSIMISO都連接到數據移位寄存器上,數據移位寄存器的內容來源於接收緩沖區及發送緩沖區以及MISOMOSI線。當向外發送數據的時候,數據移位寄存器以"發送緩沖區"為數據源,把數據一位一位地通過數據線發送出去;當從外部接收數據的時候,數據移位寄存器把數據線采樣到的數據一位一位地存儲到"接收緩沖區"中。通過寫SPI的"數據寄存器DR"把數據填充到發送緩沖區中,通過"數據寄存器DR",可以獲取接收緩沖區中的內容。其中數據幀長度可以通過"控制寄存器CR1"的"DFF位"配置成8位及16位模式;配置"LSBFIRST位"可選擇MSB先行還是LSB先行。

4.    整體控制邏輯

整體控制邏輯負責協調整個SPI外設,控制邏輯的工作模式根據我們配置的"控制寄存器(CR1/CR2)"的參數而改變,基本的控制參數包括前面提到的SPI模式、波特率、LSB先行、主從模式、單雙向模式等等。在外設工作時,控制邏輯會根據外設的工作狀態修改"狀態寄存器(SR)",我們只要讀取狀態寄存器相關的寄存器位,就可以了解SPI的工作狀態了。除此之外,控制邏輯還根據要求,負責控制產生SPI中斷信號、DMA請求及控制NSS信號線。

實際應用中,我們一般不使用STM32 SPI外設的標准NSS信號線,而是更簡單地使用普通的GPIO,軟件控制它的電平輸出,從而產生通訊起始和停止信號。

24.2.3 通訊過程

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

246中的是"主模式"流程,即STM32作為SPI通訊的主機端時的數據收發過程。

246 主發送器通訊過程

主模式收發流程及事件說明如下:

(1)    控制NSS信號線,產生起始信號(圖中沒有畫出)

(2)    把要發送的數據寫入到"數據寄存器DR"中,該數據會被存儲到發送緩沖區;

(3)    通訊開始,SCK時鍾開始運行。MOSI把發送緩沖區中的數據一位一位地傳輸出去;MISO則把數據一位一位地存儲進接收緩沖區中;

(4)    當發送完一幀數據的時候,"狀態寄存器SR"中的"TXE標志位"會被置1,表示傳輸完一幀,發送緩沖區已空;類似地,當接收完一幀數據的時候,"RXNE標志位"會被置1,表示傳輸完一幀,接收緩沖區非空;

(5)    等待到"TXE標志位"為1時,若還要繼續發送數據,則再次往"數據寄存器DR"寫入數據即可;等待到"RXNE標志位"為1時,通過讀取"數據寄存器DR"可以獲取接收緩沖區中的內容。

假如我們使能了TXERXNE中斷,TXERXNE1時會產生SPI中斷信號,進入同一個中斷服務函數,到SPI中斷服務程序后,可通過檢查寄存器位來了解是哪一個事件,再分別進行處理。也可以使用DMA方式來收發"數據寄存器DR"中的數據。

24.3 SPI初始化結構體詳解

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

代碼清單 241 SPI初始化結構體

1 typedef struct

2 {

3 uint16_t SPI_Direction; /*設置SPI的單雙向模式 */

4 uint16_t SPI_Mode; /*設置SPI的主/從機端模式 */

5 uint16_t SPI_DataSize; /*設置SPI的數據幀長度,可選8/16 */

6 uint16_t SPI_CPOL; /*設置時鍾極性CPOL,可選高/低電平*/

7 uint16_t SPI_CPHA; /*設置時鍾相位,可選奇/偶數邊沿采樣 */

8 uint16_t SPI_NSS; /*設置NSS引腳由SPI硬件控制還是軟件控制*/

9 uint16_t SPI_BaudRatePrescaler; /*設置時鍾分頻因子,fpclk/分頻數=fSCK */

10 uint16_t SPI_FirstBit; /*設置MSB/LSB先行 */

11 uint16_t SPI_CRCPolynomial; /*設置CRC校驗的表達式 */

12 } 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 ),這兩個模式的最大區別為SPISCK信號線的時序,SCK的時序是由通訊中的主機產生的。若被配置為從機模式,STM32SPI外設將接受外來的SCK信號。

(3)    SPI_DataSize

本成員可以選擇SPI通訊的數據幀大小是為8(SPI_DataSize_8b)還是16(SPI_DataSize_16b)

(4)    SPI_CPOLSPI_CPHA

這兩個成員配置SPI的時鍾極性CPOL和時鍾相位CPHA,這兩個配置影響到SPI的通訊模式,關於CPOLCPHA的說明參考前面"通訊模式"小節。

時鍾極性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

本成員設置波特率分頻因子,分頻后的時鍾即為SPISCK信號線的時鍾頻率。這個成員參數可設置為fpclk2468163264128256分頻。

(7)    SPI_FirstBit

所有串行的通訊協議都會有MSB先行(高位數據在前)還是LSB先行(低位數據在前)的問題,而STM32SPI模塊可以通過這個結構體成員,對這個特性編程控制。

(8)    SPI_CRCPolynomial

這是SPICRC校驗中的多項式,若我們使用CRC校驗時,就使用這個成員的參數(多項式),來計算CRC的值。

配置完這些結構體成員后,我們要調用SPI_Init函數把這些參數寫入到寄存器中,實現SPI的初始化,然后調用SPI_Cmd來使能SPI外設。

24.4 SPI—讀寫串行FLASH實驗

FLSAH存儲器又稱閃存,它與EEPROM都是掉電后數據不丟失的存儲器,但FLASH存儲器容量普遍大於EEPROM,現在基本取代了它的地位。我們生活中常用的U盤、SD卡、SSD固態硬盤以及我們STM32芯片內部用於存儲程序的設備,都是FLASH類型的存儲器。在存儲控制上,最主要的區別是FLASH芯片只能一大片一大片地擦寫,而在"I2C章節"中我們了解到EEPROM可以單個字節擦寫。

本小節以一種使用SPI通訊的串行FLASH存儲芯片的讀寫實驗為大家講解STM32SPI使用方法。實驗中STM32SPI外設采用主模式,通過查詢事件的方式來確保正常通訊。

24.4.1 硬件設計

247 SPI串行FLASH硬件連接圖

本實驗板中的FLASH芯片(型號:W25Q128)是一種使用SPI通訊協議的NOR FLASH存儲器,它的CS/CLK/DIO/DO引腳分別連接到了STM32對應的SDI引腳NSS/SCK/MOSI/MISO上,其中STM32NSS引腳是一個普通的GPIO,不是SPI的專用NSS引腳,所以程序中我們要使用軟件控制的方式。

FLASH芯片中還有WPHOLD引腳。WP引腳可控制寫保護功能,當該引腳為低電平時,禁止寫入數據。我們直接接電源,不使用寫保護功能。HOLD引腳可用於暫停通訊,該引腳為低電平時,通訊暫停,數據輸出引腳輸出高阻抗狀態,時鍾和數據輸入引腳無效。我們直接接電源,不使用通訊暫停功能。

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

24.4.2 軟件設計

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

1.    編程要點

(7)    初始化通訊使用的目標引腳及端口時鍾;

(8)    使能SPI外設的時鍾;

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

(10)    編寫基本SPI按字節收發的函數;

(11)    編寫對FLASH擦除及讀寫操作的的函數;

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

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

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

代碼清單 242 SPI硬件配置相關的宏

1 //SPI及時鍾初始化函數

2 #define FLASH_SPI SPI3

3 #define FLASH_SPI_CLK RCC_APB1Periph_SPI3

4 #define FLASH_SPI_CLK_INIT RCC_APB1PeriphClockCmd

5 //SCK引腳

6 #define FLASH_SPI_SCK_PIN GPIO_Pin_3

7 #define FLASH_SPI_SCK_GPIO_PORT GPIOB

8 #define FLASH_SPI_SCK_GPIO_CLK RCC_AHB1Periph_GPIOB

9 #define FLASH_SPI_SCK_PINSOURCE GPIO_PinSource3

10 #define FLASH_SPI_SCK_AF GPIO_AF_SPI3

11 //MISO引腳

12 #define FLASH_SPI_MISO_PIN GPIO_Pin_4

13 #define FLASH_SPI_MISO_GPIO_PORT GPIOB

14 #define FLASH_SPI_MISO_GPIO_CLK RCC_AHB1Periph_GPIOB

15 #define FLASH_SPI_MISO_PINSOURCE GPIO_PinSource4

16 #define FLASH_SPI_MISO_AF GPIO_AF_SPI3

17 //MOSI引腳

18 #define FLASH_SPI_MOSI_PIN GPIO_Pin_5

19 #define FLASH_SPI_MOSI_GPIO_PORT GPIOB

20 #define FLASH_SPI_MOSI_GPIO_CLK RCC_AHB1Periph_GPIOB

21 #define FLASH_SPI_MOSI_PINSOURCE GPIO_PinSource5

22 #define FLASH_SPI_MOSI_AF GPIO_AF_SPI3

23 //CS(NSS)引腳

24 #define FLASH_CS_PIN GPIO_Pin_8

25 #define FLASH_CS_GPIO_PORT GPIOI

26 #define FLASH_CS_GPIO_CLK RCC_AHB1Periph_GPIOI

27

28 //控制CS(NSS)引腳輸出低電平

29 #define SPI_FLASH_CS_LOW() {FLASH_CS_GPIO_PORT->BSRRH=FLASH_CS_PIN;}

30 //控制CS(NSS)引腳輸出高電平

31 #define SPI_FLASH_CS_HIGH() {FLASH_CS_GPIO_PORT->BSRRL=FLASH_CS_PIN;}

以上代碼根據硬件連接,把與FLASH通訊使用的SPI、引腳號、引腳源以及復用功能映射都以宏封裝起來,並且定義了控制CS(NSS)引腳輸出電平的宏,以便配置產生起始和停止信號時使用。

初始化SPI的 GPIO

利用上面的宏,編寫SPI的初始化函數,見代碼清單 243

代碼清單 243 SPI的初始化函數(GPIO初始化部分)

 1 
			

2 /**

3 * @brief SPI_FLASH初始化

4 * @param

5 * @retval

6 */

7 void SPI_FLASH_Init(void)

8 {

9 GPIO_InitTypeDef GPIO_InitStructure;

10

11 /* 使能 FLASH_SPI GPIO 時鍾 */

12 /*!< SPI_FLASH_SPI_CS_GPIO, SPI_FLASH_SPI_MOSI_GPIO,

13 SPI_FLASH_SPI_MISO_GPIO SPI_FLASH_SPI_SCK_GPIO 時鍾使能 */

14 RCC_AHB1PeriphClockCmd (FLASH_SPI_SCK_GPIO_CLK | FLASH_SPI_MISO_GPIO_CLK|

15 FLASH_SPI_MOSI_GPIO_CLK|FLASH_CS_GPIO_CLK, ENABLE);

16

17 /*!< SPI_FLASH_SPI 時鍾使能 */

18 FLASH_SPI_CLK_INIT(FLASH_SPI_CLK, ENABLE);

19

20 //設置引腳復用

21 GPIO_PinAFConfig(FLASH_SPI_SCK_GPIO_PORT,FLASH_SPI_SCK_PINSOURCE,

22 FLASH_SPI_SCK_AF);

23 GPIO_PinAFConfig(FLASH_SPI_MISO_GPIO_PORT,FLASH_SPI_MISO_PINSOURCE,

24 FLASH_SPI_MISO_AF);

25 GPIO_PinAFConfig(FLASH_SPI_MOSI_GPIO_PORT,FLASH_SPI_MOSI_PINSOURCE,

26 FLASH_SPI_MOSI_AF);

27

28 /*!< 配置 SPI_FLASH_SPI 引腳: SCK */

29 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN;

30 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

31 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;

32 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;

33 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;

35

36 GPIO_Init(FLASH_SPI_SCK_GPIO_PORT, &GPIO_InitStructure);

37

38 /*!< 配置 SPI_FLASH_SPI 引腳: MISO */

39 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;

40 GPIO_Init(FLASH_SPI_MISO_GPIO_PORT, &GPIO_InitStructure);

41

42 /*!< 配置 SPI_FLASH_SPI 引腳: MOSI */

43 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;

44 GPIO_Init(FLASH_SPI_MOSI_GPIO_PORT, &GPIO_InitStructure);

45

46 /*!< 配置 SPI_FLASH_SPI 引腳: CS */

47 GPIO_InitStructure.GPIO_Pin = FLASH_CS_PIN;

48 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;

49 GPIO_Init(FLASH_CS_GPIO_PORT, &GPIO_InitStructure);

50

51 /* 停止信號 FLASH: CS引腳高電平*/

52 SPI_FLASH_CS_HIGH();

53 /*為方便講解,以下省略SPI模式初始化部分*/

54 //......

55 }

與所有使用到GPIO的外設一樣,都要先把使用到的GPIO引腳模式初始化,配置好復用功能。GPIO初始化流程如下:

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

(2)    調用庫函數RCC_AHB1PeriphClockCmd來使能SPI引腳使用的GPIO端口時鍾,調用時使用"|"操作同時配置多個引腳。調用宏FLASH_SPI_CLK_INIT使能SPI外設時鍾(該宏封裝了APB時鍾使能的庫函數)

(3)    GPIO初始化結構體賦值,把SCK/MOSI/MISO引腳初始化成復用推挽模式。而CS(NSS)引腳由於使用軟件控制,我們把它配置為普通的推挽輸出模式。

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

配置SPI的模式

以上只是配置了SPI使用的引腳,對SPI外設模式的配置。在配置STM32SPI模式前,我們要先了解從機端的SPI模式。本例子中可通過查閱FLASH數據手冊《W25Q128》獲取。根據FLASH芯片的說明,它支持SPI模式0及模式3,支持雙線全雙工,使用MSB先行模式,支持最高通訊時鍾為104MHz,數據幀長度為8位。我們要把STM32SPI外設中的這些參數配置一致。見代碼清單 244

代碼清單 244 配置SPI模式

1 /**

2 * @brief SPI_FLASH引腳初始化

3 * @param

4 * @retval

5 */

6 void SPI_FLASH_Init(void)

7 {

8 /*為方便講解,省略了SPIGPIO初始化部分*/

9 //......

10

11 SPI_InitTypeDef SPI_InitStructure;

12 /* FLASH_SPI 模式配置 */

13 // FLASH芯片支持SPI模式0及模式3,據此設置CPOL CPHA

14 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;

15 SPI_InitStructure.SPI_Mode = SPI_Mode_Master;

16 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;

17 SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;

18 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;

19 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;

20 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;

21 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;

22 SPI_InitStructure.SPI_CRCPolynomial = 7;

23 SPI_Init(FLASH_SPI, &SPI_InitStructure);

24

25 /* 使能 FLASH_SPI */

26 SPI_Cmd(FLASH_SPI, ENABLE);

27 }

這段代碼中,把STM32SPI外設配置為主機端,雙線全雙工模式,數據幀長度為8位,使用SPI模式3(CPOL=1CPHA=1)NSS引腳由軟件控制以及MSB先行模式。最后一個成員為CRC計算式,由於我們與FLASH芯片通訊不需要CRC校驗,並沒有使能SPICRC功能,這時CRC計算式的成員值是無效的。

賦值結束后調用庫函數SPI_Init把這些配置寫入寄存器,並調用SPI_Cmd函數使能外設。

使用SPI發送和接收一個字節的數據

初始化好SPI外設后,就可以使用SPI通訊了,復雜的數據通訊都是由單個字節數據收發組成的,我們看看它的代碼實現,見代碼清單 245

代碼清單 245 使用SPI發送和接收一個字節的數據

1 #define Dummy_Byte 0xFF

2 /**

3 * @brief 使用SPI發送一個字節的數據

4 * @param byte:要發送的數據

5 * @retval 返回接收到的數據

6 */

7 u8 SPI_FLASH_SendByte(u8 byte)

8 {

9 SPITimeout = SPIT_FLAG_TIMEOUT;

10

11 /* 等待發送緩沖區為空,TXE事件 */

12 while (SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_TXE) == RESET)

13 {

14 if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);

15 }

16

17 /* 寫入數據寄存器,把要寫入的數據寫入發送緩沖區 */

18 SPI_I2S_SendData(FLASH_SPI, byte);

19

20 SPITimeout = SPIT_FLAG_TIMEOUT;

21

22 /* 等待接收緩沖區非空,RXNE事件 */

23 while (SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_RXNE) == RESET)

24 {

25 if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);

26 }

27

28 /* 讀取數據寄存器,獲取接收緩沖區數據 */

29 return SPI_I2S_ReceiveData(FLASH_SPI);

30 }

31

32 /**

33 * @brief 使用SPI讀取一個字節的數據

34 * @param

35 * @retval 返回接收到的數據

36 */

37 u8 SPI_FLASH_ReadByte(void)

38 {

39 return (SPI_FLASH_SendByte(Dummy_Byte));

40 }

SPI_FLASH_SendByte發送單字節函數中包含了等待事件的超時處理,這部分原理跟I2C中的一樣,在此不再贅述。

SPI_FLASH_SendByte函數實現了前面講解的"SPI通訊過程":

(1)    本函數中不包含SPI起始和停止信號,只是收發的主要過程,所以在調用本函數前后要做好起始和停止信號的操作;

(2)    SPITimeout變量賦值為宏SPIT_FLAG_TIMEOUT。這個SPITimeout變量在下面的while循環中每次循環減1,該循環通過調用庫函數SPI_I2S_GetFlagStatus檢測事件,若檢測到事件,則進入通訊的下一階段,若未檢測到事件則停留在此處一直檢測,當檢測SPIT_FLAG_TIMEOUT次都還沒等待到事件則認為通訊失敗,調用的SPI_TIMEOUT_UserCallback輸出調試信息,並退出通訊;

(3)    通過檢測TXE標志,獲取發送緩沖區的狀態,若發送緩沖區為空,則表示可能存在的上一個數據已經發送完畢;

(4)    等待至發送緩沖區為空后,調用庫函數SPI_I2S_SendData把要發送的數據"byte"寫入到SPI的數據寄存器DR,寫入SPI數據寄存器的數據會存儲到發送緩沖區,由SPI外設發送出去;

(5)    寫入完畢后等待RXNE事件,即接收緩沖區非空事件。由於SPI雙線全雙工模式下MOSIMISO數據傳輸是同步的(請對比"SPI通訊過程"閱讀),當接收緩沖區非空時,表示上面的數據發送完畢,且接收緩沖區也收到新的數據;

(6)    等待至接收緩沖區非空時,通過調用庫函數SPI_I2S_ReceiveData讀取SPI的數據寄存器DR,就可以獲取接收緩沖區中的新數據了。代碼中使用關鍵字"return"把接收到的這個數據作為SPI_FLASH_SendByte函數的返回值,所以我們可以看到在下面定義的SPI接收數據函數SPI_FLASH_ReadByte,它只是簡單地調用了SPI_FLASH_SendByte函數發送數據"Dummy_Byte",然后獲取其返回值(因為不關注發送的數據,所以此時的輸入參數"Dummy_Byte"可以為任意值)。可以這樣做的原因是SPI的接收過程和發送過程實質是一樣的,收發同步進行,關鍵在於我們的上層應用中,關注的是發送還是接收的數據。

控制FLASH的指令

搞定SPI的基本收發單元后,還需要了解如何對FLASH芯片進行讀寫。FLASH芯片自定義了很多指令,我們通過控制STM32利用SPI總線向FLASH芯片發送指令,FLASH芯片收到后就會執行相應的操作。

而這些指令,對主機端(STM32)來說,只是它遵守最基本的SPI通訊協議發送出的數據,但在設備端(FLASH芯片)把這些數據解釋成不同的意義,所以才成為指令。查看FLASH芯片的數據手冊《W25Q128》,可了解各種它定義的各種指令的功能及指令格式,見表 244

244 FLASH常用芯片指令表(摘自規格書《W25Q128)

指令

第一字節(指令編碼)

第二字節

第三字節

第四字節

第五字節

第六字節

第七-N字節

Write Enable

06h

  

  

  

  

  

  

Write Disable

04h

  

  

  

  

  

  

Read Status Register

05h

(S7–S0)

  

  

  

  

 

Write Status Register

01h

(S7–S0)

  

  

  

  

  

Read Data

03h

A23–A16

A15–A8

A7–A0

(D7–D0)

(Next byte)

continuous

Fast Read

0Bh

A23–A16

A15–A8

A7–A0

dummy

(D7–D0)

(Next Byte) continuous

Fast Read Dual Output

3Bh

A23–A16

A15–A8

A7–A0

dummy

I/O = (D6,D4,D2,D0) O = (D7,D5,D3,D1)

(one byte per 4 clocks, continuous)

Page Program

02h

A23–A16

A15–A8

A7–A0

D7–D0

Next byte

Up to 256 bytes

Block Erase(64KB)

D8h

A23–A16

A15–A8

A7–A0

  

  

  

Sector Erase(4KB)

20h

A23–A16

A15–A8

A7–A0

  

  

  

Chip Erase

C7h

  

  

  

  

  

  

Power-down

B9h

  

  

  

  

  

  

Release Power- down / Device ID

ABh

dummy

dummy

dummy

(ID7-ID0)

  

  

Manufacturer/ Device ID

90h

dummy

dummy

00h

(M7-M0)

(ID7-ID0)

  

JEDEC ID

9Fh

(M7-M0)

生產廠商

(ID15-ID8)

存儲器類型

(ID7-ID0) 容量

  

  

  

該表中的第一列為指令名,第二列為指令編碼,第三至第N列的具體內容根據指令的不同而有不同的含義。其中帶括號的字節參數,方向為FLASH向主機傳輸,即命令響應,不帶括號的則為主機向FLASH傳輸。表中"A0~A23"指FLASH芯片內部存儲器組織的地址;"M0~M7"為廠商號(MANUFACTURER ID);"ID0-ID15"為FLASH芯片的ID;"dummy"指該處可為任意數據;"D0~D7"為FLASH內部存儲矩陣的內容。

FLSAH芯片內部,存儲有固定的廠商編號(M7-M0)和不同類型FLASH芯片獨有的編號(ID15-ID0),見表 245

245 FLASH數據手冊的設備ID說明

FLASH型號

廠商號(M7-M0)

FLASH型號(ID15-ID0)

W25Q64

EF h

4017 h

W25Q128

EF h

4018 h

通過指令表中的讀ID指令"JEDEC ID"可以獲取這兩個編號,該指令編碼為"9F h",其中"9F h"是指16進制數"9F" (相當於C語言中的0x9F)。緊跟指令編碼的三個字節分別為FLASH芯片輸出的"(M7-M0)"、"(ID15-ID8)"及"(ID7-ID0)"

此處我們以該指令為例,配合其指令時序圖進行講解,見圖 248

248 FLASHID指令"JEDEC ID"的時序(摘自規格書《W25Q128)

主機首先通過MOSI線向FLASH芯片發送第一個字節數據為"9F h",當FLASH芯片收到該數據后,它會解讀成主機向它發送了"JEDEC指令",然后它就作出該命令的響應:通過MISO線把它的廠商ID(M7-M0)及芯片類型(ID15-0)發送給主機,主機接收到指令響應后可進行校驗。常見的應用是主機端通過讀取設備ID來測試硬件是否連接正常,或用於識別設備。

對於FLASH芯片的其它指令,都是類似的,只是有的指令包含多個字節,或者響應包含更多的數據。

實際上,編寫設備驅動都是有一定的規律可循的。首先我們要確定設備使用的是什么通訊協議。如上一章的EEPROM使用的是I2C,本章的FLASH使用的是SPI。那么我們就先根據它的通訊協議,選擇好STM32的硬件模塊,並進行相應的I2CSPI模塊初始化。接着,我們要了解目標設備的相關指令,因為不同的設備,都會有相應的不同的指令。如EEPROM中會把第一個數據解釋為內部存儲矩陣的地址(實質就是指令)。而FLASH則定義了更多的指令,有寫指令,讀指令,讀ID指令等等。最后,我們根據這些指令的格式要求,使用通訊協議向設備發送指令,達到控制設備的目標。

定義FLASH指令編碼表

為了方便使用,我們把FLASH芯片的常用指令編碼使用宏來封裝起來,后面需要發送指令編碼的時候我們直接使用這些宏即可,見代碼清單 246

代碼清單 246 FLASH指令編碼表

1 /*FLASH常用命令*/

2 #define W25X_WriteEnable 0x06

3 #define W25X_WriteDisable 0x04

4 #define W25X_ReadStatusReg 0x05

5 #define W25X_WriteStatusReg 0x01

6 #define W25X_ReadData 0x03

7 #define W25X_FastReadData 0x0B

8 #define W25X_FastReadDual 0x3B

9 #define W25X_PageProgram 0x02

10 #define W25X_BlockErase 0xD8

11 #define W25X_SectorErase 0x20

12 #define W25X_ChipErase 0xC7

13 #define W25X_PowerDown 0xB9

14 #define W25X_ReleasePowerDown 0xAB

15 #define W25X_DeviceID 0xAB

16 #define W25X_ManufactDeviceID 0x90

17 #define W25X_JedecDeviceID 0x9F

18 /*其它*/

19 #define sFLASH_ID 0XEF4018

20 #define Dummy_Byte 0xFF

讀取FLASH芯片ID

根據"JEDEC"指令的時序,我們把讀取FLASH ID的過程編寫成一個函數,見代碼清單 247

代碼清單 247 讀取FLASH芯片ID

1 /**

2 * @brief 讀取FLASH ID

3 * @param

4 * @retval FLASH ID

5 */

6 u32 SPI_FLASH_ReadID(void)

7 {

8 u32 Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;

9

10 /* 開始通訊:CS低電平 */

11 SPI_FLASH_CS_LOW();

12

13 /* 發送JEDEC指令,讀取ID */

14 SPI_FLASH_SendByte(W25X_JedecDeviceID);

15

16 /* 讀取一個字節數據 */

17 Temp0 = SPI_FLASH_SendByte(Dummy_Byte);

18

19 /* 讀取一個字節數據 */

20 Temp1 = SPI_FLASH_SendByte(Dummy_Byte);

21

22 /* 讀取一個字節數據 */

23 Temp2 = SPI_FLASH_SendByte(Dummy_Byte);

24

25 /* 停止通訊:CS高電平 */

26 SPI_FLASH_CS_HIGH();

27

28 /*把數據組合起來,作為函數的返回值*/

29 Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;

30

31 return Temp;

32 }

這段代碼利用控制CS引腳電平的宏"SPI_FLASH_CS_LOW/HIGH"以及前面編寫的單字節收發函數SPI_FLASH_SendByte,很清晰地實現了"JEDEC ID"指令的時序:發送一個字節的指令編碼"W25X_JedecDeviceID",然后讀取3個字節,獲取FLASH芯片對該指令的響應,最后把讀取到的這3個數據合並到一個變量Temp中,然后作為函數返回值,把該返回值與我們定義的宏"sFLASH_ID"對比,即可知道FLASH芯片是否正常。

FLASH寫使能以及讀取當前狀態

在向FLASH芯片存儲矩陣寫入數據前,首先要使能寫操作,通過"Write Enable"命令即可寫使能,見代碼清單 248

代碼清單 248 寫使能命令

1 /**

2 * @brief FLASH發送寫使能命令

3 * @param none

4 * @retval none

5 */

6 void SPI_FLASH_WriteEnable(void)

7 {

8 /* 通訊開始:CS */

9 SPI_FLASH_CS_LOW();

10

11 /* 發送寫使能命令*/

12 SPI_FLASH_SendByte(W25X_WriteEnable);

13

14 /*通訊結束:CS */

15 SPI_FLASH_CS_HIGH();

16 }

EEPROM一樣,由於FLASH芯片向內部存儲矩陣寫入數據需要消耗一定的時間,並不是在總線通訊結束的一瞬間完成的,所以在寫操作后需要確認FLASH芯片"空閑"時才能進行再次寫入。為了表示自己的工作狀態,FLASH芯片定義了一個狀態寄存器,見圖 249

249 FLASH芯片的狀態寄存器

我們只關注這個狀態寄存器的第0位"BUSY",當這個位為"1"時,表明FLASH芯片處於忙碌狀態,它可能正在對內部的存儲矩陣進行"擦除"或"數據寫入"的操作。

利用指令表中的"Read Status Register"指令可以獲取FLASH芯片狀態寄存器的內容,其時序見圖 2410

2410 讀取狀態寄存器的時序

只要向FLASH芯片發送了讀狀態寄存器的指令,FLASH芯片就會持續向主機返回最新的狀態寄存器內容,直到收到SPI通訊的停止信號。據此我們編寫了具有等待FLASH芯片寫入結束功能的函數,見代碼清單 249

代碼清單 249 通過讀狀態寄存器等待FLASH芯片空閑

1 /*WIP(BUSY)標志:FLASH內部正在寫入*/

2 #define WIP_Flag 0x01

3

4 /**

5 * @brief 等待WIP(BUSY)標志被置0,即等待到FLASH內部數據寫入完畢

6 * @param none

7 * @retval none

8 */

9 void SPI_FLASH_WaitForWriteEnd(void)

10 {

11 u8 FLASH_Status = 0;

12 /* 選擇 FLASH: CS */

13 SPI_FLASH_CS_LOW();

14

15 /* 發送讀狀態寄存器命令 */

16 SPI_FLASH_SendByte(W25X_ReadStatusReg);

17

18 SPITimeout = SPIT_FLAG_TIMEOUT;

19 /* FLASH忙碌,則等待 */

20 do

21 {

22 /* 讀取FLASH芯片的狀態寄存器 */

23 FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);

24 if ((SPITimeout--) == 0)

25 {

26 SPI_TIMEOUT_UserCallback(4);

27 return;

28 }

29 }

30 while ((FLASH_Status & WIP_Flag) == SET); /* 正在寫入標志 */

31

32 /* 停止信號 FLASH: CS */

33 SPI_FLASH_CS_HIGH();

34 }

這段代碼發送讀狀態寄存器的指令編碼"W25X_ReadStatusReg"后,在while循環里持續獲取寄存器的內容並檢驗它的"WIP_Flag標志"(BUSY),一直等待到該標志表示寫入結束時才退出本函數,以便繼續后面與FLASH芯片的數據通訊。

FLASH扇區擦除

由於FLASH存儲器的特性決定了它只能把原來為"1"的數據位改寫成"0",而原來為"0"的數據位不能直接改寫為"1"。所以這里涉及到數據"擦除"的概念,在寫入前,必須要對目標存儲矩陣進行擦除操作,把矩陣中的數據位擦除為"1",在數據寫入的時候,如果要存儲數據"1",那就不修改存儲矩陣,在要存儲數據"0"時,才更改該位。

通常,對存儲矩陣擦除的基本操作單位都是多個字節進行,如本例子中的FLASH芯片支持"扇區擦除"、"塊擦除"以及"整片擦除",見表 246

246 本實驗FLASH芯片的擦除單位

擦除單位

大小

扇區擦除Sector Erase

4KB

塊擦除Block Erase

64KB

整片擦除Chip Erase

整個芯片完全擦除

FLASH芯片的最小擦除單位為扇區(Sector),而一個塊(Block)包含16個扇區,其內部存儲矩陣分布見圖 2411。。

2411 FLASH芯片的存儲矩陣

使用扇區擦除指令"Sector Erase"可控制FLASH芯片開始擦寫,其指令時序見圖 2414

2412 扇區擦除時序

扇區擦除指令的第一個字節為指令編碼,緊接着發送的3個字節用於表示要擦除的存儲矩陣地址。要注意的是在扇區擦除指令前,還需要先發送"寫使能"指令,發送扇區擦除指令后,通過讀取寄存器狀態等待扇區擦除操作完畢,代碼實現見代碼清單 2410

代碼清單 2410 擦除扇區

1 /**

2 * @brief 擦除FLASH扇區

3 * @param SectorAddr:要擦除的扇區地址

4 * @retval

5 */

6 void SPI_FLASH_SectorErase(u32 SectorAddr)

7 {

8 /* 發送FLASH寫使能命令 */

9 SPI_FLASH_WriteEnable();

10 SPI_FLASH_WaitForWriteEnd();

11 /* 擦除扇區 */

12 /* 選擇FLASH: CS低電平 */

13 SPI_FLASH_CS_LOW();

14 /* 發送扇區擦除指令*/

15 SPI_FLASH_SendByte(W25X_SectorErase);

16 /*發送擦除扇區地址的高位*/

17 SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);

18 /* 發送擦除扇區地址的中位 */

19 SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);

20 /* 發送擦除扇區地址的低位 */

21 SPI_FLASH_SendByte(SectorAddr & 0xFF);

22 /* 停止信號 FLASH: CS 高電平 */

23 SPI_FLASH_CS_HIGH();

24 /* 等待擦除完畢*/

25 SPI_FLASH_WaitForWriteEnd();

26 }

這段代碼調用的函數在前面都已講解,只要注意發送擦除地址時高位在前即可。調用扇區擦除指令時注意輸入的地址要對齊到4KB

FLASH的頁寫入

目標扇區被擦除完畢后,就可以向它寫入數據了。與EEPROM類似,FLASH芯片也有頁寫入命令,使用頁寫入命令最多可以一次向FLASH傳輸256個字節的數據,我們把這個單位為頁大小。FLASH頁寫入的時序見圖 2413

2413 FLASH芯片頁寫入

從時序圖可知,第1個字節為"頁寫入指令"編碼,2-4字節為要寫入的"地址A",接着的是要寫入的內容,最多個可以發送256字節數據,這些數據將會從"地址A"開始,按順序寫入到FLASH的存儲矩陣。若發送的數據超出256個,則會覆蓋前面發送的數據。

與擦除指令不一樣,頁寫入指令的地址並不要求按256字節對齊,只要確認目標存儲單元是擦除狀態即可(即被擦除后沒有被寫入過)。所以,若對"地址x"執行頁寫入指令后,發送了200個字節數據后終止通訊,下一次再執行頁寫入指令,從"地址(x+200)"開始寫入200個字節也是沒有問題的(小於256均可)只是在實際應用中由於基本擦除單元是4KB,一般都以扇區為單位進行讀寫,想深入了解,可學習我們的"FLASH文件系統"相關的例子。

把頁寫入時序封裝成函數,其實現見代碼清單 2411

代碼清單 2411 FLASH的頁寫入

1 /**

2 * @brief FLASH按頁寫入數據,調用本函數寫入數據前需要先擦除扇區

3 * @param pBuffer,要寫入數據的指針

4 * @param WriteAddr,寫入地址

5 * @param NumByteToWrite,寫入數據長度,必須小於等於頁大小

6 * @retval

7 */

8 void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)

9 {

10 /* 發送FLASH寫使能命令 */

11 SPI_FLASH_WriteEnable();

12

13 /* 選擇FLASH: CS低電平 */

14 SPI_FLASH_CS_LOW();

15 /* 寫送寫指令*/

16 SPI_FLASH_SendByte(W25X_PageProgram);

17 /*發送寫地址的高位*/

18 SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);

19 /*發送寫地址的中位*/

20 SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);

21 /*發送寫地址的低位*/

22 SPI_FLASH_SendByte(WriteAddr & 0xFF);

23

24 if (NumByteToWrite > SPI_FLASH_PerWritePageSize)

25 {

26 NumByteToWrite = SPI_FLASH_PerWritePageSize;

27 FLASH_ERROR("SPI_FLASH_PageWrite too large!");

28 }

29

30 /* 寫入數據*/

31 while (NumByteToWrite--)

32 {

33 /* 發送當前要寫入的字節數據 */

34 SPI_FLASH_SendByte(*pBuffer);

35 /* 指向下一字節數據 */

36 pBuffer++;

37 }

38

39 /* 停止信號 FLASH: CS 高電平 */

40 SPI_FLASH_CS_HIGH();

41

42 /* 等待寫入完畢*/

43 SPI_FLASH_WaitForWriteEnd();

44 }

這段代碼的內容為:先發送"寫使能"命令,接着才開始頁寫入時序,然后發送指令編碼、地址,再把要寫入的數據一個接一個地發送出去,發送完后結束通訊,檢查FLASH狀態寄存器,等待FLASH內部寫入結束。

不定量數據寫入

應用的時候我們常常要寫入不定量的數據,直接調用"頁寫入"函數並不是特別方便,所以我們在它的基礎上編寫了"不定量數據寫入"的函數,基實現見代碼清單 2412

代碼清單 2412不定量數據寫入

1 /**

2 * @brief FLASH寫入數據,調用本函數寫入數據前需要先擦除扇區

3 * @param pBuffer,要寫入數據的指針

4 * @param WriteAddr,寫入地址

5 * @param NumByteToWrite,寫入數據長度

6 * @retval

7 */

8 void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)

9 {

10 u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;

11

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

13 Addr = WriteAddr % SPI_FLASH_PageSize;

14

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

16 count = SPI_FLASH_PageSize - Addr;

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

18 NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;

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

20 NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;

21

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

23 if (Addr == 0)

24 {

25 /* NumByteToWrite < SPI_FLASH_PageSize */

26 if (NumOfPage == 0)

27 {

28 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);

29 }

30 else /* NumByteToWrite > SPI_FLASH_PageSize */

31 {

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

33 while (NumOfPage--)

34 {

35 SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);

36 WriteAddr += SPI_FLASH_PageSize;

37 pBuffer += SPI_FLASH_PageSize;

38 }

39

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

41 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);

42 }

43 }

44 /* 若地址與 SPI_FLASH_PageSize 不對齊 */

45 else

46 {

47 /* NumByteToWrite < SPI_FLASH_PageSize */

48 if (NumOfPage == 0)

49 {

50 /*當前頁剩余的count個位置比NumOfSingle小,寫不完*/

51 if (NumOfSingle > count)

52 {

53 temp = NumOfSingle - count;

54

55 /*先寫滿當前頁*/

56 SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);

57 WriteAddr += count;

58 pBuffer += count;

59

60 /*再寫剩余的數據*/

61 SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);

62 }

63 else /*當前頁剩余的count個位置能寫完NumOfSingle個數據*/

64 {

65 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);

66 }

67 }

68 else /* NumByteToWrite > SPI_FLASH_PageSize */

69 {

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

71 NumByteToWrite -= count;

72 NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;

73 NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;

74

75 SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);

76 WriteAddr += count;

77 pBuffer += count;

78

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

80 while (NumOfPage--)

81 {

82 SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);

83 WriteAddr += SPI_FLASH_PageSize;

84 pBuffer += SPI_FLASH_PageSize;

85 }

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

87 if (NumOfSingle != 0)

88 {

89 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);

90 }

91 }

92 }

93 }

這段代碼與EEPROM章節中的"快速寫入多字節"函數原理是一樣的,運算過程在此不再贅述。區別是頁的大小以及實際數據寫入的時候,使用的是針對FLASH芯片的頁寫入函數,且在實際調用這個"不定量數據寫入"函數時,還要注意確保目標扇區處於擦除狀態。

從FLASH讀取數據

相對於寫入,FLASH芯片的數據讀取要簡單得多,使用讀取指令"Read Data"即可,其指令時序見圖 2414

2414 SPI FLASH讀取數據時序

發送了指令編碼及要讀的起始地址后,FLASH芯片就會按地址遞增的方式返回存儲矩陣的內容,讀取的數據量沒有限制,只要沒有停止通訊,FLASH芯片就會一直返回數據。代碼實現見代碼清單 2413

代碼清單 2413 FLASH讀取數據

1 /**

2 * @brief 讀取FLASH數據

3 * @param pBuffer,存儲讀出數據的指針

4 * @param ReadAddr,讀取地址

5 * @param NumByteToRead,讀取數據長度

6 * @retval

7 */

8 void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)

9 {

10 /* 選擇FLASH: CS低電平 */

11 SPI_FLASH_CS_LOW();

12

13 /* 發送指令 */

14 SPI_FLASH_SendByte(W25X_ReadData);

15

16 /* 發送地址高位 */

17 SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);

18 /* 發送地址中位 */

19 SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);

20 /* 發送地址低位 */

21 SPI_FLASH_SendByte(ReadAddr & 0xFF);

22

23 /* 讀取數據 */

24 while (NumByteToRead--)

25 {

26 /* 讀取一個字節*/

27 *pBuffer = SPI_FLASH_SendByte(Dummy_Byte);

28 /* 指向下一個字節緩沖區 */

29 pBuffer++;

30 }

31

32 /* 停止信號 FLASH: CS 高電平 */

33 SPI_FLASH_CS_HIGH();

34 }

由於讀取的數據量沒有限制,所以發送讀命令后一直接收NumByteToRead個數據到結束即可。

3.    main函數

最后我們來編寫main函數,進行FLASH芯片讀寫校驗,見代碼清單 2414

代碼清單 2414 main函數

1 /* 獲取緩沖區的長度 */

2 #define TxBufferSize1 (countof(TxBuffer1) - 1)

3 #define RxBufferSize1 (countof(TxBuffer1) - 1)

4 #define countof(a) (sizeof(a) / sizeof(*(a)))

5 #define BufferSize (countof(Tx_Buffer)-1)

6

7 #define FLASH_WriteAddress 0x00000

8 #define FLASH_ReadAddress FLASH_WriteAddress

9 #define FLASH_SectorToErase FLASH_WriteAddress

10

11

12 /* 發送緩沖區初始化 */

13 uint8_t Tx_Buffer[] = "感謝您選用秉火stm32開發板\r\n";

14 uint8_t Rx_Buffer[BufferSize];

15

16 //讀取的ID存儲位置

17 __IO uint32_t DeviceID = 0;

18 __IO uint32_t FlashID = 0;

19 __IO TestStatus TransferStatus1 = FAILED;

20

21 // 函數原型聲明

22 void Delay(__IO uint32_t nCount);

23

24 /*

25 * 函數名:main

26 * 描述:主函數

27 * 輸入:無

28 * 輸出:無

29 */

30 int main(void)

31 {

32 LED_GPIO_Config();

33 LED_BLUE;

34

35 /* 配置串口1為:115200 8-N-1 */

36 Debug_USART_Config();

37

38 printf("\r\n這是一個16M串行flash(W25Q128)實驗 \r\n");

39

40 /* 16M串行flash W25Q128初始化 */

41 SPI_FLASH_Init();

42

43 Delay( 200 );

44

45 /* 獲取 SPI Flash ID */

46 FlashID = SPI_FLASH_ReadID();

47

48 /* 檢驗 SPI Flash ID */

49 if (FlashID == sFLASH_ID)

50 {

51 printf("\r\n檢測到SPI FLASH W25Q128 !\r\n");

52

53 /* 擦除將要寫入的 SPI FLASH 扇區,FLASH寫入前要先擦除 */

54 SPI_FLASH_SectorErase(FLASH_SectorToErase);

55

56 /* 將發送緩沖區的數據寫到flash */

57 SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress, BufferSize);

58 printf("\r\n寫入的數據為:\r\n%s", Tx_Buffer);

59

60 /* 將剛剛寫入的數據讀出來放到接收緩沖區中 */

61 SPI_FLASH_BufferRead(Rx_Buffer, FLASH_ReadAddress, BufferSize);

62 printf("\r\n讀出的數據為:\r\n%s", Rx_Buffer);

63

64 /* 檢查寫入的數據與讀出的數據是否相等 */

65 TransferStatus1 = Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize);

66

67 if ( PASSED == TransferStatus1 )

68 {

69 LED_GREEN;

70 printf("\r\n16M串行flash(W25Q128)測試成功!\n\r");

71 }

72 else

73 {

74 LED_RED;

75 printf("\r\n16M串行flash(W25Q128)測試失敗!\n\r");

76 }

77 }// if (FlashID == sFLASH_ID)

78 else

79 {

80 LED_RED;

81 printf("\r\n獲取不到 W25Q128 ID!\n\r");

82 }

83

84 SPI_Flash_PowerDown();

85 while (1);

86 }

函數中初始化了LED、串口、SPI外設,然后讀取FLASH芯片的ID進行校驗,若ID校驗通過則向FLASH的特定地址寫入測試數據,然后再從該地址讀取數據,測試讀寫是否正常。

注意:

由於實驗板上的FLASH芯片默認已經存儲了特定用途的數據,如擦除了這些數據會影響到某些程序的運行。所以我們預留了FLASH芯片的"第0扇區(0-4096地址)"專用於本實驗,如非必要,請勿擦除其它地址的內容。如已擦除,可在配套資料里找到"刷外部FLASH內容"程序,根據其說明給FLASH重新寫入出廠內容。

24.4.3 下載驗證

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

24.5 每課一問

1.    在SPI外設初始化部分,MISO引腳可以設置為輸入模式嗎?為什么?實際測試現象如何?

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

3.    如果扇區未經擦除就寫入,會有什么后果?請做實驗驗證。

4.    簡述FLASH存儲器與EEPROM存儲器的區別。

 


免責聲明!

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



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