简单聊聊LevelX
WangGaojie Lv4

简介

LevelX,一款专为嵌入式存储打造的存储中间件,常与ThreadX等产品协同作业。它为嵌入式实时系统提供了卓越的Flash存储磨损均衡功能,即便独立使用也游刃有余。LevelX能够与各种文件系统和实时系统完美融合,适应多变的应用场景。

ThreadX及其相关组件最早是由Express Logic开发。该实时操作系统获得了多项安全认证。它以支持Cortex-M处理器为主,在Cortex-A处理器上,ThreadX也得到了很好的支持。除了RTOS外,它提供的文件系统、网络协议栈、USB协议栈、GUI等软件也非常优秀。2019年,微软将ThreadX及其相关组件收购,并改名为Azure RTOS。2023年底,微软宣布将Azure RTOS及其组件转移到Eclipse基金会并并宣布开源,同时更名为Eclipse ThreadX。虽然几经易主,但它任然是一款久经考验、相当成熟稳定的产品。

为了避免不同时期产品的命名差异,后面统一使用ThreadX和LevelX。相比于Threadx其它优秀的组件,LevelX看起来非常的普通,少有单独使用它的情况。我最近在测试某个nand flash芯片时准备使用该磨损均衡实现,实测一下效果。搭配FileX文件系统对比一下内建了磨损均衡的文件系统LittleFS的差异。

磨损均衡奥秘

磨损均衡技术的核心在于确保数据能够均匀地分布在存储器的各个区域,从而避免某一区域因长期写入而导致Flash写入不均衡。其关键策略在于避免数据的就地更新,而是通过分配新块来写入数据,实现数据的均匀分布。

关键术语解析

在文件系统、存储组件及芯片手册中,经常能看到看到扇区(sector)、块(block)、页(page)的描述,在不同的场景下可能存在表达含义的差异。为了后续内容便于理解,这里规定这几个词的含义。扇区对应一个可读写的存储单元,通常应用于文件系统、FTL层的接口中。块是页的集合,是最小的擦除单位。页是最小的读写单位。

LevelX工作原理 (Nand flash)

LevelX针对nor flash和nand flash实现了两套存储方案,关于nand和nor的差异这就不展开说明了,这里仅讨论关于LevelX针对nand flash的实现部分。

LevelX实现了一套扇区读写接口,通过一套内部机制,将扇区映射到存储器的页上,实现了FTL的功能。同时在块分配过程中通过寻找最小擦除次数的块来分配新的块,实现写入磨损的均匀分布。

LevelX存储在flash上的数据分为两种,一种是元数据(meta data),另一种是真实的扇区数据。元数据和扇区数据分别存储在相对各自独立的块中,即元数据所在的块不会写入扇区数据。

元数据包括flash设备信息、擦除计数表、块映射表、块状态表等。其中最关键的是块映射表,它是实现逻辑扇区到flash物理页映射的关键。块状态表用于标记块的状态,包括是否被分配使用、是否是坏块等。

执行_lx_nand_flash_format()函数的过程就是重建元数据的过程。执行_lx_nand_flash_open()函数的过程就是将在flash上查找元数据并全部保存到内存中。由于需要将元数据全部加载到内存中,使用LevelX是比较耗费内存的,大约需要使用的字节数为8倍块数+2倍的页字节数。

LevelX是基于块进行映射的,而不是基于扇区进行映射。所以相邻逻辑扇区的数据必定位于同一块中。例如扇区0和扇区1的数据被映射到物理块7的第1页和第2页(LevelX要求逻辑扇区大小等同于物理页大小)。假设一个块有64页,那么这个块存储的逻辑扇区必定是n+0~n+63。扇区在块内的排列可能是连续的,也可能是非连续的,这里后续会详细说明。

nand flash在每个页上除了存储数据的区域外,还附随了一个spara区域,用于存储该页相关联的一些数据,一般包括坏块标记和ECC校验信息。spara区域是LevelX这类FTL软件能够正常工作的必要条件。

执行_lx_nand_flash_sector_read()用于读取扇区数据。首先通过逻辑扇区号计算出逻辑块号(=扇区号/块内页数),通过块映射表定位到数据所在的物理页。此时定位到了数据所在的块,这时需要分两种情况,通过块状态表查询该块内存储的扇区是否是连续的,如果扇区是连续的,那么直接通过扇区偏移(=扇区号%块内页数)就能得到逻辑扇区所对应的物理地址数据。如果扇区编号不是连续的,那么需要遍历块内的每个页,每个页的spara区域中存储了该页实际的逻辑扇区号,通过比对逻辑扇区号和当前页的逻辑扇区号,就可以找到对应的物理地址数据。这里可以看出非连续模式下,数据的读取效率非常低,因为它需要遍历块内的所有数据。实际上只需要遍历块内的所有spara数据即可,但是由于接口设计的原因,存储区域内的数据还是会读取,导致效率低下。

如果某个扇区的数据未写入,那么它所在的块可能未分配,在映射表中将返回未映射的状态。或者块已经分配了,但是块内的数据页没有写入,这时_lx_nand_flash_sector_read()将返回全为0xff的数据。

数据的读取流程比较简单,主要都是一些数据的检索过程。数据写入将会非常复杂,除了扇区数据的写入外,还需要更新元数据。此外还需要考虑电源故障等安全问题。

为了克服电源故障,LevelX的元数据更新采用两次写入机制,即所有的元数据会有一个备份,且备份存在于不同的块中。所以写入新的元数据时,总有另外一个元数据是安全的。假设块映射表元数据占用4个页(0-1-2-3),那么每次更新时,先定位到待修改元数据的页,将该页标记为无效,同时在内存中将新的元数据写入到一个新的页中。即使元数据在块上不连续,_lx_nand_flash_open()函数会在读取元数据时自动合并各个元数据块。

lx_nand_flash_sector_write()函数用于写入扇区数据。类似于读取流程,先根据扇区号定位到块号,在通过块映射表查找数据所在物理块。如果块未分配,那么执行块分配流程。如果块已经分配,那就再块内写入新的扇区数据,如果存在原数据,需要将原数据页标记为无效。如果新新写入的数据导致块内数据不连续,那么需要更新块状态为无序状态(NON_SEQUENTIAL)。如果一个块内的数据页都被分配了,那么将执行块拷贝流程,即分配新的块,并将现有块中的有效数据全部拷贝过去,然后再写入新的数据。

块分配流程比较简单,首先通过块状态表查询出空闲的块列表,找到最后一个空闲块进行分配。为什么是最后一个?因为空闲表是一个有序表,它是按照擦除次数进行排序的。每次有新的空闲块插入表中,都会执行一个有序插入的过程。这个块分配机制在一定程度上保证了磨损均衡。LittleFS的磨损均衡采用hash函数随机分配新的块,通过hash函数的随机性来保证写入的均匀性。后者的分配效率高一些。

分配了新的块或者将旧块拷贝到新块后需要更新对应的映射表元数据,先更新内存,再更新flash上的元数据。旧的块应当被擦除(元数据需要更新空闲表、擦除计算表)。块的擦除次数如果超过一定的阈值后,会将这个块上的数据全部移动到一个擦除次数较少的块上。

COW机制贯穿LevelX的整个流程,它保证了数据写入的安全性。

LevelX还提供了软件ecc实现,但仅支持256字节长度的数据,如果扇区超过256字节的话还要分片处理。现在大部分nand芯片都提供了硬件ecc,我就没有测试这里的ecc算法,不过看了下代码,ecc的生成和校验都不依赖额外的内存空间,这点比较好。

注:本文基于LevelX v6.4.1版本进行阐述。

实测体验

在实测过程中,我尝试将LevelX与W25N01G芯片结合使用,并验证了其大量数据读写的稳定性。然而,在搭配FatFS和FileX文件系统使用时,发现写入性能不尽如人意。使用50MHz的SPI总线,在大量文件重复写入的情况下,文件写入速度在50kb/s左右,严重低于心里预期。读取速度好一些,可以达到几兆字节每秒。后面又移植了FileX文件系统,读写性能没有太大的提升,我看到部分网友与我有类似的情况。经过分析,这主要归因于LevelX在检索扇区号时的高开销。

相较于LittleFS,LevelX在性能稳定性方面略逊一筹。LittleFS的性能波动较小,而LevelX在块分配混乱后性能下降明显,影响系统实时性。然而LittleFS和LevelX本就是针对不同的存储介质而设计的,两者相比较似乎不能判断孰优孰劣。

LevelX和FileX都是耗内存大户,两者一搭配,RAM内存直接占用近30kb,对资源受限的设备不友好。尽管LevelX在性能和资源占用方面存在一定不足,但其在嵌入式存储领域的独特价值和广泛应用前景不容忽视。


tips
以上部分内容由腾讯浑元大模型AI美化及生成,希望本文能为您提供有益的参考和启示。