STM32 HAL庫驅動W25QXX Flash
1. 概述
W25QXX系列是一種SPI接口的Flash存儲器,廣泛應用于嵌入式系統中作為數據存儲設備。本文檔詳細介紹了基于STM32 HAL庫的W25QXX Flash驅動實現,包括硬件連接、驅動函數實現以及使用示例。
項目源碼倉庫:STM32_Sensor_Drives
2. 硬件連接
W25QXX Flash通過SPI接口與STM32連接,主要包括以下引腳:
- SCK - 連接到STM32的SPI1_SCK (PA5)
- MISO - 連接到STM32的SPI1_MISO (PA6)
- MOSI - 連接到STM32的SPI1_MOSI (PA7)
- CS - 連接到STM32的GPIO (PA4)
3. 驅動實現
3.1 SPI配置
首先,我們需要配置SPI接口以與W25QXX通信。在spi.c
文件中,SPI1的初始化配置如下:
void MX_SPI1_Init(void)
{hspi1.Instance = SPI1;hspi1.Init.Mode = SPI_MODE_MASTER;hspi1.Init.Direction = SPI_DIRECTION_2LINES;hspi1.Init.DataSize = SPI_DATASIZE_8BIT;hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH;hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;hspi1.Init.NSS = SPI_NSS_SOFT;hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;hspi1.Init.TIMode = SPI_TIMODE_DISABLE;hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;hspi1.Init.CRCPolynomial = 10;if (HAL_SPI_Init(&hspi1) != HAL_OK){Error_Handler();}
}
這里配置SPI為主模式,8位數據寬度,高電平空閑,第二個邊沿采樣,軟件控制NSS,波特率預分頻為8,MSB優先傳輸。
3.2 GPIO配置
W25QXX的片選信號需要通過GPIO控制,在gpio.c
中配置如下:
void MX_GPIO_Init(void)
{GPIO_InitTypeDef GPIO_InitStruct = {0};/* GPIO Ports Clock Enable */__HAL_RCC_GPIOC_CLK_ENABLE();__HAL_RCC_GPIOD_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();__HAL_RCC_GPIOB_CLK_ENABLE();/*Configure GPIO pin Output Level */HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);/*Configure GPIO pin : PA4 */GPIO_InitStruct.Pin = GPIO_PIN_4;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
PA4配置為推挽輸出模式,用于控制W25QXX的片選信號。初始狀態設置為高電平(未選中)。
3.3 W25QXX命令定義
在spi.h
文件中,定義了W25QXX的各種命令和片選引腳:
#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#define W25Q64_CHIP_SELECT_GPIO_Port GPIOA
#define W25Q64_CHIP_SELECT_Pin GPIO_PIN_4
這些命令用于實現讀取ID、讀寫數據、擦除扇區等操作。
3.4 SPI基礎通信函數
在spi.c
文件中,實現了三個基礎的SPI通信函數:
/*** @brief SPI發送指定長度的數據* @param buf —— 發送數據緩沖區首地址* @param size —— 要發送數據的字節數* @retval 成功返回HAL_OK*/
static HAL_StatusTypeDef SPI_Transmit(uint8_t* send_buf, uint16_t size)
{return HAL_SPI_Transmit(&hspi1, send_buf, size, 100);
}/*** @brief SPI接收指定長度的數據* @param buf —— 接收數據緩沖區首地址* @param size —— 要接收數據的字節數* @retval 成功返回HAL_OK*/
static HAL_StatusTypeDef SPI_Receive(uint8_t* recv_buf, uint16_t size)
{return HAL_SPI_Receive(&hspi1, recv_buf, size, 100);
}/*** @brief SPI在發送數據的同時接收指定長度的數據* @param send_buf —— 接收數據緩沖區首地址* @param recv_buf —— 接收數據緩沖區首地址* @param size —— 要發送/接收數據的字節數* @retval 成功返回HAL_OK*/
static HAL_StatusTypeDef SPI_TransmitReceive(uint8_t* send_buf, uint8_t* recv_buf, uint16_t size)
{return HAL_SPI_TransmitReceive(&hspi1, send_buf, recv_buf, size, 100);
}
這三個函數分別用于發送數據、接收數據和同時發送接收數據,是W25QXX驅動的基礎。
3.5 W25QXX驅動函數實現
3.5.1 讀取Flash ID
/*** @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 IDuint16_t device_id = 0;uint8_t send_data[4] = {ManufactDeviceID_CMD,0x00,0x00,0x00}; //待發送數據,命令+地址/* 使能片選 */HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);/* 發送并讀取數據 */if (HAL_OK == SPI_Transmit(send_data, 4)) {if (HAL_OK == SPI_Receive(recv_buf, 2)) {device_id = (recv_buf[0] << 8) | recv_buf[1];}}/* 取消片選 */HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);return device_id;
}
該函數用于讀取W25QXX的制造商ID和設備ID,通過發送0x90命令和三個字節的地址(全0),然后讀取兩個字節的數據。
3.5.2 讀取狀態寄存器
/*** @brief 讀取W25QXX的狀態寄存器,W25Q64一共有2個狀態寄存器* @param reg —— 狀態寄存器編號(1~2)* @retval 狀態寄存器的值*/
static uint8_t W25QXX_ReadSR(uint8_t reg)
{uint8_t result = 0; uint8_t send_buf[4] = {0x00,0x00,0x00,0x00};switch(reg){case 1:send_buf[0] = READ_STATU_REGISTER_1;case 2:send_buf[0] = READ_STATU_REGISTER_2;case 0:default:send_buf[0] = READ_STATU_REGISTER_1;}/* 使能片選 */HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);if (HAL_OK == SPI_Transmit(send_buf, 4)) {if (HAL_OK == SPI_Receive(&result, 1)) {HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);return result;}}/* 取消片選 */HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);return 0;
}
該函數用于讀取W25QXX的狀態寄存器,W25Q64有兩個狀態寄存器,通過參數reg
選擇要讀取的寄存器。
3.5.3 等待Flash空閑
/*** @brief 阻塞等待Flash處于空閑狀態* @param none* @retval none*/
static void W25QXX_Wait_Busy(void)
{while((W25QXX_ReadSR(1) & 0x01) == 0x01); // 等待BUSY位清空
}
該函數通過循環檢查狀態寄存器1的最低位(BUSY位),等待其變為0,表示Flash處于空閑狀態。
3.5.4 讀取數據
/*** @brief 讀取SPI FLASH數據* @param buffer —— 數據存儲區* @param start_addr —— 開始讀取的地址(最大32bit)* @param nbytes —— 要讀取的字節數(最大65535)* @retval 成功返回0,失敗返回-1*/
int W25QXX_Read(uint8_t* buffer, uint32_t start_addr, uint16_t nbytes)
{uint8_t cmd = READ_DATA_CMD;start_addr = start_addr << 8;W25QXX_Wait_Busy();/* 使能片選 */HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);SPI_Transmit(&cmd, 1);if (HAL_OK == SPI_Transmit((uint8_t*)&start_addr, 3)) {if (HAL_OK == SPI_Receive(buffer, nbytes)) {HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);return 0;}}HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);return -1;
}
該函數用于從指定地址讀取指定長度的數據,首先發送讀取命令(0x03),然后發送3字節地址,最后讀取數據。
3.5.5 寫使能和寫禁止
/*** @brief W25QXX寫使能,將S1寄存器的WEL置位* @param none* @retval*/
void W25QXX_Write_Enable(void)
{uint8_t cmd= WRITE_ENABLE_CMD;HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);SPI_Transmit(&cmd, 1);HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);W25QXX_Wait_Busy();}/*** @brief W25QXX寫禁止,將WEL清零* @param none* @retval none*/
void W25QXX_Write_Disable(void)
{uint8_t cmd = WRITE_DISABLE_CMD;HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);SPI_Transmit(&cmd, 1);HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);W25QXX_Wait_Busy();
}
這兩個函數分別用于使能和禁止寫操作。在進行寫入或擦除操作前,必須先調用寫使能函數。
3.5.6 扇區擦除
/*** @brief W25QXX擦除一個扇區* @param sector_addr —— 扇區地址 根據實際容量設置* @retval none* @note 阻塞操作*/
void W25QXX_Erase_Sector(uint32_t sector_addr)
{uint8_t cmd = SECTOR_ERASE_CMD;sector_addr *= 4096; //每個塊有16個扇區,每個扇區的大小是4KB,需要換算為實際地址sector_addr <<= 8;W25QXX_Write_Enable(); //擦除操作即寫入0xFF,需要開啟寫使能W25QXX_Wait_Busy(); //等待寫使能完成/* 使能片選 */HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);SPI_Transmit(&cmd, 1);SPI_Transmit((uint8_t*)§or_addr, 3);HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);W25QXX_Wait_Busy(); //等待扇區擦除完成
}
該函數用于擦除指定的扇區,每個扇區大小為4KB。擦除前需要先使能寫操作,擦除后需要等待操作完成。
3.5.7 頁編程(寫入數據)
/*** @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 nbytes)
{uint8_t cmd = PAGE_PROGRAM_CMD;WriteAddr <<= 8;W25QXX_Write_Enable();/* 使能片選 */HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);SPI_Transmit(&cmd, 1);SPI_Transmit((uint8_t*)&WriteAddr, 3);SPI_Transmit(dat, nbytes);HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);W25QXX_Wait_Busy();
}
該函數用于向指定地址寫入數據,寫入前需要先使能寫操作,寫入后需要等待操作完成。W25QXX的頁大小為256字節,一次寫入不能超過一頁。
4. 使用示例
在main.c
文件中,展示了如何使用W25QXX驅動進行讀寫操作:
int main(void)
{/* 省略初始化代碼 *//* Infinite loop *//* USER CODE BEGIN WHILE */printf("System will start while\n");printf("read data before write\r\n");W25QXX_Read(read_buf, 0, 10);snprintf(str, 15, "read data: %x", read_buf[2]);printf("%s\r\n", str);/* 擦除該扇區 */printf("erase sector 0 \r\n");W25QXX_Erase_Sector(0);/* 再次讀數據 */printf("read erase data\r\n");W25QXX_Read(read_buf, 0, 10);memset(str, 0, sizeof(str)); // 清空讀緩沖區snprintf(str, 15, "read data: %x", read_buf[2]);printf("%s\r\n", str);/* 寫數據 */printf("write data \r\n");for (i = 0; i < 10; i++){write_buf[i] = i;}W25QXX_Page_Program(write_buf, 0, 10); // 寫數據/* 再次讀數據 */printf("read write data \r\n");W25QXX_Read(read_buf, 0, 10);memset(str, 0, sizeof(str)); // 清空讀緩沖區snprintf(str, 15, "read data: %x", read_buf[2]);printf("%s\r\n", str);while (1){HAL_Delay(200);}
}
這個示例展示了完整的W25QXX操作流程:
- 首先讀取Flash中的數據
- 擦除扇區0
- 再次讀取數據,驗證擦除效果
- 寫入新數據
- 再次讀取數據,驗證寫入效果
5. 串口打印功能
為了方便調試,在usart.c
中實現了printf
函數的重定向:
int fputc(int ch, FILE *f)
{if (f == stdout) // 僅處理標準輸出{HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 100); // 阻塞發送if (ch == '\n') // 發送\n時自動補充\rHAL_UART_Transmit(&huart2, (uint8_t *)"\r", 1, 100);}return ch;
}
通過重定向fputc
函數,使得printf
函數的輸出通過UART2發送,便于觀察程序運行狀態。
6. 總結
本文檔詳細介紹了基于STM32 HAL庫的W25QXX Flash驅動實現,包括:
- SPI和GPIO的配置
- W25QXX的命令定義
- 基礎SPI通信函數
- W25QXX驅動函數實現(讀ID、讀寫數據、擦除扇區等)
- 使用示例
- 串口打印功能
通過這些代碼,可以方便地在STM32項目中使用W25QXX Flash進行數據存儲和讀取。
7. 注意事項
- W25QXX的寫入操作需要先擦除扇區,因為Flash只能將1變為0,不能將0變為1
- 擦除和寫入操作前必須先使能寫操作
- 寫入數據不能跨頁,一頁為256字節
- 擦除和寫入操作需要等待完成,否則可能導致數據錯誤
- 頻繁擦寫同一區域會導致Flash壽命減少,建議實現磨損均衡算法
8. 參考資料
- W25Q64數據手冊
- STM32 HAL庫文檔
- STM32 SPI通信指南