本篇詳細的記錄了如何使用STM32CubeMX配置 STM32G070RBT6 的硬體SPI外設與 SPI Flash 通信(W25Q64)。
1. 準備工作
硬體準備
-
開發闆
首先需要準備一個開發闆,這裡我準備的是STM32G070RB的開發闆
-
SPI Flash
開發闆闆載一片SPI Flash,型号為
,大小為 8 MB。W25Q64JV
軟體準備
- 需要安裝好Keil - MDK及晶片對應的包,以便編譯和下載下傳生成的代碼;
- 準備一個序列槽調試助手,這裡我使用的是
;Serial Port Utility
Keil MDK和序列槽助手Serial Port Utility 的安裝包都可以在文末關注公衆号擷取,回複關鍵字擷取相應的安裝包:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5SM4ATM3EmY2UWMxYmZxU2YxYzX1ATOwUDMxMzLcJTMxIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
2.生成MDK工程
選擇晶片型号
打開STM32CubeMX,打開MCU選擇器:
搜尋并選中晶片
STM32G070RB
:
配置時鐘源
- 如果選擇使用外部高速時鐘(HSE),則需要在System Core中配置RCC;
- 如果使用預設内部時鐘(HSI),這一步可以略過;
這裡我都使用内部時鐘:
配置序列槽
開發闆闆載了一個CH340z換序列槽,連接配接到USART1。
接下來開始配置
USART1
:
配置SPI接口
開發闆上SPI Flash的原理圖如下:
原理圖中雖然将CS片選接到了硬體SPI1的NSS引腳,因為硬體NSS使用比較麻煩,是以後面直接把PA4配置為普通GPIO,手動控制片選信号。
接下來配置 SPI1 接口。
配置SPI接口的時候有三個需要注意的點:
① 分頻系數;
② CPOL:CLK空閑時候的電平為高電平或者低電平;
③ CPHA:在第1個時鐘邊緣采樣,還是在第2個時鐘邊緣采樣;
首先配置硬體SPI的模式:
接着根據W25Q64資料手冊中給出的通信協定,配置SPI外設具體參數:
其實CPOL參數和CPHA參數,從W25Q64中随意找個時序圖就可以看出,比如我以讀取ID的時序為例:
配置時鐘樹
STM32G070RB的最高主頻到64M,是以配置PLL,最後使
HCLK = 64Mhz
即可:
生成工程設定
代碼生成設定
最後設定生成獨立的初始化檔案:
生成代碼
點選
GENERATE CODE
即可生成MDK-V5工程:
3. 重定向printf函數到USART1
參考:【STM32Cube_09】重定向printf函數到序列槽輸出的多種方法。
4. 封裝 SPI Flash(W25Q64)的指令和底層函數
MCU 通過向 SPI Flash 發送各種指令 來讀寫 SPI Flash内部的寄存器,是以這種裸機驅動,首先要先宏定義出需要使用的指令,然後利用 HAL 庫提供的庫函數,封裝出三個底層函數,便于移植:
- 向 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發送指定長度的資料
* @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);
}
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;
uint8_t send_data[4] = {ManufactDeviceID_CMD,0x00,0x00,0x00}; //待發送資料,指令+位址
/* 使能片選 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_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_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
return device_id;
}
讀取狀态寄存器資料并判斷Flash是否忙碌
上文中提到,SPI Flash的所有操作都是靠發送指令完成的,但是 Flash 接收到指令後,需要一段時間去執行該操作,這段時間内 Flash 處于“忙”狀态,MCU 發送的指令無效,不能執行,在 Flash 内部有2-3個狀态寄存器,訓示出 Flash 目前的狀态,有趣的一點是:
當 Flash 内部在執行指令時,不能再執行 MCU 發來的指令,但是 MCU 可以一直讀取狀态寄存器,這下就很好辦了,MCU可以一直讀取,然後判斷Flash是否忙完:
讀取協定如下:
根據此協定實作的讀取狀态寄存器的代碼如下:
/**
* @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_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_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
return result;
}
}
/* 取消片選 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
return 0;
}
然後編寫阻塞判斷Flash是否忙碌的函數:
/**
* @brief 阻塞等待Flash處于空閑狀态
* @param none
* @retval none
*/
static void W25QXX_Wait_Busy(void)
{
while((W25QXX_ReadSR(1) & 0x01) == 0x01); // 等待BUSY位清空
}
讀取資料
SPI Flash讀取資料可以任意位址(位址長度32bit)讀任意長度資料(最大 65535 Byte),沒有任何限制,資料手冊給出的時序如下:
根據該時序圖編寫代碼如下:
/**
* @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_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_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
return 0;
}
}
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
return -1;
}
寫使能/禁止
Flash 晶片預設禁止寫資料,是以在向 Flash 寫資料之前,必須發送指令開啟寫使能,資料手冊中給出的時序如下:
編寫函數如下:
/**
* @brief W25QXX寫使能,将S1寄存器的WEL置位
* @param none
* @retval
*/
void W25QXX_Write_Enable(void)
{
uint8_t cmd= WRITE_ENABLE_CMD;
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_RESET);
SPI_Transmit(&cmd, 1);
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_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_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_RESET);
SPI_Transmit(&cmd, 1);
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
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)
{
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_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_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
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 nbytes)
{
uint8_t cmd = PAGE_PROGRAM_CMD;
WriteAddr <<= 8;
W25QXX_Write_Enable();
/* 使能片選 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_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_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
W25QXX_Wait_Busy();
}
6. 測試驅動
在
main.c
函數中編寫代碼,測試驅動:
首先定義兩個緩存:
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
uint16_t device_id;
uint8_t read_buf[10] = {0};
uint8_t write_buf[10] = {0};
int i;
/* USER CODE END 0 */
然後在 main 函數中編寫代碼:
/* USER CODE BEGIN 2 */
printf("W25Q64 SPI Flash Test By Mculover666\r\n");
device_id = W25QXX_ReadID();
printf("W25Q64 Device ID is 0x%04x\r\n", device_id);
/* 為了驗證,首先讀取要寫入位址處的資料 */
printf("-------- read data before write -----------\r\n");
W25QXX_Read(read_buf, 0, 10);
for (i = 0;i < 10;i++) {
printf("[0x%08x]:0x%02x\r\n", i, *(read_buf+i));
}
/* 擦除該扇區 */
printf("-------- erase sector 0 -----------\r\n");
W25QXX_Erase_Sector(0);
/* 再次讀資料 */
printf("-------- read data after erase -----------\r\n");
W25QXX_Read(read_buf, 0, 10);
for (i = 0;i < 10;i++) {
printf("[0x%08x]:0x%02x\r\n", i, *(read_buf+i));
}
/* 寫資料 */
printf("-------- write data -----------\r\n");
for (i = 0; i < 10;i++) {
write_buf[i] = i;
}
W25QXX_Page_Program(write_buf, 0, 10);
/* 再次讀資料 */
printf("-------- read data after write -----------\r\n");
W25QXX_Read(read_buf, 0, 10);
for (i = 0;i < 10;i++) {
printf("[0x%08x]:0x%02x\r\n", i, *(read_buf+i));
}
/* USER CODE END 2 */