○、说明
大约用了两个礼拜不到的时间为公司的IPcamera 系统写了基于MTD 的NAND 驱动(linux-2.6.22.10 内核) ,目前已可以在该驱动的支持下跑cramfs 和jffs2 文件系统,另外,该驱动也可以同时支持small page( 每页512 Byte) 和big page( 每页2048 Byte) 两种NAND 芯片。在此整理一下与NAND 驱动相关的概念,结构体,驱动框架和流程,同时分析一下基于MTD 的NAND 驱动的部分函数,尤其是其中的nand_scan() 函数。( 涉及到具体NAND 芯片时,若不做说明,将以small page 的NAND 芯片为例。)
注:个人理解,有误难免!—— 笔者:曹荣荣
MTD 驱动程序是专门针对嵌入式 Linux 的一种驱动程序,相对于常规块设备驱动程序(比如 PC 中的 IDE 硬盘)而言, MTD 驱动程序能更好的支持和管理闪存设备,因为它本身就是专为闪存设备而设计的。
具体地讲,基于 MTD 的 FLASH 驱动,承上可以很好地支持 cramfs , jffs2 和 yaffs 等文件系统,启下也能对 FLASH 的擦除,读写, FLASH 坏块以及损耗平衡进行很好的管理。所谓损耗平衡,是指对 NAND 的擦写不能总是集中在某一个或某几个 block 中,这是由 NAND 芯片有限的擦写次数的特性决定的。
总之,在现阶段,要为 FLASH 设备开发 Linux 下的驱动程序,那么基于 MTD 的开发将几乎是省时又省力的唯一选择!
一、NAND 和 NOR 的区别
Google “Nand Flash 和Nor Flash 的区别”。
简单点说,主要的区别就是:
1、 NAND 比NOR 便宜;NAND 的容量比NOR 大(指相同成本);NAND 的擦写次数是NOR 的十倍;NAND 的擦除和写入速度比NOR 快,读取速度比NOR 稍慢;
2、 NAND 和NOR 的读都可以以字节为单位,但NAND 的写以page 为单位,而NOR 可以随机写每一个字节。NAND 和NOR 的擦除都以block 为单位,但一般NAND 的block 比NOR 的block 小。另外,不管是NAND 还是NOR ,在写入前,都必须先进行擦除操作,但是NOR 在擦除前要先写0 ;
3、 NAND 不能在片内运行程序,而NOR 可以。但目前很多CPU 都可以在上电时,以硬件的方式先将NAND 的第一个block 中的内容(一般是程序代码,且也许不足一个block ,如2KB 大小)自动copy 到ram 中,然后再运行,因此只要CPU 支持,NAND 也可以当成启动设备;
4、 NAND 和NOR 都可能发生比特位反转(但NAND 反转的几率远大于NOR ),因此这两者都必须进行ECC 操作;NAND 可能会有坏块(出厂时厂家会对坏块做标记),在使用过程中也还有可能会出现新的坏块,因此NAND 驱动必须对坏块进行管理。
二、 内核树中基于 MTD 的 NAND 驱动代码的布局
在Linux 内核中,MTD 源代码放在linux-2.6.22.10/driver/mtd 目录中,该目录中包含chips 、devices 、maps 、nand 、onenand 和ubi 六个子目录。
其中只有nand 和onenand 目录中的代码才与NAND 驱动相关,不过nand 目录中的代码比较通用,而onenand 目录中的代码相对于nand 中的代码而言则简化了很多,它是针对三星公司开发的另一类Flash芯片,即OneNAND Flash。我尚未对OneNand FLASH有过研究,只是通过网上资料得知,OneNand FLASH克服了传统NAND Flash接口复杂的缺点,具有接口简单、读写速度快、容量大、寿命长、成本低等优点,因此应该是一种较常用NAND先进的FLASH吧,只是目前似乎普及率并不高,本文也将不做讨论。
因此,若只是开发基于MTD 的NAND 驱动程序,那么我们需要关注的代码就基本上全在linux-2.6.22.10/drivers/mtd/nand 目录中了,而该目录中也不是所有的代码文件都与我们将要开发的NAND 驱动有关,除了Makefile 和Kconfig 之外,其中真正与NAND 驱动有关的代码文件只有6 个,即:
1、 nand_base.c :
定义了NAND 驱动中对NAND 芯片最基本的操作函数和操作流程,如擦除、读写page 、读写oob 等。当然这些函数都只是进行一些default 的操作,若你的系统在对NAND 操作时有一些特殊的动作,则需要在你自己的驱动代码中进行定义,然后Replace 这些default 的函数。
2、 nand_bbt.c :
定义了NAND 驱动中与坏块管理有关的函数和结构体。
3、 nand_ids.c :
定义了两个全局类型的结构体:struct nand_flash_dev nand_flash_ids[ ] 和struct nand_manufacturers nand_manuf_ids[ ] 。其中前者定义了一些NAND 芯片的类型,后者定义了NAND 芯片的几个厂商。NAND 芯片的ID 至少包含两项内容:厂商ID 和厂商为自己的NAND 芯片定义的芯片ID 。当NAND 驱动被加载的时候,它会去读取具体NAND 芯片的ID ,然后根据读取的内容到上述定义的nand_manuf_ids[ ] 和nand_flash_ids[ ] 两个结构体中去查找,以此判断该NAND 芯片是那个厂商的产品,以及该NAND 芯片的类型。若查找不到,则NAND 驱动就会加载失败,因此在开发NAND 驱动前必须事先将你的NAND 芯片添加到这两个结构体中去(其实这两个结构体中已经定义了市场上绝大多数的NAND 芯片,所以除非你的NAND 芯片实在比较特殊,否则一般不需要额外添加)。值得一提的是,nand_flash_ids[ ] 中有三项属性比较重要,即pagesize 、chipsize 和erasesize ,驱动就是依据这三项属性来决定对NAND 芯片进行擦除,读写等操作时的大小的。其中pagesize 即NAND 芯片的页大小,一般为256 、512 或2048 ;chipsize 即NAND 芯片的容量;erasesize 即每次擦除操作的大小,通常就是NAND 芯片的block 大小。
4、 nand_ecc.c :
定义了NAND 驱动中与softeware ECC 有关的函数和结构体,若你的系统支持hardware ECC ,且不需要software ECC ,则该文件也不需理会。
5、 nandsim.c :
定义了Nokia 开发的模拟NAND 设备,默认是Toshiba NAND 8MiB 1,8V 8-bit (根据ManufactureID ),开发普通NAND 驱动时不用理会。
6、 diskonchip.c :
定义了片上磁盘(DOC) 相关的一些函数,开发普通NAND 驱动时不用理会。
除了上述六个文件之外,nand 目录中其他文件基本都是特定系统的NAND 驱动程序例子,但本人看来真正有参考价值的只有cafe_nand.c 和s3c2410.c 两个,而其中又尤以cafe_nand.c 更为详细,另外,nand 目录中也似乎只有cafe_nand.c 中的驱动程序在读写NAND 芯片时用到了DMA 操作。
综上所述,若要研究基于MTD 的NAND 驱动,其实所需阅读的代码量也不是很大。
另外,在动手写NAND 驱动之前,也许需要读一下以下文档:
1、 Linux MTD 源代码分析:
该文档可以让我们对MTD 有一个直观而又相对具体的认识,但它似乎主要是针对NOR FLASH 的,对于实际开发NAND 驱动的帮助并不是很大。
2、 MTD NAND Driver Programming Interface :
该文档中关于ECC 的说明很有帮助。
3、 MTD 的官方网站:
三、NAND 相关原理
在我们开始NAND 驱动编写之前,至少应该知道:数据在NAND 中是怎样存储的,以及以怎样的方式从NAND 中读写数据时。
1、 NAND 的存储结构和操作方式
这方面的资料可以从任意一种NAND 的datasheet 中得到,因为基本上每一种NAND 的datasheet 都会介绍NAND 的组成结构和操作命令,而且事实上,大多数的NAND datasheet 都大同小异,所不同的大概只是该NAND 芯片的容量大小和读写速度等基本特性。
这里以每页512 字节的NAND FLASH 为例简单说明一下:每一块NAND 芯片由n 个block 组成-> 每一个block 由m 个page 组成-> 每一个page 由256 字节大小的column1( 也称1st half page) 、256 字节大小的column2( 也称2nd half page) 和16 字节大小的oob(out-of-band ,也称spare area) 组成。至于m 和n 的大小可以查看特定NAND 的datasheet 。相应的,若给定NAND 中的一个字节的地址,我们可以根据这个地址算出block 地址( 即第几个block) 、page 地址( 即该block 中的第几个page) 和column 地址( 即1st half page ,或2nd half page ,或oob 中的第几个字节) 。
在擦除NAND 时,必须每次至少擦除1 个block ;在写NAND 时,必须每次写1 个page( 有些NAND 也支持写不足一个page 大小的数据) ;在读NAND 时,分为三种情况( 对应三种不同的NAND 命令) ,即读column1 、读column2 和读oob ,那么为什么要分这三种情况呢?假如知道NAND 怎样根据给定的地址确定它的存储单元,那么自然也就能明白原因了,其实也并不复杂,主要是因为给定地址中的A8 并不在NAND 的视野范围之内( 也许表达并不准确) 。
事实上,在写基于MTD 的NAND 驱动时,我们并不需要实现精确到读写某一个byte 地址的函数( 除了读oob 之外) ,这是因为:
基于MTD 的NAND 驱动在读写NAND 时,可以分两种情况,即:(1) 不进行ECC 检测时,一次读写一整个page 中的MAIN 部分( 也就是那真实存储数据的512 字节) ;(2) 进行ECC 检测时( 不管是hardware ECC 还是software ECC) ,一次读写一整个page( 包括16 字节的oob 部分) 。所以部分NAND 所支持的写不足一个page 大小数据的功能,对MTD 来说是用不着的。
那么,如果只需要读写不足一个page 大小的数据怎么办?这是MTD 更上层的部分需要处理的事。也就是说,对于NAND 驱动来说,它只会读写整整一个page 的数据!
最后值得一提的是,NAND 驱动有可能只去读oob 部分,这是因为除了ECC 信息之外,坏块信息也存储在oob 之中,NAND 驱动需要读取oob 中描述坏块的那个字节( 通常是每个block 的第一个page 的oob 中的第六个字节) 来判断该block 是不是一个坏块。所以,我们只有在读oob 时,才需要实现精确到读某一个byte 地址的函数。
由此,我们也可以额外知道一件事,那就是NAND 驱动中用到的column 地址只在读oob 时才有用,而在其他情况下,column 地址都为0 。
2、 ECC 相关的结构体
struct nand_ecclayout {
uint32_t eccbytes;
uint32_t eccpos[64];
uint32_t oobavail;
struct nand_oobfree oobfree[MTD_MAX_OOBFREE_ENTRIES];
};
这是用来定义ECC 在oob 中布局的一个结构体。
前面已经提及过,oob 中主要存储两种信息:坏块信息和ECC 数据。对与small page 的NAND 芯片来说,其中坏块信息占据1 个字节( 一般固定在第六个字节) ,ECC 数据占据三个字节。所以sturct nand_ecclayout 这个结构体,也就是用来告诉那些与ECC 操作无关的函数,Nand 芯片的oob 部分中,哪些字节是用来存储ECC 的( 即不可用作它用的) ,哪些字节是空闲的,即可用的。
其实之所以有这个结构体,主要是因为硬件ECC 的缘故。以写数据为例,在使用硬件ECC 的情况下,那三个字节的ECC 数据是由硬件计算得到,并且写到NAND 芯片的oob 中去的,同时也是由硬件决定写到oob 的哪三个字节中去。这些都是由硬件做的,而NAND 驱动并不知道,所以就需要用这个结构体来告诉驱动了。
所以,在写NAND 驱动时,就有可能需要对这个结构体进行赋值。这里说“有可能”,是因为MTD 对这个结构体有一个默认的赋值,假如这个赋值所定义的ECC 位置与你的硬件一致的话,那就不必在你的驱动中手动赋值了。其实对大多数硬件( 这里所说的硬件,不是指NAND 芯片,而是NAND 控制器) 来说,是不必手动赋值的,但也有许多例外。
值得一提的是,这个结构体不仅仅用来定义ECC 布局,也可以用来将你的驱动在oob 中需要额外用到的字节位置保护起来。
现在对struct nand_ecclayout 这个结构体进行一下说明。
uint32_t eccbytes :ECC 的字节数,对于512B-per-page 的NAND 来说,eccbytes = 3 ,如果你需要额外用到oob 中的数据,那么也可以大于3.
uint32_t eccpos[64] :ECC 数据在oob 中的位置,这里之所以是个64 字节的数组,是因为对于2048-per-page 的NAND 来说,它的oob 有64 个字节。而对于512B-per-page 的NAND 来说,可以而且只可以定义它的前16 个字节。
uint32_t oobavail :oob 中可用的字节数,这个值不用赋值,MTD 会根据其它三个变量自动计算得到。
struct nand_oobfree oobfree[MTD_MAX_OOBFREE_ENTRIES] :显示定义空闲的oob 字节。