Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1297399
  • 博文数量: 860
  • 博客积分: 425
  • 博客等级: 下士
  • 技术积分: 1464
  • 用 户 组: 普通用户
  • 注册时间: 2011-08-20 19:57
个人简介

对技术执着

文章分类

全部博文(860)

文章存档

2019年(16)

2018年(12)

2015年(732)

2013年(85)

2012年(15)

我的朋友

分类: LINUX

2013-05-28 10:32:46

大约用了两个礼拜不到的时间为公司的IPcamera系统写了基于MTDNAND驱动(linux-2.6.22.10内核),目前已可以在该驱动的支持下跑cramfsjffs2文件系统,另外,该驱动也可以同时支持small page(每页512 Byte)big page(每页2048 Byte)两种NAND芯片。在此整理一下与NAND驱动相关的概念,结构体,驱动框架和流程,同时分析一下基于MTDNAND驱动的部分函数,尤其是其中的nand_scan()函数。(涉及到具体NAND芯片时,若不做说明,将以small pageNAND芯片为例。)
 
注:个人理解,有误难免!—— 笔者:曹荣荣
 
 
MTD 驱动程序是专门针对嵌入式Linux的一种驱动程序,相对于常规块设备驱动程序(比如PC中的IDE硬盘)而言,MTD驱动程序能更好的支持和管理闪存设备,因为它本身就是专为闪存设备而设计的。
具体地讲,基于MTDFLASH驱动,承上可以很好地支持cramfsjffs2yaffs等文件系统,启下也能对FLASH的擦除,读写,FLASH坏块以及损耗平衡进行很好的管理。所谓损耗平衡,是指对NAND的擦写不能总是集中在某一个或某几个block中,这是由NAND芯片有限的擦写次数的特性决定的。
总之,在现阶段,要为FLASH设备开发Linux下的驱动程序,那么基于MTD的开发将几乎是省时又省力的唯一选择!
 
一、NANDNOR的区别
 
Google Nand FlashNor Flash的区别”。
 
简单点说,主要的区别就是:
 
1、  NANDNOR便宜;NAND的容量比NOR大(指相同成本);NAND的擦写次数是NOR的十倍;NAND的擦除和写入速度比NOR快,读取速度比NOR稍慢;
 
2、  NANDNOR的读都可以以字节为单位,但NAND的写以page为单位,而NOR可以随机写每一个字节。NANDNOR的擦除都以block为单位,但一般NANDblockNORblock小。另外,不管是NAND还是NOR,在写入前,都必须先进行擦除操作,但是NOR在擦除前要先写0
 
3、  NAND不能在片内运行程序,而NOR可以。但目前很多CPU都可以在上电时,以硬件的方式先将NAND的第一个block中的内容(一般是程序代码,且也许不足一个block,如2KB大小)自动copyram中,然后再运行,因此只要CPU支持,NAND也可以当成启动设备;
 
4、  NANDNOR都可能发生比特位反转(但NAND反转的几率远大于NOR),因此这两者都必须进行ECC操作;NAND可能会有坏块(出厂时厂家会对坏块做标记),在使用过程中也还有可能会出现新的坏块,因此NAND驱动必须对坏块进行管理。
 
二、内核树中基于MTDNAND驱动代码的布局
 
Linux内核中,MTD源代码放在linux-2.6.22.10/driver/mtd目录中,该目录中包含chipsdevicesmapsnandonenandubi六个子目录。
 
其中只有nandonenand目录中的代码才与NAND驱动相关,不过nand目录中的代码比较通用,而onenand目录中的代码相对于nand中的代码而言则简化了很多,它是针对三星公司开发的另一类Flash芯片,即OneNAND Flash。我尚未对OneNand FLASH有过研究,只是通过网上资料得知,OneNand FLASH克服了传统NAND Flash接口复杂的缺点,具有接口简单、读写速度快、容量大、寿命长、成本低等优点,因此应该是一种较常用NAND先进的FLASH吧,只是目前似乎普及率并不高,本文也将不做讨论。
 
因此,若只是开发基于MTDNAND驱动程序,那么我们需要关注的代码就基本上全在linux-2.6.22.10/drivers/mtd/nand目录中了,而该目录中也不是所有的代码文件都与我们将要开发的NAND驱动有关,除了MakefileKconfig之外,其中真正与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[ ]中有三项属性比较重要,即pagesizechipsizeerasesize,驱动就是依据这三项属性来决定对NAND芯片进行擦除,读写等操作时的大小的。其中pagesizeNAND芯片的页大小,一般为2565122048chipsizeNAND芯片的容量;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.cs3c2410.c两个,而其中又尤以cafe_nand.c更为详细,另外,nand目录中也似乎只有cafe_nand.c中的驱动程序在读写NAND芯片时用到了DMA操作。
 
综上所述,若要研究基于MTDNAND驱动,其实所需阅读的代码量也不是很大。
 
另外,在动手写NAND驱动之前,也许需要读一下以下文档:
1、  Linux MTD 源代码分析:
该文档可以让我们对MTD有一个直观而又相对具体的认识,但它似乎主要是针对NOR FLASH的,对于实际开发NAND驱动的帮助并不是很大。
2、  MTD NAND Driver Programming Interface
该文档中关于ECC的说明很有帮助。
3、  MTD的官方网站:
 
三、NAND相关原理
 
在我们开始NAND驱动编写之前,至少应该知道:数据在NAND中是怎样存储的,以及以怎样的方式从NAND中读写数据时。
 
1、  NAND的存储结构和操作方式
 
这方面的资料可以从任意一种NANDdatasheet中得到,因为基本上每一种NANDdatasheet都会介绍NAND的组成结构和操作命令,而且事实上,大多数的NAND datasheet都大同小异,所不同的大概只是该NAND芯片的容量大小和读写速度等基本特性。
 
这里以每页512字节的NAND FLASH为例简单说明一下:每一块NAND芯片由nblock组成->每一个blockmpage组成->每一个page256字节大小的column1(也称1st half page)256字节大小的column2(也称2nd half page)16字节大小的oob(out-of-band,也称spare area)组成。至于mn的大小可以查看特定NANDdatasheet。相应的,若给定NAND中的一个字节的地址,我们可以根据这个地址算出block地址(即第几个block)page地址(即该block中的第几个page)column地址(1st half page,或2nd half page,或oob中的第几个字节)
 
在擦除NAND时,必须每次至少擦除1block;在写NAND时,必须每次写1page(有些NAND也支持写不足一个page大小的数据);在读NAND时,分为三种情况(对应三种不同的NAND命令),即读column1、读column2和读oob,那么为什么要分这三种情况呢?假如知道NAND怎样根据给定的地址确定它的存储单元,那么自然也就能明白原因了,其实也并不复杂,主要是因为给定地址中的A8并不在NAND的视野范围之内(也许表达并不准确)
 
事实上,在写基于MTDNAND驱动时,我们并不需要实现精确到读写某一个byte地址的函数(除了读oob之外),这是因为:
 
基于MTDNAND驱动在读写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的第一个pageoob中的第六个字节)来判断该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];
};
这是用来定义ECCoob中布局的一个结构体。
 
前面已经提及过,oob中主要存储两种信息:坏块信息和ECC数据。对与small pageNAND芯片来说,其中坏块信息占据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 eccbytesECC的字节数,对于512B-per-pageNAND来说,eccbytes = 3,如果你需要额外用到oob中的数据,那么也可以大于3.
uint32_t eccpos[64]ECC数据在oob中的位置,这里之所以是个64字节的数组,是因为对于2048-per-pageNAND来说,它的oob64个字节。而对于512B-per-pageNAND来说,可以而且只可以定义它的前16个字节。
uint32_t oobavailoob中可用的字节数,这个值不用赋值,MTD会根据其它三个变量自动计算得到。
struct nand_oobfree oobfree[MTD_MAX_OOBFREE_ENTRIES]:显示定义空闲的oob字节。
四、基于MTD的NAND驱动架构
 
1platform_deviceplatform_driver的定义和注册
 
对于我们的NAND driver,以下是一个典型的例子:

 static struct platform_driver caorr_nand_driver = {
               .driver = {
                                .name = " caorr-nand",
                                .owner = THIS_MODULE,
                },
                .probe = caorr_nand_probe,
                .remove = caorr_nand_remove,
 };

 static int __init caorr_nand_init(void)
 {
                printk("CAORR NAND Driver, (c) 2008-2009.\n");
                return platform_driver_register(&caorr_nand_driver);
 }

 static void __exit caorr_nand_exit(void)
 {
                platform_driver_unregister(&caorr_nand_driver);
 }

 module_init(caorr_nand_init);
 module_exit(caorr_nand_exit);


 
与大多数嵌入式Linux驱动一样,NAND驱动也是从module_init宏开始。caorr_nand_init是驱动初始化函数,在此函数中注册platform driver结构体,platform driver结构体中自然需要定义proberemove函数。其实在大多数嵌入式Linux驱动中,这样的套路基本已经成了一个定式
 
至于module_init有什么作用,caorr_nand_probe又是何时调用的,以及这个driver是怎么和NAND设备联系起来的,就不再多说了,这里只提三点:
 
A、以上代码只是向内核注册了NANDplatform_driver,即caorr_nand_driver,我们当然还需要一个NANDplatform_device,要不然caorr_nand_driverprobe函数就永远不会被执行,因为没有device需要这个driver
 
B、Linux内核注册NANDplatform_device有两种方式:
其一是直接定义一个NANDplatform_device结构体,然后调用platform_device_register函数注册。作为例子,我们可以这样定义NANDplatform_device结构体:

 struct platform_device caorr_nand_device = {
          .name = "caorr-nand",
          .id = -1,
          .num_resources = 0,
          .resource = NULL,
          .dev = {
              .platform_data = &caorr_platform_default_nand,
          }
 };
 platform_device_register(&caorr_nand_device);


其中num_resourcesresource与具体的硬件相关,主要包括一些寄存器地址范围和中断的定义。caorr_platform_default_nand待会儿再说。需要注意的是,这个platform_devicename的值必须与platform_driver->driver->name的值完全一致,因为platform_bus_typematch函数是根据这两者的name值来进行匹配的。

其二是用platform_device_alloc函数动态分配一个platform_device,然后再用platform_device_add函数把这个platform_device加入到内核中去。具体不再细说,Linux内核中有很多例子可以参考。
相对来说,第一种方式更加方便和直观一点,而第二种方式则更加灵活一点。
 
C、 在加载NAND驱动时,我们还需要向MTD Core提供一个信息,那就是NAND的分区信息,caorr_platform_default_nand主要就是起这个作用,更加详细的容后再说。
 
2MTD架构的简单描述
 
MTD(memory technology device存储技术设备)是用于访问memory设备(ROMflash)的Linux的子系统。MTD的主要目的是为了使新的memory设备的驱动更加简单,为此它在硬件和上层之间提供了一个抽象的接口。MTD的所有源代码在/drivers/mtd子目录下。MTD设备可分为四层(从设备节点直到底层硬件驱动),这四层从上到下依次是:设备节点、MTD设备层、MTD原始设备层和硬件驱动层。

A、Flash硬件驱动层:硬件驱动层负责驱动Flash硬件。
 
B、MTD原始设备:原始设备层有两部分组成,一部分是MTD原始设备的通用代码,另一部分是各个特定的Flash的数据,例如分区。
用于描述MTD原始设备的数据结构是mtd_info,这其中定义了大量的关于MTD的数据和操作函数。mtd_table(mtdcore.c)则是所有MTD原始设备的列表,mtd_part(mtd_part.c)是用于表示MTD原始设备分区的结构,其中包含了mtd_info,因为每一个分区都是被看成一个MTD原始设备加在mtd_table中的,mtd_part.mtd_info中的大部分数据都从该分区的主分区mtd_part->master中获得。
在drivers/mtd/maps/子目录下存放的是特定的flash的数据,每一个文件都描述了一块板子上的flash。其中调用add_mtd_device()、del_mtd_device()建立/删除mtd_info结构并将其加入/删除mtd_table(或者调用add_mtd_partition()、del_mtd_partition()(mtdpart.c)建立/删除mtd_part结构并将mtd_part.mtd_info加入/删除mtd_table 中)。
 
C、MTD设备层:基于MTD原始设备,linux系统可以定义出MTD的块设备(主设备号31)和字符设备(设备号90)。MTD字符设备的定义在mtdchar.c中实现,通过注册一系列file operation函数(lseek、open、close、read、write)。MTD块设备则是定义了一个描述MTD块设备的结构mtdblk_dev,并声明了一个名为mtdblks的指针数组,这数组中的每一个mtdblk_dev和mtd_table中的每一个mtd_info一一对应。
 
D、设备节点:通过mknod在/dev子目录下建立MTD字符设备节点(主设备号为90)和MTD块设备节点(主设备号为31),通过访问此设备节点即可访问MTD字符设备和块设备。
 
E、根文件系统:在Bootloader中将JFFS(或JFFS2)的文件系统映像jffs.image(或jffs2.img)烧到flash的某一个分区中,在/arch/arm/mach-your/arch.c文件的your_fixup函数中将该分区作为根文件系统挂载。
 
F、文件系统:内核启动后,通过mount 命令可以将flash中的其余分区作为文件系统挂载到mountpoint上。
 
以上是从网上找到的一些资料,我只是断断续续地看过一些code,没有系统地研究过,所以这里只能讲一下MTD原始设备层与FLASH硬件驱动之间的交互。
 
一个MTD原始设备可以通过mtd_part分割成数个MTD原始设备注册进mtd_table,mtd_table中的每个MTD原始设备都可以被注册成一个MTD设备,有两个函数可以完成这个工作,即add_mtd_device函数和add_mtd_partitions函数。
 
其中add_mtd_device函数是把整个NAND FLASH注册进MTD Core,而add_mtd_partitions函数则是把NAND FLASH的各个分区分别注册进MTD Core。
 
add_mtd_partitions函数的原型是:

int add_mtd_partitions(struct mtd_info *master,

            const struct mtd_partition *parts, int nbparts);

 
其中master就是这个MTD原始设备,parts即NAND的分区信息,nbparts指有几个分区。那么parts和nbparts怎么来?caorr_platform_default_nand就是起这个作用了。

 static struct mtd_partition caorr_platform_default_nand[ ] = {
    [0] = {
               .name = "Boot Strap",
               .offset = 0,
               .size = 0x40000,
    },
    [1] = {
               .name = "Bootloader",
               .offset = MTDPART_OFS_APPEND,
               .size = 0x40000,
    },
    [2] = {
               .name = "Partition Table",
               .offset = MTDPART_OFS_APPEND,
               .size = 0x40000,
    },
    [3] = {
               .name = "Linux Kernel",
               .offset = MTDPART_OFS_APPEND,
               .size = 0x500000,
    },
    [4] = {
               .name = "Rootfs",
               .offset = MTDPART_OFS_APPEND,
               .size = MTDPART_SIZ_FULL,
    },
 };

 
其中offset是分区开始的偏移地址,在后4个分区我们设为MTDPART_OFS_APPEND,表示紧接着上一个分区,MTD Core会自动计算和处理分区地址;size是分区的大小,在最后一个分区我们设为MTDPART_SIZ_FULL,表示这个NADN剩下的所有部分。
 
这样配置NAND的分区并不是唯一的,需要视具体的系统而定,我们可以在kernel中这样显式的指定,也可以使用bootloader传给内核的参数进行配置。
 
另外,MTD对NAND芯片的读写主要分三部分:
 
A、struct mtd_info中的读写函数,如read,write_oob等,这是MTD原始设备层与FLASH硬件层之间的接口;
 
B、struct nand_ecc_ctrl中的读写函数,如read_page_raw,write_page等,主要用来做一些与ecc有关的操作;
 
C、struct nand_chip中的读写函数,如read_buf,cmdfunc等,与具体的NAND controller相关,就是这部分函数与硬件交互,通常需要我们自己来实现。(注:这里提到的read,write_oob,cmdfunc等,其实都是些函数指针,所以这里所说的函数,是指这些函数指针所指向的函数,以后本文将不再另做说明。)
 
值得一提的是,struct nand_chip中的读写函数虽然与具体的NAND controller相关,但是MTD也为我们提供了default的读写函数,如果你的NAND controller比较通用(使用PIO模式),对NAND芯片的读写与MTD提供的这些函数一致,就不必自己实现这些函数了。
 
这三部分读写函数是相互配合着完成对NAND芯片的读写的。首先,MTD上层需要读写NAND芯片时,会调用struct mtd_info中的读写函数,接着struct mtd_info中的读写函数就会调用struct nand_chip或struct nand_ecc_ctrl中的读写函数,最后,若调用的是struct nand_ecc_ctrl中的读写函数,那么它又会接着调用struct nand_chip中的读写函数。如下图所示:

以读NAND芯片为例,讲解一下这三部分读写函数的工作过程。
 
首先,MTD上层会调用struct mtd_info中的读page函数,即nand_read函数。
 
接着nand_read函数会调用struct nand_chip中cmdfunc函数,这个cmdfunc函数与具体的NAND controller相关,它的作用是使NAND controller向NAND 芯片发出读命令,NAND芯片收到命令后,就会做好准备等待NAND controller下一步的读取。
 
接着nand_read函数又会调用struct nand_ecc_ctrl中的read_page函数,而read_page函数又会调用struct nand_chip中read_buf函数,从而真正把NAND芯片中的数据读取到buffer中(所以这个read_buf的意思其实应该是read into buffer,另外,这个buffer是struct mtd_info中的nand_read函数传下来的)。
 
read_buf函数返回后,read_page函数就会对buffer中的数据做一些处理,比如校验ecc,以及若数据有错,就根据ecc对数据修正之类的,最后read_page函数返回到nand_read函数中。
 
对NAND芯片的其它操作,如写,擦除等,都与读操作类似。
 
五、NAND驱动中的probe函数
 
对于很多嵌入式Linux的外设driver来说,probe函数将是我们遇到的第一个与具体硬件打交道,同时也相对复杂的函数。而且根据我的经验,对于很多外设的driver来说,只要能成功实现probe函数,那基本上完成这个外设的driver也就成功了一多半,基于MTDNAND driver就是一个典型的例子。稍后就可以看到,在NAND driverprobe函数中,就已经涉及到了对NAND芯片的读写。
 
在基于MTDNAND driverprobe函数中,主要可以分为两部分内容,其一是与很多外设driver类似的一些工作,如申请地址,中断,DMA等资源,kzalloc及初始化一些结构体,分配DMA用的内存等等;其二就是与MTD相关的一
 
些特定的工作,在这里我们将只描述第二部分内容。
 
1probe函数中与MTD相关的结构体
 
probe函数中,我们需要为三个与MTD相关的结构体分配内存以及初始化,它们是struct mtd_infostruct mtd_partitionstruct nand_chip。其中前两者已经在前面做过说明,在此略过,这里只对struct nand_chip做一些介绍。
 
struct nand_chip是一个与NAND芯片密切相关的结构体,主要包含三方面内容:
 
A.  指向一些操作NAND芯片的函数的指针,稍后将对这些函数指针作一些说明;
 
B.  表示NAND芯片特性的成员变量,主要有:
 
unsigned int options:与具体的NAND芯片相关的一些选项,如NAND_NO_AUTOINCRNAND_BUSWIDTH_16等,至于这些选项具体表示什么含义,可以参考,那里有较为详细的说明;
 
int page_shift:用位表示的NAND芯片的page大小,如某片NAND芯片的一个page512 个字节,那么page_shift就是9
 
int phys_erase_shift:用位表示的NAND芯片的每次可擦除的大小,如某片NAND芯片每次可擦除16K字节(通常就是一个block的大小),那么phys_erase_shift就是14
 
int bbt_erase_shift:用位表示的bad block table的大小,通常一个bbt占用一个block,所以bbt_erase_shift通常与phys_erase_shift相等;
 
int   chip_shift:用位表示的NAND芯片的容量;
 
int   numchips:表示系统中有多少片NAND芯片;
 
unsigned long chipsizeNAND芯片的大小;
 
int   pagemask:计算page number时的掩码,总是等于chipsize/page大小 1
int   pagebuf:用来保存当前读取的NAND芯片的page number,这样一来,下次读取的数据若还是属于同一个page,就不必再从NAND芯片读取了,而是从data_buf中直接得到;
 
int   badblockpos:表示坏块信息保存在oob中的第几个字节。在每个block的第一个pageoob中,通常用12个字节来表示这是否为一个坏块。对于绝大多数的NAND芯片,若page size  > 512,那么坏块信息从Byte 0开始存储,否则就存储在Byte 5,即第六个字节。
 
C.  eccoobbbt (bad block table)相关的一些结构体,对于坏块及坏块管理,将在稍后做专门介绍。
 
2、对NAND芯片进行实际操作的函数
 
前面已经说过,MTD为我们提供了许多default的操作NAND的函数,这些函数与具体的硬件(NAND controller)相关,而现有的NAND controller都有各自的特性和配置方式,MTD当然不可能为所有的NAND controller都提供一套这样的函数,所以在MTD中定义的这些函数只适用于通用的NAND controller(使用PIO模式)
 
如果你的NAND controller在操作或者说读写NAND时有自己独特的方式,那就必须自己定义适用于你的NAND controller的函数。一般来说,这些与硬件相关的函数都在struct nand_chip结构体中定义,或者应该说是给此结构体中的函数指针赋值。为了更好的理解,我想有必要对struct nand_chip中几个重要的函数指针做一些说明。

struct nand_chip {
    void __iomem *IO_ADDR_R;
    void __iomem *IO_ADDR_W;

    uint8_t (*read_byte)(struct mtd_info *mtd);
    u16 (*read_word)(struct mtd_info *mtd);
    void (*write_buf)(struct mtd_info *mtd, const uint8_t *buf, int len);
    void (*read_buf)(struct mtd_info *mtd, uint8_t *buf, int len);
    int (*verify_buf)(struct mtd_info *mtd, const uint8_t *buf, int len);
    void (*select_chip)(struct mtd_info *mtd, int chip);
    int (*block_bad)(struct mtd_info *mtd, loff_t ofs, int getchip);
    int (*block_markbad)(struct mtd_info *mtd, loff_t ofs);
    void (*cmd_ctrl)(struct mtd_info *mtd, int dat, unsigned int ctrl);
    int (*dev_ready)(struct mtd_info *mtd);
    void (*cmdfunc)(struct mtd_info *mtd, unsigned command, int column, int page_addr);
    int (*waitfunc)(struct mtd_info *mtd, struct nand_chip *this);
    void (*erase_cmd)(struct mtd_info *mtd, int page);
    int (*scan_bbt)(struct mtd_info *mtd);
    int (*errstat)(struct mtd_info *mtd, struct nand_chip *this, int state, int status, int page);
    int (*write_page)(struct mtd_info *mtd, struct nand_chip *chip, const uint8_t *buf, int page, int cached, int raw);

    ……

    struct nand_ecc_ctrl ecc;

    ……
}

 
IO_ADDR_RIO_ADDR_W8NAND芯片的读写地址,如果你的NAND controller是用PIO模式与NAND芯片交互,那么只要把这两个值赋上合适的地址,就完全可以使用MTD提供的default的读写函数来操作NAND芯片了。所以这两个变量视具体的NAND controller而定,不一定用得着;
 
read_byteread_word:从NAND芯片读一个字节或一个字,通常MTD会在读取NAND芯片的IDSTATUSOOB中的坏块信息时调用这两个函数,具体是这样的流程,首先MTD调用cmdfunc函数,发起相应的命令,NAND芯片收到命令后就会做好准备,最后MTD就会调用read_byteread_word函数从NAND芯片中读取芯片的IDSTATUS或者OOB
 
read_bufwrite_bufverify_buf:分别是从NAND芯片读取数据到buffer,把buffer中的数据写入到NAND芯片,和从NAND芯片中读取数据并验证。调用read_buf时的流程与read_byteread_word类似,MTD也是先调用cmdfunc函数发起读命令(NAND_CMD_READ0命令),接着NAND芯片收到命令后做好准备,最后MTD再调用read_buf函数把NAND芯片中的数据读取到buffer中。调用write_buf函数的流程与read_buf相似;
 
select_chip:因为系统中可能有不止一片NAND芯片,所以在对NAND芯片进行操作前,需要这个函数来指定一片NAND芯片;
 
cmdfunc:向NAND芯片发起命令;
 
waitfuncNAND芯片在接收到命令后,并不一定能立即响应NAND controller的下一步动作,对有些命令,比如eraseprogram等命令,NAND芯片需要一定的时间来完成,所以就需要这个waitfunc来等待NAND芯片完成命令,并再次进入准备好状态;
write_page:把一个page的数据写入NAND芯片,这个函数一般不需我们实现,因为它会调用struct nand_ecc_ctrl中的write_page_raw或者write_page函数,关于这两个函数将在稍后介绍。
 
以上提到的这些函数指针,都是REPLACEABLE的,也就是说都是可以被替换的,根据你的NAND controller,如果你需要自己实现相应的函数,那么只需要把你的函数赋值给这些函数指针就可以了,如果你没有赋值,那么MTD会把它自己定义的default的函数赋值给它们。
 
顺便提一下,以上所说的读写NAND芯片的流程并不是唯一的,如果你的NAND controller在读写NAND芯片时有自己独特的方式,那么完全可以按照自己的方式来做。就比如我们公司芯片的NAND controller,因为它使用DMA的方式从NAND芯片中读写数据,所以在我的NAND driver中,读数据的流程是这样的:首先在cmdfunc函数中初始化DMA专用的buffer,配置NAND地址,发起命令等,在cmdfunc中我几乎做了所有需要与NAND芯片交互的事情,总之等cmdfunc函数返回后,NAND芯片中的数据就已经在DMA专用的buffer中了,之后MTD会再调用read_buf函数,所以我的read_buf函数其实只是把数据从DMA专用的buffer中,拷贝到MTD提供的buffer中罢了。
 
最后,struct nand_chip结构体中还包含了一个很重要的结构体,即struct struct nand_ecc_ctrl,该结构体中也定义了几个很重要的函数指针。它的定义如下:

struct nand_ecc_ctrl {

    ……

    void (*hwctl)(struct mtd_info *mtd, int mode);
    int (*calculate)(struct mtd_info *mtd, const uint8_t *dat, uint8_t *ecc_code);
    int (*correct)(struct mtd_info *mtd, uint8_t *dat, uint8_t *read_ecc, uint8_t *calc_ecc);
    int (*read_page_raw)(struct mtd_info *mtd, struct nand_chip *chip, uint8_t *buf);
    void (*write_page_raw)(struct mtd_info *mtd, struct nand_chip *chip, const uint8_t *buf);
    int (*read_page)(struct mtd_info *mtd, struct nand_chip *chip, uint8_t *buf);
    void (*write_page)(struct mtd_info *mtd, struct nand_chip *chip, const uint8_t *buf);
    int (*read_oob)(struct mtd_info *mtd, struct nand_chip *chip, int page, int sndcmd);
    int (*write_oob)(struct mtd_info *mtd, struct nand_chip *chip, int page);
};

 
hwctl:这个函数用来控制硬件产生ecc,其实它主要的工作就是控制NAND controllerNAND芯片发出NAND_ECC_READNAND_ECC_WRITENAND_ECC_READSYN等命令,与struct nand_chip结构体中的cmdfunc类似,只不过发起的命令是ECC相关的罢了;
 
calculate:根据data计算ecc值;
 
correct:根据ecc值,判断读写数据时是否有错误发生,若有错,则立即试着纠正,纠正失败则返回错误;
 
read_page_rawwrite_page_raw:从NAND芯片中读取一个page的原始数据和向NAND芯片写入一个page的原始数据,所谓的原始数据,即不对读写的数据做ecc处理,该读写什么值就读写什么值。另外,这两个函数会读写整个page中的所有内容,即不但会读写一个pageMAIN部分,还会读写OOB部分。
 
read_pagewrite_page:与read_page_rawwrite_page_raw类似,但不同的是,read_pagewrite_page在读写过程中会加入ecc的计算,校验,和纠正等处理。
read_oobwrite_oob:读写oob中的内容,不包括MAIN部分。
 
其实,以上提到的这几个read_xxxwrite_xxx函数,最终都会调用struct nand_chip中的read_bufwrite_buf这两个函数,所以如果没有特殊需求的话,我认为不必自己实现,使用MTD提供的default的函数即可。
 
为进一步理解各函数之间的调用关系,这里提供一张从网上找到的write NAND芯片的流程图,仅供参考:
 
 
 
3probe函数的工作流程
 
由前面的说明可知,我们在要对NAND芯片进行实际操作前已经为struct mtd_infostruct mtd_partitionstruct nand_chip这三个结构体分配好了内存,接下来就要为它们做一些初始化工作。
 
其中,我们需要为struct mtd_info所做的初始化工作并不多,因为MTD Core会在稍后为它做很多初始化工作,但是有一点必须由我们来做,那就是把指向struct nand_chip结构体的指针赋给struct mtd_infopriv成员变量,因为MTD Core中很多函数之间的调用都只传递struct mtd_info,它需要通过priv成员变量得到struct nand_chip
 
对于struct mtd_partition的赋值,前面已经做过介绍,这里不再赘述。
 
所以,为struct nand_chip的初始化,才是我们在probe函数中的主要工作。其实这里所谓的初始化,主要就是为struct nand_chip结构体中的众多函数指针赋值。
 
前面已经为struct nand_chip结构体中的函数指针做过说明,想必你已经知道这些函数指针所指向的函数具体实现什么样的功能,负责做什么事情。那么如何让这些函数实现既定的功能呢?这就与具体的NAND controller有关了,实在没办法多说。根据你的NAND controller,也许你需要做很多工作,为struct nand_chip中的每一个函数指针实现特定的函数,也或许你只需要为IO_ADDR_RIO_ADDR_W赋上地址,其它则什么都不做,利用MTD提供的函数即可。
 
现在假定你定义好了所有需要的与NAND芯片交互的函数,并已经把它们赋给了struct nand_chip结构体中的函数指针。当然,此时你还不能保证这些函数一定能正确工作,但是没有关系,probe函数在接下来的工作中会调用到几乎所有的这些函数,你可以依次来验证和调试。当你的probe函数能顺利通过后,那么这些函数也就基本没什么问题了,你的NAND驱动也就已经完成了80%了。
 
接下来,probe函数就会开始与NAND芯片进行交互了,它要做的事情主要包括这几个方面:读取NAND芯片的ID,然后查表得到这片NAND芯片的如厂商,page sizeerase size以及chip size等信息,接着,根据struct nand_chipoptions的值的不同,或者在NAND芯片中的特定位置查找bad block table,或者scan整个NAND芯片,并在内存中建立bad block table。说起来复杂,但其实所有的这些动作,都可以在MTD提供的一个叫做nand_scan的函数中完成。
 
我虽然研读过nand_scan函数中的代码,但不会在这里做情景分析式的详细说明,若你对这部分代码的实现感兴趣,可以参考以下两篇文章:
 
关于nand_scan函数,在使用时我想有一个地方值得一提。
 
nand_scan函数主要有两个两个函数组成,即nand_scan_ident函数和nand_scan_tail函数。其中nand_scan_ident函数会读取NAND芯片的ID,而nand_scan_tail函数则会查找或者建立bbt (bad block table)
 
在一般情况下,我们可以直接调用nand_scan函数来完成所要做的工作,然而却并不总是如此,在有些情况下,我们必须分别调用nand_scan_ident函数和nand_scan_tail函数,因为在这两者之间,我们还需要做一些额外的工作。那么这里所谓的额外的工作,具体是做什么呢?
 
在《基于MTDNAND驱动开发()》中介绍过一个叫做struct nand_ecclayout的结构体,它用来定义eccoob中的布局。对于small page(每页512 Byte)big page(每页2048 Byte)的两种NAND芯片,它们的eccoob中的布局不尽相同。
 
如果你的driver中对这两种芯片的ecc布局与MTD中定义的default的布局一致,那么就很方便,直接调用nand_scan函数即可。但如果不是,那你就需要为这两种不同的NAND芯片分别定义你的ecc布局。于是问题来了,因为我们在调用nand_scan_ident函数之前,是不知道系统中的NAND芯片是small page类型的,还是big page类型,然而在调用nand_scan_tail函数之前,却必须确定NAND芯片的oob布局(包括ecc布局和坏块信息pattern),因为nand_scan_tail函数在读取oob以及处理ecc时需要这个信息。
所以在这种情况下,我们就需要首先调用nand_scan_ident函数,它会调用一个叫做nand_get_flash_type的函数,MTD就是在这个函数中读取NAND芯片的ID,然后就能查表(即全局变量nand_flash_ids)知道这片NAND芯片的类型(writesize的大小)了。
 
接下来,你就可以在你的NAND驱动中,根据writesize的大小来区分ecc的布局了。最后,我们就可以顺利地调用nand_scan_tail函数了。
 
六、NAND驱动中的坏块管理
 
由于NAND Flash的现有工艺不能保证NAND的Memory Array在其生命周期中保持性能的可靠,因此在NAND芯片出厂的时候,厂家只能保证block 0不是坏块,对于其它block,则均有可能存在坏块,而且NAND芯片在使用的过程中也很容易产生坏块。因此,我们在读写NAND FLASH 的时候,需要检测坏块,同时还需在NAND驱动中加入坏块管理的功能。
 
NAND驱动在加载的时候,会调用nand_scan函数,对bad block table的搜寻,建立等操作就是在这个函数的第二部分,即nand_scan_tail函数中完成的。

在nand_scan_tail函数中,会首先检查struct nand_chip结构体中的options成员变量是否被赋上了NAND_SKIP_BBTSCAN,这个宏表示跳过扫描bbt。所以,只有当你的driver中没有为options定义NAND_SKIP_BBTSCAN时,MTD才会继续与bbt相关工作,即调用struct nand_chip中的scan_bbt函数指针所指向的函数,在MTD中,这个函数指针指向nand_default_bbt函数。

bbt有两种存储方式,一种是把bbt存储在NAND芯片中,另一种是把bbt存储在内存中。对于前者,好处是驱动加载更快,因为它只会在第一次加载NAND驱动时扫描整个NAND芯片,然后在NAND芯片的某个block中建立bbt,坏处是需要至少消耗NAND芯片一个block的存储容量;而对于后者,好处是不会耗用NAND芯片的容量,坏处是驱动加载稍慢,因为存储在内存中的bbt每次断电后都不会保存,所以在每次加载NAND驱动时,都会扫描整个NAND芯片,以便建立bbt。

如果你系统中的NAND芯片容量不是太大的话,我建议还是把bbt存储在内存中比较好,因为根据本人的使用经验,对一块容量为2G bits的NAND芯片,分别采用这两种存储方式的驱动的加载速度相差不大,甚至几乎感觉不出来。

建立bbt后,以后在做擦除等操作时,就不用每次都去验证当前block是否是个坏块了,因为从bbt中就可以得到这个信息。另外,若在读写等操作时,发现产生了新的坏块,那么除了标志这个block是个坏块外,也还需更新bbt。

接下来,介绍一下MTD是如何查找或者建立bbt的。

1、MTD中与bbt相关的结构体

struct nand_chip中的scan_bbt函数指针所指向的函数,即nand_default_bbt函数会首先检查struct nand_chip中options成员变量,如果当前NAND芯片是AG-AND类型的,会强制把bbt存储在NAND芯片中,因为这种类型的NAND芯片中含有厂家标注的“好块”信息,擦除这些block时会导致丢失坏块信息。

接着nand_default_bbt函数会再次检查struct nand_chip中options成员变量,根据它是否定义了NAND_USE_FLASH_BBT,而为struct nand_chip中3个与bbt相关的结构体附上不同的值,然后再统一调用nand_scan_bbt函数,nand_scan_bbt函数会那3个结构体的不同的值做不同的动作,或者把bbt存储在NAND芯片中,或者把bbt存储在内存中。

在struct nand_chip中与bbt相关的结构体如下:

struct nand_chip {
    ……
    uint8_t     *bbt
    struct nand_bbt_descr    *bbt_td;
    struct nand_bbt_descr    *bbt_md;
    struct nand_bbt_descr    *badblock_pattern;
    ……
};

 
bbt指向一块在nand_default_bbt函数中分配的内存,若options中没有定义NAND_USE_FLASH_BBT,MTD就直接在bbt指向的内存中建立bbt,否则就会先从NAND芯片中查找bbt是否存在,若存在,就把bbt的内容读出来并保存到bbt指向的内存中,若不存在,则在bbt指向的内存中建立bbt,最后把它写入到NAND芯片中去。

bbt_td、bbt_md和badblock_pattern就是在nand_default_bbt函数中赋值的3个结构体。它们虽然是相同的结构体类型,但却有不同的作用和含义。
其中bbt_td和bbt_md是主bbt和镜像bbt的描述符(镜像bbt主要用来对bbt的update和备份),它们只在把bbt存储在NAND芯片的情况下使用,用来从NAND芯片中查找bbt。若bbt存储在内存中,bbt_td和bbt_md将会被赋值为NULL。

badblock_pattern就是坏块信息的pattern,其中定义了坏块信息在oob中的存储位置,以及内容(即用什么值表示这个block是个坏块)。

通常用1或2个字节来标志一个block是否为坏块,这1或2个字节就是坏块信息,如果这1或2个字节的内容是0xff,那就说明这个block是好的,否则就是坏块。对于坏块信息在NAND芯片中的存储位置,small page(每页512 Byte)和big page(每页2048 Byte)的两种NAND芯片不尽相同。一般来说,small page的NAND芯片,坏块信息存储在每个block的第一个page的oob的第六个字节中,而big page的NAND芯片,坏块信息存储在每个block的第一个page的oob的第1和第2个字节中。

我不能确定是否所有的NAND芯片都是如此布局,但应该绝大多数NAND芯片是这样的,不过,即使某种NAND芯片的坏块信息不是这样的存储方式也没关系,因为我们可以在badblock_pattern中自己指定坏块信息的存储位置,以及用什么值来标志坏块(其实这个值表示的应该是“好块”,因为MTD会把从oob中坏块信息存储位置读出的内容与这个值做比较,若相等,则表示是个“好块”,否则就是坏块)。

bbt_td、bbt_md和badblock_pattern的结构体类型定义如下:

struct nand_bbt_descr {
    int    options;
    int    pages[NAND_MAX_CHIPS];
    int    offs;
    int    veroffs;
    uint8_t    version[NAND_MAX_CHIPS];
    int    len;
    int    maxblocks;
    int    reserved_block_code;
    uint8_t    *pattern;
};

 
options:bad block table或者bad block的选项,可用的选择以及各选项具体表示什么含义,可以参考

pages:bbt专用。在查找bbt的时候,若找到了bbt,就把bbt所在的page号保存在这个成员变量中。若没找到bbt,就会把新建立的bbt的保存位置赋值给它。因为系统中可能会有多个NAND芯片,我们可以为每一片NAND芯片建立一个bbt,也可以只在其中一片NAND芯片中建立唯一的一个bbt,所以这里的pages是个维数为NAND_MAX_CHIPS的数值,用来保存每一片NAND芯片的bbt位置。当然,若只建立了一个bbt,那么就只使用pages[0]。

offs、len和pattern:MTD会从oob的offs中读出len长度的内容,然后与pattern指针指向的内容做比较,若相等,则表示找到了bbt,或者表示这个block是好的。

veroffs和version:bbt专用。MTD会从oob的veroffs中读出一个字节的内容,作为bbt的版本值保存在version中。

maxblocks:bbt专用。MTD在查找bbt的时候,不会查找NAND芯片中所有的block,而是最多查找maxblocks个block。

2、bbt存储在内存中时的工作流程

前文说过,不管bbt是存储在NAND芯片中,还是存储在内存中,nand_default_bbt函数都会调用nand_scan_bbt函数。

nand_scan_bbt函数会判断bbt_td的值,若是NULL,则表示bbt存储在内存中,它就在调用nand_memory_bbt函数后返回。nand_memory_bbt函数的主要工作就是在内存中建立bbt,其实就是调用了create_bbt函数。

create_bbt函数的工作方式很简单,就是扫描NAND芯片所有的block,读取每个block中第一个page的oob内容,然后根据oob中的坏块信息建立起bbt,可以参见上节关于struct nand_bbt_descr中的offs、len和pattern成员变量的解释。

3、bbt存储在NAND芯片时的工作流程

相对于把bbt存储在内存中,这种方式的工作流程稍显复杂一点。

nand_scan_bbt函数首先从NAND芯片中读取bbt的内容,它读取的方式分为两种:

其一是调用read_abs_bbts函数直接从给定的page地址读取,那么这个page地址在什么时候指定呢?就是在你的NAND driver中指定。前文说过,在struct nand_chip结构体中有两个成员变量,分别是bbt_td和bbt_md,MTD为它们附上了default的值,但是你也可以根据你的需要为它们附上你自己定义的值。假如你为bbt_td和bbt_md的options成员变量定义了NAND_BBT_ABSPAGE,同时又把你的bbt所在的page地址保存在bbt_td和bbt_md的pages成员变量中,MTD就可以直接在这个page地址中读取bbt了。值得一提的是,在实际使用时一般不这么干,因为你不能保证你保存bbt的那个block就永远不会坏,而且这样也不灵活;

其二是调用那个search_read_bbts函数试着在NAND芯片的maxblocks(请见上文关于struct nand_bbt_descr中maxblocks的说明)个block中查找bbt是否存在,若找到,就可以读取bbt了。

MTD查找bbt的过程为:如果你在bbt_td和bbt_md的options 成员变量中定义了 NAND_BBT_LASTBLOCK,那么MTD就会从NAND芯片的最后一个block开始查找(在default情况下,MTD就是这么干的),否则就从第一个block开始查找。

与查找oob中的坏块信息时类似,MTD会从所查找block的第一个page的oob中读取内容,然后与bbt_td或bbt_md中patter指向的内容做比较,若相等,则表示找到了bbt,否则就继续查找下一个block。顺利的情况下,只需查找一个block中就可以找到bbt,否则MTD最多会查找maxblocks个block。
若找到了bbt,就把该bbt所在的page地址保存到bbt_td或bbt_md的pages成员变量中,否则pages的值为-1。

如果系统中有多片NAND芯片,并且为每一片NAND芯片都建立一个bbt,那么就会在每片NAND芯片上重复以上过程。

接着,nand_scan_bbt函数会调用check_create函数,该函数会判断是否找到了bbt,其实就是判断bbt_td或者bbt_md中pages成员变量的值是否有效。若找到了bbt,就会把bbt从NAND芯片中读取出来,并保存到struct nand_chip中bbt指针指向的内存中;若没找到,就会调用create_bbt函数建立bbt(与bbt存储在内存中时情况一样),同时把bbt写入到NAND芯片中去。
 
七、总结

自从写了《基于MTD的NAND驱动开发(一)》后,好久没有动笔,时隔一年才把这篇文章写完,真是惭愧!不过,不管怎么样,总算是写完了,除了还有一些ECC相关的内容外,也基本把我想表达的内容都表达出来了。

本文没有纠缠于MTD中每一句code怎么实现这种细节,因为一来本文主要是写给我自己的,二来我觉得对于开发一个基于NAND的驱动来说,并不需要对MTD中的每一条代码都彻底细致的研究,只要能在总体或者大局上有所把握,能了解MTD中主要函数的工作流程,也就可以了。而且,我觉得对于太细节的东西,只依靠讲解是不起什么作用的,还得自己去研读代码才能明白和掌握。
 
阅读(1009) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~