嵌入式Linux——nand flash 驱动(三):源代码分析

2019-07-12 15:15发布

再次声明:本文是看过一些文章后写的,如果与你的文章有相同的地方,敬请告知,如果对你有帮助,是我的荣幸。 接下来的这篇文章我们将要分析一下nand flash在S3C2440中的驱动函数。下面我们以一张图来引入:         从上图可以看出,MTD设备层与原始设备层打交道。通过分析源代码我们可以知道当上层要求对FLASH进行读写时,它会向设备层发出请求,设备层的读写函数会调用原始设备层中的读写函数,即mtd_info结构体(mtd原始设备层中描述设备的专用结构体)中的读写函数,而mtd_info中的函数会调用nand_chip(nand硬件驱动层中描述设备的结构体,其中包含了针对特定设备的基本参数和设备操作函数)中的读写函数。所以当我们写一个flash硬件驱动程序时,有以下步骤:
1. 如果FLASH要分区,则定义mtd_partition数组,将FLASH分区信息记录其中。
2. 在模块加载时为每一个chip(主分区)分配mtd_info和nand_chip的内存,根据目标板nand 控制器的特殊情况初始化nand_chip中的实现对FLASH操作的成员函数,如hwcontrol()、dev_ready()、read_byte()、write_byte()等。填充mtd_info,并将其priv成员指向nand_chip。
3. 以mtd_info为参数调用nand_scan()函数探测NAND FLASH的存在。nand_scan()函数会从FLASH芯片中读取其参数,填充相应nand_chip成员。
4. 如果要分区,则以mtd_info和mtd_partition为参数调用add_mtd_partions(),添加分区信息。在这个函数里面会为每一个分区(不包含主分区)分配一个mtd_info结构体,填充,并注册。 而从上边的描述中我们知道,如果自己编写一个nandflash驱动,只需要填充这三个结构体:   mtd_info     nand_chip     mtd_partition 并实现对物理设备的控制,上层的驱动控制已由mtd做好了,不需要关心。 先说mtd_info:   mtd层用一个数组struct mtd_info *mtd_table[MAX_MTD_DEVICES]保存系统中所有的设备,mtd设备利用struct mtd_info 这个结构来描述,该结构中描述了存储设备的基本信息和具体操作所需要的内核函数,mtd系统的那个机制主要就是围绕这个结构来实现的。结构体在include/linux/mtd/mtd.h中定义: struct mtd_info { u_char type; //MTD 设备类型 u_int32_t flags; //MTD设备属性标志 u_int32_t size; //标示了这个mtd设备的大小 u_int32_t erasesize; //MTD设备的擦除单元大小,对于NandFlash来说就是Block的大小 u_int32_t oobblock; //oob区在页内的位置,对于2K字节一页的nand来说是2K u_int32_t oobsize; //oob区的大小,对于2K字节一页的nand来说是64 u_int32_t ecctype; //ecc校验类型 u_int32_t eccsize; //ecc的大小 char *name; //设备的名字 int index; //设备在MTD列表中的位置 struct nand_oobinfo oobinfo; //oob区的信息,包括是否使用ecc,ecc的大小 //以下是关于mtd的一些读写函数,将在nand_base中的nand_scan中重载 int (*erase) int (*read) int (*write) int (*read_ecc) int (*write_ecc) int (*read_oob) int (*read_oob) void *priv;//设备私有数据指针,对于NandFlash来说指nand芯片的结构:nand_chip } 接下来我们看nand_chip结构,在include/linux/mtd/nand.h中定义: struct nand_chip { void __iomem *IO_ADDR_R; //读8位I/O地址 void __iomem *IO_ADDR_W; //写8位I/O地址 uint8_t (*read_byte)(struct mtd_info *mtd);//从芯片读一个字节 u16 (*read_word)(struct mtd_info *mtd);//从芯片读一个字 void (*write_buf)(struct mtd_info *mtd, const uint8_t *buf, int len);//将缓冲区的数据写入芯片 void (*read_buf)(struct mtd_info *mtd, uint8_t *buf, int len);//将芯片中的数据独到缓冲区中 int (*verify_buf)(struct mtd_info *mtd, const uint8_t *buf, int len); //验证芯片和写入缓冲区中的数据 int (*block_bad)(struct mtd_info *mtd, loff_t ofs, int getchip);//检查是否坏块 int (*block_markbad)(struct mtd_info *mtd, loff_t ofs);//标记坏块 void (*select_chip)(struct mtd_info *mtd, int chip); //实现选中芯片 void (*cmd_ctrl)(struct mtd_info *mtd, int dat, unsigned int ctrl);//控制ALE/CLE/nCE,也用于写命令和地址 int (*dev_ready)(struct mtd_info *mtd);//设备就绪 void (*cmdfunc)(struct mtd_info *mtd, unsigned command, int column, int page_addr); //实现命令发送 int (*waitfunc)(struct mtd_info *mtd, struct nand_chip *this); void (*erase_cmd)(struct mtd_info *mtd, int page);//擦除命令的处理 int (*scan_bbt)(struct mtd_info *mtd);//扫描坏块 int (*errstat)(struct mtd_info *mtd, struct nand_chip *this, int state, int status, int page); int (*write_page)(struct mtd_info *mtd, struct nand_chip *chip, const uint8_t *buf, int page, int cached, int raw);//写一页 int chip_delay;//有板决定的延迟时间 unsigned int options;//与具体的NAND芯片相关的一些选项,如NAND_NO_AUTOINCR,NAND_BUSWIDTH_16等,至于这些选项具体表示什么含义,可以参考,那里有较为详细的说明; int page_shift;//用位表示的NAND芯片的page大小,如某片NAND芯片的一个page有512个字节,那么page_shift就是9; int phys_erase_shift;//用位表示的NAND芯片的每次可擦除的大小,如某片NAND芯片每次可擦除128K字节(通常就是一个block的大小) int bbt_erase_shift;//用位表示的bad block table的大小,通常一个bbt占用一个block,所以bbt_erase_shift通常与phys_erase_shift相等; int chip_shift;用位表示的NAND芯片的容量; int numchips;表示系统中有多少片NAND芯片; uint64_t chipsize;//NAND芯片的大小; int pagemask;//计算page number时的掩码,总是等于chipsize/page大小- 1; int pagebuf;用来保存当前读取的NAND芯片的page number,这样一来,下次读取的数据若还是属于同一个page,就不必再从NAND芯片读取了,而是从data_buf中直接得到;     …………………… void *priv; };   有了前面的介绍,下面我们就开始进行对S3C2440.c 的分析。同样分析一个驱动程序首先要从他的入口函数开始分析:   static int __init s3c2410_nand_init(void) { printk("S3C24XX NAND Driver, (c) 2004 Simtec Electronics "); platform_driver_register(&s3c2412_nand_driver); platform_driver_register(&s3c2440_nand_driver); /* 平台设备驱动模型中对于nand平台驱动的注册 */ return platform_driver_register(&s3c2410_nand_driver); } 上面就是平台设备驱动模型中对于nand平台驱动的注册。通过注册,平台 会将这个驱动同他所拥有的设备进行意义比较,如果 .driver= { .name= "s3c2412-nand"}中有名字为s3c2412-nand的设备,将会调用本驱动的probe函数。而通过查看我们知道有这样的设备,他定义在芯片框架下的设备文件arch/arm/plat-s3c24xx/devs.c/* NAND Controller */ static struct resource s3c_nand_resource[] = { [0] = { .start = S3C2410_PA_NAND, .end = S3C2410_PA_NAND + S3C24XX_SZ_NAND - 1, .flags = IORESOURCE_MEM, } }; struct platform_device s3c_device_nand = { .name = "s3c2410-nand", .id = -1, .num_resources = ARRAY_SIZE(s3c_nand_resource), .resource = s3c_nand_resource, }; 从上面我们可以看出,他通过resource结构体给了我们用于设置nand 寄存器的首尾地址,同时也通过platform_device设置了名字。而当我们的名字匹配上后就进入了probe函数: static int s3c24xx_nand_probe(struct platform_device *pdev, enum s3c_cpu_type cpu_type) { struct s3c2410_platform_nand *plat = to_nand_plat(pdev); struct s3c2410_nand_info *info; struct s3c2410_nand_mtd *nmtd; /* 这个结构体中就包含了mtd_info和nand_chip结构体 */ struct s3c2410_nand_set *sets; /* 在sets中有与分区相关的设置 */ struct resource *res; int size; int nr_sets; int setno; info = kmalloc(sizeof(*info), GFP_KERNEL); memzero(info, sizeof(*info)); platform_set_drvdata(pdev, info); spin_lock_init(&info->controller.lock); init_waitqueue_head(&info->controller.wq); /* get the clock source and enable it */ info->clk = clk_get(&pdev->dev, "nand"); /* 在CLKCON寄存器上获得nand的总开关 */ clk_enable(info->clk); /* 使能总开关,开启nand 服务 */ /* allocate and map the resource */ /* currently we assume we have the one resource */ res = pdev->resource; size = res->end - res->start + 1; info->area = request_mem_region(res->start, size, pdev->name); info->device = &pdev->dev; info->platform = plat; info->regs = ioremap(res->start, size); /* 为nand 的寄存器重映射,而regs就是他们虚拟地址的首地址 */ info->cpu_type = cpu_type; /* initialise the hardware */ err = s3c2410_nand_inithw(info, pdev); /* 初始化硬件,其实就是设置tacls,twrph0,twrph1的值 */ sets = (plat != NULL) ? plat->sets : NULL; nr_sets = (plat != NULL) ? plat->nr_sets : 1; /*sets为一个数据结构,里面包含有mtd_partition,即为分区信息。nr_sets为分区的个数*/ info->mtd_count = nr_sets; /* allocate our information */ size = nr_sets * sizeof(*info->mtds); info->mtds = kmalloc(size, GFP_KERNEL); memzero(info->mtds, size); /* initialise all possible chips */ for (setno = 0; setno < nr_sets; setno++, nmtd++) { s3c2410_nand_init_chip(info, nmtd, sets); /* 初始化nand_chip中的各个参数 */ nmtd->scan_res = nand_scan(&nmtd->mtd, (sets) ? sets->nr_chips : 1); /* nand扫描函数 */ if (nmtd->scan_res == 0) { s3c2410_nand_add_partition(info, nmtd, sets); /* 如果有分区信息,添加分区 */ } } }sets为一个数据结构,里面包含有mtd_partition,即为分区信息。nr_sets为分区的个数*/ info->mtd_count = nr_sets; /* allocate our information */ size = nr_sets * sizeof(*info->mtds); info->mtds = kmalloc(size, GFP_KERNEL); memzero(info->mtds, size); /* initialise all possible chips */ for (setno = 0; setno < nr_sets; setno++, nmtd++) { s3c2410_nand_init_chip(info, nmtd, sets); /* 初始化nand_chip中的各个参数 */ nmtd->scan_res = nand_scan(&nmtd->mtd, (sets) ? sets->nr_chips : 1); /* nand扫描函数 */ if (nmtd->scan_res == 0) { s3c2410_nand_add_partition(info, nmtd, sets); /* 如果有分区信息,添加分区 */ } } } 通过对上面代码的查看我们知道有四个函数在probe中是非常重要,他们是:   s3c2410_nand_inithw(info,pdev);              //设置TACLST WRPH0 TWRPH1 s3c2410_nand_init_chip(info, nmtd, sets); //初始化nand_chip nand_scan                                                  // 完成对flash的探测及mtd_info读写函数的赋值 s3c2410_nand_add_partition(info,nmtd, sets); //添加分区 而后两个函数我们在前一篇文章中已经说过,现在我们主要说前两个函数,首先分析s3c2410_nand_inithw(info,pdev):设置TACLST WRPH0 TWRPH1函数,通过此函数我们可以设置TACLST WRPH0 TWRPH1三个值,也就是设定了:   TACLS:发出ALE/CLE之后多长时间才发出nWE信号  TWRPH0:nWE的脉冲宽度,HCLK*(TWRPH0+1)  TWRPH1:nWE变为高电平后多长时间ALE/CLE能变为低电平,HCLK*(TWRPH1+1), 下面我们进入函数分析: /* controller setup */ static int s3c2410_nand_inithw(struct s3c2410_nand_info *info, struct platform_device *pdev) { struct s3c2410_platform_nand *plat = to_nand_plat(pdev); unsigned long clkrate = clk_get_rate(info->clk); int tacls_max = (info->cpu_type == TYPE_S3C2412) ? 8 : 4; int tacls, twrph0, twrph1; unsigned long cfg = 0; /* calculate the timing information for the controller */ clkrate /= 1000; /* turn clock into kHz for ease of use */ if (plat != NULL) { tacls = s3c_nand_calc_rate(plat->tacls, clkrate, tacls_max); twrph0 = s3c_nand_calc_rate(plat->twrph0, clkrate, 8); twrph1 = s3c_nand_calc_rate(plat->twrph1, clkrate, 8); } else { /* default timings */ tacls = tacls_max; twrph0 = 8; twrph1 = 8; } switch (info->cpu_type) { case TYPE_S3C2440: cfg = S3C2440_NFCONF_TACLS(tacls - 1); cfg |= S3C2440_NFCONF_TWRPH0(twrph0 - 1); cfg |= S3C2440_NFCONF_TWRPH1(twrph1 - 1); /* enable the controller and de-assert nFCE */ writel(S3C2440_NFCONT_ENABLE, info->regs + S3C2440_NFCONT); } dev_dbg(info->device, "NF_CONF is 0x%lx ", cfg); writel(cfg, info->regs + S3C2410_NFCONF); return 0; } 而接下来我们就要分析:s3c2410_nand_init_chip函数来确定他是怎样设置nand_chip结构体重的各个指针函数。 static void s3c2410_nand_init_chip(struct s3c2410_nand_info *info, struct s3c2410_nand_mtd *nmtd, struct s3c2410_nand_set *set) { struct nand_chip *chip = &nmtd->chip; void __iomem *regs = info->regs; chip->write_buf = s3c2410_nand_write_buf; /* 设置写缓冲函数 */ chip->read_buf = s3c2410_nand_read_buf; /* 设置读缓存函数 */ chip->select_chip = s3c2410_nand_select_chip; /* 设置片选函数 */ chip->chip_delay = 50; /* 设置延时时间 */ chip->priv = nmtd; /* 设置私有数据 */ chip->options = 0; chip->controller = &info->controller; switch (info->cpu_type) { /* 这里是选择CPU的型号,为了简洁我只留下了本驱动所用的S3C2440 */ case TYPE_S3C2440: chip->IO_ADDR_W = regs + S3C2440_NFDATA; /* 设置写缓冲地址 */ info->sel_reg = regs + S3C2440_NFCONT; info->sel_bit = S3C2440_NFCONT_nFCE; /* 设置片选地址 */ chip->cmd_ctrl = s3c2440_nand_hwcontrol; /* 设置写命令函数 */ chip->dev_ready = s3c2440_nand_devready; /* 设置等待就绪函数 */ break; } chip->IO_ADDR_R = chip->IO_ADDR_W; /* 设置读缓存地址 */ nmtd->info = info; nmtd->mtd.priv = chip; /* 将nand_chip放入到mtd_info的私有数据中 */ nmtd->mtd.owner = THIS_MODULE; /* 这个是很重要的 */ nmtd->set = set; chip->ecc.mode = NAND_ECC_SOFT; /* 设置ecc模式为软件检测模式 */ } 上面就是对nand_chip结构体的设置了,而以前我们分析知道,当我们没有去写nand_chip结构体下的相应的函数时,nand_scan会为我们设置默认的函数,但是这些默认的函数不一定是适合本芯片的。所以我们要自己写这些函数。 首先我们要写的是s3c2410_nand_write_buf函数 static void s3c2410_nand_write_buf(struct mtd_info *mtd, const u_char *buf, int len) { struct nand_chip *this = mtd->priv; writesb(this->IO_ADDR_W, buf, len); /* 将长度为len的buf值写到地址IO_ADDR_W中 */ } 上面代码就是要将buf的值放入到IO_ADDR_W中,而IO_ADDR_W是写地址的虚拟地址,他会上一个代码中已经设置了。他为 nand数据寄存器NFDATA 的虚拟地址。 而s3c2410_nand_read_buf函数: static void s3c2410_nand_read_buf(struct mtd_info *mtd, u_char *buf, int len) { struct nand_chip *this = mtd->priv; readsb(this->IO_ADDR_R, buf, len); /* 将长度为len的buf值读到地址IO_ADDR_W中 */ } 与s3c2410_nand_write_buf函数相似,只是将读改为了写。他同样为nand数据寄存器NFDATA 的虚拟地址。 而s3c2410_nand_select_chip函数: static void s3c2410_nand_select_chip(struct mtd_info *mtd, int chip) { struct s3c2410_nand_info *info; struct s3c2410_nand_mtd *nmtd; struct nand_chip *this = mtd->priv; unsigned long cur; nmtd = this->priv; info = nmtd->info; if (chip != -1 && allow_clk_stop(info)) clk_enable(info->clk); cur = readl(info->sel_reg); if (chip == -1) { cur |= info->sel_bit; } else { if (nmtd->set != NULL && chip > nmtd->set->nr_chips) { dev_err(info->device, "invalid chip %d ", chip); return; } if (info->platform != NULL) { if (info->platform->select_chip != NULL) (info->platform->select_chip) (nmtd->set, chip); } cur &= ~info->sel_bit; } writel(cur, info->sel_reg); if (chip == -1 && allow_clk_stop(info)) clk_disable(info->clk); } 该函数最主要的作用就是设置片选信号。当需要选中时,设置nand控制寄存器NFCONT[1]=0  ,而当需要取消时,设置nand控制寄存器NFCONT[1]=1 而接下来就是写命令/地址函数s3c2410_nand_hwcontrolstatic void s3c2410_nand_hwcontrol(struct mtd_info *mtd, int cmd, unsigned int ctrl) { struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd); if (cmd == NAND_CMD_NONE) return; if (ctrl & NAND_CLE) /* 判断当为命令时, */ writeb(cmd, info->regs + S3C2410_NFCMD); /* 将命令值写入nand的命令寄存器MFCMD中 */ else /* 否则为写地址 */ writeb(cmd, info->regs + S3C2410_NFADDR); /* 将地址值写入nand的地址寄存器NFADDR中 */ } 上函数就是通过判断参数ctrl的值来判断是写命令还是写地址,并将命令或地址写入相应的寄存器的虚拟地址中。 而我们最后要自己写的函数就是s3c2440_nand_devready等待就绪函数: static int s3c2440_nand_devready(struct mtd_info *mtd) { struct s3c2410_nand_info *info = s3c2410_nand_mtd_toinfo(mtd); return readb(info->regs + S3C2440_NFSTAT) & S3C2440_NFSTAT_READY; } 其实他就是通过返回nand状态寄存器中bit[0]来确定nand所处的状态。   而由于我们的驱动程序选择的是软件检测ECC所以不用去完成硬件ECC所要写的众多函数而只用下面一句就可以完成了: chip->ecc.mode = NAND_ECC_SOFT; 而如果你选择了硬件检测ECC将要完成以下函数的编写:             if (hardware_ecc) { chip->ecc.calculate = s3c2410_nand_calculate_ecc; chip->ecc.correct = s3c2410_nand_correct_data; chip->ecc.mode = NAND_ECC_HW; chip->ecc.size = 512; chip->ecc.bytes = 3; chip->ecc.layout = &nand_hw_eccoob; switch (info->cpu_type) { case TYPE_S3C2410: chip->ecc.hwctl = s3c2410_nand_enable_hwecc; chip->ecc.calculate = s3c2410_nand_calculate_ecc; break; case TYPE_S3C2412: chip->ecc.hwctl = s3c2412_nand_enable_hwecc; chip->ecc.calculate = s3c2412_nand_calculate_ecc; break; case TYPE_S3C2440: chip->ecc.hwctl = s3c2440_nand_enable_hwecc; chip->ecc.calculate = s3c2440_nand_calculate_ecc; break;     } 由于本驱动并未用硬件ECC所以上面程序要根据自己的nand flash去填写,如擦除大小是以block为单位,你的block为多大你就写多大。   完成上面所说的这些,如果你不用分区一个nand 的驱动程序就基本完成了,这时候你只用写 add_mtd_device(&mtd->mtd) 而如果你还要分区,那么你就要填写分区表,然后调用 : add_mtd_partitions(&mtd->mtd, set->partitions, set->nr_partitions); 而分区信息在(driversmtd and andsim.c)中:         for (i = 0; i < parts_num; ++i) { unsigned long part = parts[i]; if (!part || part > remains / ns->geom.secsz) { NS_ERR("bad partition size. "); ret = -EINVAL; goto error; } ns->partitions[i].name = get_partition_name(i); ns->partitions[i].offset = next_offset; ns->partitions[i].size = part * ns->geom.secsz; next_offset += ns->partitions[i].size; remains -= ns->partitions[i].size; } ns->nbparts = parts_num; 而详细的信息在plat-s3c24xx/common-smdk.c: static struct mtd_partition smdk_default_nand_part[] = {     [0] = {         name: "bootloader",         size: 0x00100000,         offset: 0x0,     },     [1] = {         name: "kernel",         size: 0x00300000,         offset: 0x00100000,     },     [2] = {         name: "root",         size: 0x02800000,         offset: 0x00400000,     }, }; 现在我们的驱动程序就写完了。其实这样分析下来我们会发现其实我们要去写的东西并不多就三个结构体: mtd_info    nand_chip     mtd_partition
而完成这三个结构体我们的程序就大体写完了。所以,我会在下一个篇文章中再引入一个简单的nand驱动程序的例子——AT91驱动,然后在写一个自己nand的驱动程序。