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 的存储系统设计需要处理不同于其它类型闪存的特有问题。
1 MTD结构和NAND驱动接口
Linux上设计了MTD子系统为闪存类型的设备向上层提供统一接口。MTD的主要目的是实现子系统公用部分,使新的存储设备的驱动设计更加简单。一个MTD设备按照用户访问的顺序可以得到图1所示的层次结构。
图1中NAND特定硬件驱动层为具体NAND设备的驱动,实现特定硬件的具体操作;NAND 通用驱动层是所有NAND设备的公用部分,实现了NAND设备发现、通用NAND读写等操作;MTD原始设备层是MTD原始设备的通用代码,此外还包括各个特定的闪存设备所注册的数据,例如NAND分区等。MTD向上提供块设备和字符设备两种接口。文件系统通过MTD块设备接口访问NAND闪存驱动。
图1 LinuxMTD的层次结构
具体的NAND闪存驱动是和NAND通用驱动相关联的,要实现一个NAND闪存硬件驱动,需要实现以下部分:初始化函数,硬件相关的设备就绪函数和控制函数,为了灵活起见,还可以实现硬件相关的命令函数、硬件相关的等待函数和硬件ECC 函数。
NAND设备驱动初始化时分配必要的内存,设置IO地址,然后利用NAND通用驱动提供的设施,调用nand_scan来完成通用部分的没置,最后定义闪存分区并在MTD 中注册来完成初始化。可以通过Linux内核中的实例了解NAND驱动的结构。
2 和NAND特性相关的问题
NAND闪存具有其它类型的闪存所不具有的特性,因此基于NAND的存储设计需要考虑这些问题。NAND闪存的正确、高效使用取决于以下问题的正确解决。
2.1 坏块问题
NAND闪存定位为一种低成本存储介质,从成本和技术上综合考虑,NAND闪存允许存在一定比例的坏块。坏块的产生有多种原因,如解码失败,地址线错误,存储单元错误等,出品前厂商经过测试,将确认的坏块标记出来。同时,因为NAND闪存的擦写寿命有限(一般在105一106次),当使用到一定时限后也会产生坏块,这些坏块通过擦除和写入后的状态来判断,产生擦除或写入失败的块应该认为已经损坏,并由文件系统或者其它管理程序标记出来。
坏块在NAND闪存的OOB区域中标记,NAND闪存的每页都有16B的OOB(以页为512B为例),每个块的第一页的OOB的第6个B用来记录该块是否是坏块,当该B为非0xff时则表明该块是坏块。
为了正确使用NAND闪存,需要进行坏块管理,坏块管理的基本思想是:避免操作坏块,同时存读写操作时跳过坏块而使得上层操作看来闪存存储区仍然是连续的。
2.2 与文件系统相关的坏块管理
在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相关的特定处理之外它是标准的块设备驱动,实现时可以参考文献4,5。
#define NOT2BLON 256
/*控制标志,所有NAND设备都有此标志,需要加入到BM者在其分区上设置此标志(实际上是清除该标志)*/
/*初始化、注册MTDUser,为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)*
sizeo(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!=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相反的资源操作,释放占有的资源。
2.3 损耗均衡
NAND闪存的寿命是有限的,为了保持NAND的使用寿命足够长,必须避免擦写区域的不均衡,否则闪存会因为局部达到擦写极限而报废,这实际上是浪费 必须实现机制达到磨损均衡(Wear Balance),延长闪存的有效使用寿命。
损耗均衡需要由文件系统或者附加的层次来处理。JFFS2采取日志和顺序写入,很好的解决了这个问题,Cramfs是只读的,除了初始化之外不进行擦除和写入操作,不需要关心这个问题;需要使用其它非NAND闪存特定的文件系统者(比如Ext2等)需要考虑这个问题。
3 NAND存储系统设计
3.1 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芯片和不同的操作其耗时是不同的(如SamsungK9F56xxQ0B 的写操作时间为200uum~500um,擦除操作时间为2ms~3ms),需要根据设备Data sheet来没定,不正确的设置会导致错误。
(3)连接方式问题
NAND到系统的连接有软件连接方式和硬件连接方式:软件方式是在满足时序要求的条件下直接将NAND闪存的IO管脚、控制管脚和系统的数据、地址和读写信号线相连,因此这种连接方式对NAND的命令、地址的输入和数据的输入输出类似对不同地址的几个寄存器的访问;另一种方式是通过NAND 控制器和系统相连,NAND芯片连接到NAND控制器上,由NAND控制器来满足NAND的时序,控制NAND,并实现硬件ECC等功能,命令、地址输出、数据输入输出和控制是对NAND控制器的相应寄存器的操作。软件方式硬件接口简单,但需要较多的软件操作来实现功能,硬件方式需要NAND控制器,但是可以通过NAND控制器完成更多的功能,应该根据成本和功能要求进行设计,根据实际连接方式进行NAND操作。
3.2 从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 Omap161x的平台支持第二种启动方式。许多开发平台也实现了类似的启动机制,开发者应该根据自己的平台和需求选择合适的启动方式。
4 基于OMAP161x的实现和Linux内核中的问题
以下部分通过本文的实现和遇到的问题来示例NAND驱动和子系统的设计,并指出内核中NAND部分所存在的一些问题。
4.1基于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 (见2.2.1节),增加了与JFFS2类似的ECC存储和验证方式,Cramfs通过/dev/bm/* (*为序号) 挂接对应的MTD分区(通过BM 的proc接口可以查到对应关系)。
4.2 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)输出的调试信息是发现错误并使设备驱动等正常工作的最要途径。
5 结语
全文结合我们的实现介绍了嵌入式Linux中NAND存储系统的结构和实现方式,同时就NAND特定的一些问题给出了表述和解决方法,就所要关注的问题如NAND坏块管理、从NAND闪存启动、以及当前2.4和2.6内核中的MTD版本的缺陷进行了阐述,给出了实现,并通过实例说明了NAND存储系统设计中的具体问题,对设计和开发具有参考价值。