單片機
返回首頁

STM32—SPI讀寫FLASH

2021-09-02 來源:eefocus

FLASH簡介

FLASH俗稱閃存,和EEPROM一樣,都是掉電數據不丟失的非易失行存儲器,但FLASH的存儲容量普遍大于EEPROM,現在像如U盤、SD卡、SSD固態硬盤以及STM32芯片內部存儲程序的設備都是FLASH類型的存儲器。由此可見FLASH對于我們學習和工作的重要性,EEPROM可以實現單字節的擦寫,而FLASH都是一大片的擦寫,就像是大規模殺傷性武器,其最小擦除單位:扇區的大小也是4KB。

我們此次通過SPI對FLASH存儲芯片W25Q64進行讀寫擦除的操作。


對于FLASH內部結構的詳細說明博主會專門整理一篇博客來說明,所以關于FLASH芯片的相關原理,本文中只做簡單說明,側重代碼部分。

FLASH詳細說明的博客鏈接:(沒有鏈接就說明還沒有整理出)


W25Q64

W25Q64簡介

就長這么個樣子

在這里插入圖片描述

STM32內部原理圖如下:

在這里插入圖片描述

W25Q64是一種使用SPI通信協議的NOR FLASH存儲器,它 的CS/CLK/DIO/DO 引 腳 分 別 連 接 到 了 STM32 對 應 的 SPI 引 腳NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引腳是一個普通的 GPIO,不是 SPI 的專用NSS 引腳,所以程序中我們要使用軟件控制的方式。FLASH 芯片中還有 WP 和 HOLD 引腳。 WP 引腳可控制寫保護功能,當該引腳為低電平時,禁止寫入數據。我們直接接電源,不使用寫保護功能。 HOLD 引腳可用于暫停通訊,該引腳為低電平時,通訊暫停,數據輸出引腳輸出高阻抗狀態,時鐘和數據輸入引腳無效。我們直接接電源,不使用通訊暫停功能。


W25Q64支持SPI通訊的模式0和模式3


FLASH控制指令

FLASH芯片中規定了許多指令,只要SPI向FLASH發送相應的指令,FLASH就會執行相應的操作,所以我們對FLASH的一切操作都是基于這個指令集的,接下來介紹一下FLASH的控制指令:

在這里插入圖片描述

表中第一列為指令名,第二列為相應的指令代碼,第三列及后面的內容根據指令的不同而意義不同,其中帶括號的字節參數,方向為 FLASH 向主機傳輸,即命令響應,不帶括號的則為主機向 FLASH 傳輸。表中“A0~A23” 指 FLASH 芯片內部存儲器組織的地址; “M0~M7” 為廠商號( MANUFACTURER ID); “ID0-ID15”為 FLASH 芯片的ID;“dummy”指該處可為任意數據;“D0~D7” 為 FLASH 內部存儲矩陣的內容。

看起來很復雜的樣子,其實只要在需要執行相應操作時來查這個表,只要能夠理解這些指令的使用方法,FLASH就算學會了。


例如:要知道FLASH的ID,那就在指令中找對應的取ID指令“JEDEC ID”,仔細解讀這個指令

在這里插入圖片描述

可以看出對應的指令代碼為“9F”,后面的三個字節帶括號,代表這三個字節就是FLASH向STM32發送的數據,即這三個字節就是FLASH的ID,然后使用SPI進行讀取就可以了。


我們一般是將這些指令宏定義在頭文件中,便于使用:


#define W25X_WriteEnable       0x06 

#define W25X_WriteDisable       0x04 

#define W25X_ReadStatusReg       0x05 

#define W25X_WriteStatusReg       0x01 

#define W25X_ReadData       0x03 

#define W25X_FastReadData       0x0B 

#define W25X_FastReadDual       0x3B 

#define W25X_PageProgram       0x02 

#define W25X_BlockErase       0xD8 

#define W25X_SectorErase       0x20 

#define W25X_ChipErase       0xC7 

#define W25X_PowerDown       0xB9 

#define W25X_ReleasePowerDown       0xAB 

#define W25X_DeviceID       0xAB 

#define W25X_ManufactDeviceID         0x90 

#define W25X_JedecDeviceID       0x9F


FLASH內部存儲結構

FLASH的存儲矩陣如圖:其內存分為128塊,每一塊都有16個扇區,每個扇區大小為4KB,擦除數據的時候是以扇區為基本單位的。

在這里插入圖片描述

代碼講解

代碼都是博主親手寫出來的,可以運行。

代碼部分會用到SPI的代碼,關于SPI的說明之前整理過:SPI詳解


讀取芯片ID

/************************讀取芯片ID*************************************/

uint32_t FLASH_ReadID(void)

{

uint32_t temp,temp1,temp2,temp3;

SPI_NSS_Begin();

/* 發送取ID指令 */

SPI_SendData( W25X_JedecDeviceID);

/* FLASH會連續發送三個字節數據 */

temp1=SPI_SendData(Dummy_Byte);

temp2=SPI_SendData(Dummy_Byte);

temp3=SPI_SendData(Dummy_Byte);

SPI_NSS_Stop();

/* 高為先行,將三個字節整理在一起 */

temp=temp1<<16 | temp2<<8 | temp3;

return temp;

}


由于SPI是全雙工通信,所以接收數據和發送數據用的是同一個函數


發送寫使能信號

/****************發送寫使能信號****************************/

void FLASH_WriteEnable(void)

{

SPI_NSS_Begin();

/* 發送相應的指令代碼 */

SPI_SendData(W25X_WriteEnable);

SPI_NSS_Stop();

}


在向FLASH中執行寫操作之前,都要進行寫使能操作,通過發送寫使能指令到FLASH來實現


等待FLASH不忙

/******************等待,直到不忙***********************************/

void FLASH_WaitBusy(void)

{

uint8_t StatusReg=0x01;

SPI_NSS_Begin();

   /* 讀取狀態寄存器中的數據,判斷忙標志0x01位 置位代表忙 */

SPI_SendData(W25X_ReadStatusReg);      

   /* 只讀取狀態寄存器的BUSY位,即第一位 */

while((StatusReg & 0x01) == 1) 

StatusReg=SPI_SendData(Dummy_Byte); 

SPI_NSS_Stop();

}


FLASH在通訊的過程中需要一定的時間來執行操作,在這期間,傳輸數據是無效的,因為FLASH忙著呢,所以我們就要有一個函數來專門等!等到FLASH不忙了,再進行通訊,那怎么等呢?FLASH不忙了會給出一個信號——將狀態寄存器的BUSY位重置(也就是0),所以我們需要不斷的來檢測狀態寄存器中的BUSY位是否置位,利用讀取寄存器狀態的指令來獲取狀態寄存器當下的狀態,然后根據寄存器的BUSY位(第1位)來判斷FLASH是否處于忙碌狀態。


簡單來說,這就是個延時函數,延時直到FLASH空閑,可以進行下一步傳輸。


擦除扇區

/******擦除扇區的內容,切記地址要對其到4kB,每個扇區的大小都是4KB********/

void FLASH_SectorErase(uint32_t addr)

{

/* 開始的時候要發送寫使能信號*/

FLASH_WriteEnable();

SPI_NSS_Begin();

/* 發送扇區擦除命令 */

SPI_SendData(W25X_SectorErase);

/* 發送扇區的地址,高位先行 */

SPI_SendData((addr & 0xff0000) >> 16);

SPI_SendData((addr & 0xff00) >> 8);

SPI_SendData(addr & 0xff);

SPI_NSS_Stop();

/* 最后也要等待FLASH處理完這次的信號再退出 */

FLASH_WaitBusy();

}


扇區的擦除之前要發送一個寫使能信號,先發送擦除指令,然后發送要擦除扇區的地址(分三個字節發出去),高位先行。


扇區上的內容不是1就是0,擦除的過程就是寫1的過程(將一個扇區全部寫1),因為在寫入數據的時候,可以將1寫為0,但不能將0寫為1.


寫入數據

/************按頁寫入數據,但寫入之前要進行擦除***********/

void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size)

{

/* 開始的時候要發送寫使能信號 */

FLASH_WriteEnable();

SPI_NSS_Begin();

/* 發送頁寫入命令 */

SPI_SendData(W25X_PageProgram);

/* 發送寫入的地址,高位先行 */

SPI_SendData((addr & 0xff0000) >> 16);

SPI_SendData((addr & 0xff00) >> 8);

SPI_SendData(addr & 0xff);

/* 逐位發送數據 */

while(size--)

{

SPI_SendData(*pBuffer);

pBuffer++;

}

SPI_NSS_Stop();

/* 最后也要等待FLASH處理完這次的信號再退出 */

FLASH_WaitBusy();

}


在執行寫入數據的時候函數的參數有三部分:

1.要寫入的地址

2.要寫入數據的首地址

3.要寫入數據的大小

函數在執行的過程中,首先發送一個寫使能信號,然后發送寫數據指令,緊接著發送數據要寫入的地址,然后就是逐位發送數據了,函數最后等FLASH處理完這次操作再退出。


讀取數據

/**********************讀取指定地址、指定長度的數據******************/

/* 因為讀取在了指針中,所以不需要返回值 */

void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)

{

SPI_NSS_Begin();

/* 發送讀取命令 */

SPI_SendData(W25X_ReadData);

/* 發送讀取數據的地址,高位先行 */

SPI_SendData((addr & 0xff0000) >> 16);

SPI_SendData((addr & 0xff00) >> 8);

SPI_SendData(addr & 0xff);

/* 逐位讀取數據到指針上 */

while(size--)

{

*pBuffer=SPI_SendData(Dummy_Byte);

pBuffer++;

}

SPI_NSS_Stop();

}


在執行讀出數據的時候函數的參數也有三部分:

1.要讀出的地址

2.讀出到指定地址

3.讀出數據的大小

函數執行過程,首先發送讀取指令(這時就不用發送寫使能了),然后讀取數據的地址,然后將數據逐位讀取在固定地址中(地址最好是全局變量),使用時再從全局變量地址中獲取數據。

這里涉及到函數的返回值問題,具體分析鏈接:返回多個變量怎么辦


有一個問題當時困擾了博主一天,那就是發送和讀取數據時,怎么把數據返回到主函數中,解決方法是,創建倆個全局變量數組,一個負責發送數據、另一個負責接收數據,這樣就ok了

附上主函數


#include "stm32f10x.h"

#include "usart.h"

#include "flash.h"


uint8_t  Rx[100];

uint8_t  Tx[]="小全全的實驗終于好了...",n;


int main(void)

{

DEBUG_USART_Config();

SPI_Config();

  printf("歡迎來到小全全的FLASH實驗n");

printf("FLASH的ID為0x%Xn",FLASH_ReadID());

FLASH_SectorErase(0x00000);

n=sizeof(Tx);

n--;

  FLASH_PageWrite(0x00000 ,Tx ,n);

FLASH_BufferRead(0x00000 ,Rx ,n);

printf("接收到數據為%sn",Rx);

while(1)

{

;

}

}

進入單片機查看更多內容>>
相關視頻
  • TI 新一代 C2000? 微控制器:全方位助力伺服及馬達驅動應用

  • MSP430電容觸摸技術 - 防水Demo演示

  • 直播回放: Microchip Timberwolf? 音頻處理器在線研討會

  • 新唐 8051單片機教程

  • 基于靈動MM32W0系列MCU的指夾血氧儀控制及OTA升級應用方案分享

  • 基于靈動MM32SPIN系列MCU的無感FOC便攜冰箱應用方案分享

    相關電子頭條文章
萝卜大香蕉