Flash根据存储单元电路的不同,可以分为NOR Flash和NAND Flash。NOR Flash的数据线和地址线分开,可以实现和RAM一样的随机寻址功能。NAND Flash数据线和地址线复用,不能利用地址线随机寻址,读取时只能按页读取。
由于NAND Flash引脚上可复用,因此读取速度比NOR Flash慢,但是擦除和写入速度更快,并且由于NAND Flash内部电路简单,数据密度大,体积小,成本低,因此大容量的Flash都是NAND型的,而小容量(2~12MB)的Flash大多为NOR型。
在使用寿命上,NAND Flash的可擦除次数是NOR Flash的数倍。另外,NAND Flash可以标记坏块,从而使软件跳过坏块,而NOR Flash一旦损坏则无法再使用。
以NAND Flash芯片HY27UF081G2A为例,该芯片通过EXMC接口,与微控制器连接,如下图所示。本文并不介绍该芯片的具体用法,而是介绍管理NAND Flash所需要的ECC算法、FTL等。
由于Nand Flash串行组织的存储结构,数据读取时,读出放大器所检测的信号强度会被削弱,降低信号的准确性,导致读数出错,通常采用ECC算法进行数据检测及校准。
ECC(Error Checking and Correction)是一种错误检测和校准的算法。NAND Flash数据产生错误时一般只有1bit出错,而ECC能纠正1bit错误和检测2bit的错误,并且计算速度快,但缺点是无法纠正1bit以上的错误,且不确保能检测2bit以上的错误(至于为什么,可以看下面的原理介绍再思考一下)。
ECC算法的基本原理如下:假设对512个字节的数据进行校验,那么将这些数据视为512行、8列的矩阵,即每行表示1字节数据,矩阵的每个元素表示1位(bit),如下图所示。校验过程分为行校验和列校验(下面将bitn中的n称为索引值)。
列校验:首先将矩阵每个列进行异或,得到如上图所示1号阴影区域的8位数据。其次每次取出4位并进行异或,重复进行6次,将得到的6位数据称为CP0~CP5:
CP0=bit0 ^bit2 ^bit4 ^bit8(每取1位隔1位,索引值对应二进制的bit0为0的位)
CP1=bit1 ^bit3 ^bit5 ^bit7(每隔1位取1位,索引值对应二进制的bit0为1的位)
CP2=bit0 ^bit1 ^bit4 ^bit5(每取2位隔2位,索引值对应二进制的bit1为0的位)
CP3=bit2 ^bit3 ^bit6 ^bit7(每隔2位取2位,索引值对应二进制的bit1为1的位)
CP4=bit0 ^bit1 ^bit2 ^bit3(每取4位隔4位,索引值对应二进制的bit2为0的位)
CP5=bit4 ^bit5 ^bit6 ^bit7(每隔4位取4位,索引值对应二进制的bit2为1的位)
列校验最终得到的校验值即为上述6位数据。
行校验:首先将矩阵每个行进行异或,得到如上图所示2号阴影区域的512位数据。其次每次取出256位并进行异或,重复进行18次,将得到的数据称为RP0~RP17:
RP0=bit0 ^bit2 ^bit4 ^… ^bit510(每取1位隔1位,索引值对应二进制的bit0为0的位)
RP1=bit1 ^bit3 ^bit5 ^… ^bit511(每隔1位取1位,索引值对应二进制的bit0为1的位)
RP2=bit0 ^bit1 ^bit4 ^bit5 ^… ^bit508 ^bit509(每取2位隔2位,索引值对应二进制的bit1为0的位)
RP3=bit2 ^bit3 ^bit6 ^bit7 ^… ^bit510 ^bit511(每隔2位取2位,索引值对应二进制的bit1为1的位)
……
RP16=bit0 ^bit1 ^… ^bit254 ^bit255(每取256位隔256位,索引值对应二进制的bit9为0的位)
RP17=bit256 ^bit257 ^… ^bit510 ^bit511(每隔256位取256位,索引值对应二进制的bit9为1的位)
行校验最终得到的校验值即为上述18位数据。
综上所述,通过汉明码编码的ECC校验,n字节的数据对应的校验值为2log2n+6位(log2n指以2为,n的对数)。校验值在对应数据写入Nand Flash时一同写入,被保存到空闲区域中(空闲区域是NAND Flash中特有的,用来存在除有效数据外的数据,比如校验值等)。微控制器读取对应数据时将对应校验值一同读取,并对数据进行再一次校验,将新得到的校验值与读取到的校验值进行异或,此时得到的结果为1表示校验码不同,可以判定产生了错误:如果新得到的CP1和读取到的CP1异或后为1,即两者不同,表示数据中的1、3、5、7列中存在错误,如果新得到的RP16和读取到的RP16异或后为1,表示数据中的0 ~ 255行中存在错误。校验值进行异或得到的结果有以下几种:
一般器件出现2bit及以上的错误很少见,因此汉明码编码的ECC校验基本上够用。
一般EXMC模块中NAND Flash对应的Bank都包含带有ECC算法的硬件模块,因此ECC算法的使用实际上通过寄存器完成配置即可。
注意,NAND Flash芯片不会提供ECC计算的,只提供了空闲区域而已。ECC算法使用需要在存入数据时在微控制器中计算,并写入对应的空闲区域中。然后再读出时重新计算,并与写入值进行比较。
如果觉得上面的ECC算法很难理解,实际上可以看一下老鼠试毒这篇文章,原理有点像,通过有限位的变化,反推出大量数据中某个数据的变化。
NAND Flash在生产和使用过程中都有可能产生坏块,并且每个块的擦除次数有限,即超过一定次数后将无法擦除,也就产生了坏块(注意Flash只能由1变0,因此必须擦除才能正常写入)。坏块的存在使得NAND Flash物理地址不连续,而微控制器访问存储设备时要求地址连续,否则无法通过地址直接读写存储设备,因此一般添加闪存转换层FTL(Flash Translation Layer)完成对NAND Flash的操作。FTL的功能如下:
3. 坏块管理和磨损均衡
坏块管理:当坏块产生后,用空闲并且可读写的块替代坏块在映射表中的位置,保证每个逻辑地址都映射到可读写的物理地址。例如上图的物理地址1损坏后,可以用还没使用的物理地址3187代替,这个时候使逻辑地址1对应该物理地址即可。
磨损均衡:向某个已写入值的块重新写入数据时,向其它块写入并使其替代原本已写入值的块在映射表中的位置,这是由于每个块的擦除及编程次数有限,为防止部分块访问次数过多而提前损坏,使得全部块尽可能地同时达到磨损阈值。例如想再次向上图的物理地址1写入,可以不再次擦除它,而是擦除还没使用的物理地址3187并将数据写入该物理地址,最后使逻辑地址1对应该物理地址即可。
//Nand Flash操作定义
#define NAND_CMD_AREA (*(__IO u8 *)(BANK_NAND_ADDR | EXMC_CMD_AREA)) //写命令
#define NAND_ADDR_AREA (*(__IO u8 *)(BANK_NAND_ADDR | EXMC_ADDR_AREA)) //写地址
#define NAND_DATA_AREA (*(__IO u8 *)(BANK_NAND_ADDR | EXMC_DATA_AREA)) //读写数据
u32 NandWritePage(u32 block, u32 page, u32 column, u8* buf, u32 len)
{
……
//设置写入地址
NAND_CMD_AREA = NAND_CMD_WRITE_1ST; //发送写命令
NAND_ADDR_AREA = (column >> 0) & 0xFF; //列地址低位
NAND_ADDR_AREA = (column >> 8) & 0xFF; //列地址高位
NAND_ADDR_AREA = (block << 6) | (page & 0x3F); //块地址和列地址
NAND_ADDR_AREA = (block >> 2) & 0xFF; //剩余块地址
NandDelay(NAND_TADL_DELAY); //tADL等待延迟
//写入数据
byteCnt = 0;
eccCnt = 0;
for(i = 0; i < len; i++)
{
//写入数据
NAND_DATA_AREA = buf[i];
//保存ECC值
byteCnt++;
if(byteCnt >= 512)
{
//等待FIFO空标志位
while(RESET == exmc_flag_get(EXMC_BANK1_NAND, EXMC_NAND_PCCARD_FLAG_FIFOE));
//获取ECC值
ecc[eccCnt] = exmc_ecc_get(EXMC_BANK1_NAND); //用于获取对应ECC值的固件库函数
eccCnt++;
//清空计数
byteCnt = 0;
}
}
//计算写入ECC的spare区地址
eccAddr = NAND_PAGE_SIZE + 16 + 4 * (column / 512);
//设置写入Spare位置
NandDelay(NAND_TADL_DELAY); //tADL等待延迟
NAND_CMD_AREA = 0x85; //随机写命令
NAND_ADDR_AREA = (eccAddr >> 0) & 0xFF; //随机写地址低位
NAND_ADDR_AREA = (eccAddr >> 8) & 0xFF; //随机写地址高位
NandDelay(NAND_TADL_DELAY); //tADL等待延迟
//将ECC写入Spare区指定位置
for(i = 0; i < eccCnt; i++)
{
NAND_DATA_AREA = (ecc[i] >> 0) & 0xFF;
NAND_DATA_AREA = (ecc[i] >> 8) & 0xFF;
NAND_DATA_AREA = (ecc[i] >> 16) & 0xFF;
NAND_DATA_AREA = (ecc[i] >> 24) & 0xFF;
}
//发送写入结束命令
NAND_CMD_AREA = NAND_CMD_WRITE_2ND;
……
}
u32 NandReadPage(u32 block, u32 page, u32 column, u8* buf, u32 len)
{
……
//设置读取地址
NAND_CMD_AREA = NAND_CMD_READ1_1ST; //发送读命令
NAND_ADDR_AREA = (column >> 0) & 0xFF; //列地址低位
NAND_ADDR_AREA = (column >> 8) & 0xFF; //列地址高位
NAND_ADDR_AREA = (block << 6) | (page & 0x3F); //块地址和列地址
NAND_ADDR_AREA = (block >> 2) & 0xFF; //剩余块地址
NAND_CMD_AREA = NAND_CMD_READ1_2ND; //读命令结束
if(NANDWaitRB(0)) {return NAND_FAIL;} //等待RB = 0
if(NANDWaitRB(1)) {return NAND_FAIL;} //等待RB = 1
NandDelay(NAND_TRR_DELAY); //tRR延时等待
//读取数据
byteCnt = 0;
eccCnt = 0;
for(i = 0; i < len; i++)
{
//读取数据
buf[i] = NAND_DATA_AREA;
//保存ECC值
byteCnt++;
if(byteCnt >= 512)
{
//等待FIFO空标志位
while(RESET == exmc_flag_get(EXMC_BANK1_NAND, EXMC_NAND_PCCARD_FLAG_FIFOE));
//获取ECC值
eccHard[eccCnt] = exmc_ecc_get(EXMC_BANK1_NAND);
eccCnt++;
//清空计数
byteCnt = 0;
}
}
//计算读取ECC的spare区地址
eccAddr = NAND_PAGE_SIZE + 16 + 4 * (column / 512);
//设置读取Spare位置
NandDelay(NAND_TWHR_DELAY); //tWHR等待延迟
NAND_CMD_AREA = 0x05; //随机读命令
NAND_ADDR_AREA = (eccAddr >> 0) & 0xFF; //随机读地址低位
NAND_ADDR_AREA = (eccAddr >> 8) & 0xFF; //随机读地址高位
NAND_CMD_AREA = 0xE0; //命令结束
NandDelay(NAND_TWHR_DELAY); //tWHR等待延迟
NandDelay(NAND_TREA_DELAY); //tREA等待延时
//从Spare区指定位置读出之前写入的ECC
for(i = 0; i < eccCnt; i++)
{
spare[0] = NAND_DATA_AREA;
spare[1] = NAND_DATA_AREA;
spare[2] = NAND_DATA_AREA;
spare[3] = NAND_DATA_AREA;
eccFlash[i] = ((u32)spare[3] << 24) | ((u32)spare[2] << 16) | ((u32)spare[1] << 8) | ((u32)spare[0] << 0);
}
//校验并尝试修复数据
for(i = 0; i < eccCnt; i++)
{
if(eccHard[i] != eccFlash[i])
{
if(0 != NandECCCorrection(buf + 512 * i, eccFlash[i], eccHard[i]))
{
return NAND_FAIL;
}
}
}
//读取成功
return NAND_OK;
}
u32 NandECCCorrection(u8* data, u32 eccrd, u32 ecccl)
{
……
eccrdo = NandECCGetOE(1, eccrd); //获取eccrd的奇数位
eccrde = NandECCGetOE(0, eccrd); //获取eccrd的偶数位
eccclo = NandECCGetOE(1, ecccl); //获取ecccl的奇数位
ecccle = NandECCGetOE(0, ecccl); //获取ecccl的偶数位
eccchk = eccrdo ^ eccrde ^ eccclo ^ ecccle;
//全1,说明只有1bit ECC错误
if(eccchk == 0xFFF)
{
errorpos = eccrdo ^ eccclo;
printf("NandECCCorrection: errorpos:%d\r\n", errorpos);
bytepos = errorpos / 8;
data[bytepos] ^= 1 << (errorpos % 8);
}
//不是全1,说明至少有2bit ECC错误,无法修复
else
{
printf("NandECCCorrection: 2bit ecc error or more\r\n");
return 1;
}
return 0;
}
void FTLBadBlockMark(u32 blockNum)
{
//坏块标记mark,任意值都OK,只要不是0XFF
//这里写前4个字节,方便FTL_FindUnusedBlock函数检查坏块(不检查备份区,以提高速度)
u32 mark = 0xAAAAAAAA;
//在第一个page的spare区,第一个字节做坏块标记(前4个字节都写)
NandWriteSpare(blockNum, 0, 0, (u8*)&mark, 4);
//在第二个page的spare区,第一个字节做坏块标记(备份用,前4个字节都写)
NandWriteSpare(blockNum, 1, 0, (u8*)&mark, 4);
}
u32 FTLSetBlockUseFlag(u32 blockNum)
{
u8 flag = 0xCC;
return NandWriteSpare(blockNum, 0, 1, (u8*)&flag, 1);
}
u32 FTLLogicNumToPhysicalNum(u32 logicNum)
{
if(logicNum > s_structFTLDev.blockTotalNum)
{
return INVALID_ADDR;
}
else
{
return s_structFTLDev.lut[logicNum];
}
}
LUT:显示查找表,在这里用于登记有效的块(即好块),并将其标记上对应的逻辑块编号(也就是使该物理块对应1个逻辑块)
u32 CreateLUT(void)
{
u32 i; //循环变量i
u8 spare[6]; //Spare前6个字节数据
u32 logicNum; //逻辑块编号
//清空LUT表
for(i = 0; i < s_structFTLDev.blockTotalNum; i++)
{
s_structFTLDev.lut[i] = INVALID_ADDR;
}
s_structFTLDev.goodBlockNum = 0;
s_structFTLDev.validBlockNum = 0;
//读取NandFlash中的LUT表
for(i = 0; i < s_structFTLDev.blockTotalNum; i++)
{
//读取Spare区
NandReadSpare(i, 0, 0, spare, 6);
if(0xFF == spare[0])
{
NandReadSpare(i, 1, 0, spare, 1);
}
//是好快
if(0xFF == spare[0])
{
//得到逻辑块编号
logicNum = ((u32)spare[5] << 24) | ((u32)spare[4] << 16) | ((u32)spare[3] << 8) | ((u32)spare[2] << 0);
//逻辑块号肯定小于总的块数量
if(logicNum < s_structFTLDev.blockTotalNum)
{
//更新LUT表
s_structFTLDev.lut[logicNum] = i;
}
//好块计数
s_structFTLDev.goodBlockNum++;
}
else
{
printf("CreateLUT: bad block index:%d\r\n",i);
}
}
//LUT表建立完成以后检查有效块个数
for(i = 0; i < s_structFTLDev.blockTotalNum; i++)
{
if(s_structFTLDev.lut[i] < s_structFTLDev.blockTotalNum)
{
s_structFTLDev.validBlockNum++;
}
}
//有效块数小于100,有问题.需要重新格式化
if(s_structFTLDev.validBlockNum < 100)
{
return 1;
}
//LUT表创建完成
return 0;
}