目录
1.基础知识
1).Nor Flash 和Nand Flash 的区别?
NOR FLASH | NAND FLASH | |
---|---|---|
结构 | NORflash采用内存的随机读取技术。各单元之间是并联的,对存储单元进行统一编址,所以可以随机访问任意一个字,应用程序可直接在flash内运行,而无需先拷贝到RAM。 | NANDflash数据线和地址线共用I/O线,需额外联接一些控制的输入输出 |
读写速度 | NOR flash有更快的读取速度 | NAND flash有更快的写、擦除速度 |
可靠性 | NOR的擦写次数是10万次 | NAND的擦写次数是100万次 (NAND器件的坏块是随机分布的) |
成本和容量 | 在面积和工艺相同的情况下,NAND的容量比NOR大的多,成本更低 | |
优缺点 | 无位翻转,无坏块,稳定(存储关键性的程序) | 位反转,有坏块 |
易用性 | NOR flash有专用的地址引脚来寻址,较容易和其他芯片联接,还支持本地执行 | NAND flash的IO端口采用复用的数据线和地址线,必须先通过寄存器串行地进行数据存取。各厂商对信号的定义不同,增加了应用的难度 |
编程 | NOR flash采用统一编址(有独立地址线),可随机读取每个“字”,但NOR flash不能像RAM以字节改写数据,只能按“页”写,故NOR flash不能代替RAM。擦除既可整页擦除,也可整块擦除 注意: flash进行写操作时,只能将相应的位由1变0,而擦除才能把块内所有位由0变1。所有写入数据时,如果该页已经存在数据,必须先擦除再写。 | NAND flash共用地址线和数据线,页是读写数据的最小单元,块是擦除数据的最小单元 |
NOR FLASH 的特点是:
可以像内存一样的读取,但是不能直接写。
例子: 在Nor Flash中有以下指令:
mov R0,#0 --R0赋值为0
LDR R1 ,[R0] --读取地址R0的值到寄存器R1
STR R1 ,[R0] --把R1的值写入R0寄存器
当执行STR R1 ,[R0] 不会成功,被视为无效操作。
这样子如果程序烧写在NOR FLASH 中会出现什么问题呢?
程序中含有需要更改的全局变量/静态变量(变量存储在栈中),但是如果现在bin文件烧写到NOR FLASH 上,那么意味着不能修改变量的值了,程序就会达不到我们预期的效果。所以如果想要的到想要的结果,就需要把这些全局变量/静态变量,重定位放到SDRAM(可读可写)中。
2).以S3C2440为例,设为NOR Flash 或 Nand Flash 启动时,启动地址的差别
- .基本框架
S3C2440是一个SOC,在一块芯片上面集成了有CPU、GPIO控制器、Nand控制器、Nor控制器、以及4K的SRAM。
- 启动过程(大多数ARM芯片从0地址启动)
1.NoR启动,NoR Flash 基地址为0(片内RAM地址为:0x40000000),CPU读出NOR读出上的第一个指令(前四个字节)执行,CPU继续读出其他指令执行(CPU可以在Nor Flash取指令直接执行)。
2.Nand启动,片内4K SRAM 基地之为0(Nor Falsh 不可访问),硬件2240把NAND前4K内容复制到片内内存SRAM中,然后CPU从0地址取出第一条指令(前四个字节)执行。
3).不同位宽内存设备之间的连接
内存控制器地址线和片外内存设备的接法:
可以发现他们的接法好像都是不同的,他们的规律是什么?
这时需要去查看一下芯片手册。
8-bit ROM
16-bit ROM
32-bit ROM
规律:那么就是说外接芯片的位宽有变化,那地址线的接法也有变化。
大致如下简图所示:
那为什么是这样呢?需要了解数据内部的保存结构,简图如下:
假如现在执行指令:
mov R0 ,#3 --把3的数值赋值给R0
LDRB R1, [R0] --读取地址为3的位置读取1个字节的数据
那么CPU就会把地址3发出去,也就是:0x00000011 ,那么此时地址线 A0 =1, A1 =1 ,其他(A2-A27)为0.
- 如果外界是8-bit ROM,因为A0接A0,A1连接A1,此时他收到的就是0x3
- 如果外界是16-bit ROM,因为A1接A0,A2连接A1,此时他收到的就是0x1
- 如果外界是32-bit ROM,因为A2接A0,A3连接A1,此时他收到的就是0x0
我们的任务是读取第3byte的数据:
当为8-bit ROM,那我收到的正好也是0x3这个数值,没有问题。
当为16-bit ROM,我收到的是0x1,这个数值,也就是十进制的1,读取地址编号为1的地址的值,很巧,发现我们需要第3字节的数据刚好在地址1的位置,但是这个位置有两个字节啊,怎么办?CPU不用管,它只需要发出地址就行,所以这也是内存控制器做的事,内存控制器根据收到的CPU发送的 A0 = 1 ,那么读取的就是地址编号为1,右边的那个字节(第3字节)的数据。
当为32-bit ROM,我收到的是0x0,这个数值,也就是十进制的0,读取地址编号为0的地址的值,同样,发现我们需要第3字节的数据刚好在地址0的位置,但是这个位置有四个字节啊,怎么办?CPU不用管,它只需要发出地址就行,所以这也是内存控制器做的事,内存控制器根据收到的 [A1:A0] = 11 ,那么读取的就是地址编号为0,最右边的那个字节(第3字节)的数据。
这个过程可以使用一张图来表示,就可以简单明了。
再举个例子:
假如现在执行指令:
mov R0 ,#4 --把4的数值赋值给R0
LDR R1, [R0] --读取地址为43的位置读取4个字节的数据
那他的过程可以简化为下表:
所以CPU就是一个大BOSS,它只负责发出指令(地址)给内存控制器,而关于你数据是如何拆分和组装的,CPU并不关心,我只需要发出指令,然后就收数据就好了,那剩下的事情就是手下(内存控制器)做的,它负责数据的组装和拆分,最终把正确的结果返回给CPU。
2.使用u-boot体验Nor Flash的操作
例子2:读数据
烧写u-boot,到Nor Flash 上,开发版设为Nor Flash启动、
使用OpenJTAG:输入命令读取:md.b 0 读取Nor Flash 0地址的数据:
结果如下:
查看一下uboot.bin文件的数据和读取出来是一样:
说明Nor Flash可以直接读取数据。
例子2:读取ID
查看Nor Flash的数据手册,在此开发版使用的芯片型号是:MX29LV160DBTI
步骤:
①往地址 0x555写入数据0xAA
②往地址 0x2AA写入数据0x55
③往地址 0x555写入数据0x90
④读取地址0x00得到厂家ID:0xC2
⑤读取地址0x01得到设备ID:MX29LV160DT: 22C4; MX29LV160DB: 2249
注意:设备位宽连接的问题,因为NOR Flash连接的是16位的,所以连接的简图如下
因此如果CPU需要往NOR Flash的0x555的写入数据0xAA,那么需要在2440发送地址的时候需要把0x555左移一位,再发送给Nor Flash才是正确的地址。
那么此时UBOOT怎么操作呢?
如果CPU需要左移一位,才是正确的地址,也就是是说,发送的地址需要乘上2,如下:
步骤:
①往地址 0xAAA写入数据0xAA (mw.w aaa aa)
②往地址 0x554写入数据0x55 (mw.w 554 55)
③往地址 0xAAA写入数据0x90 (mw.w aaa 90)
④读取地址0x00得到厂家ID:0xC2 (md.w 0 1)
⑤读取地址0x02得到设备ID:MX29LV160DT: 22C4; MX29LV160DB: 2249 ( md.w 2 1)
结果如下:
如何退出读ID状态?
往任意地址写入:0xF0
结果如下:
例子3:读取CFI信息
查看芯片手册的描述:
进入CFI(common flash interface)模式:
往地址0x55 写入 0x98 进入CFI模式
查询芯片容量:
从0x10 读取一个字节数据得到0x51(Q)
从0x11 读取一个字节数据得到0x51(R)
从0x12 读取一个字节数据得到0x51(Y)
从0x27 读取一个字节数据得到内存大小。
由于设备位宽不同,所以在OpenJTAG的操作是:
往地址0xAA 写入 0x98 进入CFI模式 (mw.w aa 98)
从0x20 读取一个字节数据得到0x51(Q) (md.w 20 1)
从0x22读取一个字节数据得到0x51(R) (md.w 22 1)
从0x24 读取一个字节数据得到0x51(Y) (md.w 24 1)
从0x4E 读取一个字节数据得到内存大小。(md.w 4e 1)
结果如下:
退出CFI模式,往任意地址写0xf0
例子4:写数据
试一下在地址1M以外的地方写入数据0x1234,然后读出来
那如何写数据呢?需要一定的格式,如下手册所描述:
步骤:
①往地址 0x555写入数据0xAA
②往地址 0x2AA写入数据0x55
③往地址 0x555写入数据0xA0
④往指定Addr写Data
那么此时UBOOT怎么操作呢?
如果CPU需要左移一位,才是正确的地址,也就是是说,发送的地址需要乘上2,如下:
步骤:
①往地址 0xAAA写入数据0xAA (mw.w aaa aa)
②往地址 0x554写入数据0x55 (mw.w 554 55)
③往地址 0xAAA写入数据0xA0 (mw.w aaa A0)
④往地址 0x100000 写入数据 0x1234 (mw.w 100000 1234)
⑤读取地址0x100000的数据是否写入成功 (md.w 100000 1)
结果如下:
例子5:当原来的数据不是全f的时候
解锁后能写入成功的前提,必须是这个位置的数据时全f,也就是:ffff
再次往0x100000这个地址 ,写入一个新数据,是否能成功?
那如何写呢?如果原来的数据不是全f,那么需要先对这个位置进行擦除操作
芯片数据手册的描述如下:
擦除扇区0x100000数据的步骤步骤:
①往地址 0x555写入数据0xAA
②往地址 0x2AA写入数据0x55
③往地址 0x555写入数据0x80
④往地址 0x555写入数据0xAA
⑤往地址 0x2AA写入数据0x55
⑥往扇区0x100000写0x30
那么此时UBOOT怎么操作呢?
如果CPU需要左移一位,才是正确的地址,也就是是说,发送的地址需要乘上2,如下:
步骤:
①往地址 0xAAA写入数据0xAA (mw.w aaa aa)
②往地址 0x554写入数据0x55 (mw.w 554 55)
③往地址 0xAAA写入数据0x80 (mw.w aaa 80)
④往地址 0xAAA写入数据0xAA (mw.w aaa aa)
⑤往地址 0x554写入数据0x55 (mw.w 554 55)
④往地址 0x100000 写入数据 0x30 (mw.w 100000 30)
如下:
擦除成功后就可以正常的写数据了,和上面写的步骤是一样的。
3.编写NOR-Flash测试程序
在这个测试例程中有这几个功能如下:
/*菜单选项
*1.识别nor_falsh,设备和容量
*2.读取某个地址的数据
*3.擦除nor_flash扇区数据
*4.往某个地址写数据
*/
1).识别nor_falsh打印出各个扇区的起始地址,读取设备ID和厂家ID以及设备容量
由数据手册可知,需要查询设备容量,厂家ID和设备ID之前,需要进入CFI模式
那么进入CFI模式的方式如下所描述:往0x55的地址写入0x98
退出CFI模式,如下所描述:往任意地址写0xF0
进入CFI模式之后,可以查询的信息如下描述:
读取设备容量:读取地址0x27可以得到设备容量
在CFI模式下,可以查询 erase region的有关信息:
其中region的描述需要参考:CFI publication 100 规范,如下:
代码如下:
/*进入NOR_FLASH的CFI模式,读取设备信息*/
void Scan_nor_flash(void)
{
char qry[4]; //存储读取CFI模式下的QRY
int Manifacture_ID,Devide_ID;
int n,size;
int regions; //NOR_Flash的region个数
int i,j,count=0;
/*变量含义:
* 1.region的起始地址 2.block的个数
* 3.每个block的大小 4.block的起始扇区基地址
*/
int region_Bank_Area,blocks,block_size,block_base_addr;
/*读取QRY字符到数组qry*/
nor_cmd(0x55,0x98);/*进入CFI模式,往0x55写0x98*/
qry[0] = nor_read(0x10);
qry[1] = nor_read(0x11);
qry[2] = nor_read(0x12);
qry[3] = '\0';
printf("******************msg = %s\n\r" ,qry); //打印出QRY
nor_cmd(0,0xf0); //复位,退出CFI模式
/*打印nor_flash的厂家ID,设备ID*/
nor_cmd(0x555,0xaa);
nor_cmd(0x2AA,0x55); //解锁
nor_cmd(0x555,0x90); //读取厂家ID命令
Manifacture_ID = nor_read(0x00); //读取厂家ID
Devide_ID = nor_read(0x01); //读取设备ID
printf("******************The Manifacture_ID is: 0x%x\r\n",Manifacture_ID);
printf("******************The Devide_ID is: 0x%x\r\n",Devide_ID);
nor_cmd(0,0xf0); //复位
/*打印设备容量*/
nor_cmd(0x55,0x98);/*进入CFI模式,往0x55写0x98*/
n = nor_read(0x27);
size = 1<<n; //表示 2^n
printf("******************Device size:0x%x = %d bytes = %d M \r\n",size,size,size/(1024*1024));
/*打印各个扇区的起始地址*/
regions = nor_read(0x2c); //从0x2c的地址读出region个数
region_Bank_Area =0x2d;
block_base_addr=0;
printf("Block/Sector start Address:\r\n");
for(i=0;i<regions;i++)
{
/*读出这个region总共有多少个block*/
blocks=nor_read(region_Bank_Area)+(nor_read(region_Bank_Area+1)<<8)+1;
/*读取每个block的大小*/
block_size=(nor_read(region_Bank_Area+2)+(nor_read(region_Bank_Area+3)<<8))*256;
printf("================Region %d==============\n\r",i+1);
printf("number_of_blocks=%d, block_size=0x%x=%dk\n\r",blocks,block_size,block_size/1024);
/*打印每个block的起始地址*/
for(j=0;j<blocks;j++)
{
puthex(block_base_addr);
printf("\t");
block_base_addr +=block_size;
count++;
if(count % 5 == 0)
{
printf("\r\n");
}
}
printf("\n\r");
/*region地址自加4读取下一个region的信息*/
region_Bank_Area +=4;
}
nor_cmd(0,0xf0); //复位,退出CFI模式
printf("\n\r");
}
注意事项:进入CFI模式之后,退出函数时需要退出CFI模式,才能正常访问NOR_FALSH。
程序运行结果如下:
注意事项:
编译程序加上选项:-march=armv4 ,
否则像
volatile unsigned short *p =xxx;
*p=data的操作会被分成两个strb步骤(我们需要的是strh),最后导致读取CFI设备ID和厂家ID的时候出错。
2).读取nor_flash某个地址的数据
首先从键盘输入要读取的起始地址,接着输入读取的字节,为了方便起见,读取的字节数是16的整数倍。
代码如下:
unsigned short Read_nor_flash(void)
{
//读取的基地址
unsigned int addr;
//定义一个无符号字符型变量p
volatile unsigned char *p;
int i,j;
unsigned char c;
unsigned short amount,hex_addr=0;
/*保存数据*/
unsigned char str[16];
/*获取读取地址*/
printf("Enter Read Address: ");
/*根据用户输入的地址,结果给回addr
*可输入16进制和10进制
*/
addr = get_uint();
/*把addr强制转换成unsigned char 类型,表明*p只读取一个字节的数据*/
p = (volatile unsigned char *)addr;
printf("Enter Read amount(16 times): ");
amount = get_uint();
printf("Read Data:\n\r");
printf(" 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f\n\r");
/*读取长度:64字节*/
for(i=0;i<amount;i++)
{
printf("0x%08x ",hex_addr);
/*每行打印16字节数据*/
for(j=0;j<16;j++)
{
/*先打印16进制*/
c = *p++; //读取第一个字节的数据,然后p指向下一个字节
str[j] = c; //先保存数值
printf("%02x ",c); //打印出当前字节的16进制
}
printf(" ; ");
for(j=0;j<16;j++)
{
/*打印对应的字符*/
if(str[j]<0x20 || str[j]>0x7e) /*不可视字符,打印出一个点*/
{
putchar('.');
}
else
putchar(str[j]); //可视字符,直接打印
}
hex_addr+=16;
printf("\n\r");
}
}
加入我们现在需要读取的是从基地址(0地址)开始的160字节的数据,那么从键盘输入的地址就是0,读取数量就是10,结果如下:
我们读取的nor_flash上前160字节的数据,对比一下nor_flash.bin文件的数据是否一致:
可以看出来结果完全一致,说明读取nor_flash的数据时成功的。
注意:
ascii的可打印字符:ASCII字符集由95个可打印字符(0x20-0x7E)
3).擦除nor_flash扇区数据
查看数据手册中如何擦除数据:
代码如下:
void Erase_nor_flash(void)
{
//擦除的地址
unsigned int addr;
/*从键盘获取删除地址*/
printf("Enter Erase Address: ");
addr = get_uint();
printf("erasing ... ");
nor_cmd(0x555,0xaa); /*解锁*/
nor_cmd(0x2aa,0x55);
/*注意此处擦除的是一整个扇区,擦除的最小单位就是
*一个扇区的大小,此处总共有35个扇区
*/
nor_cmd(0x555,0x80); /*擦除扇区*/
nor_cmd(0x555,0xaa); /*解锁*/
nor_cmd(0x2aa,0x55);
/*因为nor_Flash是16bit的,所以发送地址,需要右移一位*/
nor_cmd(addr>>1,0x30);
wait_ready(addr); /*等待擦除完成*/
printf("Erase finished!\n\r");
}
4)往某个地址写数据
查看数据手册如何写数据:
需要注意一个点,写入的数据是16位的,也就是两个字节,需要一次性写入一个字节,所以在写入之前需要对数据进行整合。
如何判断烧写完成,以便于进行下一次的烧写工作:
代码如下:
void Write_nor_flash(void)
{
//写数据的地址
unsigned int addr;
unsigned char write_data[100];
int i=0,j=1;
unsigned int data;
/*从键盘获取写数据起始地址*/
printf("Enter Write Address: ");
addr = get_uint();
printf("Enter Write data: ");
gets(write_data);
printf("writing... ");
/*把write_data的数组构造16bit的数据*/
/*如果数组有数据*/
while(write_data[i] && write_data[j])
{
//构造一个16bit的数据
data = write_data[i]+ (write_data[j]<<8);
/*写入数据*/
nor_cmd(0x555,0xaa); /*解锁*/
nor_cmd(0x2aa,0x55);
/*注意只有当数据为f时才能直接些,否则必须先擦除*/
nor_cmd(0x555,0xa0); /*写命令*/
/*往地址写数据*/
nor_cmd(addr>>1,data);
/*等待烧写完成:读数据Q6无变化则烧写完成*/
wait_ready(addr);
i +=2;
j +=2;
addr +=2;
}
/*IF write_data[i]!=0 write_data[j]=0
* 说明还有数据,此时:
* data = write_data[i]+ (write_data[j]<<8);
*/
/*IF write_data[i]==0 write_data[j]!=0
* 说明已经结束了,0是结束符‘\0’
* data = write_data[i]+ (0<<8); 此时data =0
*/
/*IF write_data[i]==0 write_data[j]==0
* 说明已经结束了,0是结束符‘\0’
* data = write_data[i]+ (0<<8); 此时data =0
*/
/*已上三种情况都需要再写一个数据
*为了方便和保险起见,无论是三种的哪一种
*都让 data = write_data[i]; 把结束符写进去
*/
//构造一个16bit的数据
data = write_data[i];
/*写入数据*/
nor_cmd(0x555,0xaa); /*解锁*/
nor_cmd(0x2aa,0x55);
/*注意只有当数据为f时才能直接些,否则必须先擦除*/
nor_cmd(0x555,0xa0); /*写命令*/
/*往地址写数据*/
nor_cmd(addr>>1,data);
/*等待烧写完成:读数据Q6无变化则烧写完成*/
wait_ready(addr);
printf("Write finished!\n\r");
}
此处注意还需要加一个判断数据是否写完成的函数,当一次行写入16bit时,等待写入完成再写下一个16bit,依次类推:
函数如下:
void wait_ready(unsigned int addr)
{
unsigned int pre_val;
unsigned int current_val;
pre_val = nor_read(addr>>1); //先读取一次
current_val= nor_read(addr>>1); //再次读取
/*两次读取的结果不一致,说明数据还在变化,继续等待*/
while((pre_val&(1<<6)) != (current_val&(1<<6)))
{
pre_val=current_val; //当前的值等于上一次的值
/*再次读取:Q6 位*/
current_val = nor_read(addr>>1);
}
}
上面的代码已经完成了所有功能了,接下来就是写数据进行测试了。
通过上面的例子已经知道,这个NOR_flash的大小是2M,共有4个region,而且每一个region的block数量以及block的大小是不同的。如下所示:其中地址是每个block的起始地址
注意:在写数据时,只有当数据为全f时,才能写入,因为规定,数据的bit只能从1变成0,而不能从0变成1,所以当数据不为全f时,需要对擦除对应的block,而且每次擦除的最小单位是block,比如,擦除region1的block,那么一次性擦除16k的数据,擦除region2的block,一次性擦除8k的数据。
现在读取地址:0x001D0000 开始,160 byte的数据,如下:
直接写入,写入数据:hello world !
往地址:0x001Daaaa 的位置写入,hello yuan (也就是在这个block的末尾的位置)
现在擦除地址:0x001D0000的数据,查看数据是否还在:
这也证明了,擦除操作是针对一个扇区。
源代码
数据手册