一、 存储器
- SDRAM
同步动态随机存储器。同步时指工作需要时钟;动态是指需要不断刷新来保证数据不丢失;随机说明不是线性存储,而是由指定地址进行数据的读写。如CPU使用的外部内存,即内存条。 - SRAM
Static Ram,是具有静态存取功能的内存,不需要刷电路就能保存存储的数据,比SDRAM速度快,一般用作高速缓冲存储器(Cache)。 - Norflash
非易失性外部存储介质,支持芯片内执行(XIP)。它有地址总线,CPU可以直接从Norflash中取值,直接在Norflash中运行程序。 - Nandflash
非易失性,虽然有数据总线,但没有地址总线,CPU不能够直接从发flash取值运行程序。常用于大量存储,类似硬盘。
二、 地址
1、概念
- 链接地址:链接脚本里指定的内存地址,编译时指定代码运行时应该所处的内存地址,是绝对地址。
- 运行地址:当前代码实际运行时所处的地址,是相对地址(相对于当前程序开始运行时的内存地址)。
- 加载地址:程序存储在flash中的地址。
2、 PIC与PDC
- 位置无关码(PIC,position independent code):依赖于程序当前运行的PC值(程序计数器),进行相对跳转,与位置(内存地址)无关。不管代码加载到任意地址,都能正常执行。如:B、BL、MOV、ADR……
- 位置有关码:不依赖当前的PC值,是绝对跳转,与位置(内存地址)相关。程序执行时的运行地址和编译链接时给定的链接地址必须相同,才能正常运行。如:LDR PC,=LABEL(LDR伪指令)……
- 总结:
- 正常情况下,链接地址==运行地址,此时,程序肯定能正常运行。
- 当链接地址!=运行地址时,程序运行也不一定出错。如果是位置有关码必然会出错;要是位置无关码便可正常运行,u-boot第一阶段指令能够正常执行的原因就在于此。
三、SDRAM、Norflash、Nandflash地址分配
如图所示,是存储器的地址分配图,总共有8个BANK和一个4KB大小的SRAM。
- SDRAM:只能够接在BANK6和BANK7(接片选线nGCS6、nGCS7)。
- Norflash:接在nGCS0上,选择BANK0,地址范围从0x0开始,上电时可以直接取0x0地址中的指令执行。
- Nandflash:
- 以页为单位读写,要先写命令,再给地址,才能读到NAND数据。Nandflash接在Nandflash控制器上而不是系统总线上,所以没有在8个BANK中分配地址。
- Nandfalsh控制器有个特殊功能,系统上电后,可以将Nandflash的前4KB引导程序搬移到SRAM内部。此时SRAM已经映射到了BANK0的0x0地址处,CPU对0x0~0x1000地址进行操作,实际上是在操作SRAM。
- 不管是Norflash还是Nandflash启动,ARM都是从0x0地址开始执行。在代码小于4KB,当做单片机裸跑程序的时候,就不需要进行代码的搬移等操作了。
四、u-boot总体启动流程
总共有两个阶段,第一阶段执行start.s汇编代码,最终会跳转到第二阶段C代码入口继续执行。
第一阶段:
- 硬件设备初始化
- 加载u-boot第二阶段代码到RAM空间
- 设置好栈
- 跳转到第二阶段代码入口
第二阶段:
- 初始化本阶段使用的硬件设备
- 检测系统内存映射
- 将内核从Flash读取到RAM中
- 为内核设置启动参数
- 调用内核
1、nand flash启动
- 首先需要将一个正确的bootloader烧写到Nandflash的最低位置,即从0x0开始烧写。
- 当从Nandflash启动时,硬件上将片内SRAM映射到nGCS0片选空间,即0x0位置,并自动将Nandflash的前4kB代码拷贝到CPU片内SRAM中(SRAM-cache),这个内部RAM称为Stepping stone(垫脚石、起步石)。然后,CPU从SRAM的0x0地址处获取第一条指令开始执行。
- 网上很多人说是CPU自动将Nandflash的前4kB拷贝到片内SRAM中,其实不然,实际是由Nandflash控制器(控制状态机)完成的,CPU根本没参与。
2、 nor flash启动
norflash支持片上执行代码(XIP),只需将bootloader烧写到Norflash的开始地址,由于Norflash会被映射到0x0地址处(nGCS0),上电后CPU会从0x0开始执行,也就是在Norflash中执行u-boot第一阶段,直到跳转到SDRAM中,继续执行第二阶段,也就不需要SRAM辅助运行第一阶段代码了。
五、u-boot.lds分析
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
//指定输出可执行文件是elf格式。小端,32位ARM指令
OUTPUT_ARCH(arm) //指定输出可执行文件的平台为ARM
ENTRY(_start) //入口点(地址)为_start,在cpu/arm/start.S中定义
SECTIONS
{
/*指定可执行image文件的全局入口点,通常这个地址都放在ROM(flash)0x0位置。必须是编译器知道这个地址,通常都是修改此处来完成*/
. = 0x00000000; //从0x0位置开始
. = ALIGN(4); //代码四字节对齐
.text : //这是程序存放的地方,代码段
{
arch/arm/cpu/arm1136_ambarella/start.o (.text) //start.o被链接到代码段最前面
*(.text) //其余代码段
}
. = ALIGN(4); //4字节对齐
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) } //指定只读数据段
. = ALIGN(4);
.data : { *(.data) } //指定读、写数据段
. = ALIGN(4);
.got : { *(.got) } //指定got段,got段是uboot自定义的一个段,非标准段
. = .;
__u_boot_cmd_start = .; //把__u_boot_cmd_start赋值为当前位置,即起始位置
.u_boot_cmd : { *(.u_boot_cmd) } //指定u_boot_cmd段,uboot把所有的uboot命令放在该段
__u_boot_cmd_end = .; //把__u_boot_cmd_end赋值为当前位置,即起结束位置
. = ALIGN(4);
__bss_start = .; //把__bass_start赋值为当前位置,即bass段的开始位置
.bss (NOLOAD) : { *(.bss) . = ALIGN(4); } //指定bass段,告诉加载器不要加载这个段,仅在执行域中才会有这段
_end = .; //把_end赋值为当前位置,即bss段的结束位置
}
ENTRY(_start)表示入口地址,程序运行时第一个被执行到的指令的地址便为_start。ld有多种方式设置程序的入口地址,按如下顺序(编号越前,优先级越高):
1、 ld命令行的-e选项
2、 链接脚本的ENTRY(SYMBOL)命令
3、 如果定义了start符号,使用start符号值
4、 如果存在.text section,使用.text section的首地址
5、 使用0地址指定了_start的链接地址为0x0,但编译后,查看System.map会发现,_start的链接地址不是.text的当前地址0x0,原因是编译的时候将其更新了。config.mk中有这么几行:
LDFLAGS += -Bstatic -T $(obj)u-boot.lds $(PLATFORM_LDFLAGS)
ifneq ($(TEXT_BASE),)
LDFLAGS += -Ttext $(TEXT_BASE)
可知,如果TEXT_BASE不为空,在链接时,ld命令会把参数-Ttext指定的地址TEXT_BASE赋给.text。所以指定链接地址的方式有两种:链接脚本u-boot.lds中指定、config.mk中使用ld命令设定,当同时使用时,以ld命令为准。
最终,我们一般指定链接地址为SDRAM的起始地址,因为第二阶段代码会被搬移到SDRAM中执行,需要保证链接地址和运行地址的一致性。但是最初bootloader是被烧写到flash的0x0地址,ARM架构CPU也是从0x0地址取第一条指令执行,这样链接地址就与运行地址(0x0地址)不相等了,程序运行不会出错吗?
- 这个困扰我很久的问题,其实在本文的第二部分已经解答了。Bootloader第一阶段代码的运行地址确实是0x0地址,与链接地址不相等,但实际上,程序可以正常运行,所以可以断定第一阶段代码都是位置无关码(PIC)。事实也是如此,所有的目标地址寻址都是使用当前PC值加减偏移量的方法(进行相对跳转),因此该阶段代码在任何地址处都可以正常运行。