一、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庫開發實戰指南》