Nandflash在嵌入式系统中用于充当硬盘的角色,用于保存内核代码、应用程序、文件系统和数据资料。根据物理结构上的区别,Nandflash主要分为如下两类:
SLC在存储格上只存1位数据,而MLC则存放两位数据。对比SLC和MLC,存在以下差异:
与内存不同,Nandflash不参与统一编址,对Nandflash的读写需要通过Nandflash控制器才可以进行,Nandflash控制器位于CPU内部,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具体用途,总结起来有:
关于坏块,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的命令,如下图所示:
大部分的命令都需要分两次发送,第二次发送可以看成是对第一次命令的确认,这是为了避免由于干扰导致命令码出错的情况。
注意,数据手册中,如果某个引脚定义上面带一横杠或是后面跟一个#,那说明此引脚/信号是低电平有效。如果字母头上啥都没有,就是默认的高电平有效。
下面实现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的读取步骤:
相关程序如下:
#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进行初始化,这包括三个步骤:
相关代码如下:
#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的按页写功能,函数原型如下:
/*向addr所指的页写入一个page大小的数据*/ void NF_page_write(unsigned long addr, unsigned char *buff); |
查找Nandflash中关于数据写入的时序描述,如下所示:
总结数据写入的步骤,包括以下几步:
代码实现如下:
/*向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的擦除操作,其时序图如下:
擦除的操作是以块为单位进行的,步骤如下:
代码实现如下:
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; } |