Nand Flash 和 MTD 子系统

2015-7-6 chenhui 设备驱动

Nand Flash 是一种块设备,关于块设备的基础原理,可查看 设备管理之块设备

Linux 为所有的 Nand Flash 提供了一个标准的模型,也就是 MTD,本文讲解 MTD 的原理。


块设备和字符设备不同,块设备驱动是系统中最为基础的部分之一,他规定了系统使用的磁盘的分区(每一个分区都可视为一个块设备,并使用同一个块设备驱动操作),并对这些分区进行控制,这与传统磁盘不同,传统磁盘的分区表是写在磁盘内的。


NNAND FLASH 的分区是通过软件划分的。而 MTD 就具备了这么一个能力,MTD 使用 struct mtd_partition 来描述系统中的一个分区,而所有的分区,自然就是用一个 struct mtd_partition 数组来描述的,对于 NAND FLASH 的 MTD 驱动,定义这么一个数组是必须的。

下面是 struct mtd_partition 的定义:


/* 分区描述符 */
struct mtd_partition { 

	//分区名
	char *name;	 

	//分区大小
	u_int32_t size;		 

	//分区在 MTD 设备里的偏移(开始地址偏移)
	u_int32_t offset;	 

	//分区标志
	u_int32_t mask_flags;	 

	//NAND Flash 的 OOB 相关
	struct nand_oobinfo *oobsel; 

	//指向 MTD 设备(mtd_partition 只是分区,这个 mtdp 是描述 Flash 的设备)
	struct mtd_info **mtdp;	 
};


这里的最后一个 mtdp 指针指向一个 mtd_info 结构体,这个结构体是用来描述一个 MTD 设备(比如 NAND FLASH)


/* MTD设备描述符  */
struct mtd_info {
	
	u_char type;
	u_int32_t flags; 

	u_int32_t size; 

	u_int32_t erasesize;

	...省略一大堆
	
	char *name;
	int index; 

	struct nand_oobinfo oobinfo;

	int numeraseregions;
	struct mtd_erase_region_info *eraseregions; 

	u_int32_t bank_size;

        ...这里省略一大堆函数指针
	void (*sync) (struct mtd_info *mtd);

	int (*lock) (struct mtd_info *mtd, loff_t ofs, size_t len);
	int (*unlock) (struct mtd_info *mtd, loff_t ofs, size_t len);

	int (*suspend) (struct mtd_info *mtd);
	void (*resume) (struct mtd_info *mtd);

	int (*block_isbad) (struct mtd_info *mtd, loff_t ofs);
	int (*block_markbad) (struct mtd_info *mtd, loff_t ofs);

	void *priv;

	struct module *owner;
	int usecount;
};



这个 mtd_info 里的都是一些回调函数,没什么好说的。


这里会有一个疑问,那就是块设备是用 gendisk 来进行描述的,那这里的 gendisk 结构体在哪呢?


gendisk 当然有,但不是在这里声明的,而是由内核动态生成的。实际上,mtd_info 和 mtd_partition 这两个结构体,就已经构成了一个块设备的基本条件:设备本身和分区,也就是说这两个就可以描述一个块设备了,那么块设备的 gendisk 对象在哪里生成呢?


在内核里,不管什么东西,都是先生成后注册的,所以 gendisk 对象肯定也是在注册 mtd_info 和 mtd_partition 后生成的。但是,mtd_info 和 mtd_partition 又是怎么来的呢?其实就是在驱动程序生成的!一般来说,mtd_partition 描述的分区信息是静态定义的,而 mtd_info 则是动态生成的,当然 mtd_info 也可以自己完全生成,自己实现里面必须的回调函数,也是可以的。


下面先来看一个 NAND FLASH 的驱动程序,看看他是怎么生成 mtd_partition 和 mtd_info 的:


static struct mtd_partition s3c_nand_parts[] = {
	[0] = {//第一个分区,存放 bootloader
        .name   = "bootloader",
        .size   = 0x00040000,
		.offset	= 0,
	},
	[1] = {//第二个分区,存放内核参数
        .name   = "params",
        .offset = MTDPART_OFS_APPEND,//这个表示当前分区紧挨着上一个分区
        .size   = 0x00020000,
	},
	[2] = {//第三个分区,存放内核
        .name   = "kernel",
        .offset = MTDPART_OFS_APPEND,
        .size   = 0x00200000,
	},
	[3] = {//第四个分区,存放根文件系统
        .name   = "root",
        .offset = MTDPART_OFS_APPEND,
        .size   = MTDPART_SIZ_FULL,//这个表示使用剩下所有容量
	}
};

static struct mtd_info *s3c_mtd;

static int s3c_nand_init(void)
{
	
	//... 对于 NAND FLASH,这里还会有其他数据结构和初始化,省略掉
	s3c_mtd = kzalloc(sizeof(struct mtd_info), GFP_KERNEL);
	s3c_mtd->owner = THIS_MODULE;

	s3c_mtd->priv = s3c_nand;
	
	nand_scan(s3c_mtd, 1);  /* 识别NAND FLASH, 构造mtd_info */
	
	add_mtd_partitions(s3c_mtd, s3c_nand_parts, 4);
	
	return 0;
}

static void s3c_nand_exit(void)
{
	del_mtd_partitions(s3c_mtd);
}


可以看出的是,mtd_partition 数组就是描述分区的,他是静态定义的,而 mtd_info,则是通过 nand_scan 构造出来的。


实际上在 29 行那里我省略了很多代码,这些代码主要申请了一个 nand_chip 结构体,也就是 s3c_nand,他描述了 nand flash 的硬件参数。在 nand_scan 中,就是取出这个 s3c_nand 对象,并根据他内部的硬件参数在系统中找到一个符合的 Nand 配置,并根据这个配置来填充 mtd_info。如果我们需要支持一个新的 nand flash,最好先在内核中看看有没有符合芯片的配置,如果没有,就需要自己添加了,否则是无法识别的。


在 nand_scan() 填充 mtd_info 之后,就调用了 37 行的 add_mtd_partition 把 MTD 设备和分区注册到系统中。

MTD 设备和分区是怎么注册的呢?其实很简单,在内核中,分区用 mtd_part 来描述,而且分区本身也是块设备,所以 mtd_part 自己也顺带了一个 mtd_info。所以这里要做的就是,为每个分区创建 mtd_part,并用 mtd_parition 来初始化他。


先对整个流程来个简介:

  1. 遍历 mtd_partition 数组内所有分区, 为每一个分区创建一个 mtd_part 结构体并初始化
  2. 通过 mtd_part 内部的 mtd_info 结构体来注册每一个分区
  3. 把所有分区的 mtd_info 结构体存入 mtd_table
  4. 为所有分区的 mtd_info 创建一个 gendisk 结构体,申请设备号,提供请求队列,注册他 。
  5. 接下来的就是块设备的事情了。


/* 所有的分区链表 */
static LIST_HEAD(mtd_partitions);

int add_mtd_partitions(struct mtd_info *master, 
			       const struct mtd_partition *parts,
			       int nbparts)
{
	struct mtd_part *slave;
	u_int32_t cur_offset = 0;
	int i;

	for (i = 0; i < nbparts; i++) {// 遍历分区

		slave = kmalloc (sizeof(*slave), GFP_KERNEL);
                // 申请一个 mtd_part 对象,描述一个分区。
		if (!slave) {

			printk ("memory allocation error while 。 \"%s\"\n",
				master->name);
			del_mtd_partitions(master);
			return -ENOMEM;
		} 

		memset(slave, 0, sizeof(*slave));

		list_add(&slave->list, &mtd_partitions);

		...// 省略初始化代码, 主要就是 mtd_partition 中的分区信息写到 mtd_part 内

		slave->mtd.erase = part_erase;
		slave->master = master;
		slave->offset = parts[i].offset;
		slave->index = i;
		
		//如果当前分区的偏移地址是 MTDPART_OFS_APPEND
		//那就说明这个分区紧挨着上一个分区,cur_offset 记录了这个值
		if (slave->offset == MTDPART_OFS_APPEND)
			slave->offset = cur_offset;

		if (slave->offset == MTDPART_OFS_NXTBLK) {
			u_int32_t emask = master->erasesize-1;
			slave->offset = (cur_offset + emask) & ~emask;
			if (slave->offset != cur_offset) {
				printk(KERN_NOTICE "Moving partition %d: "
				       "0x%08x -> 0x%08x\n", i,
				       cur_offset, slave->offset);
			}
		}

		//如果分区大小占满剩下所有资源,那么他的大小就是总容量减去其偏移值
		if (slave->mtd.size == MTDPART_SIZ_FULL)
			slave->mtd.size = master->size - slave->offset;

		//记录当前分区的结束地址,给下一个分区的 MTDPART_OFS_APPEND 使用
		cur_offset = slave->offset + slave->mtd.size;
	
		...//继续省略

		memcpy(&slave->mtd.oobinfo, &master->oobinfo, sizeof(slave->mtd.oobinfo));

		if(parts[i].mtdp){ 

			*parts[i].mtdp = &slave->mtd;
			slave->registered = 0;
		}
		else
		{
			add_mtd_device(&slave->mtd);
			slave->registered = 1;
		}
	}

	return 0;
}


在这个函数中,我们又能看到另一个结构体:struct mtd_part。他也是代表一个分区,他和 mtd_partition 不同的是,mtd_part 在内核中使用,而后者则被驱动程序使用。


struct mtd_part {
	struct mtd_info mtd;
	struct mtd_info *master;
	u_int32_t offset;
	int index;
	struct list_head list;
	int registered;
};


struct mtd_part 里有两个 mtd_info 值得注意,第一个不是指针,第二个则是一个指针。从名字上可以看出,mtd_info *master 指向的是这个分区的管理者的 mtd_info 描述符,而第一个 mtd_info mtd 又是干什么的?我们知道,分区也是一个块设备,而在 MTD 中,每一个块设备都使用 mtd_info 来描述,也就是说,即使是分区,他有会有一个 struct mtd_info 对象,只是分区仅仅是真正的 Nand Flash 的一部分,他们的参数是一样的,所以驱动程序那里,只需要编写一个 mtd_info 即可,分区的 mtd_info 会自动生成。


add_mtd_partitions() 这一整个大循环中,其实就是为所有的分区生成 mtd_part 对象,并初始化他的 mtd_info 对象,最终我们可以发现在 68 行,调用了 add_mtd_device() 把他的 mtd 字段传了进去,就在这个函数里,开始了分区的注册。


int add_mtd_device(struct mtd_info *mtd)
{
	int i;

	down(&mtd_table_mutex);

        // mtd_table 保存所有 MTD 设备, 这里把当前 MTD 设备(分区)插入 mtd_table
	for (i=0; i < MAX_MTD_DEVICES; i++)
		if (!mtd_table[i]) {
			struct list_head *this;

			mtd_table[i] = mtd;
			mtd->index = i;
			mtd->usecount = 0;

			DEBUG(0, "mtd: Giving out device %d to %s\n",i, mtd->name);

                        // MTD 设备有很多种,找到当前 MTD 设备对应的,调用 add 来通知他,新设备来了
			list_for_each(this, &mtd_notifiers) {
				struct mtd_notifier *not =  
                                    list_entry(this, struct mtd_notifier, list);
				not->add(mtd);
			}
			
			up(&mtd_table_mutex);
			__module_get(THIS_MODULE);
			return 0;
		}
	
	up(&mtd_table_mutex);
	return 1;
}


这里又冒出一个不知道从哪里来的 struct mtd_info *mtd_table[MAX_MTD_DEVICES],但从 8~13 行的代码可以看出,这个 mtd_table 保存了系统中所有的 mtd 设备,也就是说系统只能容纳这么点 mtd 设备,再多就没办法再注册了!


那么这里的 mtd_notifiers 又是个什么东西?这东西实际上是个 mtd_notifier 类型的链表,先来看一下这个 mtd_notifier。

struct mtd_notifier {
	void (*add)(struct mtd_info *mtd);
	void (*remove)(struct mtd_info *mtd);
	struct list_head list;
};


可以看出他的 add() 和 remove() 两个函数,根据结构体名可推断出,这两个函数实际上起到通知的效果,也就是添加一个 mtd 设备时,就调用 add() 通知一个新的 mtd 设备加入了,反之则调用 remove() 进行通知。


那么这个链表是什么时候生成的呢?我们最终会调用到哪个函数呢?实际上,内核提供了 register_mtd_user() 函数把一个 mtd_notifier 注册到链表内,而调用了这个函数的有两个地方,那就是声明是在 drivers/mtd/mtdchar.c 里的 register_mtd_blktrans() 和 声明在 drivers/mtd/mtdblock.c 里的 mtdchar_devfs_init()。


实际上,MTD 设备并不要求你一定是块设备,你也可以是一个字符设备,这两者的注册方法不同,也就是在这里体现出来的。对于块设备,会调用 blktrans_notify_add() 函数,这个函数会生成 device 设备并添加到块设备体系中;字符设备则调用 mtd_notify_add() 把设备添加到字符设备体系中。


下面介绍 blktrans_notify_add()。


static void blktrans_notify_add(struct mtd_info *mtd)
{
	struct list_head *this;

	if (mtd->type == MTD_ABSENT)
		return;

	list_for_each(this, &blktrans_majors) {

		struct mtd_blktrans_ops *tr = 
                    list_entry(this, struct mtd_blktrans_ops, list);

		tr->add_mtd(tr, mtd);
	}

}



blktrans_majors链表在register_mtd_blkrans被调用,而register_mtd_blktrans则是被init_mtdblock调用。

这里不必过多追究,只需要知道这里的 add_mtd() 调用的是 mtdblock_add_mtd() 即可。


static void mtdblock_add_mtd(struct mtd_blktrans_ops *tr, struct mtd_info *mtd)
{
	struct mtd_blktrans_dev *dev = kmalloc(sizeof(*dev), GFP_KERNEL);

	if (!dev)
		return;

	memset(dev, 0, sizeof(*dev));

	dev->mtd = mtd;
	dev->devnum = mtd->index;
	dev->blksize = 512;
	dev->size = mtd->size >> 9;
	dev->tr = tr;

	if (!(mtd->flags & MTD_WRITEABLE))
		dev->readonly = 1;

	add_mtd_blktrans_dev(dev);
}


不必过多追究这个 mtd_blktrans_dev,只需要知道,他代表一个 MTD 块设备即可,继续往下看,胜利就在眼前。


int add_mtd_blktrans_dev(struct mtd_blktrans_dev *new)
{
	struct mtd_blktrans_ops *tr = new->tr;
	struct list_head *this;
	int last_devnum = -1;
	struct gendisk *gd;

	if (!down_trylock(&mtd_table_mutex)) {
		up(&mtd_table_mutex);
		BUG();
	}

	list_for_each(this, &tr->devs) {
		struct mtd_blktrans_dev *d = 
                    list_entry(this, struct mtd_blktrans_dev, list);
		if (new->devnum == -1) {
			/* 使用第一个空闲设备号 */
			if (d->devnum != last_devnum+1) {
				/* 找到一个空闲设备号,在这里递增他 */
				new->devnum = last_devnum+1;
				list_add_tail(&new->list, &d->list);
				goto added;
			}
		} else if (d->devnum == new->devnum) {
			/* Required number taken */
			return -EBUSY;
		} else if (d->devnum > new->devnum) {
			/* Required number was free */
			list_add_tail(&new->list, &d->list);
			goto added;
		} 
		last_devnum = d->devnum;
	}
	if (new->devnum == -1)
		new->devnum = last_devnum+1;

	if ((new->devnum << tr->part_bits) > 256) {
		return -EBUSY;
	}

	init_MUTEX(&new->sem);
	list_add_tail(&new->list, &tr->devs);
 added:
	if (!tr->writesect)
		new->readonly = 1;

	gd = alloc_disk(1 << tr->part_bits);
	if (!gd) {
		list_del(&new->list);
		return -ENOMEM;
	}
	gd->major = tr->major;
	gd->first_minor = (new->devnum) << tr->part_bits;
	gd->fops = &mtd_blktrans_ops;
	
	snprintf(gd->disk_name, sizeof(gd->disk_name),
		 "%s%c", tr->name, (tr->part_bits?'a':'0') + new->devnum);
	snprintf(gd->devfs_name, sizeof(gd->devfs_name),
		 "%s/%c", tr->name, (tr->part_bits?'a':'0') + new->devnum);

	/* 2.5 has capacity in units of 512 bytes while still
	   having BLOCK_SIZE_BITS set to 10. Just to keep us amused. */
	set_capacity(gd, (new->size * new->blksize) >> 9);

	gd->private_data = new;
	new->blkcore_priv = gd;
	gd->queue = tr->blkcore_priv->rq;

	if (new->readonly)
		set_disk_ro(gd, 1); 

	add_disk(gd);
	
	return 0;
}


我们可以拿这份源代码和 设备管理之块设备 里贴出的示例源代码进行对照,是不是很像?还是原来的配方,还是熟悉的味道...都是先找到设备号,然后添加?没错,MTD 就是在这里注册进了设备模型,并触发热拔插在 /dev/ 里创建了文件。


至于分区表的扫描,他根本就不会进去扫描。这个 gendisk 本身就是 mtd_partition[] 数组中定义的一个分区,他注册成功后,自然就可以直接使用。








发表评论:

Copyright ©2015-2016 freehui All rights reserved