STM32系統學習——SPI(讀寫串行 FLASH)

一、SPI 協議簡介
SPI 協議是由摩托羅拉公司提出的通訊協議(Serial Peripheral Interface),即串行外圍設備接口,是一種高速全雙工的通信總線。它被廣泛地使用在 ADC、LCD 等設備與 MCU 間,要求通訊速率較高的場合。
可與 I2C 章節對比閱讀,體會兩種通訊總線的差異以及 EEPROM 存儲器與 FLASH 存儲器的區別。
1、物理層
這裏寫圖片描述
SPI通訊使用 3 條總線及片選線,3條總線分別爲 SCK、MOSI、MISO,片選線爲 SS,它們的作用介紹如下:
(1) SS( Slave Select):從設備選擇信號線,常稱爲片選信號線,也稱爲 NSS、CS,以下用 NSS表示。當有多個 SPI從設備與 SPI主機相連時,設備的其它信號線 SCK、MOSI及 MISO同時並聯到相同的 SPI總線上,即無論有多少個從設備,都共同只使用這 3條總線;而每個從設備都有獨立的這一條 NSS 信號線,本信號線獨佔主機的一個引腳,即有多少個從設備,就有多少條片選信號線。I2C 協議中通過設備地址來尋址、選中總線上的某個設備並與其進行通訊;而 SPI 協議中沒有設備地址,它使用 NSS 信號線來尋址,當主機要選擇從設備時,把該從設備的 NSS 信號線設置爲低電平,該從設備即被選中,即片選有效,接着主機開始與被選中的從設備進行SPI通訊。所以SPI通訊以 NSS 線置低電平爲開始信號,以 NSS線被拉高作爲結束信號。
(2) SCK (Serial Clock):時鐘信號線,用於通訊數據同步。它由通訊主機產生,決定了通訊的速率,不同的設備支持的最高時鐘頻率不一樣,如 STM32 的 SPI 時鐘頻率最大爲f pclk /2,兩個設備之間通訊時,通訊速率受限於低速設備。
(3) MOSI (Master Output, Slave Input):主設備輸出/從設備輸入引腳。主機的數據從這條信號線輸出,從機由這條信號線讀入主機發送的數據,即這條線上數據的方向爲主機到從機。
(4) MISO(Master Input,,Slave Output):主設備輸入/從設備輸出引腳。主機從這條信號線讀入數據,從機的數據由這條信號線輸出到主機,即在這條線上數據的方向爲從機到主機。
2、協議層
這裏寫圖片描述
這是一個主機的通訊時序。NSS、SCK、MOSI 信號都由主機控制產生,而 MISO 的信號由從機產生,主機通過該信號線讀取從機的數據。MOSI 與 MISO 的信號只在 NSS 爲低電平的時候纔有效,在 SCK的每個時鐘週期 MOSI和 MISO傳輸一位數據。
(1) 通訊的起始和停止信號
在圖中的標號1處,NSS 信號線由高變低,是 SPI 通訊的起始信號。NSS 是每個從機各自獨佔的信號線,當從機在自己的 NSS 線檢測到起始信號後,就知道自己被主機選中了,開始準備與主機通訊。在圖中的標號處,NSS 信號由低變高,是 SPI 通訊的停止信號,表示本次通訊結束,從機的選中狀態被取消。
(2)數據有效性
SPI使用 MOSI及 MISO信號線來傳輸數據,使用 SCK信號線進行數據同步。MOSI及MISO 數據線在 SCK 的每個時鐘週期傳輸一位數據,且數據輸入輸出是同時進行的。數據傳輸時,MSB 先行或 LSB 先行並沒有作硬性規定,但要保證兩個 SPI通訊設備之間使用同樣的協定,一般都會採用圖 中的 MSB先行模式。
觀察圖中的2345標號處,MOSI及 MISO的數據在 SCK的上升沿期間變化輸出,在SCK 的下降沿時被採樣。即在 SCK 的下降沿時刻,MOSI 及 MISO 的數據有效,高電平時表示數據“1”,爲低電平時表示數據“0”。在其它時刻,數據無效,MOSI及 MISO爲下一次表示數據做準備。
SPI每次數據傳輸可以 8 位或 16 位爲單位,每次傳輸的單位數不受限制。
(3)CPOL/CPHA及通訊模式
上面講述的圖 中的時序只是 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 的“偶數邊沿”採樣。
這裏寫圖片描述
我們來分析這個 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的偶數邊沿被採樣,見下圖。
這裏寫圖片描述
由 CPOL 及 CPHA 的不同狀態,SPI 分成了四種模式,主機與從機需要工作在相同的模式下才可以正常通訊,實際中採用較多的是“模式 0”與“模式 3”。
這裏寫圖片描述
上圖即是SPI的四種模式。
二、STM32 的 SPI 特性及架構
1、STM32的 SPI外設簡介
STM32 的 SPI 外設可用作通訊的主機及從機,支持最高的 SCK 時鐘頻率爲 f pclk /2(STM32F103型號的芯片默認 f pclk1 爲 72MHz,f pclk2 爲 36MHz),完全支持 SPI協議的 4種模式,數據幀長度可設置爲 8 位或 16 位,可設置數據 MSB 先行或 LSB 先行。它還支持雙線全雙工(前面小節說明的都是這種模式)、雙線單向以及單線模式。其中雙線單向模式可以同時使用 MOSI 及 MISO 數據線向一個方向傳輸數據,可以加快一倍的傳輸速度。而單線模式則可以減少硬件接線,當然這樣速率會受到影響。我們只講解雙線全雙工模式。
2、架構剖析
這裏寫圖片描述
1)通訊引腳
PI的所有硬件架構都從圖 中左側 MOSI、MISO、SCK及 NSS線展開的。STM32芯片有多個 SPI外設,它們的 SPI通訊信號引出到不同的 GPIO引腳上,使用時必須配置到這些指定的引腳。
這裏寫圖片描述
其中 SPI1 是 APB2上的設備,最高通信速率達 36Mbtis/s,SPI2、SPI3 是 APB1上的設備,最高通信速率爲 18Mbits/s。除了通訊速率,在其它功能上沒有差異。其中 SPI3用到了下載接口的引腳,這幾個引腳默認功能是下載,第二功能纔是 IO 口,如果想使用 SPI3 接口,則程序上必須先禁用掉這幾個 IO 口的下載功能。一般在資源不是十分緊張的情況下,這幾個 IO 口是專門用於下載和調試程序,不會複用爲 SPI3。
2)時鐘控制邏輯
SCK線的時鐘信號,由波特率發生器根據“控制寄存器CR1”中的BR[0:2]位控制,該位是對 f pclk 時鐘的分頻因子,對 f pclk 的分頻結果就是 SCK 引腳的輸出時鐘頻率。
這裏寫圖片描述
其中的 f pclk 頻率是指 SPI所在的 APB總線頻率,APB1爲 f pclk1 ,APB2爲 f pckl2 。
通過配置“控制寄存器 CR”的“CPOL 位”及“CPHA”位可以把 SPI 設置成前面分析的 4 種 SPI模式。
3)數據控制邏輯
SPI的 MOSI及 MISO 都連接到數據移位寄存器上,數據移位寄存器的數據來源及目標接收、發送緩衝區以及 MISO、MOSI 線。當向外發送數據的時候,數據移位寄存器以“發送緩衝區”爲數據源,把數據一位一位地通過數據線發送出去;當從外部接收數據的時候,數據移位寄存器把數據線採樣到的數據一位一位地存儲到“接收緩衝區”中。通過寫 SPI的“數據寄存器 DR”把數據填充到發送 F 緩衝區中,通訊讀“數據寄存器 DR”,可以獲取接收緩衝區中的內容。其中數據幀長度可以通過“控制寄存器 CR1”的“DFF 位”配置成 8位及 16位模式;配置“LSBFIRST位”可選擇 MSB 先行還是 LSB 先行。

4)整體控制邏輯
整體控制邏輯負責協調整個 SPI 外設,控制邏輯的工作模式根據我們配置的“控制寄存器(CR1/CR2)”的參數而改變,基本的控制參數包括前面提到的 SPI 模式、波特率、LSB先行、主從模式、單雙向模式等等。在外設工作時,控制邏輯會根據外設的工作狀態修改“狀態寄存器(SR)”,我們只要讀取狀態寄存器相關的寄存器位,就可以瞭解 SPI 的工作狀態了。除此之外,控制邏輯還根據要求,負責控制產生 SPI 中斷信號、DMA 請求及控制NSS 信號線。
實際應用中,我們一般不使用 STM32 SPI外設的標準 NSS 信號線,而是更簡單地使用普通的 GPIO,軟件控制它的電平輸出,從而產生通訊起始和停止信號。
3、通訊過程
STM32使用 SPI外設通訊時,在通訊的不同階段它會對“狀態寄存器 SR”的不同數據位寫入參數,我們通過讀取這些寄存器標誌來了解通訊狀態。
圖中的是“主模式”流程,即 STM32作爲 SPI通訊的主機端時的數據收發過程。
這裏寫圖片描述
主模式收發流程及事件說明如下:
(1) 控制 NSS信號線,產生起始信號(圖中沒有畫出);
(2) 把要發送的數據寫入到“數據寄存器 DR”中,該數據會被存儲到發送緩衝區;
(3) 通訊開始,SCK 時鐘開始運行。MOSI 把發送緩衝區中的數據一位一位地傳輸出去;MISO 則把數據一位一位地存儲進接收緩衝區中;
(4) 當發送完一幀數據的時候,“狀態寄存器 SR”中的“TXE 標誌位”會被置 1,表示傳輸完一幀,發送緩衝區已空;類似地,當接收完一幀數據的時候,“RXNE標誌位”會被置 1,表示傳輸完一幀,接收緩衝區非空;
(5) 等待到“TXE標誌位”爲1時,若還要繼續發送數據,則再次往“數據寄存器DR”寫入數據即可;等待到“RXNE 標誌位”爲 1時,通過讀取“數據寄存器 DR”可以獲取接收緩衝區中的內容。
假如我們使能了 TXE或 RXNE中斷,TXE或 RXNE置 1時會產生 SPI中斷信號,進入同一個中斷服務函數,到 SPI 中斷服務程序後,可通過檢查寄存器位來了解是哪一個事件,再分別進行處理。也可以使用 DMA方式來收發“數據寄存器 DR”中的數據。

三、SPI 初始化結構體詳解
跟其它外設一樣,STM32 標準庫提供了 SPI 初始化結構體及初始化函數來配置 SPI 外設。初始化結構體及函數定義在庫文件“stm32f10x_spi.h”及“stm32f10x_spi.c”中,編程時我們可以結合這兩個文件內的註釋使用或參考庫幫助文檔。瞭解初始化結構體後我們就能對 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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

這些結構體成員說明如下,其中括號內的文字是對應參數在 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外設。
四、SPI—讀寫串行 FLASH 實驗
FLSAH 存儲器又稱閃存,它與 EEPROM 都是掉電後數據不丟失的存儲器,但 FLASH存儲器容量普遍大於 EEPROM,現在基本取代了它的地位。我們生活中常用的 U 盤、SD卡、SSD 固態硬盤以及我們 STM32 芯片內部用於存儲程序的設備,都是 FLASH 類型的存儲器。在存儲控制上,最主要的區別是 FLASH 芯片只能一大片一大片地擦寫,而在“I2C章節”中我們瞭解到 EEPROM可以單個字節擦寫。
本小節以一種使用 SPI 通訊的串行 FLASH 存儲芯片的讀寫實驗講解 STM32 的SPI 使用方法。實驗中 STM32 的 SPI 外設採用主模式,通過查詢事件的方式來確保正常通訊。
1、硬件設計
本實驗板中的 FLASH芯片(型號:W25Q64)是一種使用 SPI通訊協議的NOR FLASH存儲 器 , 它 的 CS/CLK/DIO/DO 引 腳 分 別 連 接 到 了 STM32 對 應 的 SPI 引 腳NSS/SCK/MOSI/MISO 上,其中 STM32的 NSS 引腳雖然是其片上 SPI外設的硬件引腳,但實際上後面的程序只是把它當成一個普通的 GPIO,使用軟件的方式控制 NSS 信號,所以在 SPI的硬件設計中,NSS 可以隨便選擇普通的 GPIO,不必糾結於選擇硬件 NSS信號。
FLASH 芯片中還有 WP 和 HOLD 引腳。WP 引腳可控制寫保護功能,當該引腳爲低電平時,禁止寫入數據。我們直接接電源,不使用寫保護功能。HOLD 引腳可用於暫停通訊,該引腳爲低電平時,通訊暫停,數據輸出引腳輸出高阻抗狀態,時鐘和數據輸入引腳無效。
我們直接接電源,不使用通訊暫停功能。關於 FLASH 芯片的更多信息,可參考其數據手冊《W25Q64》來了解。若實驗板 FLASH的型號或控制引腳不一樣,只需根據我們的工程修改即可,程序的控制原理相同。
這裏寫圖片描述
2、軟件設計
爲了使工程更加有條理,我們把讀寫 FLASH相關的代碼獨立分開存儲,方便以後移植。
在“工程模板”之上新建“bsp_spi_flash.c”及“bsp_spi_ flash.h”文件,這些文件也可根據您的喜好命名,它們不屬於 STM32標準庫的內容,是由我們自己根據應用需要編寫的。
1) 編程要點
初始化通訊使用的目標引腳及端口時鐘;
使能 SPI外設的時鐘;
配置 SPI外設的模式、地址、速率等參數並使能 SPI外設;
編寫基本 SPI按字節收發的函數;
編寫對 FLASH 擦除及讀寫操作的的函數;
編寫測試程序,對讀寫數據進行校驗。
2) 代碼分析
SPI 硬件相關宏定義
我們把 SPI硬件相關的配置都以宏的形式定義到 “bsp_spi_ flash.h”文件中

1 /*SPI 接口定義-開頭****************************/
2 #define FLASH_SPIx SPI1
3 #define FLASH_SPI_APBxClock_FUN RCC_APB2PeriphClockCmd
4 #define FLASH_SPI_CLK RCC_APB2Periph_SPI1
5 
6 //CS(NSS)引腳 片選選普通 GPIO 即可
7 #define FLASH_SPI_CS_APBxClock_FUN RCC_APB2PeriphClockCmd
8 #define FLASH_SPI_CS_CLK RCC_APB2Periph_GPIOA
9 #define FLASH_SPI_CS_PORT GPIOA
10 #define FLASH_SPI_CS_PIN GPIO_Pin_4
11 
12 //SCK 引腳
13 #define FLASH_SPI_SCK_APBxClock_FUN RCC_APB2PeriphClockCmd
14 #define FLASH_SPI_SCK_CLK RCC_APB2Periph_GPIOA
15 #define FLASH_SPI_SCK_PORT GPIOA
16 #define FLASH_SPI_SCK_PIN GPIO_Pin_5
17 //MISO 引腳
18 #define FLASH_SPI_MISO_APBxClock_FUN RCC_APB2PeriphClockCmd
19 #define FLASH_SPI_MISO_CLK RCC_APB2Periph_GPIOA
20 #define FLASH_SPI_MISO_PORT GPIOA
21 #define FLASH_SPI_MISO_PIN GPIO_Pin_6
22 //MOSI 引腳
23 #define FLASH_SPI_MOSI_APBxClock_FUN RCC_APB2PeriphClockCmd
24 #define FLASH_SPI_MOSI_CLK RCC_APB2Periph_GPIOA
25 #define FLASH_SPI_MOSI_PORT GPIOA
26 #define FLASH_SPI_MOSI_PIN GPIO_Pin_7
27 
28 #define FLASH_SPI_CS_LOW() GPIO_ResetBits( FLASH_SPI_CS_PORT, FLASH_SPI_CS_PIN )
29 #define FLASH_SPI_CS_HIGH() GPIO_SetBits( FLASH_SPI_CS_PORT, FLASH_SPI_CS_PIN )
30 
31 /*SPI 接口定義-結尾****************************/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

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

初始化 SPI 的 GPIO
利用上面的宏,編寫 SPI的初始化函數。

1 /**
2 * @brief SPI_FLASH 初始化
3 * @param 無
4 * @retval 無
5 */
6 void SPI_FLASH_Init(void)
7 {
8 SPI_InitTypeDef SPI_InitStructure;
9 GPIO_InitTypeDef GPIO_InitStructure;
10 
11 /* 使能 SPI 時鐘 */
12 FLASH_SPI_APBxClock_FUN ( FLASH_SPI_CLK, ENABLE );
13 
14 /* 使能 SPI 引腳相關的時鐘 */
15 FLASH_SPI_CS_APBxClock_FUN ( FLASH_SPI_CS_CLK|FLASH_SPI_SCK_CLK|
16 FLASH_SPI_MISO_PIN|FLASH_SPI_MOSI_PIN, ENABLE );
17 
18 /* 配置 SPI 的 CS 引腳,普通 IO 即可 */
19 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_PIN;
20 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
21 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
22 GPIO_Init(FLASH_SPI_CS_PORT, &GPIO_InitStructure);
23 
24 /* 配置 SPI 的 SCK 引腳*/
25 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN;
26 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
27 GPIO_Init(FLASH_SPI_SCK_PORT, &GPIO_InitStructure);
28 
29 /* 配置 SPI 的 MF103-霸道引腳*/
30 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;
31 GPIO_Init(FLASH_SPI_MISO_PORT, &GPIO_InitStructure);
32 
33 /* 配置 SPI 的 MOSI 引腳*/
34 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;
35 GPIO_Init(FLASH_SPI_MOSI_PORT, &GPIO_InitStructure);
36 
37 /* 停止信號 FLASH: CS 引腳高電平*/
38 FLASH_SPI_CS_HIGH();
39 //爲方便講解,以下省略 SPI 模式初始化部分
40 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

與所有使用到 GPIO的外設一樣,都要先把使用到的 GPIO引腳模式初始化,配置好複用功能。GPIO初始化流程如下:
(1) 使用GPIO_InitTypeDef定義 GPIO初始化結構體變量,以便下面用於存儲GPIO配置;
(2) 調用庫函數 RCC_APB2PeriphClockCmd 來使能 SPI引腳使用的 GPIO 端口時鐘。
(3) 向 GPIO 初始化結構體賦值,把 SCK/MOSI/MISO 引腳初始化成複用推輓模式。而CS(NSS)引腳由於使用軟件控制,我們把它配置爲普通的推輓輸出模式。
(4) 使用以上初始化結構體的配置,調用 GPIO_Init 函數向寄存器寫入參數,完成 GPIO 的初始化。

配置 SPI 的模式
以上只是配置了 SPI 使用的引腳,對 SPI 外設模式的配置。在配置 STM32 的 SPI 模式前,我們要先了解從機端的 SPI 模式。本例子中可通過查閱 FLASH 數據手冊《W25Q64》獲取。根據 FLASH 芯片的說明,它支持 SPI模式 0及模式 3,支持雙線全雙工,使用 MSB先行模式,支持最高通訊時鐘爲 104MHz,數據幀長度爲 8 位。我們要把 STM32 的 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 /* 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_4;
21 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
22 SPI_InitStructure.SPI_CRCPolynomial = 7;
23 SPI_Init(FLASH_SPIx, &SPI_InitStructure);
24 
25 /* 使能 SPI */
26 SPI_Cmd(FLASH_SPIx, ENABLE);
27 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

這段代碼中,把 STM32 的 SPI 外設配置爲主機端,雙線全雙工模式,數據幀長度爲 8位,使用 SPI 模式 3(CPOL=1,CPHA=1),NSS 引腳由軟件控制以及 MSB 先行模式。代碼中把 SPI的時鐘頻率配置成了 4分頻,實際上可以配置成 2分頻以提高通訊速率,讀者可親自嘗試一下。最後一個成員爲 CRC 計算式,由於我們與 FLASH 芯片通訊不需要 CRC 校驗,並沒有使能 SPI的 CRC功能,這時 CRC計算式的成員值是無效的。
賦值結束後調用庫函數 SPI_Init 把這些配置寫入寄存器,並調用 SPI_Cmd 函數使能外設。

使用 SPI 發送和接收一個字節的數據
初始化好SPI外設後,就可以使用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_SPIx, SPI_I2S_FLAG_TXE) == RESET)
13 {
14 if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);
15 }
16 
17 /* 寫入數據寄存器,把要寫入的數據寫入發送緩衝區 */
18 SPI_I2S_SendData(FLASH_SPIx, byte);
19 
20 SPITimeout = SPIT_FLAG_TIMEOUT;
21 
22 /* 等待接收緩衝區非空,RXNE 事件 */
23 while (SPI_I2S_GetFlagStatus(FLASH_SPIx, SPI_I2S_FLAG_RXNE) == RESET)
24 {
25 if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);
26 }
27 
28 /* 讀取數據寄存器,獲取接收緩衝區數據 */
29 return SPI_I2S_ReceiveData(FLASH_SPIx);
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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

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芯片的數據手冊《W25Q64》,可瞭解各種它定義的各種指令的功能及指令格式

定義 FLASH 指令編碼表
爲了方便使用,我們把 FLASH芯片的常用指令編碼使用宏來封裝起來,後面需要發送
指令編碼的時候我們直接使用這些宏即可

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 0XEF4017
20 #define Dummy_Byte 0xFF
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

讀取 FLASH 芯片 ID
根據“JEDEC”指令的時序,我們把讀取 FLASH ID 的過程編寫成一個函數。

讀取 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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

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

FLASH 寫使能以及讀取當前狀態
在向 FLASH 芯片存儲矩陣寫入數據前,首先要使能寫操作,通過“Write Enable”命令即可寫使能。

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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

與 EEPROM 一樣,由於 FLASH 芯片向內部存儲矩陣寫入數據需要消耗一定的時間,並不是在總線通訊結束的一瞬間完成的,所以在寫操作後需要確認 FLASH芯片“空閒”時才能進行再次寫入。爲了表示自己的工作狀態,FLASH 芯片定義了一個狀態寄存器。
這裏寫圖片描述
我們只關注這個狀態寄存器的第0位“BUSY”,當這個位爲“1”時,表明FLASH芯片處於忙碌狀態,它可能正在對內部的存儲矩陣進行“擦除”或“數據寫入”的操作。
利用指令表中的“Read Status Register”指令可以獲取 FLASH 芯片狀態寄存器的內容,其時序見下圖
這裏寫圖片描述
只要向 FLASH 芯片發送了讀狀態寄存器的指令,FLASH 芯片就會持續向主機返回最新的狀態寄存器內容,直到收到 SPI通訊的停止信號。據此我們編寫了具有等待 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 
13 /* 選擇 FLASH: CS 低 */
14 SPI_FLASH_CS_LOW();
15 
16 /* 發送 讀狀態寄存器 命令 */
17 SPI_FLASH_SendByte(W25X_ReadStatusReg);
18 
19 /* 若 FLASH 忙碌,則等待 */
20 do
21 {
22 /* 讀取 FLASH 芯片的狀態寄存器 */
23 FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);
24 }
25 while ((FLASH_Status & WIP_Flag) == SET); /* 正在寫入標誌 */
26 
27 /* 停止信號 FLASH: CS 高 */
28 SPI_FLASH_CS_HIGH();
29 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

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

FLASH 扇區擦除
由於 FLASH存儲器的特性決定了它只能把原來爲“1”的數據位改寫成“0”,而原來爲“0”的數據位不能直接改寫爲“1”。所以這裏涉及到數據“擦除”的概念,在寫入前,必須要對目標存儲矩陣進行擦除操作,把矩陣中的數據位擦除爲“1”,在數據寫入的時候,如果要存儲數據“1”,那就不修改存儲矩陣 ,在要存儲數據“0”時,才更改該位。通常,對存儲矩陣擦除的基本操作單位都是多個字節進行,如本例子中的 FLASH芯片支持“扇區擦除”、“塊擦除”以及“整片擦除”。
扇區擦除指令的第一個字節爲指令編碼,緊接着發送的 3 個字節用於表示要擦除的存
儲矩陣地址。要注意的是在扇區擦除指令前,還需要先發送“寫使能”指令,發送扇區擦
除指令後,通過讀取寄存器狀態等待扇區擦除操作完畢

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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

這段代碼調用的函數在前面都已講解,只要注意發送擦除地址時高位在前即可。調用扇區擦除指令時注意輸入的地址要對齊到 4KB。
FLASH 的頁寫入
目標扇區被擦除完畢後,就可以向它寫入數據了。與 EEPROM 類似,FLASH 芯片也有頁寫入命令,使用頁寫入命令最多可以一次向 FLASH 傳輸 256個字節的數據,我們把這個單位爲頁大小。FLASH 頁寫入的時序見圖 。
這裏寫圖片描述
從時序圖可知,第 1 個字節爲“頁寫入指令”編碼,2-4 字節爲要寫入的“地址 A”,接着的是要寫入的內容,最多個可以發送 256 字節數據,這些數據將會從“地址 A”開始,按順序寫入到 FLASH的存儲矩陣。若發送的數據超出 256個,則會覆蓋前面發送的數據。與擦除指令不一樣,頁寫入指令的地址並不要求按 256 字節對齊,只要確認目標存儲單元是擦除狀態即可(即被擦除後沒有被寫入過)。所以,若對“地址 x”執行頁寫入指令後,發送了 200 個字節數據後終止通訊,下一次再執行頁寫入指令,從“地址(x+200)”開始寫入200個字節也是沒有問題的(小於256均可)。 只是在實際應用中由於基本擦除單元是4KB,一般都以扇區爲單位進行讀寫。

把頁寫入時序封裝成函數

 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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

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

不定量數據寫入
應用的時候我們常常要寫入不定量的數據,直接調用“頁寫入”函數並不是特別方便,所以我們在它的基礎上編寫了“不定量數據寫入”的函數。

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 整數倍,
13 運算結果 Addr 值爲 0*/
14 Addr = WriteAddr % SPI_FLASH_PageSize;
15 
16 /*差 count 個數據值,剛好可以對齊到頁地址*/
17 count = SPI_FLASH_PageSize - Addr;
18 /*計算出要寫多少整數頁*/
19 NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
20 /*mod 運算求餘,計算出剩餘不滿一頁的字節數*/
21 NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
22 
23 /* Addr=0,則 WriteAddr 剛好按頁對齊 aligned */
24 if (Addr == 0)
25 {
26 /* NumByteToWrite < SPI_FLASH_PageSize */
27 if (NumOfPage == 0)
28 {
29 SPI_FLASH_PageWrite(pBuffer, WriteAddr,
30 NumByteToWrite);
31 }
32 else /* NumByteToWrite > SPI_FLASH_PageSize */
33 {
34 /*先把整數頁都寫了*/
35 while (NumOfPage--)
36 {
37 SPI_FLASH_PageWrite(pBuffer, WriteAddr,
38 SPI_FLASH_PageSize);
39 WriteAddr += SPI_FLASH_PageSize;
40 pBuffer += SPI_FLASH_PageSize;
41 }
42 /*若有多餘的不滿一頁的數據,把它寫完*/
43 SPI_FLASH_PageWrite(pBuffer, WriteAddr,
44 NumOfSingle);
45 }
46 }
47 /* 若地址與 SPI_FLASH_PageSize 不對齊 */
48 else
49 {
50 /* NumByteToWrite < SPI_FLASH_PageSize */
51 if (NumOfPage == 0)
52 {
53 /*當前頁剩餘的 count 個位置比 NumOfSingle 小,一頁寫不完*/
54 if (NumOfSingle > count)
55 {
56 temp = NumOfSingle - count;
57 /*先寫滿當前頁*/
58 SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
59 
60 WriteAddr += count;
61 pBuffer += count;
62 /*再寫剩餘的數據*/
63 SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);
64 }
65 else /*當前頁剩餘的 count 個位置能寫完 NumOfSingle 個數據*/
66 {
67 SPI_FLASH_PageWrite(pBuffer, WriteAddr,
68 NumByteToWrite);
69 }
70 }
71 else /* NumByteToWrite > SPI_FLASH_PageSize */
72 {
73 /*地址不對齊多出的 count 分開處理,不加入這個運算*/
74 NumByteToWrite -= count;
75 NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
76 NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
77 
78 /* 先寫完 count 個數據,爲的是讓下一次要寫的地址對齊 */
79 SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
80 
81 /* 接下來就重複地址對齊的情況 */
82 WriteAddr += count;
83 pBuffer += count;
84 /*把整數頁都寫了*/
85 while (NumOfPage--)
86 {
87 SPI_FLASH_PageWrite(pBuffer, WriteAddr,
88 SPI_FLASH_PageSize);
89 WriteAddr += SPI_FLASH_PageSize;
90 pBuffer += SPI_FLASH_PageSize;
91 }
92 /*若有多餘的不滿一頁的數據,把它寫完*/
93 if (NumOfSingle != 0)
94 {
95 SPI_FLASH_PageWrite(pBuffer, WriteAddr,
96 NumOfSingle);
97 }
98 }
99 }
100 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100

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

從 從 FLASH 讀取數據
相對於寫入,FLASH 芯片的數據讀取要簡單得多,使用讀取指令“Read Data”即可。
這裏寫圖片描述
發送了指令編碼及要讀的起始地址後,FLASH 芯片就會按地址遞增的方式返回存儲矩陣的內容,讀取的數據量沒有限制,只要沒有停止通訊,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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

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

MAIN函數

 main 函數
1 int main(void)
2 {
3 LED_GPIO_Config();
4 LED_BLUE;
5 
6 /* 配置串口 1 爲:115200 8-N-1 */
7 USART_Config();
8 printf("\r\n 這是一個 8Mbyte 串行 flash(W25Q64)實驗 \r\n");
9 
10 /* 8M 串行 flash W25Q64 初始化 */
11 SPI_FLASH_Init();
12 
13 /* 獲取 Flash Device ID */
14 DeviceID = SPI_FLASH_ReadDeviceID();
15 Delay( 200 );
16 
17 /* 獲取 SPI Flash ID */
18 FlashID = SPI_FLASH_ReadID();
19 printf("\r\n FlashID is 0x%X,\
20 Manufacturer Device ID is 0x%X\r\n", FlashID, DeviceID);
21 
22 /* 檢驗 SPI Flash ID */
23 if (FlashID == sFLASH_ID)
24 {
25 printf("\r\n 檢測到串行 flash W25Q64 !\r\n");
26 
27 /* 擦除將要寫入的 SPI FLASH 扇區,FLASH 寫入前要先擦除 */
28 // 這裏擦除 4K,即一個扇區,擦除的最小單位是扇區
29 SPI_FLASH_SectorErase(FLASH_SectorToErase);
30 
31 /* 將發送緩衝區的數據寫到 flash 中 */
32 // 這裏寫一頁,一頁的大小爲 256 個字節
33 SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress, BufferSize);
34 printf("\r\n 寫入的數據爲:%s \r\t", Tx_Buffer);
35 
36 /* 將剛剛寫入的數據讀出來放到接收緩衝區中 */
37 SPI_FLASH_BufferRead(Rx_Buffer, FLASH_ReadAddress, BufferSize);
38 printf("\r\n 讀出的數據爲:%s \r\n", Rx_Buffer);
39 
40 /* 檢查寫入的數據與讀出的數據是否相等 */
41 TransferStatus1 = Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize);
42 
43 if ( PASSED == TransferStatus1 )
44 {
45 LED_GREEN;
46 printf("\r\n 8M 串行 flash(W25Q64)測試成功!\n\r");
47 }
48 else
49 {
50 LED_RED;
51 printf("\r\n 8M 串行 flash(W25Q64)測試失敗!\n\r");
52 }
53 }// if (FlashID == sFLASH_ID)
54 else// if (FlashID == sFLASH_ID)
55 {
56 LED_RED;
57 printf("\r\n 獲取不到 W25Q64 ID!\n\r");
58 }
59 
60 while (1);
61 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

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

思考
1. 在 SPI外設初始化部分,MISO引腳可以設置爲輸入模式嗎?爲什麼?實際測試現象如何?
2. 嘗試使用 FLASH 芯片存儲 int 整型變量,float 型浮點變量,編寫程序寫入數據,並讀出校驗。 提示: FLASH 存儲的數據都是 0 和 1 ,至於你存儲的是整數還是浮點型的數,FLASH 是完全不知道的,只是你解析的時候解析成什麼類型的數而已。當你存儲的是浮點型的數據的時候,那麼解析的時候就是 4 個字節纔是一個數,如果存儲的是字符型的,那麼解析的時候一個字節就是一個數。
3. 如果扇區未經擦除就寫入,會有什麼後果?請做實驗驗證。
4. 簡述 FLASH 存儲器與 EEPROM存儲器的區別。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章