版本比较
标识
- 该行被添加。
- 该行被删除。
- 格式已经改变。
Nandflash原理解析
Nandflash概述
Nandflash在嵌入式系统中用于充当硬盘的角色,用于保存内核代码、应用程序、文件系统和数据资料。根据物理结构上的区别,Nandflash主要分为如下两类:
- SLC:单层式存储
- MLC:多层式存储
SLC在存储格上只存1位数据,而MLC则存放两位数据。对比SLC和MLC,存在以下差异:
- 价格:由于MLC采用了更高密度的存储方式,因此同容量的MLC价格上远低于SLC
- 访问速度:SLC的访问速度一般要比MLC快3倍以上
- 使用寿命:SLC能进行10 万次的擦写,MLC能进行1万次
- 功耗:MLC功耗比SLC高15%左右
与内存不同,Nandflash不参与统一编址,对Nandflash的读写需要通过Nandflash控制器才可以进行,Nandflash控制器位于CPU内部,Nandflash控制器提供地址寄存器,命令寄存器,数据寄存器等寄存器用于用户访问Nandflash。
Nandflash结构
Nandflash存储器由块(block)构成,块的基本单位是页(page)。通常来说,每一个block由16,32或64个page组成。大多数的Nandflash每一个page内包含512个字节的Data area(数据存储区域),还有一个扩展的16字节的Spare area(备用区域,也叫冗余区域(redundant area),而Linux系统中一般称之为OOB(Out Of Band)),备用区域用于存储校验之类的信息。这样,一个页的大小为512+26=528字节,我们称这样的page为small page。
大容量的(1GB或更多)的Nandflash的单页容量会更大一些,Data area大小为2048字节,Spare area大小为64字节。
关于oob具体用途,总结起来有:
- 标记是否是坏快
- 存储ECC数据
- 存储一些和文件系统相关的数据。如jffs2就会用到这些空间存储一些特定信息,而yaffs2文件系统,会在oob中,存放很多和自己文件系统相关的信息。
关于坏块,Nandflash中,一个块中含有1个或多个位是坏的,就称其为坏块。注意最小单位是块(Block),而不是页。坏块的标记方式,对于现在常见的页大小为2K的Nandflash,如果块中的第一个页的oob的第一个字节不是0xFF,就说明是坏块。
下面是OK6410采用的Nandflash芯片的结构描述:
由此可以,此芯片包含4096个块,每块有128个页,每页上有4KB的数据,一共是:
4096_128_4KB=2GB
这与OK6410的硬件描述是吻合的。
读写方式
Nandflash以页为单位进行读写,而以块为单位进行擦除。并且,Nandflash芯片的每一位(bit)只能从1变为0,而不能从0变成1,所以在对其进行写入操作之前一定要将相应块进行擦除(将块的数据全变成1)。
Nandfalsh的寻址分为行地址(Row address)和列地址(Column address)。行地址就是Nandflash中页的地址,以上面这块芯片为例,一共有512K个页,所以其行地址有19位(A13-A31),2^19=512K。而列地址则是单个页内的偏移地址,上面这块芯片单页有4KB的存储空间,2^12=4K,由于还有218B的备用区域,所以有13位的列地址(A0-A12)。由于芯片提供的数据线宽度是8bit,所以这些地址要分5个Cycle进行写入。
要实现对Nandflash的操作需要了解Nandflash的命令,如下图所示:
大部分的命令都需要分两次发送,第二次发送可以看成是对第一次命令的确认,这是为了避免由于干扰导致命令码出错的情况。
信号引脚
- I/O0~IO7:用于输入地址/数据/命令,输出数据
- CLE(Command Latch Enable):命令锁存使能,在输入命令之前,要先在模式寄存器中,设置CLE使能
- ALE(Address Latch Enable):地址锁存使能,在输入地址之前,要先在模式寄存器中,设置ALE使能
- CE:芯片使能,在操作Nandflash之前,要先选中此芯片,才能操作
- RE:读允许,在读取数据之前,要先使RE有效
- WR:写允许,在写入数据之前,要先使WE有效
- WP:在写或擦除期间,提供写保护
- R/B:就绪/忙,主要用于在发送完编程/擦除命令后,检测这些操作是否完成,忙表示操作仍在进行,就绪表示操作完成
注意,数据手册中,如果某个引脚定义上面带一横杠或是后面跟一个#,那说明此引脚/信号是低电平有效。如果字母头上啥都没有,就是默认的高电平有效。
Nandflash驱动设计-读
下面实现Nandflash的读取一个页的函数接口,函数需要提供两个参数,一个是页的地址,另一个是缓冲区的地址,如下:
代码块 | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
void NF_page_read(unsigned long addr, unsigned char *buff); |
根据以上关于Nandflash的命令集合介绍可知,读取数据使用Read命令,该命令需要两个周期,第一个周期发0x00,第二个周期发0x30,参考读的时序图亦可验证:
下面摘取一段网上关于Nandflash的时序解读【详解】如何编写Linux下Nand Flash驱动:
黄色竖线所处的时刻,是在发送读操作的第一个周期的命令0x00之前的那一刻。
让我们看看,在那一刻,其所穿过好几行都对应什么值,以及进一步理解,为何要那个值。
- 黄色竖线穿过的第一行,是CLE。还记得前面介绍命令所存使能(CLE)那个引脚吧?CLE,将CLE置1,就说明你将要通过I/O复用端口发送进入Nand Flash的,是命令,而不是地址或者其他类型的数据。只有这样将CLE置1,使其有效,才能去通知了内部硬件逻辑,你接下来将收到的是命令,内部硬件逻辑,才会将受到的命令,放到命令寄存器中,才能实现后面正确的操作,否则,不去将CLE置1使其有效,硬件会无所适从,不知道你传入的到底是数据还是命令了。
- 而第二行,是CE#,那一刻的值是0。这个道理很简单,你既然要向Nand Flash发命令,那么先要选中它,所以,要保证CE#为低电平,使其有效,也就是片选有效。
- 第三行是WE#,意思是写使能。因为接下来是往Nand Flash里面写命令,所以,要使得WE#有效,所以设为低电平。
- 第四行,是ALE是低电平,而ALE是高电平有效,此时意思就是使其无效。而对应地,前面介绍的,使CLE有效,因为将要数据的是命令(此时是发送图示所示的读命令第二周期的0x30),而不是地址。如果在其他某些场合,比如接下来的要输入地址的时候,就要使其有效,而使CLE无效了。
- 第五行,RE#,此时是高电平,无效。可以看到,知道后面低6阶段,才变成低电平,才有效,因为那时候,要发生读取命令,去读取数据。
- 第六行,就是我们重点要介绍的,复用的输入输出I/O端口了,此刻,还没有输入数据,接下来,在不同的阶段,会输入或输出不同的数据/地址。
- 第七行,R/B#,高电平,表示R(Ready)/就绪,因为到了后面的第5阶段,硬件内部,在第四阶段,接受了外界的读取命令后,把该页的数据一点点送到页寄存器中,这段时间,属于系统在忙着干活,属于忙的阶段,所以,R/B#才变成低,表示Busy忙的状态的。
介绍了时刻①的各个信号的值,以及为何是这个值之后,相信,后面的各个时刻,对应的不同信号的各个值,大家就会自己慢慢分析了,也就容易理解具体的操作顺序和原理了。
参考Nandflash的读取时序图和6410芯片手册中关于Nandflash控制器的章节描述,总结出以下Nandflash的读取步骤:
- 设置片选信号,选中Nandflash芯片,使用NFCONT寄存器的bit1,将其设置为0
- 清除RnB信号,这是为了后面等待RnB信号就绪,使用NFSTAT的bit4,将其设置为1
- 发送命令0x00,使用寄存器NFCMMD
- 发送列地址,由于读取的是整页,所以列地址为零即可,分再次发送,使用寄存器NFADDR
- 发送行地址,也就是要读取页的首地址,分三次发送
- 发送命令0x30
- 等待就绪,即等待RnB信号由低电平变为高电平
- 读取数据,读取一个完整的页,使用寄存器NFDATA
相关程序如下:
代码块 | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
#define NFCONT *((unsigned volatile long *)0x70200004) #define NFSTAT *((unsigned volatile char *)0x70200028) #define NFCMMD *((unsigned volatile char *)0x70200008) #define NFADDR *((unsigned volatile char *)0x7020000C) #define NFDATA *((unsigned volatile char *)0x70200010) void select_chip() { NFCONT &= ~(1 << 1); } void deselect_chip() { NFCONT |= (1 << 1); } void clear_RnB() { NFSTAT |= (1 << 4); } void nand_cmd(unsigned char cmd) { NFCMMD = cmd; } void wait_RnB() { while(!(NFSTAT & 0x1)); } void nand_addr(unsigned char addr) { NFADDR = addr; } void NF_page_read(unsigned long addr, unsigned char *buff) { int i; /*选中芯片*/ select_chip(); /*清除RnB*/ clear_RnB(); /*发送命令0x00*/ nand_cmd(0x00); /*发送列地址*/ nand_addr(0x00); /*读取整页时列地址为0*/ nand_addr(0x00); /*发送行地址*/ nand_addr(addr & 0xff); /*行地址用于表示页的编号,分三次发送,每次发送8位*/ nand_addr((addr >> 8) & 0xff); nand_addr((addr >> 16) & 0xff); /*发送命令0x30*/ nand_cmd(0x30); /*等待就绪*/ wait_RnB(); /*读数据*/ for(i = 0; i < 1024 * 4; i++) /*OK6410使用的Nandflash芯片单页大小为4KB*/ { buff[i] = NFDATA; } /*取消片选*/ deselect_chip(); } |
除此之外,为了正常使用Nandflash,还应该先对Nandflash进行初始化,这包括三个步骤:
- 设置Nandflash的时间参数,使用寄存器NFCONF,需要设置的位是TACLS, TWRPH0, TWRPH1。这三个位的含义在6410芯片手册第8.4小节中有定义,如下图所示:
再参考Nandflash芯片手册中的示意图和表格:
综上可以得出以下计算过程:
tWP = 15ns
TCLS = 15ns
TCLH = 5ns
HCLK的频率为100MHz,周期为10ns,根据计算公式,有以下计算过程:
TACLS的值计算: HCLK _TACLS > TCLS - tWP = 0 ns , TACLS = 1即可
TWRPH0的值计算:HCLK_ (TWRPH0 + 1) > tWP = 15ns,TWRPH0 = 2即可
TWRPH1的值计算:HCLK * (TWRPH1 + 1) > TCLH = 5ns,TWRPH1 = 1即可 - 使能Nandflash controller,使用寄存器NFCONT的bit0,顺便使片选信号失效
- 复位Nandflash,发送命令0xff,等待芯片就绪即可,时序图如图所示:
相关代码如下:
代码块 | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
#define NFCONF *((volatile unsigned long *)0x70200000) void nand_reset(void) { /* 选中 */ select_chip(); /* 清除RnB */ clear_RnB(); /* 发出复位信号 */ nand_cmd(0xff); /* 等待就绪 */ wait_RnB(); /* 取消选中 */ deselect_chip(); } void nand_init() { #define TACLS 1 #define TWRPH0 2 #define TWRPH1 1 NFCONF &= ~((7<<12)|(7<<8)|(7<<4)); NFCONF |= (TACLS<<12)|(TWRPH0<<8)|(TWRPH1<<4); /* 使能 nandflash controller*/ NFCONT = 1 | (1<<1); /* 复位 */ nand_reset(); } |
最后,在实现Nandflash的读取函数后,就可以在启动阶段从Nandflash中拷贝代码到内存了,我们对代码作以下修改,首先增加一个函数用于代码拷贝:
代码块 | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
void nand_to_ram(unsigned long start_addr, unsigned char *sdram_addr, int size) { int i; for(i = 0; i < 4; i++, sdram_addr += 4096) { NF_page_read(i, sdram_addr); } } |
然后,修改启动代码start.S,首先把栈的初始化代码提前,然后执行拷贝代码的函数:
代码块 | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
reset: bl set_svc bl set_peri_port bl disable_watchdog bl disable_interrupt bl disable_mmu bl clock_init bl mem_init bl stack_init @栈的初始化要位于代码拷贝之前,因为新的代码拷贝使用了C语言编写 bl nand_init bl copy_to_ram bl clear_bss @ bl light_led ldr pc, =gboot_main @跳转到c语言中去执行 |
代码块 | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
copy_to_ram: mov r0, #0 @nand_to_ram的参数通过r0 r1 r2来传递 ldr r1, =_start ldr r2, =bss_end sub r2, r2, r1 @计算代码的长度 mov ip, lr @保存当前语句返回地址 bl nand_to_ram @从nandflash中拷贝代码 mov lr, ip mov pc, lr |
Nandflash驱动设计-写
下面实现Nandflash的按页写功能,函数原型如下:
代码块 | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
/*向addr所指的页写入一个page大小的数据*/ void NF_page_write(unsigned long addr, unsigned char *buff); |
查找Nandflash中关于数据写入的时序描述,如下所示:
总结数据写入的步骤,包括以下几步:
- 选中flash芯片
- 清除RnB
- 发送命令0x80
- 发送列地址
- 发送行地址
- 写入数据
- 发送命令0x10
- 等待RB信号
- 发送命令0x70
- 读取写入结果
- 取消选中flash芯片
代码实现如下:
代码块 | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
/*向addr所指的页写入一个page大小的数据*/ int NF_page_write(unsigned long addr, unsigned char *buff) { int i, ret; /*选中芯片*/ select_chip(); /*清除RnB*/ clear_RnB(); /*发送命令0x80*/ nand_cmd(0x80); /*发送列地址*/ nand_addr(0x00); nand_addr(0x00); /*发送行地址*/ nand_addr(addr & 0xff); nand_addr((addr >> 8) & 0xff); nand_addr((addr >> 16) & 0xff); /*写入数据*/ for(i = 0; i < 1024 * 4; i++) { NFDATA = buff[i]; } /*发送命令0x10*/ nand_cmd(0x10); /*等待RnB*/ wait_RnB(); /*发送命令0x70*/ nand_cmd(0x70); /*读取写入结果*/ ret = NFDATA; /*取消芯片选中信号*/ deselect_chip(); return ret; } |
注意,Nandflash在写入之前必须要擦除对应的区域,所以还需要实现Nandflash的擦除操作,其时序图如下:
擦除的操作是以块为单位进行的,步骤如下:
- 选中flash芯片
- 清除RnB
- 发送命令0x60
- 发送行地址(块的地址)
- 发送命令0xd0
- 等待RnB
- 发送命令0x70
- 读取擦除结果
- 取消片选信号
代码实现如下:
代码块 | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
int NF_erase(unsigned addr) { int ret; /*选中芯片*/ select_chip(); /*清除RnB*/ clear_RnB(); /*发送命令0x60*/ nand_cmd(0x60); /*发送行地址*/ nand_addr(addr & 0xff); nand_addr((addr >> 8) & 0xff); nand_addr((addr >> 16) & 0xff); /*发送命令0xd0*/ nand_cmd(0xd0); /*等待RnB*/ wait_RnB(); /*发送命令0x70*/ nand_cmd(0x70); /*读取擦除结果*/ ret = NFDATA; /*取消芯片选中信号*/ deselect_chip(); return ret; } |
目录 |
---|