struct device中的platform_data是我们自己在编写驱动程序的时候自己定义的设备结构体的指针。
因为之前不是很了解文件系统,所以要探究一下mtd来加深一下印象。
NAND FLASH每一页大小通常是512+16字节,16字节称为OOB区,通常在OOB区存放坏块标记,前面512字节的ECC校验码等。
FLASH的内部存储时MOSFET(金属半场效应管),里面有悬浮门,是真正存储数据的单元。
块是NandFlash的擦除操作的基本单元。
页是NandFlash的写入操作的基本单元。
在写数据之前,要先擦除,然后再写。
在加载完驱动之后,如果没有加入参数要求跳过坏块扫描的话,会主动扫描坏块,建立必要的BBT,以备后面坏块管理使用。
在一个块内,对每一页进行编程的话必须是顺序的,不能是随机的。
通常读取Nor速度比Nand稍快一些,而Nand写入速度快很多,NorFlash特点是芯片内执行(XIP execute in place),不需要初始化CPU取址模块就能直接从中把指令取出来。
NandFlash器件使用复杂的I/O来串行存取数据,8个引脚用来传送控制地址和数据信息。
MTD:memory technology device内存技术设备
MTD将文件系统与底层FLASH存储器进行了隔离
设备节点->MTD设备层->MTD原始设备层->硬件驱动层
MTD块设备(主设备号31) 字符设备(主设备号90)
spi_write_then_read();函数中,先分配spi_transfer 两个内存空间,一个用于写,一个用于读,然后调用spi_message_add_tail(),将spi_transfer的地址分别放入spi_message中,之后填充spi_transfer中的tx_buf和rx_buf,最后调用spi_sync();做i/o操作。该函数是__spi_async()的封装,里面最后还是调用了spi_master->transfer()之前spi驱动注册的回调函数。
如果是spi flash,对flash的操作就是对spi的读写操作。
在xxx_flash_probe()中主要还是对mtd_info结构体(MTD原始设备)的参数赋值,和回调函数的注册。
flash->mtd.type = MTD_NORFLASH;
flash->mtd.writesize = 1;
flash->mtd.flags = MTD_CAP_NORFLASH;
flash->mtd.size = info->sector_size * info->n_sectors;
flash->mtd.erase = m25p80_erase;
flash->mtd.read = m25p80_read;
/* sst flash chips use AAI word program */
if (JEDEC_MFR(info->jedec_id) == CFI_MFR_SST)
flash->mtd.write = sst_write;
else
flash->mtd.write = m25p80_write;
flash->mtd.dev.parent = &spi->dev;
flash->sector_size = info->sector_size;
flash->page_size = info->page_size;
在每次发送命令之前都需要进行写使能,然后发送命令的内容,最后再加上地址。
struct spi_transfer t[2];
struct spi_message m;
t.tx_buf = flash->command;
t.len = m25p_cmdsz(flash) + FAST_READ_DUMMY_BYTE;
spi_message_add_tail(&t, &m);
t.rx_buf = buf;t.len = len;
spi_message_add_tail(&t, &m);
flash->command = OPCODE_READ;
flash->command = WR_CMD_TYPE;
spi_sync(flash->spi, &m);
最后调用mtd_device_register().如果没有分区的话就调用add_mtd_partition(),否则就调用add_mtd_partitions().
这里有一个概念的区别:struct mtd_part和struct mtd_partition.mtd_partition就是每个子分区的分区信息,mtd_part是指一个分区的实例。真正主分区的信息还是由mtd_info获得.
分配一个分区是函数allocate_partition().
struct mtd_partition {
char *name; /* identifier string */
uint64_t size; /* partition size */
uint64_t offset; /* offset within the master MTD space */
uint32_t mask_flags; /* master MTD flags to mask out for this partition */
struct nand_ecclayout *ecclayout; /* out of band layout for this partition (NAND only) */
};
struct mtd_part {
struct mtd_info mtd;
struct mtd_info *master;
uint64_t offset;
struct list_head list;
};
/* set up the MTD object for this partition */
slave->mtd.type = master->type;
slave->mtd.flags = master->flags & ~part->mask_flags;
slave->mtd.size = part->size;
slave->mtd.writesize = master->writesize;
slave->mtd.writebufsize = master->writebufsize;
slave->mtd.oobsize = master->oobsize;
slave->mtd.oobavail = master->oobavail;
slave->mtd.subpage_sft = master->subpage_sft;
slave->mtd.name = name;
slave->mtd.owner = master->owner;
slave->mtd.backing_dev_info = master->backing_dev_info;
slave->mtd.dev.parent = master->dev.parent;
slave->mtd.read = part_read;
slave->mtd.write = part_write;
分配成功后将返回的std_part添加到mtd_partitions中去。
最后调用add_mtd_device(),注册一个mtd设备。在add_mtd_device中会遍历的调用通知链mtd_notifiers中的所有注册函数。
在mtdchar.c中init_mtdchar();中相比一般字符设备注册地不同在于文件系统的注册register_filesystem,内核挂载kern_mount(),最终调用了vfs_kern_mount();
在该函数中,分配vfsmnt结构体,并且挂载文件系统mount_fs();它最终调用了file_system_type原本就已经注册号的mount回调函数。
static struct file_system_type mtd_inodefs_type = {
.name = "mtd_inodefs",
.mount = mtd_inodefs_mount,
.kill_sb = kill_anon_super,
};
最终mtd_inodefs_mount();还是调用了mount_pseudo();//这是一个通用的挂载文件系统的函数(但是sockfs,pipefs,bdev是不能挂载的).
该函数主要是进行参数的赋值,关键的是三个数据结构struct super_block, struct inode, struct dentry;
s->s_flags = MS_NOUSER;
s->s_maxbytes = MAX_LFS_FILESIZE;
s->s_blocksize = PAGE_SIZE;
s->s_blocksize_bits = PAGE_SHIFT;
s->s_magic = magic;
s->s_op = ops ? ops : &simple_super_operations;//以便之后new_inode()调用部分回调函数
s->s_time_gran = 1;
root = new_inode(s);
if (!root)
goto Enomem;
/*
* since this is the first inode, make it number 1. New inodes created
* after this must take care not to collide with it (by passing
* max_reserved of 1 to iunique).
*/
root->i_ino = 1;
root->i_mode = S_IFDIR | S_IRUSR | S_IWUSR;
root->i_atime = root->i_mtime = root->i_ctime = CURRENT_TIME;
dentry = d_alloc(NULL, &d_name);
if (!dentry) {
iput(root);
goto Enomem;
}
dentry->d_sb = s;
dentry->d_parent = dentry;
d_instantiate(dentry, root);//关键是该函数,将inode信息填充到目录入口中。
s->s_root = dentry;
s->s_d_op = dops;
s->s_flags |= MS_ACTIVE;
return dget(s->s_root);
值得注意的是,几个file_operation还是要好好研究一下的
static const struct file_operations mtd_fops = {
.owner = THIS_MODULE,
.llseek = mtd_lseek,
.read = mtd_read,
.write = mtd_write,
.unlocked_ioctl = mtd_unlocked_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = mtd_compat_ioctl,
#endif
.open = mtd_open,
.release = mtd_close,
.mmap = mtd_mmap,
#ifndef CONFIG_MMU
.get_unmapped_area = mtd_get_unmapped_area,
#endif
};
在/mtd/mtdblock.c中初始化函数中调用了register_mtd_blktrans();最终还是调用了register_blkev();注册块设备。
static struct mtd_blktrans_ops mtdblock_tr = {
.name = "mtdblock",
.major = 31,
.part_bits = 0,
.blksize = 512,
.open = mtdblock_open,
.flush = mtdblock_flush,
.release = mtdblock_release,
.readsect = mtdblock_readsect,
.writesect = mtdblock_writesect,
.add_mtd = mtdblock_add_mtd,
.remove_dev = mtdblock_remove_dev,
.owner = THIS_MODULE,
};
在在最后调用回调函数mtd_blktrans_ops->add_mtd();在该回调函数中调用了add_mtd_blktrans_dev();
调用alloc_disk();
block_device_operation:
static const struct block_device_operations mtd_blktrans_ops = {
.owner = THIS_MODULE,
.open = blktrans_open,
.release = blktrans_release,
.ioctl = blktrans_ioctl,
.getgeo = blktrans_getgeo,
};
当给磁盘分区,在分区上创建文件系统,运行文件系统检查程序,或装载一个分区时,块设备驱动程序会调用open函数。
内核认为每个磁盘都是由512字节大小的扇区所组成的线性数组。所有的I/O请求都将定位在硬件扇区的开始位置,并且每个请求的大小都将是扇区大小的整数倍。
set_capacity();//设定扇区大小.
struct request代表了一个块设备的I/O执行请求.请求被表示为一系列段,每个段都对应内存中的一个缓冲区。如果多个请求都是对磁盘中相邻扇区进行操作,则内核将合并它们,但是内核不会合并在单独request结构中的读写操作。
从本质上讲,一个request结构是作为一个bio结构的链表实现的,bio结构是在底层对部分块设备I/O请求的描述。
bio结构的核心是bi_io_vec的数组
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};
当快设置I/O请求被转换到bio结构后,它将被单独的物理内存页所销毁。驱动程序做的所有工作就是根据这个结构体数组,使用每页传输数据。
blk_init_queue();分配请求队列,传入请求参数mtd_blktrans_request,负责块设备的读写请求(本质还是唤醒之后创建的内核线程wake_up_process())。当请求队列生成的时候,request函数就与该队列绑定在一起。
在分配好请求队列后,要创建一个内核线程mtd_blktrans_thread();
在该线程中首先通过blk_fetch_request来获得请求队列中的请求(最终还是调用__elv_next_request()来获取队列中第一个未完成的请求).
接下来最核心的还是调用do_blktrans_request() 来处理mtd块设备的请求(通过switch来执行读或者写请求)。读写操作都是调用之前注册号的回调函数(最终调用的是driver/mtd/devices/xxx_flash.c中之前已经注册好的与硬件相关的读写函数),同时读写成功之后就调用rq_flush_dcache_pages().
内核使用gendisk结构来表示一个独立的磁盘设备。一旦调用了add_disk,磁盘设备将被集火,并随时会调用它提供的方法。因此在驱动程序完全被初始化并且能够响应对磁盘的请求钱,不要调用add_disk.
对磁盘操作最昂贵的代价总是确定读写数据开始的位置,一旦位置确定了之后,实际用于读取或者写入数据的时间几乎无关紧要的。