linux3.2.0块设备及nandflash驱动框架

块设备框架:

app:      open,read,write "1.txt"
---------------------------------------------  文件的读写
文件系统: vfat, ext2, ext3, yaffs2, jffs2      (把文件的读写转换为扇区的读写)
-----------------ll_rw_block-----------------  扇区的读写
                       1. 把"读写"放入队列
                       2. 调用队列的处理函数(优化/调顺序/合并)
            块设备驱动程序     
---------------------------------------------
硬件:        硬盘,flash
分析 ll_rw_block
        for (i = 0; i < nr; i++) {
            struct buffer_head *bh = bhs[i];
            submit_bh(rw, bh);
                struct bio *bio; // 使用bh来构造bio (block input/output)
                submit_bio(rw, bio);
                    // 通用的构造请求: 使用bio来构造请求(request)
                    generic_make_request(bio);
                            request_queue_t *q = bdev_get_queue(bio->bi_bdev); // 找到队列  
                            
                            // 调用队列的"构造请求函数"
                            ret = q->make_request_fn(q, bio);

那么对于上面的 make_request_fn 是什么呢?
这个函数指针有两种方式产生,一个是自己写驱动初始化块设备队列时候设置
另一种就是使用默认的函数,那么默认的又是什么呢?

我们在写驱动时候会调用到 blk_init_queue 函数,就是在这里边设置的

blk_init_queue
	blk_init_queue_node(rfn, lock, -1);
		blk_init_allocated_queue(uninit_q, rfn, lock);
			blk_queue_make_request(q, blk_queue_bio);
				q->make_request_fn = mfn;  //所以默认的 make_request_fn 就是上面的 blk_queue_bio

下面来看下默认的函数做了什么?

blk_queue_bio
	// 先尝试合并到读写队列中
	elv_merge(q, &req, bio);  //电梯调度算法
	
	// 如果合并不成,使用bio构造请求
	init_request_from_bio(req, bio);
	
	blk_flush_plug_list(plug, false);
		queue_unplugged(q, depth, from_schedule);
			__blk_run_queue(q);
				q->request_fn(q); // 调用队列的"处理函数"

所以写块设备驱动程序最重要就是提供最后的队列处理函数,这个函数会根据不同的硬件而不同

试验部分去看韦老师的视频比较好,下面是根据他的代码在linux3.2.0上实验了一下
有几个接口变化了,这里只列一下源码。

#include <linux/module.h>
#include <linux/errno.h>
#include <linux/interrupt.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/blkdev.h>
#include <linux/blkpg.h>
#include <linux/io.h>

static struct gendisk *ramblock_disk;
static struct request_queue *ramblock_queue;

static int major;

static DEFINE_SPINLOCK(ramblock_lock);

#define RAMBLOCK_SIZE (1024*1024)
static unsigned char *ramblock_buf;

static int ramblock_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
    /* 容量=heads*cylinders*sectors*512 */
    geo->heads     = 2;
    geo->cylinders = 32;
    geo->sectors   = RAMBLOCK_SIZE/2/32/512;
    return 0;
}


static struct block_device_operations ramblock_fops =
{
    .owner	= THIS_MODULE,
    .getgeo	= ramblock_getgeo,
};

static void do_ramblock_request(struct request_queue * q)
{

    static int r_cnt = 0;
    static int w_cnt = 0;
    struct request *req;

    //printk("do_ramblock_request %d\n", ++cnt);

    req = blk_fetch_request(q);
    while (req != NULL)
    {
        /* 数据传输三要素: 源,目的,长度 */
        /* 源/目的: */
        unsigned long offset = blk_rq_pos(req) * 512;

        /* 目的/源: */
        // req->buffer

        /* 长度: */
        unsigned long len = blk_rq_cur_sectors(req) * 512;

        if (rq_data_dir(req) == READ)
        {
            printk("do_ramblock_request read %d\n", ++r_cnt);
            memcpy(req->buffer, ramblock_buf+offset, len);
        }
        else
        {
            printk("do_ramblock_request write %d\n", ++w_cnt);
            memcpy(ramblock_buf+offset, req->buffer, len);
        }

        if (!__blk_end_request_cur(req, 0))
            req = blk_fetch_request(q);
    }

}

static int ramblock_init(void)
{
    /* 1. 分配一个gendisk结构体 */
    ramblock_disk = alloc_disk(16); /* 次设备号个数: 分区个数+1 */

    /* 2. 设置 */
    /* 2.1 分配/设置队列: 提供读写能力 */
    ramblock_queue = blk_init_queue(do_ramblock_request, &ramblock_lock);
    ramblock_disk->queue = ramblock_queue;

    /* 2.2 设置其他属性: 比如容量 */
    major = register_blkdev(0, "ramblock");  /* cat /proc/devices */
    ramblock_disk->major       = major;
    ramblock_disk->first_minor = 0;
    sprintf(ramblock_disk->disk_name, "ramblock");
    ramblock_disk->fops        = &ramblock_fops;
    set_capacity(ramblock_disk, RAMBLOCK_SIZE / 512);

    /* 3. 硬件相关操作 */
    ramblock_buf = kzalloc(RAMBLOCK_SIZE, GFP_KERNEL);

    /* 4. 注册 */
    add_disk(ramblock_disk);

    return 0;
}

static void ramblock_exit(void)
{
    unregister_blkdev(major, "ramblock");
    del_gendisk(ramblock_disk);
    put_disk(ramblock_disk);
    blk_cleanup_queue(ramblock_queue);

    kfree(ramblock_buf);
}

module_init(ramblock_init);
module_exit(ramblock_exit);

MODULE_LICENSE("GPL");

nandflash的层次框架

app: open,read,write “1.txt”
--------------------------------------------- 文件的读写
文件系统: vfat, ext2, ext3, yaffs2, jffs2 (把文件的读写转换为扇区的读写)
-----------------ll_rw_block----------------- 扇区的读写
1. 把"读写"放入队列
2. 调用队列的处理函数(优化/调顺序/合并)
—————–
块设备驱动程序:把nandflash注册成块设备,提供上面分析的操作队列等
—————–
nandflash协议层:知道发什么命令来读写、操作、识别nandflash
—————–
硬件相关/体系相关:针对不同的控制芯片提供具体的读写功能
—————–
需要我们实现的就是硬件相关或者说体系相关的了,为什么可以这么分层呢,如果对比几种不同
的nand芯片会发现,其实它的操作命令或者说协议都是一样的,比如读flash ID都是先发命令90,
再发地址00既然这些协议都一样,自然可以抽出来当做协议层,那么最后的差异就是体系相关的了。
比如s3c2440通过nandflash控制器控制三个寄存器来实现命令、地址发送及数据读写,
而更低端点的可能没有nandflash控制器,那么它就只能通过直接控制nandflash的外接引脚来
提供上述功能。这些差异都体现在主控制器上。


我分析驱动习惯性会从用户层入手,一路分析到底,感觉这样会对子系统有个更感性的认知。
下面就从用户空间相关命令入手,首先看看怎么在用户空间操作flash设备。

1.在终端用 mtdinfo 可获知分区信息
mtdinfo --help 主要有如下用法:
-m, --mtdn= MTD device number to get information about
-u, --ubi-info print what would UBI layout be if it was put
on this MTD device
-a, --all print information about all MTD devices

2.以下是IAC335X的分区信息:
//内核分区信息
NAND device: Manufacturer ID: 0x2c, Chip ID: 0xda (Micron MT29F2G08ABAEAWP)
Creating 8 MTD partitions on “omap2-nand.0”:
0x000000000000-0x000000020000 : “SPL”
0x000000020000-0x000000040000 : “SPL.backup1”
0x000000040000-0x000000060000 : “SPL.backup2”
0x000000060000-0x000000080000 : “SPL.backup3”
0x000000080000-0x000000260000 : “U-Boot”
0x000000260000-0x000000280000 : “U-Boot Env”
0x000000280000-0x000000780000 : “Kernel”
0x000000780000-0x000010000000 : “File System”

3.用mtdinfo命令来验证下:
$ mtdinfo
Count of MTD devices: 8
Present MTD devices: mtd0, mtd1, mtd2, mtd3, mtd4, mtd5, mtd6, mtd7
Sysfs interface supported: yes

//指明分区号可看到确实相同
$mtdinfo -m 4
mtd4
Name: U-Boot //分区名称
Type: nand //分区类型
Eraseblock size: 131072 bytes, 128.0 KiB //可擦出块大小
Amount of eraseblocks: 15 (1966080 bytes, 1.9 MiB) //块数目
Minimum input/output unit size: 2048 bytes //最小可操作单元大小
Sub-page size: 512 bytes //页大小
OOB size: 64 bytes //OOB大小
Character device major/minor: 90:8
Bad blocks are allowed: true
Device is writable: true

这些信息的设置基本都在板级文件的am335x_nand_partitions中设置。

4.从开源包mtd-utils可以获取很多关于mtd的操作源码,其中nandflash的相关操作文件:
nanddump.c、nandwrite.c、flash_erase.c等等
以nanddump.c为例,看下它的用法:
Usage: nanddump [OPTIONS] MTD-device
Dumps the contents of a nand mtd partition.

–help display this help and exit
–version output version information and exit
-f file --file=file dump to file
-i --ignoreerrors ignore errors
-l length --length=length length
-o --omitoob omit oob data
-b --omitbad omit bad blocks from the dump
-p --prettyprint print nice (hexdump)
-s addr --startaddress=addr start address

我测了一下,不管是想要把nand的数据打印到终端还是文件里,必须加-p选项,比如
nanddump /dev/mtd0 -p //把整个/dev/mtd0的数据打印到终端

而如果我们想从某个地址打印指定长度的数据,是有限制的,比如
nanddump /dev/mtd0 -p -f mtd0dada.txt -s 0x10 -l 0x10 //我想从mtd0的0x10地址
开始读取0x10个数据到mtd0dada.txt文件中,但是会得到下面打印:
the start address (-s parameter) is not page-aligned!
The pagesize of this NAND Flash is 0x800.

意思就是开始地址是没有页对齐的,而本nand的页大小是0x800,
也就说开始地址必须是0x800=2048的倍数才行,于是我们改成:
nanddump /dev/mtd0 -p -f mtd0dada.txt -s 0x800 -l 0x10 //我想从mtd0的0x10地址
开始读取0x10个数据到mtd0dada.txt文件中,结果呢?
看下mtd0dada.txt,发现里边并不是16个数据,而是2048个字节的数据
也就说明了如果你参数小于1页的长度,那么至少读给你1页。

列出从0x1000开始1页长度的数据看下:

#nanddump /dev/mtd0 -p 0x1000 -l 0x800
ECC failed: 0
ECC corrected: 0
Number of bad blocks: 0
Number of bbt blocks: 0
Block size 131072, page size 2048, OOB size 64
Dumping data starting at 0x00000800 and ending at 0x00000810...

0x00001000: 00 30 d0 e5 01 00 13 e3 0c 00 00 1a 01 10 d0 e5
0x00001010: 02 20 d0 e5 02 20 81 e1 02 30 83 e1 03 20 d0 e5
...
0x000017f0: 24 00 80 e2 05 10 a0 e1 0f 30 8d e2 00 40 8d e5
  OOB Data: ff ff a9 fb 44 c6 78 98 2a 49 7a 91 e5 4f c9 00
  OOB Data: bc 45 ad 35 8f 97 6a 91 0e c9 6b 4b 23 00 fd fa
  OOB Data: 1d e7 96 b3 bc 92 f0 97 fd 50 83 00 00 bb e6 ba
  OOB Data: e0 73 f8 47 12 58 8e 09 e5 00 ff ff ff ff ff ff

0x17f0 - 0x1000 + 16 = 2048 正好是一页数据
后边64字节的OOB数据也打印出来了


上面只是演示了一下nanddump命令的用法,没多大意思,主要是可以通过命令
再结合源码nanddump.c就能明白用户空间是如何操控块设备的,为以后深入分析
内核的块设备提供基础。

稍微看下nanddump的源码:

main(int argc, char **argv)
	fd = open(mtddev, O_RDONLY)  //这里的mtddev就是上面参数中的 /dev/mtd0
	ioctl(fd, MEMGETINFO, &meminfo)  //获取分区信息
	end_addr = start_addr + length;  //start_addr和length都是传进来的参数
	bs = meminfo.oobblock;  //bs为一页的长度
	for(ofs = start_addr; ofs < end_addr ; ofs+=bs)
		if ((badblock = ioctl(fd, MEMGETBADBLOCK, &blockstart)) < 0)
			perror("ioctl(MEMGETBADBLOCK)");
			goto closeall;
		pread(fd, readbuf, bs, ofs) 

1.上面的open函数中mtddev即我们参数传入的/dev/mtd0是不是块设备节点呢?
$ ls /dev/mtd0 -l
crw-rw---- 1 root root 90, 0 Aug 7 2015 /dev/mtd0

从打印可以看出,/dev/mtd0其实是个字符设备,其实驱动程序为块设备的每个
mtd分区各自建立了一个块设备和一个字符设备。后边驱动分析时候会看到。

2.ioctl(fd, MEMGETINFO, &meminfo) != 0)这句实际上就是调用字符设备的ioctl节点了
这里的info是个mtd_info_t类型的结构体,定义在内核的 include/mtd/mtd-abi.h中
是应用层操作mtd重要的数据结构

3.fprintf(stderr, “Block size %u, page size %u, OOB size %u\n”,
meminfo.erasesize, meminfo.oobblock, meminfo.oobsize);
这里就是上面打印分区信息的那句话的源码了,我们可以看出,分区的总大小
其实是meminfo的可擦出大小,而页大小是oobblock,所以不要被meminfo的名字迷惑了。

4.从上面for循环可以看出,每次读取的偏移会增加一个页的长度,这也就是为什么即使
给定的长度不够一页也会读出一页的原因。而进入for循环的第一件事是判断块是否为坏块。
如果为坏块,那后边的读操作也不用做了。检测坏块的ioctl命令是MEMGETBADBLOCK。

5.如果不是坏块,那么就开始读了,这里调用的是pread函数,并不是普通的read函数,
这个函数的特点就是读完后文件的当前位置不会改变。

  1. 关于打印
if (pretty_print) {
			for (i = 0; i < bs; i += 16) {
				sprintf(pretty_buf,
					"0x%08x: %02x %02x %02x %02x %02x %02x %02x "
					"%02x %02x %02x %02x %02x %02x %02x %02x %02x\n",
					(unsigned int) (ofs + i),  readbuf[i],
					readbuf[i+1], readbuf[i+2],
					readbuf[i+3], readbuf[i+4],
					readbuf[i+5], readbuf[i+6],
					readbuf[i+7], readbuf[i+8],
					readbuf[i+9], readbuf[i+10],
					readbuf[i+11], readbuf[i+12],
					readbuf[i+13], readbuf[i+14],
					readbuf[i+15]);
				write(ofd, pretty_buf, 60);
			}
		}

打印部分的单独列一下,这个就很简单了,直接把读出内容每行16个显示
唯一有点技巧的就是并没有直接调用printf函数打印,而是先用sprintf函数把
格式化后的数据放到pretty_buf缓冲区中,然后调用write把这些写到ofd中。
ofd是由参数传递的,如果有 -f 参数,那么它就是指定的文件,如果没有
-f参数,那么就是标准输出。还有个pretty_print参数,只有这个为1时候,
才会打印,这也就解释了为什么只有加上-p参数才会打印。

后边还有读取oob数据的函数,就不再分析了,只要注意ioctl的命令是MEMREADOOB即可。


上面就是对nandflash用户空间的相关操作,从nanddump的源码可以看出来,
操作的是mtd的字符设备节点,而我们平时直接操作文件并不是用这些字符
设备接口的,从上面的层次图可以看出这些操作是由文件系统来做的,
文件系统会把相应的文件读写、拷贝等操作转换为具体的扇区读写,
不同的文件系统转换方式都是不一样的,想快速了解这块可以去参考
《linux完全注释》中关于minix文件系统的讲解。那么既然读写文件都
不用我们操心,内核提供的字符设备节点还有什么用处呢,除了上述的
相关命令外,其实可以设想这么一种情景,加入你要在用户空间升级内核
怎么办,这时候就要考虑用mtd的字符设备节点来完成了。
不管是mtd的字符设备还是块设备操作,都要用到相关体系的读写等操作。
下面就以omap平台来看下整个结构。

omap平台nandflash的驱动是…/driver/mtd/nand/omap2.c中,这个目录下
还有三星平台的nand驱动s3c2410.c,可以看出不同的体系直接实现都在这。

omap_nand_probe
	...  //先忽略体系相关代码
	nand_scan_ident(&info->mtd, 1, NULL)
	nand_scan_tail(&info->mtd)
	mtd_device_parse_register(&info->mtd, NULL, 0, pdata->parts, pdata->nr_parts);

上面三个点忽略的自然就是具体硬件操作相关的代码设置了,之所以忽略它是想
直接切入主题,先看看它是如何注册成字符设备和块设备的。假如我们已经设置好了
nand的一系列操作函数,就是上面省略的和nand_scan_ident、nand_scan_tail
做的事情,接下来的mtd_device_parse_register就是需要关注的重点了。

mtd_device_parse_register //mtdcore.c 这个属于mtd核心了
	parse_mtd_partitions(mtd, types, &real_parts, parser_data);
	add_mtd_partitions(mtd, real_parts, err);
		add_mtd_device(mtd);
			mtd->dev.type = &mtd_devtype;
			mtd->dev.class = &mtd_class;  //在/sys/class下产生mtd操作目录
			mtd->dev.devt = MTD_DEVT(i);
			dev_set_name(&mtd->dev, "mtd%d", i);
			device_register(&mtd->dev)  //产生/dev/mtd*
			device_create(...,"mtd%dro", i); //产生/dev/mtd*ro
			list_for_each_entry(not, &mtd_notifiers, list)
				not->add(mtd);  

到这里仅仅看到了相关设备节点的产生,但是字符设备最重要的fops还没看到
那么最后的链表 mtd_notifiers 就是关键了,看它出现在哪里

register_mtd_user (struct mtd_notifier *new)   //mtdcore.c
	list_add(&new->list, &mtd_notifiers);

那么谁会调用这个函数把自己加入这个链表呢?搜索这个函数可知,
drivers/mtd/mtdchar.c 和 drivers/mtd/mtd_blkdevs.c会调用这个函数
很容易想到这两个文件就是mtd的字符设备和块设备的注册函数了

先来看下mtdchar.c

init_mtdchar(void)
	__register_chrdev(MTD_CHAR_MAJOR, 0, 1 << MINORBITS, "mtd", &mtd_fops);
	register_mtd_user(&mtdchar_notifier);  //把mtdchar_notifier加入mtd核心的通知链表中

mtdcore.c中看到最后调用每个链表的add函数,对于mtdchar.c就是mtdchar_notify_add了

static struct mtd_notifier mtdchar_notifier = {
	.add = mtdchar_notify_add,
	.remove = mtdchar_notify_remove,
};

static void mtdchar_notify_add(struct mtd_info *mtd)
{
}

可以看到mtdchar_notify_add是空函数,这个可能和2.6一些早期版本不太一样,也就说对于
mtd的字符设备,mtd核心把事情做得差不多了,mtdchar只需要提供相关的fops即可

static const struct file_operations mtd_fops = {
	.owner		= THIS_MODULE,
	.llseek		= mtd_lseek,
	.read		= mtd_read,
	.write		= mtd_write,
	.unlocked_ioctl	= mtd_unlocked_ioctl,
	.open		= mtd_open,
	.release	= mtd_close,
	.mmap		= mtd_mmap,
};

这就是fops相关函数,这些函数自然会一路调用到体系相关的函数实现读写。

再来看mtd_blkdevs.c

register_mtd_blktrans(&mtdblock_tr);  //mtd_blkdevs.c
	register_mtd_user(&blktrans_notifier);
		
static struct mtd_notifier blktrans_notifier = {
	.add = blktrans_notify_add,
	.remove = blktrans_notify_remove,
};

blktrans_notify_add
	list_for_each_entry(tr, &blktrans_majors, list)
		tr->add_mtd(tr, mtd);		

怒了没,反正我怒了,一个链表接一个链表,不过也没办法,继续
看看 blktrans_majors 又是谁把自己添加进去。

register_mtd_blktrans
	list_add(&tr->list, &blktrans_majors);
	
register_mtd_blktrans被mtdblock.c和mtdblock_ro.c调用
init_mtdblock(void)	//mtdblock.c
	register_mtd_blktrans(&mtdblock_tr);

static struct mtd_blktrans_ops mtdblock_tr = {
	...
	.add_mtd	= mtdblock_add_mtd,
	...
};	

所以最终调用到的就是 mtdblock_add_mtd

mtdblock_add_mtd
	add_mtd_blktrans_dev(&dev->mbd)
		alloc_disk();
		set_capacity(gd, (new->size * tr->blksize) >> 9);
		blk_init_queue(mtd_blktrans_request, &new->queue_lock);
		add_disk(gd);

mtdblock_ro.c就不再分析了,和这个差不多
到这里就和最开始的块设备总体分析接上了,对于nand来说,它的
操作队列自然就是这个mtd_blktrans_request了。

本文目的只是理框架,至于nand的具体flash驱动,还是建议学习韦老师的视频

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浓咖啡jy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值