Nand flash具有大容量、改写速度快、接口简单等优点,适用于大量数据的存储,为固态大容量存储提供了廉价有效的解决方案。各种电子产品中如手机存储器、sd卡、u盘等均采用Nand flash存储,笔者此处就Nand驱动实现作一个简单的介绍。

1. Nand flash概述

东芝公司在1989年最先发表Nand flash结构,强调降低每比特的成本,更高的性能,并且像磁盘一样可以通过接口轻松升级。随着Nand技术的发展,根据Nand颗粒存储单元,可以分为SLC(Single Level Cell)、MLC(Multi LevelCell)、TLC(Trinary Level Cell)。SLC为1bit/cell,即1个储存单元存放1bit数据,其特点是成本高、容量小、速度快、寿命长(约10万次擦写)。MLC为2bit/cell,即1个储存单元存放2bit数据,相比SLC来说,其价格一般,容量可以更大,速度一般、寿命一般(约1万次擦写)。TLC为3bit/cell,即1个储存单元存放3bit数据,因此相同容量下其成本最低,容量相比最大,但速度最慢,寿命也最短(约1000次擦写)。目前市面上基于Nand flash的存储器,如sd卡、u盘等大多为MLC型,但对于高容量Nand存储器,如64G以上sd卡等,很有可能为TLC型Nand flash。

2. Nand驱动实现

Nand flash由Block(块)构成,Block的基本单元为Page(页),每个页又分为Data area(数据存储区域)以及Spare area(备用区域)。由于Nand flash在出厂以及在使用过程中会出现坏块,以及会出现位反转的问题,为了数据的可靠性,通常需要采用ecc(Error Correcting Code)算法,一般对于SLC Nand,采用1bit ecc即可,对于MLC Nand,需采用4bit以上ecc,而Spare area即用来保存坏块标记、ecc数据等额外信息。通常Nand flash的读取和编程以页为基础,而擦除却是基于块的。Nand flash编程只能把1编程成0,而不能把0编程成1,因此在页编程时,必须已先擦除。

笔者采用Nand flash为Samsung的K9F4G08U0E,一页有(2048+64)Byte,一个Block有64页,容量大小为(512M+16M)Byte,是一款8位宽的SLC Nand flash。

Nand flash驱动一般应实现Nand初始化、页读、页编程、坏块标记、坏块检查、块擦除这几个接口实现。

2.1. Nand初始化

Nand初始化主要是对Nand引脚功能初始化、根据具体的Nand flash,设置最佳的Nand访问时序。

void Nand_Init(void)
{
    // 配置nand控制引脚
    MP01CON_REG =(MP01CON_REG & ~(0xf<<8)) | (0x3<<8); // NFCS0
    MP01PUD_REG&= ~(3<<4); // pull-up/down disable
    MP03CON_REG =0x22222222;
    MP03PUD_REG =0;

    NFCONF_REG =(2<<12)|(2<<8)|(1<<4)|(0<<3)|(0<<2)|(1<<1);
    NFCONT_REG =(1<<23)|(1<<22)|(1<<5)|(1<<4)|(1<<2)|(1<<1)|(1<<0);
    Nand_Reset();
}

2.2. Nand页读

K9F4G08U0E一页分为2k的data area与64字节的spare area,data area用来存储正常数据,sparearea用来存储附加数据(如ecc数据)。对于flash存储器,是会出现位反转或坏块的问题,写入flash的数据或从flash读出的数据可能是有错的,因此必须采用ecc算法,确保写入的数据与读出的数据是一致的。对于SLC Nand Flash,只需1位ecc即可,每512 byte产生4 byte的ecc数据,最多可纠错1位,spare area 01字节存放坏块标记,217字节存放该页ecc数据,18~19字节存放spare area产生的ecc数据,剩余spare area存放接口需要写入的其它spare area数据。

int32_t Nand_ReadWithOob(uint32_t page, uint8_t *data,uint32_t data_len, uint8_t *oob, uint32_t oob_len)
{
    uint8_t ecc[16];
    uint32_t i;
    uint8_t *pecc;
    uint8_t *pBuffer;
    uint32_t MECC, SECC;
    uint16_t Col;
    uint8_t Status;
    if (!data && (data_len!=0)) {
        return -1;
    }
    if (!oob && (oob_len!=0)) {
        return -1;
    }
    // 保留oob前面20字节作为坏块标记以及数据ecc
    if ((data_len>2048) || (oob_len>64-20)) {
        return -2;
    }
    // ecc data 16字节开始于spare区的第2个字节偏移处
    Col = 2048 + 2;
    NF_INIT_SECC();
    NF_SECC_UNLOCK(); // 解锁spare ECC
    NF_CE_ENABLE();
    NF_CLEAR_RB();
    NF_CMD(NAND_CMD_READ0); // page read cycle 1
    NF_ADDR(Col & 0xff); // column address
    NF_ADDR(Col >> 8); // columu address
    NF_ADDR(page & 0xff); // 传输3字节的页地址
    NF_ADDR((page>>8) & 0xff);
    NF_ADDR((page>>16) & 0xff);
    NF_CMD(NAND_CMD_READSTART); // page read cycle 2
    NF_WAIT_READY(); // 等待页读完成
    for (i=0; i<16; i++) {
        ecc[i] =NF_READ_BYTE();
    }
    NF_SECC_LOCK();
    SECC = NF_READ_BYTE();
    SECC |= NF_READ_BYTE() << 8;
    for (i=0; i<oob_len; i++) {
        oob[i] =NF_READ_BYTE();
    }
    NFSECCDATA0_REG=((SECC&0xff00)<<8)|(SECC&0xff);
    NF_CE_DISABLE();
    // Read ecc status
    Status = NFESTAT0_REG;
    if (((Status>>2) & 0x3) == 1) {
        // ecc 1biterror, Correctable
        ecc[(Status>>21)&0xf]^= (1 << ((Status>>18) & 0x7));
    }
    else if (((Status>>2) & 0x3) > 1) {
        Debug("ECCuncorrectable error detected\r\n");
        return -3;
    }
    if (data_len == 0) {
        return 0;
    }
    NF_CE_ENABLE(); // 使能片选
    NF_CLEAR_RB(); // 清数据传输标志
    NF_CMD(NAND_CMD_READ0); // page read cycle 1
    NF_ADDR(0); // column address
    NF_ADDR(0); // columu address
    NF_ADDR(page & 0xff); // 写入3字节的页地址
    NF_ADDR((page>>8) & 0xff);
    NF_ADDR((page>>16) & 0xff);
    NF_CMD(NAND_CMD_READSTART); // page read cycle 2
    NF_WAIT_READY(); // 等待命令完成
    // read main area
    pecc = ecc;
    while (data_len) {
        pBuffer =data;
        NF_INIT_MECC();// main区ECC清空
        NF_MECC_UNLOCK();// main区ECC解锁,开始ECC计算
        for (i=0; i<512; i++) {
            *data++ =NF_READ_BYTE();
            data_len--;
            if(data_len == 0) {
                break;
            }
        }
        NF_MECC_LOCK();// 锁定main ECC
        // SLC: Writeecc to compare
        MECC =(pecc[1] << 16) | (pecc[0] << 0);
        NFMECCDATA0_REG= MECC;
        MECC =(pecc[3] << 16) | (pecc[2] << 0);
        NFMECCDATA1_REG= MECC;
        pecc += 4;
        // Read eccstatus
        Status =NFESTAT0_REG;
        if ((Status& 0x3) == 1) {
            // 1biterror, Correctable
            pBuffer[(Status>>7)&0x7ff]^= (1 << ((Status>>4) & 0x7));
        }
        else if((Status & 0x3) > 1) {
            Debug("ECCuncorrectable error detected\r\n");
            NF_CE_DISABLE();
            return-4;
        }
    }
    NF_CE_DISABLE();
    return 0;
}

2.3. Nand页编程

当要写数据到一个页时,要先确保这个块已经被擦除。数据写完后,为确保写入与读取的数据一致,应同时写入数据的ecc值到spare area约定好的位置。

int32_t Nand_WriteWithOob(uint32_t page, const uint8_t*data, uint32_t data_len, const uint8_t *oob, uint32_t oob_len)
{
    uint8_t ecc[16];
    uint32_t i;
    uint8_t *pecc;
    uint32_t MECC, SECC;
    uint32_t data_len_temp;
    uint16_t Col;
    uint8_t State;
    if (!data && (data_len!=0)) {
        return -1;
    }
    if (!oob && (oob_len!=0)) {
        return -1;
    }
    // 保留oob前面20字节作为坏块标记以数据ecc
    if ((data_len>2048) || (oob_len>64-20)) {
        return -2;
    }
    NF_CE_ENABLE(); // 使能片选
    NF_CLEAR_RB(); // 清数据传输标志
    NF_CMD(NAND_CMD_SEQIN); // page program cycle 1
    NF_ADDR(0); // column address
    NF_ADDR(0); // columu address
    NF_ADDR(page & 0xff); // 写入3字节页地址
    NF_ADDR((page>>8) & 0xff);
    NF_ADDR((page>>16) & 0xff);
    pecc = ecc;
    data_len_temp = data_len;
    // write main area
    while (data_len_temp) {
        NF_INIT_MECC();// main区ECC清空
        NF_MECC_UNLOCK();// main区ECC解锁,开始ECC计算
        // Write bufferto main area
        for (i=0; i<512; i++) {
            NF_WRITE_BYTE(*data++);
            data_len_temp--;
            if(data_len_temp == 0) {
                break;
            }
        }
        NF_MECC_LOCK();// 锁定main ECC
        MECC =NFMECC0_REG; // 4字节写main区数据的ECC
        *pecc++ =MECC & 0xff;
        *pecc++ =(MECC>>8) & 0xff;
        *pecc++ =(MECC>>16) & 0xff;
        *pecc++ =(MECC>>24) & 0xff;
    }
    if (data_len < 2048) {
        // 调整到oob区
        Col = 2048;
        NF_CMD(NAND_CMD_RNDIN);// 页内随机写命令
        NF_ADDR(Col& 0xff); // 2字节页内地址
        NF_ADDR((Col>>8)& 0xff);
    }
    // Reserved for bad block (2 byte)
    NF_WRITE_BYTE(0xff);
    NF_WRITE_BYTE(0xff);
    NF_INIT_SECC();
    NF_SECC_UNLOCK(); // 解锁spare ECC
    // main ecc 16 byte, start spare area offset 2
    for (i=0; i<16; i++) {
        NF_WRITE_BYTE(ecc[i]);
    }
    NF_SECC_LOCK(); // 锁定spare ECC
    SECC = NFSECC_REG; // 2字节的spare写数据ECC
    NF_WRITE_BYTE(SECC & 0xff); // 继续写入SECC
    NF_WRITE_BYTE((SECC>>8) & 0xff);
    for (i=0; i<oob_len; i++) {
        NF_WRITE_BYTE(oob[i]);
    }
    NF_CMD(NAND_CMD_PAGEPROG); // page program cycle 2
    NF_WAIT_READY(); // 等待写完
    NF_CMD(NAND_CMD_STATUS); // 读取nand状态
    do {
        State =NF_READ_BYTE();
    }
    while(!(State & (1<<6)));   // 等待状态变成Ready
    NF_CE_DISABLE();
    // 是否写成功,第0位为0则pass,不然fail
    if (State & (1<<0)) {
        return -3; //写不成功
    }
    return 0;
}

2.4. Nand坏块检查

坏块标记约定用spare area第01字节来作标志,坏块这两字节标志为非0xff,好块为0xff。我们读取block中页0,spare area第01字节的值即可判断这个block是否坏块。

int32_t Nand_IsBadBlock(uint32_t Block)
{
    uint8_toob[2];
    // 每个block第一页spare区0, 1字节非0xff标记为好坏
    Nand_Read(Block,0, 2048, 2, oob);
    if((oob[0]==0xff) && (oob[1]==0xff)) {
        return 0; // 好块
    }
    return 1; // 坏块
}

2.5. 坏块标记

如果页编程不成功或者擦除失败,检查出坏块,需要对相应的块在约定位置进行坏块标记,以免再对这个坏块进行读写。其代码实现如下:

int32_t Nand_MarkBadBlock(uint32_t Block)
{
    // 每个block第一页spare区第0, 1字节标记非0xff坏块
    uint8_t oob[2];
    oob[0] = 0;
    oob[1] = 0;
    returnNand_Write(Block, 0, 2048, 2, oob);
}

2.6. 块区擦除

数据写入块区前,对应的块应已擦除好。

int32_t Nand_EraseBlock(uint32_t Block)
{
    uint8_tState;
    NF_CE_ENABLE();
    NF_CLEAR_RB();
    NF_CMD(NAND_CMD_ERASE1);// erase block command cycle 1
    // write 3cycle block address[A28:A18]
    NF_ADDR((Block<<6)& 0xff); // [A19:A18]
    NF_ADDR((Block>>2)& 0xff); // [A27:A20]
    NF_ADDR((Block>>10)& 0xff); // A28
    NF_CMD(NAND_CMD_ERASE2);// erase block command cycle 2
    NF_WAIT_READY();
    NF_CMD(NAND_CMD_STATUS);
    do {
        State =NF_READ_BYTE();
    }
    while(!(State & (1<<6)));  // 等待状态变成Ready
    NF_CE_DISABLE();
    // 是否擦写成功,第0位为0则pass,不然fail
    if (State& (1<<0)) {
        return-1;
    }
    return 0; // 成功擦除
}

3. Nand启动

对于OneNand/Nand启动设备,BL0除了检验BL1的检验和之外,还会采用8bit ecc检验BL1代码的正确性,BL1在烧录进Nand设备时,还应生成相应的8/16位ECC数据,写入到spare area的指定位置,不然ecc失败也将无法从Nand设备启动。Nand启动需要8bit ecc处理,8bit ecc参考Bootloader中相应的NandBoot.c这个Nand驱动。

07_Nand驱动实现 - 图1

图3-1 Nand ECC检验

4. 附录

Nand.rar,Nand驱动实现。
源码: http://pan.baidu.com/s/1gd6fEIv