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次擦除够用十几年了,判断再擦除反而拖慢了存储速度;另外,因为要判断扇区再擦除,导致代码逻辑变得较为复杂。
换个教程看看。
写操作较为复杂,主要是因为写之前要擦除、最大一次性只能写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); } } } }