STM32F429入门(二十一):SPI协议及SPI读写FLASH
IIC主要用于通訊速率一般的場合,而SPI一般用于較高速的場合。
一、SPI協議簡介
SPI 協議是由摩托羅拉公司提出的通訊協議(Serial Peripheral Interface),即串行外圍設 備接口,是一種高速全雙工的通信總線。它被廣泛地使用在 ADC、LCD 等設備與 MCU 間, 要求通訊速率較高的場合。
(一)物理層
?
SPI 通訊使用 3 條總線及片選線,3 條總線分別為 SCK、MOSI、MISO,片選線為SS,它們的作用介紹如下:
-
SS:從設備選擇信號線,常稱為片選信號線,也稱為NSS、CS。每個從設備都有獨立的一條SS信號線,本信號線獨占主機的一個引腳,即有多少個從設備,就有多少條片選信號線。IIC協議中通過設備地址來尋址、選中總線上的某個設備并與其進行通訊;而SPI協議中沒有設備地址,它使用SS信號線來尋址,當主機選擇從設備時,把該從設備的SS信號線設置為低電平,該從設備即被選中,即片選有效,接著主機開始與被選中的從設備進行SPI通訊。所以SPI通訊以SS線置低電平為開始信號,以SS線被拉高作為結束信號。
-
SCK:時鐘信號線,用于通訊數據同步。它由通訊主機產生,決定了通訊的速率,不同的設備支持的最高時鐘頻率不一樣。(同步通訊)STM32 的 SPI 時鐘頻率最大為 fpclk/2,兩個設備之間通訊時,通訊速率受限于低速設備。
-
MOSI(Master Output,Slave Input):主設備輸出/從設備輸入引腳。主機的數據從這條信號線輸出,從機由這條信號線讀入主機發送的數據,即這條線上數據的方向為主機到從機。
-
MISO(Master Input,Slave Output):主設備輸入/從設備輸出的引腳。主機從這條信號線讀入數據,從機的數據由這條信號線輸出到主機,即在這條線上數據的方向為從機到主機。
(二)協議層
SPI協議定義了通訊的起始和停止信號、數據有效性、時鐘同步等環節。
(1)SPI基本通訊過程
-
通訊的起始和停止信號
SS信號線由高變低,是SPI通訊的起始信號。NSS是每個從機各自獨占的信號線,當從機檢測到NSS線檢測的起始信號后,就知道被選中了,開始準備與主機通訊。
當信號由低變高,是SPI通訊的停止信號,表示本次通訊結束,從機的選中狀態被取消。
-
數據有效性
SPI使用MOSI及MISO信號線來傳輸數據,使用SCK信號線進行數據同步,MOSI及MISO數據線在SCK的每個時鐘周期傳輸一位數據,且數據輸入輸出是同時進行的。(圖示為下降沿采集數據)
-
CPOL/CPHA及通訊模式
時鐘極性CPOL是指SPI通訊設備處于空閑狀態時,SCK信號線的電平信號(即SPI通訊開始前、NSS線為高電平時SCK的狀態)。CPOL=0時,SCK在空閑狀態時為低電平,CPOL=1時則相反。
時鐘相位CPHA是指數據在采用的時刻,當CPHA=0時,MOSI或MISO數據線上的信號將會在SCK時鐘線的“奇數邊沿”被采樣。當CPHA=1時,數據線在SCK的“偶數邊沿”采樣。(無關上升沿下降沿),下圖為奇數邊沿采樣。
?由CPOL及CPHA的不同狀態,SPI分成了四種模式,主機與從機需要工作在相同的模式下才可以正常通訊,實際中采用較多的是“模式0”與“模式3”。
?
二、STM32的SPI外設架構
?
STM32的SPI外設可用作通訊的主機及從機,支持最高的SCK時鐘 頻率為fpclk/2 (STM32F429型號的芯片默認fpclk1為90MHz,fpclk2為45MHz), 完全支持SPI協議的4種模式,數據幀長度可設置為8位或16位,可設置數據 MSB先行(高位先行,從左往右)或LSB先行(低位先行,從右往左)。它還支持雙線全雙工(前面小節說明的都是這種模式)、 雙線單向以及單線模式。
1-通訊引腳,2-時鐘控制邏輯,3-數據控制邏輯,4-整體控制邏輯。
(1)通訊引腳
其中SPI1\SPI4\SPI5\SPI6是APB2上的設備,最高通訊速率達到45Mbit/s,SPI2\SPI3是APB1上的設備,最高通信速率為22.5Mbit/s。其它功能上沒有差異。SPI2\SPI3引腳上上均有I2S,可用來設置音頻,但是IIS與SPI不可以共用。
(2)時鐘控制邏輯
SCK線的時鐘信號,由波特率發生器根據“控制寄存器CR1”中的BR[0:2]位控制,該位是對fpclk時鐘的分頻因子,對fpclk的分頻結果就是SCK引腳的輸出時鐘頻率。
?其中fpclk頻率是指SPI所在的APB總線頻率,APB1為fpclk1,APB2為fpclk2。為了協調通訊速度比較慢的設備。
(3)數據控制邏輯
SPI的MOSI及MISO都連接到數據移位寄存器上,數據移位寄存器的數據來源于接收緩沖區及發送緩沖區。
-
通過寫SPI的數據寄存器DR把數據填充到發送緩沖區中。
-
通過讀數據寄存器DR,可以獲取接收緩沖區的內容。
-
其中數據幀長度可以通過控制寄存器DR的DFF位配置成8位及16位模式:配置LSBFIRST位可以選擇MSB先行還是LSB先行。
-
SPI 的 MOSI 及 MISO 都連接到數據移位寄存器上,數據移位寄存器的內容來源于接收緩沖區及發送緩沖區以及 MISO、MOSI 線。當向外發送數據的時候,數據移位寄存器以 “發送緩沖區”為數據源,把數據一位一位地通過數據線發送出去;當從外部接收數據的 時候,數據移位寄存器把數據線采樣到的數據一位一位地存儲到“接收緩沖區”中。
?
(4)整體控制邏輯
整體控制邏輯復制協調整個SPI外設。控制邏輯的工作模式根據我們配置的“控制寄 存器(CR1/CR2)”的參數而改變,基本的控制參數包括前面提到的 SPI 模式、波特率、LSB 先行、主從模式、單雙向模式等等。我們可以通過工作狀態寄存器讀取SPI的工作狀態,“狀態寄存器(SR)”。控制邏輯還可以根據要求,負責控制產生SPI中斷信號、DMA請求及控制NSS信號線。在實際的應用中,我們一般不使用SPI外設的標準NSS信號線,而是更簡單地使用普通GPIO,軟件控制它地電平輸出,從而產生通訊起始和停止信號。
(5)通訊過程
?TXE標志代表的是緩沖區是否為空,當TXE為0時,發送緩沖區為非空,若為1時,發送緩沖區為空。當其為空時,也就說明可以準備發送下一個數據。RXNE為接收緩沖區是否為空的標志,其中0代表接收緩沖區為空,1代表接收緩沖區非空。
-
控制NSS信號線,產生起始信號。
-
把要發送的數據寫入到”數據寄存器DR“中,該數據會被存儲到發送緩沖區。
-
通訊開始,SCK時鐘開始運行。MOSI把發送緩沖區中的數據一位一位地傳輸出去;MISO則把數據一位一位地存儲進接收緩沖區中;
-
當發送完一幀數據的時候,”狀態寄存器SR“中的"TXE標志位"會被置1,表示傳輸完一幀,發送緩沖區已空;類似的,當接收完一幀數據的時候,”RXNE標志位“會被置1,表示傳輸完一幀,接收緩沖區非空;
-
等待到”TXE標志位“為1時,若還要繼續發送數據,則再次往”數據寄存器DR“寫入數據即可;等待到”RXENE標志位“為1時,通過讀取”數據寄存器DR“可以獲取接收緩沖區中的內容。
假如使能了TXE或RXNE中斷,TXE或RXNE置1時會產生SPI中斷信號,進入同一個中斷服務函數,到SPI中斷服務程序后,可通過檢查寄存器位來了解是哪一個事件,再分別進行處理。也可以使用DMA方式來收發”數據寄存器DR“中的數據。
需要注意的是CR寄存器中的SSM位:
?當我們讓這個寄存器置1時,我們可以通過軟件來模擬SPI,這也是比較常用的方式。
三、SPI結構體
typedef struct {uint16_t SPI_Direction; /*設置 SPI 的單雙向模式 */uint16_t SPI_Mode; /*設置 SPI 的主/從機端模式 */uint16_t SPI_DataSize; /*設置 SPI 的數據幀長度,可選 8/16 位 */uint16_t SPI_CPOL; /*設置時鐘極性 CPOL,可選高/低電平*/uint16_t SPI_CPHA; /*設置時鐘相位,可選奇/偶數邊沿采樣 */uint16_t SPI_NSS; /*設置 NSS 引腳由 SPI 硬件控制還是軟件控制*/uint16_t SPI_BaudRatePrescaler; /*設置時鐘分頻因子,fpclk/分頻數=fSCK */uint16_t SPI_FirstBit; /*設置 MSB/LSB 先行 */uint16_t SPI_CRCPolynomial; /*設置 CRC 校驗的表達式 */ } SPI_InitTypeDef;-
SPI_Direction:有雙線全雙工、雙線只接收、單線只接收、單線只發送模式。
-
SPI_Mode:主機模式、從機模式。這兩個模式的最大區別是在于時鐘信號線SCK信號線的時序,SCK的時序由通訊中的主機產生。若被設置為從機模式,則要接受外來的SCK信號。
-
SPI_DataSize:可以選擇SPI通訊的數據幀大小為8位或者16位。
-
SPI_CPOL和SPI_CPHA:這兩個成員配置SPI的時鐘極性CPOL和時鐘相位CPHA,這兩個配置影響到SPI的通訊模式。時鐘極性CPOL成員,可以設置為高電平或者為低電平。時鐘相位CPHA成員,可以設置為在SCK奇數邊沿采集數據或者是偶數邊沿。
-
SPI_NSS:可以選擇硬件模式或軟件模式。在硬件模式中的SPI片選信號由SPI硬件自動產生,而軟件模式則需要親自把相應的GPIO端口拉高或者置低產生非片選和片選信號。
-
SPI_BaudRatePrescaler:參數可以設定為2、4、6、8、16、32、64、128、256分頻。
-
SPI_FirstBit:MSB先行(高數據在前)還是LSB先行(低位數據在前)。
-
SPI_CRCPolynomial:適用于比較復雜的環境,這是 SPI 的 CRC 校驗中的多項式,若我們使用 CRC 校驗時,就使用這個成員的參數 (多項式),來計算 CRC 的值。
四、實踐——SPI讀寫串行FLASH
?上面是我們即將改寫的FLASH芯片。容量為16M。NCS引腳也為NSS引腳,DIO為MOSI引腳,DO為MISO引腳。WP為寫保護引腳,低電平有效。HOLD為暫停通訊或結束通訊,用的很少,接為高電平。以下為引腳的連接圖。
?在FLASH中,它一共有0-255即256個塊(Block),每個塊是64KB,16M=64*255/1024。每個塊右分為0-15個扇區(Sector),每個扇區4KB。寫入數據之前,必須要擦除數據,再重新寫入數據,擦除的最小單位為扇區。設備ID為4018H,設備ID可以用來判斷設備是否連接正常,以及設備是否配套正確。擦除整個芯片的命令為:C7h/60h。擦除扇區的命令為20h。此芯片為MSB先行。以上為該芯片手冊中得出。
?
了解這款FLASH后,我們開始進行讀寫。
(1)定義引腳以及時鐘
#define FLASH_SPI SPI5 #define FLASH_SPI_CLK RCC_APB2Periph_SPI5 #define RCC_APB_CLOCK_FUN RCC_APB2PeriphClockCmd#define FLASH_SPI_CS_GPIO_PORT GPIOF #define FLASH_SPI_CS_GPIO_CLK RCC_AHB1Periph_GPIOF #define FLASH_SPI_CS_PIN GPIO_Pin_6#define FLASH_SPI_SCK_GPIO_PORT GPIOF #define FLASH_SPI_SCK_GPIO_CLK RCC_AHB1Periph_GPIOF #define FLASH_SPI_SCK_PIN GPIO_Pin_7 #define FLASH_SPI_SCK_AF GPIO_AF_SPI5 #define FLASH_SPI_SCK_SOURCE GPIO_PinSource7#define FLASH_SPI_MISO_GPIO_PORT GPIOF #define FLASH_SPI_MISO_GPIO_CLK RCC_AHB1Periph_GPIOF #define FLASH_SPI_MISO_PIN GPIO_Pin_8 #define FLASH_SPI_MISO_AF GPIO_AF_SPI5 #define FLASH_SPI_MISO_SOURCE GPIO_PinSource8#define FLASH_SPI_MOSI_GPIO_PORT GPIOF #define FLASH_SPI_MOSI_GPIO_CLK RCC_AHB1Periph_GPIOF #define FLASH_SPI_MOSI_PIN GPIO_Pin_9 #define FLASH_SPI_MOSI_AF GPIO_AF_SPI5 #define FLASH_SPI_MOSI_SOURCE GPIO_PinSource9(2)引腳初始化(復用GPIO)
#define CS_HIGH_DISABLE() GPIO_SetBits(FLASH_SPI_CS_GPIO_PORT,FLASH_SPI_CS_PIN) #define CS_LOW_ENABLE() GPIO_ResetBits(FLASH_SPI_CS_GPIO_PORT,FLASH_SPI_CS_PIN)void FLASH_SPI_Config(void) {GPIO_InitTypeDef GPIO_InitStructure;SPI_InitTypeDef SPI_InitStructure;//1.初始化GPIO RCC_AHB1PeriphClockCmd(FLASH_SPI_CS_GPIO_CLK|FLASH_SPI_SCK_GPIO_CLK|FLASH_SPI_MISO_ GPIO_CLK|FLASH_SPI_MOSI_GPIO_CLK,ENABLE);/* 連接 引腳源*/GPIO_PinAFConfig(FLASH_SPI_SCK_GPIO_PORT,FLASH_SPI_SCK_SOURCE,FLASH_SPI_SCK_AF);/* 連接 */GPIO_PinAFConfig(FLASH_SPI_MISO_GPIO_PORT,FLASH_SPI_MISO_SOURCE,FLASH_SPI_MISO_AF);GPIO_PinAFConfig(FLASH_SPI_MOSI_GPIO_PORT,FLASH_SPI_MOSI_SOURCE,FLASH_SPI_MOSI_AF);/* 使能 SPI 時鐘 */RCC_APB_CLOCK_FUN(FLASH_SPI_CLK, ENABLE);/* GPIO初始化 */GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //復用引腳配置為輸出模式也可以進行輸入GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;/* 配置SCK引腳為復用功能 */GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN ; GPIO_Init(FLASH_SPI_SCK_GPIO_PORT, &GPIO_InitStructure);/* 配置MISO引腳為復用功能 */GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;GPIO_Init(FLASH_SPI_MISO_GPIO_PORT, &GPIO_InitStructure);/* 配置MOSI引腳為復用功能 */GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;GPIO_Init(FLASH_SPI_MOSI_GPIO_PORT, &GPIO_InitStructure);/*CS引腳 */GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; //推挽輸出,本身硬件就有一個上拉/* 配置SCK引腳為復用功能 */GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_PIN ; GPIO_Init(FLASH_SPI_CS_GPIO_PORT, &GPIO_InitStructure);//2.配置SPI工作模式SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; //最快的分頻SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge ; //偶數邊沿SPI_InitStructure.SPI_CPOL = SPI_CPOL_High ; //空閑時SCK時鐘高電平SPI_InitStructure.SPI_CRCPolynomial = 0 ; //不需要使用CRC校驗SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //數據幀SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //雙向SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//高位先行SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//軟件配置SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //主機SPI_Init(FLASH_SPI,&SPI_InitStructure); SPI_Cmd(FLASH_SPI,ENABLE); CS_HIGH_DISABLE(); }為了看初始化是否成功,我們可以獲取設備ID來檢驗。獲取ID的命令為:9Fh。
?
uint32_t Read_Device_ID(void) {uint8_t temp[3];//拉低片選CS_LOW_ENABLE();Read_Write_Byte(JEDEC_ID);temp[0] = Read_Write_Byte(DUMMY); //發送任意字節,產生時序,下面同理temp[1] = Read_Write_Byte(DUMMY);temp[2] = Read_Write_Byte(DUMMY);//拉高片選CS_HIGH_DISABLE();//將數據進行組合return temp[0]<<16|temp[1]<<8|temp[2]; }?以上的命令可以使用宏定義來方便使用:
#define DUMMY 0xFF //任意值 #define JEDEC_ID 0x9F //ID #define ERACE_SECTOR 0x20 //擦除扇區 #define READ_DATA 0x03 //讀取數據 #define READ_STATUS 0x05 //空閑 #define WRITE_ENABLE 0x06 //寫使能 #define PAGE_PROGRAM 0x02 //寫入的地址為了防止時鐘頻率錯誤響應,我們需要檢驗標志位,取決于我們什么時候讀,什么時候寫:
//先發送再接收才會產生時序,一定要注意!!否則STM32不會產生時序,產生一下后就停止,只接受到了地址的數據 uint8_t Read_Write_Byte(uint8_t data) {time_out = SPI_FLAG_TIMEOUT;while(SPI_GetFlagStatus(FLASH_SPI,SPI_FLAG_TXE) == RESET){if((time_out--)==0) return SPI_TIMEOUT_UserCallback(0);}//發送緩沖區為空SPI_I2S_SendData(FLASH_SPI,data);time_out = SPI_FLAG_TIMEOUT;//接收緩沖區為空,死循環while(SPI_GetFlagStatus(FLASH_SPI,SPI_FLAG_RXNE) == RESET){if((time_out--)==0) return SPI_TIMEOUT_UserCallback(1);}return SPI_I2S_ReceiveData (FLASH_SPI); }?其中的變量以及報錯函數定義:
/*等待超時時間*/ #define SPI_FLAG_TIMEOUT ((uint32_t)0x1000) #define SPI_LONG_TIMEOUT ((uint32_t)(10 * SPI_FLAG_TIMEOUT))/*信息輸出*/ #define FLASH_DEBUG_ON 0#define FLASH_INFO(fmt,arg...) printf("<<-FLASH-INFO->> "fmt"\n",##arg) #define FLASH_ERROR(fmt,arg...) printf("<<-FLASH-ERROR->> "fmt"\n",##arg) #define FLASH_DEBUG(fmt,arg...) do{\if(FLASH_DEBUG_ON)\printf("<<-FLASH-DEBUG->>[%s] [%d]"fmt"\n",__FILE__,__LINE__, ##arg);\}while(0) static uint8_t SPI_TIMEOUT_UserCallback(uint8_t errorCode) {FLASH_ERROR("SPI 等待超時!errorCode = %d",errorCode); return 0xFF; }為了保持在運行的過程中復位不會因為掉電而亂發數據,我們需要控制Release_Power_Down,稍后會進行補充。
(3)編寫FLASH讀寫過程
//擦除過后扇區內所有的數據都應為1 void erace_setor(uint32_t addr) {//在擦除之前必須寫使能Write_Enable();Wait_for_Ready();//拉低片選CS_LOW_ENABLE();Read_Write_Byte(ERACE_SECTOR);//一次能發送24bitRead_Write_Byte((addr>>16)&0xFF);Read_Write_Byte((addr>>8)&0xFF);Read_Write_Byte(addr&0xFF); //拉高片選CS_HIGH_DISABLE();//等待內部時序(等待擦除完成) } //寫使能函數 void Write_Enable(void) {//拉低片選CS_LOW_ENABLE();Read_Write_Byte(WRITE_ENABLE); //拉高片選CS_HIGH_DISABLE(); }在讀取的過程中,我們需要得知狀態,看它是否空閑后再寫入數據、擦除數據、讀取數據,這個函數需要在拉低片選前使用:
void Wait_for_Ready(void) {uint8_t reg_status=0x01;while(reg_status &0x01){//拉低片選CS_LOW_ENABLE();//讀狀態寄存器Read_Write_Byte(READ_STATUS);reg_status = Read_Write_Byte(DUMMY); //拉高片選CS_HIGH_DISABLE(); }}讀取數據的函數如下(整塊數據而非單個):
void Read_buffer(uint8_t* pdata,uint32_t addr,uint32_t numByteTorRead) {Wait_for_Ready();//拉低片選CS_LOW_ENABLE();Read_Write_Byte(READ_DATA);Read_Write_Byte((addr>>16)&0xFF);Read_Write_Byte((addr>>8)&0xFF);Read_Write_Byte(addr&0xFF); while(numByteTorRead--){*pdata = Read_Write_Byte(DUMMY);pdata++;}//拉高片選CS_HIGH_DISABLE();}完成了讀數據,接下來是寫入數據,最多寫入256個數據:
void Write_buffer(uint8_t* pdata,uint32_t addr,uint32_t numByteTorWrite) {Write_Enable();Wait_for_Ready();//拉低片選CS_LOW_ENABLE();Read_Write_Byte(PAGE_PROGRAM);Read_Write_Byte((addr>>16)&0xFF);Read_Write_Byte((addr>>8)&0xFF);Read_Write_Byte(addr&0xFF); while(numByteTorWrite--){Read_Write_Byte(*pdata);pdata++;}//拉高片選CS_HIGH_DISABLE();}主函數:
uint8_t readBuff[4096] = {0x0}; uint8_t writeBuff[256] = {0x0}; int main(void) { uint32_t device_id = 0;uint32_t i=0;/*初始化USART 配置模式為 115200 8-N-1,中斷接收*/Debug_USART_Config();FLASH_SPI_Config();/* 發送一個字符串 */Usart_SendString( DEBUG_USART,"這是一個FLASH實驗\n");printf("這是一個FLASH實驗\n");device_id = Read_Device_ID();printf("device_id =0x%x",device_id);erace_setor(0x00);//FLASH先擦除后寫入 //讀出擦除后的數據Read_buffer(readBuff,0x00,4096); printf("\r\n*************讀出擦除后的數據**********\r\n");for(i=0;i<4096;i++)printf("0x%x ",readBuff[i]);for(i=0;i<256;i++)writeBuff[i] = i;Write_buffer(writeBuff,0x00,256);//讀出擦除后的數據Read_buffer(readBuff,0x00,256); printf("\r\n*************讀出寫入后的數據**********\r\n");for(i=0;i<256;i++)printf("0x%x ",readBuff[i]);while(1){ } }五、看庫理清思路
下面的代碼是已經進行初始化過后,使能NSS引腳后的操作:
(1)使用SPI發送和接收一個數據
/* * @brief 使用SPI發送一個字節的數據 * @param byte:要發送的數據 * @retval 返回接收到的數據 */ u8 SPI_FLASH_SendByte(u8 byte) {SPITimeout = SPIT_FLAG_TIMEOUT;/* 等待發送緩沖區為空,TXE事件 */while (SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_TXE) == RESET){if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);}/* 寫入數據寄存器,把要寫入的數據寫入發送緩沖區 */SPI_I2S_SendData(FLASH_SPI, byte);SPITimeout = SPIT_FLAG_TIMEOUT;/* 等待接收緩沖區非空,RXNE事件 */while (SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_RXNE) == RESET){if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);}/* 讀取數據寄存器,獲取接收緩沖區數據 */return SPI_I2S_ReceiveData(FLASH_SPI); }//當使用SPI進行讀取時,我們需要先寫入,再讀出/* * @brief 使用SPI讀取一個字節的數據 * @param 無 * @retval 返回接收到的數據 */ u8 SPI_FLASH_ReadByte(void) {return (SPI_FLASH_SendByte(Dummy_Byte)); }-
函數u8 SPI_FLASH_SendByte(u8 byte)實現了SPI的通訊過程。
-
上面兩個函數都不包含SPI的起始和停止信號,只是收發的主要過程,所以以上兩個函數都是拿來調用的,前后需要做好起始和停止信號的操作。
-
通過檢測TXE,獲取發送緩沖區狀態,若發送緩沖區為空,則說明上一個數據已發送完畢,若不為空,則等待其為空后,再調用庫函數SPI_I2S_SendData把要發送的數據寫入到數據寄存器DR,寫入SPI的數據會存儲到發送緩沖區,由SPI外設發送出去。從這一點就可以說明,當你想要讀取數據時,必須先寫入,后再讀出。
-
寫入完畢后,等待RXNE事件,即接收緩沖區非空事件。由于 SPI 雙線全雙工模式下 MOSI 與 MISO 數據傳輸是同步的,當接收緩沖區非空時,表示上面的數據發送完畢,且接收緩沖區也收到新的數據。
-
等待至接收緩沖區非空時,通過調用庫函數SPI_I2S_ReceiveData讀取寄存器DR中的數據,最后將其return。
-
最后看一下讀取一個字節數據,它只是簡單地調用了一個任意值Dummy_Byte,然后獲取返回值,其實發送值是什么無關緊要,然后獲取其返回值。SPI接收過程和發送過程實質是一樣的,收發同時進行,關鍵在于上層應用關注的是接收還是發送。
(2)寫使能以及讀取當前的狀態
/* * @brief 向FLASH發送 寫使能 命令 * @param none * @retval none */ void SPI_FLASH_WriteEnable(void) {/* 通訊開始:CS低 */SPI_FLASH_CS_LOW();/* 發送寫使能命令*/SPI_FLASH_SendByte(W25X_WriteEnable);/*通訊結束:CS高 */SPI_FLASH_CS_HIGH(); }FLASH芯片向內部存儲矩陣寫入數據需要消耗一定的時間,并不是在總線通訊結束的一瞬間完成的,所以需要檢驗FLASH是否空閑。FLASH芯片定義了一個狀態寄存器:
我們需要關注這個狀態寄存器的第0位BUSY是否為1,表明FLASH芯片處于忙碌狀態,也就是說這個時候它可能在進行擦除或者寫入的操作。利用指令表中的“Read Status Register”指令可以獲取FLASH芯片寄存器的內容。并校驗第0位,判斷當前是否可以寫入。判斷函數如下:
/* * @brief 等待WIP(BUSY)標志被置0,即等待到FLASH內部數據寫入完畢 * @param none * @retval none */ void SPI_FLASH_WaitForWriteEnd(void) {u8 FLASH_Status = 0;/* 選擇 FLASH: CS 低 */SPI_FLASH_CS_LOW();/* 發送 讀狀態寄存器 命令 */SPI_FLASH_SendByte(W25X_ReadStatusReg);SPITimeout = SPIT_FLAG_TIMEOUT;/* 若FLASH忙碌,則等待 */do{/* 讀取FLASH芯片的狀態寄存器 */FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte); {if((SPITimeout--) == 0) {SPI_TIMEOUT_UserCallback(4);return;}} }while ((FLASH_Status & WIP_Flag) == SET); /* 正在寫入標志 *//* 停止信號 FLASH: CS 高 */SPI_FLASH_CS_HIGH(); }(3)FLASH扇區擦除
FLASH的存儲特性:由于 FLASH 存儲器的特性決定了它只能把原來為“1”的數據位改寫成“0”,而原 來為“0”的數據位不能直接改寫為“1”。所以這里涉及到數據“擦除”的概念,在寫入 前,必須要對目標存儲矩陣進行擦除操作,把矩陣中的數據位擦除為“1”,在數據寫入的 時候,如果要存儲數據“1”,那就不修改存儲矩陣 ,在要存儲數據“0”時,才更改該位。
擦除有以下分類:扇區擦除(Sector Erase)、塊擦除(Block Erase)、整片擦除(Chip Erase)
扇區擦除指令的第一個字節為指令編碼,緊接著發送的 4 個字節用于表示要擦除的存儲矩陣地址。要注意的是在扇區擦除指令前,還需要先發送“寫使能”指令,發送扇區擦除指令后,通過讀取寄存器狀態等待扇區擦除操作完畢。注意發送擦除地址時高位在前即可。
void SPI_FLASH_SectorErase(u32 SectorAddr) {/* 發送FLASH寫使能命令 */SPI_FLASH_WriteEnable();SPI_FLASH_WaitForWriteEnd();/* 擦除扇區 *//* 選擇FLASH: CS低電平 */SPI_FLASH_CS_LOW();/* 發送扇區擦除指令*/SPI_FLASH_SendByte(W25X_SectorErase);/*發送擦除扇區地址的高8位*/SPI_FLASH_SendByte((SectorAddr & 0xFF000000) >> 24);/*發送擦除扇區地址的中前8位*/SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);/* 發送擦除扇區地址的中后8位 */SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);/* 發送擦除扇區地址的低8位 */SPI_FLASH_SendByte(SectorAddr & 0xFF);/* 停止信號 FLASH: CS 高電平 */SPI_FLASH_CS_HIGH();/* 等待擦除完畢*/SPI_FLASH_WaitForWriteEnd(); }(4)FLASH的頁寫入
FLASH的頁寫入命令最多一次可以傳輸256個字節數據,這個單位也是頁大小。FLASH頁寫入的時序如圖:
?
從時序圖可知,第 1 個字節為“頁寫入指令”編碼,24 字節為要寫入的“地址 A”, 接著的是要寫入的內容,最多個可以發送 256 字節數據,這些數據將會從“地址 A”開始, 按順序寫入到 FLASH 的存儲矩陣。若發送的數據超出 256 個,則會覆蓋前面發送的數據。
與擦除指令不一樣,頁寫入指令的地址并不要求按 256 字節對齊,只要確認目標存儲 單元是擦除狀態即可(即被擦除后沒有被寫入過)。所以,若對“地址 x”執行頁寫入指令后, 發送了 200 個字節數據后終止通訊,下一次再執行頁寫入指令,從“地址(x+200)”開始寫 入 200 個字節也是沒有問題的(小于 256 均可)。
/* * @brief 對FLASH按頁寫入數據,調用本函數寫入數據前需要先擦除扇區 * @param pBuffer,要寫入數據的指針 * @param WriteAddr,寫入地址 * @param NumByteToWrite,寫入數據長度,必須小于等于SPI_FLASH_PerWritePageSize * @retval 無 */ void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u32 NumByteToWrite) {/* 發送FLASH寫使能命令 */SPI_FLASH_WriteEnable();/* 選擇FLASH: CS低電平 */SPI_FLASH_CS_LOW();/* 寫頁寫指令*/SPI_FLASH_SendByte(W25X_PageProgram);/*發送寫地址的高8位*/SPI_FLASH_SendByte((WriteAddr & 0xFF000000) >> 24);/*發送寫地址的中前8位*/SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);/*發送寫地址的中后8位*/SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);/*發送寫地址的低8位*/SPI_FLASH_SendByte(WriteAddr & 0xFF);if(NumByteToWrite > SPI_FLASH_PerWritePageSize){NumByteToWrite = SPI_FLASH_PerWritePageSize;FLASH_ERROR("SPI_FLASH_PageWrite too large!");}/* 寫入數據*/while (NumByteToWrite--){/* 發送當前要寫入的字節數據 */SPI_FLASH_SendByte(*pBuffer);/* 指向下一字節數據 */pBuffer++;}/* 停止信號 FLASH: CS 高電平 */SPI_FLASH_CS_HIGH();/* 等待寫入完畢*/SPI_FLASH_WaitForWriteEnd(); }先發送“寫使能”命令,接著才開始頁寫入時序,然后發送指令 編碼、地址,再把要寫入的數據一個接一個地發送出去,發送完后結束通訊,檢查 FLASH 狀態寄存器,等待 FLASH 內部寫入結束。
當我們有不定量數據寫入時,大于256時,可以用下面的函數:
/*** @brief 對FLASH寫入數據,調用本函數寫入數據前需要先擦除扇區* @param pBuffer,要寫入數據的指針* @param WriteAddr,寫入地址* @param NumByteToWrite,寫入數據長度* @retval 無*/ void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u32 NumByteToWrite) {u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;/*mod運算求余,若writeAddr是SPI_FLASH_PageSize整數倍,運算結果Addr值為0*/Addr = WriteAddr % SPI_FLASH_PageSize;/*差count個數據值,剛好可以對齊到頁地址*/count = SPI_FLASH_PageSize - Addr; /*計算出要寫多少整數頁*/NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;/*mod運算求余,計算出剩余不滿一頁的字節數*/NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;/* Addr=0,則WriteAddr 剛好按頁對齊 aligned */if (Addr == 0) {/* NumByteToWrite < SPI_FLASH_PageSize */if (NumOfPage == 0) {SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);}else /* NumByteToWrite > SPI_FLASH_PageSize */{/*先把整數頁都寫了*/while (NumOfPage--){SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);WriteAddr += SPI_FLASH_PageSize;pBuffer += SPI_FLASH_PageSize;}/*若有多余的不滿一頁的數據,把它寫完*/SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);}}/* 若地址與 SPI_FLASH_PageSize 不對齊 */else {/* NumByteToWrite < SPI_FLASH_PageSize */if (NumOfPage == 0) {/*當前頁剩余的count個位置比NumOfSingle小,寫不完*/if (NumOfSingle > count) {temp = NumOfSingle - count;/*先寫滿當前頁*/SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);WriteAddr += count;pBuffer += count;/*再寫剩余的數據*/SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);}else /*當前頁剩余的count個位置能寫完NumOfSingle個數據*/{ SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);}}else /* NumByteToWrite > SPI_FLASH_PageSize */{/*地址不對齊多出的count分開處理,不加入這個運算*/NumByteToWrite -= count;NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);WriteAddr += count;pBuffer += count;/*把整數頁都寫了*/while (NumOfPage--){SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);WriteAddr += SPI_FLASH_PageSize;pBuffer += SPI_FLASH_PageSize;}/*若有多余的不滿一頁的數據,把它寫完*/if (NumOfSingle != 0){SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);}}} }(5)從FLASH讀取數據
相對于寫入,FLASH 芯片的數據讀取要簡單得多,使用讀取指令“Read Data”即可。
發送了指令編碼及要讀的起始地址后,FLASH 芯片就會按地址遞增的方式返回存儲矩 陣的內容,讀取的數據量沒有限制,只要沒有停止通訊,FLASH 芯片就會一直返回數據。
/* * @brief 讀取FLASH數據 * @param pBuffer,存儲讀出數據的指針 * @param ReadAddr,讀取地址 * @param NumByteToRead,讀取數據長度 * @retval 無 */ void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u32 NumByteToRead) {/* 選擇FLASH: CS低電平 */SPI_FLASH_CS_LOW();/* 發送 讀 指令 */SPI_FLASH_SendByte(W25X_ReadData);/* 發送 讀 地址高8位 */SPI_FLASH_SendByte((ReadAddr & 0xFF000000) >> 24);/* 發送 讀 地址中前8位 */SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);/* 發送 讀 地址中后8位 */SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);/* 發送 讀 地址低8位 */SPI_FLASH_SendByte(ReadAddr & 0xFF);/* 讀取數據 */while (NumByteToRead--){/* 讀取一個字節*/*pBuffer = SPI_FLASH_SendByte(Dummy_Byte);/* 指向下一個字節緩沖區 */pBuffer++;}/* 停止信號 FLASH: CS 高電平 */SPI_FLASH_CS_HIGH(); }?六、FLASH存儲小數和整數
需要注意的是,存儲各種數據類型的時候,我們需要將不同的數據類型分在不同的扇區,不可以混著,主要原因是下面這個動態存儲,因為不同的字節數存儲的方式不一樣,比如說,存儲整數,我們將整數通過十六進制傳入,占兩個字節,當我們需要讀取時,四個字節四個字節的讀,即合為一個整數,若這個時候我們用浮點數的方式來運算,它就為八個字節八個字節的讀,會出錯。所以,當我們存儲不管是浮點數還是整數,存儲方式都一樣,可是你想讀出來的時候,你就應該區分他們之間的區別,不可以混為一談,怎么讀數據,還是取決于上位機的處理。
/*寫入小數數據到第一頁*/ SPI_FLASH_BufferWrite((void*)double_buffer, SPI_FLASH_PageSize*1, sizeof(double_buffer)); /*寫入整數數據到第二頁*/ SPI_FLASH_BufferWrite((void*)int_bufffer, SPI_FLASH_PageSize*2, sizeof(int_bufffer));?SPI協議初步就學習到這啦,國慶也就結束了,好像任務量也沒有完成很多,接下來也要忙比賽啦,希望自己再接再厲。
?
總結
以上是生活随笔為你收集整理的STM32F429入门(二十一):SPI协议及SPI读写FLASH的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: emwin从外部flash中读取bmp图
- 下一篇: 不用再为下载而发愁了,提供一款支持115