到目前为止,我们的程序只能使用S3C2440的片内4KB的RAM。这么小的内存空间,显然不能应付实际的应用。是时候使用片外的RAM了,本文将详细介绍片外RAM的初始化过程。
0 ROM,RAM,SRAM,DRAM,SDRAM傻傻分不清
作为软件出身的软男,很难从根本上弄清楚ROM,RAM,SDRAM,SRAM等等的原理,这里我们只要了解基本的特性就可以了。
ROM,NorFlash: 只读内存,掉电不丢失。只读指的是不能通过正常的写入接口写入数据,但是可以通过特殊的烧写逻辑写入。这就意味之,我们的程序可以直接在ROM中执行,但是程序执行时无法在ROM中保存数据。测试表明,写入操作可以执行,但是没有任何效果。TQ2440搭配了2MB的NorFlash ROM,我们的程序烧入ROM后能直接执行,但是由于不能写内存,所以程序功能收到很大限制。为此实验环境选择了从Nandflash启动,应为S3C2440硬件直接会把Nandflash的头4KB数据拷贝到片内的SRAM执行。SRAM:(Static RAM)可读写,掉电丢失数据,但是无需定期电路刷新。这种东西非常昂贵,集成度不高,性能也相当好。多用于CPU的cache等关键场合。S3C2440A片内集成了4KB的SRAM,已经相当不错了。到目前为止,我们的实验程序都是运行在这块SRAM上的。DRAM:(Dynamic RAM)可读写,掉电丢失数据,而且必须定期充电刷新。这种RAM相对便宜,集成度高,性能较SRAM差点,但是也相当不错了。SDRAM:(Synchronous DRAM)同步DRAM。首先它是DRAM,只是额外需要同步时钟才能正常工作。这也是TQ2440板子上搭配的主要内存。核心板子上配备了2块,一共64MB。本文的目的就是配置使用着64MB的SDRAM。
1 内存地址的转换全过程
对内存的操作是所有程序的最基本需求,而对内存进行寻址是所有内存操作的前提。高级编程语言里,一般会使用各种符号名来代表内存地址,例如如下C语言代码:
int main()
{
int a=
10;
printf(
"hello\n");
}
12345
12345
main,a,printf都是符号,它在编译后会被实际的内存地址取代。c语言提供了直接操作内存地址的强大工具—指针,这也是C语言能在底层开发领域统治多年的法宝。
int *pa = &a;
int *p = (
int*)
0x00001010;
12
12
C语言不仅允许我们获得变量的内存地址,而且允许直接使用内存地址。那么这里的0x00001010地址数据到底是如何对应到内存芯片上的实际物理存储单元的呢?这种对应需要2个步骤:
1.1 从虚拟地址到物理地址
上面代码中的内存地址数据 0x00001010是核心CPU看到的地址,被称作虚拟地址。对于低端的单片机,这个虚拟地址直接作为物理地址发送到地址总线。而现代高级CPU内部一般都集成了一个被称作MMU的内存管理单元,CPU核心发出的虚拟地址首先进入MMU,MMU负责把虚拟地址转换为地址总线上的物理地址,然后发送到地址总线。
虚拟地址—(MMU)—物理地址
MMU是Windows,Linux等操作系统运行的基础,也是多进程实现的基础。S3C2440A的ARM核心也继承了MMU,只是默认MMU是不启动的,这就意味着虚拟地址和物理地址完全一样。我们的实验程序也没有启动MMU,所以在程序中使用的内存地址可以直接理解成物理地址。
1.2 从物理地址到内存存储单元的行列地址和片选信号
物理地址是一个线性地址,一般不能够直接用来寻址内存单元。中间需要通过内存控制器来把物理地址转换成内存单元的行列地址以及片选信号。 物理地址—(内存控制器)—行列地址及片选信号
总之,只要不同的物理地址最终被映射到不同的内存单元就满足硬件设计的要求,而不管CPU地址总线,CPU引脚,RAM芯片引脚到底是如何组合的。如何组合是硬件设计师的工作,我们软件工程师只要使用就可以了。
2 TQ2440的SDRAM硬件配置
2.1 S3C2440内存硬件设计
内存地址相关的引脚共有35个,其中行列地址引脚A26,A25,…A0共27个,片选信号引脚nGCS0,nGCS1,…,nGCS7共8个。可见外部引脚最大寻址空间为8*2^27,正好是1GB。1GB以上的空间仅供CPU内部寄存器使用。
S3C2440把每一个片选信号对应的空间称作一个BANK,每个BANK大小128M。
2.2 TQ2440开发板SDRAM搭配硬件设计
TQ2440搭配了2片32MB的SDRM,共64MB。这两片内存并联成32位数据宽度供CPU使用。
图中看出,每个内存芯片的行列地址有13位,数据宽度为2个字节,这样可寻址:2^13*2 = 64KB。啥?不是每个芯片32MB吗?这里的玄机在于对于SDRAM来说,行列地址共用一组引脚,首先通过A0-A12读取行地址并暂存起来,然后下一个周期还是通过A0-A12读取列地址,然后把行列地址拼装起来成为最后的行列地址。
RAM芯片上还有BA1,BA0两个引脚,这也是单元地址的一部分,叫做LogicalBank,注意这里的Bank和S3C2440的内存空间BANK没有什么关系。内存控制器要只有把物理地址转换为【(BA1,BA0),(A0-A12),片选信号】,并且产生正确的时序才能完成内存单元的寻址。
开发板上SDRAM芯片信号为:LogicalBank大小为4M,共有4个LogicalBank,内存单元大小为16位(2字节),所以单片容量为:4 x 4 x 2 = 32MB。然后两个内存芯片并联后接入BANK6。
如何把物理地址转换为行列地址是内存控制器的工作,这也是初始化SDRAM的重要步骤。
3 S3C2440片外内存初始化方法
从前面的分析,我们知道程序内存地址到SDRAM内存单元地址的整个转换过程为:
虚拟地址---(MMU)---物理地址(内存控制器)---LogicalBank,行列地址,片选信号
目前我们没有启用MMU,也无需配置。现在要做的就是配置内存控制器,使其能够正确地把物理地址转换为行列地址和片选信号。 S3C2440片内集成了完整的内存控制器,我们只需要向相应的控制寄存器写入合适的数值即可完成配置。
2.1 与SDRAM有关的控制寄存器
BWSCONBANKCON6REFRESHBANKSIZEMRSR6 其中有些时间周期参数的设置依赖于HCLK的值,故需要记住在设置MPLL时产生的HCLK。上篇博文中我们设置的HCLK=200MHz,这个数值太大无法满足设置REFRESH中Tsrc的要求,所以本文实验时同时把HCLK修改成了100MHz,PCLK设置成了50MHz。由于此时FCLK:HCLK不再是1:1而是1:2,所以需要设置CPU的总线模式为异步模式,增加了相关设置代码。
在我们的试验中,FCLK=200MHz,HCLK=100MHz,也就是周期是10ns。
上述寄存器需要设置的值都在源码中了,在此贴出完整的设置代码:
2.2 配置源码
.equ WTCON,
0x53000000
.equ INTMSK,
0x4a000008
.equ MPLLCON,
0x4c000004
.equ M_MDIV,
92
.equ M_PDIV,
4
.equ M_SDIV,
1
.equ UPLLCON,
0x4c000008
.equ U_MDIV,
56
.equ U_PDIV,
2
.equ U_SDIV,
2
.equ CLKDIVN,
0x4c000014
.equ HDIVN,
1
.equ DIVN_UPLL,
0
.equ PDIVN,
1
.equ BWSCON,
0x48000000
.equ BANKCON6,
0x4800001c
.equ REFRESH,
0x48000024
.equ BANKSIZE,
0x48000028
.equ MRSRB6,
0x4800002c
.equ BWSCON_WS6,
0
.equ BWSCON_DW6,
2
.equ B6_MT,
3
.equ B6_Trcd,
0
.equ B6_SCAN,
1
.equ BANKSIZE_VAL,
0xB1
.equ REFRESH_ENABLE,
1
.equ REFRESH_MODE,
0
.equ REFRESH_Trp,
0
.equ REFRESH_Tsrc,
1
.equ REFRESH_COUNT,
1268
.equ MRSRB6_VAL,
0x30
.text
.global ResetEntry
ResetEntry:
b ResetHandler
b ResetHandler
b ResetHandler
b ResetHandler
b ResetHandler
b ResetHandler
b ResetHandler
b ResetHandler
ResetHandler:
ldr r0, =WTCON
ldr r1, =
0x0
str r1, [r0]
ldr r0, =INTMSK
ldr r1, =
0xffffffff
str r1, [r0]
ldr r0, =CLKDIVN
ldr r1, =(DIVN_UPLL<<
3) + (HDIVN<<
1) + PDIVN
str r1, [r0]
.
if HDIVN>
1
mrc p15,
0, r0, c1, c0,
0
orr r0, r0,
#0xc0000000
mcr p15,
0, r0, c1, c0,
0
.endif
ldr r0, =UPLLCON
ldr r1, =(U_MDIV<<
12) + (U_PDIV<<
4) + U_SDIV
str r1, [r0]
nop
nop
nop
nop
nop
nop
nop
ldr r0, =MPLLCON
ldr r1, =(M_MDIV<<
12) + (M_PDIV<<
4) + M_SDIV
str r1, [r0]
ldr r0, =BWSCON
ldr r1, [r0]
ldr r2, =(
1<<
26 |
3<<
24)
bic r1, r1, r2
ldr r2, =((BWSCON_WS6<<
26) | (BWSCON_DW6<<
24))
orr r1, r1, r2
str r1, [r0]
ldr r0, =BANKCON6
ldr r1, [r0]
ldr r2, =((
1<<
16) | (
1<<
15) |
0xF)
bic r1, r1, r2
ldr r2, =(B6_MT<<
15 | B6_Trcd<<
2 | B6_SCAN)
orr r1, r1, r2
str r1, [r0]
ldr r0, =REFRESH
ldr r1, [r0]
ldr r2, =(
1<<
23 |
1<<
22 |
3<<
20 |
3<<
18 |
0x7FF)
bic r1, r1, r2
ldr r2, =(REFRESH_ENABLE<<
23 | REFRESH_MODE<<
22 | REFRESH_Trp<<
20 | REFRESH_COUNT)
orr r1, r1, r2
str r1, [r0]
ldr r0, =BANKSIZE
ldr r1, =BANKSIZE_VAL
str r1, [r0]
ldr r0, =MRSRB6
ldr r1, =MRSRB6_VAL
str r1, [r0]
ldr sp, =
0x34000000
b Main
.end
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
4 开始使用片外内存
现在已经可以使用64MB的SDRAM了,这相对于片内的4KB SRAM来说已经相当大了,而且足以可以运行大型软件了。虽然目前我们的程序本身被加载到片内SRAM上运行,但是我们在代码中是可以使用[0x30000000,0x34000000)这个范围的SDRAM内存了。 首先我们可以把堆栈指针SP指向SDRAM中,这样C语言的函数参数和局部变量就自动被放到SDRAM里了。 其次我们可以直接通过C指针直接操作SDRAM的。
3.1 把堆栈设置到片外内存上
ldr sp, =
0x34000000
1
1
就是这么简单的一条命令,就可以让C程序的堆栈搬迁到SDRAM中。
3.2 在片外内存上读写数据
void Main(
void)
{
led_init();
int a =
10;
if(((
unsigned)&a) >
0x30000000) {
led_on(
2);
}
else{
led_off(
2);
}
int *pInt = (
int*)
0x30000000;
*pInt =
99;
if(*pInt ==
99){
led_on(
1);
}
else{
led_off(
1);
}
while(
1){
;
}
}
1234567891011121314151617181920212223
1234567891011121314151617181920212223
5 下一步:把程序加载到SDRAM中
尽管目前我们可以直接操作SDRAM,但是程序代码本身并没有在SDRAM中运行。为此我们需要写一个loader,把程序加载到SDRAM中。loader可以和程序混合成一个程序,也可以两者分开。如果混合在一起那就是一个完整的能独立启动的程序,例如U-Boot。如果将两者分开,那么必须首先安装loader到NandFlash中,然后启动,通过命令加指定的程序到SDRAM执行。
下一步,将实验NANDFLASH读写操作,从而为代码搬迁做好准备。