NAND Flash的数据是以bit 的方式保存在memory cell,一般来说,一个cell 中只能存储一个bit。这些cell 以8 个或者16 个为单位,连成bit line,形成所谓的byte(x8)/word(x16),这就是NAND Device 的位宽。这些Line 会再组成Page。
Nand Flash 根据的page的大小,可以分为small page和big page, 下面以samsung的K9F1G08U0B和K9F5608U0D分别说明:
K9F5608U0D:
page = 512+16 = 528 bytes (small page), 32个page组成一个block。 block = 16K +512 = 0x40000 bytes
K9F1G08U0B
page = (2K + 64)bytes (big page), 64个page组成一个block。 block = (128K + 4K)bytes
page是由两个部分组成的, 一个是main area, 是用来存储数据的, 另外一个叫Spare Area, 是用于存储ECC的校验值的。
Nand flash 以page为单位读写数据,而以block为单位擦除数据。按照这样的组织方式可以形成所谓的三类地址: --Block Address -- Page Address --Column Address(即为页内偏移地址)
对于NAND Flash 来讲,地址和命令只能在I/O[7:0]上传递,数据宽度是8 位。
512byte需要9bit来表示,对于528byte系列的NAND,这512byte被分成1st half和2nd half,各自的访问由地址指针命令来选择,A[7:0]就是所谓的column address。32 个page 需要5bit 来表示,占用A[13:9],即该page 在块内的相对地址。Block的地址是由A14 以上的bit 来表示,例如512Mb 的NAND,共4096block,因此,需要12 个bit 来表示,即A[25:14],如果是1Gbit 的528byte/page的NAND Flash,则block address用A[26:14]表示。而page address就是blcok address|page address in block, NAND Flash 的地址表示为: Block Address|Page Address in block|halfpage pointer|Column Address 地址传送顺序是Column Address,Page Address,Block Address。 由于地址只能在I/O[7:0]上传递,因此,必须采用移位的方式进行。 例如,对于512Mbit x8 的NAND flash,地址范围是0~0x3FF_FFFF,只要是这个范围内的数值表示的地址都是有效的。以NAND_ADDR 为例:第1 步是传递column address,就是NAND_ADDR[7:0],不需移位即可传递到I/O[7:0]上,而halfpage pointer 即bit8 是由操作指令决定的,即指令决定在哪个halfpage 上进行读写。而真正的bit8 的值是don\'t care 的。 第2 步就是将NAND_ADDR 右移9 位,将NAND_ADDR[16:9]传到I/O[7:0]上 第3 步将NAND_ADDR[24:17]放到I/O 上 第4 步需要将NAND_ADDR[25]放到I/O 上 因此,整个地址传递过程需要4 步才能完成,即4-step addressing。 如果NAND Flash 的容量是256Mbit 以下,那么,block adress 最高位只到bit24,因此寻址 只需要3 步。
对于big page需要两个周期才能将column address写入到flash, 两种flash的具体写address的周期,请见下表:
small page address写入到flash中的周期:
I/0 0 | I/0 1 | I/O 2 | I/0 3 | I/0 4 | I/0 5 | I/0 6 | I/0 7 | |
1st Cycle | A0 | A1 | A2 | A3 | A4 | A5 | A6 | A7 |
2st Cycle | A9 | A10 | A11 | A12 | A13 | A14 | A15 | A16 |
3st Cycle | A17 | A18 | A19 | A20 | A21 | A22 | A23 | A24 |
big page address写入到flash中的周期:
I/0 0 | I/0 1 | I/O 2 | I/0 3 | I/0 4 | I/0 5 | I/0 6 | I/0 7 | |
1st Cycle | A0 | A1 | A2 | A3 | A4 | A5 | A6 | A7 |
2st Cycle | A8 | A9 | A10 | A11 | 0 | 0 | 0 | 0 |
3st Cycle | A12 | A13 | A14 | A15 | A16 | A17 | A18 | A19 |
4st Cycle | A20 | A21 | A22 | A23 | A24 | A25 | A26 | A27 |
对应周期数,会随着flash的大小变化的, 下面会对nand flash K9f1208读写擦举例,虽然这个也是samll page的,但是有4个周期。
就x16 的NAND flash 器件稍微进行一下说明。 由于一个page 的main area 的容量为256word,仍相当于512byte。但是,这个时候没有所谓 的1st halfpage 和2nd halfpage 之分了,所以,bit8就变得没有意义了,也就是这个时候 bit8 完全不用管,地址传递仍然和x8 器件相同。除了,这一点之外,x16 的NAND使用方法和 x8 的使用方法完全相同。
每个nand flash都有一定的区别:
K9F5608U0D
function | 1st. Cycle | 2nd Cycle | Acceptable Command during Busy |
read | 00h/01h | - | |
read 2 | 50h | - | |
read ID | 90h | - | |
reset | FFh | - | 0 |
page program | 80h | 10h | |
copy back program | 00h | 8Ah | |
block erase | 60h | D0h | |
read status | 70h | - | 0 |
K9F1G08U0B
function | 1st. Cycle | 2nd Cycle | Acceptable Command during Busy |
read 1 | 00h | 30h | |
read for copy back | 00h | 35h | |
read ID | 90h | - | |
reset | FFh | - | 0 |
page program | 80h | 10h | |
copy back program | 85h | 10h | |
block erase | 60h | D0h | |
random data input | 85h | - | |
random data output | 05h | E0h | |
read status | 70h | 0 | |
read EDC status | 7Bh | 0 |
Nand Flash在出厂时会在每个block的0页面的space(512--527)空间中标记出坏块, small page和big page的地址还是不一样的, small page是在第5个字节处标明, big page是在第1个字节处标明,此页是否为坏块。如果为0xff则不是坏块,这个是nand flash出厂时的坏块检测方法。当nand flash在使用过程中出现了坏块,如何发现并且标记它呢?
答:通过某种坏块检测的软件算法对每一页的512个字节进行一定的判断,计算出一个ECC码,然后将其放入00B中,等若干时间后再用同样的算法对其进行检测,再次得到一个ECC码,将这个ECC码和之前计算出的ECC码进行比较,如果相等则不是坏块,如果不等则为坏块,然后就将其标记。
须掌握:如何遍历坏块:
ECC算法有两种:软件ECC算法,通用性好,每256个字节算出一个3byte的ECC码,故512个字节算出6byte的ECC码。
下面以small page为例,举例读写擦的过程, 对于下一节nand flash uboot驱动研究, 几乎将的都以big page为例,也有部分small page的东西。
K9f1208的寻址分为4个cycle。分别是:A[0:7]、A[9:16]、A[17:24]、A[25]。
读操作的过程为: 1、发送读取指令;2、发送第1个cycle地址;3、发送第2个cycle地址;4、发送第3个cycle地址;5、发送第4个cycle地址;6、读取数据至页末。
K9f1208提供了两个读指令,‘0x00’、‘0x01’。这两个指令区别在于‘0x00’可以将A[8]置为0,选中上半页;而‘0x01’可以将A[8]置为1,选中下半页。
虽然读写过程可以不从页边界开始,但在正式场合下还是建议从页边界开始读写至页结束。下面通过分析读取页的代码,阐述读过程。
static void ReadPage(U32 addr, U8 *buf) //addr表示flash中的第几页,即‘flash地址>>9’ { U16 i; NFChipEn(); //使能NandFlash WrNFCmd(READCMD0); //发送读指令‘0x00’,由于是整页读取,所以选用指令‘0x00’ WrNFAddr(0); //写地址的第1个cycle,即Column Address,由于是整页读取所以取0 WrNFAddr(addr); //写地址的第2个cycle,即A[9:16] WrNFAddr(addr>>8); //写地址的第3个cycle,即A[17:24] WrNFAddr(addr>>16); //写地址的第4个cycle,即A[25]。 WaitNFBusy(); //等待系统不忙 for(i=0; i<512; i++) buf[i] = RdNFDat(); //循环读出1页数据 NFChipDs(); //释放NandFlash }
写操作的过程为: 1、发送写开始指令;2、发送第1个cycle地址;3、发送第2个cycle地址;4、发送第3个cycle地址;5、发送第4个cycle地址;6、写入数据至页末;7、发送写结束指令
下面通过分析写入页的代码,阐述读写过程。
static void WritePage(U32 addr, U8 *buf) //addr表示flash中的第几页,即‘flash地址>>9’ { U32 i; NFChipEn(); //使能NandFlash WrNFCmd(PROGCMD0); //发送写开始指令’0x80’ WrNFAddr(0); //写地址的第1个cycle WrNFAddr(addr); //写地址的第2个cycle WrNFAddr(addr>>8); //写地址的第3个cycle WrNFAddr(addr>>16); 写地址的第4个cycle WaitNFBusy(); //等待系统不忙 for(i=0; i<512; i++) WrNFDat(buf[i]); //循环写入1页数据 WrNFCmd(PROGCMD1); //发送写结束指令’0x10’ NFChipDs(); //释放NandFlash }
static U32 EraseBlock(U32 addr) { UINT8T stat; // addr &= ~0x1f; NFChipEn(); //芯片使能,片选拉低,Nand flash使能 WrNFCmd(ERASECMD0); //写命令0x60表示块擦除操作将开始 WrNFAddr(addr); //地址分三次传送 WrNFAddr(addr>>8); //先传a9--a16, if(NandAddr) WrNFAddr(addr>>16); //后传a17-a24 ,a25 WrNFCmd(ERASECMD1); //传完地址后,写命令0xd0h(按时序要求) stat = WaitNFBusy(); //写完擦除命令后,执行等待 uart_printf(".......................!\n"); NFChipDs(); //关闭片选 if(stat == 0) { uart_printf("erase success!\n"); } else { uart_printf("erase error!\n"); } #ifdef ER_BAD_BLK_TEST if(!((addr+0xe0)&0xff)) stat = 1; //just for test bad block #endif //printf("Erase block 0x%x %s\n", addr, stat?"fail":"ok"); uart_printf ("."); return stat; }
当往NAND Flash的page中写入数据的时候,每256字节我们生成一个ECC校验和,称之为原ECC校验和,保存到PAGE的OOB(out-of-band)数据区中。
当从NAND Flash中读取数据的时候,每256字节我们生成一个ECC校验和,称之为新ECC校验和。
校验的时候,根据上述ECC生成原理不难推断:将从OOB区中读出的原ECC校验和新ECC校验和按位异或,若结果为0,则表示不存在错(或是出现了 ECC无法检测的错误);若3个字节异或结果中存在11个比特位为1,表示存在一个比特错误,且可纠正;若3个字节异或结果中只存在1个比特位为1,表示 OOB区出错;其他情况均表示出现了无法纠正的错误。
了能检测到单个位错误,把每256字节的数据划分为一张8x256共2048位的表,使用22位校验码进行校验,16位为行校验码,进行横向校验,6位为列校验码,按进行纵向校验。下图描述了这22位校验码产生的原理:
列校验:
CP0:对所有字节的Bit 6,4,2,0进行异或操作的结果
CP1:对所有字节的Bit 7,5,3,1进行异或操作的结果
.
.
CP5:对所有字节的Bit 7,6,5,4进行异或操作的结果
行校验:
LP0:对0,2,4,6...254字节的所有位进行异或操作的结果
LP1:对1,3,5,7...255字节的所有位进行异或操作的结果
.
.
LP15:对128~255字节的所有位进行异或操作的结果
22位校验码需要三个字节保存,前两字节保存16位行校验码LP0~LP15,第三字节保存6位列校验码CP0~CP5,剩余2位置1:
把数据写入Nand Flash时,我们产生校验码并将它保存到Flash的Spare Data Area。从Flash读出数据时,产生一个新的ECC校验码,将它和Spare Data Area读出的旧校验码进行异或操作:
结果为0:数据正确
结果有11位为1:数据有1位错误(可纠正)
其他结果:数据超过1位发生错误,无法纠正
下面是ECC产生算法的c++实现代码(仅满足示意用途,为了便于理解没有经过优化,在实际使用中,大量的异或运算可转为查表操作)
1. static inline void SetBit(unsigned int& dat, int bit, int v) 2. { 3. if (v) 4. dat |= (1<
使用ecc table的ECC算法的实现
static const u_char nand_ecc_precalc_table[] = { 0x00, 0x55, 0x56, 0x03, 0x59, 0x0c, 0x0f, 0x5a, 0x5a, 0x0f, 0x0c, 0x59, 0x03, 0x56, 0x55, 0x00, 0x65, 0x30, 0x33, 0x66, 0x3c, 0x69, 0x6a, 0x3f, 0x3f, 0x6a, 0x69, 0x3c, 0x66, 0x33, 0x30, 0x65, 0x66, 0x33, 0x30, 0x65, 0x3f, 0x6a, 0x69, 0x3c, 0x3c, 0x69, 0x6a, 0x3f, 0x65, 0x30, 0x33, 0x66, 0x03, 0x56, 0x55, 0x00, 0x5a, 0x0f, 0x0c, 0x59, 0x59, 0x0c, 0x0f, 0x5a, 0x00, 0x55, 0x56, 0x03, 0x69, 0x3c, 0x3f, 0x6a, 0x30, 0x65, 0x66, 0x33, 0x33, 0x66, 0x65, 0x30, 0x6a, 0x3f, 0x3c, 0x69, 0x0c, 0x59, 0x5a, 0x0f, 0x55, 0x00, 0x03, 0x56, 0x56, 0x03, 0x00, 0x55, 0x0f, 0x5a, 0x59, 0x0c, 0x0f, 0x5a, 0x59, 0x0c, 0x56, 0x03, 0x00, 0x55, 0x55, 0x00, 0x03, 0x56, 0x0c, 0x59, 0x5a, 0x0f, 0x6a, 0x3f, 0x3c, 0x69, 0x33, 0x66, 0x65, 0x30, 0x30, 0x65, 0x66, 0x33, 0x69, 0x3c, 0x3f, 0x6a, 0x6a, 0x3f, 0x3c, 0x69, 0x33, 0x66, 0x65, 0x30, 0x30, 0x65, 0x66, 0x33, 0x69, 0x3c, 0x3f, 0x6a, 0x0f, 0x5a, 0x59, 0x0c, 0x56, 0x03, 0x00, 0x55, 0x55, 0x00, 0x03, 0x56, 0x0c, 0x59, 0x5a, 0x0f, 0x0c, 0x59, 0x5a, 0x0f, 0x55, 0x00, 0x03, 0x56, 0x56, 0x03, 0x00, 0x55, 0x0f, 0x5a, 0x59, 0x0c, 0x69, 0x3c, 0x3f, 0x6a, 0x30, 0x65, 0x66, 0x33, 0x33, 0x66, 0x65, 0x30, 0x6a, 0x3f, 0x3c, 0x69, 0x03, 0x56, 0x55, 0x00, 0x5a, 0x0f, 0x0c, 0x59, 0x59, 0x0c, 0x0f, 0x5a, 0x00, 0x55, 0x56, 0x03, 0x66, 0x33, 0x30, 0x65, 0x3f, 0x6a, 0x69, 0x3c, 0x3c, 0x69, 0x6a, 0x3f, 0x65, 0x30, 0x33, 0x66, 0x65, 0x30, 0x33, 0x66, 0x3c, 0x69, 0x6a, 0x3f, 0x3f, 0x6a, 0x69, 0x3c, 0x66, 0x33, 0x30, 0x65, 0x00, 0x55, 0x56, 0x03, 0x59, 0x0c, 0x0f, 0x5a, 0x5a, 0x0f, 0x0c, 0x59, 0x03, 0x56, 0x55, 0x00 }; //Creates non-inverted ECC code from line parity static void nand_trans_result(u_char reg2, u_char reg3,u_char *ecc_code) { u_char a, b, i, tmp1, tmp2; /* Initialize variables */ a = b = 0x80; tmp1 = tmp2 = 0; /* Calculate first ECC byte */ for (i = 0; i < 4; i++) { if (reg3 & a) /* LP15,13,11,9 --> ecc_code[0] */ tmp1 |= b; b >>= 1; if (reg2 & a) /* LP14,12,10,8 --> ecc_code[0] */ tmp1 |= b; b >>= 1; a >>= 1; } /* Calculate second ECC byte */ b = 0x80; for (i = 0; i < 4; i++) { if (reg3 & a) /* LP7,5,3,1 --> ecc_code[1] */ tmp2 |= b; b >>= 1; if (reg2 & a) /* LP6,4,2,0 --> ecc_code[1] */ tmp2 |= b; b >>= 1; a >>= 1; } /* Store two of the ECC bytes */ ecc_code[0] = tmp1; ecc_code[1] = tmp2; } // Calculate 3 byte ECC code for 256 byte block void nand_calculate_ecc (const u_char *dat, u_char *ecc_code) { u_char idx, reg1, reg2, reg3; int j; /* Initialize variables */ reg1 = reg2 = reg3 = 0; ecc_code[0] = ecc_code[1] = ecc_code[2] = 0; /* Build up column parity */ for(j = 0; j < 256; j++) { /* Get CP0 - CP5 from table */ idx = nand_ecc_precalc_table[dat[j]]; reg1 ^= (idx & 0x3f); /* All bit XOR = 1 ? */ if (idx & 0x40) { reg3 ^= (u_char) j; reg2 ^= ~((u_char) j); } } /* Create non-inverted ECC code from line parity */ nand_trans_result(reg2, reg3, ecc_code); /* Calculate final ECC code */ ecc_code[0] = ~ecc_code[0]; ecc_code[1] = ~ecc_code[1]; ecc_code[2] = ((~reg1) << 2) | 0x03; } // Detect and correct a 1 bit error for 256 byte block int nand_correct_data (u_char *dat, u_char *read_ecc, u_char *calc_ecc) { u_char a, b, c, d1, d2, d3, add, bit, i; /* Do error detection */ d1 = calc_ecc[0] ^ read_ecc[0]; d2 = calc_ecc[1] ^ read_ecc[1]; d3 = calc_ecc[2] ^ read_ecc[2]; if ((d1 | d2 | d3) == 0) { /* No errors */ return 0; } else { a = (d1 ^ (d1 >> 1)) & 0x55; b = (d2 ^ (d2 >> 1)) & 0x55; c = (d3 ^ (d3 >> 1)) & 0x54; /* Found and will correct single bit error in the data */ if ((a == 0x55) && (b == 0x55) && (c == 0x54)) { c = 0x80; add = 0; a = 0x80; for (i=0; i<4; i++) { if (d1 & c) add |= a; c >>= 2; a >>= 1; } c = 0x80; for (i=0; i<4; i++) { if (d2 & c) add |= a; c >>= 2; a >>= 1; } bit = 0; b = 0x04; c = 0x80; for (i=0; i<3; i++) { if (d3 & c) bit |= b; c >>= 2; b >>= 1; } b = 0x01; a = dat[add]; a ^= (b << bit); dat[add] = a; return 1; } else { i = 0; while (d1) { if (d1 & 0x01) ++i; d1 >>= 1; } while (d2) { if (d2 & 0x01) ++i; d2 >>= 1; } while (d3) { if (d3 & 0x01) ++i; d3 >>= 1; } if (i == 1) { /* ECC Code Error Correction */ read_ecc[0] = calc_ecc[0]; read_ecc[1] = calc_ecc[1]; read_ecc[2] = calc_ecc[2]; return 2; } else { /* Uncorrectable Error */ return -1; } } } /* Should never happen */ return -1; }flash的烧写
这里的烧写是指打开software check ecc,如果使用nand 子指令是没有问题的,会自动跳过bad block,还会计算出ecc并写入flash中,但是前提在flash中有可用的bootcode,但是如果是空的,那就必须使用烧code机烧写 flash了, 如果使用烧code机就不一样了,需要将要烧如flash的binary处理一下,将binary中包含oob信息,这里我们自己写了两个tools来处理这件事, 两个tools分别是针对small page和big page的。
命令的使用方法:big_page/small_page [source file] [dest file] [oob file]
命令的原理:就是在适当的位置,插入oob的ECC值。
用烧code机烧flash时应注意:
erase时, access mothod中Invalid Block Management要选择Do not use, 这样擦的时候不会检测是不是坏块, Spare Area Usage选择User Data with IB Info forced。
write时,需要将Invalid Block Management改为Skip IB,写时会跳过坏块,Spare Area Usage选择User Data with IB Info forced, 选择binary中的ecc信息, 即ECC_SOFTWARE。