对SPI FLASH的设计思路

W25Q128 将 16M 的容量分为 256 个块(Block),每个块大小为 64K 字节,每个块又分为 16个扇区(Sector),每个扇区 4K 个字节。 W25Q128 的最少擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。操作需要给 W25Q128 开辟一个至少 4K 的缓存区,对 SRAM 要求比较高,要求芯片必须有 4K 以上 SRAM 才能很好的操作。

我这里主要讲的是FLASH的设计思路,因为网上对于这一方面信息很少。

FLASH分区

首先,我设计的FLASH以扇区为最小分割单位,以块为分区的最小分割单位。这里打个比方,我要把一个FLASH分成3个区,A区,B区,C区。分区有助于我们快速操作数据,索引数据。类似于window里的C盘,E盘,F盘。


A区的分区大小为256K,就是4个块(大小等于单个块大小64K)。
B区的分区大小为1M,就是16个块。
C区的分区大小为4M,就是64个块。


这样我只要计算好每个区的地址范围,根据每个区的起始地址(FLASH所有地址长度统一为32位)我就可以直接操作到对应区。


A区范围 0x00000000 - 0x0003FFFF(一个块64K,就是64*1024= 65536 = 0x10000,A区4个块,就是乘4得出,FLASH第一个区的大小范围)
B区范围 0x00040000 - 0x0013FFFF(B区范围其实是FLASH第五个块开始到第二十个块结束)
C区范围 0x00140000 - 0x0053FFFF


FLASH分区类型

接下来,我将分区分为两个类型。

结构类:指数据长度固定,结构固定。类似于C语言中的结构体或是固定长度的数组。
数据类:指数据长度不固定。类似于JAVA里的List,可变数组长度类型。

比如A区,我设计存放一些基本数据。比如A区第一个扇区存放设备基础信息,第二个扇区存放程序基础信息等等。这时A区就被设计为结构类分区,分区内存放的数据程序知道是什么数据,什么结构。

比如B区,我设计存放一些缓存数据。此类数据长度不定,因此需要以对齐的方式进行存储。而且还需要分区头信息,与扇区头信息来协助程序读写数据。关于分区头信息和扇区头信息我接下来会讲。这时B区就被设计为数据类分区。

结构类分区设计

结构类分区其实很简单,所有数据的长度都是固定的,因此只要计算相关偏移量就可以取到相关数据。

优点:读写快。
缺点:存入的数据结构,数据长度固定。

建议:此类型的分区每个扇区最好对应着一个结构体,可以更加方便的操作数据以及直观了解扇区存储内容。取数据时可以创建一个结构体,然后把缓存里的数据拷贝到结构体指针中。这样就取到了数据,写入也可以这样做。

数据类分区设计

数据类分区主要是数据都是不固定的,因此要通过分区头信息、扇区头信息、和对齐的方式进行数据的存储。

对齐

因为数据长度不定,因此一个扇区一定要以统一长度进行对齐,注意数据要以最长的数据长度进行对齐而且要以2,4,8,16,32,64… 等对齐,可以被4096整除。比如数据有2byte,4byte,8byte。必须要以8byte进行对齐。因此2byte,4byte后面的部分就为空。这也是一种空间换效率的方式。

分区头信息

分区头信息是数据类分区的第一个扇区,此扇区是一个结构类扇区。主要负责记录整个分区整体的状态记录,其实说白了,就是通过分区头信息快速定位到最后一个待使用的扇区。

分区头信息 长度
分区编号 2byte
存储规则(头部覆写,尾部覆写,弃写) 2byte
分区大小 4byte
分区尾起始地址 4byte

扇区头信息

扇区头信息(标准长度为8byte)是数据类分区除第一个扇区以外的每个扇区的第一个对齐长度,如果对齐长度大于标准长度,则后补空。小于标准长度,拆分成多条数据。主要负责记录此扇区的状态记录,其实说白了,就是通过扇区头信息快速定位到你要写入数据的起始地址。

扇区头信息 长度
对齐长度 4byte
扇区尾起始地址 4byte

优点:读写灵活,空间可扩展。
缺点:操作复杂,开发难度大,不便于维护。

对SPI FLASH的设计思路

FLASH存储流程

结构类分区存储没什么好讲的了,只要注意读写一致就可以了。 接下来主要讲一讲数据类分区

结构类分区存储流程

数据类分区存储流程

向数据类分区存数据

  1. 从程序中获取到分区起始地址。
  2. 使用起始地址获取到分区第一个扇区即分区头信息。
  3. 根据分区头信息获取到【分区编号 == 程序中分区编号】无误(防止程序中分区信息没有更新进行了不可描述的异常)。
  4. 根据分区头信息获取到分区尾起始地址,如果分区尾起始地址为0x00000000,结束存数据操作(因为分区头信息的存储规则设置为弃写,详细往下看)。
  5. 使用分区尾起始地址获取到对应扇区的扇区头信息。
  6. 如果 【扇区尾起始地址 == 扇区的结束地址】,那就直接格式化此扇区后,将新的扇区头信息(对齐信息与旧扇区头信息一致,扇区尾起始地址为偏移了此条新数据长度)与新数据组合以后存回至扇区,结束存数据操作
  7. 将扇区数据取至缓存区,格式化此扇区后。
  8. 将新数据追加至缓存区内取出的数据结尾。
  9. 【扇区尾起始地址 + 本次数据长度(扇区对齐长度) = 新扇区尾起始地址】并更新到缓存区的扇区头信息中,将缓存区的数据存回至扇区。

下面进行扇区检查,如果本扇区写满。分区头信息表的分区尾起始地址制动至下一个扇区,如果分区已写满,根据分区头信息表的存储规则进行相关操作。

  1. 【扇区尾起始地址 + 对齐长度】(此操作是为了检查此扇区是否能存储下一次数据),如果 【 < 】 扇区的结尾地址,结束存数据操作
  2. 如果【 > 】扇区的结尾地址,就将扇区尾起始地址与分区结束地址(分区起始地址加分区大小)比较。如果不等于,就将扇区尾起始地址更新到分区头信息的分区尾起始地址。如果等于,则查看分区头信息的存储规则,如果规则为头部覆写,则修改分区头信息的分区尾起始地址为分区的第一个数据类扇区的起始地址(注意此处不是分区第一个扇区,因为分区第一个扇区是分区头信息为结构类扇区)。如果规则为尾部覆写,则结束存数据操作(默认为分区最后一个数据类扇区)。如果规则为弃写,则将分区尾起始地址修改为0x00000000(此时新数据进来会在步骤4处跳出,舍弃此条新数据),结束存数据操作

向数据类分区读数据

  1. 从程序中获取到分区起始地址。
  2. 使用起始地址获取到分区第一个扇区即分区头信息。
  3. 根据分区头信息获取到分区编号与程序中分区编号对比无误(防止程序中分区信息没有更新进行了不可描述的异常)。

此处注意的是读取的方式,我这里以FILO(先写后读)的方式举例。如果需要其他读取方式请自行修改。

  1. 根据分区头信息获取到存储规则,如果为弃写,则【默认分区尾起始地址 = 分区结束地址 - 扇区大小】,其他规则则直接取分区头信息的分区尾起始地址。
  2. 使用分区尾起始地址获取到对应扇区的扇区头信息。
  3. 如果【扇区头信息的扇区尾起始地址 - 扇区头信息长度 != 分区头信息的分区尾起始地址】,说明扇区尾起始地址有效可以进行下一步。如果【 = 】,说明此扇区已取空。接下来需要比较一下分区尾起始地址是否等于第一个数据类扇区起始地址,如果不等于,将分区尾起始地址指向前一个扇区的扇区头信息,获取的扇区尾起始地址有效可以进行下一步。如果相等说明这是分区的第一个数据类扇区,此时要查询一下分区头信息的存储规则,如果规则为尾部覆写,则说明分区已空,结束取数据操作如果规则为头部覆写,需要获取分区的最后一个扇区起始地址【分区结束地址 - 扇区大小】,如果【最后一个扇区起始地址 == 扇区头信息的扇区尾起始地址 - 扇区头信息长度 】,说明分区已空,结束取数据操作。如果【!=】,获取的扇区尾起始地址有效可以进行下一步。

此时获取到的数据可以按单条取出,或者按扇区取出。这里以单条为例

  1. 将单条数据取出以后,需要将该条信息清除。也就是更新扇区头信息的【新扇区尾起始地址 = 旧扇区尾起始地址 - 对齐长度】,结束取数据操作

简要概述

每个分区都有一个记录分区信息的数据块,这个数据块里记录了指向可用扇区的指针。而指针指向的其实是记录此扇区的数据块,数据块里记录了指向最后可以写入的位置。
写操作就是找到对应扇区把数据写进去,写完以后还要移动扇区指针到最新位置。如果此扇区写满,则需要移动分区指针到新位置。
读操作就是找到对应扇区,确定有数据可读以后。把数据取出,并移动扇区指针将刚刚读出的数据清除。

对SPI FLASH的设计思路

最后

其实这只是一种想法,根据这个可以繁衍出很多个版本。
比如把存储规则去掉,会节省很多代码。
因为这边版本是按照分区对齐方式一致的基础上设计的,这样比较浪费空间,也可以设计成不同对齐的扇区。
等等…

接下来我要使用STM32试验一个版本,因此此文章会在试验的阶段,遇到不同的问题而不定的更改。
敬请谅解。