Nandflash原理解析

Nandflash概述

Nandflash在嵌入式系统中用于充当硬盘的角色,用于保存内核代码、应用程序、文件系统和数据资料。根据物理结构上的区别,Nandflash主要分为如下两类:

  • SLC:单层式存储
  • MLC:多层式存储

SLC在存储格上只存1位数据,而MLC则存放两位数据。对比SLC和MLC,存在以下差异:

  1. 价格:由于MLC采用了更高密度的存储方式,因此同容量的MLC价格上远低于SLC
  2. 访问速度:SLC的访问速度一般要比MLC快3倍以上
  3. 使用寿命:SLC能进行10 万次的擦写,MLC能进行1万次
  4. 功耗: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具体用途,总结起来有:

  1. 标记是否是坏快
  2. 存储ECC数据
  3. 存储一些和文件系统相关的数据。如jffs2就会用到这些空间存储一些特定信息,而yaffs2文件系统,会在oob中,存放很多和自己文件系统相关的信息。

关于坏块,Nandflash中,一个块中含有1个或多个位是坏的,就称其为坏块。注意最小单位是块(Block),而不是页。坏块的标记方式,对于现在常见的页大小为2K的Nandflash,如果块中的第一个页的oob的第一个字节不是0xFF,就说明是坏块。

下面是OK6410采用的Nandflash芯片的结构描述:
Alt text

由此可以,此芯片包含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的命令,如下图所示:
Alt text

大部分的命令都需要分两次发送,第二次发送可以看成是对第一次命令的确认,这是为了避免由于干扰导致命令码出错的情况。

信号引脚

Alt text

  • 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,参考读的时序图亦可验证:
Alt text

下面摘取一段网上关于Nandflash的时序解读【详解】如何编写Linux下Nand Flash驱动
Alt text

黄色竖线所处的时刻,是在发送读操作的第一个周期的命令0x00之前的那一刻。
让我们看看,在那一刻,其所穿过好几行都对应什么值,以及进一步理解,为何要那个值。

  1. 黄色竖线穿过的第一行,是CLE。还记得前面介绍命令所存使能(CLE)那个引脚吧?CLE,将CLE置1,就说明你将要通过I/O复用端口发送进入Nand Flash的,是命令,而不是地址或者其他类型的数据。只有这样将CLE置1,使其有效,才能去通知了内部硬件逻辑,你接下来将收到的是命令,内部硬件逻辑,才会将受到的命令,放到命令寄存器中,才能实现后面正确的操作,否则,不去将CLE置1使其有效,硬件会无所适从,不知道你传入的到底是数据还是命令了。
  2. 而第二行,是CE#,那一刻的值是0。这个道理很简单,你既然要向Nand Flash发命令,那么先要选中它,所以,要保证CE#为低电平,使其有效,也就是片选有效。
  3. 第三行是WE#,意思是写使能。因为接下来是往Nand Flash里面写命令,所以,要使得WE#有效,所以设为低电平。
  4. 第四行,是ALE是低电平,而ALE是高电平有效,此时意思就是使其无效。而对应地,前面介绍的,使CLE有效,因为将要数据的是命令(此时是发送图示所示的读命令第二周期的0x30),而不是地址。如果在其他某些场合,比如接下来的要输入地址的时候,就要使其有效,而使CLE无效了。
  5. 第五行,RE#,此时是高电平,无效。可以看到,知道后面低6阶段,才变成低电平,才有效,因为那时候,要发生读取命令,去读取数据。
  6. 第六行,就是我们重点要介绍的,复用的输入输出I/O端口了,此刻,还没有输入数据,接下来,在不同的阶段,会输入或输出不同的数据/地址。
  7. 第七行,R/B#,高电平,表示R(Ready)/就绪,因为到了后面的第5阶段,硬件内部,在第四阶段,接受了外界的读取命令后,把该页的数据一点点送到页寄存器中,这段时间,属于系统在忙着干活,属于忙的阶段,所以,R/B#才变成低,表示Busy忙的状态的。

介绍了时刻①的各个信号的值,以及为何是这个值之后,相信,后面的各个时刻,对应的不同信号的各个值,大家就会自己慢慢分析了,也就容易理解具体的操作顺序和原理了。

参考Nandflash的读取时序图和6410芯片手册中关于Nandflash控制器的章节描述,总结出以下Nandflash的读取步骤:

  1. 设置片选信号,选中Nandflash芯片,使用NFCONT寄存器的bit1,将其设置为0
  2. 清除RnB信号,这是为了后面等待RnB信号就绪,使用NFSTAT的bit4,将其设置为1
  3. 发送命令0x00,使用寄存器NFCMMD
  4. 发送列地址,由于读取的是整页,所以列地址为零即可,分再次发送,使用寄存器NFADDR
  5. 发送行地址,也就是要读取页的首地址,分三次发送
  6. 发送命令0x30
  7. 等待就绪,即等待RnB信号由低电平变为高电平
  8. 读取数据,读取一个完整的页,使用寄存器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进行初始化,这包括三个步骤:

  1. 设置Nandflash的时间参数,使用寄存器NFCONF,需要设置的位是TACLS, TWRPH0, TWRPH1。这三个位的含义在6410芯片手册第8.4小节中有定义,如下图所示:
    Alt text
    再参考Nandflash芯片手册中的示意图和表格:Alt textAlt text
    综上可以得出以下计算过程:
    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即可
  2. 使能Nandflash controller,使用寄存器NFCONT的bit0,顺便使片选信号失效
  3. 复位Nandflash,发送命令0xff,等待芯片就绪即可,时序图如图所示:Alt text

相关代码如下:

#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中关于数据写入的时序描述,如下所示:
Alt text

总结数据写入的步骤,包括以下几步:

  1. 选中flash芯片
  2. 清除RnB
    1. 发送命令0x80
    2. 发送列地址
    3. 发送行地址
    4. 写入数据
    5. 发送命令0x10
    6. 等待RB信号
    7. 发送命令0x70
    8. 读取写入结果
  3. 取消选中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的擦除操作,其时序图如下:
Alt text

擦除的操作是以块为单位进行的,步骤如下:

  1. 选中flash芯片
  2. 清除RnB
    1. 发送命令0x60
    2. 发送行地址(块的地址)
    3. 发送命令0xd0
    4. 等待RnB
    5. 发送命令0x70
    6. 读取擦除结果
  3. 取消片选信号

代码实现如下:

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

  • 无标签