Linux MTD系统层次分析

学习目的:

  分析Linux内核中MTD系统层次,为后面编写Nand Flash、NOR Flash驱动打下基础


前面我们实现了用内存模拟磁盘的块设备驱动程序,由于操作的是内存,优化合并后的bio请求在队列请求处理函数中被取出后,可直接根据请求数据传输方向、大小使用memcpy完成数据读写。但像Nand Flash、NOR Flash这类存储设备,读写请求需要遵从特定协议,那么内核是如何支持这一类设备,这就引出了我们今天的主角——MTD层,下面我们一点点进行分析。

1、MTD简介

MTD(Memory Technology Device)即内存技术设备,在Linux内核中,为了使新的memory设备(主要就是为Nor Flash和Nand Flash设计的,其余像接口映射、RAM、ROM等都是辅助功能)的驱动更加简单引入MTD层为memory设备在硬件和上层之间提供了一个抽象的接口。

MTD设备通常可分为四层,从上到下依次是:设备节点、MTD设备层、MTD原始设备层、硬件驱动层。其框图如下图所示:

设备节点:在/dev子目录下建立MTD块设备节点(主设备号为31)和MTD字符设备节点(主设备号为90),通过访问此设备节点即可访问MTD字符设备和块设备

MTD设备层:基于MTD原始设备,linux系统可以定义出MTD的块设备(主设备号31)和字符设备(设备号90)

  • mtdchar.c : MTD字符设备接口相关实现
  • mtdblock.c : MTD块设备接口相关实现

MTD原始设备层:用于描述MTD原始设备的数据结构是mtd_info,它定义了大量的关于MTD的数据和操作函数

  • mtdcore.c : MTD原始设备接口相关实现
  • mtdpart.c : MTD分区接口相关实现

硬件驱动层:Flash硬件驱动层负责对Flash硬件的读、写和擦除操作。MTD设备的Nand Flash芯片的驱动则drivers/mtd/nand/子目录下,Nor Flash芯片驱动位于drivers/mtd/chips/子目录下

  • drivers/mtd/chips : CFI/JEDEC接口通用驱动
  • drivers/mtd/nand : NAND通用驱动和部分底层驱动程序
  • drivers/mtd/maps : NOR Flash映射关系相关函数
  • drivers/mtd/devices : NOR Flash底层驱动

2、MTD设备层

MTD设备层分别实现了MTD字符设备和MTD块设备,允许应用程序通过打开/dev目录中的不同设备节点,以字符设备或块设备的访问形式操作MTD设备。下面对两者实现过程进行分析:

2.1 MTD设备层——字符设备(drivers/mtd/mtdchar.c)

入口函数init_mtdchar

static int __init init_mtdchar(void)
{
    register_chrdev(MTD_CHAR_MAJOR, "mtd", &mtd_fops)-------------->①
   ....
    mtd_class = class_create(THIS_MODULE, "mtd");------------------>②
    ...
    register_mtd_user(&notifier);---------------------------------->③
    return 0;
}

① 向内核注册字符设备,file_operation结构体中实现了操作MTD设备的打开、读、写等函数

② 创建了一个mtd_class,class下可以创建设备,uevent机制会自动获取class下设备信息在/dev目录下创建设备节点

③ 注册MTD user,register_mtd_user实现如下,将传入参数notifier结构体挂入到以mtd_notifiers为头部的链表中,并通过mtd_table数组查找已经注册的MTD设备,对于每个mtd设备调用新注册notifier中的add函数。

void register_mtd_user (struct mtd_notifier *new)
{
    ....
    list_add(&new->list, &mtd_notifiers);

    ....
    for (i=0; i< MAX_MTD_DEVICES; i++)
        if (mtd_table[i])
            new->add(mtd_table[i]);
    ...
}

对于字符设备入口函数中的register_mtd_user(&notifier),最终将调用到mtd_notify_add函数,mtd_notify_add函数实现如下:

static void mtd_notify_add(struct mtd_info* mtd)
{
    if (!mtd)
        return;

    class_device_create(mtd_class, NULL, MKDEV(MTD_CHAR_MAJOR, mtd->index*2),
                NULL, "mtd%d", mtd->index);

    class_device_create(mtd_class, NULL,
                MKDEV(MTD_CHAR_MAJOR, mtd->index*2+1),
                NULL, "mtd%dro", mtd->index);
}

以mtd->index为注册字符设备次设备号(即注册MTD设备在mtd_table数组中的位置),在mtd_class下创建两个设备,一个设备节点可读可写,另一个设备节点只能进行读操作

2.2 MTD设备层——块设备(drivers/mtd/mtdblock.c)

入口函数init_mtdblock

static int __init init_mtdblock(void)
{
    return register_mtd_blktrans(&mtdblock_tr);
}

入口函数调用register_mtd_blktrans注册mtd_blktrans_ops类型结构体mtdblock_tr,mtdblock_tr结构体同MTD注册字符设备的file_operation结构体一样,都实现了open、readsect、writesect等函数

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,
};

继续看register_mtd_blktrans如何注册mtd_blktrans_ops

int register_mtd_blktrans(struct mtd_blktrans_ops *tr)
{
    ...
    if (!blktrans_notifier.list.next)----------------------------------->①
        register_mtd_user(&blktrans_notifier);
        
    tr->blkcore_priv = kzalloc(sizeof(*tr->blkcore_priv), GFP_KERNEL);
    if (!tr->blkcore_priv)
        return -ENOMEM;
    ...
    ret = register_blkdev(tr->major, tr->name);------------------------>②
    ...
    tr->blkcore_priv->rq = blk_init_queue(mtd_blktrans_request, &tr->blkcore_priv->queue_lock);----------->③
    ...
    tr->blkcore_priv->rq->queuedata = tr;
    blk_queue_hardsect_size(tr->blkcore_priv->rq, tr->blksize);
    tr->blkshift = ffs(tr->blksize) - 1;

    tr->blkcore_priv->thread = kthread_run(mtd_blktrans_thread, tr,--------------------------------------->④
            "%sd", tr->name);
    ...

    list_add(&tr->list, &blktrans_majors);---------------------------------------------------------------->⑤

    for (i=0; i<MAX_MTD_DEVICES; i++) {------------------------------------------------------------------->⑥
        if (mtd_table[i] && mtd_table[i]->type != MTD_ABSENT)
            tr->add_mtd(tr, mtd_table[i]);
    }
    ...
}

① 注册一个mtd_user,register_mtd_user将blktrans_notifier添加到最终调用到mtd_notifiers为头部链表,并调用到blktrans_notifier中的add成员函数。此处的add函数最终会调用到mtdblock_tr结构体的add_mtd指针指向函数

② 注册一个块设备

③ 分配并初始化一个块设备队列,设置队列请求处理函数为mtd_blktrans_request

④ 创建一个内核线程

⑤ 将mtdblock_tr结构体添加到头部为blktrans_majors链表中

⑥ 在mtd_info数组中找到已经注册的MTD设备,调用mtdblock_tr结构体的add_mtd函数

再接着看③中队列的请求处理函数mtd_blktrans_request

static void mtd_blktrans_request(struct request_queue *rq)
{
    struct mtd_blktrans_ops *tr = rq->queuedata;
    wake_up_process(tr->blkcore_priv->thread);
}

我们知道上层构造的bio读写请求被优化合并后,块设备的队列请求处理函数被自动调用。在队列请求处理函数中一般是取出队列中请求,根据请求完成数据的读写操作。然而mtd_blktrans_request函数中只调用了wake_up_process函数唤醒内核中的一个线程,我们可猜测唤醒的线程中肯定实现了从队列中取出合并后的请求,根据请求完成数据读写等操作。

继续看创建的内核线程函数mtd_blktrans_thread

static int mtd_blktrans_thread(void *arg)
{
    ...
    while (!kthread_should_stop()) {
        struct request *req;
        struct mtd_blktrans_dev *dev;
        int res = 0;

        req = elv_next_request(rq);

        if (!req) {
            set_current_state(TASK_INTERRUPTIBLE);
            ...
            continue;
        }

        ...
        res = do_blktrans_request(tr, dev, req);
        ...

        end_request(req, res);
    }
    ...
}

mtd_blktrans_thread线程不断以电梯调度算法取出请求,如果没有读写请求就让该线程进入休眠,如果有读写请求就调用do_blktrans_request函数。

继续看do_blktrans_request函数

static int do_blktrans_request(struct mtd_blktrans_ops *tr,
                   struct mtd_blktrans_dev *dev,
                   struct request *req)
{
    ....

    switch(rq_data_dir(req)) {
    case READ:
        for (; nsect > 0; nsect--, block++, buf += tr->blksize)
            if (tr->readsect(dev, block, buf))
                return 0;
        return 1;

    case WRITE:
        if (!tr->writesect)
            return 0;

        for (; nsect > 0; nsect--, block++, buf += tr->blksize)
            if (tr->writesect(dev, block, buf))
                return 0;
        return 1;

    ...
}

do_blktrans_request函数根据传入请求,获取数据的传输方向和大小,最终调用mtdblock_tr结构体中的读扇区readsect和写扇区函数writesect

由此可见,我们上述猜想正常,队列请求处理函数中唤醒的线程,在唤醒线程中获取队列中被优化合并的请求,根据请求完成了数据的读写操作。

那么问题又来了,对于块设备驱动,我们虽然创建了队列,这个队列最后肯定要赋给gendisk结构体成员,还要设置和注册gendisk结构体,那么这些是在哪里实现的呢?

经过分析这些是在mtdblock_tr结构体成员add_mtd指针指向函数mtdblock_add_mtd实现的

static void mtdblock_add_mtd(struct mtd_blktrans_ops *tr, struct mtd_info *mtd)
{
    struct mtd_blktrans_dev *dev = kzalloc(sizeof(*dev), GFP_KERNEL);---------------------------->①

    if (!dev)
        return;

    dev->mtd = mtd;------------------------------------------------------------------------------>②
    dev->devnum = mtd->index;

    dev->size = mtd->size >> 9;
    dev->tr = tr;
    dev->readonly = 1;

    add_mtd_blktrans_dev(dev);------------------------------------------------------------------->③
}

① 分配一个mtd_blktrans_dev结构体

② 设置mtd_blktrans_dev结构体mtd成员指向mtd_table[]

③ 调用add_mtd_blktrans_dev函数,gendisk结构体的分配和注册就是在这个函数中完成的

再来看mtdblock_tr结构体读扇区函数

static int mtdblock_readsect(struct mtd_blktrans_dev *dev,
                  unsigned long block, char *buf)
{
    size_t retlen;

    if (dev->mtd->read(dev->mtd, (block * 512), 512, &retlen, buf))
        return 1;
    return 0;
}

mtdblock_readsect函数直接调用mtd_blktrans_dev结构体中mtd成员中的read函数,从上面分析mtd_blktrans_dev结构体是在mtdblock_add_mtd函数中构造的,它的mtd成员指向mtd_table[]中未被注册的设备。

从上面对MTD设备层分析可以看出,不管是MTD创建的字符设备还是块设备,它的设备节点信息都是在发现mtd_table数组中有注册设备时,调用自己的mtd_notifier结构体的add函数创建的。我们可以看出mtd_table数组是MTD原始设备层和其下面一层MTD原始设备层交流的桥梁。

3、MTD原始设备层

由上面分析,我们知道mtd_table数组是MTD设备层和原始设备层沟通的桥梁,那就以mtd_table为线索进行分析。先来看mtd_table数组在原始设备层何处被构造的。

经搜索找到了add_mtd_device函数,函数内容如下:

int add_mtd_device(struct mtd_info *mtd)
{
    ...
    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;
            ...
            list_for_each(this, &mtd_notifiers) {------------------->②
                struct mtd_notifier *not = list_entry(this, struct mtd_notifier, list);
                not->add(mtd);
            }

            ...
        }

    ...
}

① 找到mtd_table数组中未被注册的位置

② 将传入的mtd参数内容,存放到找到的mtd_table数组中未被注册的位置

② 遍历mtd_notifiers链表,调用mtd_notfiers链表中挂接的所有对象的add函数(即MTD设备层,实现的mtd_notifier结构体成员的add函数)

add_mtd_device函数根据传入参数填充了mtd_table数组,并调用mtd_notifiers链表中挂接的所有对象的add函数,使用add函数在MTD设备层创建相应设备节点信息。该函数肯定是在MTD原始设备层下面一层Flash驱动程序中使用的,我们继续搜索,查看add_mtd_device在那些地方被调用,发现内核中有多处调用这个函数地方。

我们找到了一个nand flash的驱动和nor flash的驱动的例子

1)drivers/mtd/nand/at91_nand.c的probe函数

static int __init at91_nand_probe(struct platform_device *pdev)
{
    ...
    struct mtd_info *mtd;
    struct nand_chip *nand_chip;
    ...
    mtd = &host->mtd;----------------------------------------------------------->①
    nand_chip = &host->nand_chip;----------------------------------------------->②
    host->board = pdev->dev.platform_data;
    nand_chip->priv = host;        /* link the private data structures */
    mtd->priv = nand_chip;------------------------------------------------------>①
    mtd->owner = THIS_MODULE;
    ...
    nand_chip->IO_ADDR_R = host->io_base;--------------------------------------->①
    nand_chip->IO_ADDR_W = host->io_base;
    nand_chip->cmd_ctrl = at91_nand_cmd_ctrl;
    nand_chip->dev_ready = at91_nand_device_ready;
    nand_chip->ecc.mode = NAND_ECC_SOFT;    /* enable ECC */
    nand_chip->chip_delay = 20;        /* 20us command delay time */
    ...
    if (nand_scan(mtd, 1)) {---------------------------------------------------->③
        res = -ENXIO;
        goto out;
    }
    ...
    res = add_mtd_partitions(mtd, partitions, num_partitions);------------------>④
    ...
    res = add_mtd_device(mtd);
    ...
}

① 分配nand_chip内存,根据目标板及NAND控制器初始化nand_chip中成员函数(若未初始化则使用nand_base.c中的默认函数)

② 分配mtd_info结构体,将mtd_info中的priv指向nand_chip(或板相关私有结构),设置ecc模式及处理函数

③ 以mtd_info为参数调用nand_scan()探测NAND FLash。 nand_scan()会读取nand芯片ID,并根据mtd->priv即nand_chip中成员初始化mtd_info

④ 若有分区,则以mtd_info和mtd_partition为参数调用add_mtd_partitions()添加分区信息

2)drivers/mtd/maps/omap_nor.c的probe函数

static int __devinit omapflash_probe(struct platform_device *pdev)
{
    ...
    struct omapflash_info *info;
    ...

    info = kzalloc(sizeof(struct omapflash_info), GFP_KERNEL);---------------------->①
    ...
    info->map.virt        = ioremap(res->start, size);
    ...
    info->map.name        = pdev->dev.bus_id;
    info->map.phys        = res->start;
    info->map.size        = size;
    info->map.bankwidth    = pdata->width;
    info->map.set_vpp    = omap_set_vpp;

    simple_map_init(&info->map);--------------------------------------------------->②
    info->mtd = do_map_probe(pdata->map_name, &info->map);------------------------->③
    ...
    info->mtd->owner = THIS_MODULE;

#ifdef CONFIG_MTD_PARTITIONS------------------------------------------------------->④
    err = parse_mtd_partitions(info->mtd, part_probes, &info->parts, 0);
    if (err > 0)
        add_mtd_partitions(info->mtd, info->parts, err);
    else if (err < 0 && pdata->parts)
        add_mtd_partitions(info->mtd, pdata->parts, pdata->nr_parts);
    else
#endif
        add_mtd_device(info->mtd);
    ...
}

① 定义map_info结构体, 初始化成员name, size, phys, bankwidth,通过ioremap映射成员virt(虚拟内存地址)

② 通过函数simple_map_init初始化map_info成员函数read,write,copy_from,copy_to

③ 调用do_map_probe进行cfi接口探测, 返回mtd_info结构体

④ 通过parse_mtd_partitions, add_mtd_partitions注册mtd原始设备

至此,我们通过层层分析找出了MTD系统中Nand Flash和Nor Flash驱动框架,后面将参考内核中提供驱动程序,编写自己Nand Flash和Nor Flash驱动

原文地址:https://www.cnblogs.com/053179hu/p/13966199.html