本篇詳細的記錄瞭如何使用STM32CubeMX配置STM32L431RCT6的硬件QSPI外設與 SPI Flash 通信(W25Q64)。
1. 準備工作
硬件準備
- 開發板
首先需要準備一個開發板,這裏我準備的是STM32L4的開發板(BearPi):
- SPI Flash
小熊派開發闆闆載一片SPI Flash,型號爲W25Q64
,大小爲 8 MB,最大支持 80 Mhz的操作頻率。
軟件準備
- 需要安裝好Keil - MDK及芯片對應的包,以便編譯和下載生成的代碼;
Keil MDK和串口助手的安裝包都可以關注“小熊派開源社區”微信公衆號,在資料教程一欄中可獲取安裝包。
2.生成MDK工程
選擇芯片型號
打開STM32CubeMX,打開MCU選擇器:
搜索並選中芯片STM32L431RCT6
:
配置時鐘源
- 如果選擇使用外部高速時鐘(HSE),則需要在System Core中配置RCC;
- 如果使用默認內部時鐘(HSI),這一步可以略過;
這裏我都使用外部時鐘:
配置串口
小熊派開發闆闆載ST-Link並且虛擬了一個串口,原理圖如下:
這裏我將開關撥到AT-MCU
模式,使PC的串口與USART1之間連接。
接下來開始配置USART1
:
配置QSPI接口
首先查看小熊派開發板上 SPI Flash 的原理圖:
其引腳連接情況如下:
SPI Flash連接引腳 | 對應引腳 |
---|---|
QUADSPI_BK1_NCS | PB11 |
QUADSPI_BK1_CLK | PB10 |
QUADSPI_BK1_IO0 | PB1 |
QUADSPI_BK1_IO1 | PB0 |
接下來配置 QSPI 接口:
配置時鐘樹
STM32L4的最高主頻到80M,所以配置PLL,最後使HCLK = 80Mhz
即可:
生成工程設置
代碼生成設置
最後設置生成獨立的初始化文件:
生成代碼
點擊GENERATE CODE
即可生成MDK-V5工程:
3. 在MDK中編寫、編譯、下載用戶代碼
重定向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具體型號
數據手冊上給出的操作時序如圖:
根據該時序,編寫代碼如下:
/**
* @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),沒有任何限制,數據手冊給出的時序如下:
根據該時序圖編寫代碼如下:
/**
* @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是否忙完:
首先讀取狀態寄存器的代碼如下:
/**
* @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 寫數據之前,必須發送命令開啓寫使能,數據手冊中給出的時序如下:
編寫函數如下:
/**
* @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),數據手冊中給出的時序如下:
根據此時序編寫函數如下:
/**
* @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 內部的構造,可以按頁寫入:
頁寫入的時序如圖:
編寫代碼如下:
/**
* @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 */
測試結果如下:
至此,我們已經學會如何使用硬件QSPI接口讀寫SPI Flash的數據,下一節將講述如何使用硬件SDMMC接口讀取SD卡數據。