s3c2440裸机编程-内存控制器

1 内存接口概念#

1.1 不同类型的控制器#

S3C2440是个片上系统,有GPIO控制器(接有GPIO管脚(GPA-GPH)),有串口控制器 (接有TXD RXD引脚),有memory controller内存控制器,有Nand控制器等…

1
2
3
4
1)GPIO控制器属于门电路,不涉及到时序,相对简单。
2)串口控制器属于协议类接口,类似的协议类接口还有iic、iis、spi等。
3)前面的GPIO/门电路接口、协议类接口,都不会把地址输出到外部设备,仅仅只是将地址写入到相应的控制器。
接下来的内存类接口,会把地址输出到外部,cpu将地址写入内存控制器,内存控制器还需访问外部设备,比如NorFlash、网卡、SDRAM。

1.2 如何访问控制器地址#

GPIO/门电路接口、协议类接口、内存类接口都属于CPU的统一编址。但对于Nand Flash,它没有独立的地址线和cpu的地址总线相连接,因此它不参与CPU的统一编址。

1.2.1 门电路/协议类控制器#

对于门电路接口、协议类接口,直接访问寄存器即可。

1.2.2 内存类控制器#

对于内存类接口,交给内存控制器去处理。下面详细分析:

CPU只管发出一个地址,内存控制器根据该地址范围选择不同的模块,然后从模块中得到数据或者发送数据到模块中。
如下图,SDRAM、DM9000网卡、Nor Flash都接在s3c2440的数据总线和地址总线上,CPU把数据和地址发送出去,然后内存控制器根据地址范围确定要拉低选中哪个片选信号(nCS),再根据片选信号(nCS)选择相应的设备,进行收发地址和数据,互不干扰。

image-20240411202307277

1
2
3
4
5
6
(1)当CPU发出的指令的地址范围处于0x00000000 - 0x08000000
内存控制器就会使nGCS0处于低电平(片选引脚被选中),NorFlash被选中。(NorFlash启动时才行,nand启动时nGCS0这块对应SRAM)
(2)当CPU发出的指令的地址范围处于0x20000000 - 0x28000000
内存控制器就会使nGCS4处于低电平(片选引脚被选中),网卡DM9000被选中。
(3)当CPU发出的指令的地址范围处于0x30000000 - 0x38000000
内存控制器就会使nGCS6处于低电平(片选引脚被选中),SDRAM被选中

内存控制器根据不同的地址地址范围,发出不同的片选引脚,只有被片选引脚选中的芯片才能正常工作,不被选中的芯片就像不存在一样,不工作。

从s3c2440 datasheet中我们得知内存控制器可访问的地址范围有1G(0x0000,0000-0x4000,0000),8个bank,每个bank_size为128M。理论上需要2^30(30条地址线)来确定是哪个bank,哪个地址。但是实际上只用到了27条,那么是怎么确定是哪个bank被选中了呢?

cpu每次发给内存控制器的地址都是Addr[31:0],但是内存控制器发给外设(sdram,nor,dm9000)却只用到了A[26:0]。第[29:27]被用来确定要拉低哪个nGCS,即要选中哪个bank:

1
2
3
4
5
6
bit[29:27]		bit[26:0]
0b000 -> 0x000,0000-0x7ff,ffff (bank0被选中 (128M))
0b001 -> 0x800,0000-0xfff,ffff (bank1被选中 (128M))
... ...
0b110 -> 0x000,0000-0x7ff,ffff (bank6被选中 (128M))
0b111 -> 0x800,0000-0xfff,ffff (bank7被选中 (128M))

那么对于大容量的nandflash,理论上需要更多的地址线来确认访问地址,那既然没有地址线,cpu是如何访问nand的呢?当然是通过nand控制器,nand是地址、命令、数据都共用数据总线。这里只是引入一个话题,具体见s3c2440裸机-nand控制器

2 不同位宽内存与CPU地址连接关系#

s3c2440芯片手册上外设rom与CPU地址总线连接如下:

2.1 8bit x1 rom与CPU地址线连接#

2.2 8bit x2 rom与CPU地址线连接#

2.3 8bit x4 rom与CPU地址线连接#

2.4 16bit x1 rom与CPU地址线连接#

2.5 16bit x2 rom与CPU地址线连接#


从上面的图中,我们知道可以对2片位宽为8bit的内存扩展级联成1个16bit的内存,同理可用4片位宽为8bit的内存进行级联成1个32bit的内存。

3 不同位宽内存与CPU为什么要错位相连#

从上面的图中,我们还看见一个规律:

1
2
3
4
5
6
7
8
当外设总线位宽为8bit时, 外设A0接CPU的地址总线ADDR[0],
A[1]->ADDR[1] ...A[15]->ADDR[15]

当外设总线位宽为16bit时,外设A0接CPU的地址总线ADDR[1],
A[1]->ADDR[2] ...A[15]->ADDR[16]

当外设总线位宽为32bit时,外设A0接CPU的地址总线ADDR[2],
A[1]->ADDR[3] ...A[15]->ADDR[17]

为什么要这样设计呢?先看一个例子:

1
2
MOV R0, #3
LDRB R1, [R0] @ 从内存地址为3的地方,读出一个字节到R1

如图有8bitROM、16bitROM、32bitROM:

1
2
3
4
5
6
7
8
9
10
11
(1)对于8bitROM ,8bit是一次读写的最小单位,即0地址是第一个8bit,1地址是第二个8bit;
CPU发出地址3,即A0和A1都为1,8bitROM的A0和A1收到的也都是1,
于是找到了ROM上地址为3的8bit数据,包含了我们需要的数据。

(2)对于16bitROM ,16bit是一次读写的最小单位,即0地址是第一个16bit,里面有两个8bit数据;
CPU发出地址3,即A0和A1都为1,16bitROM的A0和A1分别收到的是1和0,
于是找到了ROM上地址为1的16bit数据,包含了我们需要的数据,最后内存控制器再帮我们挑选出所需的8bit数据。

(3)对于32bitROM ,32bit是一次读写的最小单位,即0地址是第一个32bit,里面有四个8bit数据;
CPU发出的地址3,即A0和A1都为0,32bitROM的A0和A1收到的都是0,
于是找到了ROM上地址为0的32bit数据,包含了我们需要的数据,最后内存控制器再帮我们挑选出所需的8bit数据。

用表格更好理解:

ROM/bit CPU发出地址 ROM收到地址 ROM返回数据 内存控制器挑选出数据给CPU
8bit(ROM) 0b000011 0b000011 编号3的存储单元中的8数据 编号3的存储单元中的8数据
16bit(ROM) 0b000011 0b000001 编号1的存储单元中的16数据 根据”A0=1”,挑出低8bit数据
32bit(ROM) 0b000011 0b000000 编号0的存储单元中的32数据 根据”A1A0=11”,挑出最低8bit数据

对上图的数据再次整理:

ROM/bit CPU发出地址 ROM收到地址(内存控制器转发给rom) ROM返回数据 内存控制器组装数据给CPU
8bit(ROM) 0b000100 0b000100 地址4的一个1byte数据 组装地址7、6、5、4数据成4字节数据
0b000101 地址5的一个1byte数据
0b000110 地址6的一个1byte数据
0b000111 地址7的一个1byte数据
16bit(ROM) 0b000100 0b00010 地址2的一个2byte数据 组装地址3、2的数据成4字节数据
0b00011 地址3的一个2byte数据
32bit(ROM) 0b000100 0b00001 地址1的一个4byte数据 直接返回4字节数据

这里牵扯到地址、内存中数据的排列存储,有点深入,如果实在无法理解,记住怎么去错位相连就好了。
结论:

1
2
1.和cpu地址总线相连的外设地址线确定了要访问外设的地址,即哪个存储单元;
2.然后内存控制器拿到外设存储单元中的数据后,再根据那几个错开的引脚[A1-A0]的值(CPU地址总线没接的那几个引脚的值),来挑出相应的数据给CPU。

再举一个例子, 假如读取一个32位的数据时,前面读的是8位数据:

1
2
MOV R0,   #4
LDR R1, [R0] @去地址4,读取4字节数据

我们知道CPU发出的是32bit地址,那么

1
2
3
对于32bit Rom,内存控制器会给它发1次,rom也会相应的接收1次;
对于16bit Rom,内存控制器会给它发2次,rom也会相应的接收2次;
对于8bit Rom,内存控制器给它发4次, rom接收4次,

3.1 配置内存控制器位宽#

接不同的rom外设,s3c2440内存控制器总线位宽要配置成不一样。位宽和等待控制寄存器如下:
BWSCON(BUSWIDTH&WAITCONTROLREGISTER):

img

我们SDRAM的位宽为32,DW6[25:24]设置成10, 没有使用等待信号,所以WS6[26]=0。 bank7跟随bank6的配置, 因此BWSCON寄存器的值为:0x22000000

4 内存控制器地址映射范围#

怎样确定芯片的访问地址?

1
2
1. 根据片选信号确定基地址
2. 根据芯片所接地址线确定范围

外设类型 接内存控制器的哪个片选 基地址 占用CPU的地址总线 地址范围(offset + size)
nor nGCS0 0x0000,0000 ADDR0-ADDR20 0x0000,0000 ~ 0x001f,ffff(2M)
dm9000网卡 nGCS4 0x2000,0000 ADDR0和ADDR2 0x2000,0000 ~ 0x2000,0005(5byte)
sdram nGCS6 0x3000,0000 ADDR0-ADDR25 0x3000,0000 ~ 0x3000,0000 + RAM_SIZE

这里再次提醒一下: 有人发现上图中nor没有和CPU的ADDR0相连接,sdram没有和CPU的ADDR0、ADDR1相连接。不要觉得ADDR0、ADDR1没用到,由于nor数据位宽是16bit,ADDR0是给内存控制器拆分数据用的,同样sdram数据位宽32bit,ADDR0、ADDR1也是给内存控制器拆分数据用的。这个上面已分析过,这也是什么要错位连接的原因。

5 SDRAM访问实例#

以EM63A165TS-6G这款外接SRAM存储来展开介绍。

5.1 SDRAM存储结构#

5.2 SDRAM引脚接线#

这里采用2片 EM63A165TS-6G 级联作为外接内存,关于EM63A165TS-6G的规则描述参考datasheet。可以看到该sdram是16bit 的,从接线可以看出第一片存储低16位数据,第二片存储高16位数据。

引脚说明:

1
2
3
4
5
6
7
8
9
A0-A12:地址总线
D0-D15:数据总线(位宽16,2片级联成位宽32
BA0-BA1:bank选择
nSCS:片选
nSRAS:行地址选择
nSCAS:列地址选择
nWE:写使能
SCLK:时钟
SCKE:时钟使能

5.3 SDRAM地址范围#

前面提到片选接了nGCS6,地址映射的base_addr=0x3000,0000,那么size是多大呢?

容量为:4M word x 16-bit x 4-bank = 32M,再看原理图我们是两片级联,所以容量为4M word x 32-bit x 4-bank=64M。所以地址范围是**[0x3000_0000 ~ 0x33ff_ffff]**

在对比另一款W9825G6KH SDRAM为例,地址总线A0-A12,数据总线D0-D15,因此内存大小:2^13 * 2^9 = = 4194304,等于4M,加上有4个bank,数据为宽16位,因此内存大小4Mx4x2字节。

image

image

框图翻译成中文形式:

image

1
2
3
4
5
6
7
8
9
10
CLK:时钟线,SDRAM 是同步动态随机存储器,“同步”的意思就是时钟,因此需要一根额外的时钟线,这是和 SRAM 最大的不同,SRAM 没有时钟线。
CKE:时钟使能信号线,SRAM 没有 CKE 信号。
CS:片选信号,这个和 SRAM 一样,都有片选信号。
RAS:行选通信号,低电平有效,SDRAM 和 SRAM 的寻址方式不同,SDRAM 按照行、列来确定某个具体的存储区域。因此就有行地址和列地址之分,行地址和列地址共同复用同一组地址线,要访问某一个地址区域,必须要先后发送行地址和列地址
CAS:列选通信号,和 RAS 类似,低电平有效,选中以后就可以发送列地址了。
WE:写使能信号,低电平有效
A0-A12: 地址线
DQ0-DQ15: 16位数据线
BS0-BS1: BANK 选择线
LDQM,UDQM: 高低字节数据选择线

5.4 SDRAM数据访问过程#

我们知道64M=2^20*2^6=2^26,那么需要26条地址线,再看看原理图,我们发现SDRAM的地址线A[12:0]只有13条,那么最多只能访问2^13=8K的数据,地址线明显配不上这么大的容量,那么它是如何解决的呢?

答:当然是拆分地址了,多次传输

我们从SDRAM的内部存储结构得知要确定SDRAM的一个存储单元,先确定是哪个bank,然后再确定在哪一行、哪一列即可。SDRAM有4个bank,由BA0、BA1决定选中哪个bank,查看SDRAM手册见下图:

通过选中nSRAS选中行地址,从而发送行地址;最后通过选中nSCAS选中列地址,从而发送列地址。例如:

1
2
ldr	r0, =0x30000000;
ldr r1, [r0]; 从SDRAM基地址读取4byte数据

过程如下:

1
2
1.发出片选信号nGCS6,选中SDRAM
2.发出bank选中信号(BA0,BA1),确定是SDRAM上的哪个bank,从原理图得知CPU的ADDR24、ADDR25对应SDRAM上的BA0,BA1,所以让BA0、BA1=00,选中bankA。

然后从sdram规格书确定行列地址的数目:

1
2
3.发出行地址信号nSRAS,使能行地址传输。传输行地址,确定是哪一行(看SDRAM手册确定行地址数(A12-A0)13条)
4.发出列地址信号nnSCAS,使能列地址传输。传输列地址,确定是哪一列(看SDRAM手册确定列地址(A8-A0)9条)

从而发送完整的0x30000000地址到了SDRAM,SDRAM返回4byte数据给CPU。

5.5 SDRAM驱动实例#

s3c2440内存控制器共有13个寄存器。我们要设置内存控制器参数,适配外接SDRAM。

1
2
BANK0--BANK5只需要设置BWSCON和BANKCONx(x为0~5)两个寄存器;
BANK6、BANK7外接SDRAM时,除BWSCON和BANKCONx(x为6、7)外,还要设置REFRESH、BANKSIZE、MRSRB6、MRSRB7等4个寄存器。

5.5.1 位宽寄存器#

BWSCON(BUSWIDTH&WAITCONTROLREGISTER)

我们SDRAM的位宽为32,DW6[25:24]设置成10, 没有使用等待信号,所以WS6[26]=0。 bank7跟随bank6的配置, 因此BWSCON寄存器的值为:0x22000000。

5.5.2 BANK控制寄存器#

BANKCONTROLREGISTER:

在8个BANK中,只有BANK6和BANK7可以外接SRAM或SDRAM。BANKCON6设置参数如下:

1
2
MT[16:15]:设置BANK是ROM/SRAM还是DRAM,我们用的SDRAM,属于DRAM。
Trcd[3:2]:行地址和列地址间隔多长时间,看SDRAM芯片手册时间间隔Trcd>18ns,我们HCLK=100MHZ,clocks为10ns,所以设置为2clocks即可。

5.5.3 刷新控制寄存器#

REFRESH(REFRESHCONTROLREGISTER)

1
2
3
4
5
6
7
8
9
REFEN[23]:设置开启SDRAM的刷新功能。
TREFMD[22]:SDRAM的刷新模式,0=CBR/AutoRefresh,选择自动刷新。

Trp[2120]:根据sdram手册Trp>18ns, 设为0(2 clocks)即可。
Tsrc[1918]: Tsrc = Trc - Trp = Trc-20, 根据sdram手册Trc>=60,我们取Trc =70, 则Tsrc= 50ns(5clocks)即可。
RefreshCounter[10:0]:Refresh period = (211-refresh_count+1)/HCLK,
RefreshCount = 211 + 1 - 100*Refresh period,看SDRAM手册“8192 refresh cycles/64ms”, Refresh period= 64000us/8192 = 7.8us,
RefreshCount取推荐值1269= 0x4f5.
综上,REFRESH寄存器设为0x8404F5

5.5.4 BANKSIZE寄存器#

BANKSIZEREG ISTER

1
2
3
4
5
BURST_EN[7]:0=ARM核禁上突发传输,1=ARM核支持突发传输(推荐);
SCKEEN[5]:0=不使用SCKE信号令SDRAM进入省电模式,1=使用SCKE信号令SDRAM进入省电模式(推荐);
SCLK-EN[4]:0=时刻发出SCLK信号,1=仅在访问SDRAM期间发出SCLK信号(推荐);
BK76MAP[2:0]:配置banksize成64M
因此,BANKSIZE寄存器设为0xB1

5.5.5 模式设置寄存器#

SDRAM MODE REGISTER SET REGISTER(MRSR)

1
2
CL[6:4]:表示发出行、列地址后,等多久才返回收到数据, 看SDRAM手册发现Tcas >=18ns,所以配置成2 clocks即可。
MRSRB6寄存器设置为0x20

5.5.6 测试代码#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void sdram_init(void) {
BWSCON = 0x22000000;
BANKCON6 = 0x18001;
BANKCON7 = 0x18001;
REFRESH = 0x8404f5;
BANKSIZE = 0xb1;
MRSRB6 = 0x20;
MRSRB7 = 0x20;
}

int sdram_test(void) {
volatile unsigned char *p = (volatile unsigned char *)0x30000000;//sdram base addr
int i;
// write sdram
for (i = 0; i < 1000; i++)
p[i] = 0x55;

// read sdram
for (i = 0; i < 1000; i++)
if (p[i] != 0x55)
return -1;

return 0;
}
int main(void) {
uart0_init();
sdram_init();

if (sdram_test() == 0)
led_test();
return 0;
}

当进行sdram_init后可已访问0x3000_0000地址的内容,led流水灯闪烁。
不初始化sdram_init,sdram_test执行会导致程序卡死。

6 NorFlash访问实例#

6.1 Flash种类特性介绍#

flash一般分为nand flash和nor flash,各自特性如下:

Nor NAND
XIP(片上执行) yes no
性能(擦除) 非常慢(5s,块太大) 快(3ms)
性能(写)
性能(读)
可靠性 一般(容易出现位反转)
可擦除次数 10000 ~ 100000 100000 ~ 1000000
接口 与ram类似,可直接访问任意地址 I/O接口(无地址线,必须串行访问,命令、地址、数据共用8位IO)
易用性 容易 复杂
主要用途 常用于保存代码和关键数据 用于保存数据
价格
容量
常用文件系统类型 jffs yaffs

nor有以下优缺点相对nand:

1
2
3
4
5
6
7
8
优点:
操作简单(可以像内存一样随机访问)
读取速度快
可靠性高,不易出现位反转
缺点:
容量小,价格贵
擦写慢
寿命短

6.2 NorFlash地址范围#

前面介绍内存控制器地址映射范围说了,得知nor接了bank 0,地址范围是0x0000,0000 ~ 0x001f,ffff。

6.3 NorFlash引脚描述#

下面是一款典型的nor flash原理图MX29LV800BBTC

引脚信息:

1
2
3
4
地址线(A0-A20)
数据线(DQ0-DQ15)
片选信号(nCE)
读写使能信号(LnOE/LnWE)

Nor Flash可以像内存一样读,但是不能像内存一样写,需要做一些特殊的操作才能进行写操作,这是因为nor是属于rom(只读存储器),不能像ram一样可以任意的写0写1,只能将存储介质中的电平由1变成0,不能将0变成1,所以要向nor中写入数据,必须先进行擦除动作。

6.4 NorFlash硬件连接#

6.5 NorFlash数据访问过程#

下图是S3C2440的内存控制器的可编程访问周期读写时序,里面的时间参数要根据外部norflash的性能进行配置。

时序含义:

1
2
3
4
5
6
Tacs: Address set-up time before nGCSn(表示地址信号A发出多久后才能发出nGCS片选)
Tcos: Chip selection set-up time before nOE(表示片选信号nGCS发出多久后才能发出读使能信号)
Tacc: access cycle(数据访问周期)
Tacp: page模式下的访问周期
Tcoh: Chip selection hold time after nOE (nOE信号释放多久后才能释放片选nGCS)
Tcah: Address hold time after nGCSn (片选nGCS释放多久后才能释放地址信号A)

下面我们根据此款norflash MX29LV160D手册中的访问时序图来分析,如下图:

从上面MX29LV160D手册的时序图中我们看见:

1
2
3
4
5
(1)先发送地址信号A
(2)发送片选CE
(3)发送读使能OE
(4)从数据总线上读出数据
(5)释放信号...
1
2
3
发出地址数据(Addresses)后,要等待Taa(要求大于等于70ns)时间,地址数据才有效;
发出片选信号(CE#)后,要等待Tce(要求大于等于70ns)时间,片选信号才有效;
发出读信号(OE#)后要等待Toe(要求大于等于30ns)时间,读信号才有效;

Tas(地址建立时间,也就是地址发送多久后才能继续发后面的片选信号)最小可以为0,那么说明地址信号(A)、片选(CE)、读(OE)使能信号可以一起发出。

为了简单我们把地址(Addresses),片选信号(CE#),读信号(OE#),同时发出,然后让它们都等待70ns即可(等待地址信号,片选信号,读写使能信号有效)。

我们再看看上面的nor访问时序图,释放地址、片选、读使能信号都没有时间差值dt要求,那么说明地址、片选、读使能信号可以同时释放。

6.6 NorFlash时序初始化#

打开s3c2440内存控制器。

6.6.1 BANK控制寄存器设置#

6.6.1.1 内存控制器时序设置#

6.6.1.1.1 Tacc#

Tacc表示数据访问周期:

从上图可以看到Tacc的默认值是111,对应14个clocks。s3c2440系统上电采用12MHz的晶振,HCLK=OSC=12MHz,那么Tacc=(1/(12*10^6)) * 14≈1166 ns,这个值很大,远超过了我们的nor手册上的Trc=70ns,几乎可以满足所有NorFlash的要求,这也是为什么我们不做初始化也能访问norflash的原因。

启动后,由于我们的时钟HCLK设置成了100MHz,T=1000/100=10ns,Tacc= 10ns*14 >70ns, 所以内存控制器不配置Tacc也是能访问该flash的。为了让访问速率加快,因此设置Tacc>70ns即可,配置成101,8个clocks即可。

6.6.1.1.2 Tacs/Tcos/Tcoh/Tcah#

从nor的分析中,我们得知地址、片选、读使能同时发出和同时释放,所以配置Tacs,Tcos,Tcoh,Tcah皆为0。

1
2
3
4
5
BANKCON0 = (*(volatile unsigned long *)(0x48000004));
void bank0_tacc_set(int val) {
BANKCON0 = val << 8;
}
bank0_tacc_set(0x5);

6.6.2 测试代码#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(void) {
unsigned char c;
uart0_init();//参考前面的uart编程
puts("Enter the Tacc val: \n\r");
while(1) {
c = getchar();
putchar(c);
if (c >= '0' && c <= '7') {
bank0_tacc_set(c - '0');
led_test();//跑马灯代码我就不贴了,谁都会
} else{
puts("Error, val should between 0~7\n\r");
puts("Enter the Tacc val: \n\r");
}
}
return 0;
}

6.6.2.1 测试结果#

输入0~4,Tacc小于70ns,无法读取Nor Flash上数据,LED不能闪烁。

输入5~7,Tacc大于70ns,可以读取Nor Flash上数据,LED不断闪烁,且值越小越快。

结论:我们的内存控制器默认配置的tacc一般都能兼容大多数市面上的norflash,一般都是可以访问的,无需进行对内存控制器进行多余的配置。

7 u-boot命令访问NorFlash#

前提
norflash初始化正常,能够正常从nor上执行。

对s3c2440而言,cpu总是从0地址读取指令执行程序。当cpu设置成nor启动时,0地址对应nor。cpu从nand启动时,0地址对应sram。

7.1 操作NorFlash#

将板子设为nor启动,那么0地址对应nor,我们先将uboot烧写到nor中,启动uboot。

打开这款MX29LV800BBTC norflash手册,找到操作flash的命令表:

7.1.1 reset#

往任何一个地址写入F0即可。

7.1.2 读ID#

很多的Nor Flash可以配置成位宽16bit(Word),位宽8bit(Byte),我们这款norflash数据位宽为16bit。下面我们按照nor手册上的命令表尝试一下:

1
2
3
4
5
6
往地址555H写入AAH(解锁)	
往地址2AAH写入55H(解锁)
往地址555H写入90H(命令)
0地址得到厂家ID(C2H)
1地址得到设备ID(22DAH或225BH)
退出读ID状态(给任意地址写F0H就可以了)

上面的地址是对于norflash的,那么我们CPU要怎么发送地址呢?从原理图接线我们知道CPU和nor的地址是错位相连的。

cpu地址 nor地址
A15~A1 A14~A0

那么可以看到cpu的地址实际相当于是nor地址左移了一位,那么比如要想给nor上的555H地址写入AAH,那么CPU要发出的地址应该为0x555<<1,也就是nor地址的2倍。

下面对在Nor Flash的操作,cpu的操作,U-BOOT上的操作进行比较,如下表:

Nor Flash的操作 cpu的操作 U-BOOT上的操作
往地址555H写入AAH(解锁) 往地址AAAH写入AAH(解锁) mw.w aaa aa
往地址2AAH写入55H(解锁) 往地址554H写入55H(解锁) mw.w 554 55
往地址555H写入90H(命令) 往地址AAAH写入90H(命令) mw.w aaa 90
读0地址得到厂家ID(C2H) 读0地址得到厂家ID(C2H) md.w 0 1 (1:表示读一次)
读1地址得到设备ID(22DAH或225BH) 读2地址得到设备ID(22DAH或225BH) md.w 2 1
退出读ID状态(给任意地址写F0H) 退出读ID状态(给任意地址写F0H) mw.w 0 f0

我们读出厂家id为c2,设备id为2249,和我们的nor手册上是一致的。我们发出f0命令,进行复位,这时读取的数据就不再是厂家id和设备id了,而是我们norflash中的实际的数据17 00 00 ea。

7.1.3 读数据#

前面说了,nor属于rom, 有独立地址线,可以像ram一样的读,只要做好内存控制器的初始化工作就可以直接读了。

我们再用二进制编辑器打开我们烧进去的uboot.bin,发现内容一样,说明我们从norflash中读出来的数据是正确的。

7.1.4 读属性#

通常Linux内核里面要识别一个 Nor Flash 有两种方法:

一种是 jedec 探测,就是在内核里面事先定义一个数组,该数组里面放有不同厂家各个芯片的一些参数,探测的时候将 flash 的 ID 和数组里面的 ID 一一比较,如果发现相同的,就使用该数组的参数。 jedec 探测的优点就是简单,只要通过flash的数组编号,即可访问该款flash属性,缺点是如果内核要支持的 flash 种类很多,这个数组就会很庞大。

一种是 CFI(common flash interface)探测,就是直接发各种命令来读取芯片的信息,比如 ID、容量等,芯片本身就包含了电压有多大,容量有有多少等信息。

我们的这款norflash属于cfi探测,下面对在Nor Flash上操作,s3c2440上操作,U-BOOT上进行cfi 探测(读取芯片信息)。

下图是从datasheet中检索出进入cfi模式后的一些flash属性查找表,可以按照表格命令查询norflash的一些属性(容量、电压、block信息等):

1
2
3
1.根据命令表往55H地址写入98H进入cfi模式
2.读取`qry`字符
3.获取属性
Nor Flash上操作cfi 2440上操作cfi U-BOOT上操作cfi
往55H地址写入98H(进入cfi模式) 往AAH地址写入98H mw.w aa 98
读地址10H得到0051(’q’) 读地址20H得到0051 md.w 20 1
读地址11H得到0052(‘r’) 读地址22H得到0052 md.w 22 1
读地址12H得到0059(‘y’) 读地址24H得到0059 md.w 24 1
读地址27H得到容量 读地址4EH得到容量 md.w 4e 1
读地址1BH得到VCCmin 读地址36H得到VCCmin md.w 36 1

从测试结果我们看到容量为2^21=2M,Vcc最小提供电压是2.7v。

7.1.5 写数据#

前面说了,nor属于rom, 有独立地址线,可以像ram一样的读,用md命令直接读取,不能像内存一样直接写,不信我们试试:

  1. 我们在Nor Flash地址0x10000读数据

    1
    由于我们的uboot只有162k,烧录到norflash后,norflash上的的0x100000地址还没有被写入数据,norflash的容量为2M(0~0x200000),所以读取NorFlash的0x10000的地址数据是0xffff...
  2. 在Nor flash的0x10000地址写数据0x1234,然后在这个地址读出数据:

    可以看到0x1234无法写进去,读出来还是0xfffff。为什么呢?要怎么才能将0x1234写进去。找到命令表:

    Nor Flash上写操作 2440上写操作 U-BOOT上写操作
    往地址555H写AAH(解锁) 往地址AAAH写AAH(解锁) mw.w aaa aa
    往地址2AAH写55H(解锁) 往地址554H写55H(解锁) mw.w 554 55
    往地址555H写A0H 往地址AAAH写A0H mw.w aaa a0
    往地址PA写PD 往地址0x100000写1234h mw.w 100000 1234

可以看到0x1234已被写入到地址0x100000。再次往0x100000地址处,写入0x5678:

这时我们发现0x100000地址处的数据不是0x5678,而是0x1230,为什么?

1
2
3
4
5
6
7
8
原因:flash有无法位反转的特性。
具体分析:
由于原来的数据已经是0x1234不是全0xffff,flash存储介质中只能1变成0,不能将0变成1。所以在0x1234的基础上不去擦出直接继续去写0x5678是会有问题的。
如果将0x1234不擦就去写成0x5678,过程如下:
0001 0010 0011 0100(0x1234)
0101 0110 0111 1000(0x5678)
----------------------------
0001 0010 0011 0000(0x1230)

所以得到就是0x1230, 因此flash写入前一定要先擦除。

7.1.5.1 擦除#

从datasheet找到擦除命令表:

Nor Flash擦操作 u-boot擦操作
往地址555H写AAH mw.w aaa aa
往地址2AAH写55H mw.w 554 55
往地址555H写80H mw.w aaa 80
往地址555H写AAH mw.w aaa aa
往地址2AAH写55H mw.w 554 55
往地址PA写30H mw.w 100000 30

擦除后再读取发现数据就已经变成了0xffff,后面就可以进行写操作了。

7.1.5.2 写入#

找到写入命令表,进行写入:

现在数据就变成我们的0x5678了。

注意:在写norflash时,要注意不要写0地址或者是uboot所在的地址,这样写入后norflash上的uboot程序就被破坏了。比如本测试就是写了0x100000地址,这个地址在uboot之外。

7.2 操作NorFlash-拓展#

7.2.1 地址位宽不对齐导致死机分析#

uboot发送md.w 0, md.w 2, md.w 4等偶地址命令能够读取norflash,但使用md.w 1, md.w 3,md.w 5就会出现死机,为什么?

1
由于我们的norflash是16bit数据位宽的,访问时要2byte对齐。如果不想以2byte为单位进行访问,那么要用uboot中用md.b 1,md.b 3这种单字节读取命令。

7.2.2 每次写都要先擦除#

操作norflash进行擦写的时候能够解锁一次,擦写多次吗?

1
不能,每次擦写都要进行解锁动作。

image-20240412144608560

7.2.3 擦除单位-块#

擦除那么是以块(block)为单位的,那么当进行擦除时发送的地址并不是以块对齐的,会有什么结果?

也能擦除成功,会根据地址范围确定在哪一个块中。

填入的地址是0x100009,也是擦除0x100000地址对应的块。

8 NorFlash驱动实例#

8.1 识别NorFlash#

我们知道要识别norflash属性,要让norflash进入cfi模式,然后按照手册上的表格发送一系列的命令就能获取norflash属性。

8.1.1 发命令#

实现一个cpu向nor发命令的一个函数nor_cmd()。我们的norflash是16bit位宽的,所以访问nor是以16位为单位访问的。

1
2
3
4
5
6
7
8
9
10
11
12
#define NOR_FLASH_BASE  0  /* s3c2440, nor-->cs0, base addr = 0 */
/* 比如: 55H 98
** 本意是: 往(0 + (0x55)<<1)写入0x98
*/
void nor_write_word(unsigned int base, unsigned int offset, unsigned int val) {
volatile unsigned short *p = (volatile unsigned short *)(base + (offset << 1));
*p = val;
}

void nor_cmd(unsigned int offset, unsigned int cmd) {
nor_write_word(NOR_FLASH_BASE, offset, cmd);
}

nor_cmd(0x55, 0x90);即可往norflash的0x55写入了0x98。

8.1.2 读一次数据#

1
2
3
4
5
6
7
unsigned short nor_read_word(unsigned int base, unsigned int offset) {
volatile unsigned short *p = (volatile unsigned short *)(base + (offset << 1));
return *p;
}
unsigned short nor_dat(unsigned int offset) {
return nor_read_word(NOR_FLASH_BASE, offset);
}

调用nor_dat(0x100000)即可得到该地址的数据。

8.1.3 识别函数#

有了发命令函数nor_cmd和读一次数据函数nor_dat,那么就就可以参考nor芯片手册的命令表进行操作norflash了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/* 进入NOR FLASH的CFI模式
* 读取flash属性
*/
void do_scan_nor_flash(void) {
char str[4];
unsigned int size;
int regions, i;
int region_info_base = 0x2d; //第0块region的基地址2d,第1块region的基地址31,第2块region的基地址35......(参考手册表4-3)
int block_addr=0, blocks, block_size, j;
int cnt = 0;

int vendor, device;

/* 打印厂家ID、设备ID */
nor_cmd(0x555, 0xaa); /* 解锁 */
nor_cmd(0x2aa, 0x55);
nor_cmd(0x555, 0x90); /* read id */
vendor = nor_dat(0);
device = nor_dat(1);
nor_cmd(0, 0xf0); /* reset */

nor_cmd(0x55, 0x98); /* 进入cfi模式 */
str[0] = nor_dat(0x10); //读地址10H得到0051('q')
str[1] = nor_dat(0x11); //读地址11H得到0052('r')
str[2] = nor_dat(0x12); //读地址12H得到0059('y')
str[3] = '\0';
printf("str = %s\n\r", str);

/* 打印容量 */
size = 1<<(nor_dat(0x27));
printf("vendor id = 0x%x, device id = 0x%x, nor size = 0x%x = %dM\n\r", vendor, device, size, size/(1024*1024));

/* 打印各个扇区的起始地址 */
/* 名词解释:
* region : 一个nor flash含有1个或多个region, 一个region含有1个或多个block(扇区).
* Erase block region[i] information:
* 前2字节+1 : 表示该region有多少个block
* 后2字节*256 : 表示block的大小
*/

printf("Block/Sector start Address:\n\r");
regions = nor_dat(0x2c); //读出region数量
for (i = 0; i < regions; i++) {
blocks = 1 + nor_dat(region_info_base) + (nor_dat(region_info_base+1)<<8);
block_size = 256 * (nor_dat(region_info_base+2) + (nor_dat(region_info_base+3)<<8));

printf("\n\rregion %d, blocks = %d, block_size = 0x%x, block_addr = 0x%x\n\r", i, blocks, block_size, block_addr);

for (j = 0; j < blocks; j++) {
/* 打印每个block的起始地址 */
printHex(block_addr);
putchar(' ');
cnt++;
if (cnt % 5 == 0)
printf("\n\r");

block_addr += block_size;
}

region_info_base += 4; /*得到region[i]的基地址*/
}
printf("\n\r");
/* 退出CFI模式 */
nor_cmd(0, 0xf0);
}

从测试结果来看每个region的block个数和block_size不一定一样,像region[0]只有一个block,block_size为4*64K;
region[1]有2个block,block_size=2*64K。

8.2 读数据#

由于NOR Flash是内存类接口,可以像内存一样读取,那么do_read_nor_flash函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void do_read_nor_flash(void){
unsigned int addr;
volatile unsigned char *p;
int i, j;
unsigned char c;
unsigned char str[16];

/* 获得地址 */
printf("Enter the address to read: ");
addr = get_uint();

p = (volatile unsigned char *)addr;

printf("Data : \n\r");

for (i = 0; i < 4; i++){
for (j = 0; j < 16; j++){
c = *p++;
str[j] = c;
printf("%02x ", c);
}

printf(" ; ");

for (j = 0; j < 16; j++){
if (str[j] < 0x20 || str[j] > 0x7e) /* 不可视字符 */
putchar('.');
else
putchar(str[j]);
}
printf("\n\r");
}
}

8.3 擦数据#

norflash擦写都是需要一定时间的,那么当我执行擦除或者写入动作后什么时候代表一次擦写动作已经完成了呢?

芯片手册提供了一个方法,每次擦除或者烧写过程中都可以查询数据总线上的第6位(Q6),当它保持稳定的时候表示一次擦除或者烧写动作完成,如下图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void wait_ready(unsigned int addr) {
unsigned int val;
unsigned int pre;

pre = nor_dat(addr>>1);
val = nor_dat(addr>>1);
while ((val & (1<<6)) != (pre & (1<<6))) {
pre = val;
val = nor_dat(addr>>1);
}
}
void do_erase_nor_flash(void) {
unsigned int addr;

printf("Enter the address of sector to erase: ");
addr = get_uint();

printf("erasing ...\n\r");
nor_cmd(0x555, 0xaa); /* 解锁 */
nor_cmd(0x2aa, 0x55);
nor_cmd(0x555, 0x80); /* erase sector */

nor_cmd(0x555, 0xaa); /* 解锁 */
nor_cmd(0x2aa, 0x55);
nor_cmd(addr>>1, 0x30); /* 发出扇区地址 */
wait_ready(addr);
}

可以看到擦除后这个block就是全0xffff了。

8.3 写数据#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void do_write_nor_flash(void){
unsigned int addr;
unsigned char str[100];
int i, j;
unsigned int val;

/* 获得地址 */
printf("Enter the address of sector to write: ");
addr = get_uint();

printf("Enter the string to write: ");
gets(str);

printf("writing ...\n\r");

/* str[0],str[1]==>16bit
* str[2],str[3]==>16bit
*/
i = 0;
j = 1;
while (str[i] && str[j]) {
val = str[i] + (str[j]<<8);

/* 烧写 */
nor_cmd(0x555, 0xaa); /* 解锁 */
nor_cmd(0x2aa, 0x55);
nor_cmd(0x555, 0xa0); /* program */
nor_cmd(addr>>1, val);
/* 等待烧写完成 : 读数据, Q6无变化时表示结束 */
wait_ready(addr);

i += 2;
j += 2;
addr += 2;
}

val = str[i];
/* 烧写 */
nor_cmd(0x555, 0xaa); /* 解锁 */
nor_cmd(0x2aa, 0x55);
nor_cmd(0x555, 0xa0); /* program */
nor_cmd(addr>>1, val);
/* 等待烧写完成 : 读数据, Q6无变化时表示结束 */
wait_ready(addr);
}

由于我的norflash是位宽为16bit的,所以我们上面代码do_write_nor_flash进行写入时是以2byte(wold)为单位进行写入的。

总结:只要从spec中拿到了命令操作表,读写擦,识别就可以很轻松应对实现。