24.1 SPI協議簡介
SPI協議是由摩托羅拉公司提出的通訊協議(Serial Peripheral Interface),即串行外圍設備接口,是一種高速全雙工的通信總線。它被廣泛地使用在ADC、LCD等設備與MCU間,要求通訊速率較高的場合。
學習本章時,可與I2C章節(jié)對比閱讀,體會兩種通訊總線的差異以及EEPROM存儲器與FLASH存儲器的區(qū)別。下面我們分別對SPI協議的物理層及協議層進行講解。
24.1.1 SPI物理層
SPI通訊設備之間的常用連接方式見圖 241。
圖 241 常見的SPI通訊系統(tǒng)
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):主設備輸出/從設備輸入引腳。主機的數據從這條信號線輸出,從機由這條信號線讀入主機發(fā)送的數據,即這條線上數據的方向為主機到從機。
(4) MISO(Master Input,,Slave Output):主設備輸入/從設備輸出引腳。主機從這條信號線讀入數據,從機的數據由這條信號線輸出到主機,即在這條線上數據的方向為從機到主機。
24.1.2 協議層
與I2C的類似,SPI協議定義了通訊的起始和停止信號、數據有效性、時鐘同步等環(huán)節(jié)。
1. SPI基本通訊過程
先看看SPI通訊的通訊時序,見圖 242。
圖 242 SPI通訊時序
這是一個主機的通訊時序。NSS、SCK、MOSI信號都由主機控制產生,而MISO的信號由從機產生,主機通過該信號線讀取從機的數據。MOSI與MISO的信號只在NSS為低電平的時候才有效,在SCK的每個時鐘周期MOSI和MISO傳輸一位數據。
以上通訊流程中包含的各個信號分解如下:
2. 通訊的起始和停止信號
在圖 242中的標號處,NSS信號線由高變低,是SPI通訊的起始信號。NSS是每個從機各自獨占的信號線,當從機檢在自己的NSS線檢測到起始信號后,就知道自己被主機選中了,開始準備與主機通訊。在圖中的標號處,NSS信號由低變高,是SPI通訊的停止信號,表示本次通訊結束,從機的選中狀態(tài)被取消。
3. 數據有效性
SPI使用MOSI及MISO信號線來傳輸數據,使用SCK信號線進行數據同步。MOSI及MISO數據線在SCK的每個時鐘周期傳輸一位數據,且數據輸入輸出是同時進行的。數據傳輸時,MSB先行或LSB先行并沒有作硬性規(guī)定,但要保證兩個SPI通訊設備之間使用同樣的協定,一般都會采用圖 242中的MSB先行模式。
觀察圖中的標號處,MOSI及MISO的數據在SCK的上升沿期間變化輸出,在SCK的下降沿時被采樣。即在SCK的下降沿時刻,MOSI及MISO的數據有效,高電平時表示數據"1",為低電平時表示數據"0"。在其它時刻,數據無效,MOSI及MISO為下一次表示數據做準備。
SPI每次數據傳輸可以8位或16位為單位,每次傳輸的單位數不受限制。
4. CPOL/CPHA及通訊模式
上面講述的圖 242中的時序只是SPI中的其中一種通訊模式,SPI一共有四種通訊模式,它們的主要區(qū)別是總線空閑時SCK的時鐘狀態(tài)以及數據采樣時刻。為方便說明,在此引入"時鐘極性CPOL"和"時鐘相位CPHA"的概念。
時鐘極性CPOL是指SPI通訊設備處于空閑狀態(tài)時,SCK信號線的電平信號(即SPI通訊開始前、 NSS線為高電平時SCK的狀態(tài))。CPOL=0時, SCK在空閑狀態(tài)時為低電平,CPOL=1時,則相反。
時鐘相位CPHA是指數據的采樣的時刻,當CPHA=0時,MOSI或MISO數據線上的信號將會在SCK時鐘線的"奇數邊沿"被采樣。當CPHA=1時,數據線在SCK的"偶數邊沿"采樣。見圖 243及圖 244。
圖 243 CPHA=0時的SPI通訊模式
我們來分析這個CPHA=0的時序圖。首先,根據SCK在空閑狀態(tài)時的電平,分為兩種情況。SCK信號線在空閑狀態(tài)為低電平時,CPOL=0;空閑狀態(tài)為高電平時,CPOL=1。
無論CPOL=0還是=1,因為我們配置的時鐘相位CPHA=0,在圖中可以看到,采樣時刻都是在SCK的奇數邊沿。注意當CPOL=0的時候,時鐘的奇數邊沿是上升沿,而CPOL=1的時候,時鐘的奇數邊沿是下降沿。所以SPI的采樣時刻不是由上升/下降沿決定的。MOSI和MISO數據線的有效信號在SCK的奇數邊沿保持不變,數據信號將在SCK奇數邊沿時被采樣,在非采樣時刻,MOSI和MISO的有效信號才發(fā)生切換。
類似地,當CPHA=1時,不受CPOL的影響,數據信號在SCK的偶數邊沿被采樣,見圖 244。
圖 244 CPHA=1時的SPI通訊模式
由CPOL及CPHA的不同狀態(tài),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先行。它還支持雙線全雙工(前面小節(jié)說明的都是這種模式)、雙線單向以及單線模式。其中雙線單向模式可以同時使用MOSI及MISO數據線向一個方向傳輸數據,可以加快一倍的傳輸速度。而單線模式則可以減少硬件接線,當然這樣速率會受到影響。我們只講解雙線全雙工模式。
STM32的SPI外設還支持I2S功能,I2S功能是一種音頻串行通訊協議,在我們以后講解MP3播放器的章節(jié)中會進行介紹。
24.2.2 STM32的SPI架構剖析
圖 245 SPI架構圖
1. 通訊引腳
SPI的所有硬件架構都從圖 245中左側MOSI、MISO、SCK及NSS線展開的。STM32芯片有多個SPI外設,它們的SPI通訊信號引出到不同的GPIO引腳上,使用時必須配置到這些指定的引腳,見表 242。關于GPIO引腳的復用功能,可查閱《STM32F4xx規(guī)格書》,以它為準。
其中SPI1、SPI4、SPI5、SPI6是APB2上的設備,最高通信速率達45Mbtis/s,SPI2、SPI3是APB1上的設備,最高通信速率為22.5Mbits/s。除了通訊速率,在其它功能上沒有差異。
2. 時鐘控制邏輯
SCK線的時鐘信號,由波特率發(fā)生器根據"控制寄存器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都連接到數據移位寄存器上,數據移位寄存器的內容來源于接收緩沖區(qū)及發(fā)送緩沖區(qū)以及MISO、MOSI線。當向外發(fā)送數據的時候,數據移位寄存器以"發(fā)送緩沖區(qū)"為數據源,把數據一位一位地通過數據線發(fā)送出去;當從外部接收數據的時候,數據移位寄存器把數據線采樣到的數據一位一位地存儲到"接收緩沖區(qū)"中。通過寫SPI的"數據寄存器DR"把數據填充到發(fā)送緩沖區(qū)中,通過"數據寄存器DR",可以獲取接收緩沖區(qū)中的內容。其中數據幀長度可以通過"控制寄存器CR1"的"DFF位"配置成8位及16位模式;配置"LSBFIRST位"可選擇MSB先行還是LSB先行。
4. 整體控制邏輯
整體控制邏輯負責協調整個SPI外設,控制邏輯的工作模式根據我們配置的"控制寄存器(CR1/CR2)"的參數而改變,基本的控制參數包括前面提到的SPI模式、波特率、LSB先行、主從模式、單雙向模式等等。在外設工作時,控制邏輯會根據外設的工作狀態(tài)修改"狀態(tài)寄存器(SR)",我們只要讀取狀態(tài)寄存器相關的寄存器位,就可以了解SPI的工作狀態(tài)了。除此之外,控制邏輯還根據要求,負責控制產生SPI中斷信號、DMA請求及控制NSS信號線。
實際應用中,我們一般不使用STM32 SPI外設的標準NSS信號線,而是更簡單地使用普通的GPIO,軟件控制它的電平輸出,從而產生通訊起始和停止信號。
24.2.3 通訊過程
STM32使用SPI外設通訊時,在通訊的不同階段它會對"狀態(tài)寄存器SR"的不同數據位寫入參數,我們通過讀取這些寄存器標志來了解通訊狀態(tài)。
圖 246中的是"主模式"流程,即STM32作為SPI通訊的主機端時的數據收發(fā)過程。
圖 246 主發(fā)送器通訊過程
主模式收發(fā)流程及事件說明如下:
(1) 控制NSS信號線,產生起始信號(圖中沒有畫出);
(2) 把要發(fā)送的數據寫入到"數據寄存器DR"中,該數據會被存儲到發(fā)送緩沖區(qū);
(3) 通訊開始,SCK時鐘開始運行。MOSI把發(fā)送緩沖區(qū)中的數據一位一位地傳輸出去;MISO則把數據一位一位地存儲進接收緩沖區(qū)中;
(4) 當發(fā)送完一幀數據的時候,"狀態(tài)寄存器SR"中的"TXE標志位"會被置1,表示傳輸完一幀,發(fā)送緩沖區(qū)已空;類似地,當接收完一幀數據的時候,"RXNE標志位"會被置1,表示傳輸完一幀,接收緩沖區(qū)非空;
(5) 等待到"TXE標志位"為1時,若還要繼續(xù)發(fā)送數據,則再次往"數據寄存器DR"寫入數據即可;等待到"RXNE標志位"為1時,通過讀取"數據寄存器DR"可以獲取接收緩沖區(qū)中的內容。
假如我們使能了TXE或RXNE中斷,TXE或RXNE置1時會產生SPI中斷信號,進入同一個中斷服務函數,到SPI中斷服務程序后,可通過檢查寄存器位來了解是哪一個事件,再分別進行處理。也可以使用DMA方式來收發(fā)"數據寄存器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)、單線只發(fā)送模式(SPI_Direction_1Line_Tx)。
(2) SPI_Mode
本成員設置SPI工作在主機模式(SPI_Mode_Master)或從機模式(SPI_Mode_Slave ),這兩個模式的最大區(qū)別為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的說明參考前面"通訊模式"小節(jié)。
時鐘極性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固態(tài)硬盤以及我們STM32芯片內部用于存儲程序的設備,都是FLASH類型的存儲器。在存儲控制上,最主要的區(qū)別是FLASH芯片只能一大片一大片地擦寫,而在"I2C章節(jié)"中我們了解到EEPROM可以單個字節(jié)擦寫。
本小節(jié)以一種使用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引腳可用于暫停通訊,該引腳為低電平時,通訊暫停,數據輸出引腳輸出高阻抗狀態(tài),時鐘和數據輸入引腳無效。我們直接接電源,不使用通訊暫停功能。
關于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按字節(jié)收發(fā)的函數;
(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發(fā)送和接收一個字節(jié)的數據
初始化好SPI外設后,就可以使用SPI通訊了,復雜的數據通訊都是由單個字節(jié)數據收發(fā)組成的,我們看看它的代碼實現,見代碼清單 245。
代碼清單 245 使用SPI發(fā)送和接收一個字節(jié)的數據
1 #define Dummy_Byte 0xFF
2 /**
3 * @brief 使用SPI發(fā)送一個字節(jié)的數據
4 * @param byte:要發(fā)送的數據
5 * @retval 返回接收到的數據
6 */
7 u8 SPI_FLASH_SendByte(u8 byte)
8 {
9 SPITimeout = SPIT_FLAG_TIMEOUT;
10
11 /* 等待發(fā)送緩沖區(qū)為空,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 /* 寫入數據寄存器,把要寫入的數據寫入發(fā)送緩沖區(qū) */
18 SPI_I2S_SendData(FLASH_SPI, byte);
19
20 SPITimeout = SPIT_FLAG_TIMEOUT;
21
22 /* 等待接收緩沖區(qū)非空,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 /* 讀取數據寄存器,獲取接收緩沖區(qū)數據 */
29 return SPI_I2S_ReceiveData(FLASH_SPI);
30 }
31
32 /**
33 * @brief 使用SPI讀取一個字節(jié)的數據
34 * @param 無
35 * @retval 返回接收到的數據
36 */
37 u8 SPI_FLASH_ReadByte(void)
38 {
39 return (SPI_FLASH_SendByte(Dummy_Byte));
40 }
SPI_FLASH_SendByte發(fā)送單字節(jié)函數中包含了等待事件的超時處理,這部分原理跟I2C中的一樣,在此不再贅述。
SPI_FLASH_SendByte函數實現了前面講解的"SPI通訊過程":
(1) 本函數中不包含SPI起始和停止信號,只是收發(fā)的主要過程,所以在調用本函數前后要做好起始和停止信號的操作;
(2) 對SPITimeout變量賦值為宏SPIT_FLAG_TIMEOUT。這個SPITimeout變量在下面的while循環(huán)中每次循環(huán)減1,該循環(huán)通過調用庫函數SPI_I2S_GetFlagStatus檢測事件,若檢測到事件,則進入通訊的下一階段,若未檢測到事件則停留在此處一直檢測,當檢測SPIT_FLAG_TIMEOUT次都還沒等待到事件則認為通訊失敗,調用的SPI_TIMEOUT_UserCallback輸出調試信息,并退出通訊;
(3) 通過檢測TXE標志,獲取發(fā)送緩沖區(qū)的狀態(tài),若發(fā)送緩沖區(qū)為空,則表示可能存在的上一個數據已經發(fā)送完畢;
(4) 等待至發(fā)送緩沖區(qū)為空后,調用庫函數SPI_I2S_SendData把要發(fā)送的數據"byte"寫入到SPI的數據寄存器DR,寫入SPI數據寄存器的數據會存儲到發(fā)送緩沖區(qū),由SPI外設發(fā)送出去;
(5) 寫入完畢后等待RXNE事件,即接收緩沖區(qū)非空事件。由于SPI雙線全雙工模式下MOSI與MISO數據傳輸是同步的(請對比"SPI通訊過程"閱讀),當接收緩沖區(qū)非空時,表示上面的數據發(fā)送完畢,且接收緩沖區(qū)也收到新的數據;
(6) 等待至接收緩沖區(qū)非空時,通過調用庫函數SPI_I2S_ReceiveData讀取SPI的數據寄存器DR,就可以獲取接收緩沖區(qū)中的新數據了。代碼中使用關鍵字"return"把接收到的這個數據作為SPI_FLASH_SendByte函數的返回值,所以我們可以看到在下面定義的SPI接收數據函數SPI_FLASH_ReadByte,它只是簡單地調用了SPI_FLASH_SendByte函數發(fā)送數據"Dummy_Byte",然后獲取其返回值(因為不關注發(fā)送的數據,所以此時的輸入參數"Dummy_Byte"可以為任意值)。可以這樣做的原因是SPI的接收過程和發(fā)送過程實質是一樣的,收發(fā)同步進行,關鍵在于我們的上層應用中,關注的是發(fā)送還是接收的數據。
控制FLASH的指令
搞定SPI的基本收發(fā)單元后,還需要了解如何對FLASH芯片進行讀寫。FLASH芯片自定義了很多指令,我們通過控制STM32利用SPI總線向FLASH芯片發(fā)送指令,FLASH芯片收到后就會執(zhí)行相應的操作。
而這些指令,對主機端(STM32)來說,只是它遵守最基本的SPI通訊協議發(fā)送出的數據,但在設備端(FLASH芯片)把這些數據解釋成不同的意義,所以才成為指令。查看FLASH芯片的數據手冊《W25Q128》,可了解各種它定義的各種指令的功能及指令格式。
該表中的第一列為指令名,第二列為指令編碼,第三至第N列的具體內容根據指令的不同而有不同的含義。其中帶括號的字節(jié)參數,方向為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)。緊跟指令編碼的三個字節(jié)分別為FLASH芯片輸出的"(M7-M0)"、"(ID15-ID8)"及"(ID7-ID0)"。
此處我們以該指令為例,配合其指令時序圖進行講解,見圖 248。
圖 248 FLASH讀ID指令"JEDEC ID"的時序(摘自規(guī)格書《W25Q128》)
主機首先通過MOSI線向FLASH芯片發(fā)送第一個字節(jié)數據為"9F h",當FLASH芯片收到該數據后,它會解讀成主機向它發(fā)送了"JEDEC指令",然后它就作出該命令的響應:通過MISO線把它的廠商ID(M7-M0)及芯片類型(ID15-0)發(fā)送給主機,主機接收到指令響應后可進行校驗。常見的應用是主機端通過讀取設備ID來測試硬件是否連接正常,或用于識別設備。
對于FLASH芯片的其它指令,都是類似的,只是有的指令包含多個字節(jié),或者響應包含更多的數據。
實際上,編寫設備驅動都是有一定的規(guī)律可循的。首先我們要確定設備使用的是什么通訊協議。如上一章的EEPROM使用的是I2C,本章的FLASH使用的是SPI。那么我們就先根據它的通訊協議,選擇好STM32的硬件模塊,并進行相應的I2C或SPI模塊初始化。接著,我們要了解目標設備的相關指令,因為不同的設備,都會有相應的不同的指令。如EEPROM中會把第一個數據解釋為內部存儲矩陣的地址(實質就是指令)。而FLASH則定義了更多的指令,有寫指令,讀指令,讀ID指令等等。最后,我們根據這些指令的格式要求,使用通訊協議向設備發(fā)送指令,達到控制設備的目標。
定義FLASH指令編碼表
為了方便使用,我們把FLASH芯片的常用指令編碼使用宏來封裝起來,后面需要發(fā)送指令編碼的時候我們直接使用這些宏即可,見代碼清單 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 /* 發(fā)送JEDEC指令,讀取ID */
14 SPI_FLASH_SendByte(W25X_JedecDeviceID);
15
16 /* 讀取一個字節(jié)數據 */
17 Temp0 = SPI_FLASH_SendByte(Dummy_Byte);
18
19 /* 讀取一個字節(jié)數據 */
20 Temp1 = SPI_FLASH_SendByte(Dummy_Byte);
21
22 /* 讀取一個字節(jié)數據 */
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"以及前面編寫的單字節(jié)收發(fā)函數SPI_FLASH_SendByte,很清晰地實現了"JEDEC ID"指令的時序:發(fā)送一個字節(jié)的指令編碼"W25X_JedecDeviceID",然后讀取3個字節(jié),獲取FLASH芯片對該指令的響應,最后把讀取到的這3個數據合并到一個變量Temp中,然后作為函數返回值,把該返回值與我們定義的宏"sFLASH_ID"對比,即可知道FLASH芯片是否正常。
FLASH寫使能以及讀取當前狀態(tài)
在向FLASH芯片存儲矩陣寫入數據前,首先要使能寫操作,通過"Write Enable"命令即可寫使能,見代碼清單 248。
代碼清單 248 寫使能命令
1 /**
2 * @brief 向FLASH發(fā)送寫使能命令
3 * @param none
4 * @retval none
5 */
6 void SPI_FLASH_WriteEnable(void)
7 {
8 /* 通訊開始:CS低 */
9 SPI_FLASH_CS_LOW();
10
11 /* 發(fā)送寫使能命令*/
12 SPI_FLASH_SendByte(W25X_WriteEnable);
13
14 /*通訊結束:CS高 */
15 SPI_FLASH_CS_HIGH();
16 }
與EEPROM一樣,由于FLASH芯片向內部存儲矩陣寫入數據需要消耗一定的時間,并不是在總線通訊結束的一瞬間完成的,所以在寫操作后需要確認FLASH芯片"空閑"時才能進行再次寫入。為了表示自己的工作狀態(tài),FLASH芯片定義了一個狀態(tài)寄存器,見圖 249。
圖 249 FLASH芯片的狀態(tài)寄存器
我們只關注這個狀態(tài)寄存器的第0位"BUSY",當這個位為"1"時,表明FLASH芯片處于忙碌狀態(tài),它可能正在對內部的存儲矩陣進行"擦除"或"數據寫入"的操作。
利用指令表中的"Read Status Register"指令可以獲取FLASH芯片狀態(tài)寄存器的內容,其時序見圖 2410。
圖 2410 讀取狀態(tài)寄存器的時序
只要向FLASH芯片發(fā)送了讀狀態(tài)寄存器的指令,FLASH芯片就會持續(xù)向主機返回最新的狀態(tài)寄存器內容,直到收到SPI通訊的停止信號。據此我們編寫了具有等待FLASH芯片寫入結束功能的函數,見代碼清單 249。
代碼清單 249 通過讀狀態(tài)寄存器等待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 /* 發(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); /* 正在寫入標志 */
31
32 /* 停止信號 FLASH: CS 高 */
33 SPI_FLASH_CS_HIGH();
34 }
這段代碼發(fā)送讀狀態(tài)寄存器的指令編碼"W25X_ReadStatusReg"后,在while循環(huán)里持續(xù)獲取寄存器的內容并檢驗它的"WIP_Flag標志"(即BUSY位),一直等待到該標志表示寫入結束時才退出本函數,以便繼續(xù)后面與FLASH芯片的數據通訊。
FLASH扇區(qū)擦除
由于FLASH存儲器的特性決定了它只能把原來為"1"的數據位改寫成"0",而原來為"0"的數據位不能直接改寫為"1"。所以這里涉及到數據"擦除"的概念,在寫入前,必須要對目標存儲矩陣進行擦除操作,把矩陣中的數據位擦除為"1",在數據寫入的時候,如果要存儲數據"1",那就不修改存儲矩陣,在要存儲數據"0"時,才更改該位。
通常,對存儲矩陣擦除的基本操作單位都是多個字節(jié)進行,如本例子中的FLASH芯片支持"扇區(qū)擦除"、"塊擦除"以及"整片擦除"。
FLASH芯片的最小擦除單位為扇區(qū)(Sector),而一個塊(Block)包含16個扇區(qū),其內部存儲矩陣分布見圖 2411。。
圖 2411 FLASH芯片的存儲矩陣
使用扇區(qū)擦除指令"Sector Erase"可控制FLASH芯片開始擦寫,其指令時序見圖 2414。
圖 2412 扇區(qū)擦除時序
扇區(qū)擦除指令的第一個字節(jié)為指令編碼,緊接著發(fā)送的3個字節(jié)用于表示要擦除的存儲矩陣地址。要注意的是在扇區(qū)擦除指令前,還需要先發(fā)送"寫使能"指令,發(fā)送扇區(qū)擦除指令后,通過讀取寄存器狀態(tài)等待扇區(qū)擦除操作完畢,代碼實現見代碼清單 2410。
代碼清單 2410 擦除扇區(qū)
1 /**
2 * @brief 擦除FLASH扇區(qū)
3 * @param SectorAddr:要擦除的扇區(qū)地址
4 * @retval 無
5 */
6 void SPI_FLASH_SectorErase(u32 SectorAddr)
7 {
8 /* 發(fā)送FLASH寫使能命令 */
9 SPI_FLASH_WriteEnable();
10 SPI_FLASH_WaitForWriteEnd();
11 /* 擦除扇區(qū) */
12 /* 選擇FLASH: CS低電平 */
13 SPI_FLASH_CS_LOW();
14 /* 發(fā)送扇區(qū)擦除指令*/
15 SPI_FLASH_SendByte(W25X_SectorErase);
16 /*發(fā)送擦除扇區(qū)地址的高位*/
17 SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);
18 /* 發(fā)送擦除扇區(qū)地址的中位 */
19 SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);
20 /* 發(fā)送擦除扇區(qū)地址的低位 */
21 SPI_FLASH_SendByte(SectorAddr & 0xFF);
22 /* 停止信號 FLASH: CS 高電平 */
23 SPI_FLASH_CS_HIGH();
24 /* 等待擦除完畢*/
25 SPI_FLASH_WaitForWriteEnd();
26 }
這段代碼調用的函數在前面都已講解,只要注意發(fā)送擦除地址時高位在前即可。調用扇區(qū)擦除指令時注意輸入的地址要對齊到4KB。
FLASH的頁寫入
目標扇區(qū)被擦除完畢后,就可以向它寫入數據了。與EEPROM類似,FLASH芯片也有頁寫入命令,使用頁寫入命令最多可以一次向FLASH傳輸256個字節(jié)的數據,我們把這個單位為頁大小。FLASH頁寫入的時序見圖 2413。
圖 2413 FLASH芯片頁寫入
從時序圖可知,第1個字節(jié)為"頁寫入指令"編碼,2-4字節(jié)為要寫入的"地址A",接著的是要寫入的內容,最多個可以發(fā)送256字節(jié)數據,這些數據將會從"地址A"開始,按順序寫入到FLASH的存儲矩陣。若發(fā)送的數據超出256個,則會覆蓋前面發(fā)送的數據。
與擦除指令不一樣,頁寫入指令的地址并不要求按256字節(jié)對齊,只要確認目標存儲單元是擦除狀態(tài)即可(即被擦除后沒有被寫入過)。所以,若對"地址x"執(zhí)行頁寫入指令后,發(fā)送了200個字節(jié)數據后終止通訊,下一次再執(zhí)行頁寫入指令,從"地址(x+200)"開始寫入200個字節(jié)也是沒有問題的(小于256均可)。只是在實際應用中由于基本擦除單元是4KB,一般都以扇區(qū)為單位進行讀寫,想深入了解,可學習我們的"FLASH文件系統(tǒng)"相關的例子。
把頁寫入時序封裝成函數,其實現見代碼清單 2411。
代碼清單 2411 FLASH的頁寫入
1 /**
2 * @brief 對FLASH按頁寫入數據,調用本函數寫入數據前需要先擦除扇區(qū)
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 /* 發(fā)送FLASH寫使能命令 */
11 SPI_FLASH_WriteEnable();
12
13 /* 選擇FLASH: CS低電平 */
14 SPI_FLASH_CS_LOW();
15 /* 寫送寫指令*/
16 SPI_FLASH_SendByte(W25X_PageProgram);
17 /*發(fā)送寫地址的高位*/
18 SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
19 /*發(fā)送寫地址的中位*/
20 SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
21 /*發(fā)送寫地址的低位*/
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 /* 發(fā)送當前要寫入的字節(jié)數據 */
34 SPI_FLASH_SendByte(*pBuffer);
35 /* 指向下一字節(jié)數據 */
36 pBuffer++;
37 }
38
39 /* 停止信號 FLASH: CS 高電平 */
40 SPI_FLASH_CS_HIGH();
41
42 /* 等待寫入完畢*/
43 SPI_FLASH_WaitForWriteEnd();
44 }
這段代碼的內容為:先發(fā)送"寫使能"命令,接著才開始頁寫入時序,然后發(fā)送指令編碼、地址,再把要寫入的數據一個接一個地發(fā)送出去,發(fā)送完后結束通訊,檢查FLASH狀態(tài)寄存器,等待FLASH內部寫入結束。
不定量數據寫入
應用的時候我們常常要寫入不定量的數據,直接調用"頁寫入"函數并不是特別方便,所以我們在它的基礎上編寫了"不定量數據寫入"的函數,基實現見代碼清單 2412。
代碼清單 2412不定量數據寫入
1 /**
2 * @brief 對FLASH寫入數據,調用本函數寫入數據前需要先擦除扇區(qū)
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運算求余,計算出剩余不滿一頁的字節(jié)數*/
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章節(jié)中的"快速寫入多字節(jié)"函數原理是一樣的,運算過程在此不再贅述。區(qū)別是頁的大小以及實際數據寫入的時候,使用的是針對FLASH芯片的頁寫入函數,且在實際調用這個"不定量數據寫入"函數時,還要注意確保目標扇區(qū)處于擦除狀態(tài)。
從FLASH讀取數據
相對于寫入,FLASH芯片的數據讀取要簡單得多,使用讀取指令"Read Data"即可,其指令時序見圖 2414。
圖 2414 SPI FLASH讀取數據時序
發(fā)送了指令編碼及要讀的起始地址后,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 /* 發(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 /* 讀取一個字節(jié)*/
27 *pBuffer = SPI_FLASH_SendByte(Dummy_Byte);
28 /* 指向下一個字節(jié)緩沖區(qū) */
29 pBuffer++;
30 }
31
32 /* 停止信號 FLASH: CS 高電平 */
33 SPI_FLASH_CS_HIGH();
34 }
由于讀取的數據量沒有限制,所以發(fā)送讀命令后一直接收NumByteToRead個數據到結束即可。
3. main函數
最后我們來編寫main函數,進行FLASH芯片讀寫校驗,見代碼清單 2414。
代碼清單 2414 main函數
1 /* 獲取緩沖區(qū)的長度 */
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ā)送緩沖區(qū)初始化 */
13 uint8_t Tx_Buffer[] = "感謝您選用秉火stm32開發(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 * 輸入:無
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 扇區(qū),FLASH寫入前要先擦除 */
54 SPI_FLASH_SectorErase(FLASH_SectorToErase);
55
56 /* 將發(fā)送緩沖區(qū)的數據寫到flash中 */
57 SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress, BufferSize);
58 printf("\r\n寫入的數據為:\r\n%s", Tx_Buffer);
59
60 /* 將剛剛寫入的數據讀出來放到接收緩沖區(qū)中 */
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扇區(qū)(0-4096地址)"專用于本實驗,如非必要,請勿擦除其它地址的內容。如已擦除,可在配套資料里找到"刷外部FLASH內容"程序,根據其說明給FLASH重新寫入出廠內容。
24.4.3 下載驗證
用USB線連接開發(fā)板"USB TO UART"接口跟電腦,在電腦端打開串口調試助手,把編譯好的程序下載到開發(fā)板。在串口調試助手可看到FLASH測試的調試信息。
24.5 每課一問
1. 在SPI外設初始化部分,MISO引腳可以設置為輸入模式嗎?為什么?實際測試現象如何?
2. 嘗試使用FLASH芯片存儲int整型變量,float型浮點變量,編寫程序寫入數據,并讀出校驗。
3. 如果扇區(qū)未經擦除就寫入,會有什么后果?請做實驗驗證。
4. 簡述FLASH存儲器與EEPROM存儲器的區(qū)別。
上一篇:STM32學習之:DMA詳解
下一篇:STM32學習之:IAR中確認某段代碼的執(zhí)行時間
推薦閱讀
史海拾趣
隨著公司規(guī)模的擴大,Coil-Q意識到單靠自身的力量難以應對日益激烈的市場競爭。于是,公司積極尋求與其他企業(yè)的戰(zhàn)略合作,與多家知名電子品牌建立了長期穩(wěn)定的合作關系。通過共享資源、共同研發(fā),Coil-Q不僅拓展了業(yè)務領域,還提高了自身的技術水平和市場競爭力。
1997年,ELESTA繼電器有限公司正式成立,專注于制造符合IEC 61810-3標準的強制導向觸點繼電器。這一戰(zhàn)略舉措進一步鞏固了ELESTA在電子繼電器領域的領先地位,并為公司的長期發(fā)展奠定了堅實的基礎。
2013年,ELESTA公司更名為ELESTA GmbH,這一品牌重塑標志著公司進入了新的發(fā)展階段。同時,ELESTA也開始實施全球化戰(zhàn)略,積極拓展國際市場,通過不斷提升產品質量和服務水平,贏得了全球客戶的信賴和認可。
隨著電子行業(yè)的不斷發(fā)展,Curtis Industries公司意識到技術創(chuàng)新的重要性。公司投入大量資源進行技術研發(fā),成功推出了一系列具有行業(yè)領先水平的產品。其中,公司在某一關鍵領域的突破性技術,不僅解決了行業(yè)內的技術難題,還為公司贏得了廣泛的贊譽。Curtis Industries因此成為了電子行業(yè)的技術創(chuàng)新引領者,推動了整個行業(yè)的發(fā)展。
為了進一步擴大市場份額,Curtis Industries公司積極拓展市場布局。公司在全球范圍內設立了多個生產基地和研發(fā)中心,以便更好地滿足不同地區(qū)客戶的需求。同時,公司還加強了與上下游企業(yè)的合作,形成了完整的產業(yè)鏈。這些舉措不僅提升了公司的產能和研發(fā)實力,還為公司帶來了更多的商業(yè)機會。
為了進一步擴大市場份額,Curtis Industries公司積極拓展市場布局。公司在全球范圍內設立了多個生產基地和研發(fā)中心,以便更好地滿足不同地區(qū)客戶的需求。同時,公司還加強了與上下游企業(yè)的合作,形成了完整的產業(yè)鏈。這些舉措不僅提升了公司的產能和研發(fā)實力,還為公司帶來了更多的商業(yè)機會。
剛剛自學嵌入式,很多問題沒搞清楚,請路過的嵌入式前輩幫幫忙! 在內核移植中,為什么要對NandFlash分區(qū),不是在Vivi移植中已分區(qū),在Vivi階段不是把內核映像和文件系統(tǒng)映像都加載到RAM中?內核在啟動完后應該沒有對Nan ...… 查看全部問答∨ |
|
本公司代理銷售ST的STM32系列MCU 公司地址:廣東深圳市福田區(qū)南園路70號上田大廈4D 公司官網:www.upsd8051.com 聯系方式:MSN:lg_0755@hotmail.com,QQ:45293156; TEL:0755-83248843,E-Mail:hubin@u ...… 查看全部問答∨ |
|
WinCE5.0下的流設備驅動就是一個用戶態(tài)的DLL,我們寫驅動只需要寫一個動態(tài)庫,輸出他需要的函數就可以了。 CE6.0的體系結構據說變化了不少,請問CE6下的驅動也可以這樣寫嗎? … 查看全部問答∨ |
我要用到兩個個16位的ADC,對速度沒有要求,但是要求保證精度。看了stm32的手冊,上邊講是應用過采樣技術將提高ADC的精度。感覺很不錯,這樣我就能省兩個ADC的錢了,降低了不少成本。 我現在 ...… 查看全部問答∨ |
正交編碼器,旋轉編碼器的一種(增量式編碼器),用來確定機械行程的位移量與方向。通過監(jiān)控脈沖數目和兩個脈沖的相對相位,可以跟蹤旋轉的位置,旋轉 和速度。比如電機轉速。LM3S8962包含兩個正交編碼器接口,這對于電機驅動與監(jiān)控足夠,不過編碼 ...… 查看全部問答∨ |
設計資源 培訓 開發(fā)板 精華推薦
- Linux系統(tǒng)編程篇丨迅為IMX6ULL-對應視頻講解
- 嵌入式學習丨4412開發(fā)板-uboot源碼-匯編-源碼分析(一)嵌入式學習丨4412開發(fā)板-uboot源碼-匯
- 迅為IMX6ULL開發(fā)板-主頻和時鐘配置例程(二)
- 迅為IMX6ULL開發(fā)板-主頻和時鐘配置例程
- 迅為IMX6ULL開發(fā)板安裝VMware Tool工具
- i.MX6ULL終結者Debian文件系統(tǒng)的構建i.MX6ULL 移植Debian文件系統(tǒng)
- 迅為i.MX6ULL開發(fā)板按鍵例程編譯及運行
- 迅為-i.MX6開發(fā)板手冊更新-非設備樹uboot-修改默認環(huán)境變量
- 迅為-IMX6ULL-QT應用_在開發(fā)板上移植ssh
- 迅為-IMX6ULL-QT應用_在開發(fā)板上移植ssh
- 迅為-i.MX6ULL開發(fā)板-QT實戰(zhàn)項目DHT11&網絡編程實戰(zhàn)練習(一)
- 迅為-i.MX6ULL開發(fā)板-網絡測試方法(二)
- 迅為-IMX6開發(fā)板設備樹-Linux內核配置兩路can
- 4412開發(fā)板一鍵燒寫QT程序到開發(fā)板-安裝ssh
- 迅為iTOP-i.MX6ULL開發(fā)板-網絡通信-套字節(jié)UDP
- 迅為iTOP-i.MX6ULL開發(fā)板-網絡通信-套字節(jié)TCP
- ROHM推出支持負電壓和高電壓(40V/80V)的高精度電流檢測放大器
- 迅為IMX6開發(fā)板非設備樹源碼編譯環(huán)境搭建(一)
- 迅為i.MX6ULL開發(fā)板Platform設備驅動運行測試