GD32学习笔记(3)NAND Flash管理

NAND Flash介绍

       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等。

在这里插入图片描述

ECC算法

       由于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行中存在错误。校验值进行异或得到的结果有以下几种:

  1. 全为0表示数据无错误;
  2. 一半为1时,表示出现了1bit的错误,新得到的校验值与读取到的校验值中的CP5、CP3、CP1进行异或得到的3bit数据为错误位的列地址,RP2log2n-1、…、RP5、RP3、RP1进行异或得到的数据为错误位的行地址;
  3. 只有1位为1表示空闲区域出现错误;
  4. 其它情况则说明至少2bit数据错误。

       一般器件出现2bit及以上的错误很少见,因此汉明码编码的ECC校验基本上够用。

       一般EXMC模块中NAND Flash对应的Bank都包含带有ECC算法的硬件模块,因此ECC算法的使用实际上通过寄存器完成配置即可。

注意,NAND Flash芯片不会提供ECC计算的,只提供了空闲区域而已。ECC算法使用需要在存入数据时在微控制器中计算,并写入对应的空闲区域中。然后再读出时重新计算,并与写入值进行比较。

如果觉得上面的ECC算法很难理解,实际上可以看一下老鼠试毒这篇文章,原理有点像,通过有限位的变化,反推出大量数据中某个数据的变化。

FTL

       NAND Flash在生产和使用过程中都有可能产生坏块,并且每个块的擦除次数有限,即超过一定次数后将无法擦除,也就产生了坏块(注意Flash只能由1变0,因此必须擦除才能正常写入)。坏块的存在使得NAND Flash物理地址不连续,而微控制器访问存储设备时要求地址连续,否则无法通过地址直接读写存储设备,因此一般添加闪存转换层FTL(Flash Translation Layer)完成对NAND Flash的操作。FTL的功能如下:

  1. 标记坏块
           当通过读写数据或ECC校验检测出坏块时,需要将其标记以不再对该区域进行读写操作。坏块的标记一般是将空闲区域的第一个字节写入非0xFF的值来表示。
  2. 地址映射管理
           FTL将逻辑地址映射到NAND Flash中可读写的物理地址,并创建相应的映射表,当处理器对相应的逻辑地址读写数据时,实际上是通过FTL读写Nand Flash中对应的物理地址,如下图所示。由于坏块导致的物理地址不连续,逻辑地址对应的物理地址不固定,逻辑地址1可能对应物理地址5,逻辑地址3可能对应物理地址2

在这里插入图片描述
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)) //读写数据

ECC

写入数据后获取ECC并写入相应区域

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;

  ……
}

读出数据、获取ECC并重新计算ECC,校验

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

ECC校正

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

FTL

标记某一个块为坏块

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表

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;
}
  • 2
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值