【正点原子STM32】FSMC_FMC——NAND FLASH实验(存储原理、存储结构、坏块管理和磨损均衡、控制命令、FMC-NAND FLASH接口、NAND FLASH驱动步骤)

一、NAND FLASH介绍

二、FMC-NAND FLASH接口介绍

三、NAND FLASH驱动步骤
四、编程实战
五、总结

一、NAND FLASH介绍

1.1、NAND FLASH简介

在这里插入图片描述
NAND Flash是一种非易失性存储器,常用于嵌入式系统中作为主要存储介质。与NOR Flash相比,NAND Flash的特点是价格更低、密度更高,但速度较慢,适合用于大容量数据存储和读取的场景。

以下是关于NAND Flash的一些重要特点和工作原理:

  1. 结构和特点:

    • NAND Flash存储单元以页(Page)为单位进行擦除和编程,一般为512字节到4KB不等。
    • 存储密度高,成本相对较低,适合用于大容量存储。
    • 读取速度相对较慢,擦除和编程速度也较慢,通常以毫秒为单位。
  2. 工作原理:

    • 读取操作:通过向NAND Flash发送读取命令和地址,然后从数据线接收数据。
    • 编程操作:将数据通过数据线发送到NAND Flash,并发送编程命令和地址,将数据编程到指定的页中。
    • 擦除操作:擦除操作是以块(Block)为单位进行的,需要先发送擦除命令和地址,然后执行擦除操作。
  3. 控制器和接口:

    • 与NAND Flash通信的控制器通常由外部的控制器芯片或者嵌入式系统的主控芯片实现。
    • 控制器通过与NAND Flash的命令、地址和数据线进行通信来执行读取、编程和擦除等操作。
  4. 错误纠正和管理:

    • 由于NAND Flash存在擦写次数限制和数据错误的可能性,通常需要实现错误纠正和管理机制,例如使用ECC(Error Correction Code)来检测和纠正数据错误。

总的来说,NAND Flash在嵌入式系统中扮演着重要角色,特别是对于需要大容量存储和相对低成本的应用场景。然而,需要注意的是,NAND Flash的操作相对复杂,需要正确的控制和管理以确保数据的可靠性和稳定性。
在这里插入图片描述

NAND FLASH的存储原理

在这里插入图片描述
NAND Flash的存储原理基于浮栅场效应晶体管(Floating Gate Field-Effect Transistor)技术,这种技术允许在晶体管内部存储电荷,从而表示数据的状态。以下是NAND Flash存储原理的基本概述:

  1. 浮动栅极存储:

    • NAND Flash中的存储单元是浮动栅极晶体管,每个存储单元都有一个浮动栅极,其状态由储存的电荷量决定。
    • 通过向控制栅极施加正电压,电子就可以通过氧化层进入浮动栅极,当电子数量超过一定阈值时,晶体管被认为处于逻辑0的状态;反之,当电子数量低于阈值时,晶体管被认为处于逻辑1的状态。
  2. 写入操作:

    • 写入操作是通过向NAND Flash的控制栅极施加正电压来实现的。当需要写入0时,电子被注入到浮动栅极中,增加了其电荷量,使得晶体管处于导通状态;当需要写入1时,控制栅极施加的电压不足以注入电子,浮动栅极保持高电荷量,晶体管处于断开状态。
  3. 擦除操作:

    • NAND Flash的擦除操作是以块(Block)为单位进行的,需要将整个块中的所有存储单元都擦除。
    • 擦除操作是通过施加较高的电压到晶体管的衬底来实现的,这导致浮动栅极的电荷被清除,使晶体管恢复到未写入数据的状态。
  4. 存储颗粒分类:

    • NAND Flash存储颗粒主要分为单层单元(Single-Level Cell,SLC)、多层单元(Multi-Level Cell,MLC)、三层单元(Triple-Level Cell,TLC)等。
    • SLC存储单元只能存储一个比特(0或1),相比之下,MLC和TLC可以在每个存储单元中存储更多的位数,但其写入速度和数据保持能力相对较低。

总的来说,NAND Flash通过控制浮动栅极中的电荷状态来实现数据的存储和擦除操作,这种技术使得NAND Flash具有高存储密度、较低成本和相对较快的读取速度,因此在许多应用中被广泛使用。
在这里插入图片描述
MT29F4G08是一款NAND Flash存储器芯片,具有以下功能和引脚定义:

  1. CE# (Chip Enable):芯片使能信号,用于选中NAND芯片以启用操作。

  2. CLE (Command Latch Enable):命令锁存使能,表示写入的是命令。

  3. ALE (Address Latch Enable):地址锁存使能,表示写入的是地址。

  4. WE# (Write Enable):写使能信号,用于启动写入操作。

  5. RE# (Read Enable):读使能信号,用于启动读取操作。

  6. WP# (Write Protect):写保护信号,用于控制写入操作的保护状态。

  7. R/B (Ready/Busy):就绪/忙信号,用于指示芯片的操作状态,例如编程或擦除是否完成。

  8. I/Ox:地址/数据 输入/输出口,用于传输数据和地址信息。

以上引脚定义中,#符号表示引脚低电平有效,无#表示引脚高电平有效。根据具体的操作类型,通过设置相应的引脚使能信号,可以发送命令、地址和数据到NAND Flash芯片,实现读取、写入和擦除等操作。
在这里插入图片描述
NAND Flash MT29F4G08的存储结构如下:

  • 块(Block):NAND Flash由多个块组成,每个块是擦除的最小单位。通常,一个块包含多个页。

  • 页(Page):页是数据读写的最小单位。每个页由两部分组成:

    1. 数据存储区(Data Area):用于存储有效数据的区域。对于MT29F4G08,数据存储区的大小为2KB。

    2. 备用区域(Spare Area):用于存储额外的信息,如ECC(纠错码)校验值、坏块管理信息等。备用区域的大小为64字节,用于实现坏块管理和磨损均衡等功能。

NAND Flash的工作原理是通过对存储单元的读取和写入来实现数据存储和擦除操作。每个存储单元都由浮置栅极组成,根据电荷状态表示存储的数据位。在数据读取和写入过程中,需要考虑到块、页和备用区域的结构,以确保数据的正确读取和存储,同时实现坏块管理和数据可靠性保护。

坏块管理和磨损均衡

坏块管理和磨损均衡是在 NAND Flash 存储器中常见的管理策略,用于延长存储器的寿命和提高可靠性。

  1. 坏块管理

    • 坏块识别:在 NAND Flash 制造过程中,由于工艺等原因,会产生一些不良块(坏块)。这些坏块无法正常读写,因此需要在使用前进行识别和标记。
    • 坏块映射:识别后的坏块需要进行映射,通常会使用一个额外的坏块表(Bad Block Table)来记录坏块的位置,以便在文件系统或存储控制器中进行管理。
  2. 磨损均衡

    • 数据分布均衡:尽量保持数据在存储器中的分布均匀,避免频繁写入同一块或同一组块,以减缓块的擦写速度,延长存储器寿命。
    • 热点数据管理:管理可能形成的热点数据,减少对热点数据的访问次数,避免某些块的过度使用。
    • 块擦写次数平衡:监测每个块的擦写次数,对擦写次数较高的块进行数据迁移操作,将数据迁移到擦写次数较低的块上,平衡块的使用状况。
    • 擦除空闲块:及时回收已经擦除的空闲块,并确保它们在下一次写入操作时被合理地利用,减少特定块的过度使用。

这些管理策略通常由存储控制器、文件系统或专门的管理软件来实现。在设计系统时,需要考虑存储器的特性和应用场景,选择合适的管理策略,以达到最佳的均衡效果。

1.2、NAND FLASH寻址

在这里插入图片描述
NAND Flash 的寻址机制确实是关键的部分,它决定了如何在存储器中定位和操作数据。根据你提供的信息,我们可以进一步解释 NAND Flash 的寻址方式:

  1. 列地址(Column Address)

    • 列地址用于在一个页(Page)内部寻址,它决定了所选取的数据位于所选页内的哪一列。
    • 对于 MT29F4G08,列地址需要 12 根地址线(CA0~CA11),因为每个页的数据长度为 2112 字节。
  2. 页地址(Page Address)

    • 页地址用于在一个块(Block)内部寻址,它决定了所选取的数据位于所选块内的哪一页。
    • 对于 MT29F4G08,页地址需要 6 根地址线(PA0~PA5),因为每个块包含 64 个页。
  3. 块地址(Block Address)

    • 块地址用于进行块级寻址,它决定了所选取的数据位于哪一个块内。
    • 对于 MT29F4G08,块地址需要 12 根地址线(BA6~BA17),因为总共有 4096 个块。

发送这些地址信息,通常需要多个时钟周期,并且需要根据 NAND Flash 的规格来确保地址的正确传输和解释。这些地址线的组合决定了要访问的具体位置,允许读取或写入所需的数据。

对于NAND Flash的寻址,通常需要按照一定的时序来发送列地址、页地址和块地址。在发送地址时,一般会遵循以下步骤:

  1. 第一个周期:发送列地址(Column Address)。
  2. 第二个周期:发送页地址的低位(Low Page Address)。
  3. 第三个周期:发送页地址的高位(High Page Address)。
  4. 第四个周期:发送块地址的低位(Low Block Address)。
  5. 第五个周期:发送块地址的高位(High Block Address)。

这五个周期分别用来传输地址的不同部分,以确定要访问的具体位置。在每个周期内,会根据 NAND Flash 的规格和通信协议来发送相应的地址数据,并且需要确保在时序上符合要求,以保证地址的正确传输和解释。
在这里插入图片描述
根据您提供的信息,我们可以按照以下步骤计算要访问的地址:

  1. 计算页内地址(Column Address)

    • 您提到要访问的位置在第64页中的第1208字节处,因此列地址为1208,转换为十六进制为0x4B8。
  2. 计算页地址(Row Address)

    • 每个块中有64页,因此第100个块对应的页地址为64 * 100 = 6400。然后加上64页,即6400 + 64 = 6464页。
    • 由于您提到块地址从0开始,因此块地址与页地址相同。但是在实际寻址时,还要考虑块地址的高位和低位。
  3. 组合地址

    • 将页地址和列地址组合成完整的地址。页地址和列地址都需要转换为十六进制。
    • 最终的地址为0x1940_04B8。

这样,您就可以通过这个地址来访问您所需的数据。在实际操作中,还需要根据NAND Flash的通信协议和时序要求来发送这个地址。

SPARE区域如何访问?

对于NAND Flash中的SPARE区域,访问方式与访问数据存储区类似,但需要通过相应的地址来访问。已经正确地计算了访问SPARE区域的地址。

根据您提供的信息,要访问SPARE区域,您需要以下地址信息:

  • 列地址(Column Address):SPARE区域的大小通常与数据存储区域的大小相同,因此列地址等于页大小,即2048,或者十六进制表示为0x800。
  • 页地址(Row Address):根据您的计算,页地址为0x1940。

因此,要访问SPARE区域,您可以发送地址0x1940_0800。在实际操作中,发送这个地址后,您可以读取或写入SPARE区域中的数据。

1.3、NAND FLASH控制命令

在这里插入图片描述
这些NAND Flash的控制命令用于执行各种操作,包括读取、写入、擦除和其他控制功能。下面是每个命令的简要说明:

  1. READ ID (0x90):读取NAND的ID和相关特性,可用于识别设备和获取其规格信息。

  2. SET FEATURE (0xEF):设置NAND的相关参数,如时序模式、IO驱动能力等。

  3. RESET (0xFF):复位NAND,将其状态恢复到初始状态。

  4. READ STATUS (0x70):读取NAND的状态,比如可判断编程/擦除是否完成等。

  5. READ PAGE (0x00 / 0x30):用于读取一个page的数据,指令由两部分组成,通常与列地址和页地址一起使用。

  6. PROGRAM PAGE (0x80 / 0x10):用于写入一个page的数据,指令由两部分组成,通常与列地址和页地址一起使用。

  7. ERASE BLOCK (0x60 / 0xD0):用于擦除一个block,指令由两部分组成,通常与块地址一起使用。

  8. Read for internal data move (0x00 / 0x35):这两个指令组成NAND的内部数据移动操作,支持跨页拷贝一个page到另一个page,且支持拷贝时写入数据。

  9. Program for internal data move (0x85 / 0x10):用于内部数据移动操作中的写入数据操作,指令由两部分组成,通常与列地址和页地址一起使用。

这些命令组成了NAND Flash的基本操作指令集,可实现对存储器的各种读写和控制功能。
在这里插入图片描述

READ PAGE 命令 (00h-30h)

在这里插入图片描述
READ PAGE命令的执行过程如下:

  1. 发送命令:首先发送命令0x00h,表示开始读取一个page的数据。
  2. 发送地址:接着发送5次地址信息,包括块地址、页地址和列地址,用于指定要读取的页面的位置。
  3. 发送命令:发送命令0x30h,表示确认读取操作开始。
  4. 等待:等待一定的RDY时间,以确保NAND Flash准备好提供数据。
  5. 读取数据:读取页面内的数据,同时进行ECC校验以确保数据的完整性。

需要注意的是,由于NAND Flash的特性,不能跨页读取数据,因此每次最多只能读取一个page的数据,包括其中的spare区域。

PROGRAM PAGE 命令(80h-10h)

在这里插入图片描述
PROGRAM PAGE命令的执行步骤如下:

  1. 发送命令:首先发送命令0x80h,表示开始写入一个page的数据。
  2. 发送地址:接着发送5次地址信息,包括块地址、页地址和列地址,用于指定要写入的页面的位置。
  3. 等待:等待一定的tADL时间,确保NAND Flash准备好接收数据。
  4. 写入数据:发送需要写入的数据,写入的数据长度大于ECC的页大小,因此需要一并写入ECC值。
  5. 发送命令:发送命令0x10h,表示确认写入操作开始。
  6. 等待:等待一定的tPROG时间,确保数据写入完成。
  7. 发送读状态命令:发送READ STATUS命令,查询NAND FLASH状态,以确认写入操作是否完成。

需要注意的是,NAND Flash只能写0,不能写1。因此,在写入数据之前,需要先擦除对应的block,以确保所有数据位都被置为1。

ERASE BLOCK 命令(60h-D0h)

在这里插入图片描述
ERASE BLOCK命令的执行步骤如下:

  1. 发送命令:首先发送命令0x60h,表示开始擦除一个block的数据。
  2. 发送地址:接着发送3次地址信息,包括块地址,用于指定要擦除的block的位置。
  3. 发送命令:发送命令0xD0h,表示确认擦除操作开始。
  4. 等待:等待一定的tBERS时间,确保NAND Flash完成擦除操作。
  5. 发送读状态命令:发送READ STATUS命令,查询NAND FLASH状态,以确认擦除操作是否完成。

在擦除一个block之前,需要确保该block中的所有数据都已经无效,以免数据丢失。

INTERNAL DATA MOVE 命令(00h-35h && 85h-10h):用于NAND内部进行数据移动(页对页)

在这里插入图片描述
INTERNAL DATA MOVE命令的执行步骤如下:

  1. 发送READ FOR INTERNAL DATA MOVE命令(00h-35h):在这个过程中,指定源地址(源数据)。
  2. 查询状态:发送READ STATUS命令或查询RB信号,等待操作准备就绪。
  3. 发送PROGRAM FOR INTERNAL DATA MOVE命令(85h-10h):在这个过程中,指定目标地址。
  4. 等待:等待一定的tPROG时间,确保NAND Flash完成数据移动操作。
  5. 再次查询状态:发送READ STATUS命令,等待操作完成。

需要注意的是,此命令只能在同一个plane中进行数据拷贝,即源地址和目标地址必须在同一个plane内。在内部数据移动过程中,也可以写入数据。当发送命令0x10h时,开始进行拷贝操作。

时序参数

在这里插入图片描述
关于NAND Flash操作所涉及的时序参数。这些参数的含义和作用:

  1. tADL (Address to Data Load):从发送完地址到数据传输开始所需的最小时间。这个参数确保在发送完地址后,芯片能够准备好接收数据,并开始传输数据。在设计系统时,需要确保这个时间足够长,以确保数据传输的稳定性和可靠性。

  2. tPROG (Program Time):写入一页数据完成所需的时间范围。在页编程期间,NAND Flash会将数据写入到指定页,并执行必要的擦除和写入操作。这个时间范围通常在200到600微秒之间,具体取决于NAND Flash的型号和制造工艺。

  3. tBERS (Block Erase Time):擦除一个块所需的时间范围。在擦除块时,NAND Flash会将块中的所有页擦除为1。这个时间范围通常在0.7到3毫秒之间,具体取决于NAND Flash的型号和制造工艺。

  4. tWHR (Write to Read Hold Time):从写入操作切换到读取操作所需的最小时间。这个参数确保在进行写入操作后,系统能够稳定地切换到读取操作,并确保读取的数据准确性和一致性。

  5. tRHW (Read to Write Hold Time):从读取操作切换到写入操作所需的最小时间。这个参数确保在进行读取操作后,系统能够稳定地切换到写入操作,并确保写入的数据准确性和一致性。

这些时序参数在设计和操作NAND Flash时非常重要,确保了系统对NAND Flash的操作能够在时序上满足芯片的要求,从而保证了数据的可靠性和稳定性。

对于正确配置和操作FMC(Flexible Memory Controller)的时序参数,让我们来分析一下它们的含义和作用:

  1. tCLR (Command to Data Output Time):发送完命令到数据输出需要的最小时间。在发送完命令后,芯片需要一定时间准备好输出数据。这个参数确保了数据的稳定性和可靠性。

  2. tAR (Address to Data Output Time):发送完地址到数据输出需要的最小时间。与tCLR类似,这个参数确保了在发送完地址后,芯片能够准备好输出数据。

  3. tCLS/tALS (CLE/ALE Setup Time):CLE/ALE信号建立时间。在发送CLE(命令锁存使能)和ALE(地址锁存使能)信号之前,需要一定的时间确保信号的稳定性和准确性。

  4. tRP/tWP (Read/Write Pulse Time):读/写周期的低电平时间。这个参数定义了读取或写入数据时,控制信号保持在低电平的最小时间。这确保了在读/写操作期间,控制信号的稳定性。

  5. tCLH/tALH (CLE/ALE Hold Time):CLE/ALE信号保持时间。在发送CLE/ALE信号后,需要一定的时间确保信号保持在稳定状态,以确保正确识别和处理命令或地址。

这些时序参数的正确配置是确保FMC正常运行和数据传输的关键。合理设置这些参数可以提高系统对外部存储器的访问效率和可靠性。

1.4、NAND FLASH难点

在这里插入图片描述
坏块标记和ECC校验是确保 NAND FLASH 数据可靠性的重要手段。让我们逐步分析如何实现这些功能:

  1. 坏块标记:由于 NAND FLASH 存储器存在擦写次数限制,当某个块的擦写次数达到一定阈值后,就会出现坏块。因此,需要在初始化阶段进行坏块识别和标记。一种常见的方法是使用块擦除前后的校验方式,如果块在擦除后的状态与预期不符,则将其标记为坏块,并记录在特定的坏块表中。FTL(Flash Translation Layer)层可以负责管理坏块信息,并在进行数据存储时避开这些坏块。

  2. ECC校验:ECC(Error Correction Code)校验用于检测和纠正数据在读取过程中出现的错误。在写入数据时,需要使用特定的算法计算出数据的 ECC 值,并将其与数据一同存储在 NAND FLASH 中的备用区域。在读取数据时,将数据和相应的 ECC 值一同读出,并使用相同的算法重新计算 ECC 值。然后,将重新计算的 ECC 值与存储的 ECC 值进行比较。如果两者相符,表示数据未发生错误;如果不符,则表示数据发生了错误。根据错误的严重程度,可以选择进行错误纠正或者丢弃数据。

  3. 代码实现:为了实现 ECC 校验和坏块标记功能,需要在 MCU 上编写相应的代码。这些代码包括 ECC 算法的实现、坏块检测和标记逻辑的实现,以及与 NAND FLASH 基础接口的交互。通常,厂商会提供针对具体 NAND FLASH 芯片的驱动程序或者库函数,开发者可以基于这些驱动程序进行二次开发,以满足自己的需求。

通过合理的坏块管理和 ECC 校验,可以确保 NAND FLASH 存储器在长时间使用过程中的数据可靠性和稳定性。

坏块管理(1、识别标记坏块 2、转换表 3、保留区)

在这里插入图片描述
坏块管理是确保 NAND FLASH 存储器可靠性的重要一环,需要实现以下功能:

  1. 坏块识别和标记:坏块识别是指在初始化或运行时检测 NAND FLASH 中的坏块,并将其标记为不可用。常见的识别方法包括:

    • 读取厂家预留的坏块标记:在每个块的第一个和第二个页的备用区域的第一个字节中,厂家会写入非 0xFF 的特殊值来标记坏块。
    • 写入-读取测试:向每个块写入数据(例如 0xFF 或 0x00),然后再读取出来,并检查数据是否完全一致。不一致的块即为坏块。
    • ECC 检验:在读取数据时,通过 ECC 校验来检测块中是否存在错误。如果 ECC 错误超过阈值,则将该块标记为坏块。
  2. 转换表:转换表是一个映射表,用于将逻辑地址(LBA)映射到物理地址(PBA)。在坏块管理中,由于坏块的存在,需要在逻辑块地址和物理块地址之间建立映射关系,以保证数据的连续性和正确性。转换表存储在非易失性存储器中,例如 MCU 的闪存或者 NAND FLASH 的保留区。

  3. 保留区:保留区是 NAND FLASH 的一部分,用于存储元数据、转换表和其他系统信息,以保护数据的完整性和可靠性。保留区通常包括坏块表、转换表、ECC 校验码等信息,以及预留的备用块用于替换坏块。

在实现坏块管理时,需要考虑算法的复杂度和性能开销,以及对系统资源的占用情况。一个高效的坏块管理系统应该能够快速地识别和标记坏块,并且能够动态地管理转换表和保留区,以适应 NAND FLASH 存储器的变化和使用情况。

2、转换表

在这里插入图片描述
逻辑地址-物理地址转换表(LUT表)在 NAND FLASH 存储器中起着至关重要的作用,它的功能是将逻辑地址映射到物理地址,并确保不指向坏块的物理地址。这个转换表通常存储在 NAND FLASH 的保留区域中,以便在系统上电后能够重新构建。

转换表的作用包括:

  1. 逻辑地址到物理地址的映射:文件系统通过逻辑地址来访问文件,而实际的数据存储在 NAND FLASH 的物理地址上。转换表记录了每个逻辑地址对应的物理地址,确保了文件系统能够正确地读写数据。
  2. 坏块处理:转换表中记录的物理地址必须是有效的、未损坏的块。当出现坏块时,转换表需要更新,将坏块对应的逻辑地址映射到其他未损坏的物理地址上。这样可以确保文件系统不会受到坏块的影响。
  3. 数据一致性:转换表的更新必须是原子性的,即要么完全成功,要么完全失败。这样可以保证文件系统在进行数据读写时始终能够获取到正确的物理地址,确保数据的一致性和可靠性。

综上所述,逻辑地址-物理地址转换表是确保文件系统正常运行的关键之一,它需要在系统启动时进行初始化,并在运行过程中动态地维护和更新,以适应 NAND FLASH 存储器的使用和变化情况。
在这里插入图片描述

3、保留区

在这里插入图片描述
保留区在 NAND FLASH 存储器中具有重要作用,主要用于两个方面:替代坏块和实现磨损均衡。

  1. 替代坏块:当 NAND FLASH 存储器中的某个块出现坏块时,保留区中的未使用块可以被用来替代坏块,以确保系统的正常运行。这样可以保证文件系统不受坏块影响,提高系统的可靠性和稳定性。

  2. 实现磨损均衡:由于 NAND FLASH 存储器存在擦写次数的限制,为了延长整个存储器的寿命并减少块之间的擦写次数差异,需要实现磨损均衡。保留区中预留了足够多的未使用块,用于在进行数据复写时提供新的块,从而减少某些块的频繁擦写,达到均衡擦写次数的目的。

使用保留区实现磨损均衡的过程通常包括:

  • 数据复写:文件系统需要往某个已经写过数据的块中写入新数据时,通常需要先擦除该块,然后再写入新数据,这个过程会导致较长的延迟和额外的擦写操作。
  • 页拷贝功能:利用 NAND FLASH 存储器的内部数据移动指令,可以将一个块中的数据以页为单位拷贝到另一个空闲块中,并写入新的数据,而无需读出整个块的数据。这样可以大大提高写入速度,并减少额外的擦写操作,同时也实现了对块之间擦写次数的均衡分配。

综上所述,保留区在 NAND FLASH 存储器中扮演着重要角色,通过合理利用保留区的未使用块,可以有效应对坏块和擦写次数不均衡等问题,提高系统的可靠性和性能。

ECC校验

在这里插入图片描述
ECC校验(Error Correction Code)是一种用于检测和纠正数据传输中错误的算法。在 NAND FLASH 存储器中,由于存储单元的串行组织结构以及信号衰减等因素,读取数据时可能会出现错误。ECC算法能够检测出这些错误,并尝试修正错误的数据位,提高数据的可靠性。

在 NAND FLASH 存储器中,常用的 ECC 算法有汉明码(Hamming Code)、RS码(Reed Solomon Code)和BCH码(Bose-Chaudhuri-Hocquenghem Code)。在 STM32 的 FMC 模块中,硬件支持 ECC 计算,使用的是汉明码算法。汉明码虽然无法对错误进行修正,但可以检测出错误的位置,从而提高数据的可靠性。

对于 ECC 算法的应用,需要按照一定的规则生成 ECC 校验数据,并将其与原始数据一起存储。在读取数据时,将读取到的数据与相应的 ECC 校验数据进行比较,以检测出错误并尝试修正错误的数据位。

以 STM32 的 FMC 模块为例,它支持按照不同的页大小(如256、512、1024、2048、4096和8192字节)进行 ECC 计算。对于512字节为单位的数据,通常只需要24bit的 ECC 校验数据来表示,这样就可以检测出并纠正数据传输中的错误。

汉明码

在这里插入图片描述
在这里插入图片描述

汉明码是一种经典的错误检测和纠正编码方式,在数据传输中广泛应用。对于 NAND FLASH 存储器,汉明码被用于计算 ECC 值,以提高数据的可靠性。

汉明码的计算纠错过程如下:

  1. 数据分割和奇偶校验

    • 原始数据包按照指定的分割比例(1/2、1/4、1/8)进行分割,然后分别计算每部分的奇偶校验值,得到奇校验值(ECCo)和偶校验值(ECCe)。
  2. 存储ECC值

    • 将得到的奇偶校验值与原始数据一起存储到 NAND FLASH 中。
  3. 读取数据和重新计算ECC值

    • 在读取数据时,会重新计算 ECC 值,并将其与存储的 ECC 值进行比较,以检测数据传输中的错误。
  4. 检测错误

    • 如果比较后发现异常,则意味着数据传输中出现了错误,需要进行纠正。
  5. 定位错误位

    • 首先,将所有存储的和重新计算的 ECC 值以及它们的补码(nECC)进行按位异或运算,得到一个结果(temp)。如果结果全为1,则表示有1个bit出错。
    • 然后,通过对比原有的奇校验值(ECCo)和新的奇校验值(nECCo)的异或运算,找到出错的bit位置。
  6. 纠正错误

    • 最后,对于出错的数据的bit位进行取反,即与1进行异或操作,得到正确的数据。

这个过程确保了在数据传输中发生错误时,可以通过汉明码进行检测和纠正,从而提高数据的可靠性和完整性。
在这里插入图片描述

二、FMC-NAND FLASH接口介绍

2.1、FMC-NAND FLASH控制器简介

2.2、FMC-NAND FLASH访问参数介绍在这里插入图片描述

FMC(Flexible Memory Controller)是一种功能强大的存储器控制器,能够处理多种外部存储器的读写操作,并将其与微控制器内部的地址操作相对应。针对 NAND FLASH 存储器,FMC 负责将内部的地址操作转换为外部存储器的时序读写操作。

在读操作中,FMC 控制器负责读取微控制器内部的寄存器,并生成一个时序来从 NAND FLASH 中读取数据。而在写操作中,当微控制器向内部地址 0x8000 0000 写入数据时,FMC 控制器会根据配置控制外部引脚,以产生符合 NAND FLASH 时序要求的信号,并将数据发送到 NAND FLASH。

FMC 的关键在于配置正确的时序寄存器,以确保生成的时序满足外部存储器的访问时间要求。这些时序参数包括读写周期的持续时间、地址建立时间、数据输出延迟等,通过正确配置这些参数,FMC 能够与外部存储器有效地进行通信,实现可靠的数据读写操作。

简单理解外设FMC与NAND FLASH通信过程

在这里插入图片描述
在这段代码中,对外设 FMC 与 NAND FLASH 的通信过程进行了简单的描述:

  1. 地址设置: (0x80000000 | 1 << 16) 这一部分代码设置了地址,将地址 0x80000000 中的第 16 位(假设为控制位)设置为 1,以指示向 NAND FLASH 发送命令。

  2. 数据发送: cmd 表示要发送给 NAND FLASH 的命令数据。这个命令数据可以是命令、地址或实际数据,具体取决于与 NAND FLASH 的通信所需的操作类型。根据情况,这个数据可以是指定的命令、目标地址或实际的读/写数据。

综合起来,这段代码的含义是向外设 FMC 发送了一个指定命令或数据,通过 FMC 控制器与 NAND FLASH 进行通信。根据具体的需求,可以使用不同的地址和数据来执行不同的 NAND FLASH 操作,如发送命令、写入数据或读取数据等。

FMC接口的NAND FLASH/PC卡控制器具有如下特点:

在这里插入图片描述
在这里插入图片描述
FMC 接口的 NAND FLASH/PC 卡控制器的特点如下:

  1. 两个存储区域: 可以独立配置的两个存储区域,提供了灵活性和可定制性。
  2. 支持的位宽: 支持8位和16位的 NAND FLASH 存储器,以及16位 PC 卡兼容设备,适应了不同存储器的需求。
  3. 硬件 ECC 计算支持: 支持硬件 ECC 计算,使用汉明码进行错误检测,提高了数据的可靠性。
  4. 预等待功能: 支持 NAND FLASH 的预等待功能,可以提高访问速度和效率。
  5. 存储区配置: 给出了 MT29F4G08 存储区域 3 的起始地址为 0x80000000。
  6. 地址表示:
    • 代表发送命令的地址为 0x80000000 | 1 << 16,用于向 NAND FLASH 发送命令。
    • 代表发送地址的地址为 0x80000000 | 1 << 17,用于向 NAND FLASH 发送地址信息。
    • 代表发送数据的地址为 0x80000000,用于向 NAND FLASH 发送数据。

综上所述,FMC 接口的 NAND FLASH/PC 卡控制器具有灵活的存储配置、多种位宽支持、硬件 ECC 计算、预等待功能等特点,适用于不同的存储器应用场景,并提供了简单易用的地址表示方式。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在 FMC-NAND FLASH 访问中,以下是一些关键的参数及其对应的器件时序参数:

  1. MEMxSET: 设置时间,对应到器件上的时序参数是 tCLS/tALS。这个参数指定了从地址设置到数据输出的时间,即命令或地址在输出之前的稳定时间。

  2. MEMxWAIT: 等待时间,对应到器件上的时序参数是 tRP/tWP。这个参数指定了读取周期或写入周期的低电平时间,即在命令/地址之后,等待操作完成的时间。

  3. MEMxHOLD: 保持时间,对应到器件上的时序参数是 tCLH/tALH。这个参数指定了命令或地址在失效之前的保持时间,即在数据输出之后,保持命令或地址的时间。

  4. MEMxHIZ: 高阻态时间,对应到器件上的时序参数是 tCLS/tALS-tDS。这个参数指定了从命令或地址输出变为高阻态的时间,即在地址或命令失效后,引脚进入高阻态的时间。

这些参数在 FMC-NAND FLASH 访问中非常重要,确保了与外部 NAND FLASH 设备之间的稳定和可靠的通信。通过合适地设置这些参数,可以保证读取和写入操作的正确性和稳定性。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.3、STM32的FMC控制器硬件ECC

在这里插入图片描述
STM32的FMC控制器硬件ECC功能提供了对NAND FLASH数据的错误检测能力,但并不提供数据修复功能。用户在使用时需要注意以下几点:

  1. ECC计算和存储: 在写入数据时,需要开启STM32的硬件ECC计算功能,并将计算得到的ECC值存储在NAND FLASH数据所存放的Page的spare区。这样,在读取数据时,可以通过比较新计算的ECC值和之前存储的ECC值来检测数据是否发生错误。

  2. 错误检测和数据修复: 当读取数据时,硬件ECC会重新计算一个ECC值,并与之前存储的ECC值进行比较。如果两者不相等,说明读取的数据可能存在错误。在这种情况下,用户需要实现错误检测和数据修复的逻辑,例如重新读取数据,或者使用备份数据进行修复。

  3. 存储区域限制: 需要注意的是,STM32的硬件ECC仅支持存储区域2和存储区域3,不支持存储区域4。因此,在配置FMC时需要确保选择的存储区域符合硬件ECC的要求。

通过合理地利用STM32的硬件ECC功能,并结合适当的错误检测和数据修复策略,可以提高系统对NAND FLASH数据的可靠性和稳定性。

2.4、FMC-NAND FLASH控制器相关寄存器介绍

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.5、FMC-NAND FLASH控制器相关HAL库函数介绍

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

三、NAND FLASH驱动步骤

在这里插入图片描述
NAND FLASH驱动步骤基本配置和使用NAND FLASH的基本流程:

  1. 配置FMC: 在配置FMC时,除了使能FMC时钟并配置相关的IO和时钟使能外,还需要设置FMC的时序参数,以确保与NAND FLASH的通信满足时序要求。这些时序参数通常在FMC_SDRAM_Timing_Init函数中进行配置。

  2. 设置NAND FLASH相关参数: 在设置NAND FLASH相关参数时,除了调用HAL_NAND_Init进行初始化外,还应该根据NAND FLASH的具体要求配置控制参数和时间参数。这些参数包括页大小、块大小、时序参数等。在调用HAL_NAND_Init函数之前,需要先设置NAND FLASH的控制参数和时间参数。这些参数包括芯片的型号、页大小、块大小、ECC校验等。这些参数的设置需要根据具体的NAND FLASH型号和芯片规格来确定。

  3. 使能存储区域的操作: 在初始化NAND FLASH之后,需要调用__FMC_NAND_ENABLE函数来使能存储区域的操作。这个操作通常在HAL_NAND_Init函数内部完成,您不需要显式调用。

  4. 访问NAND FLASH器件: 在访问NAND FLASH器件时,需要根据NAND FLASH的操作规范,向其发送命令、地址和数据。这些操作需要通过特定的地址来实现,如你所示,使用的是0x8000 0000作为基地址,并通过设置相应的位来控制命令和地址的传输。在发送地址时需要区分列地址和行地址,并且需要根据NAND FLASH的页大小和块大小进行正确的地址计算。

总体而言,列出的步骤已经涵盖了配置和使用NAND FLASH的主要流程,可以作为在STM32中使用NAND FLASH的基本指南。但是在实际应用中需要注意一些细节,并根据具体的硬件和需求进行适当的调整和优化。

四、编程实战

在这里插入图片描述

NAND FLASH工程 - 不用FTL层

nand.c

#include <stdio.h>
#include "./BSP/NAND/nand.h"
#include "./SYSTEM/delay/delay.h"
#include "./MALLOC/malloc.h"


NAND_HandleTypeDef g_nand_handle;     /* NAND FLASH句柄 */
nand_attriute nand_dev;               /* nand重要参数结构体 */

/**
 * @brief       初始化NAND FLASH
 * @param       无
 * @retval      0,成功; 1,失败
 */
uint8_t nand_init(void)
{
    FMC_NAND_PCC_TimingTypeDef comspacetiming, attspacetiming;

    g_nand_handle.Instance              = FMC_NAND_DEVICE;                      /* NAND设备 */
    g_nand_handle.Init.NandBank         = FMC_NAND_BANK3;                       /* NAND挂在BANK3上 */
    g_nand_handle.Init.Waitfeature      = FMC_NAND_PCC_WAIT_FEATURE_DISABLE;    /* 关闭等待特性 */
    g_nand_handle.Init.MemoryDataWidth  = FMC_NAND_PCC_MEM_BUS_WIDTH_8;         /* 8位数据宽度 */
    g_nand_handle.Init.EccComputation   = FMC_NAND_ECC_DISABLE;                 /* 不使用FMC的ECC功能,需要用到再使能 */
    g_nand_handle.Init.ECCPageSize      = FMC_NAND_ECC_PAGE_SIZE_512BYTE;       /* ECC页大小为512B */
    g_nand_handle.Init.TCLRSetupTime    = 1;    /* 设置TCLR(tCLR=CLE到RE的延时)=(TCLR+TSET+2)*THCLK,THCLK=1/180M=5.5ns */
    g_nand_handle.Init.TARSetupTime     = 1;    /* 设置TAR(tAR=ALE到RE的延时)=(TAR+TSET+2)*THCLK,THCLK=1/180M=5.5n */
   
    comspacetiming.SetupTime        = 2;    /* 建立时间 */
    comspacetiming.WaitSetupTime    = 3;    /* 等待时间 */
    comspacetiming.HoldSetupTime    = 2;    /* 保持时间 */
    comspacetiming.HiZSetupTime     = 1;    /* 高阻态时间 */
    
    attspacetiming.SetupTime        = 2;    /* 建立时间 */
    attspacetiming.WaitSetupTime    = 3;    /* 等待时间 */
    attspacetiming.HoldSetupTime    = 2;    /* 保持时间 */
    attspacetiming.HiZSetupTime     = 1;    /* 高阻态时间 */
    
    HAL_NAND_Init(&g_nand_handle, &comspacetiming, &attspacetiming);    /* NANDFLASH初始化 */
    
    nand_reset();                           /* 复位NAND */
    delay_ms(100);
    nand_dev.id = nand_readid();            /* 读取ID */
    nand_modeset(4);                        /* 设置为MODE4,高速模式 */

    if (nand_dev.id == MT29F16G08ABABA)     /* NAND为MT29F16G08ABABA */
    {
        nand_dev.page_totalsize = 4320;     /* nand一个page的总大小(包括spare区) */
        nand_dev.page_mainsize  = 4096;     /* nand一个page的有效数据区大小 */
        nand_dev.page_sparesize = 224;      /* nand一个page的spare区大小 */
        nand_dev.block_pagenum  = 128;      /* nand一个block所包含的page数目 */
        nand_dev.plane_blocknum = 2048;     /* nand一个plane所包含的block数目 */
        nand_dev.block_totalnum = 4096;     /* nand的总block数目 */
    }
    else if (nand_dev.id == MT29F4G08ABADA) /* NAND为MT29F4G08ABADA */
    {
        nand_dev.page_totalsize = 2112;     /* nand一个page的总大小(包括spare区) */
        nand_dev.page_mainsize  = 2048;     /* nand一个page的有效数据区大小 */
        nand_dev.page_sparesize = 64;       /* nand一个page的spare区大小 */
        nand_dev.block_pagenum  = 64;       /* nand一个block所包含的page数目 */
        nand_dev.plane_blocknum = 2048;     /* nand一个plane所包含的block数目 */
        nand_dev.block_totalnum = 4096;     /* nand的总block数目 */
    }
    else
    {
        return 1;   /* 错误 */
    }
    
    return 0;       /* 成功 */
}

/**
 * @brief       NAND FALSH底层驱动,引脚配置,时钟使能
 * @note        此函数会被HAL_NAND_Init()调用
 * @param       hnand  : nand句柄
 * @retval      无
 */
void HAL_NAND_MspInit(NAND_HandleTypeDef *hnand)
{
    GPIO_InitTypeDef gpio_init_struct;
    
    __HAL_RCC_FMC_CLK_ENABLE();                       /* 使能FMC时钟 */
    __HAL_RCC_GPIOD_CLK_ENABLE();                     /* 使能GPIOD时钟 */
    __HAL_RCC_GPIOE_CLK_ENABLE();                     /* 使能GPIOE时钟 */
    __HAL_RCC_GPIOG_CLK_ENABLE();                     /* 使能GPIOG时钟 */

    gpio_init_struct.Pin = NAND_RB_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_INPUT;          /* 输入 */
    gpio_init_struct.Pull = GPIO_PULLUP;              /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_HIGH;         /* 高速 */
    HAL_GPIO_Init(NAND_RB_GPIO_PORT, &gpio_init_struct);

    gpio_init_struct.Pin = GPIO_PIN_9;
    gpio_init_struct.Mode = GPIO_MODE_AF_PP;          /* 输入 */
    gpio_init_struct.Pull = GPIO_NOPULL;              /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_HIGH;         /* 高速 */
    gpio_init_struct.Alternate = GPIO_AF12_FMC;       /* 复用为FMC */
    HAL_GPIO_Init(GPIOG, &gpio_init_struct);  

    /* 初始化PD0,1,4,5,11,12,14,15 */
    gpio_init_struct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5 | \
                           GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_14 | GPIO_PIN_15;
    HAL_GPIO_Init(GPIOD, &gpio_init_struct);

    /* 初始化PE7,8,9,10*/
    gpio_init_struct.Pin = GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10;
    HAL_GPIO_Init(GPIOE, &gpio_init_struct);
}

/**
 * @brief       设置NAND速度模式
 * @param       mode    : 0~5, 表示速度模式
 * @retval      0,成功; 其他,失败
 */
uint8_t nand_modeset(uint8_t mode)
{   
    /* 参考MT29F4G08器件手册P50 SET FEATURE操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_FEATURE; /* 向NAND FLASH发送设置特性指令 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = 0X01;         /* 向NAND FLASH发送 设置时序模式 的地址 */
    *(volatile uint8_t *)NAND_ADDRESS               = mode;         /* P1参数,设置mode */
    *(volatile uint8_t *)NAND_ADDRESS               = 0;
    *(volatile uint8_t *)NAND_ADDRESS               = 0;
    *(volatile uint8_t *)NAND_ADDRESS               = 0;

    if (nand_wait_for_ready() == NSTA_READY)
    {
        return 0;   /* 成功 */
    }
    else 
    {
        return 1;   /* 失败 */
    }
}

/**
 * @brief       读取NAND FLASH的ID
 * @note        不同的NAND略有不同,请根据自己所使用的NAND FALSH数据手册来编写函数
 * @param       无
 * @retval      NAND FLASH的ID值
 */
uint32_t nand_readid(void)
{
    uint8_t deviceid[5]; 
    uint32_t id;
    
    /* 参考MT29F4G08器件手册P35 READ ID操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_READID;  /* 向NAND FLASH发送读取ID命令 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = 0X00;         /* 向NAND FLASH发送读取地址00 */

    /* ID信息一共有5个字节(MT29F4G08ABADA) */
    deviceid[0] = *(volatile uint8_t *)NAND_ADDRESS;   /* 2Ch */
    deviceid[1] = *(volatile uint8_t *)NAND_ADDRESS;   /* DCh */
    deviceid[2] = *(volatile uint8_t *)NAND_ADDRESS;   /* 90h */
    deviceid[3] = *(volatile uint8_t *)NAND_ADDRESS;   /* 95h */
    deviceid[4] = *(volatile uint8_t *)NAND_ADDRESS;   /* 56h */

    /* 镁光的NAND FLASH的ID一共5个字节,但是为了方便我们只取4个字节组成一个32位的ID值
       根据NAND FLASH的数据手册,只要是镁光的NAND FLASH,那么一个字节ID的第一个字节都是0X2C
       所以我们就可以抛弃这个0X2C,只取后面四字节的ID值。*/
    id = ((uint32_t)deviceid[1]) << 24 | ((uint32_t)deviceid[2]) << 16 | ((uint32_t)deviceid[3]) << 8 | deviceid[4];

    return id;
}

/**
 * @brief       读NAND状态
 * @param       无
 * @retval      NAND状态值
 *              bit0:0,成功; 1,错误(编程/擦除/READ)
                bit6:0,Busy; 1,Ready
 */
uint8_t nand_readstatus(void)
{
    volatile uint8_t data = 0;
    
    /* 参考MT29F4G08器件手册P55 READ STATUS操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_READSTA;  /* 向NAND FLASH发送读状态命令,便于判断是否处于空闲 */
    nand_delay(NAND_TWHR_DELAY);                                    /* 等待tWHR时间后,NAND FLASH返回状态值 */
    data = *(volatile uint8_t *)NAND_ADDRESS;                       /* 读取状态值,状态值的定义参考器件手册P54 */

    return data;
}

/**
 * @brief       等待NAND准备好
 * @param       无
 * @retval      NSTA_TIMEOUT  等待超时了
 *              NSTA_READY    已经准备好
 */
uint8_t nand_wait_for_ready(void)
{
    uint8_t status = 0;
    volatile uint32_t time = 0;

    while (1)                           /* 等待ready */
    {
        status = nand_readstatus();     /* 获取状态值 */

        if (status & NSTA_READY)        /* 判断NAND FLASH是否准备好 */
        {
            break;
        }

        time++;

        if (time >= 0X1FFFFFFF)
        {
            return NSTA_TIMEOUT;        /* 超时 */
        }
    }

    return NSTA_READY;                  /* 准备好 */
}

/**
 * @brief       复位NAND
 * @param       无
 * @retval      0,成功; 其他,失败
 */
uint8_t nand_reset(void)
{
    /* 参考MT29F4G08器件手册P34 RESET 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_RESET; /* 复位NAND */

    if (nand_wait_for_ready() == NSTA_READY)
    {
        return 0;   /* NAND FLASH复位成功 */
    }
    else
    {
        return 1;   /* NAND FLASH复位失败 */
    }
}

/**
 * @brief       等待RB信号为某个电平
 * @param       rb: 0,等待RB==0;
 *                  1,等待RB==1;
 * @retval      0,成功; 1,超时
 */
uint8_t nand_waitrb(volatile uint8_t rb)
{
    volatile uint32_t time = 0;

    while (time < 0X1FFFFFF)
    {
        time++;

        if (NAND_RB == rb)
        {
            return 0;
        }
    }

    return 1;
}

/**
 * @brief       NAND延时
 * @note        一个i++至少需要4ns
 * @param       i       : 等待的时间
 * @retval      无
 */
void nand_delay(volatile uint32_t i)
{
    while (i > 0)
    {
        i--;
    }
}

/**
 * @brief       读取NAND Flash的指定页指定列的数据(main区和spare区都可以使用此函数)
 * @param       pagenum         : 要读取的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 要读取的列开始地址(也就是页内地址),范围:0~(page_totalsize-1)
 * @param       *pbuffer        : 指向数据存储区
 * @param       numbyte_to_read : 读取字节数(不能跨页读)
 * @retval      0,成功; 其他,错误代码
 */
uint8_t nand_readpage(uint32_t pagenum, uint16_t colnum, uint8_t *pbuffer, uint16_t numbyte_to_read)
{
    volatile uint16_t i = 0;
    uint8_t res = 0;
    uint8_t eccnum = 0;     /* 需要计算的ECC个数,每NAND_ECC_SECTOR_SIZE字节计算一个ecc */
    uint8_t eccstart = 0;   /* 第一个ECC值所属的地址范围 */
    uint8_t errsta = 0;
    uint8_t *p;
    
    /* 参考MT29F4G08器件手册P65 READ PAGE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_AREA_A;              /* 向NAND FLASH发送READ PAGE的第一个命令 */
    /* 发送5个字节地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)colnum;          /* 发送第一个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(colnum >> 8);   /* 发送第二个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)pagenum;         /* 发送第一个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 8);  /* 发送第二个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 16); /* 发送第三个字节的页地址 */
    
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_AREA_TRUE1;           /* 向NAND FLASH发送读取READ PAGE的第二个命令 */

    /* 下面两行代码是等待R/B引脚变为低电平,其实主要起延时作用的,等待NAND操作R/B引脚。因为我们是通过
       将STM32的NWAIT引脚(NAND的R/B引脚)配置为普通IO,代码中通过读取NWAIT引脚的电平来判断NAND是否准备
       就绪的。这个也就是模拟的方法,所以在速度很快的时候有可能NAND还没来得及操作R/B引脚来表示NAND的忙
       闲状态,结果我们就读取了R/B引脚,这个时候肯定会出错的,事实上确实是会出错!大家也可以将下面两行
       代码换成延时函数,只不过这里我们为了效率所以没有用延时函数。 */
    res = nand_waitrb(0);       /* 等待RB=0 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }

    /* 下面2行代码是真正判断NAND是否准备好的 */
    res = nand_waitrb(1);       /* 等待RB=1 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }

    if (numbyte_to_read % NAND_ECC_SECTOR_SIZE)     /* 不是NAND_ECC_SECTOR_SIZE的整数倍,不进行ECC校验 */
    { 
        for (i = 0;i < numbyte_to_read; i++)        /* 读取NAND FLASH中的值 */
        {
            *(volatile uint8_t *)pbuffer++ = *(volatile uint8_t *)NAND_ADDRESS;
        }
    }
    else
    {
        eccnum = numbyte_to_read / NAND_ECC_SECTOR_SIZE;    /* 得到ecc计算次数 */
        eccstart = colnum / NAND_ECC_SECTOR_SIZE;           /* 第一个ECC值所属的地址范围 */
        p = pbuffer;

        for (res = 0; res < eccnum; res++)
        {
            FMC_Bank2_3->PCR3 |= 1 << 6;                    /* 使能ECC校验 */

            for (i = 0; i < NAND_ECC_SECTOR_SIZE; i++)      /* 读取NAND_ECC_SECTOR_SIZE个数据 */
            {
                *(volatile uint8_t *)pbuffer++ = *(volatile uint8_t *)NAND_ADDRESS;
            }

            while (!(FMC_Bank2_3->SR3 & (1 << 6)));         /* 等待FIFO空 */

            nand_dev.ecc_hdbuf[res + eccstart] = FMC_Bank2_3->ECCR3;    /* 读取硬件计算后的ECC值 */
            FMC_Bank2_3->PCR3 &= ~(1 << 6);                 /* 禁止ECC校验 */
        } 

        i = nand_dev.page_mainsize + 0X10 + eccstart * 4;   /* 从spare区的0X10位置开始读取之前存储的ecc值 */
        nand_delay(NAND_TRHW_DELAY);                        /* 等待tRHW */
        
        /* 参考MT29F4G08器件手册P123 RANDOM DATA READ 操作时序 */
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = 0X05;             /* 随机读指令 */
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)i;       /* 发送ecc所在的列地址 */
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(i >> 8);
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = 0XE0;             /* 开始读数据 */

        nand_delay(NAND_TWHR_DELAY);                            /* 等待tWHR */
        pbuffer = (uint8_t *)&nand_dev.ecc_rdbuf[eccstart];     /* 把读取过程中的ECC值暂存在pbuffer */
  
        for (i = 0; i < 4 * eccnum; i++)    /* 读取保存在spare的ECC值 */
        {
            *(volatile uint8_t *)pbuffer++ = *(volatile uint8_t *)NAND_ADDRESS;
        }
  
        for (i = 0; i < eccnum; i++)        /* 检验ECC */
        {
            if (nand_dev.ecc_rdbuf[i + eccstart] != nand_dev.ecc_hdbuf[i + eccstart])   /* 不相等,需要校正 */
            {
                printf("err hd,rd:0x%x,0x%x\r\n", nand_dev.ecc_hdbuf[i + eccstart], nand_dev.ecc_rdbuf[i + eccstart]); 
                printf("eccnum,eccstart:%d,%d\r\n", eccnum, eccstart);
                printf("PageNum,ColNum:%d,%d\r\n", pagenum, colnum);
                res = nand_ecc_correction(p + NAND_ECC_SECTOR_SIZE * i, nand_dev.ecc_rdbuf[i + eccstart], nand_dev.ecc_hdbuf[i + eccstart]);    /* ECC校验 */

                if (res)
                {
                    errsta = NSTA_ECC2BITERR;   /* 标记2BIT及以上ECC错误 */
                }
                else
                {
                    errsta = NSTA_ECC1BITERR;   /* 标记1BIT ECC错误 */
                }
            }
        }
    }

    if (nand_wait_for_ready() != NSTA_READY)
    {
        errsta = NSTA_ERROR;    /* 失败 */
    }

    return errsta;              /* 成功 */
}

/**
 * @brief       读取NAND Flash的指定页指定列的数据(main区和spare区都可以使用此函数),并对比(FTL管理时需要)
 * @param       pagenum         : 要读取的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 要读取的列开始地址(也就是页内地址),范围:0~(page_totalsize-1)
 * @param       cmpval          : 要对比的值,以uint32_t为单位
 * @param       numbyte_to_read : 读取字数(以4字节为单位,不能跨页读)
 * @param       numbyte_equal   : 从初始位置持续与CmpVal值相同的数据个数
 * @retval      0,成功; 其他,错误代码
 */
uint8_t nand_readpagecomp(uint32_t pagenum, uint16_t colnum, uint32_t cmpval, uint16_t numbyte_to_read, uint16_t *numbyte_equal)
{
    uint16_t i = 0;
    uint8_t res = 0;
    
    /* 参考MT29F4G08器件手册P65 READ PAGE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_AREA_A;              /* 向NAND FLASH发送READ PAGE的第一个命令 */
    /* 发送5个字节的地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)colnum;          /* 发送第一个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(colnum >> 8);   /* 发送第二个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)pagenum;         /* 发送第一个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 8);  /* 发送第二个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 16); /* 发送第三个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_AREA_TRUE1;          /* 向NAND FLASH发送读取READ PAGE的第二个命令 */

    /**
     * 下面两行代码是等待R/B引脚变为低电平,其实主要起延时作用的,等待NAND操作R/B引脚。因为我们是通过
     * 将STM32的NWAIT引脚(NAND的R/B引脚)配置为普通IO,代码中通过读取NWAIT引脚的电平来判断NAND是否准备
     * 就绪的。这个也就是模拟的方法,所以在速度很快的时候有可能NAND还没来得及操作R/B引脚来表示NAND的忙
     * 闲状态,结果我们就读取了R/B引脚,这个时候肯定会出错的,事实上确实是会出错!大家也可以将下面两行
     * 代码换成延时函数,只不过这里我们为了效率所以没有用延时函数。
     */
    res = nand_waitrb(0);       /* 等待RB=0 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }

    /* 下面2行代码是真正判断NAND是否准备好的 */
    res = nand_waitrb(1);       /* 等待RB=1 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }

    for (i = 0; i < numbyte_to_read; i++)       /* 读取数据,每次读4字节 */
    {
        if (*(volatile uint32_t *)NAND_ADDRESS != cmpval)
        {
            break;  /* 如果有任何一个值,与cmpval不相等,则退出 */
        }
    }

    *numbyte_equal = i;     /* 与cmpval值相同的个数 */

    if (nand_wait_for_ready() != NSTA_READY)
    {
        return NSTA_ERROR;  /* 失败 */
    }

    return 0;               /* 成功 */
}

/**
 * @brief       在NAND一页中写入指定个字节的数据(main区和spare区都可以使用此函数)
 * @param       pagenum         : 要写入的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 要写入的列开始地址(也就是页内地址),范围:0~(page_totalsize-1)
 * @param       pbuffer         : 指向数据存储区
 * @param       numbyte_to_write: 要写入的字节数,该值不能超过该页剩余字节数!!!
 * @retval      0,成功; 其他,错误代码
 */
uint8_t nand_writepage(uint32_t pagenum, uint16_t colnum, uint8_t *pbuffer, uint16_t numbyte_to_write)
{
    volatile uint16_t i = 0;  
    uint8_t res = 0;
    uint8_t eccnum = 0;     /* 需要计算的ECC个数,每NAND_ECC_SECTOR_SIZE字节计算一个ecc */
    uint8_t eccstart = 0;   /* 第一个ECC值所属的地址范围 */
    
    /* 参考MT29F4G08器件手册P72 PROGRAM PAGE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_WRITE0;              /* 向NAND FLASH发送PRAGRAM PAGE的第一个命令 */
    /* 发送5个字节的地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)colnum;          /* 发送第一个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(colnum >> 8);   /* 发送第二个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)pagenum;         /* 发送第一个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 8);  /* 发送第二个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 16); /* 发送第三个字节的页地址 */
    
    nand_delay(NAND_TADL_DELAY);    /* 等待tADL */

    if (numbyte_to_write % NAND_ECC_SECTOR_SIZE)    /* 不是NAND_ECC_SECTOR_SIZE的整数倍,不进行ECC校验 */
    {  
        for (i = 0; i < numbyte_to_write; i++)      /* 写入数据 */
        {
            *(volatile uint8_t *)NAND_ADDRESS = *(volatile uint8_t *)pbuffer++;
        }
    }
    else
    {
        eccnum = numbyte_to_write / NAND_ECC_SECTOR_SIZE;   /* 得到ecc计算次数 */
        eccstart = colnum / NAND_ECC_SECTOR_SIZE;           /* 第一个ECC值所属的地址范围 */

        for (res = 0; res < eccnum; res++)
        {
            FMC_Bank2_3->PCR3 |= 1 << 6;                        /* 使能ECC校验 */

            for (i = 0; i < NAND_ECC_SECTOR_SIZE; i++)          /* 写入NAND_ECC_SECTOR_SIZE个数据 */
            {
                *(volatile uint8_t *)NAND_ADDRESS = *(volatile uint8_t *)pbuffer++;
            }
            while (!(FMC_Bank2_3->SR3 & (1 << 6)));             /* 等待FIFO空 */

            nand_dev.ecc_hdbuf[res+eccstart] = FMC_Bank2_3->ECCR3;    /* 读取硬件计算后的ECC值 */

            FMC_Bank2_3->PCR3 &= ~(1 << 6);                     /* 禁止ECC校验 */
        }

        i = nand_dev.page_mainsize + 0X10 + eccstart * 4;       /* 计算写入ECC的spare区地址 */
        
        nand_delay(NAND_TADL_DELAY);    /* 等待tADL */
        
        /* 参考MT29F4G08器件手册P93 PROGRAM PAGE和RANDOM DATA INPUT 操作时序 */
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = 0X85;          /* 向NAND FLASH发送随机写指令 */
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)i;   /* 发送ECC所在的列地址 */
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(i >> 8);
        
        nand_delay(NAND_TADL_DELAY);    /* 等待tADL */
        
        pbuffer = (uint8_t *)&nand_dev.ecc_hdbuf[eccstart];

        for (i = 0; i < eccnum; i++)    /* 写入ECC */
        { 
            for (res = 0; res < 4; res++)
            {
                *(volatile uint8_t *)NAND_ADDRESS = *(volatile uint8_t *)pbuffer++;
            }
        }
    }
    
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_WRITE_TURE1;  /* 向NAND FLASH发送PRAGRAM PAGE的第二个命令 */
    delay_us(NAND_TPROG_DELAY); /* 等待tPROG */

    if (nand_wait_for_ready() != NSTA_READY)
    {
        return NSTA_ERROR;      /* 失败 */
    }

    return 0;                   /* 成功 */
}

/**
 * @brief       在NAND一页中的指定地址开始,写入指定长度的恒定数字
 * @param       pagenum         : 要写入的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 要写入的列开始地址(也就是页内地址),范围:0~(page_totalsize-1)
 * @param       cval            : 要写入的指定常数
 * @param       numbyte_to_write: 要写入的字节数(以4字节为单位)
 * @retval      0,成功; 其他,错误代码
 */
uint8_t nand_write_pageconst(uint32_t pagenum, uint16_t colnum, uint32_t cval, uint16_t numbyte_to_write)
{
    uint16_t i = 0;
    
    /* 参考MT29F4G08器件手册P72 PROGRAM PAGE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_WRITE0;              /* 向NAND FLASH发送PRAGRAM PAGE的第一个命令 */
    /* 发送5个字节的地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)colnum;          /* 发送第一个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(colnum >> 8);   /* 发送第二个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)pagenum;         /* 发送第一个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 8);  /* 发送第二个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 16); /* 发送第三个字节的页地址 */

    nand_delay(NAND_TADL_DELAY);                /* 等待tADL */

    for (i = 0; i < numbyte_to_write; i++)      /* 写入数据,每次写4字节 */
    {
        *(volatile uint32_t *)NAND_ADDRESS = cval;
    }

    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_WRITE_TURE1;          /* 向NAND FLASH发送PRAGRAM PAGE的第二个命令 */
    delay_us(NAND_TPROG_DELAY);     /* 等待tPROG */

    if (nand_wait_for_ready() != NSTA_READY)
    {
        return NSTA_ERROR;  /* 失败  */
    }

    return 0;               /* 成功 */
}

/**
 * @brief       将一页数据拷贝到另一页,不写入新数据
 * @note        源页和目的页要在同一个Plane内!
 * @param       source_pagenum  : 源页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       dest_pagenum    : 目的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @retval      0,成功; 其他,错误代码
 */
uint8_t nand_copypage_withoutwrite(uint32_t source_pagenum, uint32_t dest_pagenum)
{
    uint8_t res = 0;
    uint16_t source_block = 0, dest_block = 0;
    /* 判断源页和目的页是否在同一个plane中 */
    source_block = source_pagenum / nand_dev.block_pagenum;
    dest_block = dest_pagenum / nand_dev.block_pagenum;

    if ((source_block % 2) != (dest_block % 2))
    {
        return NSTA_ERROR;  /* 不在同一个plane内 */
    }
    
    /* 参考MT29F4G08器件手册P80 READ FOR INTERNAL DATA MOVE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_MOVEDATA_CMD0;   /* 向NAND FLASH发送READ FOR INTERNAL DATA MOVE的第一个命令 */
    /* 发送源页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)0;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)0;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)source_pagenum;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(source_pagenum >> 8);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(source_pagenum >> 16);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_MOVEDATA_CMD1;   /* 向NAND FLASH发送READ FOR INTERNAL DATA MOVE的第二个命令 */
    
    /**
     *   下面两行代码是等待R/B引脚变为低电平,其实主要起延时作用的,等待NAND操作R/B引脚。因为我们是通过
     *   将STM32的NWAIT引脚(NAND的R/B引脚)配置为普通IO,代码中通过读取NWAIT引脚的电平来判断NAND是否准备
     *   就绪的。这个也就是模拟的方法,所以在速度很快的时候有可能NAND还没来得及操作R/B引脚来表示NAND的忙
     *   闲状态,结果我们就读取了R/B引脚,这个时候肯定会出错的,事实上确实是会出错!大家也可以将下面两行
     *   代码换成延时函数,只不过这里我们为了效率所以没有用延时函数。
    */
    res = nand_waitrb(0);       /* 等待RB=0 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }
    
    /* 下面2行代码是真正判断NAND是否准备好的 */
    res = nand_waitrb(1);       /* 等待RB=1 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }
    
    /* 参考MT29F4G08器件手册P129 INTERNAL DATA MOVE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_MOVEDATA_CMD2;   /* 向NAND FLASH发送INTERNAL DATA MOVE的第一个命令 */
    /* 发送目的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)0;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)0;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)dest_pagenum;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(dest_pagenum >> 8);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(dest_pagenum >> 16);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_MOVEDATA_CMD3;    /* 向NAND FLASH发送INTERNAL DATA MOVE的第二个命令 */
    
    delay_us(NAND_TPROG_DELAY);     /* 等待tPROG  */

    if (nand_wait_for_ready() != NSTA_READY)
    {
        return NSTA_ERROR;          /* NAND未准备好 */
    }

    return 0;   /* 成功 */
}

/**
 * @brief       将一页数据拷贝到另一页,并且可以写入数据
 * @note        源页和目的页要在同一个Plane内!
 * @param       source_pagenum  : 源页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       dest_pagenum    : 目的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 页内列地址,范围:0~(page_totalsize-1)
 * @param       pbuffer         : 要写入的数据
 * @param       numbyte_to_write: 要写入的数据个数
 * @retval      0,成功; 其他,错误代码
 */
uint8_t nand_copypage_withwrite(uint32_t source_pagenum, uint32_t dest_pagenum, uint16_t colnum, uint8_t *pbuffer, uint16_t numbyte_to_write)
{
    uint8_t res = 0;
    volatile uint16_t i = 0;
    uint16_t source_block = 0, dest_block = 0;  
    uint8_t eccnum = 0;     /* 需要计算的ECC个数,每NAND_ECC_SECTOR_SIZE字节计算一个ecc */
    uint8_t eccstart = 0;   /* 第一个ECC值所属的地址范围 */
    /* 判断源页和目的页是否在同一个plane中 */
    source_block = source_pagenum / nand_dev.block_pagenum;
    dest_block = dest_pagenum / nand_dev.block_pagenum;

    if ((source_block % 2) != (dest_block % 2))
    {
        return NSTA_ERROR;      /* 不在同一个plane内 */
    }
    
    /* 参考MT29F4G08器件手册P80 READ FOR INTERNAL DATA MOVE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_MOVEDATA_CMD0;   /* 向NAND FLASH发送READ FOR INTERNAL DATA MOVE的第一个命令 */
    /* 发送源页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)0;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)0;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)source_pagenum;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(source_pagenum >> 8);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(source_pagenum >> 16);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_MOVEDATA_CMD1;    /* 向NAND FLASH发送READ FOR INTERNAL DATA MOVE的第二个命令 */

    /**
     * 下面两行代码是等待R/B引脚变为低电平,其实主要起延时作用的,等待NAND操作R/B引脚。因为我们是通过
     * 将STM32的NWAIT引脚(NAND的R/B引脚)配置为普通IO,代码中通过读取NWAIT引脚的电平来判断NAND是否准备
     * 就绪的。这个也就是模拟的方法,所以在速度很快的时候有可能NAND还没来得及操作R/B引脚来表示NAND的忙
     * 闲状态,结果我们就读取了R/B引脚,这个时候肯定会出错的,事实上确实是会出错!大家也可以将下面两行
     * 代码换成延时函数,只不过这里我们为了效率所以没有用延时函数。
     */
    res = nand_waitrb(0);       /* 等待RB=0 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }

    /* 下面2行代码是真正判断NAND是否准备好的 */
    res = nand_waitrb(1);       /* 等待RB=1 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }
    
    /* 参考MT29F4G08器件手册P129 INTERNAL DATA MOVE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_MOVEDATA_CMD2;    /* 向NAND FLASH发送READ FOR INTERNAL DATA MOVE的第一个命令 */
    /* 发送目的页地址 和 页内列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)colnum;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(colnum >> 8);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)dest_pagenum;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(dest_pagenum >> 8);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(dest_pagenum >> 16);

    nand_delay(NAND_TADL_DELAY);    /* 等待tADL */

    if (numbyte_to_write % NAND_ECC_SECTOR_SIZE)    /* 不是NAND_ECC_SECTOR_SIZE的整数倍,不进行ECC校验 */
    {  
        for (i = 0; i < numbyte_to_write; i++)      /* 写入数据 */
        {
            *(volatile uint8_t *)NAND_ADDRESS = *(volatile uint8_t *)pbuffer++;
        }
    }
    else
    {
        eccnum = numbyte_to_write / NAND_ECC_SECTOR_SIZE;   /* 得到ecc计算次数 */
        eccstart = colnum / NAND_ECC_SECTOR_SIZE;           /* 第一个ECC值所属的地址范围 */

        for (res = 0; res < eccnum; res++)
        {
            FMC_Bank2_3->PCR3 |= 1 << 6;                    /* 使能ECC校验 */

            for (i = 0; i < NAND_ECC_SECTOR_SIZE; i++)      /* 写入NAND_ECC_SECTOR_SIZE个数据 */
            {
                *(volatile uint8_t *)NAND_ADDRESS = *(volatile uint8_t *)pbuffer++;
            }

            while (!(FMC_Bank2_3->SR3 & (1<<6)));                     /* 等待FIFO空 */

            nand_dev.ecc_hdbuf[res + eccstart] = FMC_Bank2_3->ECCR3;  /* 读取硬件计算后的ECC值 */

            FMC_Bank2_3->PCR3 &= ~(1 << 6);                           /* 禁止ECC校验 */
        }

        i = nand_dev.page_mainsize + 0X10 + eccstart * 4;       /* 计算写入ECC的spare区地址 */
        nand_delay(NAND_TADL_DELAY);                            /* 等待tADL */
        
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = 0X85;  /* 随机写指令 */
        /* 发送地址 */
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)i;
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(i >> 8);
        nand_delay(NAND_TADL_DELAY);        /* 等待tADL */

        pbuffer = (uint8_t *)&nand_dev.ecc_hdbuf[eccstart];

        for (i = 0; i < eccnum; i++)        /* 写入ECC */
        { 
            for (res = 0; res < 4; res++)
            {
                *(volatile uint8_t *)NAND_ADDRESS = *(volatile uint8_t *)pbuffer++;
            }
        }
    }

    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_MOVEDATA_CMD3;     /* 发送命令0X10 */
    delay_us(NAND_TPROG_DELAY);     /* 等待tPROG */

    if (nand_wait_for_ready() != NSTA_READY)
    {
        return NSTA_ERROR;  /* 失败 */
    }

    return 0;               /* 成功 */ 
}

/**
 * @brief       读取spare区中的数据
 * @param       pagenum         : 要写入的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 要写入的spare区地址(spare区中哪个地址),范围:0~(page_sparesize-1)
 * @param       pbuffer         : 接收数据缓冲区
 * @param       numbyte_to_read : 要读取的字节数(不大于page_sparesize)
 * @retval      0,成功; 其他,错误代码
 */
uint8_t nand_readspare(uint32_t pagenum, uint16_t colnum, uint8_t *pbuffer, uint16_t numbyte_to_read)
{
    uint8_t temp = 0;
    uint8_t remainbyte = 0;
    remainbyte = nand_dev.page_sparesize - colnum;

    if (numbyte_to_read > remainbyte) 
    {
        numbyte_to_read = remainbyte;   /* 确保要写入的字节数不大于spare剩余的大小 */
    }

    temp = nand_readpage(pagenum, colnum + nand_dev.page_mainsize, pbuffer, numbyte_to_read);   /* 读取数据 */
    return temp;
}

/**
 * @brief       向spare区中写数据
 * @param       pagenum         : 要写入的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 要写入的spare区地址(spare区中哪个地址),范围:0~(page_sparesize-1)
 * @param       pbuffer         : 要写入的数据首地址
 * @param       numbyte_to_write: 要写入的字节数(不大于page_sparesize)
 * @retval      0,成功; 其他,失败
 */
uint8_t nand_writespare(uint32_t pagenum, uint16_t colnum, uint8_t *pbuffer, uint16_t numbyte_to_write)
{
    uint8_t temp = 0;
    uint8_t remainbyte = 0;

    remainbyte = nand_dev.page_sparesize - colnum;

    if (numbyte_to_write > remainbyte)
    {
        numbyte_to_write = remainbyte;  /* 确保要读取的字节数不大于spare剩余的大小 */
    }

    temp = nand_writepage(pagenum, colnum + nand_dev.page_mainsize, pbuffer, numbyte_to_write); /* 读取 */

    return temp;
}

/**
 * @brief       擦除一个块
 * @param       blocknum        : 要擦除的BLOCK编号,范围:0-(block_totalnum-1)
 * @retval      0,擦除成功; 其他,擦除失败
 */
uint8_t nand_eraseblock(uint32_t blocknum)
{
     /* 将块地址转换为页地址 */
    if (nand_dev.id == MT29F16G08ABABA)
    {
        blocknum <<= 7;
    }
    else if (nand_dev.id == MT29F4G08ABADA)
    {
        blocknum <<= 6;
    }
    
    /* 参考MT29F4G08器件手册P77 ERASE BLOCK 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_ERASE0;  /* 向NAND FLASH发送ERASE BLOCK的第一个命令 */ 
    /* 发送块地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)blocknum;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(blocknum >> 8);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(blocknum >> 16);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_ERASE1;  /* 向NAND FLASH发送ERASE BLOCK的第二个命令 */ 

    delay_ms(NAND_TBERS_DELAY);     /* 等待擦除成功 */

    if (nand_wait_for_ready() != NSTA_READY)
    {
        return NSTA_ERROR;  /* 失败 */
    }

    return 0;               /* 成功 */
}

/**
 * @brief       全片擦除NAND FLASH
 * @param       无
 * @retval      无
 */
void nand_erasechip(void)
{
    uint8_t status;
    uint16_t i = 0;

    for (i = 0; i < nand_dev.block_totalnum; i++)   /* 循环擦除所有的块 */
    {
        status = nand_eraseblock(i);

        if (status)
        {
            printf("Erase %d block fail!!,错误码为%d\r\n",i,status);  /* 擦除失败 */
        }
    }
}

/**
 * @brief       获取ECC的奇数位/偶数位
 * @param       oe      : 0,偶数位; 1,奇数位
 * @param       eccval  : 输入的ecc值
 * @retval      计算后的ecc值(最多16位)
 */
uint16_t nand_ecc_get_oe(uint8_t oe, uint32_t eccval)
{
    uint8_t i;
    uint16_t ecctemp = 0;

    for (i = 0; i < 24; i++)
    {
        if ((i % 2) == oe)      /* 通过oe传参把奇数位置和偶数位置区分 */
        {
            /* 把eccval当前位的位置数据做判断 */
            if ((eccval >> i) & 0X01)       /* 该位置为1 */
            {
                ecctemp += 1 << (i >> 1);   /* 当前位为1的情况下,(i >> 1)即eccval该位位置相对于ecctemp的位位置
                                               而 1 << (i >> 1)即对acctemp相应位置置1
                                             */
            }
            /* 该位置为0,不做处理 */
        }
    }

    return ecctemp;
}

/**
 * @brief       ECC校正函数(修正1bit错误,并报告2bit及以上错误)
 * @param       data_buf    : 数据缓存区
 * @param       eccrd       : 读取出来, 原来保存的ECC值
 * @param       ecccl       : 读取数据时, 硬件计算的ECC值
 * @retval      0,错误已修正; 其他,ECC错误(有大于2个bit的错误,无法恢复)
 */
uint8_t nand_ecc_correction(uint8_t* data_buf, uint32_t eccrd, uint32_t ecccl)
{
    uint16_t eccrdo, eccrde, eccclo, ecccle;
    uint16_t eccchk = 0;
    uint16_t errorpos = 0;
    uint32_t bytepos = 0;
    
    eccrdo = nand_ecc_get_oe(1, eccrd);     /* 获取eccrd的奇数位 */
    eccrde = nand_ecc_get_oe(0, eccrd);     /* 获取eccrd的偶数位 */
    eccclo = nand_ecc_get_oe(1, ecccl);     /* 获取ecccl的奇数位 */
    ecccle = nand_ecc_get_oe(0, ecccl);     /* 获取ecccl的偶数位 */
    eccchk = eccrdo^eccrde^eccclo^ecccle;   /* 4个ECC数值进行按位异或 */

    if (eccchk == 0XFFF)                    /* 全1,说明只有1bit ECC错误 */
    {
        errorpos = eccrdo^eccclo;           /* 出错bit位置可通过原有eccrdo和eccclo进行按位异或求得 */
        printf("errorpos:%d\r\n", errorpos); 
        bytepos = errorpos / 8;             /* 计算字节位置 */
        data_buf[bytepos] ^= 1 << (errorpos % 8);   /* 对出错位进行取反,修正错误 */
    }
    else                                    /* 不是全1,说明至少有2bit ECC错误,无法修复 */
    {
        printf("2bit ecc error or more\r\n");       /* 不全是1,说明至少有2bit ECC错误,无法修复 */
        return 1;
    } 

    return 0;
}

nand.h
nandtest.c
nandtest.h

ftl.c

#include "string.h"
#include "./BSP/NAND/ftl.h"
#include "./MALLOC/malloc.h"
#include "./BSP/NAND/nand.h"
#include "./SYSTEM/usart/usart.h"


/**
 *        每个块,第一个page的spare区,前四个字节的含义:
 *        第一个字节,表示该块是否是坏块:0XFF,正常块;其他值,坏块.
 *        第二个字节,表示该块是否被用过:0XFF,没有写过数据;0XCC,写过数据了.
 *        第三和第四个字节,表示该块所属的逻辑块编号. 

 *        每个page,spare区16字节以后的字节含义:
 *        第十六字节开始,后续每4个字节用于存储一个扇区(大小:NAND_ECC_SECTOR_SIZE)的ECC值,用于ECC校验
 */

/**
 * @brief       FTL层初始化
 * @param       无
 * @retval      0,正常;其他,失败
 */
uint8_t ftl_init(void)
{
    uint8_t temp;

    if (nand_init())
    {
        return 1;                                                   /* 初始化NAND FLASH */
    }

    if (nand_dev.lut)
    {
        myfree(SRAMIN, nand_dev.lut);
    }

    nand_dev.lut = mymalloc(SRAMIN, (nand_dev.block_totalnum) * 2); /* 给LUT表申请内存 */
    memset(nand_dev.lut, 0, nand_dev.block_totalnum * 2);           /* 全部清理 */

    if (!nand_dev.lut)
    {
        return 1;                                                   /* 内存申请失败  */
    }

    temp = ftl_create_lut(1);

    if (temp) 
    {
        printf("format nand flash...\r\n");
        temp = ftl_format();                                        /* 格式化NAND */

        if (temp)
        {
            printf("format failed!\r\n");
            return 2;
        }
    }
    else                                                            /* 创建LUT表成功 */
    {
        printf("total block num: %d\r\n", nand_dev.block_totalnum);
        printf("good block num: %d\r\n", nand_dev.good_blocknum);
        printf("valid block num: %d\r\n", nand_dev.valid_blocknum);
    }

    return 0;
} 

/**
 * @brief       标记某一个块为坏块
 * @param       blocknum   : 块编号,范围:0~(block_totalnum-1)
 * @retval      无
 */
void ftl_badblock_mark(uint32_t blocknum)
{
    uint32_t temp = 0XAAAAAAAA; /* 坏块标记mark,任意值都OK,只要不是0XFF.这里写前4个字节,方便ftl_find_unused_block函数检查坏块.(不检查备份区,以提高速度) */
    nand_writespare(blocknum * nand_dev.block_pagenum, 0, (uint8_t *)&temp, 4);      /* 在第一个page的spare区,第一个字节做坏块标记(前4个字节都写) */
    nand_writespare(blocknum * nand_dev.block_pagenum + 1, 0, (uint8_t *)&temp, 4);  /* 在第二个page的spare区,第一个字节做坏块标记(备份用,前4个字节都写) */
}

/**
 * @brief       检查某一块是否是坏块
 * @param       blocknum    : 块编号,范围:0~(block_totalnum-1)
 * @retval      0,好块;
 *              其他,坏块
 */
uint8_t ftl_check_badblock(uint32_t blocknum)
{
    uint8_t flag = 0;

    nand_readspare(blocknum * nand_dev.block_pagenum, 0, &flag, 1);         /* 读取坏块标志 */

    if (flag == 0XFF)                                                       /* 好块?,读取备份区坏块标记 */
    {
        nand_readspare(blocknum * nand_dev.block_pagenum + 1, 0, &flag, 1); /* 读取备份区坏块标志 */

        if (flag == 0XFF)
        {
            return 0;                                                       /* 好块 */
        }

        else return 1;                                                      /* 坏块 */
    }

    return 2; 
}

/**
 * @brief       标记某一个块已经使用
 * @param       blocknum    : 块编号,范围:0~(block_totalnum-1)
 * @retval      0,成功;
 *              其他,失败
 */
uint8_t ftl_used_blockmark(uint32_t blocknum)
{
    uint8_t usedflag = 0XCC;
    uint8_t temp = 0;
    temp = nand_writespare(blocknum * nand_dev.block_pagenum, 1, (uint8_t *)&usedflag, 1); /* 写入块已经被使用标志 */

    return temp;
}

/**
 * @brief       从给定的块开始找到往前找到一个未被使用的块(指定奇数/偶数)
 * @param       sblock      : 开始块,范围:0~(block_totalnum-1)
 * @param       flag        : 0,偶数快; 1,奇数块.
 * @retval      0XFFFFFFFF,失败;
 *              其他值,未使用块号
 */
uint32_t ftl_find_unused_block(uint32_t sblock, uint8_t flag)
{
    uint32_t temp = 0;
    uint32_t blocknum = 0;

    for (blocknum = sblock + 1; blocknum > 0; blocknum--)
    {
        if (((blocknum - 1) % 2) == flag)   /* 奇偶合格,才检测 */
        {
            nand_readspare((blocknum - 1) * nand_dev.block_pagenum, 0, (uint8_t *)&temp, 4); /* 读块是否被使用标记 */
            if (temp == 0XFFFFFFFF)
            {
                return (blocknum - 1);      /* 找到一个空块,返回块编号 */
            }
        }
    }

    return 0XFFFFFFFF;                      /* 未找到空余块 */
}

/**
 * @brief       查找与给定块在同一个plane内的未使用的块
 * @param       sblock    : 给定块,范围:0~(block_totalnum-1)
 * @retval      0XFFFFFFFF,失败;其他值,未使用块号
 */
uint32_t ftl_find_same_plane_unused_block(uint32_t sblock)
{
    static uint32_t curblock = 0XFFFFFFFF;
    uint32_t unusedblock = 0;

    if (curblock > (nand_dev.block_totalnum - 1))
    {
        curblock = nand_dev.block_totalnum - 1;                   /* 超出范围了,强制从最后一个块开始 */
    }

    unusedblock = ftl_find_unused_block(curblock,sblock % 2);     /* 从当前块,开始,向前查找空余块  */

    if (unusedblock == 0XFFFFFFFF && curblock < (nand_dev.block_totalnum - 1))  /* 未找到,且不是从最末尾开始找的 */
    {
        curblock = nand_dev.block_totalnum - 1;                   /* 强制从最后一个块开始 */
        unusedblock = ftl_find_unused_block(curblock,sblock % 2); /* 从最末尾开始,重新找一遍 */ 
    }
    if (unusedblock == 0XFFFFFFFF)
    {
        return 0XFFFFFFFF;                                        /* 找不到空闲block */
    }

    curblock = unusedblock;                                       /* 当前块号等于未使用块编号.下次则从此处开始查找 */

    return unusedblock;                                           /* 返回找到的空闲block */
}

/**
 * @brief       将一个块的数据拷贝到另一块,并且可以写入数据
 * @param       source_pagenum  : 要写入数据的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 要写入的列开始地址(也就是页内地址),范围:0~(page_totalsize-1)
 * @param       pbuffer         : 要写入的数据
 * @param       numbyte_to_write: 要写入的字节数,该值不能超过块内剩余容量大小
 * @retval      0,成功;
 *              其他,失败
 */
uint8_t ftl_copy_and_write_to_block(uint32_t source_pagenum, uint16_t colnum, uint8_t *pbuffer, uint32_t numbyte_to_write)
{
    uint16_t i = 0, temp = 0, wrlen;
    uint32_t source_block = 0, pageoffset = 0;
    uint32_t unusedblock = 0; 
    source_block = source_pagenum / nand_dev.block_pagenum;         /* 获得页所在的块号 */
    pageoffset = source_pagenum % nand_dev.block_pagenum;           /* 获得页在所在块内的偏移 */

retry:
    unusedblock = ftl_find_same_plane_unused_block(source_block);   /* 查找与源块在一个plane的未使用块 */

    if (unusedblock > nand_dev.block_totalnum)
    {
        return 1;                                                   /* 当找到的空余块号大于块总数量的话肯定是出错了 */
    }

    for (i = 0; i < nand_dev.block_pagenum; i++)                    /* 将一个块的数据复制到找到的未使用块中 */
    {
        if (i >= pageoffset && numbyte_to_write)                    /* 数据要写入到当前页 */
        { 
            if (numbyte_to_write > (nand_dev.page_mainsize - colnum)) /* 要写入的数据,超过了当前页的剩余数据 */
            {
                wrlen = nand_dev.page_mainsize - colnum;            /* 写入长度等于当前页剩余数据长度 */
            }
            else 
            {
                wrlen = numbyte_to_write;                           /* 写入全部数据 */
            }

            temp = nand_copypage_withwrite(source_block * nand_dev.block_pagenum + i, unusedblock * nand_dev.block_pagenum + i, colnum, pbuffer, wrlen);
            colnum = 0;                                             /* 列地址归零 */
            pbuffer += wrlen;                                       /* 写地址偏移 */
            numbyte_to_write -= wrlen;                              /* 写入数据减少 */
        }

        else                                                        /* 无数据写入,直接拷贝即可 */
        {
            temp = nand_copypage_withoutwrite(source_block * nand_dev.block_pagenum + i, unusedblock * nand_dev.block_pagenum + i);
        }

        if (temp)                                                   /* 返回值非零,当坏块处理 */
        { 
            ftl_badblock_mark(unusedblock);                         /* 标记为坏块 */
            ftl_create_lut(1);                                      /* 重建LUT表 */
            goto retry;
        }
    }

    if (i == nand_dev.block_pagenum)                                /* 拷贝完成 */
    {
        ftl_used_blockmark(unusedblock);                            /* 标记块已经使用 */
        nand_eraseblock(source_block);                              /* 擦除源块 */
        for (i = 0; i < nand_dev.block_totalnum; i++)               /* 修正LUT表,用unusedblock替换source_block */
        {
            if (nand_dev.lut[i] == source_block)
            {
                nand_dev.lut[i] = unusedblock;
                break;
            }
        }
    }

    return 0;   /* 成功 */
}

/**
 * @brief       逻辑块号转换为物理块号 
 * @param       lbnnum    : 逻辑块编号
 * @retval      物理块编号
 */
uint16_t ftl_lbn_to_pbn(uint32_t lbnnum)
{
    uint16_t pbnno = 0;

    /* 当逻辑块号大于有效块数的时候返回0XFFFF */
    if (lbnnum > nand_dev.valid_blocknum)
    {
        return 0XFFFF;
    }

    pbnno = nand_dev.lut[lbnnum];

    return pbnno;
}

/**
 * @brief       写扇区(支持多扇区写),FATFS文件系统使用
 * @param       pbuffer     : 要写入的数据
 * @param       sectorno    : 起始扇区号
 * @param       sectorsize  : 扇区大小(不能大于NAND_ECC_SECTOR_SIZE定义的大小,否则会出错!!)
 * @param       sectorcount : 要写入的扇区数量
 * @retval      0,成功;
 *              其他,失败
 */
uint8_t ftl_write_sectors(uint8_t *pbuffer, uint32_t sectorno, uint16_t sectorsize, uint32_t sectorcount)
{
    uint8_t flag = 0;
    uint16_t temp;
    uint32_t i = 0;
    uint16_t wsecs;                 /* 写页大小 */
    uint32_t wlen;                  /* 写入长度 */
    uint32_t lbnno;                 /* 逻辑块号 */
    uint32_t pbnno;                 /* 物理块号 */
    uint32_t phypageno;             /* 物理页号 */
    uint32_t pageoffset;            /* 页内偏移地址 */
    uint32_t blockoffset;           /* 块内偏移地址 */
    uint32_t markdpbn = 0XFFFFFFFF; /* 标记了的物理块编号 */

    for (i = 0; i < sectorcount; i++)
    {
        lbnno = (sectorno+i) / (nand_dev.block_pagenum * (nand_dev.page_mainsize / sectorsize));/* 根据逻辑扇区号和扇区大小计算出逻辑块号 */
        pbnno = ftl_lbn_to_pbn(lbnno);                                                          /* 将逻辑块转换为物理块 */

        if (pbnno >= nand_dev.block_totalnum)
        {
            return 1;                                                                           /* 物理块号大于NAND FLASH的总块数,则失败. */
        }

        blockoffset =((sectorno + i) % (nand_dev.block_pagenum * (nand_dev.page_mainsize / sectorsize))) * sectorsize; /* 计算块内偏移 */
        phypageno = pbnno * nand_dev.block_pagenum + blockoffset / nand_dev.page_mainsize;                             /* 计算出物理页号 */
        pageoffset = blockoffset % nand_dev.page_mainsize;                                                             /* 计算出页内偏移地址  */
        temp = nand_dev.page_mainsize - pageoffset;                                                                    /* page内剩余字节数 */
        temp /= sectorsize;                                                                                            /* 可以连续写入的sector数  */
        wsecs = sectorcount - i;                                                                                       /* 还剩多少个sector要写 */

        if (wsecs >= temp)
        {
            wsecs = temp;                                                               /* 大于可连续写入的sector数,则写入temp个扇区 */
        }

        wlen = wsecs * sectorsize;                                                      /* 每次写wsecs个sector */

        /* 读出写入大小的内容判断是否全为0XFF */
        flag = nand_readpagecomp(phypageno, pageoffset, 0XFFFFFFFF, wlen / 4, &temp);   /* 读一个wlen/4大小个数据,并与0XFFFFFFFF对比 */

        if (flag)
        {
            return 2;                                                   /* 读写错误,坏块 */
        }
        if (temp == (wlen / 4)) 
        {
            flag = nand_writepage(phypageno, pageoffset, pbuffer, wlen);/* 全为0XFF,可以直接写数据 */
        }
        else
        {
            flag = 1;                                                   /* 不全是0XFF,则另作处理 */
        }
        if (flag == 0 && (markdpbn != pbnno))                           /* 全是0XFF,且写入成功,且标记了的物理块与当前物理块不同 */
        {
            flag = ftl_used_blockmark(pbnno);                           /* 标记此块已经使用 */
            markdpbn = pbnno;                                           /* 标记完成,标记块=当前块,防止重复标记 */
        }
        if (flag)                                                       /* 不全为0XFF/标记失败,将数据写到另一个块 */
        {
            temp = ((uint32_t)nand_dev.block_pagenum * nand_dev.page_mainsize - blockoffset) / sectorsize;/* 计算整个block还剩下多少个SECTOR可以写入 */
            wsecs = sectorcount - i;                                    /* 还剩多少个sector要写 */

            if (wsecs >= temp)
            {
                wsecs = temp;                                           /* 大于可连续写入的sector数,则写入temp个扇区 */
            }

            wlen = wsecs * sectorsize;                                  /* 每次写wsecs个sector */
            flag = ftl_copy_and_write_to_block(phypageno, pageoffset, pbuffer, wlen);  /* 拷贝到另外一个block,并写入数据 */
            if (flag)
            {
                return 3;                                               /* 失败 */
            }
        }

        i += wsecs - 1;
        pbuffer += wlen;                                                /* 数据缓冲区指针偏移 */
    }
    return 0;
} 

/**
 * @brief       读扇区(支持多扇区读),FATFS文件系统使用
 * @param       pbuffer     : 数据缓存区
 * @param       sectorno    : 起始扇区号
 * @param       sectorsize  : 扇区大小
 * @param       sectorcount : 要写入的扇区数量
 * @retval      0,成功;
 *              其他,失败
 */
uint8_t ftl_read_sectors(uint8_t *pbuffer, uint32_t sectorno, uint16_t sectorsize, uint32_t sectorcount)
{
    uint8_t flag = 0;
    uint16_t rsecs;         /* 单次读取页数 */
    uint32_t i = 0;
    uint32_t lbnno;         /* 逻辑块号 */
    uint32_t pbnno;         /* 物理块号 */
    uint32_t phypageno;     /* 物理页号 */
    uint32_t pageoffset;    /* 页内偏移地址 */
    uint32_t blockoffset;   /* 块内偏移地址 */

    for (i = 0; i < sectorcount; i++)
    {
        lbnno = (sectorno + i) / (nand_dev.block_pagenum * (nand_dev.page_mainsize / sectorsize));  /* 根据逻辑扇区号和扇区大小计算出逻辑块号 */
        pbnno = ftl_lbn_to_pbn(lbnno);                                                              /* 将逻辑块转换为物理块 */

        if (pbnno >= nand_dev.block_totalnum)
        {
            return 1;                                                                               /* 物理块号大于NAND FLASH的总块数,则失败. */
        }

        blockoffset = ((sectorno + i) % (nand_dev.block_pagenum * (nand_dev.page_mainsize / sectorsize))) * sectorsize; /* 计算块内偏移 */
        phypageno = pbnno * nand_dev.block_pagenum + blockoffset / nand_dev.page_mainsize;          /* 计算出物理页号 */
        pageoffset = blockoffset%nand_dev.page_mainsize;                                            /* 计算出页内偏移地址 */
        rsecs = (nand_dev.page_mainsize - pageoffset) / sectorsize;                                 /* 计算一次最多可以读取多少页 */

        if (rsecs > (sectorcount - i))
        {
            rsecs = sectorcount - i;                                                                /* 最多不能超过SectorCount-i */
        }
        flag = nand_readpage(phypageno, pageoffset, pbuffer, rsecs * sectorsize);                   /* 读取数据 */

        if (flag == NSTA_ECC1BITERR)                                                                /* 对于1bit ecc错误,可能为坏块 */
        {
            flag = nand_readpage(phypageno, pageoffset, pbuffer, rsecs * sectorsize);               /* 重读数据,再次确认 */

            if (flag == NSTA_ECC1BITERR)
            {
                ftl_copy_and_write_to_block(phypageno, pageoffset, pbuffer, rsecs * sectorsize);    /* 搬运数据 */
                flag = ftl_blockcompare(phypageno / nand_dev.block_pagenum, 0XFFFFFFFF);            /* 全1检查,确认是否为坏块 */

                if (flag == 0)
                {
                    flag = ftl_blockcompare(phypageno / nand_dev.block_pagenum, 0X00);              /* 全0检查,确认是否为坏块 */
                    nand_eraseblock(phypageno / nand_dev.block_pagenum);                            /* 检测完成后,擦除这个块 */
                }

                if (flag)                                                                           /* 全0/全1检查出错,肯定是坏块了. */
                {
                    ftl_badblock_mark(phypageno/nand_dev.block_pagenum);                            /* 标记为坏块 */
                    ftl_create_lut(1);                                                              /* 重建LUT表 */
                }

                flag = 0;
            }
        }

        if (flag == NSTA_ECC2BITERR)
        {
            flag = 0;                       /* 2bit ecc错误,不处理(可能是初次写入数据导致的) */
        }
        if (flag)
        {
            return 2;                       /* 失败 */
        }

        pbuffer += sectorsize * rsecs;      /* 数据缓冲区指针偏移 */
        i += rsecs - 1;
    }
    return 0; 
}

/**
 * @brief       重新创建LUT表
 * @param       mode    : 0,仅检查第一个坏块标记
 *              1,两个坏块标记都要检查(备份区也要检查)
 * @retval      0,成功;其他,失败
 */
uint8_t ftl_create_lut(uint8_t mode)
{
    uint32_t i;
    uint8_t buf[4];
    uint32_t lbnnum = 0;                                             /* 逻辑块号 */

    for (i = 0; i < nand_dev.block_totalnum; i++)                    /* 复位LUT表,初始化为无效值,也就是0XFFFF */
    {
        nand_dev.lut[i] = 0XFFFF;
    }

    nand_dev.good_blocknum = 0;
    for (i = 0; i < nand_dev.block_totalnum; i++)
    {
        nand_readspare(i * nand_dev.block_pagenum, 0, buf, 4);         /* 读取4个字节 */

        if (buf[0] == 0XFF && mode)
        {
            nand_readspare(i * nand_dev.block_pagenum + 1, 0, buf, 1); /* 好块,且需要检查2次坏块标记 */
        }

        if (buf[0] == 0XFF)                                            /* 是好块 */
        { 
            lbnnum = ((uint16_t)buf[3] << 8) + buf[2];                 /* 得到逻辑块编号 */
            if (lbnnum < nand_dev.block_totalnum)                      /* 逻辑块号肯定小于总的块数量 */
            {
                nand_dev.lut[lbnnum] = i;                              /* 更新LUT表,写LBNnum对应的物理块编号 */
            }
            nand_dev.good_blocknum++;
        }
        else
        {
            printf("bad block index:%d\r\n", i);
        }
    } 
    /* LUT表建立完成以后检查有效块个数 */
    for (i = 0; i < nand_dev.block_totalnum; i++)
    {
        if (nand_dev.lut[i] >= nand_dev.block_totalnum)
        {
            nand_dev.valid_blocknum = i;
            break;
        }
    }

    if (nand_dev.valid_blocknum < 100)
    {
        return 2;               /* 有效块数小于100,有问题.需要重新格式化 */
    }

    return 0;                   /* LUT表创建完成 */
}

/**
 * @brief       FTL整个Block与某个数据对比
 * @param       blockx      : block编号
 * @param       cmpval      : 要与之对比的值
 * @retval      0,检查成功,全部相等;
                1,检查失败,有不相等的情况
 */
uint8_t ftl_blockcompare(uint32_t blockx, uint32_t cmpval)
{
    uint8_t res;
    uint16_t i, j, k;

    for (i = 0; i < 3; i++)                             /* 允许3次机会 */
    {
        for (j = 0; j < nand_dev.block_pagenum; j++)
        {
            /* 检查一个page,并与0XFFFFFFFF对比 */
            nand_readpagecomp(blockx * nand_dev.block_pagenum, 0, cmpval, nand_dev.page_mainsize / 4, &k); 

            if (k != (nand_dev.page_mainsize / 4))break;
        }

        if (j == nand_dev.block_pagenum)
        {
            return 0;                                   /* 检查合格,直接退出 */
        }

        res = nand_eraseblock(blockx);

        if (res)
        {
            printf("error erase block:%d\r\n", i);
        }
        else
        { 
            if (cmpval != 0XFFFFFFFF)                   /* 不是判断全1,则需要重写数据 */
            {
                for (k = 0; k < nand_dev.block_pagenum; k++)
                {
                    nand_write_pageconst(blockx * nand_dev.block_pagenum + k, 0, 0, nand_dev.page_mainsize / 4); /* 写PAGE */
                }
            }
        }
    }

    printf("bad block checked:%d\r\n", blockx);
    return 1;
}

/**
 * @brief       FTL初始化时,搜寻所有坏块,使用:擦-写-读 方式
 * @note        512M的NAND ,需要约3分钟时间,来完成检测
 *              对于RGB屏,由于频繁读写NAND,会引起屏幕乱闪
 * @param       无
 * @retval      好块的数量
 */
uint32_t ftl_search_badblock(void)
{
    uint8_t *blktbl;
    uint8_t res;
    uint32_t i, j; 
    uint32_t goodblock = 0;

    blktbl = mymalloc(SRAMIN, nand_dev.block_totalnum);  /* 申请block坏块表内存,对应项:0,好块;1,坏块; */
    nand_erasechip();                                    /* 全片擦除 */

    for (i = 0; i < nand_dev.block_totalnum; i++)        /* 第一阶段检查,检查全1 */
    {
        res = ftl_blockcompare(i, 0XFFFFFFFF);           /* 全1检查 */

        if (res)
        {
            blktbl[i] = 1;                               /* 坏块  */
        }
        else
        { 
            blktbl[i] = 0;/* 好块 */

            for (j = 0; j < nand_dev.block_pagenum; j++) /* 写block为全0,为后面的检查准备 */
            {
                nand_write_pageconst(i * nand_dev.block_pagenum + j, 0, 0, nand_dev.page_mainsize / 4);
            } 
        }
    }

    for (i = 0; i < nand_dev.block_totalnum; i++)        /* 第二阶段检查,检查全0 */
    { 
        if (blktbl[i] == 0)                              /* 在第一阶段,没有被标记坏块的,才可能是好块 */
        {
            res = ftl_blockcompare(i, 0);                /* 全0检查 */

            if (res)
            {
                blktbl[i] = 1;                           /* 标记坏块 */
            }
            else
            {
                goodblock++; 
            }
        }
    }

    nand_erasechip();                                    /* 全片擦除 */

    for (i = 0; i < nand_dev.block_totalnum; i++)        /* 第三阶段检查,标记坏块 */
    { 
        if (blktbl[i])
        {
            ftl_badblock_mark(i);                        /* 是坏块 */
        }
    }

    return goodblock;                                    /* 返回好块的数量 */
}

/**
 * @brief       格式化NAND 重建LUT表
 * @param       无
 * @retval      0,成功;
                其他,失败
 */
uint8_t ftl_format(void)
{
    uint8_t temp;
    uint32_t i, n;
    uint32_t goodblock = 0;
    nand_dev.good_blocknum = 0;

#if FTL_USE_BAD_BLOCK_SEARCH == 1                    /* 使用擦-写-读的方式,检测坏块 */
    nand_dev.good_blocknum = FTL_SearchBadBlock();   /* 搜寻坏块.耗时很久 */
#else                                                /* 直接使用NAND FLASH的出厂坏块标志(其他块,默认是好块) */

    for (i = 0; i < nand_dev.block_totalnum; i++)
    {
        temp = ftl_check_badblock(i);                /* 检查一个块是否为坏块 */

        if (temp == 0)                               /* 好块 */
        {
            temp = nand_eraseblock(i);

            if (temp)                                /* 擦除失败,认为坏块 */
            {
                printf("Bad block:%d\r\n", i);
                ftl_badblock_mark(i);                /* 标记是坏块 */
            }
            else
            {
                nand_dev.good_blocknum++;            /* 好块数量加一 */
            }
        }
    }
#endif
    printf("good_blocknum:%d\r\n", nand_dev.good_blocknum);

    if (nand_dev.good_blocknum < 100)
    {
        return 1;                                    /* 如果好块的数量少于100,则NAND Flash报废 */
    }

    goodblock = (nand_dev.good_blocknum * 93) / 100; /* %93的好块用于存储数据 */

    n = 0;

    for (i = 0; i < nand_dev.block_totalnum; i++)    /* 在好块中标记上逻辑块信息 */
    {
        temp = ftl_check_badblock(i);                /* 检查一个块是否为坏块 */

        if (temp == 0)                               /* 好块 */
        { 
            nand_writespare(i * nand_dev.block_pagenum, 2, (uint8_t*)&n, 2);  /* 写入逻辑块编号 */
            n++;                                     /* 逻辑块编号加1 */

            if (n == goodblock) break;               /* 全部标记完了 */
        }
    } 
    if (ftl_create_lut(1))
    {
        return 2;                                    /* 重建LUT表失败 */
    }

    return 0;
}

ftl.h

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./USMART/usmart.h"
#include "./BSP/SDRAM/sdram.h"
#include "./MALLOC/malloc.h"
#include "./BSP/NAND/ftl.h"
#include "./BSP/NAND/nand.h"
#include "./BSP/NAND/nandtester.h"

int main(void)
{
    uint8_t key, t = 0;
    uint16_t i;
    uint8_t wd_buf[512];
    uint8_t rd_buf[512];

    HAL_Init();                             /* 初始化HAL库 */
    sys_stm32_clock_init(360, 25, 2, 8);    /* 设置时钟,180Mhz */
    delay_init(180);                        /* 延时初始化 */
    usart_init(115200);                     /* 串口初始化为115200 */
    usmart_dev.init(90);                    /* 初始化USMART */
    led_init();                             /* 初始化LED */
    key_init();                             /* 初始化按键 */
    sdram_init();                           /* 初始化SDRAM */

    my_mem_init(SRAMIN);                    /* 初始化内部内存池 */
    my_mem_init(SRAMEX);                    /* 初始化外部内存池 */
    my_mem_init(SRAMCCM);                   /* 初始化CCM内存池 */
    
    nand_init();                            /* NAND FLASH初始化 */
    
    while (1)
    {
        key = key_scan(0);

        switch (key)
        {
            case KEY0_PRES:     /* KEY0按下,读取数据 */
                nand_readpage(100 * (128/2), 0, rd_buf, 512);
                printf("Read data: \r\n");
                for (i = 0; i < 512; i++)
                {
                    printf("%#x ", rd_buf[i]);     /* 串口打印读取到的数据 */
                }
                printf("end      \r\n");

                break;

            case KEY1_PRES:     /* KEY1按下,写入数据 */
                
                printf("\r\n");

                for (i = 0; i < 256; i++)
                {
                    wd_buf[i] = i;
                    printf("%#x ",wd_buf[i]);
                }

                key = nand_writepage(100 * (128/2), 0, wd_buf, NAND_ECC_SECTOR_SIZE / 2);

                if (key == 0)
                {
                    printf("Write data successed    \r\n");
                }
                else 
                {
                    printf("Write data failed       \r\n");
                }
                break;

            case KEY2_PRES:     /* KEY2按下,写入数据 */
                key = nand_writepage(100 * (128/2), 300, "123456789ABCD", 13);      /* 写入扇区 */
                
                if (key == 0)
                {
                    printf("Write data successed    \r\n");
                }
                else 
                {
                    printf("Write data failed       \r\n");
                }
                break;
                
            case WKUP_PRES:     /* WKUP按下,测试 */

                key = nand_eraseblock(100);
                if (key == 0)
                {
                    printf("ERASE data successed    \r\n");
                }
                else 
                {
                    printf("ERASE data failed       \r\n");
                }

                break;
        }

        t++;
        delay_ms(10);

        if (t == 20)
        {
            LED0_TOGGLE();               /* 红灯闪烁 */
            t = 0;
        }
    }
}

在这里插入图片描述

NAND FLASH源码

在这里插入图片描述
在这里插入图片描述
nand.c

#include <stdio.h>
#include "./BSP/NAND/nand.h"
#include "./SYSTEM/delay/delay.h"
#include "./MALLOC/malloc.h"


NAND_HandleTypeDef g_nand_handle;     /* NAND FLASH句柄 */
nand_attriute nand_dev;               /* nand重要参数结构体 */

/**
 * @brief       初始化NAND FLASH
 * @param       无
 * @retval      0,成功; 1,失败
 */
uint8_t nand_init(void)
{
    FMC_NAND_PCC_TimingTypeDef comspacetiming, attspacetiming;

    g_nand_handle.Instance              = FMC_NAND_DEVICE;                      /* NAND设备 */
    g_nand_handle.Init.NandBank         = FMC_NAND_BANK3;                       /* NAND挂在BANK3上 */
    g_nand_handle.Init.Waitfeature      = FMC_NAND_PCC_WAIT_FEATURE_DISABLE;    /* 关闭等待特性 */
    g_nand_handle.Init.MemoryDataWidth  = FMC_NAND_PCC_MEM_BUS_WIDTH_8;         /* 8位数据宽度 */
    g_nand_handle.Init.EccComputation   = FMC_NAND_ECC_DISABLE;                 /* 不使用FMC的ECC功能,需要用到再使能 */
    g_nand_handle.Init.ECCPageSize      = FMC_NAND_ECC_PAGE_SIZE_2048BYTE;      /* ECC页大小为2k */
    g_nand_handle.Init.TCLRSetupTime    = 0;    /* 设置TCLR(tCLR=CLE到RE的延时)=(TCLR+TSET+2)*THCLK,THCLK=1/180M=5.5ns */
    g_nand_handle.Init.TARSetupTime     = 1;    /* 设置TAR(tAR=ALE到RE的延时)=(TAR+TSET+2)*THCLK,THCLK=1/180M=5.5n */
   
    comspacetiming.SetupTime        = 2;    /* 建立时间 */
    comspacetiming.WaitSetupTime    = 3;    /* 等待时间 */
    comspacetiming.HoldSetupTime    = 2;    /* 保持时间 */
    comspacetiming.HiZSetupTime     = 1;    /* 高阻态时间 */
    
    attspacetiming.SetupTime        = 2;    /* 建立时间 */
    attspacetiming.WaitSetupTime    = 3;    /* 等待时间 */
    attspacetiming.HoldSetupTime    = 2;    /* 保持时间 */
    attspacetiming.HiZSetupTime     = 1;    /* 高阻态时间 */
    
    HAL_NAND_Init(&g_nand_handle, &comspacetiming, &attspacetiming);    /* NANDFLASH初始化 */
    
    nand_reset();                           /* 复位NAND */
    delay_ms(100);
    nand_dev.id = nand_readid();            /* 读取ID */
    nand_modeset(4);                        /* 设置为MODE4,高速模式 */

    if (nand_dev.id == MT29F16G08ABABA)     /* NAND为MT29F16G08ABABA */
    {
        nand_dev.page_totalsize = 4320;     /* nand一个page的总大小(包括spare区) */
        nand_dev.page_mainsize  = 4096;     /* nand一个page的有效数据区大小 */
        nand_dev.page_sparesize = 224;      /* nand一个page的spare区大小 */
        nand_dev.block_pagenum  = 128;      /* nand一个block所包含的page数目 */
        nand_dev.plane_blocknum = 2048;     /* nand一个plane所包含的block数目 */
        nand_dev.block_totalnum = 4096;     /* nand的总block数目 */
    }
    else if (nand_dev.id == MT29F4G08ABADA) /* NAND为MT29F4G08ABADA */
    {
        nand_dev.page_totalsize = 2112;     /* nand一个page的总大小(包括spare区) */
        nand_dev.page_mainsize  = 2048;     /* nand一个page的有效数据区大小 */
        nand_dev.page_sparesize = 64;       /* nand一个page的spare区大小 */
        nand_dev.block_pagenum  = 64;       /* nand一个block所包含的page数目 */
        nand_dev.plane_blocknum = 2048;     /* nand一个plane所包含的block数目 */
        nand_dev.block_totalnum = 4096;     /* nand的总block数目 */
    }
    else
    {
        return 1;   /* 错误 */
    }
    
    return 0;       /* 成功 */
}

/**
 * @brief       NAND FALSH底层驱动,引脚配置,时钟使能
 * @note        此函数会被HAL_NAND_Init()调用
 * @param       hnand  : nand句柄
 * @retval      无
 */
void HAL_NAND_MspInit(NAND_HandleTypeDef *hnand)
{
    GPIO_InitTypeDef gpio_init_struct;
    
    __HAL_RCC_FMC_CLK_ENABLE();                       /* 使能FMC时钟 */
    __HAL_RCC_GPIOD_CLK_ENABLE();                     /* 使能GPIOD时钟 */
    __HAL_RCC_GPIOE_CLK_ENABLE();                     /* 使能GPIOE时钟 */
    __HAL_RCC_GPIOG_CLK_ENABLE();                     /* 使能GPIOG时钟 */

    gpio_init_struct.Pin = NAND_RB_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_INPUT;          /* 输入 */
    gpio_init_struct.Pull = GPIO_PULLUP;              /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_HIGH;         /* 高速 */
    HAL_GPIO_Init(NAND_RB_GPIO_PORT, &gpio_init_struct);

    gpio_init_struct.Pin = GPIO_PIN_9;
    gpio_init_struct.Mode = GPIO_MODE_AF_PP;          /* 输入 */
    gpio_init_struct.Pull = GPIO_NOPULL;              /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_HIGH;         /* 高速 */
    gpio_init_struct.Alternate = GPIO_AF12_FMC;       /* 复用为FMC */
    HAL_GPIO_Init(GPIOG, &gpio_init_struct);  

    /* 初始化PD0,1,4,5,11,12,14,15 */
    gpio_init_struct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5 | \
                           GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_14 | GPIO_PIN_15;
    HAL_GPIO_Init(GPIOD, &gpio_init_struct);

    /* 初始化PE7,8,9,10*/
    gpio_init_struct.Pin = GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10;
    HAL_GPIO_Init(GPIOE, &gpio_init_struct);
}

/**
 * @brief       设置NAND速度模式
 * @param       mode    : 0~5, 表示速度模式
 * @retval      0,成功; 其他,失败
 */
uint8_t nand_modeset(uint8_t mode)
{   
    /* 参考MT29F4G08器件手册P50 SET FEATURE操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_FEATURE; /* 向NAND FLASH发送设置特性指令 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = 0X01;         /* 向NAND FLASH发送 设置时序模式 的地址 */
    *(volatile uint8_t *)NAND_ADDRESS               = mode;         /* P1参数,设置mode */
    *(volatile uint8_t *)NAND_ADDRESS               = 0;
    *(volatile uint8_t *)NAND_ADDRESS               = 0;
    *(volatile uint8_t *)NAND_ADDRESS               = 0;

    if (nand_wait_for_ready() == NSTA_READY)
    {
        return 0;   /* 成功 */
    }
    else 
    {
        return 1;   /* 失败 */
    }
}

/**
 * @brief       读取NAND FLASH的ID
 * @note        不同的NAND略有不同,请根据自己所使用的NAND FALSH数据手册来编写函数
 * @param       无
 * @retval      NAND FLASH的ID值
 */
uint32_t nand_readid(void)
{
    uint8_t deviceid[5]; 
    uint32_t id;
    
    /* 参考MT29F4G08器件手册P35 READ ID操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_READID;  /* 向NAND FLASH发送读取ID命令 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = 0X00;         /* 向NAND FLASH发送读取地址00 */

    /* ID信息一共有5个字节(MT29F4G08ABADA) */
    deviceid[0] = *(volatile uint8_t *)NAND_ADDRESS;   /* 2Ch */
    deviceid[1] = *(volatile uint8_t *)NAND_ADDRESS;   /* DCh */
    deviceid[2] = *(volatile uint8_t *)NAND_ADDRESS;   /* 90h */
    deviceid[3] = *(volatile uint8_t *)NAND_ADDRESS;   /* 95h */
    deviceid[4] = *(volatile uint8_t *)NAND_ADDRESS;   /* 56h */

    /* 镁光的NAND FLASH的ID一共5个字节,但是为了方便我们只取4个字节组成一个32位的ID值
       根据NAND FLASH的数据手册,只要是镁光的NAND FLASH,那么一个字节ID的第一个字节都是0X2C
       所以我们就可以抛弃这个0X2C,只取后面四字节的ID值。*/
    id = ((uint32_t)deviceid[1]) << 24 | ((uint32_t)deviceid[2]) << 16 | ((uint32_t)deviceid[3]) << 8 | deviceid[4];

    return id;
}

/**
 * @brief       读NAND状态
 * @param       无
 * @retval      NAND状态值
 *              bit0:0,成功; 1,错误(编程/擦除/READ)
                bit6:0,Busy; 1,Ready
 */
uint8_t nand_readstatus(void)
{
    volatile uint8_t data = 0;
    
    /* 参考MT29F4G08器件手册P55 READ STATUS操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_READSTA;  /* 向NAND FLASH发送读状态命令,便于判断是否处于空闲 */
    nand_delay(NAND_TWHR_DELAY);                                    /* 等待tWHR时间后,NAND FLASH返回状态值 */
    data = *(volatile uint8_t *)NAND_ADDRESS;                       /* 读取状态值,状态值的定义参考器件手册P54 */

    return data;
}

/**
 * @brief       等待NAND准备好
 * @param       无
 * @retval      NSTA_TIMEOUT  等待超时了
 *              NSTA_READY    已经准备好
 */
uint8_t nand_wait_for_ready(void)
{
    uint8_t status = 0;
    volatile uint32_t time = 0;

    while (1)                           /* 等待ready */
    {
        status = nand_readstatus();     /* 获取状态值 */

        if (status & NSTA_READY)        /* 判断NAND FLASH是否准备好 */
        {
            break;
        }

        time++;

        if (time >= 0X1FFFFFFF)
        {
            return NSTA_TIMEOUT;        /* 超时 */
        }
    }

    return NSTA_READY;                  /* 准备好 */
}

/**
 * @brief       复位NAND
 * @param       无
 * @retval      0,成功; 其他,失败
 */
uint8_t nand_reset(void)
{
    /* 参考MT29F4G08器件手册P34 RESET 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_RESET; /* 复位NAND */

    if (nand_wait_for_ready() == NSTA_READY)
    {
        return 0;   /* NAND FLASH复位成功 */
    }
    else
    {
        return 1;   /* NAND FLASH复位失败 */
    }
}

/**
 * @brief       等待RB信号为某个电平
 * @param       rb: 0,等待RB==0;
 *                  1,等待RB==1;
 * @retval      0,成功; 1,超时
 */
uint8_t nand_waitrb(volatile uint8_t rb)
{
    volatile uint32_t time = 0;

    while (time < 0X1FFFFFF)
    {
        time++;

        if (NAND_RB == rb)
        {
            return 0;
        }
    }

    return 1;
}

/**
 * @brief       NAND延时
 * @note        一个i++至少需要4ns
 * @param       i       : 等待的时间
 * @retval      无
 */
void nand_delay(volatile uint32_t i)
{
    while (i > 0)
    {
        i--;
    }
}

/**
 * @brief       读取NAND Flash的指定页指定列的数据(main区和spare区都可以使用此函数)
 * @param       pagenum         : 要读取的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 要读取的列开始地址(也就是页内地址),范围:0~(page_totalsize-1)
 * @param       *pbuffer        : 指向数据存储区
 * @param       numbyte_to_read : 读取字节数(不能跨页读)
 * @retval      0,成功; 其他,错误代码
 */
uint8_t nand_readpage(uint32_t pagenum, uint16_t colnum, uint8_t *pbuffer, uint16_t numbyte_to_read)
{
    volatile uint16_t i = 0;
    uint8_t res = 0;
    uint8_t eccnum = 0;     /* 需要计算的ECC个数,每NAND_ECC_SECTOR_SIZE字节计算一个ecc */
    uint8_t eccstart = 0;   /* 第一个ECC值所属的地址范围 */
    uint8_t errsta = 0;
    uint8_t *p;
    
    /* 参考MT29F4G08器件手册P65 READ PAGE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_AREA_A;              /* 向NAND FLASH发送READ PAGE的第一个命令 */
    /* 发送5个字节地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)colnum;          /* 发送第一个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(colnum >> 8);   /* 发送第二个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)pagenum;         /* 发送第一个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 8);  /* 发送第二个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 16); /* 发送第三个字节的页地址 */
    
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_AREA_TRUE1;           /* 向NAND FLASH发送读取READ PAGE的第二个命令 */

    /* 下面两行代码是等待R/B引脚变为低电平,其实主要起延时作用的,等待NAND操作R/B引脚。因为我们是通过
       将STM32的NWAIT引脚(NAND的R/B引脚)配置为普通IO,代码中通过读取NWAIT引脚的电平来判断NAND是否准备
       就绪的。这个也就是模拟的方法,所以在速度很快的时候有可能NAND还没来得及操作R/B引脚来表示NAND的忙
       闲状态,结果我们就读取了R/B引脚,这个时候肯定会出错的,事实上确实是会出错!大家也可以将下面两行
       代码换成延时函数,只不过这里我们为了效率所以没有用延时函数。 */
    res = nand_waitrb(0);       /* 等待RB=0 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }

    /* 下面2行代码是真正判断NAND是否准备好的 */
    res = nand_waitrb(1);       /* 等待RB=1 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }

    if (numbyte_to_read % NAND_ECC_SECTOR_SIZE)     /* 不是NAND_ECC_SECTOR_SIZE的整数倍,不进行ECC校验 */
    { 
        for (i = 0;i < numbyte_to_read; i++)        /* 读取NAND FLASH中的值 */
        {
            *(volatile uint8_t *)pbuffer++ = *(volatile uint8_t *)NAND_ADDRESS;
        }
    }
    else
    {
        eccnum = numbyte_to_read / NAND_ECC_SECTOR_SIZE;    /* 得到ecc计算次数 */
        eccstart = colnum / NAND_ECC_SECTOR_SIZE;           /* 第一个ECC值所属的地址范围 */
        p = pbuffer;

        for (res = 0; res < eccnum; res++)
        {
            FMC_Bank2_3->PCR3 |= 1 << 6;                    /* 使能ECC校验 */

            for (i = 0; i < NAND_ECC_SECTOR_SIZE; i++)      /* 读取NAND_ECC_SECTOR_SIZE个数据 */
            {
                *(volatile uint8_t *)pbuffer++ = *(volatile uint8_t *)NAND_ADDRESS;
            }

            while (!(FMC_Bank2_3->SR3 & (1 << 6)));         /* 等待FIFO空 */

            nand_dev.ecc_hdbuf[res + eccstart] = FMC_Bank2_3->ECCR3;    /* 读取硬件计算后的ECC值 */
            FMC_Bank2_3->PCR3 &= ~(1 << 6);                 /* 禁止ECC校验 */
        } 

        i = nand_dev.page_mainsize + 0X10 + eccstart * 4;   /* 从spare区的0X10位置开始读取之前存储的ecc值 */
        nand_delay(NAND_TRHW_DELAY);                        /* 等待tRHW */
        
        /* 参考MT29F4G08器件手册P123 RANDOM DATA READ 操作时序 */
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = 0X05;             /* 随机读指令 */
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)i;       /* 发送ecc所在的列地址 */
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(i >> 8);
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = 0XE0;             /* 开始读数据 */

        nand_delay(NAND_TWHR_DELAY);                            /* 等待tWHR */
        pbuffer = (uint8_t *)&nand_dev.ecc_rdbuf[eccstart];     /* 把读取过程中的ECC值暂存在pbuffer */
  
        for (i = 0; i < 4 * eccnum; i++)    /* 读取保存在spare的ECC值 */
        {
            *(volatile uint8_t *)pbuffer++ = *(volatile uint8_t *)NAND_ADDRESS;
        }
  
        for (i = 0; i < eccnum; i++)        /* 检验ECC */
        {
            if (nand_dev.ecc_rdbuf[i + eccstart] != nand_dev.ecc_hdbuf[i + eccstart])   /* 不相等,需要校正 */
            {
                printf("err hd,rd:0x%x,0x%x\r\n", nand_dev.ecc_hdbuf[i + eccstart], nand_dev.ecc_rdbuf[i + eccstart]); 
                printf("eccnum,eccstart:%d,%d\r\n", eccnum, eccstart);
                printf("PageNum,ColNum:%d,%d\r\n", pagenum, colnum);
                res = nand_ecc_correction(p + NAND_ECC_SECTOR_SIZE * i, nand_dev.ecc_rdbuf[i + eccstart], nand_dev.ecc_hdbuf[i + eccstart]);    /* ECC校验 */

                if (res)
                {
                    errsta = NSTA_ECC2BITERR;   /* 标记2BIT及以上ECC错误 */
                }
                else
                {
                    errsta = NSTA_ECC1BITERR;   /* 标记1BIT ECC错误 */
                }
            }
        }
    }

    if (nand_wait_for_ready() != NSTA_READY)
    {
        errsta = NSTA_ERROR;    /* 失败 */
    }

    return errsta;              /* 成功 */
}

/**
 * @brief       读取NAND Flash的指定页指定列的数据(main区和spare区都可以使用此函数),并对比(FTL管理时需要)
 * @param       pagenum         : 要读取的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 要读取的列开始地址(也就是页内地址),范围:0~(page_totalsize-1)
 * @param       cmpval          : 要对比的值,以uint32_t为单位
 * @param       numbyte_to_read : 读取字数(以4字节为单位,不能跨页读)
 * @param       numbyte_equal   : 从初始位置持续与CmpVal值相同的数据个数
 * @retval      0,成功; 其他,错误代码
 */
uint8_t nand_readpagecomp(uint32_t pagenum, uint16_t colnum, uint32_t cmpval, uint16_t numbyte_to_read, uint16_t *numbyte_equal)
{
    uint16_t i = 0;
    uint8_t res = 0;
    
    /* 参考MT29F4G08器件手册P65 READ PAGE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_AREA_A;              /* 向NAND FLASH发送READ PAGE的第一个命令 */
    /* 发送5个字节的地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)colnum;          /* 发送第一个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(colnum >> 8);   /* 发送第二个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)pagenum;         /* 发送第一个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 8);  /* 发送第二个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 16); /* 发送第三个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_AREA_TRUE1;          /* 向NAND FLASH发送读取READ PAGE的第二个命令 */

    /**
     * 下面两行代码是等待R/B引脚变为低电平,其实主要起延时作用的,等待NAND操作R/B引脚。因为我们是通过
     * 将STM32的NWAIT引脚(NAND的R/B引脚)配置为普通IO,代码中通过读取NWAIT引脚的电平来判断NAND是否准备
     * 就绪的。这个也就是模拟的方法,所以在速度很快的时候有可能NAND还没来得及操作R/B引脚来表示NAND的忙
     * 闲状态,结果我们就读取了R/B引脚,这个时候肯定会出错的,事实上确实是会出错!大家也可以将下面两行
     * 代码换成延时函数,只不过这里我们为了效率所以没有用延时函数。
     */
    res = nand_waitrb(0);       /* 等待RB=0 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }

    /* 下面2行代码是真正判断NAND是否准备好的 */
    res = nand_waitrb(1);       /* 等待RB=1 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }

    for (i = 0; i < numbyte_to_read; i++)       /* 读取数据,每次读4字节 */
    {
        if (*(volatile uint32_t *)NAND_ADDRESS != cmpval)
        {
            break;  /* 如果有任何一个值,与cmpval不相等,则退出 */
        }
    }

    *numbyte_equal = i;     /* 与cmpval值相同的个数 */

    if (nand_wait_for_ready() != NSTA_READY)
    {
        return NSTA_ERROR;  /* 失败 */
    }

    return 0;               /* 成功 */
}

/**
 * @brief       在NAND一页中写入指定个字节的数据(main区和spare区都可以使用此函数)
 * @param       pagenum         : 要写入的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 要写入的列开始地址(也就是页内地址),范围:0~(page_totalsize-1)
 * @param       pbuffer         : 指向数据存储区
 * @param       numbyte_to_write: 要写入的字节数,该值不能超过该页剩余字节数!!!
 * @retval      0,成功; 其他,错误代码
 */
uint8_t nand_writepage(uint32_t pagenum, uint16_t colnum, uint8_t *pbuffer, uint16_t numbyte_to_write)
{
    volatile uint16_t i = 0;  
    uint8_t res = 0;
    uint8_t eccnum = 0;     /* 需要计算的ECC个数,每NAND_ECC_SECTOR_SIZE字节计算一个ecc */
    uint8_t eccstart = 0;   /* 第一个ECC值所属的地址范围 */
    
    /* 参考MT29F4G08器件手册P72 PROGRAM PAGE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_WRITE0;              /* 向NAND FLASH发送PRAGRAM PAGE的第一个命令 */
    /* 发送5个字节的地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)colnum;          /* 发送第一个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(colnum >> 8);   /* 发送第二个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)pagenum;         /* 发送第一个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 8);  /* 发送第二个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 16); /* 发送第三个字节的页地址 */
    
    nand_delay(NAND_TADL_DELAY);    /* 等待tADL */

    if (numbyte_to_write % NAND_ECC_SECTOR_SIZE)    /* 不是NAND_ECC_SECTOR_SIZE的整数倍,不进行ECC校验 */
    {  
        for (i = 0; i < numbyte_to_write; i++)      /* 写入数据 */
        {
            *(volatile uint8_t *)NAND_ADDRESS = *(volatile uint8_t *)pbuffer++;
        }
    }
    else
    {
        eccnum = numbyte_to_write / NAND_ECC_SECTOR_SIZE;   /* 得到ecc计算次数 */
        eccstart = colnum / NAND_ECC_SECTOR_SIZE;           /* 第一个ECC值所属的地址范围 */

        for (res = 0; res < eccnum; res++)
        {
            FMC_Bank2_3->PCR3 |= 1 << 6;                        /* 使能ECC校验 */

            for (i = 0; i < NAND_ECC_SECTOR_SIZE; i++)          /* 写入NAND_ECC_SECTOR_SIZE个数据 */
            {
                *(volatile uint8_t *)NAND_ADDRESS = *(volatile uint8_t *)pbuffer++;
            }
            while (!(FMC_Bank2_3->SR3 & (1 << 6)));             /* 等待FIFO空 */

            nand_dev.ecc_hdbuf[res+eccstart] = FMC_Bank2_3->ECCR3;    /* 读取硬件计算后的ECC值 */

            FMC_Bank2_3->PCR3 &= ~(1 << 6);                     /* 禁止ECC校验 */
        }

        i = nand_dev.page_mainsize + 0X10 + eccstart * 4;       /* 计算写入ECC的spare区地址 */
        
        nand_delay(NAND_TADL_DELAY);    /* 等待tADL */
        
        /* 参考MT29F4G08器件手册P93 PROGRAM PAGE和RANDOM DATA INPUT 操作时序 */
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = 0X85;          /* 向NAND FLASH发送随机写指令 */
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)i;   /* 发送ECC所在的列地址 */
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(i >> 8);
        
        nand_delay(NAND_TADL_DELAY);    /* 等待tADL */
        
        pbuffer = (uint8_t *)&nand_dev.ecc_hdbuf[eccstart];

        for (i = 0; i < eccnum; i++)    /* 写入ECC */
        { 
            for (res = 0; res < 4; res++)
            {
                *(volatile uint8_t *)NAND_ADDRESS = *(volatile uint8_t *)pbuffer++;
            }
        }
    }
    
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_WRITE_TURE1;  /* 向NAND FLASH发送PRAGRAM PAGE的第二个命令 */
    delay_us(NAND_TPROG_DELAY); /* 等待tPROG */

    if (nand_wait_for_ready() != NSTA_READY)
    {
        return NSTA_ERROR;      /* 失败 */
    }

    return 0;                   /* 成功 */
}

/**
 * @brief       在NAND一页中的指定地址开始,写入指定长度的恒定数字
 * @param       pagenum         : 要写入的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 要写入的列开始地址(也就是页内地址),范围:0~(page_totalsize-1)
 * @param       cval            : 要写入的指定常数
 * @param       numbyte_to_write: 要写入的字节数(以4字节为单位)
 * @retval      0,成功; 其他,错误代码
 */
uint8_t nand_write_pageconst(uint32_t pagenum, uint16_t colnum, uint32_t cval, uint16_t numbyte_to_write)
{
    uint16_t i = 0;
    
    /* 参考MT29F4G08器件手册P72 PROGRAM PAGE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_WRITE0;              /* 向NAND FLASH发送PRAGRAM PAGE的第一个命令 */
    /* 发送5个字节的地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)colnum;          /* 发送第一个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(colnum >> 8);   /* 发送第二个字节的列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)pagenum;         /* 发送第一个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 8);  /* 发送第二个字节的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(pagenum >> 16); /* 发送第三个字节的页地址 */

    nand_delay(NAND_TADL_DELAY);                /* 等待tADL */

    for (i = 0; i < numbyte_to_write; i++)      /* 写入数据,每次写4字节 */
    {
        *(volatile uint32_t *)NAND_ADDRESS = cval;
    }

    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_WRITE_TURE1;          /* 向NAND FLASH发送PRAGRAM PAGE的第二个命令 */
    delay_us(NAND_TPROG_DELAY);     /* 等待tPROG */

    if (nand_wait_for_ready() != NSTA_READY)
    {
        return NSTA_ERROR;  /* 失败  */
    }

    return 0;               /* 成功 */
}

/**
 * @brief       将一页数据拷贝到另一页,不写入新数据
 * @note        源页和目的页要在同一个Plane内!
 * @param       source_pagenum  : 源页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       dest_pagenum    : 目的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @retval      0,成功; 其他,错误代码
 */
uint8_t nand_copypage_withoutwrite(uint32_t source_pagenum, uint32_t dest_pagenum)
{
    uint8_t res = 0;
    uint16_t source_block = 0, dest_block = 0;
    /* 判断源页和目的页是否在同一个plane中 */
    source_block = source_pagenum / nand_dev.block_pagenum;
    dest_block = dest_pagenum / nand_dev.block_pagenum;

    if ((source_block % 2) != (dest_block % 2))
    {
        return NSTA_ERROR;  /* 不在同一个plane内 */
    }
    
    /* 参考MT29F4G08器件手册P80 READ FOR INTERNAL DATA MOVE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_MOVEDATA_CMD0;   /* 向NAND FLASH发送READ FOR INTERNAL DATA MOVE的第一个命令 */
    /* 发送源页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)0;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)0;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)source_pagenum;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(source_pagenum >> 8);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(source_pagenum >> 16);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_MOVEDATA_CMD1;   /* 向NAND FLASH发送READ FOR INTERNAL DATA MOVE的第二个命令 */
    
    /**
     *   下面两行代码是等待R/B引脚变为低电平,其实主要起延时作用的,等待NAND操作R/B引脚。因为我们是通过
     *   将STM32的NWAIT引脚(NAND的R/B引脚)配置为普通IO,代码中通过读取NWAIT引脚的电平来判断NAND是否准备
     *   就绪的。这个也就是模拟的方法,所以在速度很快的时候有可能NAND还没来得及操作R/B引脚来表示NAND的忙
     *   闲状态,结果我们就读取了R/B引脚,这个时候肯定会出错的,事实上确实是会出错!大家也可以将下面两行
     *   代码换成延时函数,只不过这里我们为了效率所以没有用延时函数。
    */
    res = nand_waitrb(0);       /* 等待RB=0 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }
    
    /* 下面2行代码是真正判断NAND是否准备好的 */
    res = nand_waitrb(1);       /* 等待RB=1 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }
    
    /* 参考MT29F4G08器件手册P129 INTERNAL DATA MOVE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_MOVEDATA_CMD2;   /* 向NAND FLASH发送INTERNAL DATA MOVE的第一个命令 */
    /* 发送目的页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)0;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)0;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)dest_pagenum;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(dest_pagenum >> 8);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(dest_pagenum >> 16);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_MOVEDATA_CMD3;    /* 向NAND FLASH发送INTERNAL DATA MOVE的第二个命令 */
    
    delay_us(NAND_TPROG_DELAY);     /* 等待tPROG  */

    if (nand_wait_for_ready() != NSTA_READY)
    {
        return NSTA_ERROR;          /* NAND未准备好 */
    }

    return 0;   /* 成功 */
}

/**
 * @brief       将一页数据拷贝到另一页,并且可以写入数据
 * @note        源页和目的页要在同一个Plane内!
 * @param       source_pagenum  : 源页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       dest_pagenum    : 目的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 页内列地址,范围:0~(page_totalsize-1)
 * @param       pbuffer         : 要写入的数据
 * @param       numbyte_to_write: 要写入的数据个数
 * @retval      0,成功; 其他,错误代码
 */
uint8_t nand_copypage_withwrite(uint32_t source_pagenum, uint32_t dest_pagenum, uint16_t colnum, uint8_t *pbuffer, uint16_t numbyte_to_write)
{
    uint8_t res = 0;
    volatile uint16_t i = 0;
    uint16_t source_block = 0, dest_block = 0;  
    uint8_t eccnum = 0;     /* 需要计算的ECC个数,每NAND_ECC_SECTOR_SIZE字节计算一个ecc */
    uint8_t eccstart = 0;   /* 第一个ECC值所属的地址范围 */
    /* 判断源页和目的页是否在同一个plane中 */
    source_block = source_pagenum / nand_dev.block_pagenum;
    dest_block = dest_pagenum / nand_dev.block_pagenum;

    if ((source_block % 2) != (dest_block % 2))
    {
        return NSTA_ERROR;      /* 不在同一个plane内 */
    }
    
    /* 参考MT29F4G08器件手册P80 READ FOR INTERNAL DATA MOVE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_MOVEDATA_CMD0;   /* 向NAND FLASH发送READ FOR INTERNAL DATA MOVE的第一个命令 */
    /* 发送源页地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)0;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)0;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)source_pagenum;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(source_pagenum >> 8);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(source_pagenum >> 16);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_MOVEDATA_CMD1;    /* 向NAND FLASH发送READ FOR INTERNAL DATA MOVE的第二个命令 */

    /**
     * 下面两行代码是等待R/B引脚变为低电平,其实主要起延时作用的,等待NAND操作R/B引脚。因为我们是通过
     * 将STM32的NWAIT引脚(NAND的R/B引脚)配置为普通IO,代码中通过读取NWAIT引脚的电平来判断NAND是否准备
     * 就绪的。这个也就是模拟的方法,所以在速度很快的时候有可能NAND还没来得及操作R/B引脚来表示NAND的忙
     * 闲状态,结果我们就读取了R/B引脚,这个时候肯定会出错的,事实上确实是会出错!大家也可以将下面两行
     * 代码换成延时函数,只不过这里我们为了效率所以没有用延时函数。
     */
    res = nand_waitrb(0);       /* 等待RB=0 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }

    /* 下面2行代码是真正判断NAND是否准备好的 */
    res = nand_waitrb(1);       /* 等待RB=1 */
    if (res)
    {
        return NSTA_TIMEOUT;    /* 超时退出 */
    }
    
    /* 参考MT29F4G08器件手册P129 INTERNAL DATA MOVE 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_MOVEDATA_CMD2;    /* 向NAND FLASH发送READ FOR INTERNAL DATA MOVE的第一个命令 */
    /* 发送目的页地址 和 页内列地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)colnum;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(colnum >> 8);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)dest_pagenum;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(dest_pagenum >> 8);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(dest_pagenum >> 16);

    nand_delay(NAND_TADL_DELAY);    /* 等待tADL */

    if (numbyte_to_write % NAND_ECC_SECTOR_SIZE)    /* 不是NAND_ECC_SECTOR_SIZE的整数倍,不进行ECC校验 */
    {  
        for (i = 0; i < numbyte_to_write; i++)      /* 写入数据 */
        {
            *(volatile uint8_t *)NAND_ADDRESS = *(volatile uint8_t *)pbuffer++;
        }
    }
    else
    {
        eccnum = numbyte_to_write / NAND_ECC_SECTOR_SIZE;   /* 得到ecc计算次数 */
        eccstart = colnum / NAND_ECC_SECTOR_SIZE;           /* 第一个ECC值所属的地址范围 */

        for (res = 0; res < eccnum; res++)
        {
            FMC_Bank2_3->PCR3 |= 1 << 6;                    /* 使能ECC校验 */

            for (i = 0; i < NAND_ECC_SECTOR_SIZE; i++)      /* 写入NAND_ECC_SECTOR_SIZE个数据 */
            {
                *(volatile uint8_t *)NAND_ADDRESS = *(volatile uint8_t *)pbuffer++;
            }

            while (!(FMC_Bank2_3->SR3 & (1<<6)));                     /* 等待FIFO空 */

            nand_dev.ecc_hdbuf[res + eccstart] = FMC_Bank2_3->ECCR3;  /* 读取硬件计算后的ECC值 */

            FMC_Bank2_3->PCR3 &= ~(1 << 6);                           /* 禁止ECC校验 */
        }

        i = nand_dev.page_mainsize + 0X10 + eccstart * 4;       /* 计算写入ECC的spare区地址 */
        nand_delay(NAND_TADL_DELAY);                            /* 等待tADL */
        
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = 0X85;  /* 随机写指令 */
        /* 发送地址 */
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)i;
        *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(i >> 8);
        nand_delay(NAND_TADL_DELAY);        /* 等待tADL */

        pbuffer = (uint8_t *)&nand_dev.ecc_hdbuf[eccstart];

        for (i = 0; i < eccnum; i++)        /* 写入ECC */
        { 
            for (res = 0; res < 4; res++)
            {
                *(volatile uint8_t *)NAND_ADDRESS = *(volatile uint8_t *)pbuffer++;
            }
        }
    }

    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD) = NAND_MOVEDATA_CMD3;     /* 发送命令0X10 */
    delay_us(NAND_TPROG_DELAY);     /* 等待tPROG */

    if (nand_wait_for_ready() != NSTA_READY)
    {
        return NSTA_ERROR;  /* 失败 */
    }

    return 0;               /* 成功 */ 
}

/**
 * @brief       读取spare区中的数据
 * @param       pagenum         : 要写入的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 要写入的spare区地址(spare区中哪个地址),范围:0~(page_sparesize-1)
 * @param       pbuffer         : 接收数据缓冲区
 * @param       numbyte_to_read : 要读取的字节数(不大于page_sparesize)
 * @retval      0,成功; 其他,错误代码
 */
uint8_t nand_readspare(uint32_t pagenum, uint16_t colnum, uint8_t *pbuffer, uint16_t numbyte_to_read)
{
    uint8_t temp = 0;
    uint8_t remainbyte = 0;
    remainbyte = nand_dev.page_sparesize - colnum;

    if (numbyte_to_read > remainbyte) 
    {
        numbyte_to_read = remainbyte;   /* 确保要写入的字节数不大于spare剩余的大小 */
    }

    temp = nand_readpage(pagenum, colnum + nand_dev.page_mainsize, pbuffer, numbyte_to_read);   /* 读取数据 */
    return temp;
}

/**
 * @brief       向spare区中写数据
 * @param       pagenum         : 要写入的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 要写入的spare区地址(spare区中哪个地址),范围:0~(page_sparesize-1)
 * @param       pbuffer         : 要写入的数据首地址
 * @param       numbyte_to_write: 要写入的字节数(不大于page_sparesize)
 * @retval      0,成功; 其他,失败
 */
uint8_t nand_writespare(uint32_t pagenum, uint16_t colnum, uint8_t *pbuffer, uint16_t numbyte_to_write)
{
    uint8_t temp = 0;
    uint8_t remainbyte = 0;

    remainbyte = nand_dev.page_sparesize - colnum;

    if (numbyte_to_write > remainbyte)
    {
        numbyte_to_write = remainbyte;  /* 确保要读取的字节数不大于spare剩余的大小 */
    }

    temp = nand_writepage(pagenum, colnum + nand_dev.page_mainsize, pbuffer, numbyte_to_write); /* 读取 */

    return temp;
}

/**
 * @brief       擦除一个块
 * @param       blocknum        : 要擦除的BLOCK编号,范围:0-(block_totalnum-1)
 * @retval      0,擦除成功; 其他,擦除失败
 */
uint8_t nand_eraseblock(uint32_t blocknum)
{
     /* 将块地址转换为页地址 */
    if (nand_dev.id == MT29F16G08ABABA)
    {
        blocknum <<= 7;
    }
    else if (nand_dev.id == MT29F4G08ABADA)
    {
        blocknum <<= 6;
    }
    
    /* 参考MT29F4G08器件手册P77 ERASE BLOCK 操作时序 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_ERASE0;  /* 向NAND FLASH发送ERASE BLOCK的第一个命令 */ 
    /* 发送块地址 */
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)blocknum;
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(blocknum >> 8);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_ADDR) = (uint8_t)(blocknum >> 16);
    *(volatile uint8_t *)(NAND_ADDRESS | NAND_CMD)  = NAND_ERASE1;  /* 向NAND FLASH发送ERASE BLOCK的第二个命令 */ 

    delay_ms(NAND_TBERS_DELAY);     /* 等待擦除成功 */

    if (nand_wait_for_ready() != NSTA_READY)
    {
        return NSTA_ERROR;  /* 失败 */
    }

    return 0;               /* 成功 */
}

/**
 * @brief       全片擦除NAND FLASH
 * @param       无
 * @retval      无
 */
void nand_erasechip(void)
{
    uint8_t status;
    uint16_t i = 0;

    for (i = 0; i < nand_dev.block_totalnum; i++)   /* 循环擦除所有的块 */
    {
        status = nand_eraseblock(i);

        if (status)
        {
            printf("Erase %d block fail!!,错误码为%d\r\n",i,status);  /* 擦除失败 */
        }
    }
}

/**
 * @brief       获取ECC的奇数位/偶数位
 * @param       oe      : 0,偶数位; 1,奇数位
 * @param       eccval  : 输入的ecc值
 * @retval      计算后的ecc值(最多16位)
 */
uint16_t nand_ecc_get_oe(uint8_t oe, uint32_t eccval)
{
    uint8_t i;
    uint16_t ecctemp = 0;

    for (i = 0; i < 24; i++)
    {
        if ((i % 2) == oe)      /* 通过oe传参把奇数位置和偶数位置区分 */
        {
            /* 把eccval当前位的位置数据做判断 */
            if ((eccval >> i) & 0X01)       /* 该位置为1 */
            {
                ecctemp += 1 << (i >> 1);   /* 当前位为1的情况下,(i >> 1)即eccval该位位置相对于ecctemp的位位置
                                               而 1 << (i >> 1)即对acctemp相应位置置1
                                             */
            }
            /* 该位置为0,不做处理 */
        }
    }

    return ecctemp;
}

/**
 * @brief       ECC校正函数(修正1bit错误,并报告2bit及以上错误)
 * @param       data_buf    : 数据缓存区
 * @param       eccrd       : 读取出来, 原来保存的ECC值
 * @param       ecccl       : 读取数据时, 硬件计算的ECC值
 * @retval      0,错误已修正; 其他,ECC错误(有大于2个bit的错误,无法恢复)
 */
uint8_t nand_ecc_correction(uint8_t* data_buf, uint32_t eccrd, uint32_t ecccl)
{
    uint16_t eccrdo, eccrde, eccclo, ecccle;
    uint16_t eccchk = 0;
    uint16_t errorpos = 0;
    uint32_t bytepos = 0;
    
    eccrdo = nand_ecc_get_oe(1, eccrd);     /* 获取eccrd的奇数位 */
    eccrde = nand_ecc_get_oe(0, eccrd);     /* 获取eccrd的偶数位 */
    eccclo = nand_ecc_get_oe(1, ecccl);     /* 获取ecccl的奇数位 */
    ecccle = nand_ecc_get_oe(0, ecccl);     /* 获取ecccl的偶数位 */
    eccchk = eccrdo^eccrde^eccclo^ecccle;   /* 4个ECC数值进行按位异或 */

    if (eccchk == 0XFFF)                    /* 全1,说明只有1bit ECC错误 */
    {
        errorpos = eccrdo^eccclo;           /* 出错bit位置可通过原有eccrdo和eccclo进行按位异或求得 */
        printf("errorpos:%d\r\n", errorpos); 
        bytepos = errorpos / 8;             /* 计算字节位置 */
        data_buf[bytepos] ^= 1 << (errorpos % 8);   /* 对出错位进行取反,修正错误 */
    }
    else                                    /* 不是全1,说明至少有2bit ECC错误,无法修复 */
    {
        printf("2bit ecc error or more\r\n");       /* 不全是1,说明至少有2bit ECC错误,无法修复 */
        return 1;
    } 

    return 0;
}

nand.h
nandtest.c
nandtest.h
ftl.c

#include "string.h"
#include "./BSP/NAND/ftl.h"
#include "./MALLOC/malloc.h"
#include "./BSP/NAND/nand.h"
#include "./SYSTEM/usart/usart.h"


/**
 *        每个块,第一个page的spare区,前四个字节的含义:
 *        第一个字节,表示该块是否是坏块:0XFF,正常块;其他值,坏块.
 *        第二个字节,表示该块是否被用过:0XFF,没有写过数据;0XCC,写过数据了.
 *        第三和第四个字节,表示该块所属的逻辑块编号. 

 *        每个page,spare区16字节以后的字节含义:
 *        第十六字节开始,后续每4个字节用于存储一个扇区(大小:NAND_ECC_SECTOR_SIZE)的ECC值,用于ECC校验
 */

/**
 * @brief       FTL层初始化
 * @param       无
 * @retval      0,正常;其他,失败
 */
uint8_t ftl_init(void)
{
    uint8_t temp;

    if (nand_init())   /* 初始化NAND FLASH */
    {
        return 1;
    }

    if (nand_dev.lut)
    {
        myfree(SRAMIN, nand_dev.lut);
    }

    nand_dev.lut = mymalloc(SRAMIN, (nand_dev.block_totalnum) * 2); /* 给LUT表申请内存 */
    memset(nand_dev.lut, 0, nand_dev.block_totalnum * 2);           /* 全部清理 */

    if (!nand_dev.lut)
    {
        return 1;                                                   /* 内存申请失败  */
    }

    temp = ftl_create_lut(1);   /* 重新创建LUT表 */

    if (temp) 
    {
        printf("format nand flash...\r\n");
        temp = ftl_format();                                        /* 格式化NAND */

        if (temp)
        {
            printf("format failed!\r\n");
            return 2;
        }
    }
    else                                                            /* 创建LUT表成功 */
    {
        printf("total block num: %d\r\n", nand_dev.block_totalnum);
        printf("good block num: %d\r\n", nand_dev.good_blocknum);
        printf("valid block num: %d\r\n", nand_dev.valid_blocknum);
    }

    return 0;
} 

/**
 * @brief       标记某一个块为坏块
 * @param       blocknum   : 块编号,范围:0~(block_totalnum-1)
 * @retval      无
 */
void ftl_badblock_mark(uint32_t blocknum)
{
    uint32_t temp = 0XAAAAAAAA; /* 坏块标记mark,任意值都OK,只要不是0XFF.这里写前4个字节,方便ftl_find_unused_block函数检查坏块.(不检查备份区,以提高速度) */
    nand_writespare(blocknum * nand_dev.block_pagenum, 0, (uint8_t *)&temp, 4);      /* 在第一个page的spare区,第一个字节做坏块标记(前4个字节都写) */
    nand_writespare(blocknum * nand_dev.block_pagenum + 1, 0, (uint8_t *)&temp, 4);  /* 在第二个page的spare区,第一个字节做坏块标记(备份用,前4个字节都写) */
}

/**
 * @brief       检查某一块是否是坏块
 * @param       blocknum    : 块编号,范围:0~(block_totalnum-1)
 * @retval      0,好块;
 *              其他,坏块
 */
uint8_t ftl_check_badblock(uint32_t blocknum)
{
    uint8_t flag = 0;

    nand_readspare(blocknum * nand_dev.block_pagenum, 0, &flag, 1);         /* 读取坏块标志 */

    if (flag == 0XFF)                                                       /* 好块?,读取备份区坏块标记 */
    {
        nand_readspare(blocknum * nand_dev.block_pagenum + 1, 0, &flag, 1); /* 读取备份区坏块标志 */

        if (flag == 0XFF)
        {
            return 0;                                                       /* 好块 */
        }

        else return 1;                                                      /* 坏块 */
    }

    return 2; 
}

/**
 * @brief       标记某一个块已经使用
 * @param       blocknum    : 块编号,范围:0~(block_totalnum-1)
 * @retval      0,成功;
 *              其他,失败
 */
uint8_t ftl_used_blockmark(uint32_t blocknum)
{
    uint8_t usedflag = 0XCC;
    uint8_t temp = 0;
    temp = nand_writespare(blocknum * nand_dev.block_pagenum, 1, (uint8_t *)&usedflag, 1); /* 写入块已经被使用标志 */

    return temp;
}

/**
 * @brief       从给定的块开始找到往前找到一个未被使用的块(指定奇数/偶数)
 * @param       sblock      : 开始块,范围:0~(block_totalnum-1)
 * @param       flag        : 0,偶数快; 1,奇数块.
 * @retval      0XFFFFFFFF,失败;
 *              其他值,未使用块号
 */
uint32_t ftl_find_unused_block(uint32_t sblock, uint8_t flag)
{
    uint32_t temp = 0;
    uint32_t blocknum = 0;

    for (blocknum = sblock + 1; blocknum > 0; blocknum--)
    {
        if (((blocknum - 1) % 2) == flag)   /* 奇偶合格,才检测 */
        {
            nand_readspare((blocknum - 1) * nand_dev.block_pagenum, 0, (uint8_t *)&temp, 4); /* 读块是否被使用标记 */
            if (temp == 0XFFFFFFFF)
            {
                return (blocknum - 1);      /* 找到一个空块,返回块编号 */
            }
        }
    }

    return 0XFFFFFFFF;                      /* 未找到空余块 */
}

/**
 * @brief       查找与给定块在同一个plane内的未使用的块
 * @param       sblock    : 给定块,范围:0~(block_totalnum-1)
 * @retval      0XFFFFFFFF,失败;其他值,未使用块号
 */
uint32_t ftl_find_same_plane_unused_block(uint32_t sblock)
{
    static uint32_t curblock = 0XFFFFFFFF;
    uint32_t unusedblock = 0;

    if (curblock > (nand_dev.block_totalnum - 1))
    {
        curblock = nand_dev.block_totalnum - 1;                   /* 超出范围了,强制从最后一个块开始 */
    }

    unusedblock = ftl_find_unused_block(curblock,sblock % 2);     /* 从当前块,开始,向前查找空余块  */

    if (unusedblock == 0XFFFFFFFF && curblock < (nand_dev.block_totalnum - 1))  /* 未找到,且不是从最末尾开始找的 */
    {
        curblock = nand_dev.block_totalnum - 1;                   /* 强制从最后一个块开始 */
        unusedblock = ftl_find_unused_block(curblock,sblock % 2); /* 从最末尾开始,重新找一遍 */ 
    }
    if (unusedblock == 0XFFFFFFFF)
    {
        return 0XFFFFFFFF;                                        /* 找不到空闲block */
    }

    curblock = unusedblock;                                       /* 当前块号等于未使用块编号.下次则从此处开始查找 */

    return unusedblock;                                           /* 返回找到的空闲block */
}

/**
 * @brief       将一个块的数据拷贝到另一块,并且可以写入数据
 * @param       source_pagenum  : 要写入数据的页地址,范围:0~(block_pagenum*block_totalnum-1)
 * @param       colnum          : 要写入的列开始地址(也就是页内地址),范围:0~(page_totalsize-1)
 * @param       pbuffer         : 要写入的数据
 * @param       numbyte_to_write: 要写入的字节数,该值不能超过块内剩余容量大小
 * @retval      0,成功;
 *              其他,失败
 */
uint8_t ftl_copy_and_write_to_block(uint32_t source_pagenum, uint16_t colnum, uint8_t *pbuffer, uint32_t numbyte_to_write)
{
    uint16_t i = 0, temp = 0, wrlen;
    uint32_t source_block = 0, pageoffset = 0;
    uint32_t unusedblock = 0; 
    source_block = source_pagenum / nand_dev.block_pagenum;         /* 获得页所在的块号 */
    pageoffset = source_pagenum % nand_dev.block_pagenum;           /* 获得页在所在块内的偏移 */

retry:
    unusedblock = ftl_find_same_plane_unused_block(source_block);   /* 查找与源块在一个plane的未使用块 */

    if (unusedblock > nand_dev.block_totalnum)
    {
        return 1;                                                   /* 当找到的空余块号大于块总数量的话肯定是出错了 */
    }

    for (i = 0; i < nand_dev.block_pagenum; i++)                    /* 将一个块的数据复制到找到的未使用块中 */
    {
        if (i >= pageoffset && numbyte_to_write)                    /* 数据要写入到当前页 */
        { 
            if (numbyte_to_write > (nand_dev.page_mainsize - colnum)) /* 要写入的数据,超过了当前页的剩余数据 */
            {
                wrlen = nand_dev.page_mainsize - colnum;            /* 写入长度等于当前页剩余数据长度 */
            }
            else 
            {
                wrlen = numbyte_to_write;                           /* 写入全部数据 */
            }

            temp = nand_copypage_withwrite(source_block * nand_dev.block_pagenum + i, unusedblock * nand_dev.block_pagenum + i, colnum, pbuffer, wrlen);
            colnum = 0;                 /* 列地址归零 */
            pbuffer += wrlen;           /* 写地址偏移 */
            numbyte_to_write -= wrlen;  /* 写入数据减少 */
        }
        else    /* 无数据写入,直接拷贝即可 */
        {
            temp = nand_copypage_withoutwrite(source_block * nand_dev.block_pagenum + i, unusedblock * nand_dev.block_pagenum + i);
        }

        if (temp)       /* 返回值非零,当坏块处理 */
        { 
            ftl_badblock_mark(unusedblock);     /* 标记为坏块 */
            ftl_create_lut(1);                  /* 重建LUT表 */
            goto retry;
        }
    }

    if (i == nand_dev.block_pagenum)        /* 拷贝完成 */
    {
        ftl_used_blockmark(unusedblock);    /* 标记块已经使用 */
        nand_eraseblock(source_block);      /* 擦除源块 */
        
        for (i = 0; i < nand_dev.block_totalnum; i++)   /* 修正LUT表,用unusedblock替换source_block */
        {
            if (nand_dev.lut[i] == source_block)
            {
                nand_dev.lut[i] = unusedblock;
                break;
            }
        }
    }

    return 0;   /* 成功 */
}

/**
 * @brief       逻辑块号转换为物理块号 
 * @param       lbnnum    : 逻辑块编号
 * @retval      物理块编号
 */
uint16_t ftl_lbn_to_pbn(uint32_t lbnnum)
{
    uint16_t pbnno = 0;

    /* 当逻辑块号大于有效块数的时候返回0XFFFF */
    if (lbnnum > nand_dev.valid_blocknum)
    {
        return 0XFFFF;
    }

    pbnno = nand_dev.lut[lbnnum];

    return pbnno;
}

/**
 * @brief       写扇区(支持多扇区写),FATFS文件系统使用
 * @param       pbuffer     : 要写入的数据
 * @param       sectorno    : 起始扇区号
 * @param       sectorsize  : 扇区大小(不能大于NAND_ECC_SECTOR_SIZE定义的大小,否则会出错!!)
 * @param       sectorcount : 要写入的扇区数量
 * @retval      0,成功;
 *              其他,失败
 */
uint8_t ftl_write_sectors(uint8_t *pbuffer, uint32_t sectorno, uint16_t sectorsize, uint32_t sectorcount)
{
    uint8_t flag = 0;
    uint16_t temp;
    uint32_t i = 0;
    uint16_t wsecs;                 /* 写页大小 */
    uint32_t wlen;                  /* 写入长度 */
    uint32_t lbnno;                 /* 逻辑块号 */
    uint32_t pbnno;                 /* 物理块号 */
    uint32_t phypageno;             /* 物理页号 */
    uint32_t pageoffset;            /* 页内偏移地址 */
    uint32_t blockoffset;           /* 块内偏移地址 */
    uint32_t markdpbn = 0XFFFFFFFF; /* 标记了的物理块编号 */

    for (i = 0; i < sectorcount; i++)
    {
        lbnno = (sectorno+i) / (nand_dev.block_pagenum * (nand_dev.page_mainsize / sectorsize));/* 根据逻辑扇区号和扇区大小计算出逻辑块号 */
        pbnno = ftl_lbn_to_pbn(lbnno);      /* 将逻辑块转换为物理块 */

        if (pbnno >= nand_dev.block_totalnum)
        {
            return 1;       /* 物理块号大于NAND FLASH的总块数,则失败. */
        }

        blockoffset =((sectorno + i) % (nand_dev.block_pagenum * (nand_dev.page_mainsize / sectorsize))) * sectorsize; /* 计算块内偏移 */
        phypageno = pbnno * nand_dev.block_pagenum + blockoffset / nand_dev.page_mainsize;      /* 计算出物理页号 */
        pageoffset = blockoffset % nand_dev.page_mainsize;  /* 计算出页内偏移地址  */
        temp = nand_dev.page_mainsize - pageoffset;         /* page内剩余字节数 */
        temp /= sectorsize;         /* 可以连续写入的sector数  */
        wsecs = sectorcount - i;    /* 还剩多少个sector要写 */

        if (wsecs >= temp)
        {
            wsecs = temp;   /* 大于可连续写入的sector数,则写入temp个扇区 */
        }

        wlen = wsecs * sectorsize;      /* 每次写wsecs个sector */

        /* 读出写入大小的内容判断是否全为0XFF */
        flag = nand_readpagecomp(phypageno, pageoffset, 0XFFFFFFFF, wlen / 4, &temp);   /* 读一个wlen/4大小个数据,并与0XFFFFFFFF对比 */

        if (flag)
        {
            return 2;   /* 读写错误,坏块 */
        }
        
        if (temp == (wlen / 4)) 
        {
            flag = nand_writepage(phypageno, pageoffset, pbuffer, wlen);/* 全为0XFF,可以直接写数据 */
        }
        else
        {
            flag = 1;   /* 不全是0XFF,则另作处理 */
        }

        if (flag == 0 && (markdpbn != pbnno))   /* 全是0XFF,且写入成功,且标记了的物理块与当前物理块不同 */
        {
            flag = ftl_used_blockmark(pbnno);   /* 标记此块已经使用 */
            markdpbn = pbnno;   /* 标记完成,标记块=当前块,防止重复标记 */
        }
        
        if (flag)   /* 不全为0XFF/标记失败,将数据写到另一个块 */
        {
            temp = ((uint32_t)nand_dev.block_pagenum * nand_dev.page_mainsize - blockoffset) / sectorsize;/* 计算整个block还剩下多少个SECTOR可以写入 */
            wsecs = sectorcount - i;    /* 还剩多少个sector要写 */

            if (wsecs >= temp)
            {
                wsecs = temp;           /* 大于可连续写入的sector数,则写入temp个扇区 */
            }

            wlen = wsecs * sectorsize;  /* 每次写wsecs个sector */
            flag = ftl_copy_and_write_to_block(phypageno, pageoffset, pbuffer, wlen);  /* 拷贝到另外一个block,并写入数据 */
            if (flag)
            {
                return 3;   /* 失败 */
            }
        }

        i += wsecs - 1;
        pbuffer += wlen;    /* 数据缓冲区指针偏移 */
    }
    
    return 0;
} 

/**
 * @brief       读扇区(支持多扇区读),FATFS文件系统使用
 * @param       pbuffer     : 数据缓存区
 * @param       sectorno    : 起始扇区号
 * @param       sectorsize  : 扇区大小
 * @param       sectorcount : 要读取的扇区数量
 * @retval      0,成功;
 *              其他,失败
 */
uint8_t ftl_read_sectors(uint8_t *pbuffer, uint32_t sectorno, uint16_t sectorsize, uint32_t sectorcount)
{
    uint8_t flag = 0;
    uint16_t rsecs;         /* 单次读取页数 */
    uint32_t i = 0;
    uint32_t lbnno;         /* 逻辑块号 */
    uint32_t pbnno;         /* 物理块号 */
    uint32_t phypageno;     /* 物理页号 */
    uint32_t pageoffset;    /* 页内偏移地址 */
    uint32_t blockoffset;   /* 块内偏移地址 */

    for (i = 0; i < sectorcount; i++)
    {
        lbnno = (sectorno + i) / (nand_dev.block_pagenum * (nand_dev.page_mainsize / sectorsize));  /* 根据逻辑扇区号和扇区大小计算出逻辑块号 */
        pbnno = ftl_lbn_to_pbn(lbnno);      /* 将逻辑块转换为物理块 */

        if (pbnno >= nand_dev.block_totalnum)
        {
            return 1;       /* 物理块号大于NAND FLASH的总块数,则失败. */
        }

        blockoffset = ((sectorno + i) % (nand_dev.block_pagenum * (nand_dev.page_mainsize / sectorsize))) * sectorsize; /* 计算块内偏移 */
        phypageno = pbnno * nand_dev.block_pagenum + blockoffset / nand_dev.page_mainsize;          /* 计算出物理页号 */
        pageoffset = blockoffset%nand_dev.page_mainsize;                                            /* 计算出页内偏移地址 */
        rsecs = (nand_dev.page_mainsize - pageoffset) / sectorsize;                                 /* 计算一次最多可以读取多少页 */

        if (rsecs > (sectorcount - i))
        {
            rsecs = sectorcount - i;    /* 最多不能超过SectorCount-i */
        }
        
        flag = nand_readpage(phypageno, pageoffset, pbuffer, rsecs * sectorsize);   /* 读取数据 */

        if (flag == NSTA_ECC1BITERR)    /* 对于1bit ecc错误,可能为坏块 */
        {
            flag = nand_readpage(phypageno, pageoffset, pbuffer, rsecs * sectorsize);   /* 重读数据,再次确认 */

            if (flag == NSTA_ECC1BITERR)
            {
                ftl_copy_and_write_to_block(phypageno, pageoffset, pbuffer, rsecs * sectorsize);    /* 搬运数据 */
                flag = ftl_blockcompare(phypageno / nand_dev.block_pagenum, 0XFFFFFFFF);            /* 全1检查,确认是否为坏块 */

                if (flag == 0)
                {
                    flag = ftl_blockcompare(phypageno / nand_dev.block_pagenum, 0X00);              /* 全0检查,确认是否为坏块 */
                    nand_eraseblock(phypageno / nand_dev.block_pagenum);                            /* 检测完成后,擦除这个块 */
                }

                if (flag)                                                                           /* 全0/全1检查出错,肯定是坏块了. */
                {
                    ftl_badblock_mark(phypageno/nand_dev.block_pagenum);                            /* 标记为坏块 */
                    ftl_create_lut(1);                                                              /* 重建LUT表 */
                }

                flag = 0;
            }
        }

        if (flag == NSTA_ECC2BITERR)
        {
            flag = 0;                       /* 2bit ecc错误,不处理(可能是初次写入数据导致的) */
        }
        if (flag)
        {
            return 2;                       /* 失败 */
        }

        pbuffer += sectorsize * rsecs;      /* 数据缓冲区指针偏移 */
        i += rsecs - 1;
    }
    
    return 0; 
}

/**
 * @brief       重新创建LUT表
 * @param       mode    : 0,仅检查第一个坏块标记
 *                        1,两个坏块标记都要检查(备份区也要检查)
 * @retval      0,成功;其他,失败
 */
uint8_t ftl_create_lut(uint8_t mode)
{
    uint32_t i;
    uint8_t buf[4];
    uint32_t lbnnum = 0;    /* 逻辑块号 */

    for (i = 0; i < nand_dev.block_totalnum; i++)   /* 复位LUT表,初始化为无效值,也就是0XFFFF */
    {
        nand_dev.lut[i] = 0XFFFF;
    }

    nand_dev.good_blocknum = 0;
    
    for (i = 0; i < nand_dev.block_totalnum; i++)
    {
        nand_readspare(i * nand_dev.block_pagenum, 0, buf, 4);         /* 读取4个字节 */

        if (buf[0] == 0XFF && mode)
        {
            nand_readspare(i * nand_dev.block_pagenum + 1, 0, buf, 1); /* 好块,且需要检查2次坏块标记 */
        }

        if (buf[0] == 0XFF)                                            /* 是好块 */
        { 
            lbnnum = ((uint16_t)buf[3] << 8) + buf[2];                 /* 得到逻辑块编号 */
            if (lbnnum < nand_dev.block_totalnum)                      /* 逻辑块号肯定小于总的块数量 */
            {
                nand_dev.lut[lbnnum] = i;                              /* 更新LUT表,写LBNnum对应的物理块编号 */
            }
            nand_dev.good_blocknum++;
        }
        else
        {
            printf("bad block index:%d\r\n", i);
        }
    }
    
    /* LUT表建立完成以后检查有效块个数 */
    for (i = 0; i < nand_dev.block_totalnum; i++)
    {
        if (nand_dev.lut[i] >= nand_dev.block_totalnum)
        {
            nand_dev.valid_blocknum = i;
            break;
        }
    }

    if (nand_dev.valid_blocknum < 100)
    {
        return 2;               /* 有效块数小于100,有问题.需要重新格式化 */
    }

    return 0;                   /* LUT表创建完成 */
}

/**
 * @brief       FTL整个Block与某个数据对比
 * @param       blockx      : block编号
 * @param       cmpval      : 要与之对比的值
 * @retval      0,检查成功,全部相等;
                1,检查失败,有不相等的情况
 */
uint8_t ftl_blockcompare(uint32_t blockx, uint32_t cmpval)
{
    uint8_t res;
    uint16_t i, j, k;

    for (i = 0; i < 3; i++)                             /* 允许3次机会 */
    {
        for (j = 0; j < nand_dev.block_pagenum; j++)
        {
            /* 检查一个page,并与0XFFFFFFFF对比 */
            nand_readpagecomp(blockx * nand_dev.block_pagenum, 0, cmpval, nand_dev.page_mainsize / 4, &k); 

            if (k != (nand_dev.page_mainsize / 4))break;
        }

        if (j == nand_dev.block_pagenum)
        {
            return 0;                                   /* 检查合格,直接退出 */
        }

        res = nand_eraseblock(blockx);

        if (res)
        {
            printf("error erase block:%d\r\n", i);
        }
        else
        { 
            if (cmpval != 0XFFFFFFFF)                   /* 不是判断全1,则需要重写数据 */
            {
                for (k = 0; k < nand_dev.block_pagenum; k++)
                {
                    nand_write_pageconst(blockx * nand_dev.block_pagenum + k, 0, 0, nand_dev.page_mainsize / 4); /* 写PAGE */
                }
            }
        }
    }

    printf("bad block checked:%d\r\n", blockx);
    return 1;
}

/**
 * @brief       FTL初始化时,搜寻所有坏块,使用:擦-写-读 方式
 * @note        512M的NAND ,需要约3分钟时间,来完成检测
 *              对于RGB屏,由于频繁读写NAND,会引起屏幕乱闪
 * @param       无
 * @retval      好块的数量
 */
uint32_t ftl_search_badblock(void)
{
    uint8_t *blktbl;
    uint8_t res;
    uint32_t i, j; 
    uint32_t goodblock = 0;

    blktbl = mymalloc(SRAMIN, nand_dev.block_totalnum);  /* 申请block坏块表内存,对应项:0,好块;1,坏块; */
    nand_erasechip();                                    /* 全片擦除 */

    for (i = 0; i < nand_dev.block_totalnum; i++)        /* 第一阶段检查,检查全1 */
    {
        res = ftl_blockcompare(i, 0XFFFFFFFF);           /* 全1检查 */

        if (res)
        {
            blktbl[i] = 1;                               /* 坏块  */
        }
        else
        { 
            blktbl[i] = 0;/* 好块 */

            for (j = 0; j < nand_dev.block_pagenum; j++) /* 写block为全0,为后面的检查准备 */
            {
                nand_write_pageconst(i * nand_dev.block_pagenum + j, 0, 0, nand_dev.page_mainsize / 4);
            } 
        }
    }

    for (i = 0; i < nand_dev.block_totalnum; i++)        /* 第二阶段检查,检查全0 */
    { 
        if (blktbl[i] == 0)                              /* 在第一阶段,没有被标记坏块的,才可能是好块 */
        {
            res = ftl_blockcompare(i, 0);                /* 全0检查 */

            if (res)
            {
                blktbl[i] = 1;                           /* 标记坏块 */
            }
            else
            {
                goodblock++; 
            }
        }
    }

    nand_erasechip();                                    /* 全片擦除 */

    for (i = 0; i < nand_dev.block_totalnum; i++)        /* 第三阶段检查,标记坏块 */
    { 
        if (blktbl[i])
        {
            ftl_badblock_mark(i);                        /* 是坏块 */
        }
    }

    return goodblock;                                    /* 返回好块的数量 */
}

/**
 * @brief       格式化NAND 重建LUT表
 * @param       无
 * @retval      0,成功;
                其他,失败
 */
uint8_t ftl_format(void)
{
    uint8_t temp;
    uint32_t i, n;
    uint32_t goodblock = 0;
    nand_dev.good_blocknum = 0;

#if FTL_USE_BAD_BLOCK_SEARCH == 1                       /* 使用擦-写-读的方式,检测坏块 */
    nand_dev.good_blocknum = ftl_search_badblock();     /* 搜寻坏块.耗时很久 */
#else                                                   /* 直接使用NAND FLASH的出厂坏块标志(其他块,默认是好块) */

    for (i = 0; i < nand_dev.block_totalnum; i++)
    {
        temp = ftl_check_badblock(i);                /* 检查一个块是否为坏块 */

        if (temp == 0)                               /* 好块 */
        {
            temp = nand_eraseblock(i);

            if (temp)                                /* 擦除失败,认为坏块 */
            {
                printf("Bad block:%d\r\n", i);
                ftl_badblock_mark(i);                /* 标记是坏块 */
            }
            else
            {
                nand_dev.good_blocknum++;            /* 好块数量加一 */
            }
        }
    }
#endif
    printf("good_blocknum:%d\r\n", nand_dev.good_blocknum);

    if (nand_dev.good_blocknum < 100)
    {
        return 1;                                    /* 如果好块的数量少于100,则NAND Flash报废 */
    }

    goodblock = (nand_dev.good_blocknum * 93) / 100; /* 93%的好块用于存储数据 */

    n = 0;

    for (i = 0; i < nand_dev.block_totalnum; i++)    /* 在好块中标记上逻辑块信息 */
    {
        temp = ftl_check_badblock(i);                /* 检查一个块是否为坏块 */

        if (temp == 0)                               /* 好块 */
        { 
            nand_writespare(i * nand_dev.block_pagenum, 2, (uint8_t*)&n, 2);  /* 写入逻辑块编号 */
            n++;                                     /* 逻辑块编号加1 */

            if (n == goodblock) break;               /* 全部标记完了 */
        }
    }
    
    if (ftl_create_lut(1))
    {
        return 2;                                    /* 重建LUT表失败 */
    }

    return 0;
}

ftl.h
main.h

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./USMART/usmart.h"
#include "./BSP/SDRAM/sdram.h"
#include "./MALLOC/malloc.h"
#include "./BSP/NAND/ftl.h"
#include "./BSP/NAND/nand.h"
#include "./BSP/NAND/nandtester.h"

int main(void)
{
    uint8_t key, t = 0;
    uint16_t i;
    uint8_t *buf;
    uint8_t *backbuf;
    uint32_t eccval = 0x3F;

    HAL_Init();                                  /* 初始化HAL库 */
    sys_stm32_clock_init(360, 25, 2, 8);         /* 设置时钟,180Mhz */
    delay_init(180);                             /* 延时初始化 */
    usart_init(115200);                          /* 串口初始化为115200 */
    usmart_dev.init(90);                         /* 初始化USMART */
    led_init();                                  /* 初始化LED */
    key_init();                                  /* 初始化按键 */
    sdram_init();                                /* 初始化SDRAM */
    lcd_init();                                  /* 初始化LCD */

    my_mem_init(SRAMIN);                         /* 初始化内部内存池 */
    my_mem_init(SRAMEX);                         /* 初始化外部内存池 */
    my_mem_init(SRAMCCM);                        /* 初始化CCM内存池 */

    lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30,  70, 200, 16, 16, "NAND TEST", RED);
    lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "KEY0:Read Sector 2", RED);
    lcd_show_string(30, 130, 200, 16, 16, "KEY1:Write Sector 2", RED);
    lcd_show_string(30, 150, 200, 16, 16, "KEY2:Recover Sector 2", RED);

    while (ftl_init())      /* 检测NAND FLASH,并初始化FTL */
    {
        lcd_show_string(30, 130, 200, 16, 16, "NAND Error!", RED);
        delay_ms(500);
        lcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED);
        delay_ms(500);
        LED0_TOGGLE();                                                   /* 红灯闪烁 */
    }
    
    backbuf = mymalloc(SRAMIN, NAND_ECC_SECTOR_SIZE);   /* 申请一个扇区的缓存 */
    buf = mymalloc(SRAMIN, NAND_ECC_SECTOR_SIZE);       /* 申请一个扇区的缓存 */
    sprintf((char *)buf, "NAND Size:%dMB", (nand_dev.block_totalnum / 1024) * (nand_dev.page_mainsize / 1024) * nand_dev.block_pagenum);
    lcd_show_string(30, 190, 200, 16, 16, (char *)buf, BLUE);            /* 显示NAND容量 */
    ftl_read_sectors(backbuf, 2, NAND_ECC_SECTOR_SIZE, 1);               /* 预先读取扇区2到备份区域,防止乱写导致文件系统损坏. */

    while (1)
    {
        key = key_scan(0);

        switch (key)
        {
            case KEY0_PRES:                                              /* KEY0按下,读取sector */
                key = ftl_read_sectors(buf, 2, NAND_ECC_SECTOR_SIZE, 1); /* 读取扇区 */

                if (key == 0)   /* 读取成功 */
                {
                    lcd_show_string(30, 210, 200, 16, 16, "USART1 Sending Data...  ", BLUE);
                    printf("Sector 2 data is:\r\n");

                    for (i = 0; i < NAND_ECC_SECTOR_SIZE; i++)
                    {
                        printf("%x ",buf[i]);                            /* 输出数据 */
                    }

                    printf("\r\ndata end.\r\n");
                    lcd_show_string(30, 210, 200, 16, 16, "USART1 Send Data Over!  ", BLUE); 
                }
                break;

            case KEY1_PRES:     /* KEY1按下,写入sector */
                for (i = 0; i < NAND_ECC_SECTOR_SIZE; i++)
                {
                    buf[i] = i + t;     /* 填充数据(随机的,根据t的值来确定) */
                }

                lcd_show_string(30, 210, 210, 16, 16, "Writing data to sector..", BLUE);
                key = ftl_write_sectors(buf, 2, NAND_ECC_SECTOR_SIZE, 1);                    /* 写入扇区 */

                if (key == 0)
                {
                    lcd_show_string(30, 210, 200, 16, 16, "Write data successed    ", BLUE); /* 写入成功 */
                }
                else 
                {
                    lcd_show_string(30, 210, 200, 16, 16, "Write data failed       ", BLUE); /* 写入失败 */
                }
                break;

            case KEY2_PRES:                                                                  /* KEY2按下,恢复sector的数据 */
                lcd_show_string(30, 210, 210, 16, 16, "Recovering data...      ", BLUE);
                key = ftl_write_sectors(backbuf, 2, NAND_ECC_SECTOR_SIZE, 1);                /* 写入扇区 */

                if (key == 0)
                {
                    lcd_show_string(30, 210, 200, 16, 16, "Recovering data OK      ", BLUE); /* 恢复成功 */
                }
                else 
                {
                    lcd_show_string(30, 210, 200, 16, 16, "Recovering data failed  ", BLUE); /* 恢复失败 */
                }
                break;
        }

        t++;
        delay_ms(10);

        if (t == 20)
        {
            LED0_TOGGLE();               /* 红灯闪烁 */
            t = 0;
        }
    }
}

在这里插入图片描述

五、总结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 15
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咖喱年糕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值