天天看點

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

本篇詳細的記錄了如何使用STM32CubeMX配置 STM32G070RBT6 的硬體SPI外設與 SPI Flash 通信(W25Q64)。

1. 準備工作

硬體準備

  • 開發闆

    首先需要準備一個開發闆,這裡我準備的是STM32G070RB的開發闆

  • SPI Flash

    開發闆闆載一片SPI Flash,型号為

    W25Q64JV

    ,大小為 8 MB。

軟體準備

  • 需要安裝好Keil - MDK及晶片對應的包,以便編譯和下載下傳生成的代碼;
  • 準備一個序列槽調試助手,這裡我使用的是

    Serial Port Utility

Keil MDK和序列槽助手Serial Port Utility 的安裝包都可以在文末關注公衆号擷取,回複關鍵字擷取相應的安裝包:
STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

2.生成MDK工程

選擇晶片型号

打開STM32CubeMX,打開MCU選擇器:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

搜尋并選中晶片

STM32G070RB

:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

配置時鐘源

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

這裡我都使用内部時鐘:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

配置序列槽

開發闆闆載了一個CH340z換序列槽,連接配接到USART1。

接下來開始配置

USART1

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

配置SPI接口

開發闆上SPI Flash的原理圖如下:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)
原理圖中雖然将CS片選接到了硬體SPI1的NSS引腳,因為硬體NSS使用比較麻煩,是以後面直接把PA4配置為普通GPIO,手動控制片選信号。

接下來配置 SPI1 接口。

配置SPI接口的時候有三個需要注意的點:

① 分頻系數;

② CPOL:CLK空閑時候的電平為高電平或者低電平;

③ CPHA:在第1個時鐘邊緣采樣,還是在第2個時鐘邊緣采樣;

首先配置硬體SPI的模式:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

接着根據W25Q64資料手冊中給出的通信協定,配置SPI外設具體參數:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

其實CPOL參數和CPHA參數,從W25Q64中随意找個時序圖就可以看出,比如我以讀取ID的時序為例:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

配置時鐘樹

STM32G070RB的最高主頻到64M,是以配置PLL,最後使

HCLK = 64Mhz

即可:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

生成工程設定

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

代碼生成設定

最後設定生成獨立的初始化檔案:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

生成代碼

點選

GENERATE CODE

即可生成MDK-V5工程:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

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具體型号

資料手冊上給出的操作時序如圖:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

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

/**
 * @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是否忙完:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

讀取協定如下:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

根據此協定實作的讀取狀态寄存器的代碼如下:

/**
 * @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),沒有任何限制,資料手冊給出的時序如下:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

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

/**
 * @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 寫資料之前,必須發送指令開啟寫使能,資料手冊中給出的時序如下:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)
STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

編寫函數如下:

/**
 * @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),資料手冊中給出的時序如下:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

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

/**
 * @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*)&sector_addr, 3);
    
    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
    
    W25QXX_Wait_Busy();       //等待扇區擦除完成
}
           

頁寫入操作

向 Flash 晶片寫資料的時候,因為 Flash 内部的構造,可以按頁寫入:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

頁寫入的時序如圖:

STM32CubeMX | 30-使用硬體SPI讀寫FLASH(W25Q64)

編寫代碼如下:

/**
 * @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 */