嵌入式Linux下NAND存储系统的设计与实现
原文地址: http://www.edires.net/hot/1710.html
作者: 胡勇其,侯紫峰
摘要:讨论嵌入式Linux下与NAND闪存存储设备相关的Linux MTD子系统、NAND驱动,并就与NAND闪存相关的文件系统、内核以及NAND闪存存储设计所关注的问题如坏块处理、从NAND启动、当前2.4 和2.6 内核中NAND通用驱动所存在的缺陷进行讨论并给出解决方案。以Omap161x H2开发板为例,给出了NAND闪存存储实现实例并指出设计中需要关注的问题。
引 言
NAND 和NOR是现在市场上两种主要的非易失闪存技术。相对于NOR 而言,NAND结构能提供极高的单元密度,可以达到高存储密度,并且写入和擦除速度很快,同时,NAND闪存的成本要低于NOR 闪存。因此尽管NAND的接口特殊,管理复杂,读取速度不及NOR,但是从性价比出发,NAND闪存逐渐成为嵌入式系统的首选存储设备。
NAND 闪存具有以下特点:NAND器件是基于I/O接口的(NOR闪存是基于Bus的RAM接口),以页为单位读写,以块为单位擦除。NAND芯片通过多个引脚 传送命令、地址和数据,使用较复杂的I/O接口来控制,同时,根据NAND规范,NAND闪存中允许存在坏块。NAND闪存的每一页都有8B(页长度 256B)或者16B(页长度为512B)的OOB(Out Of Band)数据区,用来存放ECC(Error Checking &Correction)、ECC有效标志、坏块标志等。所有这些决定了基于NAND的存储系统设计需要处理不同于其它类型闪存的特有问题。
MTD 结构和NAND驱动接口
Linux 上设计了MTD子系统为闪存类型的设备向上层提供统一接口。MTD的主要目的是实现子系统公用部分,使新的存储设备的驱动设计更加简单。一个MTD设备按 照用户访问的顺序可以得到图1 所示的层次结构。图1中NAND特定硬件驱动层为具体NAND设备的驱动,实现特定硬件的具体操作;NAND通用驱动层是所有NAND设备的公用部分,实 现了NAND设备发现、通用NAND读写等操作;MTD 原始设备层是MTD原始设备的通用代码,此外还包括各个特定的闪存设备所注册的数据,例如NAND分区等。MTD向上提供块设备和字符设备两种接口。文件 系统通过MTD 块设备接口访问NAND闪存驱动。
图1 Linux MTD 的层次结构
具体的NAND闪存驱动是和NAND通用驱动相关联的,要实现一个NAND闪存硬件驱动,需要实现以下部分:初始化函数,硬件相关的设备就绪函数和控制函数,为了灵活起见,还可以实现硬件相关的命令函数、硬件相关的等待函数和硬件ECC函数。
NAND设备驱动初始化时分配必要的内存,设置IO地址,然后利用NAND通用驱动提供的设施,调用nand_scan来完成通用部分的设置,最后定义闪存分区并在MTD中注册来完成初始化。可以通过Linux 内核中的实例了解NAND驱动的结构。
和NAND特性相关的问题
NAND闪存具有其它类型的闪存所不具有的特性,因此基于NAND的存储设计需要考虑这些问题。NAND闪存的正确、高效使用取决于以下问题的正确解决。
坏块问题
NAND 闪存定位为一种低成本存储介质,从成本和技术上综合考虑,NAND闪存允许存在一定比例的坏块。坏块的产生有多种原因,如解码失败,地址线错误,存储单元 错误等,出品前厂商经过测试,将确认的坏块标记出来。同时,因为NAND闪存的擦写寿命有限(一般在105-106次),当使用到一定时限后也会产生坏块, 这些坏块通过擦除和写入后的状态来判断,产生擦除或写入失败的块应该认为已经损坏,并由文件系统或者其它管理程序标记出来。
坏块在NAND闪存的OOB 区域中标记,NAND闪存的每页都有16B的OOB(以页为512B 为例),每个块的第一页的OOB的第6个B用来记录该块是否是坏块,当该B为非0xff时则表明该块是坏块。
为了正确使用NAND闪存,需要进行坏块管理,坏块管理的基本思想是:避免操作坏块,同时在读写操作时跳过坏块而使得上层操作看来闪存存储区仍然是连续的。
与文件系统相关的坏块管理
在NAND闪存上使用较多的文件系统是JFFS2和Cramfs,其中JFFS2有针对NAND闪存的实现,因此可以很好地处理NAND特性相关的问题, 包括坏块,但是Cramfs无法意识到NAND闪存的存在(Cramfs 不是为NAND闪存设计的),为了能够在NAND闪存上使用Cramfs,需要增加坏块管理,考虑到文件系统的复杂性,直接在Cramfs中增加比较困 难,因此本文考虑在NAND闪存和Cramfs文件系统之间增加一个管理层次来处理这个问题。
Samsung S3C2440X有一个类似的实现方式,但是这个设计存在以下不足:该实现通过存储在NAND闪存分区尾部的分区表来定义又一级的分区,本文认为这是多 的,一般来说用户不会再在闪存分区上定义另外的分区,也没有必要因此而考虑使用专用的操作工具,基于NAND闪存驱动中定义的分区来直接管理坏块并作为用 户分区已经足够,而且这样不需要专有工具支持,设计和使用都比较简单;该实现需要把MTD分区中的坏块信息存储到闪存上,这需要占用空间,本文的实现通过 打开NAND分区时扫描分区上的全部块来记录坏块,由于是每块仅读一次OOB的1位,总耗时不会太长;同时也没有必要把所有的NAND分区都加入到坏块控 制中,只有要使用Cramfs的分区才是需要的,因此应该给予用户选择权。
作者所设计的NAND块管理模块BM 实现为一个只读的块设备驱动程序,它将读操作的地址经过转换处理之后用新的地址执行MTD 的读操作。除了执行与MTD 和NAND相关的特定处理之外它是标准的块设备驱动。
# define NOT2BLON 256
/* 控制标志,所有NAND设备都有此标志,需要加入到BM 者
在其分区上设置此标志(实际上是清除该标志)*/
/* 初始化, 注册MTD User,为MTD 提供设备加入时的回调函
数*/
struct mtd_notifier BM_Notifier = {
add: BM_notify_add,
remove: BM_notify_remove
};
register_mtd_user(&BM_Notifier);
/* 当有新设备加入,或者注册时已经存在MTD 设备时,MTD通过调用BM_notifier_add 通知BM 驱动*/
/*BM_notifier_add 判断设备是否需要加入BM,如果是NAND闪存并且无NOT2BLON 标志则加入BM*/
if (!mtd->type!=MTD_NANDFLASH) return;
if(mtd->flags & MTD_NOT2BLON) return;
/* 扫描设备,获得所有坏块的地址,并保存到bad_blocks 中*/
bad_blocks=kmalloc ((1+mtd->size/mtd->erasesize)*
sizeof(unsigned short), GFP_KERNEL);
offset=0; length=mtd_size –mtd_block_size
for(;offset<= length; offset+=mtd_block_size) {
if (MTD_READOOB(mtd, offset, 8, &retlen, oobbuf) < 0)
continue
if (oobbuf[5] != 0xff)
bad_blocks[badnum++]=offset/mtd->erasesize;
}
/* 读操作时,计算所有小于地址old_block_address 的坏块,得到此地址之前的坏块个数,然后将该地址增加相应的长度作为新地址
new_block_adderss 访问设备*/
if (bad_blocks) {
unsigned short *bad = bad_blocks;
while(*bad++ <= old_block_addrerss)
block++;
}
new_block_adderss=old_block_adderss+block;
BM_notifier_remove 在删除MTD 设备时调用,执行与
BM_notify_add 相反的资源操作,释放占有的资源。
损耗均衡
NAND闪存的寿命是有限的,一个NAND块的擦写次数大概在105-106 次,为了保持NAND的使用寿命足够长,必须避免擦写区域的不均衡,否则闪存会因为局部达到擦写极限而报废,这实际上是浪费。必须实现机制达到磨损均衡(Wear Balance),延长闪存的有效使用寿命。
损耗均衡需要由文件系统或者附加的层次来处理。JFFS2采取日志和顺序写入,很好的解决了这个问题,Cramfs是只读的,除了初始化之外不进行擦除和 写入操作,不需要关心这个问题;需要使用其它非NAND闪存特定的文件系统者(比如Ext2 等)需要考虑这个问题。
NAND存储系统设计
NAND驱动设计相关的问题
除了上述NAND设备的共性问题之外,在设计和实现中还有以下问题需要考虑:
(1)地址自动增加问题
存在两种类型的NAND地址处理方式:地址自动增加和地址不自动增加。对于地址自动增加的NAND,当读数据至一页的末尾时,NAND内部地址自动增加到 下一页的数据区,读取OOB数据时,也会自动增加到下一页的OOB区(部分NAND支持通过GND管脚信号把整个数据区和OOB作为一个连续的区域增加地 址),这个过程会持续到一块结束,然后需要重新向NAND闪存写命令和地址;地址不自动增加的NAND闪存的开始每一页操作都需要重新写命令和地址,否则 内部地址不会自动增加到下一页,无法继续正确操作。这是NAND闪存驱动设计需要注意的,后面就会看到,现在的Linux内核中的MTD版本恰恰存在这方 面的问题。
(2)设备就绪和时间限制
确定设备是否就绪用于判断是否可以开始执行下一个操 作,实现的方式有:延时,读取NAND的状态,读取R/B(Ready/Busy)管脚的输入信号。R/B管脚输入是最快捷的方式,一般的设计中会将 R/B管脚连接到GPIO,NAND驱动通过读取GPIO 输入获得R/B状态,无R/B输入就需要通过延时和读取NAND的状态寄存器来判断。不同的NAND芯片和不同的操作其耗时是不同的(如Samsung K9F56xxQ0B 的写操作时间为200μum-500μm,擦除操作时间为2ms-3ms),需要根据设备Data sheet来设定,不正确的设置会导致错误。
(3)连接方式问题
NAND到系统的连接有软 件连接方式和硬件连接方式:软件方式是在满足时序要求的条件下直接将NAND闪存的IO管脚、控制管脚和系统的数据、地址和读写信号线相连,因此这种连接 方式对NAND的命令、地址的输入和数据的输入输出类似对不同地址的几个寄存器的访问;另一种方式是通过NAND控制器和系统相连,NAND芯片连接到 NAND控制器上,由NAND控制器来满足NAND的时序,控制NAND,并实现硬件ECC等功能,命令、地址输出、数据输入输出和控制是对NAND控制 器的相应寄存器的操作。软件方式硬件接口简单,但需要较多的软件操作来实现功能,硬件方式需要NAND控制器,但是可以通过NAND控制器完成更多的功 能,应该根据成本和功能要求进行设计,根据实际连接方式进行NAND操作。
从NAND闪存启动
作为嵌入式系统的存储设备,从NAND闪存启动的要求是很自然的。由于NAND闪存采用IO接口,无SRAM接口(NOR闪存是RAM接口),不能通过 Bus访问NAND,在NAND驱动加载之前无法访问NAND,因此无法直接从NAND上启动系统,需要其它的辅助机制。有如下的启动机制可供需要从 NAND启动的系统使用:
(1)Boot loader方式:通过系统上的ROM或者NOR闪存,存储一段boot代码,这段代码可以是boot loader的一部分,也可以是全部boot loader,由于ROM和NOR闪存是可以直接启动的,系统启动后加载这部分代码,执行这段代码从NAND读入boot loader的剩余部分和内核映像或者直接读入内核映像,实现从NAND启动。
(2)启动机方式(Boot Engine Method):在系统加电后和产生CPUReset信号之前,通过附加的硬件电路产生符合NAND读操作流程的操作时序(命令、地址和数据读入信号), 将NAND闪存的第一块数据自动加载到系统内存,然后再产生CPU的Reset信号,CPU执行这部分加载到内存中的代码,由这段代码从NAND闪存读入 boot loader的其余部分和内核映像。按照NAND规范的要求,NAND芯片厂商保证出厂时其NAND产品的第一块是完好的,因此所有NAND闪存都可以作 为启动设备。一些NAND闪存支持加电时自动首页读入,当NAND加电时,NAND闪存自动把第一页的内容放到内部缓存,这种情况下实现第二种启动机制所 要做的工作可以简化为把NAND的读信号使能,然后读入第一页。许多系统同时拥有NAND闪存和NOR 闪存,因此从NOR启动不需要额外的工作,第一种启动方式是更好的选择,但是从成本等考虑,第二种启动方式更有吸引力。基于TI Omap 161x的平台支持第二种启动方式。许多开发平台也实现了类似的启动机制,开发者应该根据自己的平台和需求选择合适的启动方式。
基于OMAP161x的实现和Linux内核中的问题
以下部分通过本文的实现和遇到的问题来示例NAND驱动和子系统的设计,并指出内核中NAND部分所存在的一些问题。
基于Omap161x H2的实现
采用TI Omap161x H2开发板来实现基于NAND闪存的存储系统,TI 的Omap 平台在通信类产品中有着很广泛的应用,同时其比较复杂的设计使得这个平台下NAND存储的设计更具典型性。Linux 2.4 和2.6 内核中还没有该平台下NAND的驱动支持,需要开发者来实现。
Omap平台系统支持从NAND启动。Omap161x H2所采用的NAND芯片为Samsung K9F5608Q0B(8位)和K9F5616Q0B(16 位),由于2.4 和2.6.*内核的MTD 版本不支持16 位接口,因此本文采用8 位接口。
该NAND闪存具有如下特点:
页大小:512B,块大小:16kB,OOB 大小:16B;该芯片不支持地址自动增加;采用软件连接方式和Omap 连接。
为了实现该NAND闪存的驱动,需要完成以下工作:
配置EMIFS 寄存器:这是Omap161x 和NAND芯片或者NAND控制器的直接控制接口,需要配置EMIFS与NAND对应的片选寄存器CS2 和NAND管脚复用,片选寄存器CS2需要设置使系统I/O 时序满足NAND的时序要求,同时,需要使和NAND相连接的管脚支持NOR 闪存工作模式。对于首次接触Omap平台的开发者来说,开放源码的Boot Loader是很好的参考,如U-Boot,大部分工作可参考U-Boot中的设置。
实现NAND特定的硬件相关的函数:
由于omap161x H2上NAND是以软件方式连接的,地址比较特殊,需要更改已有的nand_command 函数,实现设备特定的nand_command 函数:Omap161x H2上的NAND闪存通过地址0x0A000000,0x0A000002,0x0A000004进行数据数输入输出、命令输入和地址输入,在内核看来这 是3个8位的寄存器,需要在命令发送、地址发送和数据输入输出中使用这些地址。
实现设备特定的nand_ready, nand_wait 函数,通过超时和读取NAND的状态查询设备是否就绪。Omap161x上的NAND芯片的R/B管脚通过GPIO连接到Omap,可以通过GPIO获得 R/B信号输入,但是设置GPIO比较复杂,尤其是2.4 内核对它的支持很少,更简单的方式是通过超时和读取NAND的状态来实现,这完全可以达到所要求的效果。
本系统采用的 文件系统为JFFS2 和Cramfs,JFFS2 能够直接管理NAND的ECC和坏块,为了在NAND闪存上运行Cramfs并使用ECC,实现了块管理层BM,增加了与JFFS2类似的ECC存储和验 证方式,Cramfs 通过/dev/bm/* (*为序号) 挂接对应的MTD 分区(通过BM 的proc 接口可以查到对应关系)。
2.4 和2.6 内核中存在的问题
实现过程中额外的工作是更正内核中的错误。2.4 和2.6 内核的MTD 版本都假设NAND是支持地址自动增加的,而且没有可供用户选择的地方,因此对于不支持地址自动增加的NAND设备,NAND通用驱动部分的 nand_read_ecc 和nand_read_oob 函数无法正常工作,所产生的错误调试信息往往使开发者陷入误区。作者更改了内核的这部分,通过每一页的读操作都发送读命令和地址,解决了这个问题,这样对 两种类型的NAND闪存都是正确的(尽管对地址自动增加的NAND而言有些多余)。NAND通用驱动中的写操作始终以1 页为1 个操作单元,不存在这个问题。2.4 和2.6 内核中的MTD 版本的NAND通用实现中有关NAND状态和操作队列的实现也存在错误,会导致操作中断和错误的重入。在nand_get_chip 函数中有如下的代码:
if (this->state == FL_ERASING) {
if (new_state != FL_ERASING) {
this->state = new_state;
spin_unlock_bh (&this->chip_lock);
nand_select (); /* select in any case */
this->cmdfunc(mtd, AND_CMD_RESET, -1, -1);
return;
} }
这种实现方式会打断NAND操作,导致未完成擦除操作时重入,从而破坏操作队列和后续操作,引入错误。作者更改了nand_get_chip,删除这部分代码以避免错误的重入。
其它细节问题需要通过调试逐渐优化。充分利用MTD和文件系统(如JFFS2)输出的调试信息是发现错误并使设备驱动等正常工作的重要途径。
结 语
全文结合我们的实现介绍了嵌入式Linux中NAND存储系统的结构和实现方式,同时就NAND特定的一些问题给出了表述和解决方法,就所要关注的问题如 NAND坏块管理、从NAND闪存启动、以及当前2.4 和2.6 内核中的MTD 版本的缺陷进行了阐述,给出了实现,并通过实例说明了NAND存储系统设计中的具体问题,对设计和开发具有参考价值。