STM32SPIFLASH讀寫

T7H發表於2024-02-29

STM32SPIFLASH讀寫

1.1 SPI注意事項

SPI是同步通訊,即通訊雙方每次資訊互動必會帶有一問一答,這代表在正常的單核MCU(例如STM32)中很難實現軟體模擬的雙向SPI通訊(TFT螢幕一類的外設不算,那些頂多屬於單向SPI),因為無法同時傳送和接收資料。而在STM32中,硬體實現同步通訊的辦法是利用硬體緩衝區,以位元組為單位,每次傳送一個位元組的資料,接收緩衝區就會快取一個位元組的接收資料,如此實現同時接收和傳送。

1.2 SPI程式碼編寫

SPI的程式碼需要引用如下的標準庫標頭檔案:

#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_spi.h"

其中rcc標頭檔案用於外設時鐘的初始化,gpio標頭檔案用於GPIO口的初始化,spi標頭檔案用於SPI外設的初始化。

1.1.1 初始化結構體與定義變數

//定義SPI裝置
#define FLASH_SPI 					    SPI1                                
//定義CS引腳		
#define FLASH_SPI_CS_PORT 				GPIOC
#define FLASH_SPI_CS_Pin 				GPIO_Pin_0
//定義SCK引腳		
#define FLASH_SPI_SCK_PORT 				GPIOA
#define FLASH_SPI_SCK_Pin 				GPIO_Pin_5
//定義MISO引腳		
#define FLASH_SPI_MISO_PORT 			GPIOA
#define FLASH_SPI_MISO_Pin 				GPIO_Pin_6
//定義MOSI引腳		
#define FLASH_SPI_MOSI_PORT 			GPIOA
#define FLASH_SPI_MOSI_Pin 				GPIO_Pin_7
//定義CS引腳控制函式
#define SPI_FLASH_CS_High() 			GPIO_SetBits(FLASH_SPI_CS_PORT,FLASH_SPI_CS_Pin)
#define SPI_FLASH_CS_Low() 				GPIO_ResetBits(FLASH_SPI_CS_PORT,FLASH_SPI_CS_Pin)
//定義無意義位元組,用於擠佔同步通訊以讀取資料
#define Dummy_Byte 								0xFF

void SPI_FLASH_Init()                                       //SPI初始化函式
{
	SPI_InitTypeDef FLASH_InitStructure;                    //定義SPI初始化結構體
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);     //使能SPI1時鐘
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);    //使能GPIOA時鐘
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);    //使能GPIOC時鐘
	
	GPIO_InitTypeDef GPIO_InitStructure;                    //定義GPIO初始化結構體
	GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_Pin;         //定義CS引腳
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;        //定義CS引腳為推輓輸出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;       //定義CS引腳速度為50MHz
	GPIO_Init(FLASH_SPI_CS_PORT,&GPIO_InitStructure);       //初始化CS引腳
	
	GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_Pin;        //定義SCK引腳
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;         //定義SCK引腳為複用推輓輸出
	GPIO_Init(FLASH_SPI_SCK_PORT,&GPIO_InitStructure);      //初始化SCK引腳
	
	GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_Pin;       //定義MOSI引腳
	GPIO_Init(FLASH_SPI_MOSI_PORT,&GPIO_InitStructure);     //初始化MOSI引腳
	
	GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_Pin;       //定義MISO引腳
	GPIO_Init(FLASH_SPI_MISO_PORT,&GPIO_InitStructure);     //初始化MISO引腳
	
	SPI_FLASH_CS_High();                                    //CS引腳開啟後最好將其置高以釋放CS片選線
	
	FLASH_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;    //定義SPI為雙線全雙工模式
	FLASH_InitStructure.SPI_Mode = SPI_Mode_Master;         //定義SPI為主模式
	FLASH_InitStructure.SPI_DataSize = SPI_DataSize_8b;     //定義SPI資料大小為8位
	FLASH_InitStructure.SPI_CPOL = SPI_CPOL_High;           //定義時鐘極性為高電平
	FLASH_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;          //定義時鐘相位為第二個時鐘邊沿
	FLASH_InitStructure.SPI_NSS = SPI_NSS_Soft;             //定義NSS訊號由軟體控制
	FLASH_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;    //定義波特率預分頻為4
	FLASH_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;    //定義資料傳輸從MSB位開始
	FLASH_InitStructure.SPI_CRCPolynomial = 7;              //定義CRC多項式為7
	SPI_Init(FLASH_SPI,&FLASH_InitStructure);               //初始化SPI
	
	SPI_Cmd(FLASH_SPI,ENABLE);
}

其中SPI外設的主要引數為SPI_Direction、SPI_Mode、SPI_DataSize、SPI_NSS、SPI_BaudRatePrescaler、SPI_FirstBit。SPI_Direction決定了SPI外設的工作模式,可設定為雙線全雙工、雙線只接收、單線只接收、單線只傳送四個模式。SPI_Mode決定了SPI外設的工作模式,可設定為主模式、從模式。SPI_DataSize決定了每一幀資料幀的長度。SPI_NSS決定了NSS訊號的來源,NSS訊號即為CS訊號,可設定為軟體控制、硬體控制。SPI_BaudRatePrescaler決定了波特率預分頻,可設定為主頻的2、4、6、8、16、128、256分頻。SPI_FirstBit決定了資料傳輸的起始位,可設定為MSB位或LSB位,即資料從左向右讀取與從右向左讀取。
其餘引數也很重要,但若只是使用的話並沒有上面那些起決定性作用。其中SPI_CPOL與SPI_CPHA決定了資料的讀取模式,SPI_CPOL設定時鐘訊號為高電平有效或低電平有效,SPI_CPHA設定時鐘相位為第一個時鐘邊沿或第二個時鐘邊沿有效。SPI_CRCPolynomial決定了CRC校驗的中的多項式。

1.1.2 SPI傳送與接收函式

uint8_t SPI_FLASH_SendRecive(uint8_t byte)      //SPI傳送接收函式
{
	while(SPI_I2S_GetFlagStatus(FLASH_SPI,SPI_I2S_FLAG_TXE) == RESET);      //等待傳送緩衝區為空
	SPI_I2S_SendData(FLASH_SPI,byte);           //傳送一個位元組
	while(SPI_I2S_GetFlagStatus(FLASH_SPI,SPI_I2S_FLAG_RXNE) == RESET);     //等待接收緩衝區非空
	return SPI_I2S_ReceiveData(FLASH_SPI);      //從接收緩衝區讀取一個位元組
}

傳送與接收函式沒什麼好說的,就單純的等待SPI外設暫存器狀態,然後傳送或接收資料。不過這點與IIC不同的是,傳送與接收無法分開,必須要同步進行。

2.1 FLASH注意事項

我所使用的開發板為野火STM32F103VET6開發板,其板載SPI FLASH晶片為W25Q64,若板載SPI FLASH晶片不同,則指令與扇區大小也不同,需檢視手冊確定。
在W25Q64中,有128個塊(BLOCK),每個塊有16個扇區(SECTOR),每個扇區有64個頁(PAGE)。塊 = 64KB,扇區 = 4KB,頁 = 256B。每次擦除FLASH最小為整個扇區擦除,每次寫入FLASH最大不超過一頁,即256個位元組。

2.2 FLASH程式碼編寫

FLASH的標頭檔案引用需要如下標頭檔案:

#include "Spi.h"

Spi標頭檔案內為文章上述SPI相關的程式碼。

2.2.1 定義變數與初始化

W25Q64為自帶微型處理器的裝置,STM32作為主裝置若利用W25Q64讀取與寫入資料,需要對應其指令表進行操作,下列程式碼為W25Q64的指令定義:

#define W25X_WriteEnable 				0x06    //寫使能指令
#define W25X_WriteDisable 				0x04    //寫失能指令
#define W25X_ReadStatusReg 				0x05    //讀暫存器指令
#define W25X_WriteStatusReg 			0x01    //寫暫存器指令
#define W25X_ReadData					0x03    //讀資料指令
#define W25X_FastReadData				0x0B    //快速讀取資料指令
#define W25X_FastReadDual				0x3B    //快速讀取兩倍資料指令
#define W25X_PageProgram				0x02    //頁程式設計指令
#define W25X_BlockErase 				0xD8    //塊擦除指令
#define W25X_SectorErase				0x20    //扇區擦除指令
#define W25X_ChipErase 					0xC7    //整片擦除指令
#define W25X_PowerDown					0xB9    //掉電模式指令
#define W25X_ReleasePowerDown 		    0xAB    //喚醒模式指令
#define W25X_DeviceID					0xAB    //裝置ID指令
#define W25X_ManufactDeviceID			0x90    //生產ID指令
#define W25X_JedecDeviceID				0x9F    //JEDEC裝置ID指令
#define sFlash_ID						0xEF4017    //JEDEC裝置ID

#define WIP_Flag						0x01    //裝置忙標誌位
#define FLASH_Page_Size				    0x1000//定義FLASH每頁的大小,4K = 4096 = 0x1000

2.2.2 W25Q64通訊驗證

當STM32透過SPI向W25Q64傳送W25X_JedecDeviceID命令時,W25Q64會返回一個24位的JEDECID,可透過此ID確定通訊是否成功。

uint8_t FLASH_Device_Init(void)                         //初始化FLASH裝置
{
	uint32_t temp1 = 0,temp2 = 0,temp3 = 0;             //定義三個臨時變數
	uint8_t status = 0;                                 //定義狀態變數
	SPI_FLASH_CS_Low();                                 //拉低CS片選線,選中FLASH
	SPI_FLASH_SendRecive(W25X_JedecDeviceID);           //傳送W25X_JedecDeviceID命令
	temp1 = SPI_FLASH_SendRecive(Dummy_Byte);           //接收一個位元組
	temp2 = SPI_FLASH_SendRecive(Dummy_Byte);           //接收一個位元組
	temp3 = SPI_FLASH_SendRecive(Dummy_Byte);           //接收一個位元組
	SPI_FLASH_CS_High();                                //拉高CS片選線,取消選中FLASH
	if( ((temp1<<16)|(temp2<<8)|temp3) == sFlash_ID)    //比較接收到的JEDECID與預設的JEDECID
	{
		status = 0x01;                                  //通訊成功
		return status;                                  //返回通訊狀態
	}
	else
	{
		return 0;                                       //通訊失敗
	}
}

2.2.2 寫入使能及防寫檢測

W25Q64具有嚴格的防寫機制,若想要向其寫入資料,必須保證寫使能,且寫入的頁面必須為擦除後的頁面。而且每次對FLASH內容進行修改後,W25Q64硬體會自動寫失能,會觸發寫失能的操作有“寫狀態暫存器”、“頁程式設計”、“扇區擦除”、“塊區擦除”、“晶片擦除”。要想開啟寫使能,只需向W25Q64傳送W25X_WriteEnable命令後釋放CS片選線即可。

void FLASH_WriteEnable(void)				//寫入使能
{
	SPI_FLASH_CS_Low();						//拉低CS片選線,選中FLASH
	SPI_FLASH_SendRecive(W25X_WriteEnable);	//傳送W25X_WriteEnable命令
	SPI_FLASH_CS_High();					//拉高CS片選線,取消選中FLASH
}

W25Q64對於自身的狀態有一個8位的暫存器專門存放,當主裝置向W25Q64傳送狀態暫存器查詢時,不論此時W25Q64處於什麼狀態,都會將8位的狀態暫存器返回給主裝置。而這8位的狀態暫存器中,第0位為防寫位,若該位為1,則表示W25Q64處於防寫狀態,此時主裝置無法向其寫入資料。可以在此時寫一個死迴圈重複讀取其狀態暫存器,直到該位為0,在進行後續操作。

void FLASH_WaitForWriteEnd(void)				//等待寫入結束
{
	uint8_t FLASH_Status = 0xff;				//初始化狀態變數
	SPI_FLASH_CS_Low();							//拉低CS片選線,選中FLASH
	SPI_FLASH_SendRecive(W25X_ReadStatusReg);	//傳送W25X_ReadStatusReg命令
	while((FLASH_Status & WIP_Flag) == SET)		//若寫入標誌位為1,則表示寫入未完成
	{
		FLASH_Status = SPI_FLASH_SendRecive(Dummy_Byte);	//傳送佔位位元組,接收狀態暫存器
	}
	SPI_FLASH_CS_High();						//拉高CS片選線,取消選中FLASH
}

2.2.3 FLASH扇區擦除

在前文中提到過,W25Q64的最小擦除單位位扇區,每個扇區的大小為4KB。若想要擦除W25Q64中的資料,只需向其傳送W25X_SectorErase命令,再將其24位的地址傳送給W25Q64即可。

void FLASH_SecortErase(uint32_t addr)				//扇區擦除
{
	FLASH_WaitForWriteEnd();						//等待寫入結束
	FLASH_WriteEnable();							//寫入使能
	FLASH_WaitForWriteEnd();						//等待寫入結束
	SPI_FLASH_CS_Low();								//拉低CS片選線,選中FLASH
	SPI_FLASH_SendRecive(W25X_SectorErase);			//傳送W25X_SectorErase命令
	SPI_FLASH_SendRecive((addr & 0xFF0000) >>16);	//傳送24位地址中的高8位
	SPI_FLASH_SendRecive((addr & 0xFF00)>>8);		//傳送24位地址中的中間8位
	SPI_FLASH_SendRecive(addr & 0xFF);				//傳送24位地址中的低8位
	SPI_FLASH_CS_High();							//拉高CS片選線,取消選中FLASH
	FLASH_WaitForWriteEnd();						//等待寫入結束	
}

2.2.4 FLASH頁寫入

W25Q64的最小寫入單位為頁,每個頁的大小為256位元組。若想要向W25Q64中寫入資料,只需向其傳送W25X_PageProgram命令,再將其24位的地址傳送給W25Q64,最後傳送要寫入的資料即可。但是有個限制,每次寫入的資料大小不能超過256位元組,即不能超過一頁,超過一頁就需要重新等待寫入結束,寫使能,傳送寫指令與地址。

void FLASH_Write(uint8_t* databuff,uint32_t addr,uint32_t data_length)	//頁寫入
{
	FLASH_WaitForWriteEnd();						//等待寫入結束
	FLASH_WriteEnable();							//寫入使能
	FLASH_WaitForWriteEnd();						//等待寫入結束
	SPI_FLASH_CS_Low();								//拉低CS片選線,選中FLASH
	SPI_FLASH_SendRecive(0x02);						//傳送W25X_PageProgram命令
	SPI_FLASH_SendRecive((addr & 0xFF0000) >>16);	//傳送24位地址中的高8位
	SPI_FLASH_SendRecive((addr & 0xFF00)>>8);		//傳送24位地址中的中間8位
	SPI_FLASH_SendRecive(addr & 0xFF);				//傳送24位地址中的低8位
	while(data_length--)							//迴圈傳送資料
	{
		SPI_FLASH_SendRecive(*databuff);			//傳送一個位元組的資料
		databuff++;									//指標後移
	}
	SPI_FLASH_CS_High();							//拉高CS片選線,取消選中FLASH
	FLASH_WaitForWriteEnd();						//等待寫入結束
}

2.2.5 FLASH不定頁寫入

不定頁寫入與頁寫入類似,區別在於不定頁寫入每次寫入的資料大小透過處理將其劃分為每一頁依次寫入,即可以呼叫該函式寫入任意長度的資料。

void FLASH_PageWrite(uint8_t* databuff,uint32_t addr,uint32_t data_length)	//不定頁寫入
{
	uint32_t page_count,page_other,i,addr_start,addr_other;	//定義變數
	addr_start = addr % 256;				//計算地址的起始位置
	addr_other = 256 - addr_start;				//計算地址的剩餘位置
	if(addr_start == 0)						//如果地址的起始位置為0
	{
		if(data_length >= 256)				//如果資料長度大於等於256
		{
			page_count = data_length/256;	//計算頁數
			page_other = data_length%256;	//計算剩餘資料長度
			for(i=0;i<page_count;i++)		//迴圈寫入頁資料
			{
				FLASH_Write(databuff,(addr += (i * 256)),256);	//寫入256個位元組的資料
				*databuff += page_count * 256;					//指標後移
			}
			FLASH_Write(databuff,(addr += (page_count * 256)),page_other);	//寫入剩餘資料
		}
		else								//如果資料長度小於256
		{	
			FLASH_Write(databuff,addr,data_length);				//寫入資料
		}
	}
	else									//如果地址的起始位置不為0
	{
		FLASH_Write(databuff,addr,addr_other);					//先寫入地址的剩餘位置資料
		*databuff += addr_other;								//指標後移
		data_length -= addr_other;								//資料長度減去不滿一頁地址的長度
		if(data_length >= 256)									//如果資料長度大於等於256
		{			
			page_count = data_length/256;						//計算頁數
			page_other = data_length%256;						//計算剩餘資料長度
			for(i=0;i<page_count;i++)							//迴圈寫入頁資料
			{
				FLASH_Write(databuff,(addr += (i * 256)),256);	//寫入256個位元組的資料
				*databuff += page_count * 256;					//指標後移
			}
			FLASH_Write(databuff,(addr += (page_count * 256)),page_other);	//寫入剩餘資料
		}
		else
		{
			FLASH_Write(databuff,addr,data_length);				//寫入資料
		}
	}
}

2.2.5 FLASH讀取

雖然FLASH寫入有256位元組的限制,但是讀取時沒有限制,只要傳送W25X_ReadData與讀取起始即可一直接收資料。

void FLASH_Read(uint8_t* databuff,uint32_t addr,uint32_t data_length)	//讀取資料
{
	FLASH_WaitForWriteEnd();											//等待寫入完成
	SPI_FLASH_CS_Low();													//拉低CS片選線,選中FLASH
	SPI_FLASH_SendRecive(W25X_ReadData);								//傳送讀取命令
	SPI_FLASH_SendRecive((addr & 0xFF0000) >>16);						//傳送地址的高8位
	SPI_FLASH_SendRecive((addr & 0xFF00)>>8);							//傳送地址的中8位
	SPI_FLASH_SendRecive(addr & 0xFF);									//傳送地址的低8位
	while(data_length--)												//迴圈讀取資料
	{
		*databuff = SPI_FLASH_SendRecive(Dummy_Byte);					//讀取資料
		databuff++;														//指標後移
	}
	SPI_FLASH_CS_High();												//拉高CS片選線,取消選中FLASH
	FLASH_WaitForWriteEnd();											//等待寫入完成
}