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驱动。
4. 附录
Nand.rar,Nand驱动实现。
源码: http://pan.baidu.com/s/1gd6fEIv