下面以最简单的read操作为例,解释如何理解时序图,以及将时序图中的要求,转化为代码。
解释时序图之前,让我们先要搞清楚,我们要做的事情:
从Nand Flash的某个页Page里面,读取我们要的数据。
要实现此功能,会涉及到几部分的知识,即使我们不太懂Nand Flash的细节,但是通过前面的基本知识的介绍,那么以我们的常识,至少很容易想到的就是,需要用到哪些命令,怎么发这些命令,怎么计算所需要的地址,怎么读取我们要的数据等等。
下面就一步步的解释,需要做什么,以及如何去做:
首先,是要了解,对于读取数据,要用什么命令:
根据前面关于Nand Flash的命令集合介绍,我们知道,要读取数据,要用到Read命令,该命令需要2个周期,第一个周期发0x00,第二个周期发0x30。
知道了用何命令后,再去了解如何发送这些命令。
提示 | |
---|---|
在开始解释前,关于”使能”这个词要罗嗦一下,以防有些读者和我以前一样,在听这类词语的时候,属于初次接触,或者接触不多的,就很容易被搞得一头雾水的(虽然该词汇对于某些专业人士说是属于最基本的词汇了,囧)。 使能(Enable),是指使其(某个信号)有效,使其生效的意思,“使其”“能够”怎么怎么样。。。。比如,上面图中的CLE线号,是高电平有效,如果此时将其设为高电平,我们就叫做,将CLE使能,也就是使其生效的意思。 |
注意 | |
---|---|
此图来自三星的型号K9K8G08U0A的Nand Flash的数据手册(datasheet)。 |
我们来一起看看,我在图6中的特意标注的①边上的黄色竖线。
黄色竖线所处的时刻,是在发送读操作的第一个周期的命令0x00之前的那一刻。
让我们看看,在那一刻,其所穿过好几行都对应什么值,以及进一步理解,为何要那个值。
- 黄色竖线穿过的第一行,是CLE。还记得前面介绍命令所存使能(CLE)那个引脚吧?CLE,将CLE置1,就说明你将要通过I/O复用端口发送进入Nand Flash的,是命令,而不是地址或者其他类型的数据。只有这样将CLE置1,使其有效,才能去通知了内部硬件逻辑,你接下来将收到的是命令,内部硬件逻辑,才会将受到的命令,放到命令寄存器中,才能实现后面正确的操作,否则,不去将CLE置1使其有效,硬件会无所适从,不知道你传入的到底是数据还是命令了。
- 而第二行,是CE#,那一刻的值是0。这个道理很简单,你既然要向Nand Flash发命令,那么先要选中它,所以,要保证CE#为低电平,使其有效,也就是片选有效。
- 第三行是WE#,意思是写使能。因为接下来是往Nand Flash里面写命令,所以,要使得WE#有效,所以设为低电平。
- 第四行,是ALE是低电平,而ALE是高电平有效,此时意思就是使其无效。而对应地,前面介绍的,使CLE有效,因为将要数据的是命令(此时是发送图示所示的读命令第二周期的0x30),而不是地址。如果在其他某些场合,比如接下来的要输入地址的时候,就要使其有效,而使CLE无效了。
- 第五行,RE#,此时是高电平,无效。可以看到,知道后面低6阶段,才变成低电平,才有效,因为那时候,要发生读取命令,去读取数据。
- 第六行,就是我们重点要介绍的,复用的输入输出I/O端口了,此刻,还没有输入数据,接下来,在不同的阶段,会输入或输出不同的数据/地址。
- 第七行,R/B#,高电平,表示R(Ready)/就绪,因为到了后面的第5阶段,硬件内部,在第四阶段,接受了外界的读取命令后,把该页的数据一点点送到页寄存器中,这段时间,属于系统在忙着干活,属于忙的阶段,所以,R/B#才变成低,表示Busy忙的状态的。
介绍了时刻①的各个信号的值,以及为何是这个值之后,相信,后面的各个时刻,对应的不同信号的各个值,大家就会自己慢慢分析了,也就容易理解具体的操作顺序和原理了。
在介绍具体读取数据的详细流程之前,还要做一件事,那就是,先要搞懂我们要访问的地址,以及这些地址,如何分解后,一点点传入进去,使得硬件能识别才行。
此处还是以K9K8G08U0A为例,此Nand Flash,一共有8192个块,每个块内有64页,每个页是2K+64 Bytes。
假设,我们要访问其中的第7000个块中的第64页中的1208字节处的地址,此时,我们就要先把具体的地址算出来:
物理地址
=块大小×块号 + 页大小×页号 + 页内地址
=128K×7000 + 2K×64 + 1208
=0x36B204B8
接下来,我们就看看,怎么才能把这个实际的物理地址,转化为Nand Flash所要求的格式。
在解释地址组成之前,先要来看看其datasheet中关于地址周期的介绍:
结合图 1.7 “Nand Flash数据读取操作的时序图”中的2,3阶段,我们可以看出,此Nand Flash地址周期共有5个,2个列(Column)周期,3个行(Row)周期。
- 对应地,列地址A0~A10,就是页内地址,地址范围是从0到2047。
细心的读者可能注意到了,为何此处多出来个A11呢?
这样从A0到A11,一共就是12位,可以表示的范围就是0~212,即0~4096了。
实际上,由于我们访问页内地址,可能会访问到oob的位置,即2048-2111这64个字节的范围内,所以,此处实际上只用到了2048~2111,用于表示页内的oob区域,其大小是64字节。
- 对应地,A12~A30,称作页号,页的号码,可以定位到具体是哪一个页。
A18~A30,表示对应的块号,即属于哪个块。
简单解释完了地址组成,那么就很容易分析上面例子中的地址了。
注意,下面这样的方法,是错误的:
0x36B204B8 = 11 0110 1011 0010 0000 0100 1011 1000,分别分配到5个地址周期就是:
1st周期 | A7 ~ A0 | 1011 1000 = 0xB8 |
2nd周期 | A11~ A8 | 0100 = 0x04 |
3rd周期 | A19~A12 | 0010 0000 = 0x20 |
4th周期 | A27~A20 | 0110 1011 = 0x6B |
5th周期 | A30~A28 | 11 = 0x03 |
注意 | |
---|---|
与图 1.7 “Nand Flash数据读取操作的时序图”中对应的,*L,意思是地电平,由于未用到那些位,datasheet中强制要求设为0,所以,才有上面的2nd周期中的高4位是0000.其他的A30之后的位也是类似原理,都是0。 |
而至于上述计算方法为何是错误的,那是因为上面计算过程中,把第11位的值,本来是属于页号的位A11,给算成页内地址里面的值了。
应该是这样计算,才是对的:
0x36B204B8 = 11 0110 1011 0010 0000 0100 1011 1000
1st周期 | A7 ~ A0 | 1011 1000 = 0xB8 |
2nd周期 | A10~ A8 | 100 = 0x04 |
3rd周期 | A19~A12 | 010 0000 0 = 0x40 |
4th周期 | A27~A20 | 110 1011 0 = 0xD6 |
5th周期 | A30~A28 | 11 0 = 0x06 |
那有人会问了,上面表11中,不是明明写的A0到A30,其中包括A11,不是正好对应着此处地址中的bit0到bit30吗?
其实,我开始也是犯了同样的错误,误以为我们要传入的地址的每一位,就是对应着表11中的A0到A30呢,实际上,表11中的A11,是比较特殊的,只有当我们访问页内地址处于oob的位置,即属于2048~2111的时候,A11才会其效果,才会用A0-A11用来表示对应的某个属于2048~2111的某个值,属于oob的某个位置。
而我们此处的页内地址为2108,还没有超过2047呢,所以A11肯定是0。
这么解释,显得很绕,很难看懂。
换种方式来解释,就容易听懂了:
说白了,我们就是要访问第7000个块中的第64页中的1208字节处,对应着
页内地址
=1208
=0x4B8
页号
=块数×页数/块 + 块内的页号
= 7000×(128K/2K) + 64
= 7000×64 + 64
= 448064
=0x6D640
也就是,我们要访问0x6D640页内的0x4B8地址,这样很好理解吧,^_^。
然后对应的:
页内地址=0x4B8
分成两个对应的列地址,就变成
0x4B8 :列地址1=0xB8,列地址2=0x04
页号=0x6D640,分成三个行号就是:
0x6D640:行号1=0x40,行号2=0xD6,行号3=0x06
再回头看看上面的计算方法,
最开始计算出来的:
列地址1=0xB8
列地址2=0x04
行号1=0x20
行号2=0x6B
行号3=0x03
是错误的。
而第二次计算正确的:
列地址1=0xB8
列地址2=0x04
行号1=0x40
行号2=0xD6
行号3=0x06
才是对的,也和我们此处自己手动计算,是一致的。
第一次之所以计算错,就是错误的把行地址的最低一位A11,放到列地址中的最高位了。
至此,才算把如何手动计算行地址和列地址,解释明白和正确了。
对应的,Linux的源码\drivers\mtd\nand\nand_base.c中,也是这样处理的:
static void nand_command_lp(struct mtd_info *mtd, unsigned int command, int column, int page_addr) { ...... /* Serially input address */ if (column != -1) { ...... chip->cmd_ctrl(mtd, column, ctrl); /* 发送Col Addr 1 */ ctrl &= ~NAND_CTRL_CHANGE; chip->cmd_ctrl(mtd, column >> 8, ctrl); /* 发送Col Addr 2 */ } if (page_addr != -1) { chip->cmd_ctrl(mtd, page_addr, ctrl); /* 发送Row Addr 1 */ chip->cmd_ctrl(mtd, page_addr >> 8, /* 发送Row Addr 2 */ NAND_NCE | NAND_ALE); /* One more address cycle for devices > 128MiB */ if (chip->chipsize > (128 << 20)) chip->cmd_ctrl(mtd, page_addr >> 16, /* 发送Row Addr 3 */ NAND_NCE | NAND_ALE); } }
因此,我们要访问第7000个块中的第64页中的1208字节处的话,所要传入的地址就是分5个周期:
分别传入两个列地址的:
列地址1=0xB8
列地址2=0x04
然后再传3个行地址的:
行号1=0x40
行号2=0xD6
行号3=0x06
这样硬件才能识别。
而接下来的内容,也就是介绍硬件是如何处理这些输入的。
准备工作终于完了,下面就可以开始解释说明,对于读操作的,上面图中标出来的,1-6个阶段,具体是什么含义。
操作准备阶段:此处是读(Read)操作,所以,先发一个图5中读命令的第一个阶段的0x00,表示,让硬件先准备一下,接下来的操作是读。
发送两个周期的列地址。也就是页内地址,表示,我要从一个页的什么位置开始读取数据。
接下来再传入三个行地址。对应的也就是页号。
然后再发一个读操作的第二个周期的命令0x30。接下来,就是硬件内部自己的事情了。
Nand Flash内部硬件逻辑,负责去按照你的要求,根据传入的地址,找到哪个块中的哪个页,然后把整个这一页的数据,都一点点搬运到页缓存中去。而在此期间,你所能做的事,也就只需要去读取状态寄存器,看看对应的位的值,也就是R/B#那一位,是1还是0,0的话,就表示,系统是busy,仍在”忙“(着读取数据),如果是1,就说系统活干完了,忙清了,已经把整个页的数据都搬运到页缓存里去了,你可以接下来读取你要的数据了。
对于这里。估计有人会问了,这一个页一共2048+64字节,如果我传入的页内地址,就像上面给的1208一类的值,只是想读取1028到2011这部分数据,而不是页开始的0地址整个页的数据,那么内部硬件却读取整个页的数据出来,岂不是很浪费吗?答案是,的确很浪费,效率看起来不高,但是实际就是这么做的,而且本身读取整个页的数据,相对时间并不长,而且读出来之后,内部数据指针会定位到你刚才所制定的1208的那个位置。
接下来,就是你“窃取“系统忙了半天之后的劳动成果的时候了,呵呵。通过先去Nand Flash的控制器中的数据寄存器中写入你要读取多少个字节(byte)/字(word),然后就可以去Nand Flash的控制器的FIFO中,一点点读取你要的数据了。
至此,整个Nand Flash的读操作就完成了。
对于其他操作,可以根据我上面的分析,一点点自己去看datasheet,根据里面的时序图去分析具体的操作过程,然后对照代码,会更加清楚具体是如何实现的。