若對SPI通訊協(xié)議不了解,可先閱讀《SPI總線(xiàn)協(xié)議介紹》文檔的內容學(xué)習。
關(guān)于FLASH存儲器,請參考"常用存儲器介紹"章節,實(shí)驗中FLASH芯片的具體參數,請參考其規格書(shū)《W25Q128》來(lái)了解。
SPI協(xié)議是由摩托羅拉公司提出的通訊協(xié)議(Serial Peripheral Interface),即串行外圍設備接口,是一種高速全雙工的通信總線(xiàn)。它被廣泛地使用在ADC、LCD等設備與MCU間,要求通訊速率較高的場(chǎng)合。
學(xué)習本章時(shí),可與I2C章節對比閱讀,體會(huì )兩種通訊總線(xiàn)的差異以及EEPROM存儲器與FLASH存儲器的區別。下面我們分別對SPI協(xié)議的物理層及協(xié)議層進(jìn)行講解。
SPI通訊設備之間的常用連接方式見(jiàn)圖 241。

圖 241 常見(jiàn)的SPI通訊系統
SPI通訊使用3條總線(xiàn)及片選線(xiàn),3條總線(xiàn)分別為SCK、MOSI、MISO,片選線(xiàn)為,它們的作用介紹如下:
(1) ( Slave Select):從設備選擇信號線(xiàn),常稱(chēng)為片選信號線(xiàn),也稱(chēng)為NSS、CS,以下用NSS表示。當有多個(gè)SPI從設備與SPI主機相連時(shí),設備的其它信號線(xiàn)SCK、MOSI及MISO同時(shí)并聯(lián)到相同的SPI總線(xiàn)上,即無(wú)論有多少個(gè)從設備,都共同只使用這3條總線(xiàn);而每個(gè)從設備都有獨立的這一條NSS信號線(xiàn),本信號線(xiàn)獨占主機的一個(gè)引腳,即有多少個(gè)從設備,就有多少條片選信號線(xiàn)。I2C協(xié)議中通過(guò)設備地址來(lái)尋址、選中總線(xiàn)上的某個(gè)設備并與其進(jìn)行通訊;而SPI協(xié)議中沒(méi)有設備地址,它使用NSS信號線(xiàn)來(lái)尋址,當主機要選擇從設備時(shí),把該從設備的NSS信號線(xiàn)設置為低電平,該從設備即被選中,即片選有效,接著(zhù)主機開(kāi)始與被選中的從設備進(jìn)行SPI通訊。所以SPI通訊以NSS線(xiàn)置低電平為開(kāi)始信號,以NSS線(xiàn)被拉高作為結束信號。
(2) SCK (Serial Clock):時(shí)鐘信號線(xiàn),用于通訊數據同步。它由通訊主機產(chǎn)生,決定了通訊的速率,不同的設備支持的最高時(shí)鐘頻率不一樣,如STM32的SPI時(shí)鐘頻率最大為fpclk/2,兩個(gè)設備之間通訊時(shí),通訊速率受限于低速設備。
(3) MOSI (Master Output, Slave Input):主設備輸出/從設備輸入引腳。主機的數據從這條信號線(xiàn)輸出,從機由這條信號線(xiàn)讀入主機發(fā)送的數據,即這條線(xiàn)上數據的方向為主機到從機。
(4) MISO(Master Input,,Slave Output):主設備輸入/從設備輸出引腳。主機從這條信號線(xiàn)讀入數據,從機的數據由這條信號線(xiàn)輸出到主機,即在這條線(xiàn)上數據的方向為從機到主機。
與I2C的類(lèi)似,SPI協(xié)議定義了通訊的起始和停止信號、數據有效性、時(shí)鐘同步等環(huán)節。
先看看SPI通訊的通訊時(shí)序,見(jiàn)圖 242。

圖 242 SPI通訊時(shí)序
這是一個(gè)主機的通訊時(shí)序。NSS、SCK、MOSI信號都由主機控制產(chǎn)生,而MISO的信號由從機產(chǎn)生,主機通過(guò)該信號線(xiàn)讀取從機的數據。MOSI與MISO的信號只在NSS為低電平的時(shí)候才有效,在SCK的每個(gè)時(shí)鐘周期MOSI和MISO傳輸一位數據。
以上通訊流程中包含的各個(gè)信號分解如下:
在圖 242中的標號?處,NSS信號線(xiàn)由高變低,是SPI通訊的起始信號。NSS是每個(gè)從機各自獨占的信號線(xiàn),當從機檢在自己的NSS線(xiàn)檢測到起始信號后,就知道自己被主機選中了,開(kāi)始準備與主機通訊。在圖中的標號?處,NSS信號由低變高,是SPI通訊的停止信號,表示本次通訊結束,從機的選中狀態(tài)被取消。
SPI使用MOSI及MISO信號線(xiàn)來(lái)傳輸數據,使用SCK信號線(xiàn)進(jìn)行數據同步。MOSI及MISO數據線(xiàn)在SCK的每個(gè)時(shí)鐘周期傳輸一位數據,且數據輸入輸出是同時(shí)進(jìn)行的。數據傳輸時(shí),MSB先行或LSB先行并沒(méi)有作硬性規定,但要保證兩個(gè)SPI通訊設備之間使用同樣的協(xié)定,一般都會(huì )采用圖 242中的MSB先行模式。
觀(guān)察圖中的????標號處,MOSI及MISO的數據在SCK的上升沿期間變化輸出,在SCK的下降沿時(shí)被采樣。即在SCK的下降沿時(shí)刻,MOSI及MISO的數據有效,高電平時(shí)表示數據"1",為低電平時(shí)表示數據"0"。在其它時(shí)刻,數據無(wú)效,MOSI及MISO為下一次表示數據做準備。
SPI每次數據傳輸可以8位或16位為單位,每次傳輸的單位數不受限制。
上面講述的圖 242中的時(shí)序只是SPI中的其中一種通訊模式,SPI一共有四種通訊模式,它們的主要區別是總線(xiàn)空閑時(shí)SCK的時(shí)鐘狀態(tài)以及數據采樣時(shí)刻。為方便說(shuō)明,在此引入"時(shí)鐘極性CPOL"和"時(shí)鐘相位CPHA"的概念。
時(shí)鐘極性CPOL是指SPI通訊設備處于空閑狀態(tài)時(shí),SCK信號線(xiàn)的電平信號(即SPI通訊開(kāi)始前、 NSS線(xiàn)為高電平時(shí)SCK的狀態(tài))。CPOL=0時(shí), SCK在空閑狀態(tài)時(shí)為低電平,CPOL=1時(shí),則相反。
時(shí)鐘相位CPHA是指數據的采樣的時(shí)刻,當CPHA=0時(shí),MOSI或MISO數據線(xiàn)上的信號將會(huì )在SCK時(shí)鐘線(xiàn)的"奇數邊沿"被采樣。當CPHA=1時(shí),數據線(xiàn)在SCK的"偶數邊沿"采樣。見(jiàn)圖 243及圖 244。

圖 243 CPHA=0時(shí)的SPI通訊模式
我們來(lái)分析這個(gè)CPHA=0的時(shí)序圖。首先,根據SCK在空閑狀態(tài)時(shí)的電平,分為兩種情況。SCK信號線(xiàn)在空閑狀態(tài)為低電平時(shí),CPOL=0;空閑狀態(tài)為高電平時(shí),CPOL=1。
無(wú)論CPOL=0還是=1,因為我們配置的時(shí)鐘相位CPHA=0,在圖中可以看到,采樣時(shí)刻都是在SCK的奇數邊沿。注意當CPOL=0的時(shí)候,時(shí)鐘的奇數邊沿是上升沿,而CPOL=1的時(shí)候,時(shí)鐘的奇數邊沿是下降沿。所以SPI的采樣時(shí)刻不是由上升/下降沿決定的。MOSI和MISO數據線(xiàn)的有效信號在SCK的奇數邊沿保持不變,數據信號將在SCK奇數邊沿時(shí)被采樣,在非采樣時(shí)刻,MOSI和MISO的有效信號才發(fā)生切換。
類(lèi)似地,當CPHA=1時(shí),不受CPOL的影響,數據信號在SCK的偶數邊沿被采樣,見(jiàn)圖 244。

圖 244 CPHA=1時(shí)的SPI通訊模式
由CPOL及CPHA的不同狀態(tài),SPI分成了四種模式,見(jiàn)表 241,主機與從機需要工作在相同的模式下才可以正常通訊,實(shí)際中采用較多的是"模式0"與"模式3"。
表 241 SPI的四種模式
SPI模式 | CPOL | CPHA | 空閑時(shí)SCK時(shí)鐘 | 采樣時(shí)刻 |
0 | 0 | 0 | 低電平 | 奇數邊沿 |
1 | 0 | 1 | 低電平 | 偶數邊沿 |
2 | 1 | 0 | 高電平 | 奇數邊沿 |
3 | 1 | 1 | 高電平 | 偶數邊沿 |
與I2C外設一樣,STM32芯片也集成了專(zhuān)門(mén)用于SPI協(xié)議通訊的外設。
STM32的SPI外設可用作通訊的主機及從機,支持最高的SCK時(shí)鐘頻率為fpclk/2 (STM32F429型號的芯片默認fpclk1為90MHz,fpclk2為45MHz),完全支持SPI協(xié)議的4種模式,數據幀長(cháng)度可設置為8位或16位,可設置數據MSB先行或LSB先行。它還支持雙線(xiàn)全雙工(前面小節說(shuō)明的都是這種模式)、雙線(xiàn)單向以及單線(xiàn)模式。其中雙線(xiàn)單向模式可以同時(shí)使用MOSI及MISO數據線(xiàn)向一個(gè)方向傳輸數據,可以加快一倍的傳輸速度。而單線(xiàn)模式則可以減少硬件接線(xiàn),當然這樣速率會(huì )受到影響。我們只講解雙線(xiàn)全雙工模式。
STM32的SPI外設還支持I2S功能,I2S功能是一種音頻串行通訊協(xié)議,在我們以后講解MP3播放器的章節中會(huì )進(jìn)行介紹。

圖 245 SPI架構圖
SPI的所有硬件架構都從圖 245中左側MOSI、MISO、SCK及NSS線(xiàn)展開(kāi)的。STM32芯片有多個(gè)SPI外設,它們的SPI通訊信號引出到不同的GPIO引腳上,使用時(shí)必須配置到這些指定的引腳,見(jiàn)表 242。關(guān)于GPIO引腳的復用功能,可查閱《STM32F4xx規格書(shū)》,以它為準。
表 242 STM32F4xx的SPI引腳(整理自《STM32F4xx規格書(shū)》)
引腳 | 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 |
其中SPI1、SPI4、SPI5、SPI6是APB2上的設備,最高通信速率達45Mbtis/s,SPI2、SPI3是APB1上的設備,最高通信速率為22.5Mbits/s。除了通訊速率,在其它功能上沒(méi)有差異。
SCK線(xiàn)的時(shí)鐘信號,由波特率發(fā)生器根據"控制寄存器CR1"中的BR[0:2]位控制,該位是對fpclk時(shí)鐘的分頻因子,對fpclk的分頻結果就是SCK引腳的輸出時(shí)鐘頻率,計算方法見(jiàn)表 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總線(xiàn)頻率,APB1為fpclk1,APB2為fpckl2。
通過(guò)配置"控制寄存器CR"的"CPOL位"及"CPHA"位可以把SPI設置成前面分析的4種SPI模式。
SPI的MOSI及MISO都連接到數據移位寄存器上,數據移位寄存器的內容來(lái)源于接收緩沖區及發(fā)送緩沖區以及MISO、MOSI線(xiàn)。當向外發(fā)送數據的時(shí)候,數據移位寄存器以"發(fā)送緩沖區"為數據源,把數據一位一位地通過(guò)數據線(xiàn)發(fā)送出去;當從外部接收數據的時(shí)候,數據移位寄存器把數據線(xiàn)采樣到的數據一位一位地存儲到"接收緩沖區"中。通過(guò)寫(xiě)SPI的"數據寄存器DR"把數據填充到發(fā)送緩沖區中,通過(guò)"數據寄存器DR",可以獲取接收緩沖區中的內容。其中數據幀長(cháng)度可以通過(guò)"控制寄存器CR1"的"DFF位"配置成8位及16位模式;配置"LSBFIRST位"可選擇MSB先行還是LSB先行。
整體控制邏輯負責協(xié)調整個(gè)SPI外設,控制邏輯的工作模式根據我們配置的"控制寄存器(CR1/CR2)"的參數而改變,基本的控制參數包括前面提到的SPI模式、波特率、LSB先行、主從模式、單雙向模式等等。在外設工作時(shí),控制邏輯會(huì )根據外設的工作狀態(tài)修改"狀態(tài)寄存器(SR)",我們只要讀取狀態(tài)寄存器相關(guān)的寄存器位,就可以了解SPI的工作狀態(tài)了。除此之外,控制邏輯還根據要求,負責控制產(chǎn)生SPI中斷信號、DMA請求及控制NSS信號線(xiàn)。
實(shí)際應用中,我們一般不使用STM32 SPI外設的標準NSS信號線(xiàn),而是更簡(jiǎn)單地使用普通的GPIO,軟件控制它的電平輸出,從而產(chǎn)生通訊起始和停止信號。
STM32使用SPI外設通訊時(shí),在通訊的不同階段它會(huì )對"狀態(tài)寄存器SR"的不同數據位寫(xiě)入參數,我們通過(guò)讀取這些寄存器標志來(lái)了解通訊狀態(tài)。
圖 246中的是"主模式"流程,即STM32作為SPI通訊的主機端時(shí)的數據收發(fā)過(guò)程。

圖 246 主發(fā)送器通訊過(guò)程
主模式收發(fā)流程及事件說(shuō)明如下:
(1) 控制NSS信號線(xiàn),產(chǎn)生起始信號(圖中沒(méi)有畫(huà)出);
(2) 把要發(fā)送的數據寫(xiě)入到"數據寄存器DR"中,該數據會(huì )被存儲到發(fā)送緩沖區;
(3) 通訊開(kāi)始,SCK時(shí)鐘開(kāi)始運行。MOSI把發(fā)送緩沖區中的數據一位一位地傳輸出去;MISO則把數據一位一位地存儲進(jìn)接收緩沖區中;
(4) 當發(fā)送完一幀數據的時(shí)候,"狀態(tài)寄存器SR"中的"TXE標志位"會(huì )被置1,表示傳輸完一幀,發(fā)送緩沖區已空;類(lèi)似地,當接收完一幀數據的時(shí)候,"RXNE標志位"會(huì )被置1,表示傳輸完一幀,接收緩沖區非空;
(5) 等待到"TXE標志位"為1時(shí),若還要繼續發(fā)送數據,則再次往"數據寄存器DR"寫(xiě)入數據即可;等待到"RXNE標志位"為1時(shí),通過(guò)讀取"數據寄存器DR"可以獲取接收緩沖區中的內容。
假如我們使能了TXE或RXNE中斷,TXE或RXNE置1時(shí)會(huì )產(chǎn)生SPI中斷信號,進(jìn)入同一個(gè)中斷服務(wù)函數,到SPI中斷服務(wù)程序后,可通過(guò)檢查寄存器位來(lái)了解是哪一個(gè)事件,再分別進(jìn)行處理。也可以使用DMA方式來(lái)收發(fā)"數據寄存器DR"中的數據。
跟其它外設一樣,STM32標準庫提供了SPI初始化結構體及初始化函數來(lái)配置SPI外設。初始化結構體及函數定義在庫文件"stm32f4xx_spi.h"及"stm32f4xx_spi.c"中,編程時(shí)我們可以結合這兩個(gè)文件內的注釋使用或參考庫幫助文檔。了解初始化結構體后我們就能對SPI外設運用自如了,見(jiàn)代碼清單 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的數據幀長(cháng)度,可選8/16位 */
6 uint16_t SPI_CPOL; /*設置時(shí)鐘極性CPOL,可選高/低電平*/
7 uint16_t SPI_CPHA; /*設置時(shí)鐘相位,可選奇/偶數邊沿采樣 */
8 uint16_t SPI_NSS; /*設置NSS引腳由SPI硬件控制還是軟件控制*/
9 uint16_t SPI_BaudRatePrescaler; /*設置時(shí)鐘分頻因子,fpclk/分頻數=fSCK */
10 uint16_t SPI_FirstBit; /*設置MSB/LSB先行 */
11 uint16_t SPI_CRCPolynomial; /*設置CRC校驗的表達式 */
12 } SPI_InitTypeDef;
這些結構體成員說(shuō)明如下,其中括號內的文字是對應參數在STM32標準庫中定義的宏:
(1) SPI_Direction
本成員設置SPI的通訊方向,可設置為雙線(xiàn)全雙工(SPI_Direction_2Lines_FullDuplex),雙線(xiàn)只接收(SPI_Direction_2Lines_RxOnly),單線(xiàn)只接收(SPI_Direction_1Line_Rx)、單線(xiàn)只發(fā)送模式(SPI_Direction_1Line_Tx)。
(2) SPI_Mode
本成員設置SPI工作在主機模式(SPI_Mode_Master)或從機模式(SPI_Mode_Slave ),這兩個(gè)模式的最大區別為SPI的SCK信號線(xiàn)的時(shí)序,SCK的時(shí)序是由通訊中的主機產(chǎn)生的。若被配置為從機模式,STM32的SPI外設將接受外來(lái)的SCK信號。
(3) SPI_DataSize
本成員可以選擇SPI通訊的數據幀大小是為8位(SPI_DataSize_8b)還是16位(SPI_DataSize_16b)。
(4) SPI_CPOL和SPI_CPHA
這兩個(gè)成員配置SPI的時(shí)鐘極性CPOL和時(shí)鐘相位CPHA,這兩個(gè)配置影響到SPI的通訊模式,關(guān)于CPOL和CPHA的說(shuō)明參考前面"通訊模式"小節。
時(shí)鐘極性CPOL成員,可設置為高電平(SPI_CPOL_High)或低電平(SPI_CPOL_Low )。
時(shí)鐘相位CPHA 則可以設置為SPI_CPHA_1Edge(在SCK的奇數邊沿采集數據) 或SPI_CPHA_2Edge (在SCK的偶數邊沿采集數據) 。
(5) SPI_NSS
本成員配置NSS引腳的使用模式,可以選擇為硬件模式(SPI_NSS_Hard )與軟件模式(SPI_NSS_Soft ),在硬件模式中的SPI片選信號由SPI硬件自動(dòng)產(chǎn)生,而軟件模式則需要我們親自把相應的GPIO端口拉高或置低產(chǎn)生非片選和片選信號。實(shí)際中軟件模式應用比較多。
(6) SPI_BaudRatePrescaler
本成員設置波特率分頻因子,分頻后的時(shí)鐘即為SPI的SCK信號線(xiàn)的時(shí)鐘頻率。這個(gè)成員參數可設置為fpclk的2、4、6、8、16、32、64、128、256分頻。
(7) SPI_FirstBit
所有串行的通訊協(xié)議都會(huì )有MSB先行(高位數據在前)還是LSB先行(低位數據在前)的問(wèn)題,而STM32的SPI模塊可以通過(guò)這個(gè)結構體成員,對這個(gè)特性編程控制。
(8) SPI_CRCPolynomial
這是SPI的CRC校驗中的多項式,若我們使用CRC校驗時(shí),就使用這個(gè)成員的參數(多項式),來(lái)計算CRC的值。
配置完這些結構體成員后,我們要調用SPI_Init函數把這些參數寫(xiě)入到寄存器中,實(shí)現SPI的初始化,然后調用SPI_Cmd來(lái)使能SPI外設。
FLSAH存儲器又稱(chēng)閃存,它與EEPROM都是掉電后數據不丟失的存儲器,但FLASH存儲器容量普遍大于EEPROM,現在基本取代了它的地位。我們生活中常用的U盤(pán)、SD卡、SSD固態(tài)硬盤(pán)以及我們STM32芯片內部用于存儲程序的設備,都是FLASH類(lèi)型的存儲器。在存儲控制上,最主要的區別是FLASH芯片只能一大片一大片地擦寫(xiě),而在"I2C章節"中我們了解到EEPROM可以單個(gè)字節擦寫(xiě)。
本小節以一種使用SPI通訊的串行FLASH存儲芯片的讀寫(xiě)實(shí)驗為大家講解STM32的SPI使用方法。實(shí)驗中STM32的SPI外設采用主模式,通過(guò)查詢(xún)事件的方式來(lái)確保正常通訊。

圖 247 SPI串行FLASH硬件連接圖
本實(shí)驗板中的FLASH芯片(型號:W25Q128)是一種使用SPI通訊協(xié)議的NOR FLASH存儲器,它的CS/CLK/DIO/DO引腳分別連接到了STM32對應的SDI引腳NSS/SCK/MOSI/MISO上,其中STM32的NSS引腳是一個(gè)普通的GPIO,不是SPI的專(zhuān)用NSS引腳,所以程序中我們要使用軟件控制的方式。
FLASH芯片中還有WP和HOLD引腳。WP引腳可控制寫(xiě)保護功能,當該引腳為低電平時(shí),禁止寫(xiě)入數據。我們直接接電源,不使用寫(xiě)保護功能。HOLD引腳可用于暫停通訊,該引腳為低電平時(shí),通訊暫停,數據輸出引腳輸出高阻抗狀態(tài),時(shí)鐘和數據輸入引腳無(wú)效。我們直接接電源,不使用通訊暫停功能。
關(guān)于FLASH芯片的更多信息,可參考其數據手冊《W25Q128》來(lái)了解。若您使用的實(shí)驗板FLASH的型號或控制引腳不一樣,只需根據我們的工程修改即可,程序的控制原理相同。
為了使工程更加有條理,我們把讀寫(xiě)FLASH相關(guān)的代碼獨立分開(kāi)存儲,方便以后移植。在"工程模板"之上新建"bsp_spi_flash.c"及"bsp_spi_ flash.h"文件,這些文件也可根據您的喜好命名,它們不屬于STM32標準庫的內容,是由我們自己根據應用需要編寫(xiě)的。
(7) 初始化通訊使用的目標引腳及端口時(shí)鐘;
(8) 使能SPI外設的時(shí)鐘;
(9) 配置SPI外設的模式、地址、速率等參數并使能SPI外設;
(10) 編寫(xiě)基本SPI按字節收發(fā)的函數;
(11) 編寫(xiě)對FLASH擦除及讀寫(xiě)操作的的函數;
(12) 編寫(xiě)測試程序,對讀寫(xiě)數據進(jìn)行校驗。
我們把SPI硬件相關(guān)的配置都以宏的形式定義到"bsp_spi_ flash.h"文件中,見(jiàn)代碼清單 242。
代碼清單 242 SPI硬件配置相關(guān)的宏
1 //SPI號及時(shí)鐘初始化函數
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號、引腳號、引腳源以及復用功能映射都以宏封裝起來(lái),并且定義了控制CS(NSS)引腳輸出電平的宏,以便配置產(chǎn)生起始和停止信號時(shí)使用。
利用上面的宏,編寫(xiě)SPI的初始化函數,見(jiàn)代碼清單 243。
代碼清單 243 SPI的初始化函數(GPIO初始化部分)
1 2 /**
3 * @brief SPI_FLASH初始化
4 * @param 無(wú)
5 * @retval 無(wú)
6 */
7 void SPI_FLASH_Init(void)
8 {
9 GPIO_InitTypeDef GPIO_InitStructure;
10
11 /* 使能 FLASH_SPI 及 GPIO 時(shí)鐘 */
12 /*!< SPI_FLASH_SPI_CS_GPIO, SPI_FLASH_SPI_MOSI_GPIO,
13 SPI_FLASH_SPI_MISO_GPIO和 SPI_FLASH_SPI_SCK_GPIO 時(shí)鐘使能 */
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 時(shí)鐘使能 */
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來(lái)使能SPI引腳使用的GPIO端口時(shí)鐘,調用時(shí)使用"|"操作同時(shí)配置多個(gè)引腳。調用宏FLASH_SPI_CLK_INIT使能SPI外設時(shí)鐘(該宏封裝了APB時(shí)鐘使能的庫函數)。
(3) 向GPIO初始化結構體賦值,把SCK/MOSI/MISO引腳初始化成復用推挽模式。而CS(NSS)引腳由于使用軟件控制,我們把它配置為普通的推挽輸出模式。
(4) 使用以上初始化結構體的配置,調用GPIO_Init函數向寄存器寫(xiě)入參數,完成GPIO的初始化。
以上只是配置了SPI使用的引腳,對SPI外設模式的配置。在配置STM32的SPI模式前,我們要先了解從機端的SPI模式。本例子中可通過(guò)查閱FLASH數據手冊《W25Q128》獲取。根據FLASH芯片的說(shuō)明,它支持SPI模式0及模式3,支持雙線(xiàn)全雙工,使用MSB先行模式,支持最高通訊時(shí)鐘為104MHz,數據幀長(cháng)度為8位。我們要把STM32的SPI外設中的這些參數配置一致。見(jiàn)代碼清單 244。
代碼清單 244 配置SPI模式
1 /**
2 * @brief SPI_FLASH引腳初始化
3 * @param 無(wú)
4 * @retval 無(wú)
5 */
6 void SPI_FLASH_Init(void)
7 {
8 /*為方便講解,省略了SPI的GPIO初始化部分*/
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 }
這段代碼中,把STM32的SPI外設配置為主機端,雙線(xiàn)全雙工模式,數據幀長(cháng)度為8位,使用SPI模式3(CPOL=1,CPHA=1),NSS引腳由軟件控制以及MSB先行模式。最后一個(gè)成員為CRC計算式,由于我們與FLASH芯片通訊不需要CRC校驗,并沒(méi)有使能SPI的CRC功能,這時(shí)CRC計算式的成員值是無(wú)效的。
賦值結束后調用庫函數SPI_Init把這些配置寫(xiě)入寄存器,并調用SPI_Cmd函數使能外設。
初始化好SPI外設后,就可以使用SPI通訊了,復雜的數據通訊都是由單個(gè)字節數據收發(fā)組成的,我們看看它的代碼實(shí)現,見(jiàn)代碼清單 245。
代碼清單 245 使用SPI發(fā)送和接收一個(gè)字節的數據
1 #define Dummy_Byte 0xFF
2 /**
3 * @brief 使用SPI發(fā)送一個(gè)字節的數據
4 * @param byte:要發(fā)送的數據
5 * @retval 返回接收到的數據
6 */
7 u8 SPI_FLASH_SendByte(u8 byte)
8 {
9 SPITimeout = SPIT_FLAG_TIMEOUT;
10
11 /* 等待發(fā)送緩沖區為空,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 /* 寫(xiě)入數據寄存器,把要寫(xiě)入的數據寫(xiě)入發(fā)送緩沖區 */
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讀取一個(gè)字節的數據
34 * @param 無(wú)
35 * @retval 返回接收到的數據
36 */
37 u8 SPI_FLASH_ReadByte(void)
38 {
39 return (SPI_FLASH_SendByte(Dummy_Byte));
40 }
SPI_FLASH_SendByte發(fā)送單字節函數中包含了等待事件的超時(shí)處理,這部分原理跟I2C中的一樣,在此不再贅述。
SPI_FLASH_SendByte函數實(shí)現了前面講解的"SPI通訊過(guò)程":
(1) 本函數中不包含SPI起始和停止信號,只是收發(fā)的主要過(guò)程,所以在調用本函數前后要做好起始和停止信號的操作;
(2) 對SPITimeout變量賦值為宏SPIT_FLAG_TIMEOUT。這個(gè)SPITimeout變量在下面的while循環(huán)中每次循環(huán)減1,該循環(huán)通過(guò)調用庫函數SPI_I2S_GetFlagStatus檢測事件,若檢測到事件,則進(jìn)入通訊的下一階段,若未檢測到事件則停留在此處一直檢測,當檢測SPIT_FLAG_TIMEOUT次都還沒(méi)等待到事件則認為通訊失敗,調用的SPI_TIMEOUT_UserCallback輸出調試信息,并退出通訊;
(3) 通過(guò)檢測TXE標志,獲取發(fā)送緩沖區的狀態(tài),若發(fā)送緩沖區為空,則表示可能存在的上一個(gè)數據已經(jīng)發(fā)送完畢;
(4) 等待至發(fā)送緩沖區為空后,調用庫函數SPI_I2S_SendData把要發(fā)送的數據"byte"寫(xiě)入到SPI的數據寄存器DR,寫(xiě)入SPI數據寄存器的數據會(huì )存儲到發(fā)送緩沖區,由SPI外設發(fā)送出去;
(5) 寫(xiě)入完畢后等待RXNE事件,即接收緩沖區非空事件。由于SPI雙線(xiàn)全雙工模式下MOSI與MISO數據傳輸是同步的(請對比"SPI通訊過(guò)程"閱讀),當接收緩沖區非空時(shí),表示上面的數據發(fā)送完畢,且接收緩沖區也收到新的數據;
(6) 等待至接收緩沖區非空時(shí),通過(guò)調用庫函數SPI_I2S_ReceiveData讀取SPI的數據寄存器DR,就可以獲取接收緩沖區中的新數據了。代碼中使用關(guān)鍵字"return"把接收到的這個(gè)數據作為SPI_FLASH_SendByte函數的返回值,所以我們可以看到在下面定義的SPI接收數據函數SPI_FLASH_ReadByte,它只是簡(jiǎn)單地調用了SPI_FLASH_SendByte函數發(fā)送數據"Dummy_Byte",然后獲取其返回值(因為不關(guān)注發(fā)送的數據,所以此時(shí)的輸入參數"Dummy_Byte"可以為任意值)??梢赃@樣做的原因是SPI的接收過(guò)程和發(fā)送過(guò)程實(shí)質(zhì)是一樣的,收發(fā)同步進(jìn)行,關(guān)鍵在于我們的上層應用中,關(guān)注的是發(fā)送還是接收的數據。
搞定SPI的基本收發(fā)單元后,還需要了解如何對FLASH芯片進(jìn)行讀寫(xiě)。FLASH芯片自定義了很多指令,我們通過(guò)控制STM32利用SPI總線(xiàn)向FLASH芯片發(fā)送指令,FLASH芯片收到后就會(huì )執行相應的操作。
而這些指令,對主機端(STM32)來(lái)說(shuō),只是它遵守最基本的SPI通訊協(xié)議發(fā)送出的數據,但在設備端(FLASH芯片)把這些數據解釋成不同的意義,所以才成為指令。查看FLASH芯片的數據手冊《W25Q128》,可了解各種它定義的各種指令的功能及指令格式,見(jiàn)表 244。
表 244 FLASH常用芯片指令表(摘自規格書(shū)《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) 生產(chǎn)廠(chǎng)商 | (ID15-ID8) 存儲器類(lèi)型 | (ID7-ID0) 容量 |
|
|
|
該表中的第一列為指令名,第二列為指令編碼,第三至第N列的具體內容根據指令的不同而有不同的含義。其中帶括號的字節參數,方向為FLASH向主機傳輸,即命令響應,不帶括號的則為主機向FLASH傳輸。表中"A0~A23"指FLASH芯片內部存儲器組織的地址;"M0~M7"為廠(chǎng)商號(MANUFACTURER ID);"ID0-ID15"為FLASH芯片的ID;"dummy"指該處可為任意數據;"D0~D7"為FLASH內部存儲矩陣的內容。
在FLSAH芯片內部,存儲有固定的廠(chǎng)商編號(M7-M0)和不同類(lèi)型FLASH芯片獨有的編號(ID15-ID0),見(jiàn)表 245。
表 245 FLASH數據手冊的設備ID說(shuō)明
FLASH型號 | 廠(chǎng)商號(M7-M0) | FLASH型號(ID15-ID0) |
W25Q64 | EF h | 4017 h |
W25Q128 | EF h | 4018 h |
通過(guò)指令表中的讀ID指令"JEDEC ID"可以獲取這兩個(gè)編號,該指令編碼為"9F h",其中"9F h"是指16進(jìn)制數"9F" (相當于C語(yǔ)言中的0x9F)。緊跟指令編碼的三個(gè)字節分別為FLASH芯片輸出的"(M7-M0)"、"(ID15-ID8)"及"(ID7-ID0)"。
此處我們以該指令為例,配合其指令時(shí)序圖進(jìn)行講解,見(jiàn)圖 248。

圖 248 FLASH讀ID指令"JEDEC ID"的時(shí)序(摘自規格書(shū)《W25Q128》)
主機首先通過(guò)MOSI線(xiàn)向FLASH芯片發(fā)送第一個(gè)字節數據為"9F h",當FLASH芯片收到該數據后,它會(huì )解讀成主機向它發(fā)送了"JEDEC指令",然后它就作出該命令的響應:通過(guò)MISO線(xiàn)把它的廠(chǎng)商ID(M7-M0)及芯片類(lèi)型(ID15-0)發(fā)送給主機,主機接收到指令響應后可進(jìn)行校驗。常見(jiàn)的應用是主機端通過(guò)讀取設備ID來(lái)測試硬件是否連接正常,或用于識別設備。
對于FLASH芯片的其它指令,都是類(lèi)似的,只是有的指令包含多個(gè)字節,或者響應包含更多的數據。
實(shí)際上,編寫(xiě)設備驅動(dòng)都是有一定的規律可循的。首先我們要確定設備使用的是什么通訊協(xié)議。如上一章的EEPROM使用的是I2C,本章的FLASH使用的是SPI。那么我們就先根據它的通訊協(xié)議,選擇好STM32的硬件模塊,并進(jìn)行相應的I2C或SPI模塊初始化。接著(zhù),我們要了解目標設備的相關(guān)指令,因為不同的設備,都會(huì )有相應的不同的指令。如EEPROM中會(huì )把第一個(gè)數據解釋為內部存儲矩陣的地址(實(shí)質(zhì)就是指令)。而FLASH則定義了更多的指令,有寫(xiě)指令,讀指令,讀ID指令等等。最后,我們根據這些指令的格式要求,使用通訊協(xié)議向設備發(fā)送指令,達到控制設備的目標。
為了方便使用,我們把FLASH芯片的常用指令編碼使用宏來(lái)封裝起來(lái),后面需要發(fā)送指令編碼的時(shí)候我們直接使用這些宏即可,見(jiàn)代碼清單 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
根據"JEDEC"指令的時(shí)序,我們把讀取FLASH ID的過(guò)程編寫(xiě)成一個(gè)函數,見(jiàn)代碼清單 247。
代碼清單 247 讀取FLASH芯片ID
1 /**
2 * @brief 讀取FLASH ID
3 * @param 無(wú)
4 * @retval FLASH ID
5 */
6 u32 SPI_FLASH_ReadID(void)
7 {
8 u32 Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;
9
10 /* 開(kāi)始通訊:CS低電平 */
11 SPI_FLASH_CS_LOW();
12
13 /* 發(fā)送JEDEC指令,讀取ID */
14 SPI_FLASH_SendByte(W25X_JedecDeviceID);
15
16 /* 讀取一個(gè)字節數據 */
17 Temp0 = SPI_FLASH_SendByte(Dummy_Byte);
18
19 /* 讀取一個(gè)字節數據 */
20 Temp1 = SPI_FLASH_SendByte(Dummy_Byte);
21
22 /* 讀取一個(gè)字節數據 */
23 Temp2 = SPI_FLASH_SendByte(Dummy_Byte);
24
25 /* 停止通訊:CS高電平 */
26 SPI_FLASH_CS_HIGH();
27
28 /*把數據組合起來(lái),作為函數的返回值*/
29 Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;
30
31 return Temp;
32 }
這段代碼利用控制CS引腳電平的宏"SPI_FLASH_CS_LOW/HIGH"以及前面編寫(xiě)的單字節收發(fā)函數SPI_FLASH_SendByte,很清晰地實(shí)現了"JEDEC ID"指令的時(shí)序:發(fā)送一個(gè)字節的指令編碼"W25X_JedecDeviceID",然后讀取3個(gè)字節,獲取FLASH芯片對該指令的響應,最后把讀取到的這3個(gè)數據合并到一個(gè)變量Temp中,然后作為函數返回值,把該返回值與我們定義的宏"sFLASH_ID"對比,即可知道FLASH芯片是否正常。
在向FLASH芯片存儲矩陣寫(xiě)入數據前,首先要使能寫(xiě)操作,通過(guò)"Write Enable"命令即可寫(xiě)使能,見(jiàn)代碼清單 248。
代碼清單 248 寫(xiě)使能命令
1 /**
2 * @brief 向FLASH發(fā)送寫(xiě)使能命令
3 * @param none
4 * @retval none
5 */
6 void SPI_FLASH_WriteEnable(void)
7 {
8 /* 通訊開(kāi)始:CS低 */
9 SPI_FLASH_CS_LOW();
10
11 /* 發(fā)送寫(xiě)使能命令*/
12 SPI_FLASH_SendByte(W25X_WriteEnable);
13
14 /*通訊結束:CS高 */
15 SPI_FLASH_CS_HIGH();
16 }
與EEPROM一樣,由于FLASH芯片向內部存儲矩陣寫(xiě)入數據需要消耗一定的時(shí)間,并不是在總線(xiàn)通訊結束的一瞬間完成的,所以在寫(xiě)操作后需要確認FLASH芯片"空閑"時(shí)才能進(jìn)行再次寫(xiě)入。為了表示自己的工作狀態(tài),FLASH芯片定義了一個(gè)狀態(tài)寄存器,見(jiàn)圖 249。

圖 249 FLASH芯片的狀態(tài)寄存器
我們只關(guān)注這個(gè)狀態(tài)寄存器的第0位"BUSY",當這個(gè)位為"1"時(shí),表明FLASH芯片處于忙碌狀態(tài),它可能正在對內部的存儲矩陣進(jìn)行"擦除"或"數據寫(xiě)入"的操作。
利用指令表中的"Read Status Register"指令可以獲取FLASH芯片狀態(tài)寄存器的內容,其時(shí)序見(jiàn)圖 2410。

圖 2410 讀取狀態(tài)寄存器的時(shí)序
只要向FLASH芯片發(fā)送了讀狀態(tài)寄存器的指令,FLASH芯片就會(huì )持續向主機返回最新的狀態(tài)寄存器內容,直到收到SPI通訊的停止信號。據此我們編寫(xiě)了具有等待FLASH芯片寫(xiě)入結束功能的函數,見(jiàn)代碼清單 249。
代碼清單 249 通過(guò)讀狀態(tài)寄存器等待FLASH芯片空閑
1 /*WIP(BUSY)標志:FLASH內部正在寫(xiě)入*/
2 #define WIP_Flag 0x01
3
4 /**
5 * @brief 等待WIP(BUSY)標志被置0,即等待到FLASH內部數據寫(xiě)入完畢
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 /* 發(fā)送讀狀態(tài)寄存器命令 */
16 SPI_FLASH_SendByte(W25X_ReadStatusReg);
17
18 SPITimeout = SPIT_FLAG_TIMEOUT;
19 /* 若FLASH忙碌,則等待 */
20 do
21 {
22 /* 讀取FLASH芯片的狀態(tài)寄存器 */
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); /* 正在寫(xiě)入標志 */
31
32 /* 停止信號 FLASH: CS 高 */
33 SPI_FLASH_CS_HIGH();
34 }
這段代碼發(fā)送讀狀態(tài)寄存器的指令編碼"W25X_ReadStatusReg"后,在while循環(huán)里持續獲取寄存器的內容并檢驗它的"WIP_Flag標志"(即BUSY位),一直等待到該標志表示寫(xiě)入結束時(shí)才退出本函數,以便繼續后面與FLASH芯片的數據通訊。
由于FLASH存儲器的特性決定了它只能把原來(lái)為"1"的數據位改寫(xiě)成"0",而原來(lái)為"0"的數據位不能直接改寫(xiě)為"1"。所以這里涉及到數據"擦除"的概念,在寫(xiě)入前,必須要對目標存儲矩陣進(jìn)行擦除操作,把矩陣中的數據位擦除為"1",在數據寫(xiě)入的時(shí)候,如果要存儲數據"1",那就不修改存儲矩陣,在要存儲數據"0"時(shí),才更改該位。
通常,對存儲矩陣擦除的基本操作單位都是多個(gè)字節進(jìn)行,如本例子中的FLASH芯片支持"扇區擦除"、"塊擦除"以及"整片擦除",見(jiàn)表 246。
表 246 本實(shí)驗FLASH芯片的擦除單位
擦除單位 | 大小 |
扇區擦除Sector Erase | 4KB |
塊擦除Block Erase | 64KB |
整片擦除Chip Erase | 整個(gè)芯片完全擦除 |
FLASH芯片的最小擦除單位為扇區(Sector),而一個(gè)塊(Block)包含16個(gè)扇區,其內部存儲矩陣分布見(jiàn)圖 2411。。

圖 2411 FLASH芯片的存儲矩陣
使用扇區擦除指令"Sector Erase"可控制FLASH芯片開(kāi)始擦寫(xiě),其指令時(shí)序見(jiàn)圖 2414。

圖 2412 扇區擦除時(shí)序
扇區擦除指令的第一個(gè)字節為指令編碼,緊接著(zhù)發(fā)送的3個(gè)字節用于表示要擦除的存儲矩陣地址。要注意的是在扇區擦除指令前,還需要先發(fā)送"寫(xiě)使能"指令,發(fā)送扇區擦除指令后,通過(guò)讀取寄存器狀態(tài)等待扇區擦除操作完畢,代碼實(shí)現見(jiàn)代碼清單 2410。
代碼清單 2410 擦除扇區
1 /**
2 * @brief 擦除FLASH扇區
3 * @param SectorAddr:要擦除的扇區地址
4 * @retval 無(wú)
5 */
6 void SPI_FLASH_SectorErase(u32 SectorAddr)
7 {
8 /* 發(fā)送FLASH寫(xiě)使能命令 */
9 SPI_FLASH_WriteEnable();
10 SPI_FLASH_WaitForWriteEnd();
11 /* 擦除扇區 */
12 /* 選擇FLASH: CS低電平 */
13 SPI_FLASH_CS_LOW();
14 /* 發(fā)送扇區擦除指令*/
15 SPI_FLASH_SendByte(W25X_SectorErase);
16 /*發(fā)送擦除扇區地址的高位*/
17 SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);
18 /* 發(fā)送擦除扇區地址的中位 */
19 SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);
20 /* 發(fā)送擦除扇區地址的低位 */
21 SPI_FLASH_SendByte(SectorAddr & 0xFF);
22 /* 停止信號 FLASH: CS 高電平 */
23 SPI_FLASH_CS_HIGH();
24 /* 等待擦除完畢*/
25 SPI_FLASH_WaitForWriteEnd();
26 }
這段代碼調用的函數在前面都已講解,只要注意發(fā)送擦除地址時(shí)高位在前即可。調用扇區擦除指令時(shí)注意輸入的地址要對齊到4KB。
目標扇區被擦除完畢后,就可以向它寫(xiě)入數據了。與EEPROM類(lèi)似,FLASH芯片也有頁(yè)寫(xiě)入命令,使用頁(yè)寫(xiě)入命令最多可以一次向FLASH傳輸256個(gè)字節的數據,我們把這個(gè)單位為頁(yè)大小。FLASH頁(yè)寫(xiě)入的時(shí)序見(jiàn)圖 2413。

圖 2413 FLASH芯片頁(yè)寫(xiě)入
從時(shí)序圖可知,第1個(gè)字節為"頁(yè)寫(xiě)入指令"編碼,2-4字節為要寫(xiě)入的"地址A",接著(zhù)的是要寫(xiě)入的內容,最多個(gè)可以發(fā)送256字節數據,這些數據將會(huì )從"地址A"開(kāi)始,按順序寫(xiě)入到FLASH的存儲矩陣。若發(fā)送的數據超出256個(gè),則會(huì )覆蓋前面發(fā)送的數據。
與擦除指令不一樣,頁(yè)寫(xiě)入指令的地址并不要求按256字節對齊,只要確認目標存儲單元是擦除狀態(tài)即可(即被擦除后沒(méi)有被寫(xiě)入過(guò))。所以,若對"地址x"執行頁(yè)寫(xiě)入指令后,發(fā)送了200個(gè)字節數據后終止通訊,下一次再執行頁(yè)寫(xiě)入指令,從"地址(x+200)"開(kāi)始寫(xiě)入200個(gè)字節也是沒(méi)有問(wèn)題的(小于256均可)。只是在實(shí)際應用中由于基本擦除單元是4KB,一般都以扇區為單位進(jìn)行讀寫(xiě),想深入了解,可學(xué)習我們的"FLASH文件系統"相關(guān)的例子。
把頁(yè)寫(xiě)入時(shí)序封裝成函數,其實(shí)現見(jiàn)代碼清單 2411。
代碼清單 2411 FLASH的頁(yè)寫(xiě)入
1 /**
2 * @brief 對FLASH按頁(yè)寫(xiě)入數據,調用本函數寫(xiě)入數據前需要先擦除扇區
3 * @param pBuffer,要寫(xiě)入數據的指針
4 * @param WriteAddr,寫(xiě)入地址
5 * @param NumByteToWrite,寫(xiě)入數據長(cháng)度,必須小于等于頁(yè)大小
6 * @retval 無(wú)
7 */
8 void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
9 {
10 /* 發(fā)送FLASH寫(xiě)使能命令 */
11 SPI_FLASH_WriteEnable();
12
13 /* 選擇FLASH: CS低電平 */
14 SPI_FLASH_CS_LOW();
15 /* 寫(xiě)送寫(xiě)指令*/
16 SPI_FLASH_SendByte(W25X_PageProgram);
17 /*發(fā)送寫(xiě)地址的高位*/
18 SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
19 /*發(fā)送寫(xiě)地址的中位*/
20 SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
21 /*發(fā)送寫(xiě)地址的低位*/
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 /* 寫(xiě)入數據*/
31 while (NumByteToWrite--)
32 {
33 /* 發(fā)送當前要寫(xiě)入的字節數據 */
34 SPI_FLASH_SendByte(*pBuffer);
35 /* 指向下一字節數據 */
36 pBuffer++;
37 }
38
39 /* 停止信號 FLASH: CS 高電平 */
40 SPI_FLASH_CS_HIGH();
41
42 /* 等待寫(xiě)入完畢*/
43 SPI_FLASH_WaitForWriteEnd();
44 }
這段代碼的內容為:先發(fā)送"寫(xiě)使能"命令,接著(zhù)才開(kāi)始頁(yè)寫(xiě)入時(shí)序,然后發(fā)送指令編碼、地址,再把要寫(xiě)入的數據一個(gè)接一個(gè)地發(fā)送出去,發(fā)送完后結束通訊,檢查FLASH狀態(tài)寄存器,等待FLASH內部寫(xiě)入結束。
應用的時(shí)候我們常常要寫(xiě)入不定量的數據,直接調用"頁(yè)寫(xiě)入"函數并不是特別方便,所以我們在它的基礎上編寫(xiě)了"不定量數據寫(xiě)入"的函數,基實(shí)現見(jiàn)代碼清單 2412。
代碼清單 2412不定量數據寫(xiě)入
1 /**
2 * @brief 對FLASH寫(xiě)入數據,調用本函數寫(xiě)入數據前需要先擦除扇區
3 * @param pBuffer,要寫(xiě)入數據的指針
4 * @param WriteAddr,寫(xiě)入地址
5 * @param NumByteToWrite,寫(xiě)入數據長(cháng)度
6 * @retval 無(wú)
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運算求余,若writeAddr是SPI_FLASH_PageSize整數倍,運算結果Addr值為0*/
13 Addr = WriteAddr % SPI_FLASH_PageSize;
14
15 /*差count個(gè)數據值,剛好可以對齊到頁(yè)地址*/
16 count = SPI_FLASH_PageSize - Addr;
17 /*計算出要寫(xiě)多少整數頁(yè)*/
18 NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
19 /*mod運算求余,計算出剩余不滿(mǎn)一頁(yè)的字節數*/
20 NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
21
22 /* Addr=0,則WriteAddr 剛好按頁(yè)對齊 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 /*先把整數頁(yè)都寫(xiě)了*/
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 /*若有多余的不滿(mǎn)一頁(yè)的數據,把它寫(xiě)完*/
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 /*當前頁(yè)剩余的count個(gè)位置比NumOfSingle小,寫(xiě)不完*/
51 if (NumOfSingle > count)
52 {
53 temp = NumOfSingle - count;
54
55 /*先寫(xiě)滿(mǎn)當前頁(yè)*/
56 SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
57 WriteAddr += count;
58 pBuffer += count;
59
60 /*再寫(xiě)剩余的數據*/
61 SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);
62 }
63 else /*當前頁(yè)剩余的count個(gè)位置能寫(xiě)完NumOfSingle個(gè)數據*/
64 {
65 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
66 }
67 }
68 else /* NumByteToWrite > SPI_FLASH_PageSize */
69 {
70 /*地址不對齊多出的count分開(kāi)處理,不加入這個(gè)運算*/
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 /*把整數頁(yè)都寫(xiě)了*/
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 /*若有多余的不滿(mǎn)一頁(yè)的數據,把它寫(xiě)完*/
87 if (NumOfSingle != 0)
88 {
89 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
90 }
91 }
92 }
93 }
這段代碼與EEPROM章節中的"快速寫(xiě)入多字節"函數原理是一樣的,運算過(guò)程在此不再贅述。區別是頁(yè)的大小以及實(shí)際數據寫(xiě)入的時(shí)候,使用的是針對FLASH芯片的頁(yè)寫(xiě)入函數,且在實(shí)際調用這個(gè)"不定量數據寫(xiě)入"函數時(shí),還要注意確保目標扇區處于擦除狀態(tài)。
相對于寫(xiě)入,FLASH芯片的數據讀取要簡(jiǎn)單得多,使用讀取指令"Read Data"即可,其指令時(shí)序見(jiàn)圖 2414。

圖 2414 SPI FLASH讀取數據時(shí)序
發(fā)送了指令編碼及要讀的起始地址后,FLASH芯片就會(huì )按地址遞增的方式返回存儲矩陣的內容,讀取的數據量沒(méi)有限制,只要沒(méi)有停止通訊,FLASH芯片就會(huì )一直返回數據。代碼實(shí)現見(jiàn)代碼清單 2413。
代碼清單 2413 從FLASH讀取數據
1 /**
2 * @brief 讀取FLASH數據
3 * @param pBuffer,存儲讀出數據的指針
4 * @param ReadAddr,讀取地址
5 * @param NumByteToRead,讀取數據長(cháng)度
6 * @retval 無(wú)
7 */
8 void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)
9 {
10 /* 選擇FLASH: CS低電平 */
11 SPI_FLASH_CS_LOW();
12
13 /* 發(fā)送讀指令 */
14 SPI_FLASH_SendByte(W25X_ReadData);
15
16 /* 發(fā)送讀地址高位 */
17 SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
18 /* 發(fā)送讀地址中位 */
19 SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);
20 /* 發(fā)送讀地址低位 */
21 SPI_FLASH_SendByte(ReadAddr & 0xFF);
22
23 /* 讀取數據 */
24 while (NumByteToRead--)
25 {
26 /* 讀取一個(gè)字節*/
27 *pBuffer = SPI_FLASH_SendByte(Dummy_Byte);
28 /* 指向下一個(gè)字節緩沖區 */
29 pBuffer++;
30 }
31
32 /* 停止信號 FLASH: CS 高電平 */
33 SPI_FLASH_CS_HIGH();
34 }
由于讀取的數據量沒(méi)有限制,所以發(fā)送讀命令后一直接收NumByteToRead個(gè)數據到結束即可。
最后我們來(lái)編寫(xiě)main函數,進(jìn)行FLASH芯片讀寫(xiě)校驗,見(jiàn)代碼清單 2414。
代碼清單 2414 main函數
1 /* 獲取緩沖區的長(cháng)度 */
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 /* 發(fā)送緩沖區初始化 */
13 uint8_t Tx_Buffer[] = "感謝您選用秉火stm32開(kāi)發(fā)板\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 * 輸入:無(wú)
28 * 輸出:無(wú)
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這是一個(gè)16M串行flash(W25Q128)實(shí)驗 \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 /* 擦除將要寫(xiě)入的 SPI FLASH 扇區,FLASH寫(xiě)入前要先擦除 */
54 SPI_FLASH_SectorErase(FLASH_SectorToErase);
55
56 /* 將發(fā)送緩沖區的數據寫(xiě)到flash中 */
57 SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress, BufferSize);
58 printf("\r\n寫(xiě)入的數據為:\r\n%s", Tx_Buffer);
59
60 /* 將剛剛寫(xiě)入的數據讀出來(lái)放到接收緩沖區中 */
61 SPI_FLASH_BufferRead(Rx_Buffer, FLASH_ReadAddress, BufferSize);
62 printf("\r\n讀出的數據為:\r\n%s", Rx_Buffer);
63
64 /* 檢查寫(xiě)入的數據與讀出的數據是否相等 */
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進(jìn)行校驗,若ID校驗通過(guò)則向FLASH的特定地址寫(xiě)入測試數據,然后再從該地址讀取數據,測試讀寫(xiě)是否正常。
注意:
由于實(shí)驗板上的FLASH芯片默認已經(jīng)存儲了特定用途的數據,如擦除了這些數據會(huì )影響到某些程序的運行。所以我們預留了FLASH芯片的"第0扇區(0-4096地址)"專(zhuān)用于本實(shí)驗,如非必要,請勿擦除其它地址的內容。如已擦除,可在配套資料里找到"刷外部FLASH內容"程序,根據其說(shuō)明給FLASH重新寫(xiě)入出廠(chǎng)內容。
用USB線(xiàn)連接開(kāi)發(fā)板"USB TO UART"接口跟電腦,在電腦端打開(kāi)串口調試助手,把編譯好的程序下載到開(kāi)發(fā)板。在串口調試助手可看到FLASH測試的調試信息。
1. 在SPI外設初始化部分,MISO引腳可以設置為輸入模式嗎?為什么?實(shí)際測試現象如何?
2. 嘗試使用FLASH芯片存儲int整型變量,float型浮點(diǎn)變量,編寫(xiě)程序寫(xiě)入數據,并讀出校驗。
3. 如果扇區未經(jīng)擦除就寫(xiě)入,會(huì )有什么后果?請做實(shí)驗驗證。
4. 簡(jiǎn)述FLASH存儲器與EEPROM存儲器的區別。
聯(lián)系客服