• 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)) //读写数据
    
    • 1
    • 2
    • 3
    • 4

    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;
    
      ……
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    读出数据、获取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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    标记某一个块已经使用

    u32 FTLSetBlockUseFlag(u32 blockNum)
    {
      u8 flag = 0xCC;
      return NandWriteSpare(blockNum, 0, 1, (u8*)&flag, 1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    逻辑块号转换为物理块号

    u32 FTLLogicNumToPhysicalNum(u32 logicNum)
    {
      if(logicNum > s_structFTLDev.blockTotalNum)
      {
        return INVALID_ADDR;
      }
      else
      {
        return s_structFTLDev.lut[logicNum];
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    创建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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
  • 相关阅读:
    LeetCode【45】跳跃游戏2
    AnyLabeling标定及转化成labelmaskID
    这里有篇Charles详细教程,看完后就可以把Fiddler卸载了
    SpringBoot如何优雅的输出异常信息?
    Java算法解题小记
    Webpack Bundle Analyzer包分析器
    代理类型升级,APISIX 支持 Kafka 作为上游
    (附源码)ssm人才市场招聘信息系统 毕业设计 271621
    Html5API(自定义属性、媒体元素、canvas画布)(一)
    DSA之查找(1):线性表的查找
  • 原文地址:https://blog.csdn.net/weixin_47447179/article/details/126023616