作者 Shaozhong Liang
YAFFS(Yet Another Flash File System)文件系统是专门针对NAND闪存设计的嵌入式文件系统。在YAFFS中,文件是以固定大小的数据块进行存储的,块的大小可以是512字节、1024字节或者2048字节。这种实现依赖于它能够将一个数据块头和每个数据块关联起来。每个文件(包括目录)都有一个数据块头与之相对应,数据块头中保存了ECC(Error Correction Code)和文件系统的组织信息,用于错误检测和坏块处理。
YAFFS在文件进行改写时总是先写入新的数据块,然后删除旧的数据块,这样即使意外掉电,丢失的也只是这一次修改数据的最小写入单位,从而实现了掉电保护,保证了数据完整性。
YAFFS是为NAND FLASH设计的,它作了以下的假设或定义。
NAND Flash是基于块(block)的,每一个Block大小相同,由整数个chunk组成。每一个Block可单独擦除。一页(page,或chunk)为Flash的分配单元。所有的访问(读或者写)都是基于页(或chunk)的。
当对NAND Flash编程时,只有二进制中的0被编程,而1则“不关心”。比如,一个字节包含的二进制数为1010,那么当编程1001,会导致这两个数的位与操作。结果为1000.这和NOR FLASH不同。
YAFFS 分别用块号和 chunk id 标示块和 chunk 。它将空块(填满0xFF)当作空闲块或者已擦除块。这样,格式化一个YAFFS分区等价于擦除所有未损坏的块。 因此YAFFS最少需要一些函数能够擦除块,读一个页,写一个页。
1. 直接接口(Direct Interface)的相关文件
仅需要提取少量文件。使用yaffs的直接接口,你不需要所有的文件。你实际需要的文件列在下面,其余文件不需要编译。
direct/yaffsfs.c
yaffs_guts.c
direct/yaffscfg.c
yaffs_nand.c
yaffs_tagsvalidity.c
yaffs_checkptrw.c
yaffs_qsort.c
yaffs_tagscompat.c
yaffs_ecc.c
yaffs_packedtags2.c
2. YAFFS 存储设备
YAFFS对文件系统上的所有内容(比如正常文件,目录,链接,设备文件等等)都统一当作文件来处理,每个文件都有一个页面专门存放文件头,文件头保存了文件的模式、所有者id、组id、长度、文件名、Parent Object ID等信息。因为需要在一页内放下这些内容,所以对文件名的长度,符号链接对象的路径名等长度都有限制。前面说到对于NAND FLASH上的每一页数据,都有额外的空间用来存储附加信息,通常NAND驱动只使用了这些空间的一部分,YAFFS正是利用了这部分空间中剩余的部分来存储文件系统相关的内容。以512+16B为一个PAGE的NAND FLASH芯片为例,Yaffs文件系统数据的存储布局如下所示:
0 to 511 | 数据区域 |
512 to 515 | YAFFS TAG |
516 | Data status byte |
517 | Block status byte 坏块标志位 |
518 to 519 | YAFFS TAG |
520 to 522 | 后256字节数据的ECC校验结果 |
523 to 524 | YAFFS TAG |
525 to 527 | 前256字节数据的ECC校验结果 |
可以看到在这里YAFFS一共使用了8个BYTE用来存放文件系统相关的信息(yaffs_Tags)。这8个Byte的具体使用情况按顺序如下:
Bits | Content |
20 | ChunkID,该page在一个文件内的索引号,所以文件大小被限制在2^20 PAGE 即512Mb |
2 | 2 bits serial number |
10 | ByteCount 该page内的有效字节数 |
18 | ObjectID 对象ID号,用来唯一标示一个文件 |
12 | Ecc, Yaffs_Tags本身的ECC校验和 |
2 | Unused |
其中Serial Number在文件系统创建时都为0,以后每次写具有同一ObjectID和ChunkID的page的时候都加一,因为YAFFS在更新一个PAGE的时候总是在一个新的物理Page上写入数据,再将原先的物理Page删除,所以该serial number可以在断电等特殊情况下,当新的page已经写入但老的page还没有被删除的时候用来识别正确的Page,保证数据的正确性。 ObjectID号为18bit,所以文件的总数限制在256K即26万个左右。
由于文件系统的基本组织信息保存在页面的备份空间中,因此,在文件系统加载时只需要扫描各个页面的备份空间,即可建立起整个文件系统的结构,而不需要像JFFS1/2 那样扫描整个介质,从而大大加快了文件系统的加载速度。
一个YAFFS设备是一个逻辑设备,它代表了一个物理设备的部分或整体。你可以认为它是一个Nand上的一个“分区”。比如,该分区可能覆盖整个NAND,也许只是一半,而另外一半就是另一个Yaffs_Device.它也可以用于你使用一个非flash设备(比如RAM)来测试的情况下。 一个Yaffs_Device记录了起始和结束块。通过改变它的起始和结束块,你就可以在同一个物理设备上使用不止一个的Yaffs_Device。 这里将需要你自己建立的Yaffs_Device结构的数据域列出,其他数据域由Yaffs自动创建。
int nDataBytesPerChunk
如其名,这是每一个chunk的字节数,还记得吧,在yaffs术语中,一个页就是一个chunk,因而它也是一页的字节数。它是数据字节数,即不包含OOB的数据。比如一页时2048字节+64字节的OOB,那么数值nDataBytesPerChunk为2048。
int nChunksPerBlock
物理Nand设备中每页包含的chunk(就是Nand上的页)的数目,最少是2。
int spareBytesPerChunk
空闲域(spare area)大小,比如:每个chunk(页)的OOB字节数。
int startBlock
该逻辑Yaffs_Device设备第一个块的块号(而字节地址),注意,yaffs需要第一个块是空闲的,因此你不可以设置该变量为0,如果设置为0,Yaffs会给它加1,并且会在结束块号上也加1,在你设置设备从块0开始,到最后一个块结束,这意味着yaffs试图写一个不存在的块,从而出现错误。
int endBlock
该逻辑Yaffs_Device设备的最后一个块号。如果startBlock为0,那么yaffs会使用endBlock+1,至少使startBlock+nReservedBlocks+2
int nReservedBlocks
这是YAFFS必须保留,用于垃圾回收和块错误恢复的可擦除块的数目。至少是2,但是5更好。如果你使用一个不会损坏的介质,比如RAM或者RAM盘,或者主机文件系统模拟,那么可以是2。
int nShortOpCaches
配置当前设备YAFFS Cache项的数目。0值表示不使用cache。对于大多数系统,推荐使用10到20之间的一个数值。不能大于YAFFS_MAX_SHORT_OP_CACHES定义的数值。
int useNANDECC
这是一个标志,用于指示是由yaffs执行ECC计算,还是由NAND驱动程序来执行ECC计算。(译者注:此数值取0,则使用yaffs来执行ECC计算,软件ECC计算。如果想要使用硬件ECC校验时,应该设置为1,并且在NAND驱动程序中加入硬件ECC校验的代码。)
void *genericDevice
这是一个指针,它应该指向任何数据,底层NAND驱动程序需要知道以从物理设备读、写。
int isYaffs2
我们使用的是否YAFFS2版本?
int inbandTags
是否为OOB区,如果不是,那么它为真,仅用于yaffs2
u32 totalBytesPerChunk
这个名字可能有点误导人,它应该等于nDataBytesPerChunk ,而非其名字暗示的nDataBytesPerChunk + spareBytesPerChunk。如果inbandTags为真,那么yaffs设置nDataBytesPerChunk,因此有足够的空闲空间存储数据,yaffs会在空闲域中正常存储。
write/readChunkWithTagsFromNAND,
markNANDBlockBad
queryNANDBlock
这些都是函数指针,你需要提供这些函数来给YAFFS,读写nand flash。
该函数将页(chunk)写入nand中,向nand中写入数据。数据和标签(tags)永远不应为 NULL. chunkInNand 是将要写入的页的页号,而不是需要转换的地址。
int (*readChunkWithTagsFromNAND) (struct yaffs_DeviceStruct * dev, int chunkInNAND, u8 * data, yaffs_ExtendedTags * tags);
dev: 要写入的yaffs_Device逻辑设备.
chunkInNAND: 将chunk读入的页
Data: 指向需要读入的数据的缓冲区指针
tags: 指向未压缩(打包)的OOB数据的缓冲区指针
Return: YAFFS_OK / YAFFS_FAIL
该函数执行上一个函数的相反的功能,首先,读取数据和OOB字节,接着将这些输入放在一个由参数data指向的缓冲区中
int (*markNANDBlockBad) (struct yaffs_DeviceStruct * dev, int blockNo);
dev: 要写入的yaffs_Device逻辑设备.
blockNo: 要标记的块.
Return: YAFFS_OK / YAFFS_FAIL
int (*queryNANDBlock) (struct yaffs_DeviceStruct * dev, int blockNo, yaffs_BlockState * state, u32 *sequenceNumber);
dev: 要写入的Yaffs_Device逻辑设备.
blockNo: 要标记的块.
state: Upon returning this should be set to the relevant state of this particular block.
Sequance number: 该块的顺序号(The Sequence number),为0表示此块未使用
Return: YAFFS_OK / YAFFS_FAIL
它应检查一个块是否是有效的。如果在OOB中设置了坏块标记,那么*state应该被赋值为YAFFS_BLOCK_STATE_DEAD,*sequenceNumber赋值为0,然后返回YAFFS_FAIL。
如果该块没坏,那么应解压缩标签。标签解压缩后,若发现chunk已使用(查看tags.chunkUsed),则*sequenceNumber应赋值为tags.sequenceNumber,*state赋值为YAFFS_BLOCK_STATE_NEEDS_SCANNING,否则该块未使用,则*sequenceNumber赋值为0,*state赋值为YAFFS_BLOCK_STATE_EMPTY
4. YAFFS的缓存机制
由于NandFlash是有一定的读写次数的,所以在对一个文件进行操作的时候往往是先通过缓冲进行,对最后一次性写入NandFlash,这有效的减少了用户对NandFlash的频繁操作,延长了NandFlash的寿命。下面大致说一下YAFFS的缓存机制:
4.1.首先在yaffs_mount的时候会对yaffs_dev这个结构体进行注册,和缓冲部分相关的有:
dev->nShortOpCaches//这个变量决定了有多少个缓冲,因为缓冲会大量的占用堆栈的空间,所以在yaffs不建议缓冲的数量很大,即使你填一个很大的数,系统也不会超过YAFFS_MAX_SHORT_OP_CACHES的总数。
yaffs_ChunkCache *srCache;//缓冲区的首地址,dev->srCache = YMALLOC( dev->nShortOpCaches * sizeof(yaffs_ChunkCache));下面介绍一下缓冲区这个结构体的组成:
typedef struct
{
struct yaffs_ObjectStruct *object;//一个缓冲区对应一个文件
int chunkId;
int lastUse; //通过lastUse来
int dirty; //标志了这一个缓冲区是否被使用
int nBytes;
__u8 data[YAFFS_BYTES_PER_CHUNK];//数据区
} yaffs_ChunkCache;
4.2.什么时候用到缓冲区?
用到缓冲区最多的地方显而易见是对已经创建的文件进行写操作。而且是需要写的大小和512不一致的时候,这是因为如果是刚好512的话,系统会直接写入NandFlash中。对于小于512的系统首先会调用yaffs_FindChunkCache(in,chunk)这个函数来判断in这个object是否在缓冲区中存在。如果存在会调用已有缓冲区进行操作。当然如果是第一次对一个object进行操作,肯定在缓冲区中是不存在对应它的空间的,因此系统会调用yaffs_GrabChunkCache函数为此次操作分配一个缓冲区。
YAFFS为连接的应用程序提供了一组函数。大部分跟标准C库函数,如open/close一致,只是增加了yaffs_前缀,如 yaffs_open. 这些函数定义在direct/yaffsfs.h中。
初始化yaffs来完成读写,你必须在每个你要使用的yaffs设备上调用yaffs_mount。比如yaffs_mount(”/boot”)。这可以在系统启动的时候执行,如果存在一个操作系统,那么程序需要考虑这点。在你完成使用的时候,你也需要调用yaffs_umount函数,这样yaffs就会将它需要状态写入磁盘。 如果读写文件的应用层接口已经存在,你可以根据相关的操作系统调用来封装相关的函数调用。
1 yaffs_mount( )
功能说明:加载指定器件。
输入参数:path 要加载的器件。
输出参数:无。
返回值: 表明加载的状态。
调用的函数:yaffsfs_FindDevice( )、yaffs_GutsInitialise( )。
2 yaffs_open( )
功能说明:按照指定方式打开文件。
输入参数:path 文件的绝对路径;
Oflag 打开的方式;
Mode 文件许可模式。
输出参数:无。
返回值:句柄。
调用的函数:yaffsfs_GetHandle( )、yaffsfs_FindDirectory( )、yaffs_MknodFile( )、yaffsfs_PutHandle( )、yaffs_ResizeFile( )、yaffsfs_FindObject( )。
3 yaffs_write( )
功能说明:根据打开文件的句柄,从指定数组处读指定字节写入文件中。
输入参数:fd 要写入的文件的句柄;
Buf 要写入的数据的首地址;
Nbyte 要写入的字节数。
输出参数:无。
返回值: 写入了的字节数。
调用的函数:yaffs_WriteDataToFile( )、yaffsfs_GetHandlePointer( )、yaffsfs_GetHandleObject( )
4 yaffs_read( )
功能说明:根据打开文件的句柄,从文件中读出指定字节数据存入指定地址。
输入参数:fd 要读出的文件的句柄;
Buf 读出文件后要存入的数据的首地址;
Nbyte 要读出的字节数。
输出参数:无。
返回值: 读出了的字节数。
调用的函数:yaffs_ReadDataFromFile( )、yaffsfs_GetHandlePointer( )
5 yaffs_close( )
功能说明:关闭已经打开的文件句柄,yaffs 有缓冲机制,当调用yaffs_close()关闭文件之后能够保证将内容写入nandflash。
输入参数:fd 需要关闭的文件的句柄;
输出参数:无。
返回值:无。
YAFFS提供了直接调用模式,可以方便移植到 none-OS或者light-weighted OS中。附件是将YAFFS移植到Freescale MQX实时操作系统的源代码和工程,可以在II型集中器的Demo Board上运行。
初步的测试表明YAFFS工作正常,能够完成创建目录,创建/删除文件,读/写文件操作等。
YAFFS非常适合none-OS或者是light-weighted OS,使用YAFFS需要关注的是RAM的消耗,适合小量文件(<20)。
如果不想使用MQX默认的MFS(FAT32文件系统),YAFFS可以作为一个文件系统的备选方案。
Thank you for your tutorial! I will try to integrate it!!!