转载自:
https://blog.csdn.net/zhangjinxing_2006/article/details/75050611
SPIFFS设计
SPIFFS的设计灵感来源于YAFFS。可是YAFFS使转为NAND闪存而设计的文件系统,由于目标更大所以需要更多的RAM。写SPIFFS时很多巧妙的方法都是从YAFFS借鉴过来的,鼓励下。
写SPIFFS的主要复杂原因是它不假设目标系统具有Heap堆内存的能力。SPIFFS只需要一定的工作RAM缓冲区即可。这让它可以适用于更多的地方。
使用NOR技术的SPI闪存设备
以下是SPI闪存设备的简短描述,可以用来理解SPIFFS的选择设计。
SPI闪存在物理上分为多个Block块,一些SPI器件右进一步划分为页。数据手册有时候将块称为扇区。
SPI闪存器件通常存储容量为512KB,最多可达8MB字节数据。其中块的容量可能为64KB.如果支持页,很多页容量为4KB。许多SPI闪存的块大小都是相同的,也有些器件的块大小是不同的,例如前面一些块的大小为4KB,其余块的大小为64KB。
整个存储器空间是线性的,可以对其进行随机读写访问。但是擦除操作只能以块和扇区为单位进行,或者整个擦除。
SPI闪存设备通常有100000到1000000次的擦除周期,直到其操作失败。
工厂内全新的SPI器件内的所有数据位都被设置为1.一个擦除操作将这些数据位修改为0.块或扇区擦除将会将这些数据位全部写为0。
向数据位写入0将会改写数据位,写入1数据位将无任何变化。
例如:器件某地址数据本来为0b00001111,则写入数据0b10101010后,原数据和写数据将相与(NAND)操作,则结果为0b00001010。这种NAND的特性在SPIFFS中被大量应用。
最后,与NAND闪存不同,NOR闪存几乎不需要任何错误纠正。它们总是写正确的。
符合逻辑结构
继续之前的术语,物理块/扇区都在数据手册中规定。逻辑块数量和页都由程序员选择。
块和页
SPI闪存设备的一部分或全部分数据区域被分配给SPIFFS。这些区域被分为逻辑块,再被分为逻辑页。逻辑块的边界必须与一个或多个物理块对齐。所有逻辑快或逻辑页的大小都必须保持一致。
示例:非均匀物理块器件映射到128KB逻辑块中
物理块 逻辑块
+-----------------------+ - - - +-----------------------+
| Block 1 : 16kB | | Block 1 : 128kB |
+-----------------------+ | |
| Block 2 : 16kB | | |
+-----------------------+ | |
| Block 3 : 16kB | | |
+-----------------------+ | |
| Block 4 : 16kB | | |
+-----------------------+ | |
| Block 5 : 64kB | | |
+-----------------------+ - - - +-----------------------+
| Block 6 : 64kB | | Block 2 : 128kB |
+-----------------------+ | |
| Block 7 : 64kB | | |
+-----------------------+ - - - +-----------------------+
| Block 8 : 64kB | | Block 3 : 128kB |
+-----------------------+ | |
| Block 9 : 64kB | | |
+-----------------------+ - - - +-----------------------+
| ... | | ... |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
选择页大小
逻辑块又进一步分为多个逻辑页。页page定义了最小的数据保存单元。因此一个文件即使只有1字节大小,它也会占用闪存中的1个索引页面和一个数据页面,即会占用2个逻辑页。所以选择较小的页大小是有益的。
每个页都有一个5到9字节的元数据头。也就是说,如果页容量非常小,则元数据将会占用较大的存储空间比例。例如页大小为64字节,则将浪费8%-14%的空间存储元数据。而256字节的页大小的元数据占用比例为2%-4%,这样看来选择较大的页大小会更好些。
此外SPIFFS使用一个大小为2倍逻辑页大小的RAM缓冲区。该内存区用于加载和操作页,也用于查找文件ID、扫描文件系统等算法。太小的页大小意味着SPIFFS的工作缓存更小,将会需要更多的读取操作,会导致文件系统操作更慢。
选择系统的逻辑页大小涉及到许多因素:
- 逻辑块的大小
- 常用文件的大小
- 内部ram的大小
- 必须将多少元数据保存到文件系统中
- 文件系统的速度
- 其它一些事情
所以选择最佳的页面大小似乎很棘手。但是别烦恼:永远没有最佳的页大小。这与目标的使用方式有关。可以使用黄金法则:
逻辑页大小 = 逻辑块大小 / 256
这是一个很好的开始,然后可以根据需要再决定最终的大小。
对象、索引和查找
对象标识
一个文件或在SPIFFS中被使用的对象,都由一个对象ID来标识。次对象标识是页数据头的一部分。所以,所有页都知道自己属于哪个对象/文件-无需计算空闲页。
一个对象由两种类型的页组成:索引页和数据页。
- 索引页包含对象有关的元数据,用来指示哪些数据页是对象的一部分
- 数据页包含用户实际存储的数据
页数据头
页数据头还包含称为span index(跨度索引)的东西。例如一个文件写入了3个数据页。则第一个数据页的span index为0,第二个为1,最后一个索引为2。
最后每个页数据头都包含标志,用于指示页是否被使用、删除、完结、索引页和数据页等。
对象指示也有span index跨度索引,其中索引为0的称为对象索引头。此页面步进包含对数据页的引用,还包括对象名称、对象大小(单位为字节)、文件和目录标记等附加信息。
如果要创建一个覆盖3个数据也的文件,例如命名为spandex-joke.txt文件,并且其对象ID为12,则页数据看起来像这样子:
页0 随后介绍
页1 obj_id:12, span_ix:0, flags:USED|DATA 文件的第1页数据
页2 obj_id:12, span_ix:1, flags:USED|DATA 文件的第2页数据
页3 obj_id:54, span_ix:6, flags:USED|DATA 54 ID对象的某个数据
页4 obj_id:12, span_ix:2, flags:USED|DATA 文件的第3页数据
页5 obj_id:12, span_ix:0, flags:USED|INDEX 文件的索引数据
obj_id header: name:"spandex-joke.txt", size:600bytes, flags:FILE
obj ix: 1, 2, 3
1
2
3
4
5
6
7
8
在”对象索引头页”章节中有介绍,对象索引数组按顺序引用每个数据页,如前所述。对象索引数组的索引与数据页的跨度索引相关联:
entry ix: 0 1 2
obj ix: [1 2 4]
| | |
页1, 数据页, 跨度索引 0 --------/ | |
页2, 数据页, 跨度索引 1 --------/ |
页4, 数据页, 跨度索引 2 --------/
1
2
3
4
5
6
很多事情都在第0页实现- SPIFFS专为第RAM系统而设计的。我们无法在每个对象的索引数据头所在的位置保留动态列表,所以我们可以快速的找到一个文件。甚至在没有堆的场合。但是我们不想扫描闪存中的所有页头来朝朝对象索引头。
第0页
每一块的第一页包含对象的查找结果。这些不是普通的页,它们没有页头数据,这些页更像一个数组,用于指出块中所有其余页所属的对象ID。
通过这种方法,只需要扫描每个块的第一个页面就可以找到包含所需对象的索引头页面。
对象查找冗余元数据。假设它从每块中读取整页数据到RAM的开销比较小,并且以此完成搜索。而不是单独从页中读取一部分页头数据。SPI闪存每次读操作都需要包含额外的数据,包括读命令和地址等。其它环境下可能需要互斥性的读取事务。
下面是一个示例:
页0 [ 12 12 545 12 12 34 34 4 0 0 0 0 ...]
页1 页数据头: [obj_id:12 span_ix:0 flags:USED|DATA] ...
页2 页数据头: [obj_id:12 span_ix:1 flags:USED|DATA] ...
页3 页数据头: [obj_id:545 span_ix:13 flags:USED|DATA] ...
页4 页数据头: [obj_id:12 span_ix:2 flags:USED|DATA] ...
页5 页数据头: [obj_id:12 span_ix:0 flags:USED|INDEX] ...
页6 页数据头: [obj_id:34 span_ix:0 flags:USED|DATA] ...
页7 页数据头: [obj_id:34 span_ix:1 flags:USED|DATA] ...
页8 页数据头: [obj_id:4 span_ix:1 flags:USED|INDEX] ...
页9 页数据头: [obj_id:23 span_ix:0 flags:DELETED|INDEX] ...
页10 页数据头: [obj_id:23 span_ix:0 flags:DELETED|DATA] ...
页11 页数据头: [obj_id:23 span_ix:1 flags:DELETED|DATA] ...
页12 页数据头: [obj_id:23 span_ix:2 flags:DELETED|DATA] ...
1
2
3
4
5
6
7
8
9
10
11
12
13
这里有个问题:为什么页9到页12中数据头的对象ID是23,而在页0中被标识为0那?这是因为这些页面被删除了,所以这些被标记为0以供查找。这是利用闪存只能写如0的与操作的示例。
事实上有两个特殊的对象ID:
- obj_id = 0:用于表示页面已经被删除
- obj_id = 0xff:用于表示该页面为空闲页面
另外对象页ID有一个特殊的地方:其最高有效位被置位表示该页为一个索引页,最高有效位为0表示该页为一个数据页。所以准确的来说第0页应该是这样子:
页0 [12 12 545 12 *12 34 34 *4 0 0 0 0 ...]
1
星号表示对象ID的最高位。
这是在查找对象索引时加快搜索的另一种方法。通过查看对象id中的最高有效为,就可以找出对象的索引页和数据页。