2015年(109)
分类: LINUX
2015-01-23 16:37:58
原文地址:sep4020的nand驱动开发 作者:myleeming
花了近三个星期,终于和阿虚一起将sep4020上的nand驱动给搞定了。
第一部分:MTD
首先是花了好几天来了解nand以及在linux下nand的开发,在linux中使用了一个mtd层来作为具体的硬件设备驱动和上层文件系统的桥梁。.mtd给出了系统中所有mtd设备(nand,nor,diskonchip)的统一组织方式。
使用mtd层的好处有:
●1.我们要做mtd设备的驱动模块,所需要做的事情就是去填满那些公共接口函数的实际内容。有了mtd,在设计驱动模块的时候,不用去理会字符(块)设备驱动设计标准,因为所有这些复杂的与内核的交互接口机制mtd已经替我们做好了,我们只需要实现对物理设备的范围控制。——这是对于写驱动的人来说的方便。
●2.上层应用只需要访问mtd抽象层提供的字符设备方式或者块设备方式来访问mtd设备,因此具体驱动对于上层应用来说是具有独立性的,即使底层驱动修改了,上层拥有也不需要改动。并且由于mtd抽象层,上层应用就可以避免直接对具体硬件操作,而是对mtd操作,这样的话这些应用就不是建立在某个具体的设备上,更好的实现了通用性和兼容性——这是对于上层应用来说的方便。
关于mtd层:
●.mtd抽象层用一个数组struct mtd_info *mtd_table[MAX_MTD_DEVICES]保存系统中所有的设备,mtd设备利用struct mtd_info 这个结构来描述,该结构中描述了存储设备的基本信息和具体操作所需要的内核函数,mtd系统的那个机制主要就是围绕这个结构来实现的。下面简单介绍下这个结构:
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区在页内的位置,对于512字节一页的nand来说是512
u_int32_t oobsize; // oob区的大小,对于512字节一页的nand来说是16
u_int32_t ecctype;
u_int32_t eccsize;
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; //设备私有数据指针,对于nand Flash来说指向,nand芯片的结构
第二部分:具体的Nand Flash驱动
搞清楚了MTD,内核,nandflash设备驱动的关系后,现在就是如何编写针对我们这款处理器的驱动,首先介绍一下nandflash结构struct nand_chip
●. struct nand_chip {
void __iomem *IO_ADDR_R; //这是nandflash的读写寄存器,对于我们的芯片是
void __iomem *IO_ADDR_W; //EMI_NAND_DATA
//以下都是nandflash的操作函数,这些函数将根据相应的配置进行重载,也是在nand_scan这个函数中,nand_scan这个函数非常重要,在下文讲详细阐述
u_char (*read_byte)(struct mtd_info *mtd);
void (*write_byte)(struct mtd_info *mtd, u_char byte);
u16 (*read_word)(struct mtd_info *mtd);
void (*write_word)(struct mtd_info *mtd, u16 word);
void (*write_buf)(struct mtd_info *mtd, const u_char *buf, int len);
void (*read_buf)(struct mtd_info *mtd, u_char *buf, int len);
int (*verify_buf)(struct mtd_info *mtd, const u_char *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 (*hwcontrol)(struct mtd_info *mtd, int cmd);
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, int state);
int (*calculate_ecc)(struct mtd_info *mtd, const u_char *dat, u_char *ecc_code);
int (*correct_data)(struct mtd_info *mtd, u_char *dat, u_char *read_ecc, u_char *calc_ecc);
void (*enable_hwecc)(struct mtd_info *mtd, int mode);
void (*erase_cmd)(struct mtd_info *mtd, int page);
int (*scan_bbt)(struct mtd_info *mtd);
int eccmode; //ecc是软件校验?硬件校验?无?
int chip_delay; //芯片时序延迟参数
int page_shift; //页偏移,对于512/页的,一般是9
u_char *data_buf; //数据缓存区
(其它参数解释详解linux内核源码)
●其实我们需要做的事情,就是将上述这些函数,根据我们sep4020的特性,将其重载。
主要重载的函数有:
this->hwcontrol = sep4020_hwcontrol;
这是一个硬件操作函数,由于在nandflash中对寄存器的操作都是通过IO_ADDR_R和IO_ADDR_W来实现的,而我们需要根据需要在地址寄存器,数据寄存器,命令寄存器,ID寄存器,状态寄存器之间切换,通过这个函数来实现对IO_ADDR_R和IO_ADDR_W的变动。
this->dev_ready = sep4020_nand_dev_ready;
nandflash是否完成的函数,通过读取EMI_NAND_IDLE寄存器位来判断。
this->write_buf = sep4020_nand_write_buf;
Nandflash的写函数,nandflash的最基本也是最重要的命令之一。
this->read_buf = sep4020_nand_read_buf;
Nanflash的读函数,功能同上。
this->write_byte = sep4020_nand_write_byte;
this->read_byte = sep4020_nand_read_byte;
实际这两个函数实现了读取和写入寄存器一个byte的功能,
this->eccmode = NAND_ECC_SOFT;//软件ecc校验
this->select_chip = sep4020_nand_select_chip;//无此功能,空留
this->cmdfunc = sep4020_nand_command;
在linux中对nand的操作都是通过两个函数来实现的,一个是发命令也就是此处的cmdfuc,还有一个就是读写函数,由于sep4020nandflash控制器的特殊,需要将这个函数重载(下文会介绍我们nandflash的特性)
this->erase_cmd = sep4020_nand_cmd_erase;
擦除命令,理由同上。
●关于nand_scan函数
Nand_scan是在初始化nand的时候对nand进行的一步非常好重要的操作,在nand_scan中会对我们所写的关于特定芯片的读写函数重载到nand_chip结构中去,并会将mtd_info结构体中的函数用nand的函数来重载,实现了mtd到底层驱动的联系。
并且在nand_scan函数中会通过读取nand芯片的设备号和厂家号自动在芯片列表中寻找相应的型号和参数,并将其注册进去。
第三部分:根据sep4020编写具体nand驱动
sep4020是一款非常好优秀的arm720T芯片,以下介绍一下sep4020的nand驱动编写
1. sep4020的nand控制器只支持dma搬运,这也是考虑到一般对nand进行读写都是一页一页的,所以利用dma可以有效的提高效率。
DMA也带了一个比较重要的问题,DMA的源和目的需要用实地址,因为DMA不走cpu,在这点上我卡了好久,在linux中久了,什么都习惯性的填写虚地址,结果在测试的时候发现dma怎么都不工作,把所有的东西都看了又看,都没问题,郁闷的不行了,最后在张师兄的提醒下才发现DMA不能给虚地址,恍然大悟啊!
2. 但是由于直接给DMA又带来了另外一个问题,在操作系统中,所有的地址都是虚拟地址,如何得到实际物理地址?我记得在当初在内核移植的时候写过一个函数:
#define __virt_to_bus(x) __virt_to_phys(x) ,跟进去一看解释是物理地址,虚拟地址的转换,此时我认为操作系统的地址转换就是通过这个来实现的,因此在这里我直接给DMA赋了一个没用到的物理空间0x30002000,根据上式则应该对应的虚拟地址是0xc0002000。
我也这么做了,发现可以实现,但是总是会出现一些莫名其妙的错误,我和阿虚都快崩溃了,最后发现其实这种读法是不正确的,我们在进行同一页重复性读取实验中,10次中有一次不正确,排除了所有其他可能的因素,最后又怀疑到操作系统是不是真的这样将虚实地址的?操作系统可操作的地址空间应该有
由于第一次在操作系统中使用dma,我们边做边摸索,以后绝对不在dma上再吃这么多跟头了,呵呵。
3 sep4020中所有的寄存器都是32位的,这点也是和其他芯片的nand控制所不同的,打个比方:我们需要给nand控制器的地址寄存器一个地址,我们是直接将地址赋给这个32位的寄存器,但是对于其他的芯片(比如2410),它的地址寄存器用8位,需要你串行的将地址依次输入进去,这点是需要我们注意的。
还有就是我们的nand控制器的命令寄存器比较特殊,它有个使能位,芯片手册推荐配好地址寄存器后,再配命令寄存器,同时使能,但由于尽量不变动linux的内核源码,我使用的是先按照mtd设备中通用的做法,发命令,发地址,然后在读写函数的最后一条指令将nand使能。
4. 有一点比较重要,sep4020的nand控制器只能528字节的写,而在linux中大量采用了先写512再写16,甚至有单独写16的函数,因此这里必须做改动。我们采用了先读后写的方法,比如写16字节(写16肯定是最后的00b区),我们先读出这一页的所有528字节,将前面的512取出再接上我们要写的16字节,这样凑满了528字节后写入。
为了实现在读写的速度更快,我们根据nand的特性进行分析,在写512字节的时候,那一页肯定是空白的,未被使用过的,因此它的oob区也肯定是全ff,因此在写512时我们将后面剩余的16字节自动填充ff,这样就省去了从nand读出的步骤,大大加快了nand写的速度。
5. 奇怪的write_byte
在我们调试的时候曾发现过一个问题,给nand发了一条写命令0x80,但是此时还未使能,但通过printk发现此时nand控制器一直处于忙状态,最后通过一步步的锁定,发现在给nand发了0x80给我们的com寄存器,但是此时printk这个寄存器的值竟然是0x80808080,而我们的第一位恰好是使能位,因此此时由于调用系统的write_byte,com寄存器从本应的0x00000080变成了0x80808080,导致了我们nand控制一直是忙,无法进行下面的操作。最后无奈之下重载了write_byte和read_byte,问题就解决了,很奇怪的是我也跟进去看过系统的代码,和我的一样啊,怎么到它那儿就这样了呢?现在觉得有可能的是:write_byte的寄存器一般都是8位的,而我们的寄存器是32位的,因此系统连续赋了4次,这仅仅是我的一种猜测,等待以后验证。
6. 编写中碰上的一些非常傻的问题:
*(unsigned long *)EMI_NAND_COM 和EMI_NAND_COM混淆了,还是指针学的不好。
出现一些很古怪的问题的时候,一定要记得检查你的头文件
切记保证你的make menuconfig中的选项正确
7.不能将yaffs 中的Turn off debug chunk erase check选上,不然会出现mtd ecc chunk的问题