SPIFlash-W25QXX以及STM32内部Flash使用总结

2023-10-28 10:50

本文主要是介绍SPIFlash-W25QXX以及STM32内部Flash使用总结,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

W25QXX简介

W25QXX,后面的XX指的是Mbit

常见的型号有:

W25Q80

W25Q16

W25Q32

W25Q64

W25Q128

注意80是表示8而不是80

所以,换算成字节数,从上到下为:

1MB

2MB

4MB

8MB

16MB

整个flash分成多个块,一个块分成多个扇区,一个扇区分成多个页。

以W25Q64为例,8MB,共分为128个块(block),即每个块64KB,每个块又分为16个扇区(sector),那么每个扇区就是4KB

各型号分成的块和扇区大小是一样的,只是不同大小的flash分成块的数量不一样。

比如W25Q64分成了128个块,W25Q128系列就分成了256个块。

一个扇区4K,有多大呢?4K,也就是4096个字节,已知每个中文占两个字节,也就是一个扇区能存储一篇2048字的作文。

其实每个扇区下面,还分了16个页,也就是每页4*1024/16 = 256字节。

接下来讲解常用操作。

擦除扇区

通常:扇区是擦除的最小单位,也可以一次性全片擦除。

注意几个地方

发送24位地址,因为一次只能发送1个字节,所以需要发3次,如果配置的是MSB优先,则先发最高8位,再发中间8位,再发低8位。

这里难以理解的是“扇区地址”乘以4096

事实上,这里传入的不是扇区地址,而是扇区的id号,所以需要再乘以4096(每个扇区4Kbyte字节),得出该扇区的起始地址。

128个块,每块16个扇区,则一共有2048个扇区,则扇区id从0到2047。

怎么知道一个地址落在哪个扇区内呢?

通常,给一个地址,比如0x0000FF(255),用这个地址除以4096,得到整数结果,就是所在的扇区号。因为一个扇区4096个字节,类似于进制每4096进1,所以除以进制后取整就能得到扇区号。再把扇区号乘以4096,就能得到扇区的首地址。

比如地址0x7FFFFF(8,388,607),除以4096,结果为2,047.999755859375…,取整为2047,刚好是最后一个扇区id,再把2047乘以4096,就能得到最后一个扇区的起始地址8,384,512,也就是0x7FF000,经验证是正确的。

其实很好理解,如果一个扇区是10单位的大小,那么0~9就会落入第一个扇区,10~19就会落入第二个扇区……类比理解。

地址/4096可以得到扇区号;地址%4096可以得到在该扇区中的偏移量。

全片擦除

读数据

W25Q128 支持以任意地址(但是不能超过 W25Q128 的地址范围)开始读取数据。循环读数据时, 其地址会自动增加的,要注意不能超过了 W25Q128 的地址范围,否则读出来的数据就不是你想要的数据了。

理论上,只要能读,可以一直从头读到尾。

写数据

写操作要注意

写之前,一定要判断待写入区域是否没被写过,即是否全为0xFF,为什么要做这个判断呢?这是因为flash的特性:FLASH未写入时里面的数据为全1,即0xFF,重要的是,写入时,只支持把1写成0,不能把0写成1,如果要把0变成1,只能擦除后再整体写入。

如果之前已经写过,并且没有被擦除,那么,我写入数据时,因为之前的0不能变成1,所以会导致存储的数据是不准确的。

一开始,我还在想,如果一个地方有数据,我为了写入新的数据,不就把原来的数据给覆盖了?如果判断某个地方有被写入过,那就找个别的空闲地方写入不就行了?就像内存一样,一个地址在某时刻被某个数据用了,就不应该再分配给其他数据用了。否则会引起不可预知的错误。

后来我想,Flash的应用场景应该是这样的:对于每一个应用程序,Flash会划分一块区域给它使用,这样,各应用数据的存储区域各不相干。这种情况下,写数据其实就是为了更改特定应用的数据,此时,就是用新数据覆盖旧数据,是合理的。比如,用JLINK下载程序时,就是会下载到指定起始地址的一段Flash中,每次程序更改重新下载时,会先擦除原来的数据,然后再写入新的数据。

那么,写数据时,是否可以直接擦除目标区域,然后再写入新的数据呢?

之所以提出这种疑问,是因为在看正点原子的视频时,他们的做法是这样的:写入一段数据时,会先去判断要写入数据的地址部分是否被擦除,如果已经处于擦除状态,那么就直接写入,如果没有擦除,就先将这个扇区的内容读出来放到缓存里,然后将目标区域擦除,再将要写的数据合并到缓存中,再写入刚才的目标扇区。

于是,我产生一个疑问,直接擦除再写入不就行了?为什么还要先读出来,擦除后再写入?

难道是为了实现局部修改?即只让修改的地方被重新写入。考虑一种极端情况,一个扇区中,只需要修改一个字节,一种做法是,我将整个扇区直接擦除,再直接写入整个扇区的新内容。还有一种做法就是,判断这个扇区是不是处于擦除状态,不是的话就先读出数据,在缓存中将这个数据修改,同时擦除原来的区域,再将缓存中的数据写入。。。。。。。发现这里还是要重新把整个扇区的内容写入一遍,也没得到优化呀。。。。。。

有点复杂,不好理解。

原子哥程序的解读,直接参考:

W25Qxx系列FLASH初级使用指南(W25Q64 W25Q128等) - 知乎

拉到最后一节,有详细描述,虽然图片看不太清晰。

有个点需要注意,就是判断再擦除并不是必要的;保护原来的数据也并不是必要的。

有几个理由:一就是各功能数据会分开存放,不会互相干扰,我只会覆盖原来的旧数据,没必要保护;再就是10w次擦除差不多够用了,判断再擦除反而拖慢了存储速度;另外,因为要判断扇区再擦除,导致代码逻辑变得较为复杂。

//SPI在一页(共65536页)内写入少于256个字节的数据
//在指定地址开始写入最大256字节的数据
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!	 
//该函数没有判断是否超过一页,只是单纯提供了一个写页函数,所以,该函数并不安全
//只是是用来被其他函数调用的,具体的判断,会在调用函数中进行。
void W25QXX_Write_Page(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite)
{uint16_t i;  W25QXX_Write_Enable();                  //SET WELW25QXX_ENABLE_CS;                            //使能器件SPI2_ReadWriteByte(W25X_PageProgram);      //发送写页命令SPI2_ReadWriteByte((uint8_t)((WriteAddr)>>16)); //发送24bit地址SPI2_ReadWriteByte((uint8_t)((WriteAddr)>>8));   SPI2_ReadWriteByte((uint8_t)WriteAddr);   for(i=0; i<NumByteToWrite; i++)SPI2_ReadWriteByte(pBuffer[i]);//循环写数W25QXX_DISABLE_CS;                            //取消片选W25QXX_Wait_Busy();					   //等待写入结束
}//无检验(是说在这个函数里没有校验是否被擦除过)写SPI FLASH 
//如果不需要检验擦除问题,可以直接使用
//具有自动换页功能 
//在指定地址开始写入指定长度的数据,但是要确保地址不越界!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
//CHECK OK
void W25QXX_Write_NoCheck(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite)
{uint16_t pageremain;//WriteAddr%256可以得到该地址在页中的偏移量,再用256减去该值,就能得到该页往后剩余的容量pageremain=256-WriteAddr%256; //单页剩余的字节数if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//如果要写入的字节数小于该页剩余字节数while(1){	   W25QXX_Write_Page(pBuffer,WriteAddr,pageremain);if(NumByteToWrite==pageremain)break;//要写入的字节数小于页中剩余字节数,所以写入结束了else //NumByteToWrite>pageremain,如果要写的字节数超过了该页{pBuffer+=pageremain;//指针指向已写入数据的下一个字节WriteAddr+=pageremain;//同时待写入的地址也同步增长,这里其实就是完成了手动换到了下一页//如果不是手动换,继续写会回到当前页的开头覆盖页起始处的数据NumByteToWrite-=pageremain;			  //减去已经写入了的字节数if(NumByteToWrite>256)pageremain=256; //一次可以写入256个字节else pageremain=NumByteToWrite; 	  //最后肯定有数据是不会超过256个字节的}};	    
}//写SPI FLASH
//在指定地址开始写入指定长度的数据
//该函数带擦除操作!如果不用判断擦除,可以不使用该函数。
//必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)					
//NumByteToWrite:要写入的字节数(最大65535)
uint8_t W25QXX_BUFFER[4096];//定义扇区大小的缓存	 
void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite)   
{ uint32_t secpos;uint16_t secoff;uint16_t secremain;	   uint16_t i;    uint8_t * W25QXX_BUF;	  W25QXX_BUF=W25QXX_BUFFER;secpos=WriteAddr/4096;//得到扇区地址,也就是扇区id号secoff=WriteAddr%4096;//在扇区内的偏移secremain=4096-secoff;//扇区剩余空间大小//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//要写入的内容小于扇区剩余空间while(1) {	W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容到缓存中for(i=0;i<secremain;i++)//校验后续待写入的空间是否被擦除过{if(W25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除,跳出判断  }if(i<secremain)//因为i在没有检查完就跳出了,所以结论是需要擦除{W25QXX_Erase_Sector(secpos);//擦除这个扇区for(i=0;i<secremain;i++)	   //复制{W25QXX_BUF[i+secoff]=pBuffer[i];//待写入数据写入缓存中扇区后续的空间//扇区中写入位置之前的数据没有被破坏}W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区,将新的数据整个写入扇区}else W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写已经擦除了的,直接写入扇区剩余区间. 				   if(NumByteToWrite==secremain)break;//数据大小在当前扇区剩余空间内,写入结束了else//写入未结束{secpos++;//扇区地址增1secoff=0;//偏移位置为0pBuffer+=secremain;  //指针偏移WriteAddr+=secremain;//写地址偏移	   NumByteToWrite-=secremain;				//减去已经写入的字节数if(NumByteToWrite>4096)secremain=4096;	//下一个扇区还是写不完else secremain=NumByteToWrite;			//下一个扇区可以写完了}//这里的逻辑和按页写类似,不过这里之所以在按页写的基础上再按扇区写,是因为要判断扇区是否擦除。};
}

换个教程看看。

写操作较为复杂,主要是因为写之前要擦除、最大一次性只能写256 Byte(即一个完整的page,硬件不会自动换page,所以要编程)所以要考虑换页、写操作给的指令中的起始地址再某一页的哪个位置等等。

在W25Q64数据手册中,写入数据是页面编程指令02H

单次指令最多只能写入256个字节

对于页对齐这个概念,我一开始没太明白,一次只能写一页,接着往后写多写几页不就行了?反正地址也是会继续增加。看了文档的说明才明白,如果一次写超过了一页,那么数据会回到页开头,覆盖原来的数据,而不是地址继续增加。

页对齐是说,是否是从页的起始地址写的。

那么,这个写入函数到底按照怎么样的思路去写?

参考:

这里的实现就是基于页对齐去写的,擦除扇区是在main函数中要写入数据之前直接擦除的,并没有做什么判断。

/** @name   SPI_Flash_WritePage* @brief  写入页(256Bytes),写入长度不超过256字节* @param  pWriteBuffer:待写入数据的指针*         WriteAddr   :写入地址*         WriteLength :写入数据长度,必须小于等于SPI_FLASH_PerWritePageSize(256Bytes)* @retval None
*/
static void SPI_Flash_WritePage(uint8_t* pWriteBuffer, uint32_t WriteAddr, uint16_t WriteLength)
{//检测flash是否处于忙碌状态SPI_Flash_WaitForWriteEnd();//Flash写使能,允许写入SPI_Flash_WriteEnable();//选择Flash芯片: CS输出低电平CLR_SPI_Flash_CS;//发送命令:页面编程SPI_Flash_WriteByte(W25X_PageProgram);	//发送地址高字节SPI_Flash_WriteByte((WriteAddr & 0xFF0000) >> 16);//发送地址中字节SPI_Flash_WriteByte((WriteAddr & 0xFF00) >> 8);//发送地址低字节SPI_Flash_WriteByte(WriteAddr & 0xFF);if(WriteLength > SPI_FLASH_PageSize){WriteLength = SPI_FLASH_PageSize;printf("Error: Flash每次写入数据不能超过256字节!\n");}//开始写入数据while (WriteLength--){/* 读取一个字节*/SPI_Flash_WriteByte(*pWriteBuffer);/* 指向下一个字节缓冲区 */pWriteBuffer++;}//禁用Flash芯片: CS输出高电平SET_SPI_Flash_CS;//等待写入完毕SPI_Flash_WaitForWriteEnd();
}
/** @name   SPI_Flash_WriteUnfixed* @brief  写入不固定长度数据* @param  pWriteBuffer:待写入数据的缓存指针*         WriteAddr   :写入地址*         WriteLength :写入数据长度* @retval None
*/
static void SPI_Flash_WriteUnfixed(uint8_t* pWriteBuffer, uint32_t WriteAddr, uint32_t WriteLength)
{uint32_t PageNumofWirteLength     = WriteLength / SPI_FLASH_PageSize;            //待写入页数uint8_t  NotEnoughNumofPage       = WriteLength % SPI_FLASH_PageSize;            //不足一页的数量uint8_t  WriteAddrPageAlignment   = WriteAddr % SPI_FLASH_PageSize;              //如果取余为0,则地址页对齐,可以写连续写入256字节uint8_t  NotAlignmentNumofPage    = SPI_FLASH_PageSize - WriteAddrPageAlignment; //地址不对齐部分,最多可以写入的字节数//写入地址页对齐if(WriteAddrPageAlignment == 0){//待写入数据不足一页if(PageNumofWirteLength == 0){SPI_Flash_WritePage(pWriteBuffer,WriteAddr,WriteLength);}//待写入数据超过一页else{//先写入整页while(PageNumofWirteLength--){SPI_Flash_WritePage(pWriteBuffer,WriteAddr,SPI_FLASH_PageSize);pWriteBuffer += SPI_FLASH_PageSize;WriteAddr    += SPI_FLASH_PageSize;}//再写入不足一页的数据if(NotEnoughNumofPage > 0){SPI_Flash_WritePage(pWriteBuffer,WriteAddr,NotEnoughNumofPage);}}}//写入地址与页不对齐else{//待写入数据不足一页if(PageNumofWirteLength == 0){//不足一页的数据 <= 地址不对齐部分if(NotEnoughNumofPage <= NotAlignmentNumofPage){SPI_Flash_WritePage(pWriteBuffer,WriteAddr,WriteLength);}//不足一页的数据 > 地址不对齐部分else{//先写地址不对齐部分允许写入的最大长度SPI_Flash_WritePage(pWriteBuffer,WriteAddr,NotAlignmentNumofPage);				pWriteBuffer += NotAlignmentNumofPage;WriteAddr    += NotAlignmentNumofPage;//再写没写完的数据SPI_Flash_WritePage(pWriteBuffer,WriteAddr,NotEnoughNumofPage-NotAlignmentNumofPage);}}//待写入数据超过一页else{//先写地址不对齐部分允许写入的最大长度,地址此时对齐了SPI_Flash_WritePage(pWriteBuffer,WriteAddr,NotAlignmentNumofPage);				pWriteBuffer += NotAlignmentNumofPage;WriteAddr    += NotAlignmentNumofPage;//地址对其后,重新计算写入页数与不足一页的数量WriteLength           -= NotAlignmentNumofPage;PageNumofWirteLength   = WriteLength / SPI_FLASH_PageSize;            //待写入页数NotEnoughNumofPage     = WriteLength % SPI_FLASH_PageSize; //先写入整页while(PageNumofWirteLength--){SPI_Flash_WritePage(pWriteBuffer,WriteAddr,SPI_FLASH_PageSize);pWriteBuffer += SPI_FLASH_PageSize;WriteAddr    += SPI_FLASH_PageSize;}//再写入不足一页的数据if(NotEnoughNumofPage > 0){SPI_Flash_WritePage(pWriteBuffer,WriteAddr,NotEnoughNumofPage);}}}
}

STM32内部Flash

如果硬件上没有外接FLASH,并且有少量数据需要存储,就可以使用STM32自带的FLASH,关于FLASH的介绍,可以自行查阅数据手册。

不同型号的 STM32,其 FLASH 容量也有所不同,最小的只有 16K 字节,最大的则达到了

1024K 字节。正点原子战舰 STM32 开发板选择的 STM32F103ZET6 FLASH 容量为 512K 字节,属于大容量产品(另外还有中容量和小容量产品),大容量产品的闪存模块组织如图 39.1.1 所示:

STM32的flash只划分为两个层级,块和页,页是最小的擦除单位。

注意:

在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正确地进行;既在进行写或擦除操作时,不能进行代码或数据的读取操作;

读写时均可在通用地址空间直接寻址,但是写之前要判断擦除,而且要页对齐;

STM32 闪存的编程每次必须写入 16 位,不能单纯的写入 8 位数据哦!写入任何非半字的数据,FPEC 都会产生总线错误;

开发时,直接拿正点原子的例程来用即可。

#include "stmflash.h"
#include "delay.h"
#include "usart.h"//	 
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK战舰STM32开发板
//STM32 FLASH 驱动代码	   
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//修改日期:2012/9/13
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2009-2019
//All rights reserved									  
////读取指定地址的半字(16位数据)
//faddr:读地址(此地址必须为2的倍数!!)
//返回值:对应数据.
u16 STMFLASH_ReadHalfWord(u32 faddr)
{return *(vu16*)faddr; 
}
#if STM32_FLASH_WREN	//如果使能了写   
//不检查的写入
//WriteAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数   
void STMFLASH_Write_NoCheck(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
{ 			 		 u16 i;for(i=0;i<NumToWrite;i++){FLASH_ProgramHalfWord(WriteAddr,pBuffer[i]);WriteAddr+=2;//地址增加2.}  
} 
//从指定地址开始写入指定长度的数据
//WriteAddr:起始地址(此地址必须为2的倍数!!)
//pBuffer:数据指针
//NumToWrite:半字(16位)数(就是要写入的16位数据的个数.)
#if STM32_FLASH_SIZE<256
#define STM_SECTOR_SIZE 1024 //字节
#else 
#define STM_SECTOR_SIZE	2048
#endif		 
u16 STMFLASH_BUF[STM_SECTOR_SIZE/2];//最多是2K字节
void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)	
{u32 secpos;	   //扇区地址u16 secoff;	   //扇区内偏移地址(16位字计算)u16 secremain; //扇区内剩余地址(16位字计算)	   u16 i;    u32 offaddr;   //去掉0X08000000后的地址if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址FLASH_Unlock();						//解锁offaddr=WriteAddr-STM32_FLASH_BASE;		//实际偏移地址.secpos=offaddr/STM_SECTOR_SIZE;			//扇区地址  0~127 for STM32F103RBT6secoff=(offaddr%STM_SECTOR_SIZE)/2;		//在扇区内的偏移(2个字节为基本单位.)secremain=STM_SECTOR_SIZE/2-secoff;		//扇区剩余空间大小   if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围while(1) {	STMFLASH_Read(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容for(i=0;i<secremain;i++)//校验数据{if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除  	  }if(i<secremain)//需要擦除{FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区for(i=0;i<secremain;i++)//复制{STMFLASH_BUF[i+secoff]=pBuffer[i];	  }STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区  }else STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//写已经擦除了的,直接写入扇区剩余区间. 				   if(NumToWrite==secremain)break;//写入结束了else//写入未结束{secpos++;				//扇区地址增1secoff=0;				//偏移位置为0 	 pBuffer+=secremain;  	//指针偏移WriteAddr+=(secremain*2);	//写地址偏移	   NumToWrite-=secremain;	//字节(16位)数递减if(NumToWrite>(STM_SECTOR_SIZE/2))secremain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完else secremain=NumToWrite;//下一个扇区可以写完了}	 };	FLASH_Lock();//上锁
}
#endif//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数
void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead)   	
{u16 i;for(i=0;i<NumToRead;i++){pBuffer[i]=STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节.ReadAddr+=2;//偏移2个字节.	}
}//
//WriteAddr:起始地址
//WriteData:要写入的数据
void Test_Write(u32 WriteAddr,u16 WriteData)   	
{STMFLASH_Write(WriteAddr,&WriteData,1);//写入一个字 
}
#ifndef __STMFLASH_H__
#define __STMFLASH_H__
#include "sys.h"  
//	 
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK战舰STM32开发板
//STM32 FLASH 驱动代码	   
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//修改日期:2012/9/13
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2009-2019
//All rights reserved									  
////
//用户根据自己的需要设置
#define STM32_FLASH_SIZE 512 	 		//所选STM32的FLASH容量大小(单位为K)
#define STM32_FLASH_WREN 1              //使能FLASH写入(0,不是能;1,使能)
////FLASH起始地址
#define STM32_FLASH_BASE 0x08000000 	//STM32 FLASH的起始地址
//FLASH解锁键值u16 STMFLASH_ReadHalfWord(u32 faddr);		  //读出半字  
void STMFLASH_WriteLenByte(u32 WriteAddr,u32 DataToWrite,u16 Len);	//指定地址开始写入指定长度的数据
u32 STMFLASH_ReadLenByte(u32 ReadAddr,u16 Len);						//指定地址开始读取指定长度数据
void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite);		//从指定地址开始写入指定长度的数据
void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead);   		//从指定地址开始读出指定长度的数据//测试写入
void Test_Write(u32 WriteAddr,u16 WriteData);								   
#endif

地址怎么选才能不和程序重合呢?

首先,程序存储一般都是0x08000000作为其实地址,我们可以根据map文件中的flash占用大小来计算一下程序要用多少地址。keil设置里也能看提供给程序使用的地址。

从0x08000000~0x08040000,剩下的就可以用来存储少量数据。

一般,可以拿flash的末尾几个页来用,比如最后一页的起始地址0x0807F800

不过,要注意,地址一定要是偶数。

另外,还有个问题,需要补充下。不管是针对内部还是外部flash。

假如现在有很多方案需要存储,各方案是可以独立修改的,如果我存储在一个页里面,那么我改其中一个方案然后重新存储时,就会将其他方案都擦掉;如果我分成多个页去存储,那又很浪费空间。

那如何能充分利用空间,又能不至于改其中一个方案就影响到其他方案的数据呢?

方法是,擦除之前,先判断该区域有没有被擦除过,如果已经处于擦除状态,则直接写,如果还没擦除,则先将数据读出来放到一个缓存里再擦除,接着在缓存里将目标数据修改掉,再将其存入刚才擦除的区域。这样,就能保证其他数据不丢失。

经验证,正点原子采用的是第二种方案。

//先写入10个半字的数据
STMFLASH_Write(CUSTOM_SCHEME_ADDR, testData, SPACE_PER_CUSTOM_SCHEME);
//读出来看看对不对
STMFLASH_Read(CUSTOM_SCHEME_ADDR, testData2, SPACE_PER_CUSTOM_SCHEME);
for(uint8_t i = 0; i < SPACE_PER_CUSTOM_SCHEME; i++)
{printf("%d ", testData2[i]);
}
printf("\r\n");
//再修改中间几个半字的数据
STMFLASH_Write(CUSTOM_SCHEME_ADDR + 8, testData3, 6);
//再读出来对比
STMFLASH_Read(CUSTOM_SCHEME_ADDR, testData4, SPACE_PER_CUSTOM_SCHEME);
for(uint8_t i = 0; i < SPACE_PER_CUSTOM_SCHEME; i++)
{printf("%d ", testData4[i]);
}

实际开发中根据要存储的数据量来选择合适的方式。是分页存储,还是在同一页存储,这取决于你的需求。

这篇关于SPIFlash-W25QXX以及STM32内部Flash使用总结的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/292699

相关文章

Pydantic中Optional 和Union类型的使用

《Pydantic中Optional和Union类型的使用》本文主要介绍了Pydantic中Optional和Union类型的使用,这两者在处理可选字段和多类型字段时尤为重要,文中通过示例代码介绍的... 目录简介Optional 类型Union 类型Optional 和 Union 的组合总结简介Pyd

Vue3使用router,params传参为空问题

《Vue3使用router,params传参为空问题》:本文主要介绍Vue3使用router,params传参为空问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录vue3使用China编程router,params传参为空1.使用query方式传参2.使用 Histo

使用Python自建轻量级的HTTP调试工具

《使用Python自建轻量级的HTTP调试工具》这篇文章主要为大家详细介绍了如何使用Python自建一个轻量级的HTTP调试工具,文中的示例代码讲解详细,感兴趣的小伙伴可以参考一下... 目录一、为什么需要自建工具二、核心功能设计三、技术选型四、分步实现五、进阶优化技巧六、使用示例七、性能对比八、扩展方向建

使用Python实现一键隐藏屏幕并锁定输入

《使用Python实现一键隐藏屏幕并锁定输入》本文主要介绍了使用Python编写一个一键隐藏屏幕并锁定输入的黑科技程序,能够在指定热键触发后立即遮挡屏幕,并禁止一切键盘鼠标输入,这样就再也不用担心自己... 目录1. 概述2. 功能亮点3.代码实现4.使用方法5. 展示效果6. 代码优化与拓展7. 总结1.

使用Python开发一个简单的本地图片服务器

《使用Python开发一个简单的本地图片服务器》本文介绍了如何结合wxPython构建的图形用户界面GUI和Python内建的Web服务器功能,在本地网络中搭建一个私人的,即开即用的网页相册,文中的示... 目录项目目标核心技术栈代码深度解析完整代码工作流程主要功能与优势潜在改进与思考运行结果总结你是否曾经

Linux中的计划任务(crontab)使用方式

《Linux中的计划任务(crontab)使用方式》:本文主要介绍Linux中的计划任务(crontab)使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、前言1、linux的起源与发展2、什么是计划任务(crontab)二、crontab基础1、cro

kotlin中const 和val的区别及使用场景分析

《kotlin中const和val的区别及使用场景分析》在Kotlin中,const和val都是用来声明常量的,但它们的使用场景和功能有所不同,下面给大家介绍kotlin中const和val的区别,... 目录kotlin中const 和val的区别1. val:2. const:二 代码示例1 Java

C++变换迭代器使用方法小结

《C++变换迭代器使用方法小结》本文主要介绍了C++变换迭代器使用方法小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1、源码2、代码解析代码解析:transform_iterator1. transform_iterat

C++中std::distance使用方法示例

《C++中std::distance使用方法示例》std::distance是C++标准库中的一个函数,用于计算两个迭代器之间的距离,本文主要介绍了C++中std::distance使用方法示例,具... 目录语法使用方式解释示例输出:其他说明:总结std::distance&n编程bsp;是 C++ 标准

vue使用docxtemplater导出word

《vue使用docxtemplater导出word》docxtemplater是一种邮件合并工具,以编程方式使用并处理条件、循环,并且可以扩展以插入任何内容,下面我们来看看如何使用docxtempl... 目录docxtemplatervue使用docxtemplater导出word安装常用语法 封装导出方