写在前面
大家都知道,目前主要流利的日志文件系统有JFFS,YAFFS等,这些都是目前在Linux中应用较多的日志文件系统。前期在做嵌入式方面的开发工作时,发现原来同事使用FLASH存储数据时使用了非常多的全局变量来保存FLASH的状态,同时在数据写入、读取和删除代码中又引用了大量的代码,感觉很乱。当时想,难道在ARM7上就没有一个好用的FLASH文件系统吧,当时也有人研究FAT32,但基本上是应用于SD卡的。其实操作FLASH并写入想要写的数据,并不难,所以这方面的封装库不多见,也属正常。再者,由于转入到编写嵌入式代码工作时间也不长,因此对于此行业的状态也了解不多。作为监控领域的嵌入式行业,最重要的两点就是数据采集和数据历史存储。数据采集与监控的目标对象有关,数据历史存储,目前在低端的ARM7或者Cortex-M3系统的架构上,基本上使用外部FLASH来做存储。随着目前芯片集成技术的推进,目前在一片CPU上也有好几十上百K的FLASH空间,这样一来,实现廉价的监控方案就方便了。然后,在软件行业,面向对象、封装已经风行多年,由于单片系统资源有限,在此领域内封装的库不多,但随着芯片制造技术的飞跃,32位单片机已经非常常见,而且便宜。因此在单片机系统上,进行封装开发也会越来越多。像目前Cortex-M3提供的外围设备库已经属于标配,因此在单片系统开发已经不是懂硬件人的专利了。当我看到外围设备库,看到原来混乱的数据处理代码,我决定对这些代码进行良好封装,以提高自己的开发效率。
FLASH日志文件系统在应用中需要解决的问题主要有两个:
1、FLASH平均擦写能力
2、FLASH在掉电或者异常时恢复问题
由于在单片系统(裸奔)中,大部分使用的都是NOR FLASH,此类FLASH可靠性高,因此NAND FLASH的坏块问题不多见,只要处理好基本的异常情况,基本上可敷使用。
然后,在使用FLASH存储数据时,需要解析的问题也有两个:
1、操作接口方便、抽象,无需考虑过多细节
2、异常时,不需要过多操心
基于自己对封装的理解,设计出了自己的FLASH日志文件系统,该系统功能有限,但对于单片系统而言,对数据的写入、读取和删除都可以很好的支持,最关键的就是支持裸奔、代码简洁(5K左右)。
设计 思路:
FLASH一般按扇区组织,最小的擦除单位也是扇区,因此此文件系统按扇区建立最小的操作单位。按一般文件系统设计理念,针对每扇区记录分区表,以索引记录。
本文件系统将所有日志记录形成一个链表记录,即指定头和尾,即可找到记录位置,删除只能头删除,添加只在尾添加,如此可保证日志文件系统的有序性(是监控系统必须保证的)。如果将各扇区有组织的统一管理起来,需要在扇区中包含一部分索引信息,以方便初始化和操作。为保证数据的正确性,数据写入时,要求写入校验和。同时为支持不定长记录,数据写入时还包括数据的写入长度。
各扇区形成链表,需要在各扇区中保存链信息,由于数据的写入和读出,会导致链表信息更新,因此需要合理利用扇区的物理特性来记录各扇区的状态。
定义的扇区状态要如下:
扇区分区由扇区头,扇区数据标识区和数据区三部分组成,前二部分构成扇区分区信息。
扇区区定义了扇区的必要信息,结构如下:
typedef struct _sector_lfs { unsigned char flag; //标志字节:未初始化为:0xFFFF unsigned char type; unsigned short id; unsigned short logsize; unsigned short mapbytes; }sector_lfs_t, *psector_lfs;此结构为FLASH中结构标识,此结构为外部FLASH的标识,通过SPI总线的NOR FLASH具有写0有效的编程特性,因此利用此特性,可以将扇区的标志状态使用一字节表示。
type表示此文件的唯一标识,如果与文件描述的不相同时会重新初始化引扇区。
id暂时未使用
logsize表示此日志的最大记录大小
mapbytes为本扇区数据标识区占用的大小,此结构之后为数据标识区,数据标识区按扇区最大可容量量计算
扇区数据标识区为位图表示方式,同时利用FLASH的物理特性,利用两个位表示一条记录的状态(写入,删除)。
日志文件表示结构:
// 扇区字节数 0x10000 #define SECTOR_SIZE 0x10000 // 扇区个数(支持最大扇区数65535) #define SECTOR_CNT 128 typedef struct _lfs { unsigned int start; // 开始扇区号 , 范围范围为[start, end) unsigned int end; // 结束的扇区号 unsigned char type; // 该文件系统的类型标识 unsigned short logsize; // 记录的日志最大大小 int head; // 用于内部表示开始的头(写的位置),初始化时需start int tail; // 用于内部表示开始的尾(读的位置) unsigned int count; // 可用日志记录条数 unsigned int rd_id; // 当前读的头位置(扇区的ID位置) }lfs_t;
文件系统的加载
文件系统的加载过程是必须的步骤,在系统准备使用此日志文件前,必须进行文件加载。
通过指定lfs_t结构的start,end, type, logsize四个参数后,使用lfs_load即可加载此文件。加载文件的过程会扫描指定的扇区,并试图扫描扇区链表的前、尾,和记录数量。对于不属于此文件或者与扇区保留信息不相同的扇区时,将重新初始化此扇区。
文件系统的操作
文件系统的操作在文件系统加载之后,可以对记录进行读、写和删除。读可以从有效记录的任意位置进行读,写将在尾部写入。删除只能在链表头删除。