块设备框架:
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函数,
这个函数的特点就是读完后文件的当前位置不会改变。
- 关于打印
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驱动,还是建议学习韦老师的视频