STM32單片機基礎18——使用硬件QSPI讀寫SPI Flash(W25Q64)

本篇詳細的記錄瞭如何使用STM32CubeMX配置STM32L431RCT6的硬件QSPI外設與 SPI Flash 通信(W25Q64)。

1. 準備工作

硬件準備

mark

  • SPI Flash
    小熊派開發闆闆載一片SPI Flash,型號爲 W25Q64,大小爲 8 MB,最大支持 80 Mhz的操作頻率。

軟件準備

  • 需要安裝好Keil - MDK及芯片對應的包,以便編譯和下載生成的代碼;

Keil MDK和串口助手的安裝包都可以關注“小熊派開源社區”微信公衆號,在資料教程一欄中可獲取安裝包。

2.生成MDK工程

選擇芯片型號

打開STM32CubeMX,打開MCU選擇器:
mark

搜索並選中芯片STM32L431RCT6:
mark

配置時鐘源

  • 如果選擇使用外部高速時鐘(HSE),則需要在System Core中配置RCC;
  • 如果使用默認內部時鐘(HSI),這一步可以略過;

這裏我都使用外部時鐘:

mark

配置串口

小熊派開發闆闆載ST-Link並且虛擬了一個串口,原理圖如下:

mark

這裏我將開關撥到AT-MCU模式,使PC的串口與USART1之間連接。

接下來開始配置USART1

mark

配置QSPI接口

首先查看小熊派開發板上 SPI Flash 的原理圖:

mark

其引腳連接情況如下:

SPI Flash連接引腳 對應引腳
QUADSPI_BK1_NCS PB11
QUADSPI_BK1_CLK PB10
QUADSPI_BK1_IO0 PB1
QUADSPI_BK1_IO1 PB0

接下來配置 QSPI 接口:

mark

配置時鐘樹

STM32L4的最高主頻到80M,所以配置PLL,最後使HCLK = 80Mhz即可:
mark

生成工程設置

mark

代碼生成設置

最後設置生成獨立的初始化文件:

mark

生成代碼

點擊GENERATE CODE即可生成MDK-V5工程:

mark

3. 在MDK中編寫、編譯、下載用戶代碼

重定向printf( )函數

參考:重定向printf函數到串口輸出的多種方法

4. 封裝 SPI Flash(W25Q64)的命令和底層函數

MCU 通過向 SPI Flash 發送各種命令 來讀寫 SPI Flash內部的寄存器,所以這種裸機驅動,首先要先宏定義出需要使用的命令,然後利用 HAL 庫提供的庫函數,封裝出三個底層函數,便於移植

  • 向 SPI Flash 發送命令的函數
  • 向 SPI Flash 發送數據的函數
  • 從 SPI Flash 接收數據的函數

接下來開始編寫代碼~

宏定義操作命令

#define ManufactDeviceID_CMD	0x90
#define READ_STATU_REGISTER_1   0x05
#define READ_STATU_REGISTER_2   0x35
#define READ_DATA_CMD	        0x03
#define WRITE_ENABLE_CMD	    0x06
#define WRITE_DISABLE_CMD	    0x04
#define SECTOR_ERASE_CMD	    0x20
#define CHIP_ERASE_CMD	        0xc7
#define PAGE_PROGRAM_CMD        0x02

封裝發送命令的函數(重點)

/**
 * @brief		向SPI Flash發送指令
 * @param		instruction —— 要發送的指令
 * @param		address     —— 要發送的地址
 * @param		dummyCycles	—— 空指令週期數
 * @param		instructionMode —— 指令發送模式
 * @param		addressMode —— 地址發送模式
 * @param		addressSize	—— 地址大小
 * @param		dataMode    —— 數據發送模式
 * @retval	    成功返回HAL_OK
*/
HAL_StatusTypeDef QSPI_Send_Command(uint32_t instruction, 
									uint32_t address, 
									uint32_t dummyCycles, 
									uint32_t instructionMode, 
									uint32_t addressMode, 
									uint32_t addressSize, 
									uint32_t dataMode)
{
    QSPI_CommandTypeDef cmd;

    cmd.Instruction = instruction;                 	//指令
    cmd.Address = address;                          //地址
    cmd.DummyCycles = dummyCycles;                  //設置空指令週期數
    cmd.InstructionMode = instructionMode;			//指令模式
    cmd.AddressMode = addressMode;   				//地址模式
    cmd.AddressSize = addressSize;   				//地址長度
    cmd.DataMode = dataMode;             			//數據模式
    cmd.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;       	//每次都發送指令
    cmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE; //無交替字節
    cmd.DdrMode = QSPI_DDR_MODE_DISABLE;           	//關閉DDR模式
    cmd.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
    
	return HAL_QSPI_Command(&hqspi, &cmd, 5000);
}

封裝發送數據的函數

/**
* @brief    QSPI發送指定長度的數據
* @param    buf  —— 發送數據緩衝區首地址
* @param    size —— 要發送數據的字節數
 * @retval	成功返回HAL_OK
 */
HAL_StatusTypeDef QSPI_Transmit(uint8_t* send_buf, uint32_t size)
{
    hqspi.Instance->DLR = size - 1;                         //配置數據長度
    return HAL_QSPI_Transmit(&hqspi, send_buf, 5000);	    //接收數據
}

封裝接收數據的函數

/**
 * @brief	  QSPI接收指定長度的數據
 * @param   buf  —— 接收數據緩衝區首地址
 * @param   size —— 要接收數據的字節數
 * @retval	成功返回HAL_OK
 */
HAL_StatusTypeDef QSPI_Receive(uint8_t* recv_buf, uint32_t size)
{
    hqspi.Instance->DLR = size - 1;                       //配置數據長度
    return HAL_QSPI_Receive(&hqspi, recv_buf, 5000);			//接收數據
}

5. 編寫W25Q64的驅動程序

接下來開始利用上一節封裝的宏定義和底層函數,編寫W25Q64的驅動程序:

讀取Manufacture ID和Device ID

讀取 Flash 內部這兩個ID有兩個作用:

  • 檢測SPI Flash是否存在
  • 可以根據ID判斷Flash具體型號

數據手冊上給出的操作時序如圖:

mark

根據該時序,編寫代碼如下:

/**
 * @brief   讀取Flash內部的ID
 * @param   none
 * @retval	成功返回device_id
 */
uint16_t W25QXX_ReadID(void)
{
	uint8_t recv_buf[2] = {0};	//recv_buf[0]存放Manufacture ID, recv_buf[1]存放Device ID
	uint16_t device_id = 0;
	if(HAL_OK == QSPI_Send_Command(ManufactDeviceID_CMD, 0, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_1_LINE, QSPI_ADDRESS_24_BITS, QSPI_DATA_1_LINE))
	{
        //讀取ID
        if(HAL_OK == QSPI_Receive(recv_buf, 2))
        {
            device_id = (recv_buf[0] << 8) | recv_buf[1];
            return device_id;
        }
        else
        {
            return 0;
        }
	}
	else
	{
		return 0;
	}
}

讀取數據

SPI Flash讀取數據可以任意地址(地址長度32bit)讀任意長度數據(最大 65535 Byte),沒有任何限制,數據手冊給出的時序如下:

mark

根據該時序圖編寫代碼如下:

/**
 * @brief	讀取SPI FLASH數據
 * @param   dat_buffer —— 數據存儲區
 * @param   start_read_addr —— 開始讀取的地址(最大32bit)
 * @param   byte_to_read —— 要讀取的字節數(最大65535)
 * @retval  none
 */
void W25QXX_Read(uint8_t* dat_buffer, uint32_t start_read_addr, uint16_t byte_to_read)
{
	QSPI_Send_Command(READ_DATA_CMD, start_read_addr, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_1_LINE, QSPI_ADDRESS_24_BITS, QSPI_DATA_1_LINE);
    QSPI_Receive(dat_buffer, byte_to_read);
}

讀取狀態寄存器數據並判斷Flash是否忙碌

上文中提到,SPI Flash的所有操作都是靠發送命令完成的,但是 Flash 接收到命令後,需要一段時間去執行該操作,這段時間內 Flash 處於“忙”狀態,MCU 發送的命令無效,不能執行,在 Flash 內部有2-3個狀態寄存器,指示出 Flash 當前的狀態,有趣的一點是:

當 Flash 內部在執行命令時,不能再執行 MCU 發來的命令,但是 MCU 可以一直讀取狀態寄存器,這下就很好辦了,MCU可以一直讀取,然後判斷Flash是否忙完

mark

首先讀取狀態寄存器的代碼如下:

/**
 * @brief	讀取W25QXX的狀態寄存器,W25Q64一共有2個狀態寄存器
 * @param 	reg  —— 狀態寄存器編號(1~2)
 * @retval	狀態寄存器的值
 */
uint8_t W25QXX_ReadSR(uint8_t reg)
{
	uint8_t cmd = 0, result = 0;	
	switch(reg)
	{
		case 1:
			/* 讀取狀態寄存器1的值 */
			cmd = READ_STATU_REGISTER_1;
		case 2:
			cmd = READ_STATU_REGISTER_2;
		case 0:
		default:
			cmd = READ_STATU_REGISTER_1;
	}
	QSPI_Send_Command(cmd, 0, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_NONE, QSPI_ADDRESS_24_BITS, QSPI_DATA_1_LINE);
	QSPI_Receive(&result, 1);
	
	return result;
}

然後編寫阻塞判斷Flash是否忙碌的函數:

/**
 * @brief	阻塞等待Flash處於空閒狀態
 * @param   none
 * @retval  none
 */
void W25QXX_Wait_Busy(void)
{
    while((W25QXX_ReadSR(1) & 0x01) == 0x01); // 等待BUSY位清空
}

寫使能/禁止

Flash 芯片默認禁止寫數據,所以在向 Flash 寫數據之前,必須發送命令開啓寫使能,數據手冊中給出的時序如下:

mark

mark

編寫函數如下:

/**
 * @brief	W25QXX寫使能,將S1寄存器的WEL置位
 * @param	none
 * @retval
 */
void W25QXX_Write_Enable(void)
{
    QSPI_Send_Command(WRITE_ENABLE_CMD, 0, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_NONE, QSPI_ADDRESS_8_BITS, QSPI_DATA_NONE);
		W25QXX_Wait_Busy();
}

/**
 * @brief	W25QXX寫禁止,將WEL清零
 * @param	none
 * @retval	none
 */
void W25QXX_Write_Disable(void)
{
    QSPI_Send_Command(WRITE_DISABLE_CMD, 0, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_NONE, QSPI_ADDRESS_8_BITS, QSPI_DATA_NONE);
		W25QXX_Wait_Busy();
}

擦除扇區

SPI Flash有個特性:

數據位可以由1變爲0,但是不能由0變爲1。

所以在向 Flash 寫數據之前,必須要先進行擦除操作,並且 Flash 最小隻能擦除一個扇區,擦除之後該扇區所有的數據變爲 0xFF(即全爲1),數據手冊中給出的時序如下:

mark

根據此時序編寫函數如下:

/**
 * @brief	W25QXX擦除一個扇區
 * @param   sector_addr	—— 扇區地址 根據實際容量設置
 * @retval  none
 * @note	阻塞操作
 */
void W25QXX_Erase_Sector(uint32_t sector_addr)
{
    sector_addr *= 4096;	//每個塊有16個扇區,每個扇區的大小是4KB,需要換算爲實際地址
    W25QXX_Write_Enable();  //擦除操作即寫入0xFF,需要開啓寫使能
    W25QXX_Wait_Busy();		//等待寫使能完成
    QSPI_Send_Command(SECTOR_ERASE_CMD, sector_addr, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_1_LINE, QSPI_ADDRESS_24_BITS, QSPI_DATA_NONE);
    W25QXX_Wait_Busy();   	//等待扇區擦除完成
}

頁寫入操作

向 Flash 芯片寫數據的時候,因爲 Flash 內部的構造,可以按頁寫入:

mark

頁寫入的時序如圖:

mark

編寫代碼如下:

/**
 * @brief	頁寫入操作
 * @param	dat —— 要寫入的數據緩衝區首地址
 * @param	WriteAddr —— 要寫入的地址
 * @param   byte_to_write —— 要寫入的字節數(0-256)
 * @retval	none
 */
void W25QXX_Page_Program(uint8_t* dat, uint32_t WriteAddr, uint16_t byte_to_write)
{
	W25QXX_Write_Enable();
	QSPI_Send_Command(PAGE_PROGRAM_CMD, WriteAddr, 0, QSPI_INSTRUCTION_1_LINE, QSPI_ADDRESS_1_LINE, QSPI_ADDRESS_24_BITS, QSPI_DATA_1_LINE);
	QSPI_Transmit(dat, byte_to_write);
    W25QXX_Wait_Busy();
}

6. 測試驅動

main.c 函數中編寫代碼,測試驅動:

首先定義兩個緩存:

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
uint8_t dat[11] = "mculover666";
uint8_t read_buf[11] = {0};
/* USER CODE END 0 */

然後在 main 函數中編寫代碼:

/* USER CODE BEGIN 2 */
printf("Test W25QXX...\r\n");
device_id = W25QXX_ReadID();
printf("device_id = 0x%04X\r\n\r\n", device_id);

/* 爲了驗證,首先讀取要寫入地址處的數據 */
printf("-------- read data before write -----------\r\n");
W25QXX_Read(read_buf, 5, 11);
printf("read date is %s\r\n", (char*)read_buf);

/* 擦除該扇區 */
printf("-------- erase sector 0 -----------\r\n");
W25QXX_Erase_Sector(0);

/* 寫數據 */
printf("-------- write data -----------\r\n");
W25QXX_Page_Program(dat, 5, 11);

/* 再次讀數據 */
printf("-------- read data after write -----------\r\n");
W25QXX_Read(read_buf, 5, 11);
printf("read date is %s\r\n", (char*)read_buf);
/* USER CODE END 2 */

測試結果如下:

mark

至此,我們已經學會如何使用硬件QSPI接口讀寫SPI Flash的數據,下一節將講述如何使用硬件SDMMC接口讀取SD卡數據。

發佈了50 篇原創文章 · 獲贊 2 · 訪問量 5572
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章