龙空技术网

正点原子STM32F4/F7水星开发板资料连载第四十四章NAND FLASH实验

正点原子日常 84

前言:

目前朋友们对“flash均衡擦写算法”大体比较珍视,你们都需要学习一些“flash均衡擦写算法”的相关资讯。那么小编在网络上汇集了一些对于“flash均衡擦写算法””的相关资讯,希望同学们能喜欢,兄弟们一起来了解一下吧!

1)实验平台:正点原子水星 STM32F4/F7 开发板

2)摘自《STM32F7 开发指南(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子

3)全套实验源码+手册+视频下载地址:

第四十四章 NAND FLASH 实验

水星 STM32F767 核心板上面,板载了一颗 512MB 的 NAND FLASH 芯片,型号为:

MT29F4G08,我们可以用它来存储数据,相对于 SPI FLASH(W25Q256)和 SD 卡等存储设备,

NAND FLASH 采用 8 位并口访问,具有访问速度快的优势。

本章,我们将使用 STM32F767 来驱动 MT29F4G08,并结合一个简单的坏块管理与磨损均

衡算法,实现对 MT29F4G08 的读写控制。本章分为如下几个部分:

44.1 NAND FLASH 简介

44.2 硬件设计

44.3 软件设计

44.4 下载验证

44.1.1 NAND FLASH 简介

NAND FLASH 的概念是由东芝公司在 1989 年率先提出,它内部采用非线性宏单元模式,

为固态大容量内存的实现提供了廉价有效的解决方案。NAND FLASH 存储器具有容量较大,

改写速度快等优点,适用于大量数据的存储,在业界得到了广泛应用,如:SD 卡、TF 卡、U

盘等,一般都是采用 NAND FLASH 作为存储的。关于 NAND FLASH 的基础知识,请大家自

行百度学习。接下来,我们介绍 NAND FLASH 的一些重要知识。

(1)NAND FLASH 信号线

NAND FLASH 的信号线如表 44.1.1.1 所示:

表 44.1.1.1 NAND FLASH 信号线

因为 NAND FLASH 地址/数据是共用数据线的,所以必须有 CLE/ALE 信号,告诉 NAND

FLASH,发送的数据是命令还是地址。

(2)存储单元

NAND FLASH 存储单元介绍,我们以水星 STM32F767 开发板所使用的 MT29F4G08(x8,

8 位数据)为例进行介绍,MT29F4G08 的存储单元组织结构如图 44.1.1.1 所示:

图 44.1.1.1 MT29F4G08 存储单元组织结构图

由图可知:MT29F4G08 由 2 个 plane 组成,每个 plane 有 2048 个 block,每个 block 由 64

个 page 组成,每个 page 有 2K+64 字节(2112 字节)的存储容量。所以,MT29F4G08 的总容

量为:2*2048*64*(2K+64)= 553648128 字节(512MB)。其中,plane、block、page 等的个数

根据 NAND FLASH 型号的不同,会有所区别,大家注意查看对应 NAND FLASH 芯片的数据

手册。

NAND FLASH 的最小擦除单位是 block,对 MT29F4G08 来说,是(128+4)K 字节,NAND

FLASH 的写操作具有只可以写 0,不能写 1 的特性,所以,在写数据的时候,必须先擦除 block

(擦除后,block 数据全部为 1),才可以写入。

NAND FLASH 的 page 由 2 部分组成:数据存储区(data area)和备用区域(spare area),

对 MT29F4G08 来说,数据存储区大小为 2K 字节,备用区域大小为 64 字节。我们存储的有效

数据,一般都是存储在数据存储区(data area)。备用区域(spare area),一般用来存放 ECC(Error

Checking and Correcting)校验值,在本章中,我们将利用这个区域,来实现 NAND FLASH 坏

块管理和磨损均衡。

NAND FLASH 的地址分为三类:块地址(Block Address)、页地址(Page Address)和列地

址(Column Address)。以 MT29F4G08 为例,这三个地址,通过 5 个周期发送,如表 44.1.1.2

所示:

表 44.1.1.2 MT29F4G08 寻址说明

表中,CA0~CA11 为列地址(Column Address),用于在一个 Page 内部寻址,MT29F4G08

的一个 Page 大小为 2112 字节,需要 12 个地址线寻址;PA0~PA5 为页地址(Page Address),用

于在一个 Block 内部寻址,MT29F4G08 一个 Block 大小为 64 个 Page,需要 6 个地址线寻址;

BA6~BA17 为块地址(Block Address),用于块寻址,MT29F4G08 总共有 4096 个 Block,需要

12 根地址线寻址。

整个寻址过程,分 5 次发送(5 个周期),首先发送列地址,在发送页地址和块地址。这里提醒一下:块地址和页地址,其实是可以写在一起的,由一个参数传递即可,所以表中的 BA

并不是由 BA0 开始的,大家可以理解为这个地址(PA+BA)为整个 NAND FLASH 的 Page 地

址。在完成寻址以后,数据线 I/O0~ I/O7 来传输数据了。

(3)控制命令

NAND FLASH 的驱动需要用到一系列命令,这里我们列出常用的一些命令,给大家做一

个简单介绍,方便大家了解 NAND FLASH 的操作,如表 44.1.1.3 所示:

表 44.1.1.3 NADN FLASH 操作常用命令

表 44.1.1.3 中,我们需要注意两点:1,有的指令一个周期完成传送,有的指令需要分两次

传送(2 个周期);2,指令名称,不同厂家的数据手册里面,标注可能不一样,但是其指令值

(HEX 值)一般都是一样的。

上表中,前四条命令相对比较简单,这里我们主要介绍后面五条指令。

1,READ PAGE

该指令用于读取 NAND 的一个 Page(包括 spare 区数据,但不能跨页读),该指令时序如

图 46.1.1.2 所示:

图 44.1.1.2 READ PAGE 指令时序图

由图可知,READ PAGE 的命令分两次发送,首先发送 00H 命令,然后发送 5 次地址

(Block&Page&Column 地址),指定读取的地址,随后发送 30H 命令,在等待 RDY 后,即可

读取 PAGE 里面的数据。注意:不能跨页读,所以最多一次读取一个 PAGE 的数据(包括 spare

区)。

2,WRITE PAGE

该指令用于写一个Page的数据(包括spare区数据,但不能跨页写),该指令时序如图46.1.1.3

所示:

图 44.1.1.3 READ PAGE 指令时序图

由图可知,WRITE PAGE 的命令分两次发送,首先发送 80H 命令,然后发送 5 次地址

(Block&Page&Column 地址),指定写入的地址,在地址写入完成后,等待 tADL 时间后,开

始发送需要写入的数据,在数据发送完毕后,发送 10H 命令,最后发送 READ STATUS 命令,

查询 NAND FLASH 状态,等待状态为 READY 后,完成一次 PAGE 写入操作。

3,ERASE BLOCK

该指令用于擦除 NAND 的一个 Block(NAND 的最小擦除单位),该指令时序如图 44.1.1.4

所示:

图 44.1.1.4 ERASE BLOCK 指令时序图

由图可知,ERASE BLOCK 的命令分两次发送,首先发送 60H 命令,然后发送 3 次地址

(BLOCK 地址),指定要擦除的 BLOCK 地址,随后发送 D0H 命令,在等待 RDY 成功后,完

成一个 BLOCK 的擦除。

4,READ FOR INTERNAL DATA MOVE

该指令用于在 NAND 内部进行数据移动时(页对页),指定需要读取的 PAGE 地址,如有

必要,可以读取出 PAGE 里面的数据。该指令时序如图 44.1.1.5 所示:

图 44.1.1.5 READ FOR INTERNAL DATA MOVE 指令时序图

由图可知,READ FOR INTERNAL DATA MOVE 的命令分两次发送,首先发送 00H 命令,

然后发送 5 次地址(Block&Page&Column 地址),指定读取的地址,随后发送 35H 命令,在等

待 RDY 后,可以读取对应 PAGE 里面的数据。在内部数据移动过程中,我们仅用该指令指定

需要拷贝的 PAGE 地址(源地址),并不需要读取其数据,所以最后的 Dout 过程,一般都可以

省略。

5,PROGRAM FOR INTERNAL DATA MOVE

该指令用于在 NAND 内部进行数据移动时(页对页),指定需要写入的 PAGE 地址(目标

地址),如有必要,在拷贝过程中,可以写入新的数据,该指令时序如图 46.1.1.6 所示:

图 44.1.1.6 PROGRAM FOR INTERNAL DATA MOVE 指令时序图

如图 44.1.1.6 所示,该指令,首先发送 85H 命令,然后发送 5 次地址(Block&Page&Column

地址),指定写入的页地址(目标地址),源地址则由 READ FOR INTERNAL DATA MOVE 指

令指定。接下来分两种情况:1,要写入新的数据(覆盖源 PAGE 的内容);2,无需写入新的

数据;

对于第 1 种情况,在等待 tWHR(或 tADL)之后,开始写入新的数据,数据在页内的起始

地址,由 C1&C2 指定,写入完成后,发送 10H 命令,开始进行页拷贝,在等待 RDY 后,完成

一次页对页的数据拷贝(带新数据写入)。

对于第 2 种情况,在发送完 5 次地址后,无需发送新数据,直接发送 10H 命令,开始进行

页拷贝,在等待 RDY 后,完成一次页对页的数据拷贝(不带数据写入)。

注意:页对页拷贝,仅支持同一个 plane 里面互相拷贝(源地址和目标地址,必须在同一

个 plane 里面),如果不是同一个 plane 里面的页,则不可以执行页对页拷贝。

NAND FLASH 其他命令的介绍,请大家参考 MT29F4G08 的数据手册。

(4)ECC 校验

ECC,英文全称为:Error checking and Correction,是一种对传输数据的错误检测和修正的

算法。NAND FLASH 存储单元是串行组织的,当读取一个单元的时候,读出放大器所检测到

信号强度会被这种串行存储结构削弱,这就降低了所读信号的准确性,导致读数出错(一般只

有 1 个 bit 出错)。ECC 就可以检测这种错误,并修正错误的数据位,因此,ECC 在 NAND FLASH

驱动里面,被广泛使用。

ECC 有三种常用的算法:汉明码(Hamming Code)、RS 码(Reed Solomon Code)和 BCH 码。

STM32 的 FMC 模块就支持硬件 ECC 计算,使用的就是汉明码,接下来,我们就给大家简单介

绍一下汉明码的编码和使用。

1,汉明码编码

汉明码的编码计算比较简单,通过计算块上数据包得到 2 个 ECC 值(ECCo 和 ECCe)。为

计算 ECC 值,数据包中的比特数据要先进行分割,如 1/2 组、1/4 组、1/8 组等,直到其精度达

到单个比特为止。我们以 8 bit 即 1 字节的数据包为例进行说明,如表 44.1.1.4 所示:

表 44.1.1.4 8bit 数据包校验的数据分割

8 位数据可以按:1/2、1/4 和 1/8 进行分割(1/8 分割时,达到单个比特精度)。简单说一下

上表的看法,以 1/2 分割偶校验为例,1/2 分割时,每 4 个 bit 组成一个新 bit,新的 bit0 等于原

来的 bit0~3,新的 bit1 等于原来的 bit4~7,而我们只要偶数位的数据,也就是新 bit0 的数据,

实际上就是原来的 bit0~3 的数据,这样就获取了 1/2 偶校验的数据。其他分割以此类推。

表 46.1.1.3 中,数据包上方的三行数据经计算后,得到偶校验值(ECCe),数据包下方的

三行数据经计算后,得到奇校验值(ECCo)。1/2 校验值经“异或”操作构成 ECC 校验的最高

有效位,1/4 校验值构成 ECC 校验的次高有效位,最低有效位由具体到比特的校验值填补。ECC

校验值(ECCo 和 ECCe)的计算过程,如图 46.1.1.7 所示:

图 46.1.1.7 计算奇偶 ECC 值

即偶校验值 ECCe 为“101”,奇校验值 ECCo 为“010”。图 1 所示为只有 1 字节数据的数

据包,更大的数据包需要更多的 ECC 值。事实上,每 n bit 的 ECC 数值可满足 2^n bit 数据包的

校验要求。不过,汉明码算法要求一对 ECC 数据(奇+偶),所以总共要求 2n bit 的 ECC 校验

数据来处理 2^n bit 的数据包。

得到 ECC 值后,我们需要将原数据包和 ECC 数值都写入 NAND 里面。当原数据包从 NAND

中读取时,ECC 值将重新计算,如果新计算的 ECC 不同于先前编入 NAND 器件的 ECC,那么

表明数据在读写过程中发生了错误。例如,原始数据 01010001 中有 1 个单一的比特出现错误,

出错后的数据是 01010101。此时,重新计算 ECC 的过程如图 46.1.1.8 所示:

图 46.1.1.8 出错时计算的奇偶 ECC 值

可以看到,此时的 nECCo 和 nECCe 都为 000。此时,我们把所有 4 个 ECC 数值进行按位

“异或”,就可以判断是否出现了 1 个单一比特的错误或者是多比特的错误。如果计算结果为全

“0”,说明数据在读写过程中未发生变化。如果计算的结果为全“1”,表明发生了 1 bit 错误,

其他情况,则说明有至少 2bit 数据出现了错误。不过,汉明码编码算法只能够保证更正单一比

特的错误,如果两个或是更多的比特出错,汉明码就无能为力了(可以检测出错误,但无法修

正)。不过,一般情况 SLC NAND 器件(STM32 仅支持 SLC NAND)出现 2bit 及以上的错误

非常罕见,所以,我们使用的汉明码基本上够用。

对 4 个 ECC 进行异或的计算方法如下:

ECCe^ECCo^nECCe^nECCo=101^010^000^000=111

这样,经过上式计算,4 个 ECC 的“异或”结果为全 1,表示有 1 个 bit 出错了。对于这种

1 bit 错误的情况,出错的地址可通过将原有 ECCo 值和新 ECCo 值(nECCo)进行按位“异或”

来得到,计算方法如下:

ECCo^nECCo=010^000=010

计算结果为 010(2),表明原数据第 2 bit 位出现了问题。然后,对出错后的数据的 bit2 进

行取反(该位与 1“异或”即可),就可以得到正确的数据:01010101^00000100=01010001。

一个 8 位数据,需要 6 位 ECC 码,看起来效率不高,但是随着数据的增多,汉明码编码效

率将会越来越高。比如,我们一般以 512 字节为单位来计算 ECC 值,只需要 24bit 的 ECC 码即

可表示(2^12=4096bit=512 字节,12*2=24bit)。汉明码编码算法的原理,我们就给大家介绍到

这里。

2,STM32 硬件 ECC

STM32 的 FMC 支持 NAND FLASH 硬件 ECC 计算,采用的就是汉明码计算方法。可以实

现 1bit 错误的修正和 2bit 以上错误的检测,支持页大小按 256、512、1024、2048、4096 和 8192字节为单位进行 ECC 计算。

当 FMC 的硬件 ECC 功能开启后,FMC 模块根据用户设置的参数(计算页大小、数据位宽

等)对 NAND FLASH 数据线上传递的(读/写)数据进行 ECC 计算,数据传输结束后,ECC

计算结果自动存放在 FMC_ECCR 寄存器中。不过 STM32 的硬件 ECC 只负责计算 ECC 值,并

不对数据进行修复。错误检测和数据修复,需要用户自己实现。另外,STM32 的硬件 ECC 支

持存储区域 3,其他存储区域不支持!!

STM32 硬件 ECC 计算结果读取过程(以 512 字节页大小为例):

1, 设置 FMC_PCR 的 ECCEN 位为 1,使能 ECC 计算

2, 写入/读取 512 字节数据

3, 等待 FMC_SR 的 FEMPT 位为 1(等待 FIFO 空)

4, 读取 FMC_ECCR,得到 ECC 值

5, 设置 FMC_PCR 的 ECCEN 位为 0,关闭 ECC,以便下一次重新计算

重复以上步骤,就可以在不同时刻进行读/写数据的 ECC 计算。在实际使用的时候,我们

在写入/读取数据时,都要开启 STM32 的硬件 ECC 计算。写入的时候,将 STM32 硬件 ECC 计

算出来的 ECC 值写入 NAND FLASH 数据所存 Page 的 spare 区。在读取数据的时候,STM32

硬件 ECC 会重新计算一个 ECC 值(ecccl),而从 spare 区对应位置,又可以读取之前写入的 ECC

值(eccrd),当这两个 ECC 值不相等的时候,说明读数有问题,需要进行 ECC 校验,ECC 检

查和修正代码如下:

//获取 ECC 的奇数位/偶数位

//oe:0,偶数位

// 1,奇数位

//eccval:输入的 ecc 值

//返回值:计算后的 ecc 值(最多 16 位)

u16 NAND_ECC_Get_OE(u8 oe,u32 eccval)

{

u8 i;

u16 ecctemp=0;

for(i=0;i<24;i++)

{

if((i%2)==oe) if((eccval>>i)&0X01)ecctemp+=1<<(i>>1);

}

return ecctemp;

}

//ECC 校正函数

//eccrd:读取出来,原来保存的 ECC 值

//ecccl:读取数据时,硬件计算的 ECC 只

//返回值:0,错误已修正

// 其他,ECC 错误(有大于 2 个 bit 的错误,无法恢复)

u8 NAND_ECC_Correction(u8* data_buf,u32 eccrd,u32 ecccl)

{

u16 eccrdo,eccrde,eccclo,ecccle;

u16 eccchk=0;

u16 errorpos=0;

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

if(eccchk==0XFFF)

//全 1,说明只有 1bit ECC 错误

{

errorpos=eccrdo^eccclo;

//计算出错 bit 位置

bytepos=errorpos/8;

//计算字节位置

data_buf[bytepos]^=1<<(errorpos%8); //对出错位进行取反,修正错误

}else return 1;

//不是全 1,说明至少有 2bit ECC 错误,无法修复

return 0;

}

经过以上代码处理,我们就可以利用 STM32 的硬件 ECC,修正 1bit 错误,并报告 2bit 及

以上错误。

44.1.2 FTL 简介

因为 NAND FLASH 在使用过程中可能会产生坏块,且每个 BLOCK 的擦除次数是有限制

的,超过规定次数后,BLOCK 将无法再擦除(即产生坏块),因此,我们需要这样一段程序,

它可以实现:1,坏块管理;2,磨损均衡;从而使应用程序可以很方便的访问 NAND FLASH

(无需关系坏块问题),且最大限度的延长 NAND FLASH 的寿命。

这里给大家介绍 FTL,FTL 是 Flash Translation Layer 的简写,即闪存转换层,它是一个

NAND 闪存芯片与基础文件系统之间的一个转换层,它自带了坏块管理和磨损均衡算法,使得

操作系统和文件系统能够像访问硬盘一样访问 NAND 闪存设备,而无需关心坏块和磨损均衡问

题。

本章,我们将给大家介绍一个比较简单的 FTL 层算法,它可以支持坏块管理和磨损均衡,

提供支持文件系统(如:FATFS)的访问接口,通过这个 FTL,我们可以很容易的实现 NAND

FLASH 的文件系统访问。

要做好 NAND FLASH 的坏块管理,我们有以下几点需要实现:

1, 如何识别坏块,标记坏块;

2, 转换表

3, 保留区

1,如何识别坏块,标记坏块

经过前面的介绍,我知道 NAND 在使用过程中,会产生坏块,而坏块我们是不能再用来存

储数据的,必须对坏块进行识别和标记,并保存这些标记。

NAND FLASH 的坏块识别有几种方式:1,NAND 厂家出厂的时候,会在每个 Block 的第

一个 page 和第二个 page 的 spare 区的第一个字节写入非 0XFF 的值来表示,我们可以通过这个

判断该块是否为坏块;2,通过给每个 Block 写入数值(0XFF/0X00),然后读取出来,判断写

入的数据和读取的数据是否完全一样,来识别坏块;3,通过读取数据时,校验 ECC 错误,来

识别坏块。

NAND FLASH 的坏块标记:坏块标记,我们使用每个 Block 的第一个 page 和第二个 page

(第二个 page 是备份用的)spare 区的第一个字节来标记,当这个字节的值为 0XFF 时,表示该块为好块,当这个字节的值不等于 0XFF 时,表示该块为坏块。以 MT29F4G08 为例,坏块

表示方法,如表 44.1.2.1 所示:

表 44.1.2.1 NAND FLASH 坏块标记说明表

上图中,假设某个 Block 为坏块,那么它的第一个 page 和第二个 page 的 spare 区第一个字

节,就不是 0XFF 了(我们改为 0XAA),以表示其是一个坏块。如果是好块,这两个字节,必

须都是 0XFF,只要任何一个不是 0XFF,则表示该块是一个坏块。

这样,我们只需要判断每个 Block 的第一和第二个 page 的 spare 区的第一个字节,就可以

判断是否为坏块。达到了标记和保存坏块标记的目的。

2,转换表

文件系统访问文件的时候,使用的是逻辑地址,是按顺序编号的,它不考虑坏块情况。而

NAND FLASH 存储地址,我们称为物理地址,是有可能存在坏块的。所以,这两个地址之间,

必须有一个映射表,将逻辑地址转换为物理地址,且不能指向坏块的物理地址,这个映射表我

们称之为逻辑地址-物理地址转换表,简称转换表,如图 44.1.2.1 所示:

图 44.1.2.1 逻辑-物理地址转换表

图 44.1.2.1 表示某个时刻,逻辑地址与物理地址的对应关系。由图可知,逻辑地址(0~n)

到物理地址的映射,映射关系不是一一对应的,而是无序的,这个映射关系是不固定的,随时

可能会变化,逻辑地址到物理地址,通过映射表进行映射,所以映射表也是随时需要更新的。

图中,我们假定 NAND 的第 5 个 Block 是坏块,那么映射表一定不能将这个块地址映射给

逻辑地址,所以,必须对这个块进行坏块标记,不再作为正常块使用,坏块标记请参考前面的介绍。另外,当产生了一个坏块的时候,我们必须从保留区(下文介绍)提取一个未用过的块

(Block),来替代这个坏块,以确保所有逻辑地址,都有正常的物理地址可用。

逻辑地址到物理地址的映射关系,我们采用一个数组来存储,这个数组即映射表(简称:

lut 表),同时,这个映射表必须存储到 NAND FLASH 里面,以便上电后重建。这里,我们也

是利用每个 Block 的第一个 page 的 spare 区来存储映射表,另外,还需要标记这个 Block 是否

被使用了,所以,Block 第一个 page 的 spare 区规划,如表 44.1.2.2 所示:

表 44.1.2.2 每个 Block 第一个 page 的 spare 区前 4 个字节规划表

如表 44.1.2.2 所示,每个 Block 第一个 page 的 spare 区第一个字节用来表示该块是否为坏

块(前面已介绍);第二个字节用来表示该块是否被占用(0XFF,表示未占用;0XCC,表示已

被占用);第三和第四个字节,用来存储该块所映射到的逻辑地址,如果为 0XFFFF,则表示该

块还未被分配逻辑地址,其他值,则表示该块对应的逻辑地址,MT29F4G08 有 4096 个 Block,

所以这两个字节表示的有效逻辑地址范围,就是 0~4095。因此,我们要想判断一个 Block 是否

是一个未被使用的好块,只需读取这个 Block 第一个 page 的 spare 区前四个字节,如果为

0XFFFFFFFF 则说明是一个未被使用的好块。

上电的时候,重建映射表(lut 表)的过程就是读取 NAND FLASH 每个 Block 第一个 page

的 spare 区前 4 个字节,当这个块是好块(第一个字节为 0XFF),且第三和第四字节组成的 u16

类型数据(逻辑地址,记为:LBNnum),小于 NAND FLASH 的总块数,则这个 Block 地址(物

理地址,记为:M)就是映射表里面第 LBNnum 个元素所对应的地址,即:lut[LBNnum]=M。

3,保留区

保留区有两个作用:1,产生坏块的时候,用来替代坏块;2,在复写数据的时候,用来替

代被复写的块,以提高写入速度,并实现磨损均衡处理;

第一个作用,如图 46.1.2.1所示,当产生坏块后(第 5个块),使用一个保留区里面的块(n+2),

来替代这个坏块,从而保证每个逻辑地址,都有对应的物理地址(好块地址)。

第二个作用,当文件系统要往某个已经被写过数据的块里面写入新数据的时候,由于

NAND 的特性,必须是要先擦除,才能写入新数据。一般的方法:先将整个块数据读出来,然

后改写需要写入新数据的部分,然后擦除这个块,然后重新写入这个块,这个过程非常耗时,

且需要很大的内存(MT29F4G08 一个 Block 大小为 128K 字节),所以不太实用。

比较好的办法,是利用 NAND FLASH 的页拷贝功能,它可以将 NAND FLASH 内部某个

Block 的数据,以页为单位,拷贝到另外一个空闲的 Block 里面,而且可以写入新的数据(参

见 46.1.1 节的指令介绍),利用这个功能,我们无需读出整个 Block 的数据,只需要在页拷贝过

程中,在正确的地址写入我们需要写入的新数据即可。这就要求 NAND FLASH 必须有空闲的

块,用作页拷贝的目标地址,保留区里面的块,就可以作为空闲块,给页拷贝使用。而且,为

了保证不频繁擦除一个块(提高寿命),我们在保留区里面应预留足够的空闲块,用来均分擦除

次数,从而实现简单的磨损均衡处理。

这样,FTL 层的坏块管理和磨损均衡原理,就给大家介绍完了。我们根据这个原理,去设

计相应的代码,就可以实现 FTL 层的功能,从而更好的使用 NAND FLASH。

前面提到,我们需要用到 ECC 校验,来确保数据的正确性,一般的软件 ECC 校验都是由

FTL 层实现,我们为了简化代码,并利用 STM32 的硬件 ECC 计算,加快 ECC 计算的速度,我

们在 FTL 层并不做 ECC 计算和校验,而是放到 NAND FLASH 的底层驱动去实现。在 46.1.1

节最后,我们给大家介绍了 ECC 原理和纠错方法,每 512 个字节的数据,会生成 3 个字节的

ECC 值,而这个 ECC 值是必须存储在 NAND FLASH 里面,同样我们将 ECC 值存储在每个 page

的 spare 区,而且,为了方便读写,我们用 4 个字节来存储这 3 个字节的 ECC 值,ECC 存储关

系如表 44.1.2.3 所示:

表 44.1.2.3 每个 page 的 spare 区 ECC 存储关系表

如上表所示,每个 page 的一个数据区有 2048 个字节,而每 512 字节数据生成一个 ECC 值,

用 4 个字节存储,这样,每个 page 需要 16 个字节用于存储 ECC 值。表中的 ECCx1~ECCx4,

(x=0~63),就是存储的 ECC 值,从每个 page 的 spare 区第 16 个字节(0X10)开始存储 ECC

值,总共占用 16 字节,对应关系为:ECCx1→数据区 0~511 字节;ECCx2→数据区 512~1023

字节;ECCx3→数据区 1024~1535 字节;ECCx4→数据区 1536~2047 字节;

在 page 写入数据的时候,我们将 STM32 硬件计算出的 ECC 值写入 spare 区对应的地址,

当读取 page 数据的时候,STM32 硬件 ECC 会计算出一个新的 ECC 值,同时,可以在该 page

的 spare 区读取之前保存的 ECC 值,比较这两个 ECC 值,就可以判断数据是否有误,以及进行

数据修复(1bit)。注意:我们对 ECC 处理是以 512 字节为单位的,如果写入/读取的数据不是

512 的整数倍,则不会进行 ECC 处理。

44.1.3 FMC NAND FLASH 接口简介

在第十八和十九章,我们对 STM32F767 的 FMC 接口进行了简介,并利用 FMC 接口,实

现了对 MCU 屏和 SDRAM 的驱动。本章,我们将介绍如何利用 FMC 接口,驱动 NAND FLASH。

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

⚫ 两个 NAND FLASH 存储区域,可独立配置

⚫ 支持 8 位和 16 位 NAND FLASH

⚫ 支持 16 位 PC 卡兼容设备

⚫ 支持硬件 ECC 计算(汉明码)

⚫ 支持 NAND FLASH 预等待功能

通过 46.1.1 的介绍,我们对 NAND FLASH 已经有了一个比较深入的了解,包括接线、控

制命令和读写流程等,接下来,我们介绍一些配置 FMC NAND FLASH 控制器需要用到的几个

寄存器。

首先,我们介绍 NAND FLASH 的控制寄存器:FMC_PCR,该寄存器各位描述如图 44.1.3.1

所示:

图 44.1.3.1 FMC_PCR 寄存器各位描述

该寄存器只有部分位有效,且都需要进行配置:

PWAITEN:该位用于设置等待特性:0,禁止;1,使能。这里我们设置为 0,禁止使用控

制器自带的等待特性,因为如果使能的话,将导致 RGB 屏抖动(STM32 硬件 bug)。

PBKEN:该位用于使能存储区域:0,禁止;1,使能。我们要正常使用某个存储区域,必

须设置该位为 1,所以,这个位要设置为 1。

PTYP:该位用于设置存储器类型:

0,保留;

1,NAND FLASH。我们用来驱动 NAND FLASH,

所以该位设置为 1。

PWID:这两个位,用于设置数据总线宽度:00,8 位宽度;01,16 位宽度。我们使用的

MT29F4G08 为 8 位宽度,所以这里应该设置为:00。

ECCEN:该位用于使能 STM32 的硬件 ECC 计算逻辑:0,禁止/复位 ECC;1,使能 ECC

计算;每次读写数据前,应该设置该位为 1,在数据读写完毕,读取完 ECC 值之后,设置该位

为 0,复位 ECC,以便下一次 ECC 计算。

TCLR:这四个位用于设置 CLE 到 RE 的延迟:0000~1111,表示 1~16 个 HCLK 周期。对

应 NAND FLASH 数据手册的 tCLR 时间参数,这里设置的 t_clr=(TCLR+SET+2)*THCLK。TCLR

就是本寄存器的设置,SET 对应 MEMSET 的值(我只用到 MEMSET),THCLK 对应 HCLK 的

周期。MT29F4G08 的 tCLR 时间最少为 10ns,以 216M 主频计算,一个 HCLK=4.63ns(下同),

我们设置 TCLR=5,则 t_clr 至少为 6 个 HCLK 即 27.8ns。

TAR:这四个位用于设置 ALE 到 RE 的延迟:0000~1111,表示 1~16 个 HCLK 周期。对应

NAND FLASH 数据手册的 tAR 时间参数,这里设置的 t_ar=(TAR+SET+2)*THCLK。TAR 就是

本寄存器的设置,SET 对应 MEMSET 的值(我只用到 MEMSET),THCLK 对应 HCLK 的周期。

MT29F4G08 的 tAR 时间最少为 10ns,我们设置 TAR=5,则 t_ar 至少为 6 个 HCLK 即 27.8ns。

ECCCPS:这三个位用于设置 ECC 的页大小:000,256 字节;001,512 字节;010,1024

字节;011,2048 字节;100,4096 字节;101,8192 字节。我们需要以 512 字节为单位进行

ECC 计算,所以 ECCCPS 设置为:001 即可。

接下来,我们介绍 NAND FLASH 的空间时序寄存器:FMC_PMEM,该寄存器各位描述如

图 44.1.3.2 所示:

图 44.1.3.2 FMC_PMEM 寄存器各位描述

该寄存器用于控制 NAND FLASH 的访问时序,非常重要。我们先来了解下 NAND FLASH

控制器的通用存储器访问波形,如图 44.1.3.3 所示:

图 44.1.3.3 NAND FLASH 通用存储器访问波形

由图可知,MEMxSET+MEMxHOLD 控制 NWE/NOE 的高电平时间,MEMxWAIT 控制

NWE/NOE 的低电平时间,MEMxHIZ 控制写入时数据线高阻态时间。接下来我们分别介绍这

几个参数:

MEMSETx:这八个位定义使能命令(NWE/NOE)前,地址建立所需要的 HCLK 时钟周期

数,表示 NWE/NOE 的高电平时间,0000 0000~1111 1111 表示 1~256 个 HCLK 周期。MT29F4G08

的 tREH/tWH 最少为 10ns,我们设置 MEMSETx=1,即 2 个 HCLK 周期,约 9.3ns。另外,

MEMHOLDx,也可以用于控制 NWE/NOE 的高电平时间,在连续访问的时候,MEMHOLDx

和 MEMSETx 共同构成 NWE/NOE 的高电平脉宽时间。

MEMWAITx:这八个位用于设置使能命令(NWE/NOE)所需的最小 HCLK 时钟周期数(使

能 NWAIT 将使这个时间延长),实际上就是 NWE/NOE 的低电平时间,0000 0000~1111 1111

表示 1~256 个 HCLK 周期。MT29F4G08 的 tRP/tWP 最少为 10ns,我们设置 MEMWAITx =3,

即 4 个 HCLK 周期,约 18.5ns。这里需要设置时间比较长一点,否则可能访问不正常。

MEMHOLDx:这八个位用于设置禁止使能命令(NWE/NOE)后,保持地址(和写访问数

据)的 HCLK 时钟周期数,也可以用于设置一个读写周期内的 NWE/NOE 高电平时间,0000

0000~1111 1111 表示 0~255 个 HCLK 周期。我们设置 MEMHOLDx=1,表示 1 个 HCLK 周期,

加上前面的 MEMSETx,所以 NEW/NOE 高电平时间为 3 个 HCLK,即 13.9ns 左右。

MEMHIZx:这八个位定义通用存储空间开始执行写访问之后,数据总线保持高阻态所持

续的 HCLK 时钟周期数。该参数仅对写入事务有效,0000~1111 1111 表示 1~256 个 HCLK 周期。

我们设置 MEMHIZx=1,表示 2 个 HCLK 周期,即 9.3ns 左右。

接下来,我们介绍 NAND FLASH 的 ECC 结果寄存器:FMC_ECCR,该寄存器各位描述如

图 46.1.3.4 所示:

图 44.1.3.4 FMC_ ECCR 寄存器各位描述

该寄存器包含由 ECC 计算逻辑计算所得的结果。根据 ECCPS 位(在 FMC_PCRx 寄存器)

的设置,ECCx 的有效位数也有差异,如表 44.1.3.1 所示:

表 44.1.3.1 ECC 结果相关位

我们以 512 字节为页大小(ECCPS=001),所以 ECCx 的低 24 位有效,用于存储计算所得

的 ECC 值。

接下来,我们介绍 NAND FLASH 的状态和中断寄存器:FMC_SR,该寄存器各位描述如

图 46.1.3.5 所示:

图 44.1.3.5 FMC_SR 寄存器各位描述

该寄存器我们只关心第 6 位:FEMPT 位,该位用于表示 FIFO 的状态。当 FEMPT=0 时,

表示 FIFO 非空,表示还有数据在传输;当 FEMPT=1 时,表示 FIFO 为空,表示数据传输完成。

在计算 ECC 的时候,我们必须等待 FEMPT=1,再去读取 ECCR 寄存器的值,确保数据全部传

输完毕。

至此,FMC NAND FLASH 部分的寄存器就介绍完了,关于 FMC NAND FLASH 控制器的

详细介绍,请大家参考《STM32F7 中文参考手册》第 13.6 节。通过以上三个小节的了解,我

们就可以开始编写 NAND FLASH 的驱动代码了。

水星 STM32F767 核心板板载的 MT29F4G08 芯片挂在 FMC NAND FLASH 的控制器上面

(NCE3),其原理图如图 44.1.3.6 所示:

图 44.1.3.6 MT29F4G08 原理图

从原理图可以看出,MT29F4G08 同 STM32F767 的连接关系:

I/O[0:7]接 FMC_D[0:7]

CLE 接 FMC_A16_CLE

ALE 接 FMC_A17_ALE

WE 接 FMC_NWE

RE 接 FMC_NOE

CE 接 FMC_NCE3

R/B 接 FMC_NWAIT

最后,我们来看看实现对 MT29F4G08 的驱动,需要对 FMC 进行哪些配置。这里我们需要

引入 stm32f7xx_II_fmc.c/stm32f7xx_hal_nand.c 源文件以及对应的头文件。具体步骤如下:

1)使能 FMC 时钟,并配置 FMC 相关的 IO 及其时钟使能。

要使用 FMC,当然首先得开启其时钟。然后需要把 FMC_D0~7、FMC_A16_CLE 和

FMC_A17_ALE 等相关 IO 口,全部配置为复用输出,并使能各 IO 组的时钟。使能 FMC 时钟

和 IO 口初始化方法前面多次讲解,这里就不累赘了。

2)初始化 NAND,设置控制参数(设置 FMC_PCR3)和时间参数(设置 FMC_PMEM3)。

该步骤通过设置寄存器 FMC_PCR3(因为使用的是 FMC_NAND_BANK3 所以对应寄存器

FMC_PCR3)来设置 NAND FLASH 的相关控制参数,比如数据宽度、CLR/AR 延迟、ECC 页

大小等,通过设置寄存器 FMC_PMEM3(因为使用的是 FMC_NAND_BANK3,所以对应寄存

器 FMC_PMEM3)来设置 NAND 的相关时间参数,控制 FMC 访问 NAND FLASH 的时序。HAL

库提供了 NAND FLASH 初始化函数 HAL_NAND_Init:

HAL_StatusTypeDef HAL_NAND_Init(NAND_HandleTypeDef *hnand,

FMC_NAND_PCC_TimingTypeDef *ComSpace_Timing,

FMC_NAND_PCC_TimingTypeDef *AttSpace_Timing);

该函数有三个入口参数,第一个入口参数 hnand 用来设置 NAND FLASH 的控制参数,第

二个入口参数 ComSpace_Timing 用来设置 NAND 通用存储器空间时序,第三个入口参数

AttSpace_Timing 用来设置 NAND 特性存储器空间时序。这里我们着重讲解第一个入口参数

hnand 的定义,该参数为 NAND_HandleTypeDef 结构体类型,该结构体定义为:

typedef struct

{

FMC_NAND_TypeDef

*Instance;

FMC_NAND_InitTypeDef Init;

HAL_LockTypeDef

Lock;

__IO HAL_NAND_StateTypeDef State;

NAND_InfoTypeDef Info;

}NAND_HandleTypeDef;

这里我们主要关注成员变量 Init 的含义,该成员变量是 FMC_NAND_InitTypeDef 结构体类

型,该结构体定义为:

typedef struct

{

uint32_t NandBank;

//BANK 编号

uint32_t Waitfeature;

//等待 特性使能/失能

uint32_t MemoryDataWidth; //数据总线宽度:8 位/16 位

uint32_t EccComputation;

//ECC 计算逻辑使能/失能

uint32_t ECCPageSize;

//ECC 页大小:256/512/1024/2048/4096/8192 字节

uint32_t TCLRSetupTime;

//CLE 到 RE 的延迟

uint32_t TARSetupTime;

//ALE 到 RE 的延迟

}FMC_NAND_InitTypeDef;

这些成员变量设置值对应的是 FMC_ PCR 寄存器相应位,这些成员变量的含义我们都已经

注释了,如有不理解的地方请参考前面 FMC_ PCR 寄存器讲解。

对于 FMC 的时序参数,这里我们就不做 过都讲解,请参考前面讲解。

HAL 库同样为 NAND 初始化提供了 MSP 回调函数 HAL_NAND_MspInit:

void HAL_NAND_MspInit(NAND_HandleTypeDef *hnand);

该函数内部一般用来使能时钟,初始化 IO 口。

3)配置 FMC_PCR3 寄存器,使能存储区域 3。

在 FMC 的配置完成后,最后,设置 FMC_PCR3 寄存器的 PBKEN 位(bit2)为 1,使能存

储区域 3。 如果使用 HAL 库,那么在函数 HAL_NAND_Init 的尾部有使能存储区域的操作,

我们就不需要重复进行此步骤。操作方法为:

__FMC_NAND_ENABLE(hnand->Instance, hnand->Init.NandBank);

通过以上几个步骤,我们就完成了 FMC 的配置,可以访问 MT29F4G08 了,最后,因为我

们使用的是 FMC 的 BANK3,所以 MT29F4G08 的访问地址为 0X80000000,而 NAND FLASH

的命令/地址控制由 CLE/ALE 控制,也就是由 FMC_A17_CLE 和 FMC_A16_ALE 控制,因此,

发送命令和地址的语句为:

*(vu8*)(0X80000000|(1<<17))=CMD;*(vu8*)(0X80000000|(1<<16))=ADDR;

44.2 硬件设计

本章实验功能简介:开机后,先检测 NAND FLASH 并初始化 FTL,如果初始化成功,则

显示提示信息,然后按下 KEY0 按键,可以通过 FTL 读取扇区 2 的数据;按 KEY1 按键,可以

通过 FTL 写入扇区 2 的数据;按WK_UP 按键,则可以恢复扇区 2 的数据(防止损坏文件系统);

DS0 指示程序运行状态。

本实验用到的硬件资源有:

1) 指示灯 DS0

2) KEY0、KEY1 和 WK_UP 按键

3) 串口

4) LCD 模块

5) MT29F4G08

这些我们都已经介绍过(MT29F4G08 与 STM32F767 的各 IO 对应关系,请参考光盘原理

图),接下来我们开始软件设计。

44.3 软件设计

打开本章实验工程可以看到,我们在 HARDWARE 分组之下添加了 nand.c,ftl.c 以及

nandtester.c 三个源文件,同时包含了对应的头文件。

由于代码量比较多,我们这里就不将所有代码贴出来了,仅挑一些重点的函数,给大家介

绍,详细的代码请大家打开本例程源码查看。

这里我们需要说明一下,由于 ST 官方 HAL 库提供的 NAND 相关驱动函数我们在使用过

程发现了很多兼容性问题,并且没有提供坏块管理操作,使用起来并不是非常方便。所以我们

ALIENTEK 团队重写了一套 NAND 操作函数供大家参考。

在 nand.c 里面,我们只介绍:NAND_Init、HAL_NAND_MspInit、NAND_ReadPage 和

NAND_WritePage 等三四个函数。NAND_Init 函数代码如下:

//初始化 NAND FLASHu8 NAND_Init(void){ FMC_NAND_PCC_TimingTypeDef ComSpaceTiming,AttSpaceTiming; NAND_MPU_Config(); NAND_Handler.Instance=FMC_Bank3; NAND_Handler.Init.NandBank=FMC_NAND_BANK3; //NAND 挂在 BANK3 上 NAND_Handler.Init.Waitfeature=FMC_NAND_PCC_WAIT_FEATURE_DISABLE;//关闭等待特性 NAND_Handler.Init.MemoryDataWidth=FMC_NAND_PCC_MEM_BUS_WIDTH_8; //8 位数据宽度 NAND_Handler.Init.EccComputation=FMC_NAND_ECC_DISABLE;//禁止 ECC NAND_Handler.Init.ECCPageSize=FMC_NAND_ECC_PAGE_SIZE_512BYTE; //ECC 页大小为 512 字节 NAND_Handler.Init.TCLRSetupTime=10; //设置 TCLR(tCLR=CLE 到 RE 的延时)=//(TCLR+TSET+2)*THCLK,THCLK=1/180M=4.6ns NAND_Handler.Init.TARSetupTime=10;//设置 TAR(tAR=ALE 到 RE 的延时)=(TAR+TSET+1)*THCLK,THCLK=1/180M=4.6n。 ComSpaceTiming.SetupTime=10; //建立时间 ComSpaceTiming.WaitSetupTime=10; //等待时间 ComSpaceTiming.HoldSetupTime=10;//保持时间 ComSpaceTiming.HiZSetupTime=10; //高阻态时间 AttSpaceTiming.SetupTime=10; //建立时间 AttSpaceTiming.WaitSetupTime=10; //等待时间 AttSpaceTiming.HoldSetupTime=10; //保持时间 AttSpaceTiming.HiZSetupTime=10; //高阻态时间 HAL_NAND_Init(&NAND_Handler,&ComSpaceTiming,&AttSpaceTiming); NAND_Reset();//复位 NAND delay_ms(100); nand_dev.id=NAND_ReadID(); //读取 ID printf("NAND ID:%#x\r\n",nand_dev.id);NAND_ModeSet(4); //设置为 MODE4,高速模式 if(nand_dev.id==MT29F16G08ABABA) //NAND 为 MT29F16G08ABABA { nand_dev.page_totalsize=4320; nand_dev.page_mainsize=4096; nand_dev.page_sparesize=224;nand_dev.block_pagenum=128; nand_dev.plane_blocknum=2048; nand_dev.block_totalnum=4096; } else if(nand_dev.id==MT29F4G08ABADA)//NAND 为 MT29F4G08ABADA { nand_dev.page_totalsize=2112; nand_dev.page_mainsize=2048; nand_dev.page_sparesize=64; nand_dev.block_pagenum=64; nand_dev.plane_blocknum=2048; nand_dev.block_totalnum=4096; }else return 1; //错误,返回 return 0;}

该函数用于初始化 NAND FLASH,主要是调用函数 HAL_NAND_Init 函数初始化 NAND,

配置相关控制参数和 FMC 时序,另外,该函数会读取 NAND ID,从而判断 NAND FLASH 的

型号,执行不同的参数初始化。nand_dev 是我们在 nand.h 里面定义的一个 NAND 属性结构体,

存储 NAND FLASH 的一些特性参数,方便驱动。

函数 HAL_NAND_MspInit 内容这里我们就不列出来了,该函数是 NAND 的 MSP 初始化回

调函数,用来初始化与 MCU 相关的步骤,包括时钟使能和 IO 初始化。

接下来,我们看 NAND_ReadPage 函数的代码,如下:

//读取 NAND Flash 的指定页指定列的数据(main 区和 spare 区都可以使用此函数)//PageNum:要读取的页地址,范围:0~(block_pagenum*block_totalnum-1)//ColNum:要读取的列开始地址(也就是页内地址),范围:0~(page_totalsize-1)//*pBuffer:指向数据存储区//NumByteToRead:读取字节数(不能跨页读)//返回值:0,成功// 其他,错误代码u8 NAND_ReadPage(u32 PageNum,u16 ColNum,u8 *pBuffer,u16 NumByteToRead){ vu16 i=0; u8 res=0;u8 eccnum=0;//需要计算的 ECC 个数,每 NAND_ECC_SECTOR_SIZE 一个 eccu8 eccstart=0;//第一个 ECC 值所属的地址范围u8 errsta=0; u8 *p; *(vu8*)(NAND_ADDRESS|NAND_CMD)=NAND_AREA_A; //发送命令 *(vu8*)(NAND_ADDRESS|NAND_ADDR)=(u8)ColNum;//发送地址 *(vu8*)(NAND_ADDRESS|NAND_ADDR)=(u8)(ColNum>>8); *(vu8*)(NAND_ADDRESS|NAND_ADDR)=(u8)PageNum; *(vu8*)(NAND_ADDRESS|NAND_ADDR)=(u8)(PageNum>>8); *(vu8*)(NAND_ADDRESS|NAND_ADDR)=(u8)(PageNum>>16); *(vu8*)(NAND_ADDRESS|NAND_CMD)=NAND_AREA_TRUE1; //下面两行代码是等待 R/B 引脚变为低电平,其实主要起延时作用的,等待 NAND//操作 R/B 引脚。因为我们是通过将 STM32 的 NWAIT 引脚(NAND 的 R/B 引脚)配置//为普通 IO,代码中通过读取 NWAIT 引脚的电平来判断 NAND 是否准备就绪.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(NumByteToRead%NAND_ECC_SECTOR_SIZE)//不是 NAND_ECC_SECTOR_SIZE 的整数倍,不进行 ECC 校验{//读取 NAND FLASH 中的数据for(i=0;i<NumByteToRead;i++)*(vu8*)pBuffer++ = *(vu8*)NAND_ADDRESS;}else{eccnum=NumByteToRead/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++)//读取数据{*(vu8*)pBuffer++ = *(vu8*)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 区,之前存储的 ecc 值NAND_Delay(30);//等待 tADL*(vu8*)(NAND_ADDRESS|NAND_CMD)=0X05;//随机读指令*(vu8*)(NAND_ADDRESS|NAND_ADDR)=(u8)i;//发送地址*(vu8*)(NAND_ADDRESS|NAND_ADDR)=(u8)(i>>8);*(vu8*)(NAND_ADDRESS|NAND_CMD)=0XE0;//开始读数据NAND_Delay(30);//等待 tADLpBuffer=(u8*)&nand_dev.ecc_rdbuf[eccstart];for(i=0;i<4*eccnum;i++)//读取保存的 ECC 值{*(vu8*)pBuffer++= *(vu8*)NAND_ADDRESS;}for(i=0;i<eccnum;i++)//检验 ECC{if(nand_dev.ecc_rdbuf[i+eccstart]!=nand_dev.ecc_hdbuf[i+eccstart])//不相等{//进行 ECC 校验,并纠正 1bit ECC 错误res=NAND_ECC_Correction(p+NAND_ECC_SECTOR_SIZE*i,nand_dev.ecc_rdbuf[i+eccstart],nand_dev.ecc_hdbuf[i+eccstart]);if(res)errsta=NSTA_ECC2BITERR;//标记 2BIT 及以上 ECC 错误else errsta=NSTA_ECC1BITERR;//标记 1BIT ECC 错误}}} if(NAND_WaitForReady()!=NSTA_READY)errsta=NSTA_ERROR;//失败 return errsta; //成功}

该函数用于读取 NAND 里面的数据,通过指定页地址(PageNum)和列地址(ColNum),

就可以读取 NAND FLASH 里面任何地址的数据,不过该函数读数据时不能跨页读,所以一次

最多读取一个 Page 的数据(包括 spare 区数据)。当读取数据长度为 NAND_ECC_SECTOR_SIZE

(512 字节)的整数倍时,将执行 ECC 校验,ECC 校验完全是按照 46.1 节介绍的方法来实现,

当出现 ECC 错误时,调用 NAND_ECC_Correction 函数(46.1.1 节已经介绍)进行 ECC 纠错,

可以实现 1bit 错误纠正,并报告 2bit 及以上的错误。

接下来,我们看 NAND_WritePage 函数的代码,如下:

u8 NAND_WritePage(u32 PageNum,u16 ColNum,u8 *pBuffer,u16 NumByteToWrite){ vu16 i=0;u8 res=0;u8 eccnum=0; //需要计算的 ECC 个数,//每 NAND_ECC_SECTOR_SIZE 字节计算一个 eccu8 eccstart=0;//第一个 ECC 值所属的地址范围*(vu8*)(NAND_ADDRESS|NAND_CMD)=NAND_WRITE0; //发送地址 *(vu8*)(NAND_ADDRESS|NAND_ADDR)=(u8)ColNum; *(vu8*)(NAND_ADDRESS|NAND_ADDR)=(u8)(ColNum>>8); *(vu8*)(NAND_ADDRESS|NAND_ADDR)=(u8)PageNum; *(vu8*)(NAND_ADDRESS|NAND_ADDR)=(u8)(PageNum>>8); *(vu8*)(NAND_ADDRESS|NAND_ADDR)=(u8)(PageNum>>16);NAND_Delay(30);//等待 tADLif(NumByteToWrite%NAND_ECC_SECTOR_SIZE)//不是 NAND_ECC_SECTOR_SIZE 的整数倍,不进行 ECC 校验{for(i=0;i<NumByteToWrite;i++)//写入数据{*(vu8*)NAND_ADDRESS=*(vu8*)pBuffer++;}}else{eccnum=NumByteToWrite/NAND_ECC_SECTOR_SIZE; //得到 ecc 计算次数eccstart=ColNum/NAND_ECC_SECTOR_SIZE;for(res=0;res<eccnum;res++){FMC_Bank3->PCR|=1<<6;//使能 ECC 校验for(i=0;i<NAND_ECC_SECTOR_SIZE;i++)//写入 NAND_ECC_SECTOR_SIZE 个数据{*(vu8*)NAND_ADDRESS=*(vu8*)pBuffer++;}while(!(FMC_Bank3->SR&(1<<6)));//等待 FIFO 空nand_dev.ecc_hdbuf[res+eccstart]=FMC_Bank3->ECCR;//读取硬件计算后的 ECC 值FMC_Bank3->PCR&=~(1<<6);//禁止 ECC 校验}i=nand_dev.page_mainsize+0X10+eccstart*4;//计算写入 ECC 的 spare 区地址NAND_Delay(30);//等待*(vu8*)(NAND_ADDRESS|NAND_CMD)=0X85;//随机写指令//发送地址*(vu8*)(NAND_ADDRESS|NAND_ADDR)=(u8)i;*(vu8*)(NAND_ADDRESS|NAND_ADDR)=(u8)(i>>8);NAND_Delay(30);//等待 tADLpBuffer=(u8*)&nand_dev.ecc_hdbuf[eccstart];for(i=0;i<eccnum;i++)//写入 ECC{for(res=0;res<4;res++){*(vu8*)NAND_ADDRESS=*(vu8*)pBuffer++;}}} *(vu8*)(NAND_ADDRESS|NAND_CMD)=NAND_WRITE_TURE1; if(NAND_WaitForReady()!=NSTA_READY)return NSTA_ERROR;//失败 return 0;//成功}

该函数用于往 NAND 里面写数据,通过指定页地址(PageNum)和列地址(ColNum),就

可以往 NAND FLASH 里面任何地址写数据(包括 spare 区),同样,该函数也不支持跨页写。

当读取数据长度为 NAND_ECC_SECTOR_SIZE(512 字节)的整数倍时,将执行 ECC 校验,

并将 ECC 值写入 spare 区对应的地址,以便读取数据时,进行 ECC 校验。

nand.c 里面的其他代码以及 nand.h 里面的代码,请大家参考本例程源码。接下来,我看 ftl.c

里面的代码,该文件,我们只介绍:FTL_Init、FTL_Format、FTL_CreateLUT、FTL_LBNToPBN、

FTL_WriteSectors 和 FTL_ReadSectors 等七个函数。

首先,FTL_Init 函数代码如下:

//FTL 层初始化//返回值:0,正常 其他,失败u8 FTL_Init(void){ u8 temp; if(NAND_Init())return 1;//初始化 NAND FLASHif(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_CreateLUT(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;}

该函数用于初始化 FTL,包括:初始化 NAND FLASH、为 lut 表申请内存、创建 lut 表等

操作,如果创建 lut 表失败,则会通过 FTL_Format 函数格式化 NAND FLASH。

FTL_Format 函数代码如下:

u8 FTL_Format(void){ u8 temp; u32 i,n; u32 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_CheckBadBlock(i);//检查一个块是否为坏块 if(temp==0)//好块 {temp=NAND_EraseBlock(i);if(temp)//擦除失败,认为坏块{printf("Bad block:%d\r\n",i);FTL_BadBlockMark(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_CheckBadBlock(i);//检查一个块是否为坏块 if(temp==0)//好块 { NAND_WriteSpare(i*nand_dev.block_pagenum,2,(u8*)&n,2);//写入逻辑块编号 n++;//逻辑块编号加 1 if(n==goodblock) break;//全部标记完了 } } if(FTL_CreateLUT(1))return 2;//重建 LUT 表失败 return 0;}

该函数用于格式化 NAND FLASH,执行的操作包括:1,检测/搜索整个 NAND 的坏块,

并做标记;2,分割所有好块,93%用作物理地址(并进行逻辑编号),7%用作保留区;3,重

新创建 lut 表。此函数,将我们在 46.1.2 节介绍的 FTL 层坏块管理的几个要点(识别坏块并标

记、生成转换表、生成保留区),都实现了,从而完成对 NAND FLASH 的格式化(不是文件系

统那种格式化,这里的格式化是指针对 FTL 层的初始化设置)。

接下来,我们看 FTL_CreateLUT 函数,该函数代码如下:

//重新创建 LUT 表//mode:0,仅检查第一个坏块标记// 1,两个坏块标记都要检查(备份区也要检查)//返回值:0,成功// 其他,失败u8 FTL_CreateLUT(u8 mode){ u32 i;u8 buf[4]; u32 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=((u16)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 表创建完成}

该函数用于重建 lut 表,读取保存在每个 Block 第一个 page 的 spare 区的逻辑编号,存储在

nand_dev.lut 表里,并初始化有效块(nand_dev.valid_blocknum)和好块(nand_dev.good_blocknum)

的数量,完成转换表(lut 表)的创建。

接下来,我们看 FTL_LBNToPBN 函数,该函数代码如下:

//逻辑块号转换为物理块号//LBNNum:逻辑块编号//返回值:物理块编号u16 FTL_LBNToPBN(u32 LBNNum){ u16 PBNNo=0; //当逻辑块号大于有效块数的时候返回 0XFFFF if(LBNNum>nand_dev.valid_blocknum)return 0XFFFF; PBNNo=nand_dev.lut[LBNNum]; return PBNNo;}

该函数用于将逻辑块地址改为物理块地址,输入参数:LBNNum,表示逻辑块编号,返回

值表示 LBNNum 对应的物理块地址。有了该函数,就可以很方便的实现逻辑块地址到物理块

地址的映射。

接下来,我们看 FTL_WriteSectors 函数,该函数代码如下:

//写扇区(支持多扇区写),FATFS 文件系统使用//pBuffer:要写入的数据//SectorNo:起始扇区号//SectorSize:扇区大小(不能大于 NAND_ECC_SECTOR_SIZE 定义的大小,否则会出错!!)//SectorCount:要写入的扇区数量//返回值:0,成功//其他,失败u8 FTL_WriteSectors(u8 *pBuffer,u32 SectorNo,u16 SectorSize,u32 SectorCount){ u8 flag=0; u16 temp; u32 i=0;u16 wsecs;//写页大小u32 wlen;//写入长度 u32 LBNNo; //逻辑块号 u32 PBNNo; //物理块号 u32 PhyPageNo; //物理页号 u32 PageOffset; //页内偏移地址 u32 BlockOffset; //块内偏移地址u32 markdpbn=0XFFFFFFFF;//标记了的物理块编号for(i=0;i<SectorCount;i++) { LBNNo=(SectorNo+i)/(nand_dev.block_pagenum*(nand_dev.page_mainsize/SectorSize));//根据逻辑扇区号和扇区大小计算出逻辑块号 PBNNo=FTL_LBNToPBN(LBNNo);//将逻辑块转换为物理块 if(PBNNo>=nand_dev.block_totalnum)return 1; //物理块号大于总块数,则失败. 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//读出写入大小的内容判断是否全为 0XFFFFFFFF(以 4 字节为单位读取)flag=NAND_ReadPageComp(PhyPageNo,PageOffset,0XFFFFFFFF,wlen/4,&temp);if(flag)return 2;//读写错误,坏块//全为 0XFF,可以直接写数据if(temp==(wlen/4))flag=NAND_WritePage(PhyPageNo,PageOffset,pBuffer,wlen);else flag=1;//不全是 0XFF,则另作处理if(flag==0&&(markdpbn!=PBNNo))//标记了的物理块与当前物理块不同{flag=FTL_UsedBlockMark(PBNNo); //标记此块已经使用markdpbn=PBNNo;//标记完成,标记块=当前块,防止重复标记}if(flag)//不全为 0XFF/标记失败,将数据写到另一个块 {temp=((u32)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//拷贝到另外一个 block,并写入数据 flag=FTL_CopyAndWriteToBlock(PhyPageNo,PageOffset,pBuffer,wlen); if(flag)return 3;//失败 }i+=wsecs-1;pBuffer+=wlen;//数据缓冲区指针偏移 } return 0;}

该函数非常重要,它是 FTL 层对文件系统的接口函数,用于往 NAND FLASH 里面写入数

据,用户调用该函数时,无需关心坏块和磨损均衡问题,完全可以把 NAND FLASH 当成一个

SD 卡来访问。该函数输入参数:SectorNo 用于指定扇区地址,扇区大小由 SectorSize 指定,一

般我们设置 SectorSize=NAND_ECC_SECTOR_SIZE,方便进行 ECC 校验处理。

该函数根据 SectorNo 和 SectorSize,首先计算出逻辑块地址(LBNNo),然后将逻辑块地

址转换为物理块地址(PBNNo),然后再计算出块内的页地址(PhyPageNo)和页内的偏移地址

(PageOffset),然后计算该页内还可以连续写入的扇区数(如果可以连续写,则可以提高速度),

然后判断要写入的区域,数据是否全为 0XFF,如果全是 0XFF,则直接写入,写入完成对该物

理块进行已被使用标记。如果不是全 0XFF,则需要利用 NAND 页拷贝功能,将本页数据拷贝

到另外一个 Block,并写入要写入的数据,这个操作由 FTL_CopyAndWriteToBlock 函数来完成。

最后,我们看 FTL_ReadSectors 函数,该函数代码如下:

//读扇区(支持多扇区读),FATFS 文件系统使用//pBuffer:数据缓存区//SectorNo:起始扇区号//SectorSize:扇区大小//SectorCount:要写入的扇区数量//返回值:0,成功//其他,失败u8 FTL_ReadSectors(u8 *pBuffer,u32 SectorNo,u16 SectorSize,u32 SectorCount){ u8 flag=0; u32 i=0;u16 rsecs;//单次读取页数 u32 LBNNo;//逻辑块号 u32 PBNNo;//物理块号 u32 PhyPageNo;//物理页号 u32 PageOffset;//页内偏移地址 u32 BlockOffset;//块内偏移地址 for(i=0;i<SectorCount;i++) { LBNNo=(SectorNo+i)/(nand_dev.block_pagenum*(nand_dev.page_mainsize/SectorSize)); //根据逻辑扇区号和扇区大小计算出逻辑块号 PBNNo=FTL_LBNToPBN(LBNNo);//将逻辑块转换为物理块 if(PBNNo>=nand_dev.block_totalnum)return 1; //物理块号大于总块数,则失败. 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-iflag=NAND_ReadPage(PhyPageNo,PageOffset,pBuffer,rsecs*SectorSize);//读取数据if(flag==NSTA_ECC1BITERR) //对于 1bit ecc 错误,可能为坏块,读 2 次确认 flag=NAND_ReadPage(PhyPageNo,PageOffset,pBuffer,rsecs*SectorSize);if(flag==NSTA_ECC1BITERR) //重读数据,再次确认,还是有 1BIT ECC 错误{//将整个数据,搬运到另外一个 block,防止此 block 是坏块FTL_CopyAndWriteToBlock(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_BadBlockMark(PhyPageNo/nand_dev.block_pagenum);//标记为坏块FTL_CreateLUT(1);//重建 LUT 表}flag=0;}//2bit ecc 错误,不处理(可能是初次读取数据导致的)if(flag==NSTA_ECC2BITERR)flag=0;if(flag)return 2;//失败pBuffer+=SectorSize*rsecs;//数据缓冲区指针偏移i+=rsecs-1; } return 0;}

该函数也是 FTL 层对文件系统的接口函数,用于读取 NAND FLASH 里面的数据,同样,

用户在调用该函数时,无需关系坏块管理和磨损均衡问题,可以像访问 SD 卡一样,调用该函

数,实现读取 NAND FLASH 的数据。该函数的实现原理,同前面介绍的 FTL_WriteSectors 函

数基本类似,不过该函数对读数时出现的 ECC 错误,进行了处理,对于读取数据时出现 1bit ECC

错误的 Block,进行两次读取(多次确认,以免误操作),如果两次读取都有 1bitECC 错误,那

么该 Block 可能是坏块,当出现此错误后,我们先将该 Block 的数据,拷贝到另外一个 Block

(备份现有数据),然后对该 Block 进行擦除和写 0,然后判断擦除/写 0 是否正常,如果正常,

则说明这个块不是坏块,还可以继续使用。如果不正常,则说明该块确实是一个坏块,必须进

行坏块标记,并重建 lut 表。如果读数时出现 2bit ecc 错误,这个不一定就是出错了,而有可能

是读取还未写入过数据的 Block(未写入过数据,那么 ECC 值,肯定也是未写入过,如果进行

ECC 校验的话,必定出错),导致的 ECC 错误,对于此类错误,我们直接不予处理(忽略)就

可以了。

ftl.c 里面的其他代码以及 ftl.h 里面的代码,这里就不做介绍了,请大家参考本例程源码。

另外,nandtester.c 和 nandtester.h 这两个文件,主要用于 usmart 调试 nand.c 和 ftl.c 里面的相关

函数,这里也不做介绍了,请大家参考本例程源码。

最后,打开 main.c 文件,代码如下:

int main(void){u8 key,t=0;u16 i;u8 *buf;u8 *backbuf; Cache_Enable(); //打开 L1-Cache MPU_Memory_Protection();//保护相关存储区域 HAL_Init(); //初始化 HAL 库 Stm32_Clock_Init(432,25,2,9);//设置时钟,216Mhz delay_init(216);//延时初始化uart_init(115200);//串口初始化usmart_dev.init(108);//初始化 USMARTLED_Init(); //初始化 LED KEY_Init(); //初始化按键 SDRAM_Init(); //初始化 SDRAM LCD_Init(); //初始化 LCD my_mem_init(SRAMIN);//初始化内部内存池 my_mem_init(SRAMEX); //初始化外部 SDRAM 内存池 POINT_COLOR=RED;LCD_ShowString(30,50,200,16,16,"Apollo STM32F4/F7");LCD_ShowString(30,70,200,16,16,"NAND TEST");LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");LCD_ShowString(30,110,200,16,16,"2016/7/15");LCD_ShowString(30,130,200,16,16,"KEY0:Read Sector 2");LCD_ShowString(30,150,200,16,16,"KEY1:Write Sector 2");LCD_ShowString(30,170,200,16,16,"KEY2:Recover Sector 2");while(FTL_Init()) //检测 NAND FLASH,并初始化 FTL{LCD_ShowString(30,190,200,16,16,"NAND Error!");delay_ms(500);LCD_ShowString(30,190,200,16,16,"Please Check");delay_ms(500);LED0_Toggle;//DS0 闪烁}backbuf=mymalloc(SRAMIN,NAND_ECC_SECTOR_SIZE); //申请一个扇区的缓存buf=mymalloc(SRAMIN,NAND_ECC_SECTOR_SIZE); //申请一个扇区的缓存POINT_COLOR=BLUE;//设置字体为蓝色sprintf((char*)buf,"NAND Size:%dMB",(nand_dev.block_totalnum/1024)*(nand_dev.page_mainsize/1024)* \nand_dev.block_pagenum);LCD_ShowString(30,190,200,16,16,buf); //显示 NAND 容量FTL_ReadSectors(backbuf,2,NAND_ECC_SECTOR_SIZE,1);//预先读取扇区 0 到备份区域,防止乱写导致文件系统损坏.while(1){key=KEY_Scan(0);switch(key){case KEY0_PRES://KEY0 按下,读取 sectorkey=FTL_ReadSectors(buf,2,NAND_ECC_SECTOR_SIZE,1);//读取扇区if(key==0)//读取成功{LCD_ShowString(30,210,200,16,16,"USART1 Sending Data... ");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_ShowString(30,210,200,16,16,"USART1 Send Data Over! ");}break;case KEY1_PRES://KEY1 按下,写入 sectorfor(i=0;i<NAND_ECC_SECTOR_SIZE;i++)buf[i]=i+t;//填充数据(随机的,根据 t 的值来确定)LCD_ShowString(30,210,210,16,16,"Writing data to sector..");key=FTL_WriteSectors(buf,2,NAND_ECC_SECTOR_SIZE,1);//写入扇区if(key==0)LCD_ShowString(30,210,200,16,16,"Write data successed ");//写入成功elseLCD_ShowString(30,210,200,16,16,"Write data failed ");//写入失败break;case KEY2_PRES://KEY2 按下,恢复 sector 的数据LCD_ShowString(30,210,210,16,16,"Recovering data... ");key=FTL_WriteSectors(backbuf,2,NAND_ECC_SECTOR_SIZE,1);//写入扇区if(key==0)LCD_ShowString(30,210,200,16,16,"Recovering data OK ");//恢复成功else LCD_ShowString(30,210,200,16,16,"Recovering data failed ");//恢复失败break;}t++;delay_ms(10);if(t==20){LED0_Toggle;t=0;}}}

此部分代码比较简单,我们先初始化相关外设,然后初始化 FTL,在 FTL 初始化成功以后,

先对扇区 2 的数据进行备份,随后进入死循环,检测按键,可以通过 KEY0/KEY1/KEY2 按键

对扇区 2 的数据进行读取、写入和还原操作。同样,DS0 闪烁,用于提示程序正在运行。

最 后 , 我 们 将 NAND_EraseChip 、 NAND_EraseBlock 、 FTL_CreateLUT 、 FTL_Format 、

test_writepage 和 test_readpage 等函数加入 USMART 控制,这样,我们就可以通过串口调

试助手,测试 NAND FLASH 的各种操作了,方便大家测试。软件部分就给大家介绍到这里。

44.4 下载验证

在代码编译成功之后,我们通过下载代码到 ALIENTEK 水星 STM32 开发板上,得到如图

44.4.1 所示界面:

图 44.4.1 程序运行效果图

此时,我们可以按下 KEY0/KEY1/WK_UP 等按键进行对应的测试。我们按 KEY0,可以读

取扇区 2 里面的数据,通过串口调试助手查看,如图 44.4.2 所示:

图 44.4.3 串口观看扇区 2 里面的数据

另外,我们还可以利用 usmart,调用相关函数,执行不同的操作,如图 44.4.4 所示:

图 44.4.4 USMART 调用相关函数

标签: #flash均衡擦写算法 #存储过程简写 #驱动1未分配的空间