第24章 SPI—讀寫串行FLASH
全套200集視頻教程和1000頁PDF教程請到秉火論壇下載: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),即串行外圍設備接口,是一種高速全雙工的通信總線。它被廣泛地使用在ADC、LCD等設備與MCU間,要求通訊速率較高的場合。
學習本章時,可與I2C章節對比閱讀,體會兩種通訊總線的差異以及EEPROM存儲器與FLASH存儲器的區別。下面我們分別對SPI協議的物理層及協議層進行講解。
24.1.1 SPI物理層
SPI通訊設備之間的常用連接方式見圖 241。
圖 241 常見的SPI通訊系統
SPI通訊使用3條總線及片選線,3條總線分別為SCK、MOSI、MISO,片選線為,它們的作用介紹如下:
(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通訊時序
這是一個主機的通訊時序。NSS、SCK、MOSI信號都由主機控制產生,而MISO的信號由從機產生,主機通過該信號線讀取從機的數據。MOSI與MISO的信號只在NSS為低電平的時候才有效,在SCK的每個時鍾周期MOSI和MISO傳輸一位數據。
以上通訊流程中包含的各個信號分解如下:
2. 通訊的起始和停止信號
在圖 242中的標號處,NSS信號線由高變低,是SPI通訊的起始信號。NSS是每個從機各自獨占的信號線,當從機檢在自己的NSS線檢測到起始信號后,就知道自己被主機選中了,開始准備與主機通訊。在圖中的標號處,NSS信號由低變高,是SPI通訊的停止信號,表示本次通訊結束,從機的選中狀態被取消。
3. 數據有效性
SPI使用MOSI及MISO信號線來傳輸數據,使用SCK信號線進行數據同步。MOSI及MISO數據線在SCK的每個時鍾周期傳輸一位數據,且數據輸入輸出是同時進行的。數據傳輸時,MSB先行或LSB先行並沒有作硬性規定,但要保證兩個SPI通訊設備之間使用同樣的協定,一般都會采用圖 242中的MSB先行模式。
觀察圖中的 標號處,MOSI及MISO的數據在SCK的上升沿期間變化輸出,在SCK的下降沿時被采樣。即在SCK的下降沿時刻,MOSI及MISO的數據有效,高電平時表示數據"1",為低電平時表示數據"0"。在其它時刻,數據無效,MOSI及MISO為下一次表示數據做准備。
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時,MOSI或MISO數據線上的信號將會在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的采樣時刻不是由上升/下降沿決定的。MOSI和MISO數據線的有效信號在SCK的奇數邊沿保持不變,數據信號將在SCK奇數邊沿時被采樣,在非采樣時刻,MOSI和MISO的有效信號才發生切換。
類似地,當CPHA=1時,不受CPOL的影響,數據信號在SCK的偶數邊沿被采樣,見圖 244。
圖 244 CPHA=1時的SPI通訊模式
由CPOL及CPHA的不同狀態,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外設簡介
STM32的SPI外設可用作通訊的主機及從機,支持最高的SCK時鍾頻率為fpclk/2 (STM32F429型號的芯片默認fpclk1為90MHz,fpclk2為45MHz),完全支持SPI協議的4種模式,數據幀長度可設置為8位或16位,可設置數據MSB先行或LSB先行。它還支持雙線全雙工(前面小節說明的都是這種模式)、雙線單向以及單線模式。其中雙線單向模式可以同時使用MOSI及MISO數據線向一個方向傳輸數據,可以加快一倍的傳輸速度。而單線模式則可以減少硬件接線,當然這樣速率會受到影響。我們只講解雙線全雙工模式。
STM32的SPI外設還支持I2S功能,I2S功能是一種音頻串行通訊協議,在我們以后講解MP3播放器的章節中會進行介紹。
24.2.2 STM32的SPI架構剖析
圖 245 SPI架構圖
1. 通訊引腳
SPI的所有硬件架構都從圖 245中左側MOSI、MISO、SCK及NSS線展開的。STM32芯片有多個SPI外設,它們的SPI通訊信號引出到不同的GPIO引腳上,使用時必須配置到這些指定的引腳,見表 242。關於GPIO引腳的復用功能,可查閱《STM32F4xx規格書》,以它為准。
表 242 STM32F4xx的SPI引腳(整理自《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 |
其中SPI1、SPI4、SPI5、SPI6是APB2上的設備,最高通信速率達45Mbtis/s,SPI2、SPI3是APB1上的設備,最高通信速率為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總線頻率,APB1為fpclk1,APB2為fpckl2。
通過配置"控制寄存器CR"的"CPOL位"及"CPHA"位可以把SPI設置成前面分析的4種SPI模式。
3. 數據控制邏輯
SPI的MOSI及MISO都連接到數據移位寄存器上,數據移位寄存器的內容來源於接收緩沖區及發送緩沖區以及MISO、MOSI線。當向外發送數據的時候,數據移位寄存器以"發送緩沖區"為數據源,把數據一位一位地通過數據線發送出去;當從外部接收數據的時候,數據移位寄存器把數據線采樣到的數據一位一位地存儲到"接收緩沖區"中。通過寫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"可以獲取接收緩沖區中的內容。
假如我們使能了TXE或RXNE中斷,TXE或RXNE置1時會產生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 ),這兩個模式的最大區別為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外設。
24.4 SPI—讀寫串行FLASH實驗
FLSAH存儲器又稱閃存,它與EEPROM都是掉電后數據不丟失的存儲器,但FLASH存儲器容量普遍大於EEPROM,現在基本取代了它的地位。我們生活中常用的U盤、SD卡、SSD固態硬盤以及我們STM32芯片內部用於存儲程序的設備,都是FLASH類型的存儲器。在存儲控制上,最主要的區別是FLASH芯片只能一大片一大片地擦寫,而在"I2C章節"中我們了解到EEPROM可以單個字節擦寫。
本小節以一種使用SPI通訊的串行FLASH存儲芯片的讀寫實驗為大家講解STM32的SPI使用方法。實驗中STM32的SPI外設采用主模式,通過查詢事件的方式來確保正常通訊。
24.4.1 硬件設計
圖 247 SPI串行FLASH硬件連接圖
本實驗板中的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的型號或控制引腳不一樣,只需根據我們的工程修改即可,程序的控制原理相同。
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外設模式的配置。在配置STM32的SPI模式前,我們要先了解從機端的SPI模式。本例子中可通過查閱FLASH數據手冊《W25Q128》獲取。根據FLASH芯片的說明,它支持SPI模式0及模式3,支持雙線全雙工,使用MSB先行模式,支持最高通訊時鍾為104MHz,數據幀長度為8位。我們要把STM32的SPI外設中的這些參數配置一致。見代碼清單 244。
代碼清單 244 配置SPI模式
1 /**
2 * @brief SPI_FLASH引腳初始化
3 * @param 無
4 * @retval 無
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外設配置為主機端,雙線全雙工模式,數據幀長度為8位,使用SPI模式3(CPOL=1,CPHA=1),NSS引腳由軟件控制以及MSB先行模式。最后一個成員為CRC計算式,由於我們與FLASH芯片通訊不需要CRC校驗,並沒有使能SPI的CRC功能,這時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雙線全雙工模式下MOSI與MISO數據傳輸是同步的(請對比"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 FLASH讀ID指令"JEDEC ID"的時序(摘自規格書《W25Q128》)
主機首先通過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芯片的常用指令編碼使用宏來封裝起來,后面需要發送指令編碼的時候我們直接使用這些宏即可,見代碼清單 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運算求余,若writeAddr是SPI_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存儲器的區別。