你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

基于STM32的语经验分享—SPI读写FLASH

[复制链接]
攻城狮Melo 发布时间:2023-3-18 15:00
FLASH简介
FLASH俗称闪存,和EEPROM一样,都是掉电数据不丢失的非易失行存储器,但FLASH的存储容量普遍大于EEPROM,现在像如U盘、SD卡、SSD固态硬盘以及STM32芯片内部存储程序的设备都是FLASH类型的存储器。由此可见FLASH对于我们学习和工作的重要性,EEPROM可以实现单字节的擦写,而FLASH都是一大片的擦写,就像是大规模杀伤性武器,其最小擦除单:扇区的大小也是4KB。
我们此次通过SPI对FLASH存储芯片W25Q64进行读写擦除的操作。

对于FLASH内部结构的详细说明博主会专门整理一篇博客来说明,所以关于FLASH芯片的相关原理,本文中只做简单说明,侧重代码部分。
FLASH详细说明的博客链接:(没有链接就说明还没有整理出)

W25Q64
W25Q64简介

就长这么个样子

20190814195953540.png

STM32内部原理图如下:

20190814200413370.png


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的控制指令:

20190814200912265.png

表中第一列为指令名,第二列为相应的指令代码,第三列及后面的内容根据指令的不同而意义不同,其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,不带括号的则为主机向 FLASH 传输。表中“A0~A23” 指 FLASH 芯片内部存储器组织的地址; “M0~M7” 为厂商号( MANUFACTURER ID); “ID0-ID15”为 FLASH 芯片的ID;“dummy”指该处可为任意数据;“D0~D7” 为 FLASH 内部存储矩阵的内容。
看起来很复杂的样子,其实只要在需要执行相应操作时来查这个表,只要能够理解这些指令的使用方法,FLASH就算学会了。

例如:要知道FLASH的ID,那就在指令中找对应的取ID指令“JEDEC ID”,仔细解读这个指令
20190814201942797.png

可以看出对应的指令代码为“9F”,后面的三个字节带括号,代表这三个字节就是FLASH向STM32发送的数据,即这三个字节就是FLASH的ID,然后使用SPI进行读取就可以了。

我们一般是将这些指令宏定义在头文件中,便于使用:
  1. #define W25X_WriteEnable                      0x06
  2. #define W25X_WriteDisable                      0x04
  3. #define W25X_ReadStatusReg                      0x05
  4. #define W25X_WriteStatusReg                      0x01
  5. #define W25X_ReadData                              0x03
  6. #define W25X_FastReadData                      0x0B
  7. #define W25X_FastReadDual                      0x3B
  8. #define W25X_PageProgram                      0x02
  9. #define W25X_BlockErase                              0xD8
  10. #define W25X_SectorErase                      0x20
  11. #define W25X_ChipErase                              0xC7
  12. #define W25X_PowerDown                              0xB9
  13. #define W25X_ReleasePowerDown              0xAB
  14. #define W25X_DeviceID                              0xAB
  15. #define W25X_ManufactDeviceID         0x90
  16. #define W25X_JedecDeviceID                      0x9F

复制代码

FLASH内部存储结构
FLASH的存储矩阵如图:其内存分为128块,每一块都有16个扇区,每个扇区大小为4KB,擦除数据的时候是以扇区为基本单位的。

20190814203311343.png

代码讲解
代码都是博主亲手写出来的,可以运行。
代码部分会用到SPI的代码,关于SPI的说明之前整理过:SPI详解

读取芯片ID
  1. /************************读取芯片ID*************************************/
  2. uint32_t FLASH_ReadID(void)
  3. {
  4.         uint32_t temp,temp1,temp2,temp3;
  5.         SPI_NSS_Begin();
  6.         /* 发送取ID指令 */
  7.         SPI_SendData( W25X_JedecDeviceID);
  8.         /* FLASH会连续发送三个字节数据 */
  9.         temp1=SPI_SendData(Dummy_Byte);
  10.         temp2=SPI_SendData(Dummy_Byte);
  11.         temp3=SPI_SendData(Dummy_Byte);
  12.         SPI_NSS_Stop();
  13.         /* 高为先行,将三个字节整理在一起 */
  14.         temp=temp1<<16 | temp2<<8 | temp3;
  15.         return temp;
  16. }
复制代码

由于SPI是全双工通信,所以接收数据和发送数据用的是同一个函数

发送写使能信号
  1. /****************发送写使能信号****************************/
  2. void FLASH_WriteEnable(void)
  3. {
  4.         SPI_NSS_Begin();
  5.         /* 发送相应的指令代码 */
  6.         SPI_SendData(W25X_WriteEnable);
  7.         SPI_NSS_Stop();
  8. }
复制代码

在向FLASH中执行写操作之前,都要进行写使能操作,通过发送写使能指令到FLASH来实现

等待FLASH不忙
  1. /******************等待,直到不忙***********************************/
  2. void FLASH_WaitBusy(void)
  3. {
  4.         uint8_t StatusReg=0x01;
  5.         SPI_NSS_Begin();
  6.    /* 读取状态寄存器中的数据,判断忙标志0x01位 置位代表忙 */
  7.         SPI_SendData(W25X_ReadStatusReg);      
  8.    /* 只读取状态寄存器的BUSY位,即第一位 */
  9.         while((StatusReg & 0x01) == 1)
  10.                 StatusReg=SPI_SendData(Dummy_Byte);
  11.         SPI_NSS_Stop();
  12. }


复制代码

FLASH在通讯的过程中需要一定的时间来执行操作,在这期间,传输数据是无效的,因为FLASH忙着呢,所以我们就要有一个函数来专门等!等到FLASH不忙了,再进行通讯,那怎么等呢?FLASH不忙了会给出一个信号——将状态寄存器的BUSY位重置(也就是0),所以我们需要不断的来检测状态寄存器中的BUSY位是否置位,利用读取寄存器状态的指令来获取状态寄存器当下的状态,然后根据寄存器的BUSY位(第1位)来判断FLASH是否处于忙碌状态。
简单来说,这就是个延时函数,延时直到FLASH空闲,可以进行下一步传输。


擦除扇区

  1. /******擦除扇区的内容,切记地址要对其到4kB,每个扇区的大小都是4KB********/
  2. void FLASH_SectorErase(uint32_t addr)
  3. {
  4.         /* 开始的时候要发送写使能信号*/
  5.         FLASH_WriteEnable();
  6.         SPI_NSS_Begin();
  7.         /* 发送扇区擦除命令 */
  8.         SPI_SendData(W25X_SectorErase);
  9.         /* 发送扇区的地址,高位先行 */
  10.         SPI_SendData((addr & 0xff0000) >> 16);
  11.         SPI_SendData((addr & 0xff00) >> 8);
  12.         SPI_SendData(addr & 0xff);
  13.         SPI_NSS_Stop();
  14.         /* 最后也要等待FLASH处理完这次的信号再退出 */
  15.         FLASH_WaitBusy();
  16. }

复制代码

扇区的擦除之前要发送一个写使能信号,先发送擦除指令,然后发送要擦除扇区的地址(分三个字节发出去),高位先行。
扇区上的内容不是1就是0,擦除的过程就是写1的过程(将一个扇区全部写1),因为在写入数据的时候,可以将1写为0,但不能将0写为1.


写入数据
  1. /************按页写入数据,但写入之前要进行擦除***********/
  2. void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size)
  3. {
  4.         /* 开始的时候要发送写使能信号 */
  5.         FLASH_WriteEnable();
  6.         SPI_NSS_Begin();
  7.         /* 发送页写入命令 */
  8.         SPI_SendData(W25X_PageProgram);
  9.         /* 发送写入的地址,高位先行 */
  10.         SPI_SendData((addr & 0xff0000) >> 16);
  11.         SPI_SendData((addr & 0xff00) >> 8);
  12.         SPI_SendData(addr & 0xff);
  13.         /* 逐位发送数据 */
  14.         while(size--)
  15.         {
  16.                 SPI_SendData(*pBuffer);
  17.                 pBuffer++;
  18.         }
  19.         SPI_NSS_Stop();
  20.         /* 最后也要等待FLASH处理完这次的信号再退出 */
  21.         FLASH_WaitBusy();
  22. }

复制代码

在执行写入数据的时候函数的参数有三部分:
1.要写入的地址
2.要写入数据的首地址
3.要写入数据的大小
函数在执行的过程中,首先发送一个写使能信号,然后发送写数据指令,紧接着发送数据要写入的地址,然后就是逐位发送数据了,函数最后等FLASH处理完这次操作再退出。


读取数据
  1. /**********************读取指定地址、指定长度的数据******************/
  2. /* 因为读取在了指针中,所以不需要返回值 */
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
  4. {
  5.         SPI_NSS_Begin();
  6.         /* 发送读取命令 */
  7.         SPI_SendData(W25X_ReadData);
  8.         /* 发送读取数据的地址,高位先行 */
  9.         SPI_SendData((addr & 0xff0000) >> 16);
  10.         SPI_SendData((addr & 0xff00) >> 8);
  11.         SPI_SendData(addr & 0xff);
  12.         /* 逐位读取数据到指针上 */
  13.         while(size--)
  14.         {
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);
  16.                 pBuffer++;
  17.         }
  18.         SPI_NSS_Stop();
  19. }

复制代码

在执行读出数据的时候函数的参数也有三部分:
1.要读出的地址
2.读出到指定地址
3.读出数据的大小
函数执行过程,首先发送读取指令(这时就不用发送写使能了),然后读取数据的地址,然后将数据逐位读取在固定地址中(地址最好是全局变量),使用时再从全局变量地址中获取数据。
这里涉及到函数的返回值问题,具体分析链接:返回多个变量怎么办


有一个问题当时困扰了博主一天,那就是发送和读取数据时,怎么把数据返回到主函数中,解决方法是,创建俩个全局变量数组,一个负责发送数据、另一个负责接收数据,这样就ok了
附上主函数
  1. /**********************读取指定地址、指定长度的数据******************/
  2. /* 因为读取在了指针中,所以不需要返回值 */
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
  4. {
  5.         SPI_NSS_Begin();
  6.         /* 发送读取命令 */
  7.         SPI_SendData(W25X_ReadData);
  8.         /* 发送读取数据的地址,高位先行 */
  9.         SPI_SendData((addr & 0xff0000) >> 16);
  10.         SPI_SendData((addr & 0xff00) >> 8);
  11.         SPI_SendData(addr & 0xff);
  12.         /* 逐位读取数据到指针上 */
  13.         while(size--)
  14.         {
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);
  16.                 pBuffer++;
  17.         }
  18.         SPI_NSS_Stop();
  19. }

复制代码

————————————————
版权声明:Aspirant-GQ

如有侵权请联系删除



收藏 评论0 发布时间:2023-3-18 15:00

举报

0个回答
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版