Spi flash基于FAT的简单日志系统(FTL)设计

        最近一直在想给自己做的简易Hmi组态屏做一个保证FAT的稳定层,也就是所谓的日志系统(好像听人说这类玩意有个名字叫做FTL,又叫擦写均衡算法,嘛,反正纠结名词不是我喜欢的做法,所以就叫FTL吧)。

        首先我用的硬件是LPC1788+SDRAM+W25Q128的组合,软件用的是RT-Thread RTOS以及它的组件driversSpi框架和DFS文件系统,底层文件系统则是FAT。

        为什么是是FAT呢?首先是考虑到拷贝数据方便。因为实际上我开发能力很弱,没多少开发经验,所以要我用一个人做LPC1788跟电脑的USB device驱动并自动读写,这是不可能的,我欠缺必要的USB知识,另外一个人搞HIM组态是很吃力的,还去学驱动是不可能有时间的,所简单的,选择直接跑Host Massstoge协议,直接读取U盘文件,因为一般我们用的都是Windows,U盘都是FAT系统,所以下面也跑FAT系统比较方便,可以实现文件拷贝。此外,因为用的是SPI flash,空间很小,但是本身坏块概率不大,如果上linux上面的那些文件系统,感觉是相当不靠谱的……

        正题,FAT文件系统本身比较简单,什么都不带,它的功能就是读取跟保存,不带其他多余的东西,不像别的系统一样带FTL算法(就是擦写均衡以及坏块管理、日志),一般情况下只要操作得当,系统是没问题,但是当所在环境不对的时候,问题就会出来了。因为FAT读写都是直接地址,比如说,它的系统信息必定会保存在一开始连着的那几块里面,而任何文件系统相关改写劲操作,都会写这部分区域。而就我们知道的,spiflash是写前要先擦除的,如果在擦除到写入这短时间整个系统掉电,那么恭喜你。你系统可能挂了!!!

        在一开始用这个的时候我就知道有这个弊端了,但是当时没办法,首先是因为我用的环境很少断电,另外,则是因为基本功能都没做完就去考虑别的是傻逼的行为!!!基本东西都没做完考虑稳定性是有毛用?

        但是后面随着自己上位机也开发出来,硬件也定型后,也得考虑怎么去解决这个问题的,因此就开始用笔在纸上画,考虑具体环境与相应解决措施:

1.系统日志

        首先要考虑的是,FAT为什么会挂掉呢?说白了是表头信息丢失或者不完整,这后果简单的,只需要重新创建文件系统,下对应资源就好,但是丢失的文件是找不回来了。而严重的就是因为表信息不完整,FAT在解析一半的时候直接死机了,这时候真是没办法了,只能维修了(刷修复系统固件,拒收再刷回来)。

        那么为什么别人的系统就不会挂呢?这时候想起的是一个叫“系统日志”的名词,据说稳定的文件系统都是带是日志功能的,能在文件系统挂掉的时候自动恢复。但是怎么实现呢?因为都解析错误死机了,是不可能修复的,因为已经陷入while(1)里面了(Cortex-M3 Hault Handler中断会在异常情况下触发)。

        这时候我就在想,FAT会挂是因为原来的信息比改掉了,但是如果原来信息不被改掉那们情况就不一样了,如果能做到FAT在改写的时候独立出来,不改变原来的信息,在改写完了之后才会真正的把改写的信息当前表信息处理,不然就还用原来的表信息,那么系统能在一定程度上保证自己即使操作失败也是当前劲爆操作失败,但是以前的东西还在,并且还是一个完全的文件系统信息表(操作前的)!!这不就是一个简单的日志逻辑吗(能恢复到上一个状态!)

        2.地址映射表

        接着继续考虑如何来把当前的系统操作与以前的操作隔离开。

        在上面是明确了,如果想保证可可恢复性,就必须具有一个完整的信息表,但是如何保证呢?一开始就已经确定了一个问题,FAT在读取这个文件系统的信息表的时候是会读取指定的块地址的,这个地址不管任何时候都不会变。那么如果建立一个表,这个表的大小与spi flash可用块数大小一样,然后在这个表里面填上另一个块的地址,然后读写的地址都通过这个表转换成真实的地址。在这样的前提下,假如我有两个表,一个是没操作前的表,已经保存,一个是在操作的表,在没操作前,表头那几个块地址是分别是1、2、3、4,而在表2,这几个块地址则是5、6、7、8,那样的话,其实改写的地址就变成了新的了,因此不会覆盖掉原来的数据,从而保证原先数据代收完整。我把这表就叫地址映射表。

        3.块回收表与简单擦写均衡

        但是单纯有映射表是不够的,因为块的个数是一定的,在对其进行编号后,这个编号在地址表里面最多只能存在一个,不然就可能出现同个数据块给改写,比较危险的情况是因为这个块有可能就是原本系统信息所在的块,那样的话,系统也一样是挂掉了。此外,假如新的改写成功了,那么旧的块其实就没用了,但是映射表是不带记录的,它只知道自己现在是哪块,所以这时候需要另一个表,记录所有回收回来的块,来保证重利用。

        这时,有必要引入一个人们都经常说的擦写均衡算法的开涮念,因为spi flash的块擦写次数是有限的,为了保证尽可能长的spi flash使用周期,就采用轮流擦写的方式将次数分摊给空闲的块,使周期成倍数延长,这个具体就不说了,网上有资料。如果一个块回收回来,但是马上又用出去,而其他的块却一直都空着是很不合理的。网上看到有人介绍trueffs的算法是掉电保护、自动马查找最小次数块等均衡算法,但是个人不喜欢,因为太封复杂的运算是会拖慢速度,理论上是说有效提升使用周期,但是每次取一块与放一块会很麻烦。我是很懒的人,只会用最简单然后有效的方法--链表!

        首先,设置两个参数,是对回收表的寻址,分别指向回收表的表头与回收表的表尾,表尾是用来保存回收块地址的,表头用来保存下一块有效空闲块的地址,两个参数都环形递增。这样就够成一个简单链表,因为所有回收块都会放在表尾,取块都在表头,这样就不需要什么多余算法来保证均衡,它自己已经形成了一个均衡的体系。

4.映射与回收的合理性

        块回收表必须与地址映射表成对保存,并且同一个编号会出现不可能同时出现在两个表格内,出现了,就说明这个系统离挂不远了。所以需要一个比较合理的逻辑,保证两个的值正确。

这其中其实涉及了一个比较紧密的逻辑。首先是采用的FatFs文件系统具有一个_USE_ERASE定义(所以都说是基于FAT文件系统了),它会在FAT格式化或者删除东西的时候调用块擦除接口,而我们在这个接口里面做了空闲块回收操作。

        这么说吧一开始空闲表必须是初始化化成0xffff,而0xffff也就表示这个地址没有可用块,而对地址映射表则采用了从0开始的递增编号。

        这时候:

        a.文件系统初始化的时候,因为一开始没有空闲块,我们强制要求它还是直接用当前映射表的值进行读写。

        b.接下来,fta就会调用擦除接口,把映射表除开表信息剩余区域删除,这时候地址映射表的空闲块就会都回到块回收表,使回收表具有有效块,而把自己对应地址值反而改成0xffff,即无效映射。

        C.当再写的时候,因为块回收已经有东西了,把表头块送出去,然映射表旧值拿回来,如果有效就放回到表尾。

        这时候有两种情况,一个是映射表的值是0xffff,一个是有效值,但是在上现三步我们发现,在两个表内本身已经出现一个问题,就是同一个值,绝对只在其中一个表中出现,不会同时出现或者不出现,因为原本地址映射表的值是唯一的,块总数不变,编号也不变,在编号从一个表跑到另一个表的时候,原来位置会用无效值或者另外有效编号替换,所以实际上有效总数不变,并且不重复。

        更重要的是,它实现太简单了,没有什么多余算法,代码也特别的少,不过它不是平均的,从原理上就表明了性能肯定比不上那些算法,所以纠结这些的还是死心吧。

        5.日志的保存

        功能是出来了,但是怎么样来保存才安全呢?

        首先是明确的,我们需要保存的数据除了修改数据之外,还有两个表,但是仅仅这样就可以了吗?我们必须保证回收表完整性,所以包括它的两个地址参数都需要保存的,此外我们保证了原始数据完全,但是表的安全呢?

        想想,为了保存系统日志,最关键的现在反而成了保证这两个表的安全性,只要这两个表上个状态还在,那么很可能(注意,是可能)系统就不会挂,能恢复到上一个状态。这说明这两个表必须不能覆盖写,需要一样的采用均衡擦写。这时候,分配flash的前面一定数量的块,这时候引入一个环型递增的变量,这个变量是为了让保存连两个表在这些块区域里面的真实位置的。也就是说,如果这个变量保存好了,那么两个表也就安全了,文件系统也就安全了,同时表是在分配的区域里面自己均衡,也能延长对应寿命。

所以这样子一来,我们所担心的问题,反而成了保存一个表地址变量安全以及两个回收表参数安全的问题,此外还需要在结尾加上一个标志位,表示数据完整性(用于辅助判断是不是正确数据)。好到这一级的时候,我们不绕弯子,直接分配两个块(必须两块)给这几个参数轮询做均衡!!也许你们会奇怪,为什么是必须两个块。感觉就这几个参数,分配这么多块很浪费?理由是安全性。要知道spiflash写入速度也不慢,但是擦除却非常慢,如果我在前面操作都做完了却在最后保存参数的时候掉了链子,因为擦除却来不及写入或者数据错误导致了系统挂掉,那不是更可惜了。因此比较正确的策略的是,在第一块写完的时候立刻擦除另一块,然后下次就能直接在第二块轮询了,第二块写到最后也是同样道理,这样的话避免写入擦除操作是在同一块内造成损坏,从而最大限度保证数据安全。

(不知道这算不算传说中的掉电保护呢 ==   。==!!);

        6.效率问题

        我们知道每次改写因为块回收表的均衡特性,每次读写操作都会改写这两个表,所以这几个表都需要保存一次,但是该怎么保存好呢?直接保存是肯定不行的,假设4096块的大小的spiflash,那么光是表就需要保存成4块,比写一块的工作量还多,要知道spi flash 一般是norflash,擦除速度可是很慢的,如果真的是每次修改表都保存一次,那么别的都不干,光是数据拷贝效率就会下降5倍!!!再者,它的擦写更频繁!!!

        因此引入了一个闲时保存的线程(说了是用RT-Thread RTOS的)。

        具体是:

        a.创建一个线程与一个事件。线程任务很简单,就是死等事件,在事件没触发之前一直在挂起状态。

        b.加入操作了文件系统,每次回收块与写块因为都涉及到块的改写操作,所以都发事件通知线程

        c.线程收到事件唤醒了,下一步动作,等待在一定时间里面不再有对应的事件发生!

        假如真的等到了(也就是在一段时间内不在有文件系统相关操作),那么再把表格保存起来。然后继续到第一步。

        这样一来,保存表信息总在等待时间超时后,而在操作期间不会有任何操作,从而保证了数据拷贝效率!!(当然,为了保证数据操作,加了互斥锁)

        不过这里如果是用裸机就简单了,什么都不用,只需要两个标志变量判断下再保存就好了,毕竟裸机不存在变量互操的安全性问题。

        7.缺陷

        前面说了那么多,但是实际上这个设计本身是靠取巧来实现功能的做法,存在很多不合理的地方!

        第一,因为没有碎片整理,当块回收的可用块大小小于需要改写的倍数的时候,本身就已经出现不安全了,因为那些块数据已经被改写,恢复不回来了,这时候最好祈祷别掉电啊。

        第二,因为属于非实时的保存信息,虽然不损失操作效率,但是操作太多,还没来的及保存的会全部丢失,如果加上第二条,基本都是挂定了。

        第三,维护表格以及跑任务都需要额外至少几K的RAM,这点跑不掉了,硬件差的留步。

        第五,因为所谓的均衡只是简单的看几块空着就轮着用, 不是平均的,所以均衡性能不见得很好。

        最后声明,这个思路是自己想出来的,并自己编写了代码验证,并不参考任何别的相关代码或者文件系统,所以如有雷同,只能说明我跟你一样聪明了。。。。。。。

猜你喜欢

转载自blog.csdn.net/xuzhenglim/article/details/42367719
SPI
今日推荐