NAND FLASH启动流程
在这里我先以TQ2440的启动代码分析,因为手上有本书,反正Nand Flash启动流程都是一样的对于mini2440和TQ2440来说。TQ2440的启动代码部分如下:
;**********************************************************************************
..... ;38行之前的代码都是一些准备工作
..... ;如宏定义、符号导入之类的
35 AREA Init,CODE,READONLY
36 ENTRY
37 ResetEntry
38 b ResetHandled ;程序执行代码的第一句
.... ;跳转到标号ResetHandler处执行
....
63 ResetHandler
64 ldr r0,=WTCON
65 ldr r0,=0x0
.....
.....
106 ldr r0,=BWSCON
107 ldr r0,[r0]
108 ands r0,r0,#6 ;OM[1:0]!=0,NOR Flash 启动
109 bne copy_proc_beg ;do not read nand flash
110 adr r0,ResetEntry ;OM[1:0]==0,NAND Flash启动
111 cmp r0,#0 ;if use Multi-ice
112 bne copy_proc_beg ;do not read nand flash for boot
113 nand_boot_beg
114 bl RdNF2SDRAM
115 ldr pc,=copy_proc_beg
116 copy_proc_beg
117 adr r0,ResetEntry
118 ldr r2,BaseOfROM
119 cmp r0,r2
120 ldreq r0,TopOfRom
121 beq InitRam
122 ldr r3,TopOfRom
123 0
124 ldmia r0!,{r4-r7}
125 stmia r2!,{r4-r7}
126 cmp r2,r3
127 bcc %B0
128 sub r2,r2,r3
129 sub r0,r0,r2
130 InitRam
131 ldr r2,BaseOfBSS
132 ldr r3,BaseOfZero
133 0
134 cmp r2,r3
135 ldrcc r1,[r0],#4
136 strcc r1,[r2],#4
137 bcc %B0
138 mov r0,#0
139 ldr r3,EndOfBSS
140 1
141 cmp r2,r3
142 strcc r0,[r2],#4
143 bcc %B1
;安装中断向量表
144 ldr r0,=HandleIRQ
145 ldr r1,=IsrIRQ
146 str r1,[r0]
147 b Main
148 InitStacks
149 ;堆栈初始化代码部分的开始行
....
168 ;堆栈初始化代码部分的结束行
....
....
231 END ;启动代码结束
现在开始分析代码:
106 ldr r0,=BWSCON
107 ldr r0,[r0]
108 ands r0,r0,#6 ;OM[1:0]!=0,NOR Flash 启动
109 bne copy_proc_beg ;do not read nand flash
110 adr r0,ResetEntry ;OM[1:0]==0,NAND Flash启动
111 cmp r0,#0 ;if use Multi-ice
112 bne copy_proc_beg ;do not read nand flash for boot
第106~112行才是选择从NAND FLASH或者NOR FLASH启动的关键。
第106~107行将BWSCON寄存器里的数据读入寄存器r0。从S3C2440的数据手册上可以查到BWSCON寄存器第1、2位,反映了系统总线宽度的信息。
第108行,and指令主要用于测试数据的某几位是0还是1,后面的s表示该指令的运算结果影响标志位。因此,该条指令主要是测试r0第1、2两位。当运算结果是0时,说明r0的第1、2位是0,即BWSCON寄存器的第1、2位是0,即系统是从Nand Flash启动的,则执行第110行程序;当运算结果不为0时,说明是从Nor Flash启动,在执行第109行程序。
第109行,执行该条指令的前提是系统从Nor Flash启动,然后跳转到copy_proc_beg标号处执行,其功能是:实现代码从加载域到运行域的搬移工作,这也是启动代码的核心。关于加载域和运行域在后面会讲到。
第110~112行,其中adr指令比较难理解,adr伪指令是将标号基于PC的相对地址加载到寄存器中,因为是将程序下载到了NADN FLASH的0地址处,所以ResetEntry的地址为0,所以最终结果就是r0=0。关于ldr与adr指令、相对地址与绝对地址请看博客:点此打开链接
第112行,由上面的分析可以知道,该行指令不执行,执行第113行指令。
113 nand_boot_beg
114 bl RdNF2SDRAM
115 ldr pc,=copy_proc_beg
第113行,只是一个程序标号,没有实际的用处。
第114行,调用C语言函数RdNF2SDRAM,将代码从NAND FLASH读入到内存中,这个函数是在C文件中定义的。
第115行,用ldr指令将copy_proc_beg的绝对地址加载到程序计数器PC中,即跳转到了内存中去执行。
到此为止,从NAND FLASH启动的流程已经完全讲清楚了,尽管我还是不”很清楚“,下面就结合图1详细分析从NAND FLASH启动总流程,做到真清楚!
① 系统上电后,S3C2440处理器通过硬件电路自动将NAND FLASH中的前4KB代码复制到内部的Stepping Stonezhong ,该Stepping Stone就是一块RAM,可以执行程序。
② 程序从Stepping Stone的0地址处开始执行第1条指令。
③ 程序执行到bl RdNF2SDRAM(第114行)时,调用NAND FLASH函数,将NAND FLASH中的代码全部复制到SDRAM中。
④ 执行 ldr pc,=copy_proc_beg,将copy_proc_beg的绝对地址加载到程序计数器PC中。
⑤ 程序跳转到SDRAM中copy_proc_beg标号处执行。
注意:第(4)步中提到了copy_proc_beg的绝对地址,那么什么是绝对地址呢?绝对地址是程序编译链接后确定的,如图2所示
在链接时,指定的entry 地址是0x3000 0000,这个entry地址即第36行的ENTRY,也就是说,在最终编译链接后生成的可执行程序中,copy_proc_beg的绝对地址=0x3000 0000+偏移量。这个偏移量是多少呢?就是copy_proc_beg距离ENTRY的偏移量。因此,在第(4)步执行完后,虽然stepping stone中也有copy_proc_beg,SDRAM中也有copy_proc_beg,但是程序并没有跳到stepping stone中的copy_proc_beg处执行,而是跳到了SDRAM中的copy_proc_beg处去执行。
加载域和运行域、RW、RO、ZI
一个简单的可执行程序的映像文件如图3所示。它由RO、RW、和ZI三个段组成,其中RO为代码段和只读数据段;RW为可读/写的数据段;ZI为未初始化的数据段。
此外,映像文件还可以分为加载域(Load View)和运行域(Execution View)。加载域反映了ARM可执行映像文件各个段存放在存储器中时的位置关系,运行域反映了ARM可执行映像文件各个段真正执行时在存储器中的位置关系。
前文讲到,从NOR FLASH启动后,当启动代码执行到第109行时,会跳转到copy_proc_beg处执行。此时,程序加载域和运行域的分布情况如图4所示
加载域的起始地址是0x0000 0000,为什么呢?因为加载域反映了ARM可执行文件存放在存储器中时的位置关系,程序下载到NOR FLASH中,又知道NOR FLASH的起始地址是0x0000 0000,因此加载域的起始地址是0x0000 0000,在加载域中,首先存放RO,后面紧跟着存放的是RW,并没有ZI。RO为代码段,属性为只读;RW为已经初始化的全局变量段,属性可读、可写;ZI为未初始化的全局变量段。为什么在加载域中没有ZI呢,这主要是为了减小ARM可执行映像文件的容量,因为ZI中存放的是未初始化的全局变量,那么只需要记录该段的容量即可,在运行域中需要根据这个容量分配内存,然后用0填充。
运行域的起始地址是0x3000 0000,这又是为什么呢?
如图4所示,为ADS中在配置选项”ARM Linker“中有一项是”RO Base“,这个地方就表明了运行域中RO的起始地址。"RW Base"为空,这表明在运行域总共RW紧跟着RO的后面。
启动代码做的工作就是如图3中虚线部分所指示的,将各个段搬移到指定的位置,然后将ZI初始化为0.下面这段程序就是围绕这个工作来展开。
116 copy_proc_beg
117 adr r0,ResetEntry
118 ldr r2,BaseOfROM
119 cmp r0,r2
120 ldreq r0,TopOfRom
121 beq InitRam
122 ldr r3,TopOfRom
123 0
124 ldmia r0!,{r4-r7}
125 stmia r2!,{r4-r7}
126 cmp r2,r3
127 bcc %B0
128 sub r2,r2,r3
129 sub r0,r0,r2
有了上面的讲解,再看第116~129行程序就很容易理解。在上述程序中,BaseOfROM和TopOfROM是用DCD分配的内存单元,里面存放的是RO的起始地址和结束地址(准确点说是结束地址+1)。该起始地址和结束地址是由编译器自动生成的,在程序中使用即可,见第183~184行
183 BaseOfROM DCD |Image$$RO$$Base|
184 TopOfROM DCD |Image$$RO$$Limit|
其中|Image$$RO$$Base| 和|Image$$RO$$Base| 是ADS编译器自动生成的符号,在该启动代码的开头使用IMPORT引入了,如下
28 IMPORT |Image$$RO$$Base| ;Base of ROM code
29 IMPORT |Image$$RO$$Limit| ;End of ROM code dd
编译生成的符号 | 含义 |
---|---|
|Image$$RO$$Base| | RO段起始地址 |
|Image$$RO$$Limit| | RO段结束地址+1 |
|Image$$RW$$Base| | RW段起始地址 |
|Image$$RW$$Limit| | RW段结束地址+1 |
|Image$$ZI$$Base| | ZI段起始地址 |
|Image$$ZI$$Limit| | ZI段结束地址+1 |
下面接着分析第116~129行。
第117行,使用adr指令将ResetEntry的相对地址加载到寄存器r0中,这个地址就是0。
第118行,使用ldr指令将RO的起始地址加载到寄存器r2中。
第119行,比较r0和r2是否相同,如图5所示,可以看出r0和r2并不相等,因此第120~121行并不执行。
第122行,使用ldr指令将RO的结束地址+1加载到寄存器r3中。
执行完上述指令后,图5详细展示程序执行的最后结果,下面的工作就是将代码从r0指示的加载域中的RO搬移到r2、r3所限定的运行域中的RO。
第123行,定义了一个局部标号0。
第124行,使用批量加载指令ldmia,将r0地址出的数据加载到寄存器r4~r7中,注意后面的感叹号“!”说明取完数据后,r0的地址自动更新,即指向下一个地址处。此外,可以得出这样的结论每次搬移16个字节(每个寄存器是4个字节的长度,共4个寄存器) 。
第125行,将r4~r7中的数据存储到r2开始的地址处,同时r2地址自动更新。
第126~127行,比较r2和r3的值,如果r2的值小于r3的值,就跳转到第123行定义的局部标号0处知心。更形象的理解是,如图5所示,当r2小于r3时,说明RO还没有搬移完,因此就接着搬移,直到r2的值大于r3的值。
第128~129行,这两行的作用就是调整r0的值,使其指向加载域中RW的起始地址。有的人会说,怎么r0就指向RW起始地址呢?已知咱们的RW段紧跟着RO段,当搬移完RO内容后,很可能r2不一定等于r3,如果r2大于r3,说明r2指向的地址超过了运行域中的RW段起始地址,此时r0指向的地址也超过了加载域中RW段的起始地址。为了更形象的展示这一调整的过程,请看图6:
由第124行的讨论可知,每次搬移16个字节,因此最后肯那个出现的情况如图6所示,当r2的值大于r3的值时,停止搬移,那么现在的情况是已经完成了RO从加载域到运行域的搬移,下面需要完成RW从加载域到运行域的搬移。面临的首要问题是如何在加载域中找到RW的起始地址,现在知道的信息是在加载域中RW紧邻着RO存放。由图6可以看到r0已经移到了RW,只要计算出这个偏移,即可计算出RW的首地址。这个偏移地址怎么计算呢,其实就是r2-r3。
第128行,计算出偏移地址r2-r3,将其存放在寄存器r2中。
第129行,将r0的值减去这个偏移地址,即得到了RW的起始地址。
其实,在这种加载域和运行域布局情况下,即加载域和运行域中RO和RW紧挨着存放,完全可以将RO和RW整体搬移,但是该启动代码照顾到更为一般的情况,并没有采取这种搬移方法,而是采取各个段分别搬移的方法。下面的程序就是搬移RW段。
130 InitRam
131 ldr r2,BaseOfBSS
132 ldr r3,BaseOfZero
133 0
134 cmp r2,r3
135 ldrcc r1,[r0],#4
136 strcc r1,[r2],#4
137 bcc %B0
138 mov r0,#0
139 ldr r3,EndOfBSS
140 1
141 cmp r2,r3
142 strcc r0,[r2],#4
143 bcc %B1
上述的BaseOfBss和BaseOfZero仍然是用DCD分配的内存单元。如下:
185 BaseOfBss DCD |Image$$RW$$Base| ;RW段起始地址
186 BaseOfZero DCD |Image$$ZI$$Base| ;ZI段起始地址
187 EndOfBss DCD |Image$$ZI$$Limit| ;ZI段结束地址
有了前面RO搬移的讲解,下面RW的搬移工作变得较为简单。在RO搬移完以后,r0已经指向了加载域中RW的起始地址处。
第131行,将r2指向运行域中RW的起始地址处,其中BaseOfBSS是在185行定义的。
第132行,将r3指向RW的结束地址+1处,又因为ZI在RW后面紧挨着存放,所以可以得出这样的结论:RW的结束地址+1就是ZI的起始地址。
RW的搬移如图7所示:
第133行定义了一个局部标号 0。
第134行比较r2和r3的值。
第135行,ldr指令后面的cc表示条件执行,如果r2的值小于r3的值,就从r0地址处取出一个字数据,加载到r1中,然后r0自动加4,即r0=r0+4
第136行,将r1中的数据存储到r2指向的地址处,然后,r2的值自动加4,即r2=r2+4。
第137行,如果r2的值小于r3,则跳转到133行定义的局部标号0处执行。
经过前面的讲解,已经实现了RO和RW的搬移工作,下面需要做的就是将ZI初始化为0即可。注意,这里并不是ZI的搬移,因为在加载域中没有ZI,所以不存在搬移这一说法。
138 mov r0,#0
139 ldr r3,EndOfBSS
140 1
141 cmp r2,r3
142 strcc r0,[r2],#4
143 bcc %B1
RW搬移结束后,r2指向了RW的结束地址+1处,即ZI的起始地址处。
第138行,将0赋值给寄存器r0.
第139行,将ZI的结束地址+1加载到寄存器r3,即r3指向了ZI的结束地址+1处。程序执行到了现在,加载域中的分布情况如图8所示,可以看出,r2指向了ZI的起始地址,r3指向了ZI的结束地址+1处。
第140~143行就是将ZI用0填充,即实现了初始化为0。
到此为止,启动代码做的工作已经接近尾声,下面回顾一下启动代码将程序从加载域到运行域的搬移过程。
① 找到加载域中各个段的起始地址和结束地址。
方法:用adr指令找到了RO的起始地址。
② 找到运行域中各个段的起始地址和结束地址。
方法: 使用编译器自动生成的各个段的起始地址和结束地址。
③ 使用循环执行搬移即可。
144 ldr r0,=HandleIRQ
145 ldr r1,=IsrIRQ
146 str r1,[r0]
对于第144~146行,就是安装中断向量表。
147 b Main ;
第147行使用b跳转指令跳转到C语言函数Main函数处执行。
到这里,我有了一个大概的想法,可不可以在MDK中自带的S3C2440.s中加入这段由加载域搬移到运行域,然后用MDK生成bin文件,利用J-Flash ARM将bin文件下载到nor flash中去,这样子代码就可以搬移到SDRAM中去运行了。不能用axf好像,我看网上说是什么axf携带地址信息,现在还不是很懂。
我想上述方法可行,关键就是要RO RW怎么安放,这又好像和分散加载文件scatter有关,再接着看。
RealView MDK中如何获得RO,RW,ZI的地址和长度?
问题分析:
在RealView MDK里有专门的字符用来表示RO,RW,ZI的起始地址和长度。
解决办法:
1.在不使用Scatter文件时,默认的为Image$$RW$$Base、Image$$RW$$Limit、Image$$RO$$Base、Image$$RO$$Limit、Image$$ZI$$Base和Image$$ZI$$Limit等6个地址,它的长度这样计算:Length = (Image$$RW$$Limit-Image$$RW$$Base)。
2. 在使用Scatter文件后,上述的6个默认地址没有了,取而代之的是Image$$段名$$Base 和Image$$段名$$Limit表示的地址,长度计算的方法和上述一样,即Length = (Image$$段名$$Limit-Image$$段名
3. 关于Scatter文件的使用方法请参考下面的网址:http://www.realview.com.cn/wen-list3.asp?id=330