《网蜂A8实战演练》——9.Linux NAND FLASH 驱动

时间:2020-12-26 17:32:59

第11章 Linux NAND FLASH 驱动


这一章学习 NAND FLASH 驱动,和前面的 LCD 驱动一样, Linux 内核里面已经有了一套代码是支持这类存储器的驱动,叫做 MTD(memory technologydevice 内存技术设备)。 他帮我们做了很多工作,从而使增加新的内存设备驱动程序变得更简单。但是这一层代码看起来就不怎么简单了。这一章我们先来了解一下 linux 内核的 MTD 子系统,然后再开始写我们的 nand flash 驱动。


11.1 MTD 子系统


MTD 子系统是在硬件和文件系统层之间的提供了一个抽象的接口,用来访问内存设备(如: ROM、 flash)的中间层,它将内存设备的共有特性抽取出来,从而使增加新的内存设备驱动程序变得更简单。MTD 的源代码都在/drivers/mtd 目录中。MTD 中间层细分为四层,按从上到下依次为:设备节点、 MTD 设备层、 MTD原始设备层和硬件驱动层。 MTD 中间层层次结构如图 11.1 所示 :

11.1 MTD 子系统


MTD 子系统是在硬件和文件系统层之间的提供了一个抽象的接口,用来访问内存设备(如: ROM、 flash)的中间层,它将内存设备的共有特性抽取出来,从而使增加新的内存设备驱动程序变得更简单。


MTD 的源代码都在/drivers/mtd 目录中。


MTD 中间层细分为四层,按从上到下依次为:设备节点、 MTD 设备层、 MTD原始设备层和硬件驱动层。 MTD 中间层层次结构如图 11.1 所示 :

《网蜂A8实战演练》——9.Linux NAND FLASH 驱动

11.1.1 设备节点层


通过 mknod 在/dev 子目录下建立 MTD 字符设备(规定主设备号 90)和块设备号(规定的主设备号 31 ) .

11.1.2 MTD 设备层


这一层分为 MTD 字符设备和 MTD 块设备。 MTD 字符设备的定义在driver/mtd/mtdchar.c 中,通过 file_operations mtd_fops 结构体,注册 lseek,open , close,read,write,ioctl 等 设 备 操 作 函 数 ; MTD 块 设 备 的 定 义 在driver/mtd/mtdblock.c 中,与字符设备相似,注册设备操作函数的结构体为mtd_blktrans_ops,描述块设备的结构体则为 mtdblk_dev。


11.1.3 MTD 原始层


MTD 原始层由两部分组成,一部分是 MTD 原始设备的通用代码,另一部分是各个特定的 FLASH 的数据,例如分区。


11.1.4 硬件驱动层


linux MTD 设备的驱动程序,根据不同的 MTD 设备他们存放在不同目录,如 NAND FLASH 驱 动 存 放 在 /driver/mtd/nand, 而 NOR FLASH 存 放 于/driver/mtd/chips.

11.2 Linux MTD 系统接口 


Linux MTD 各层的接口函数和关键结构体如图 11.2 所示:我们这一小节先来看一下一些结构体,具体的函数我们下一节再结合一个内核包含的 nand flash 驱动来分析。这一节对下一节也是很有紧密联系的哦。

《网蜂A8实战演练》——9.Linux NAND FLASH 驱动


11.2.1 MTD 原始设备层


先从 MTD 原始设备层看,因为他正好处于中间层,承上启下,比较关键。先来看第一个结构体: mtd_info。

11.2.1.1 mtd_info 结构体


mtd_info 是表示 MTD 原始设备的结构体,每个分区也被认为是一个mtd_info.例如有一个 MTD 设备,他有两个分区,系统认为他有两个 mtd_info。这些 mtd_info 的指针存放在名为 mtd_table 的数组里。


mtd_info 在 include/linux/mtd/mtd.h 里:


struct mtd_info {
u_char type; /* MTD 类型,包括 MTD_NORFLASH,MTD_NANDFLASH
等(可参考 mtd-abi.h) */
uint32_t flags; /* MTD 属性标志, MTD_WRITEABLE,MTD_NO_ERASE 等
(可参考 mtd-abi.h) */
uint64_t size; /* MTD 设备的大小 */
uint32_t erasesize; /* MTD 设备的擦除单元大小,对于 NandFlash 来说就
是 Block 的大小 */
uint32_t writesize; /* 写大小,对 nandFlash 为一页 */
uint32_t oobsize; /* OOB 字节数 */
uint32_t oobavail; /* 可用的 OOB 字节数 */
unsigned int erasesize_shift; /* 默认为 0,不重要 */
unsigned int writesize_shift;/* 默认为 0,不重要 */
unsigned int erasesize_mask; /* 默认为 1 ,不重要 */
unsigned int writesize_mask; /* 默认为 1 ,不重要 */
const char *name; /* 名字, 不重要*/
int index; /* 索引号,不重要 */
int numeraseregions; /* 通常为 1 */
struct mtd_erase_region_info *eraseregions;/* 可变擦除区域 */
void *priv;/*设备私有数据指针,对 NandFlash 来说指 nand_chip 结构体*/
struct module *owner; /* 一般设置为 THIS_MODULE */
/* 擦除函数 */
int (*erase) (struct mtd_info *mtd, struct erase_info *instr);
/* 读写 flash 函数 */
int (*read) (struct mtd_info *mtd, loff_t from, size_t len, size_t *retlen,u_char *buf);
int (*write) (struct mtd_info *mtd, loff_t to, size_t len, size_t *retlen, constu_char *buf);
/* 带 oob 读写 Flash 函数 */
int (*read_oob) (struct mtd_info *mtd, loff_t from,struct mtd_oob_ops *ops);
int (*write_oob) (struct mtd_info *mtd, loff_t to,struct mtd_oob_ops *ops);

int (*get_fact_prot_info) (struct mtd_info *mtd, struct otp_info *buf, size_t  len);
int (*read_fact_prot_reg) (struct mtd_info *mtd, loff_t from, size_t len,size_t *retlen, u_char *buf);
int (*get_user_prot_info) (struct mtd_info *mtd, struct otp_info *buf, size_t  len);
int (*read_user_prot_reg) (struct mtd_info *mtd, loff_t from, size_t len,size_t *retlen, u_char *buf);
int (*write_user_prot_reg) (struct mtd_info *mtd, loff_t from, size_t len,size_t *retlen, u_char *buf);
int (*lock_user_prot_reg) (struct mtd_info *mtd, loff_t from, size_t len);
int (*writev) (struct mtd_info *mtd, const struct kvec *vecs, unsigned long count, loff_t to, size_t *retlen);
int (*panic_write) (struct mtd_info *mtd, loff_t to, size_t len, size_t *retlen,const u_char *buf);
/* 同步*/
void (*sync) (struct mtd_info *mtd);
/* Chip-supported device locking */
int (*lock) (struct mtd_info *mtd, loff_t ofs, uint64_t len);
int (*unlock) (struct mtd_info *mtd, loff_t ofs, uint64_t len);
/* 电源管理函数 */
int (*suspend) (struct mtd_info *mtd);
void (*resume) (struct mtd_info *mtd);
/* 坏块管理函数 */
int (*block_isbad) (struct mtd_info *mtd, loff_t ofs);
int (*block_markbad) (struct mtd_info *mtd, loff_t ofs);
void (*unpoint) (struct mtd_info *mtd, loff_t from, size_t len);
unsigned long (*get_unmapped_area) (struct mtd_info *mtd,
unsigned long len,
unsigned long offset,
unsigned long flags);
struct backing_dev_info *backing_dev_info;
struct notifier_block reboot_notifier; /* default mode before reboot */
/* ECC 状态信息 */
struct mtd_ecc_stats ecc_stats;
int subpage_sft;
struct device dev;

int usecount;
int (*get_device) (struct mtd_info *mtd);
void (*put_device) (struct mtd_info *mtd);
};


在上面提到几个名词, ECC 和 OOB,他们是什么呢?


NAND Flash 每一页大小为(512+16)字节(还有其他格式的 NANDFlash,比如每页大小为(256+8)、 (2048+64)等),其中的 512 字节就是一般存储数据的区域, 16 字节称为 OOB(Out OfBand)区。通常在 OOB 区存放坏块标记、前面 512字节的 ECC 较验码等。


cramfs、 jffs2 文件系统映像文件中并没有 OOB 区的内容,如果将它们烧入NORFlash 中,则是简单的“平铺”关系;如果将它们烧入 NAND Flash 中,则NANDFlash 的驱动程序首先根据 OOB 的标记略过坏块,然后将一页数据(512字节)写入后,还会计算这 512 字节的 ECC 较验码,最后将它写入 OOB 区,如此循环。 cramfs、 jffs2 文件系统映像文件的大小通常是 512 的整数倍。而 yaffs 文件系统映像文件的格式则跟它们不同,文件本身就包含了 OOB 区的数据(里面有坏块标记、 ECC 较验码、其他 yaffs 相关的信息)。所以烧写时,不需要再计算 ECC 值,首先检查是否坏块(是则跳过),然后写入 512 字节的数据,最后写入 16 字节的 OOB 数据,如此循环。 yaffs 文件系统映像文件的大小是(512+16)的整数倍。


注意:烧写 yaffs 文件系统映像时,分区上第一个可用的(不是坏块)块也要跳过。


ECC 的全称是 Error Checking and Correction,是一种用于 Nand 的差错检测和修正算法。如果操作时序和电路稳定性不存在问题的话, NAND Flash 出错的时候一般不会造成整个 Block 或是 Page 不能读取或是全部出错,而是整个Page(例如 512Bytes)中只有一个或几个 bit 出错。 ECC 能纠正 1 个比特错误和检测 2 个比特错误,而且计算速度很快,但对 1 比特以上的错误无法纠正,对 2 比特以上的错误不保证能检测。


校验码生成算法: ECC 校验每次对 256 字节的数据进行操作,包含列校验和行校验。对每个待校验的 Bit 位求异或,若结果为 0,则表明含有偶数个 1;若结果为 1 ,则表明含有奇数个 1 。列校验规则如表 1 所示。 256 字节数据形成256 行、 8 列的矩阵,矩阵每个元素表示一个 Bit 位。


算法的 c 语言实现在 drivers/mtd/nand/nand_ecc.c。


OOB 是每个页都有的数据, 里面存的有 ECC(当然不仅仅);而 BBT 是一个 FLASH 才有一个;针对每个 BLOCK 的坏块识别则是该块第一页 spare area的第六个字节。


ECC 一般每 256 字节原始数据生成 3 字节 ECC 校验数据,这三字节共 24比特分成两部分: 6 比特的列校验和 16 比特的行校验,多余的两个比特置 1 。( 512生成两组 ECC,共 6 字节) 


当往 NAND Flash 的 page 中写入数据的时候,每 256 字节我们生成一个ECC 校验和,称之为原 ECC 校验和,保存到 PAGE 的 OOB ( out- of-band)数据区中。其位置就是 eccpos[]。校验的时候,根据上述 ECC 生成原理不难推断:将从 OOB 区中读出的原 ECC 校验和新 ECC 校验和按位异或,若结果为 0,则表示不存在错(或是出现了 ECC 无法检测的错误);若 3 个字节异或结果中存在 11 个比特位为 1 ,表示存在一个比特错误,且可纠正;若 3 个字节异或结果中只存在 1 个比特位为 1 ,表示 OOB 区出错;其他情况均表示出现了无法纠正的错误。

11.2.1.2 mtd_partition 结构体


存放在内核文件 include/linux/mtd/partitions.h 中


struct mtd_partition {
char *name; /* 分区名 ,自己随便命名 */
u_int32_t size; /* 分区大小 */
u_int32_t offset; /* 主 MTD 空间内的偏移地址 */
u_int32_t mask_flags; /* 掩码标志 ,不重要 */
struct nand_oobinfo *oobsel; /* OOB 布局 */
struct mtd_info **mtdp; /* 存储 MTD 对象的指针 */
}


11.2.1.3 mtd_part 结构体


在 driver/mtd/mtdpart.c 这里还有一个结构体要说明


struct mtd_part {
struct mtd_info mtd; /* 分区信息, 大部分由 master 决定 */
struct mtd_info *master; /* 分区的主分区 */
uint64_t offset; /* 分区的偏移地址 */
struct list_head list; /* 将 mtd_part 链成一个链表 mtd_partitons */
};


看到这里,有同学就要问了,这里的 mtd_part 和 mtd_partition 的结构定义好 像 喔 , 他 们 之 间 有 什 么 关 系 , 有 什 么 不 同 吗 ? 等 一 下 后 面 讲add_mtd_partitions 函数你就会知道啦。

11.2.2 MTD 设备层


MTD 设备主要有 MTD 字符设备( mtdchar.c)和 MTD 块设备(mtdblock.c)11.2.2.1 mtd_notifier 结构体MTD 设备通知结构体, notifier 中文 就是通告 的意思 ^-^ ,存 放在include/linux/mtd/mtd.h


struct mtd_notifier {
/* 加入 MTD 原始/字符/块设备时执行 */
void (*add)(struct mtd_info *mtd);
/* 移除 MTD 原始/字符/块设备时执行 */
void (*remove)(struct mtd_info *mtd);

struct list_head list;//list 是双向链表,定义在 include/linux/list.h
};


有没有发现这个结构体在设备层和原始设备层都有出现,对的,就是通过这个结构体联系这两层的,具体分析我们下面会慢慢分析,到时你领悟到编写内核的牛人们的思想。

11.2.2.2 MTD 字符设备( mtdchar.c) 


主要看下面几个东西:struct file_operations mtd_fops ,这个结构注册了 read, wirte, map, open等函数。写过字符设备人应该都知道。


static const struct file_operations mtd_fops = {
.owner = THIS_MODULE,
.llseek = mtdchar_lseek,
.read = mtdchar_read,
.write = mtdchar_write,
.unlocked_ioctl = mtdchar_unlocked_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = mtdchar_compat_ioctl,
#endif
.open = mtdchar_open,
.release = mtdchar_close,
.mmap = mtdchar_mmap,
#ifndef CONFIG_MMU
.get_unmapped_area = mtdchar_get_unmapped_area,
#endif
};


init_mtdchar 设备初始化函数,在这里完成设备节点的注册。


static int __init init_mtdchar(void)
{
int ret;
/*
#define MTD_CHAR_MAJOR 90
#define MTD_BLOCK_MAJOR 31
*/
/*__register_chrdev 注册设备号*/
ret = __register_chrdev(MTD_CHAR_MAJOR, 0, 1 << MINORBITS,
"mtd", &mtd_fops);
if (ret < 0) {
pr_notice("Can't allocate major number %d for "
"Memory Technology Devices.\n", MTD_CHAR_MAJOR);
return ret;

}
/*注册文件系统*/
ret = register_filesystem(&mtd_inodefs_type);
if (ret) {
pr_notice("Can't register mtd_inodefs filesystem: %d\n", ret);
goto err_unregister_chdev;
}
return ret;
err_unregister_chdev:
__unregister_chrdev(MTD_CHAR_MAJOR, 0, 1 << MINORBITS, "mtd");
return ret;
}


MTD 块设备与此类似,只不过其设备类型不同而已,这里就不再分析了。


11.2.3 FLASH 硬件层


我们的 A8 板上只有 NAND FLASH,这一章的驱动也是在这一层上实现的。LINUX 的 nand flash 驱动都存放在 drivers\mtd\nand 里面,框架如下图 11.3 所示:

《网蜂A8实战演练》——9.Linux NAND FLASH 驱动

其中在 nand_base.c 文件中,实现了最基本的 read(), write(), read_oob(),write_oob() 等等成员函数,这些就不再需要我们去实现了。


mtd_info 在原始设备层已经介绍过了,其中最后有一个 void *priv 私有数据指针,在这里 mtd_info 的私有成员就指向给了 nand_chip 结构体,在 FLASH 层中,我们也主要是对 nand_chip 数据结构的一些比较特殊成员的初始化,然后通过 nand_scan(mtd_info,1)去设置 mtd_info 里面的 nand_chip 的其他成员,以及其他成员函数。

11.2.3.1 nand_chip 结构体


MTD 使用 nand_chip 数据结构表示一个 NAND Flash 芯片,这个结构体中包含了关于 NAND Flash 的地址信息、读写方法、 ECC 模式、硬件控制等一些底层机制。我们只有看了这个结构体才能知道什么需要设置。


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);
/* 选中芯片 */
void (*select_chip)(struct mtd_info *mtd, int chip);
/* 检测是否有坏块 */
int (*block_bad)(struct mtd_info *mtd, loff_t ofs, int getchip);
/* 标记坏块 */
int (*block_markbad)(struct mtd_info *mtd, loff_t ofs);
/* 命令、地址、数据控制函数 */
void (*cmd_ctrl)(struct mtd_info *mtd, int dat,unsigned int ctrl);
/* 设备是否就绪 */
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, intstatus, 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; /* 由板决定的延迟时间 */
/* 与具体的 NAND 芯片相关的一些选项,如 NAND_NO_AUTOINCR,
NAND_BUSWIDTH_16 等 */
unsigned int options;
/* 用位表示的 NAND 芯片的 page 大小,如某片 NAND 芯片
* 的一个 page 有 512 个字节,那么 page_shift 就是 9
*/
int page_shift;
/* 用位表示的 NAND 芯片的每次可擦除的大小,如某片 NAND 芯片每次可
* 擦除 16K 字节(通常就是一个 block 的大小),那么 phys_erase_shift 就是 14
*/
int phys_erase_shift;
/* 用位表示的 bad block table 的大小,通常一个 bbt 占用一个 block,
* 所以 bbt_erase_shift 通常与 phys_erase_shift 相等
*/
int bbt_erase_shift;
/* 用位表示的 NAND 芯片的容量 */
int chip_shift;
/* NADN FLASH 芯片的数量 */
int numchips;
/* NAND 芯片的大小 */
uint64_t chipsize;
int pagemask;
int pagebuf;
int subpagesize;
uint8_t cellinfo;
int badblockpos;
nand_state_t state;
uint8_t *oob_poi;
struct nand_hw_control *controller;
struct nand_ecclayout *ecclayout; /* ECC 布局 */
/* ECC 校验结构体,里面有大量的函数进行 ECC 校验 */
struct nand_ecc_ctrl ecc;
struct nand_buffers *buffers;
struct nand_hw_control hwcontrol;
struct mtd_oob_ops ops;
uint8_t *bbt;
struct nand_bbt_descr *bbt_td;
struct nand_bbt_descr *bbt_md;
struct nand_bbt_descr *badblock_pattern;
void *priv;
};


11.3 从 atmel_nand.c 跟踪分析驱动


通过上一节层次的分析,还有 blabla 讲了一堆结构体,大家可能还是一头雾水,但是这都是基本的应该了解的知识,到了这一节,我们通过被内核收进 nand分支的驱动是怎么写的,并分析上一节没有分析的函数。


注意:以下代码因为版面限制,不重要的代码没有全部贴出来,严重建议学习驱动要建一个 soughtinsight 的工程,然后在里面看代码,怎么建工程在我们
的《网蜂科技 A8 开发平台使用教程》有哦。


好了,现在我们开始跟随 atmel 公司写的 nand flash 驱动开始:分析 drivers\mtd\nand\atmel_nand.c,从入口函数开始:


static int __init atmel_nand_init(void)
{
return platform_driver_probe(&atmel_nand_driver, atmel_nand_probe);
}从这里知道这个驱动用的是 platform 的模型,当 match 到会跳转到 probe函数:
static int __init atmel_nand_probe(struct platform_device *pdev)
{
struct mtd_info *mtd; //分配上一节介绍过的结构体
struct nand_chip *nand_chip;
mtd->priv = nand_chip;
/*进行一些 nand_chip 的设置*/
/* 第一阶段扫描,找到设备并获取页大小 */
/* 传的是 mtd_info 类型的参数,在这个函数会对 mtd_info 进行一些设置 */
if (nand_scan_ident(mtd, 1, NULL))
{
res = -ENXIO;
goto err_scan_ident;
}
/* 第二阶段扫描 */
if (nand_scan_tail(mtd)) {
res = -ENXIO;
goto err_scan_tail;
}
/*MTD 设备的注册*/
res = mtd_device_parse_register(mtd, NULL, &ppdata,host->board.parts, host->board.num_parts);
}


至此我们自己在硬件层需做的工作就结束了(一些配置的代码没贴出来),但是这个 nand 驱动是怎么被实现的呢,我们看看被 linux 收纳进去的代码是怎么做到的。根据上面三个函数,我们开始分析。

11.3.1 nand_scan() 


在上一节我们有提到这个函数,但是貌似这个 atmel 公司写的驱动好像没有用到它呀,不要急,我们进入这个函数看看在 drivers\mtd\nand\nand_base.c


int nand_scan(struct mtd_info *mtd, int maxchips)
{
int ret;
if (!mtd->owner && caller_is_module())
{
pr_crit("%s called with NULL mtd->owner!\n", __func__);
BUG();
}
ret = nand_scan_ident(mtd, maxchips, NULL);
if (!ret)
ret = nand_scan_tail(mtd);
return ret;
}


感觉很短是不是,因为在这里他调用了下面这两个函数。

11.3.1.1 nand_scan_ident () 


int nand_scan_ident(struct mtd_info *mtd, int maxchips, struct nand_flash_dev *table)
{
/*从传参获取 nand_chip 结构体内容*/
struct nand_chip *chip = mtd->priv;
/* Get buswidth to select the correct functions */
busw = chip->options & NAND_BUSWIDTH_16;
/*设置默认的配置*/
nand_set_defaults(chip, busw);
/*获取 FLASH 的类型 */
type = nand_get_flash_type(mtd, chip, busw,&nand_maf_id, &nand_dev_id, table);

/*选中芯片*/
chip->select_chip(mtd, -1);
/* Check for a chip array */
/* … */
/* Store the number of chips and calc total size for mtd */
/* … */
}


这里用到一个面向对象的思想,不防跟踪进来看一下


static void nand_set_defaults(struct nand_chip *chip, int busw)
{
/* 检查 chip->chip_delay 在之前是否被设置过了,如果没有的话
* 设为默认值 20,如果我们在之前自己的驱动去设置了,
* 那么就不会被设置为这个默认的参数了 ,以下同理
*/
if (!chip->chip_delay)
chip->chip_delay = 20;
/* check, if a user supplied command function given */
if (chip->cmdfunc == NULL)
chip->cmdfunc = nand_command;
/* check, if a user supplied wait function given */
if (chip->waitfunc == NULL)
chip->waitfunc = nand_wait;
if (!chip->select_chip)
chip->select_chip = nand_select_chip;
if (!chip->read_byte)
chip->read_byte = busw ? nand_read_byte16 : nand_read_byte;
if (!chip->read_word)
chip->read_word = nand_read_word;
if (!chip->block_bad)
chip->block_bad = nand_block_bad;
if (!chip->block_markbad)
chip->block_markbad = nand_default_block_markbad;
if (!chip->write_buf)
chip->write_buf = busw ? nand_write_buf16 : nand_write_buf;
if (!chip->read_buf)
chip->read_buf = busw ? nand_read_buf16 : nand_read_buf;
if (!chip->scan_bbt)
chip->scan_bbt = nand_default_bbt;

if (!chip->controller) {
chip->controller = &chip->hwcontrol;
spin_lock_init(&chip->controller->lock);
init_waitqueue_head(&chip->controller->wq);
}
}


至此我们知道 nand_scan 函数是以 mtd_info 为参数探测 NAND flash 的存在,存在的话选中芯片,还有设置 mtd_info -> nand_chip 里面的参数。设置过程是如果没配置过的会用默认的配置。默认配置在 nand_base.c 里面,是一些比较通用的函数,而一些配置要在自己的驱动程序来配置。这里典型的利用了面向对象的继承和重载的思想,不得不佩服写 linux 的这些大牛们。

11.3.1.2 nand_scan_tail() 


int nand_scan_tail(struct mtd_info *mtd)
{
struct nand_chip *chip = mtd->priv;
/*和前面一样,如果之前没配置用默认值 */
if (!chip->ecc.layout && (chip->ecc.mode != NAND_ECC_SOFT_BCH)) {
switch (mtd->oobsize) {
case 8:
chip->ecc.layout = &nand_oob_8;
break;
case 16:
chip->ecc.layout = &nand_oob_16;
break;
case 64:
chip->ecc.layout = &nand_oob_64;
break;
case 128:
chip->ecc.layout = &nand_oob_128;
break;
default:
pr_warn("No oob scheme defined for oobsize %d\n",mtd->oobsize);
BUG();
}
}
if (!chip->write_page)
chip->write_page = nand_write_page;

/* … */
/* 填充 mtd_info 结构剩下的成员数据*/
mtd->type = MTD_NANDFLASH;
mtd->flags = (chip->options & NAND_ROM) ? MTD_CAP_ROM :
MTD_CAP_NANDFLASH;
mtd->_erase = nand_erase;
mtd->_point = NULL;
mtd->_unpoint = NULL;
mtd->_read = nand_read;
mtd->_write = nand_write;
mtd->_panic_write = panic_nand_write;
mtd->_read_oob = nand_read_oob;
mtd->_write_oob = nand_write_oob;
mtd->_sync = nand_sync;
mtd->_lock = NULL;
mtd->_unlock = NULL;
mtd->_suspend = nand_suspend;
mtd->_resume = nand_resume;
mtd->_block_isbad = nand_block_isbad;
mtd->_block_markbad = nand_block_markbad;
mtd->writebufsize = mtd->writesize;
/* propagate ecc info to mtd_info */
/*…*/
}


到这里 mtd_info 的配置都设置好了,因为我们用了 nand_scan()去设置,所以调用完 mtd_info 结构体里面的参数,想 read, write 函数就能支持 nand flash了。


我们来看一下他的调用过程:

App call read() ->
mtdchar_read() [struct file_operations mtd_fops of mtdchar.c]
->mtd_read(mtd, *ppos, len, &retlen, kbuf); [mtdchar_read() of mtdchar.c]
->mtd->_read = nand_read;
->nand_do_read_ops(mtd, from, &ops)
-> chip->ecc.read_page(mtd, chip, bufpoi,oob_required, page);
chip->ecc.read_page = nand_read_page_hwecc()->chip->read_buf()


通过这些配置,一层层下来就能对 flash 操作了。

11.3.2 add_mtd_partitions() 


接下来我们继续看这个函数 mtd_device_parse_register()


int mtd_device_parse_register(struct mtd_info *mtd, const char **types, struct mtd_part_parser_data *parser_data, const struct mtd_partition *parts, int nr_parts)
{
int err;
struct mtd_partition *real_parts;
err = parse_mtd_partitions(mtd, types, &real_parts, parser_data);
if (err <= 0 && nr_parts && parts) {
real_parts = kmemdup(parts, sizeof(*parts) * nr_parts,GFP_KERNEL);
if (!real_parts)
err = -ENOMEM;
else
err = nr_parts;
}
if (err > 0) {
err = add_mtd_partitions(mtd, real_parts, err); /*调用了这个函数*/
kfree(real_parts);
} else if (err == 0) {
err = add_mtd_device(mtd);
if (err == 1)
err = -ENODEV;
}
return err;
}


接 下 来 我 们 就 来 仔 细 分 析 这 个 函 数 add_mtd_partitions() , 他 处 于drivers/mtd/mtdpart.c。


add_mtd_partitions()看函数名就知道他大概是添加分区的作用。其实他会新建一个 mtd_part 结构体,将其加入 mtd_partions 中,并调用 add_mtd_device()将此分区作为 MTD 设备加入 radix 树,便于查找。(我们使用的内核是 linux 3.8已经不再使用 mtd_table 这个变量了) .现在我们明白了 mtd_part 与 partitions的关系了吧!当然细心同学又留意到了一个新的名词 radix 树,这个是什么呢?等一下看到后面我们就知道。我们来分析一些这个 add_mtd_partitions()都做了些什么。

/ * *
此函数将创建 nbparts 个 mtd_part,其中每个 mtd_part 包含了一个*mtd_info,mtd_info 的数据内容来自 master 和 parts。程序将 mtd_info*加入 radix 树,并将 mtd_part 中的 list_head 加入 list
* /
int add_mtd_partitions(struct mtd_info *master,const struct mtd_partition*parts,int nbparts)
{
struct mtd_part *slave; // mtd_part 结构体指针
uint64_t cur_offset = 0; //当前的便移量
int i;
printk(KERN_NOTICE "Creating %d MTD partitions on \"%s\":\n", nbparts,master->name);
/*这个 for 循环将分配 nbparts 个 mtd_part*/
for (i = 0; i < nbparts; i++) {
/*分配 mtd_part 内存*/
slave = allocate_partition(master, parts + i, i, cur_offset);
if (IS_ERR(slave))
return PTR_ERR(slave);
/*加锁*/
mutex_lock(&mtd_partitions_mutex);
/*将 mtd_partitions 加入链表*/
list_add(&slave->list, &mtd_partitions);\
/*解锁*/
mutex_unlock(&mtd_partitions_mutex);
/*加入 mtd_table, &slave->mtd 是 mtd_info 结构体*/
add_mtd_device(&slave->mtd);
cur_offset = slave->offset + slave->mtd.size;
}
return 0;
}


为了解开 radix 树是什么的谜,我们看一下 add_mtd_device(&slave->mtd);是如何将 mtd_info 加入 radix 树的。在 driver/mtd/mtdcore.c 可以找到他

11.3.2.1 add_mtd_device() 


int add_mtd_device(struct mtd_info *mtd)
{
struct mtd_notifier *not; //mtd_notifier MTD 设备通知结构体
int i, error;

/*根据 mtd_info->type 判度设备类型,对 backing_dev_info 赋值*/
/*backing_dev_info 是 linux 3.x writeback 机制一个重要结构体,该数据结构
描述了 backing_dev 的所有信息,通常块设备的 request queue 中会包含
backing_dev 对象.*/
if (!mtd->backing_dev_info) {
switch (mtd->type) {
case MTD_RAM:
mtd->backing_dev_info = &mtd_bdi_rw_mappable;
break;
case MTD_ROM:
mtd->backing_dev_info = &mtd_bdi_ro_mappable;
break;
default:
mtd->backing_dev_info = &mtd_bdi_unmappable;
break;
}
}
BUG_ON(mtd->writesize == 0);
/*上锁*/
mutex_lock(&mtd_table_mutex);
/* idr_get_new, idr 机制的一个函数,分配 ID 号并将 ID 号和指针关联*/
do {
if (!idr_pre_get(&mtd_idr, GFP_KERNEL))
goto fail_locked;
error = idr_get_new(&mtd_idr, mtd, &i);
} while (error == -EAGAIN);
if (error)
goto fail_locked;
mtd->index = i;
mtd->usecount = 0;
/* 这个 bitflip_threshold 可以参考
*Documentation/ABI/testing/sysfs-class-mtd,他的值与
*mtd->ecc_strength 有关
* /
/* default value if not set by driver */
if (mtd->bitflip_threshold == 0)
mtd->bitflip_threshold = mtd->ecc_strength;

if (is_power_of_2(mtd->erasesize))
mtd->erasesize_shift = ffs(mtd->erasesize) - 1;
else
mtd->erasesize_shift = 0;
if (is_power_of_2(mtd->writesize))
mtd->writesize_shift = ffs(mtd->writesize) - 1;
else
mtd->writesize_shift = 0;
mtd->erasesize_mask = (1 << mtd->erasesize_shift) - 1;
mtd->writesize_mask = (1 << mtd->writesize_shift) - 1;
/* Some chips always power up locked. Unlock them now */
/*解锁 MTD 设备*/
if ((mtd->flags & MTD_WRITEABLE) && (mtd->flags & MTD_POWERUP_LOCK)) {
error = mtd_unlock(mtd, 0, mtd->size);
if (error && error != -EOPNOTSUPP)
printk(KERN_WARNING "%s: unlock failed, writes may not work\n",mtd->name);
}
/* Caller should have set dev.parent to match the
* physical device.
*/
mtd->dev.type = &mtd_devtype;
mtd->dev.class = &mtd_class;
mtd->dev.devt = MTD_DEVT(i);
dev_set_name(&mtd->dev, "mtd%d", i);
dev_set_drvdata(&mtd->dev, mtd);
/*假如 mtd->dev 不是 null,则注册设备.这个跟设备节点注册有关*/
if (device_register(&mtd->dev) != 0)
goto fail_added;
if (MTD_DEVT(i))
device_create(&mtd_class, mtd->dev.parent,MTD_DEVT(i) + 1,NULL, "mtd%dro", i);
pr_debug("mtd: Giving out device %d to %s\n", i, mtd->name);

/*遍历 list 链表将每个 mtd_notifier 执行 add()函数,对新加入的 mtd 设备操作,通知所有的 MTD user 新的 MTD 设备的到来 */
list_for_each_entry(not, &mtd_notifiers, list)
not->add(mtd);
mutex_unlock(&mtd_table_mutex);
/* We _know_ we aren't being removed, because our caller is still holding us here. So none of this try_ nonsense, and no bitching about it
either. :) */
__module_get(THIS_MODULE);
return 0;
fail_added:
idr_remove(&mtd_idr, i);
fail_locked:
mutex_unlock(&mtd_table_mutex);
return 1;
}


这 些 添 加 通过 遍 历 这个 队 列 ,然 后 添 加进 来 。 搜索 mtd_notifier 在drivers\mtd\mtd_blkdevs.c,通过这个结构体到了 MTD 设备层了,看他的.add成员。

static struct mtd_notifier blktrans_notifier = {
.add = blktrans_notify_add,
.remove = blktrans_notify_remove,
};


追踪有


static void blktrans_notify_add(struct mtd_info *mtd)
{
struct mtd_blktrans_ops *tr;
if (mtd->type == MTD_ABSENT)
return;
list_for_each_entry(tr, &blktrans_majors, list)
tr->add_mtd(tr, mtd);
}


搜索 tr 的原型 mtd_blktrans_ops 有


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


追踪有


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 (add_mtd_blktrans_dev(dev))
kfree(dev);
}


追踪有


int add_mtd_blktrans_dev(struct mtd_blktrans_dev *new)
{
struct gendisk *gd;
gd = alloc_disk(1 << tr->part_bits);
new->rq = blk_init_queue(mtd_blktrans_request, &new->queue_lock);
gd->queue = new->rq;
add_disk(gd);
}


好了,对块设备驱动有了解的同学就知道到这里,就到了块设备驱动的这一层了。所以一层层下来就跟踪完了。他的流程图如图 11.4 所示:

《网蜂A8实战演练》——9.Linux NAND FLASH 驱动

我们来总结一下,并且为下一节我们自己从零写驱动做下铺垫。在 flash 硬件层,我们需要分配一个 mtd_info 结构体,并针对自己的芯片做一些设置,然后通过 nand_scan()对 mtd_info 的其他一些成员进行配置并选中芯片。然后通过add_mtd_partitions()添加分区,它会根据你传进去的参数,去创建字符型设备节点和块设备节点。

11.3.3 设备节点的创建


通过上面的分析,我们大概知道了整个驱动的调用流程,关于设备节点的创建过程我们完全不知,只是知道 MTD 系统架构帮我们搞定了这些工作。没关系,接下来就让我来告诉大家吧。


第一步: MTD 设备层。

( MTD 子系统)register_chrdev 注册字符型 mtd 设备,并添加该设备到内核,主设备号为90。但是此时还未在/dev 下形成 mtd 设备节点。

第二步: MTD 原始设备层。

( MTD 子系统)class_register 注册一个 mtd 类 mtd_class,后面再注册 mtd 设备时会用到该class。

第三步:驱动层。

(调用接口完成)在添加 MTD 设备同时,在原始设备层注册的那个 class 接口上创建了设备节点。 /dev/mtdXXX 出现

具体代码如下:mtdchar.c


static int __init init_mtdchar(void)
{
int ret;
/*
#define MTD_CHAR_MAJOR 90
#define MTD_BLOCK_MAJOR 31
*/
/*__register_chrdev 注册设备号*/
ret = __register_chrdev(MTD_CHAR_MAJOR, 0, 1 << MINORBITS,"mtd", &mtd_fops);
if (ret < 0) {
pr_notice("Can't allocate major number %d for ""Memory Technology Devices.\n", MTD_CHAR_MAJOR);
return ret;
}
}


mtdcore.c


static int __init init_mtd(void)
{
int ret;
/*注册 mtd_class 设备*/
ret = class_register(&mtd_class);
if (ret)
goto err_reg;
ret = mtd_bdi_init(&mtd_bdi_unmappable, "mtd-unmap");
if (ret)
goto err_bdi1;
ret = mtd_bdi_init(&mtd_bdi_ro_mappable, "mtd-romap");
if (ret)
goto err_bdi2;
ret = mtd_bdi_init(&mtd_bdi_rw_mappable, "mtd-rwmap");
if (ret)
goto err_bdi3;
/* 创建 proc 文件体系接口 "/proc/mtd" */
#ifdef CONFIG_PROC_FS

proc_mtd = proc_create("mtd", 0, NULL, &mtd_proc_ops);
#endif /* CONFIG_PROC_FS */
return 0;
}


mtd_class 是 class 结构体的变量,是设备类,完全是抽象出来的概念,没有对应的实体。所谓设备类,是指提供的用户接口相似的一类设备的集合,常见的设备类的有 block、 tty、 input、 usb 等等。


类是一个设备的高层视图,它抽象出了底层的实现细节,从而允许用户空间使用设备所提供的功能,而不用关心设备是如何连接和工作的。类成员通常由上层代码所控制,而无需驱动的明确支持。但有些情况下驱动也需要直接处理类。在驱动开发中,多使用 mknod 命令手动创建设备节点,但当动态申请设备号时必须通过命令查出设备号,再添加。或者使用 DEVFS 文件系统函数添加设备节点。 DEVFS 在现在 linux 内核中已取消,取而代之的是 UDEV, UDEV 是处于用户态的程序。它根据内核发出的 EVENTS,动态创建事件。内核是通过device_create 发出 event.而在 device_create 中就有一个参数是 class,这个参数最终会在 device_create 中调用:


device_create_vargs()->device_register()->device_add()


调用.device_create_sys_dev_entry 在/sys/dev 目录建立对设备的软链接。所以我采用 mtd_class 类型后,我的 mtd 设备无论有多少个分区都能自动挂载并创建路径。生成 /dev/XXX。这个节点的生成工作在 webee210_ nand_flash.c的 add_mtd_partitions 的函数调用,最后将会将把分区设备都注册到 mtd_class,生成/dev/mtdXXX.设备路径。 具体的函数调动过程如下:


add_mtd_partitions(webee_nand_mtd, webee_nand_part, 3);->

add_mtd_device(&slave->mtd);->

device_create(&mtd_class, mtd->dev.parent, MTD_DEVT(i) + 1, NULL,"mtd%dro", i);->

device_create_vargs()->device_register()->device_add()


11.4 从零编写 NAND 设备驱动


通过上一节基于整个驱动的分析,这一节将针对 atmel_nand.c 的做个总结,介绍一下 atmel_nand_probe 函数做了哪些事情,有兴趣的读者请自行分析drivers/mtd/nand/atmel.c


1. 分配一个 nand_chip 结构体

2. 进行 IO 的映射

3. 让 mtd->prvi 指向 nand->chip
4. 对 nand->chip 进行设置

5. 提供选中芯片,发命令,发地址,读数据,写数据,等待等操作

6. 设置相应 GPIO 管脚用于 Nand

7. 使能时钟

8. 扫描选中芯片并进行默认设置9. 添加分区

11.4.1 图说 NAND FLASH 设备驱动编写流程


1. 分配一个 nand_chip 结构体

2. 设置 nand_chip 结构体成员

3. 硬件相关的操作4. 使用5. 添加分区

《网蜂A8实战演练》——9.Linux NAND FLASH 驱动

11.4.2 从零编写 LCD 设备驱动分析


NAND FLASH 设备驱动源码路径为:

webee210_drivers\12th_ nand_flash \webee210_ nand_flash.c


NAND FLASH 裸机源码路径为: 210_code\9.nand_flash

11.4.2.1 入口函数


本驱动并没有使用平台总线驱动设备模型,直接在一个文件上使用了 NANDFLASH 设备驱动,入口函数完成了上图的大部分工作。1. 分配一个 nand_chip 结构体,并进行 IO 的映射


webee_nand_chip = kzalloc(sizeof(struct nand_chip),GFP_KERNEL);
nand_regs = ioremap(0xB0E00000,sizeof(struct nand_regs));
mp0_3con = ioremap(0xE0200320,4);
clk_gate_ip1 = ioremap(0xE0100464,4);
clk_gate_block = ioremap(0xE0100480,4);


2.1 设置 nand_chip 结构体成员


/*
* 初始化 nand_chip 结构体中的函数指针
* 提供选中芯片,发命令,发地址,读数据,写数据,等待等操作
*/
webee_nand_chip->select_chip = webee_nand_select_chip;
webee_nand_chip->cmd_ctrl = webee_nand_cmd_ctrl;
webee_nand_chip->IO_ADDR_R = &nand_regs->nfdata;
webee_nand_chip->IO_ADDR_W = &nand_regs->nfdata;
webee_nand_chip->dev_ready = webee_nand_dev_ready;
webee_nand_chip->ecc.mode = NAND_ECC_SOFT;
/*这些操作可以在源码文件中看不懂的可以参考之前的裸机程序,这里不再
重复*/


3.1 设置硬件相关 -- 时钟


/*使能时钟*/
*clk_gate_ip1 = 0xffffffff;
*clk_gate_block = 0xffffffff;;


3.2 设置硬件相关 -- GPIO 管脚
/* 设置相应 GPIO 管脚用于 Nand */
*mp0_3con = 0x22222222;;


3.3 设置硬件相关--时序


/* 设置时序 */
#define TWRPH1 1
#define TWRPH0 1
#define TACLS 1
nand_regs->nfconf |= (TACLS<<12) | (TWRPH0<<8) | (TWRPH1<<4);
/* AddrCycle[1]:1 = 发送地址需要 5 个周期 */
nand_regs->nfconf |= 1<<1;

/*
* MODE[0]:1 = 使能 Nand Flash 控制器
* Reg_nCE0[1]:1 = 取消片选
*/
nand_regs->nfcont |= (1<<1)|(1<<0)


4. 使用


/*使用*/
webee_nand_mtd = kzalloc(sizeof(struct mtd_info), GFP_KERNEL);
webee_nand_mtd->owner = THIS_MODULE;
webee_nand_mtd->priv = webee_nand_chip;
nand_scan(webee_nand_mtd, 1);


5.添加分区


add_mtd_partitions(webee_nand_mtd, webee_nand_part, 4);
/* 函数原型如下:
* 第一个参数为: mtd->info 结构体
* 第二个参数为: mtd_partition,这个结构存放了你的分区信息:名字和大小
* 第三个参数为: int 你实际分区的个数
*/


11.4.2.2 出口函数


出口函数还是一如既往的, Webee 总喜欢把它形容为“聪明的跟屁虫”,为什么这么说呢?因为出口函数总喜欢做入口函数相反的事情,如果非要文艺点形容的话,还是那句话,做人要有始有终嘛~~


static void webee_nand_exit(void)
{
nand_release(webee_nand_mtd);
kfree(webee_nand_mtd);
iounmap(nand_regs);
kfree(webee_nand_chip);
}


11.4.3 NAND FLASH 设备驱动的测试


Nand flaah 的测试程序在这里就不自己写了,借用一个小工具就可以了,这个工具叫做 utils,具体使用方法看以下过程。


1.重新配置一个不带 nand_flash 的内核配置


在 webee_config 的配置下 make menuconfig 去掉下面这项

-> Device Drivers

   -> Memory Technology Device (MTD) support (MTD [=y])

        -> NAND Device Support (MTD_NAND [=y])

             < > NAND Flash support for Webee210

2.重新编译内核


make uImage -j 2


编译得到新的内核,烧写进 nand flash看内核启动信息,确定内核没有 nand flash 驱动


3.设置 NFS,并从 nfs 启动根文件系统

注意:做这个实验请先确定你的 nfs 环境搭建好了再做


3.1 PC 机(Ubuntu 13.04):


sudo ifconfig eth0 192.168.1.111
sudo /etc/init.d/nfs-kernel-server restart


3.2 webee210 板子:


set serverip 192.168.1.111
set ipaddr 192.168.1.155
set gateway 192.168.1.1
set gatewayip 192.168.1.1
setenv bootargs noinitrd root=/dev/nfs nfsroot=192.168.1.111:你的根目录,tcp
ip=192.168.1.155:192.168.1.111:192.168.1.1:255.255.255.0::eth0:off
init=/linuxrc console=ttySAC0,115200
saveenv


4.编译工具(在 PC 机操作):


4.1.


tar xjf mtd-utils-05.07.23.tar.bz2


4.2.


cd mtd-utils-05.07.23/util


修改 Makefile:#CROSS=arm-linux-改为CROSS=arm-linux-4.3.


make


4.4.


cp flash_erase flash_eraseall /your_rootfs/bin/


5.insmod 编译好的驱动模块(以下操作都在开发板上进行)


insmod webee_nand.ko


注意要把 webee_nand.ko 拷贝进根文件系统里面,在开发板上操作。

6. 格式化 (参考下面编译工具)


flash_eraseall /dev/mtd3


7. 挂接


mkdir /mnt
mount -t yaffs /dev/mtdblock3 /mnt


8. 在/mnt 目录下建文件


vi webee_nand_test


随便写一些内容,比如 hello nand flash9.


umont /mnt


10. 重新启动重新挂载


insmod webee_nand.ko
mount -t yaffs /dev/mtdblock3 /mnt


查看刚刚创建的文件,如果内容和你之前写的一样说明成功了。

11.5 本章小结


本章重点讲解了 Linux 下的 NAND FLASH 驱动的架构与编程方法,介绍了NAND FLASH 驱动的整体结构,还分析了 MTD 设备驱动的几个重要的函数。通过学习 MTD 子系统,能够更好的理解如何编写 NAND FLASH 设备驱动。接着,我们还讲解了如何从零编写一个 NAND FLASH 设备驱动。有图有真相哟~~~我们重点讲解的是它的编程框架,对于寄存器的设置我们并没有详细分析了,读者可参考以前的裸机源码。这表明什么?表明学习 Linux 驱动,主要抓层次抓框架,而不是细节。