一、start.S 解析7
总结回顾:lowlevel_init.S 中总共做了哪些事情:
检查复位状态、IO 恢复、关看门狗、开发板供电锁存、时钟初始化、DDR 初始化、串口初始化并打印 ‘O’、tzpc 初始化、打印 ‘K’。
其中值得关注的:关看门狗、开发板供电锁存、时钟初始化、DDR初始化、打印"OK"。
1、再次设置栈(DDR 中的栈)
(1) 再次开发板供电锁存。第一,做 2 次供电锁存是不会错的;第二,做 2 次则第 2 次无意义;做代码移植时有一个古怪谨慎保守策略:就是尽量添加代码而不要删除代码。
(2) 之前在调用 lowlevel_init 程序前,设置过 1 次栈(start.S 284-287行),那时候因为 DDR 尚未初始化,因此程序执行都是在 SRAM 中,所以在 SRAM 中分配了一部分内存作为栈。
本次因为 DDR 已经被初始化了,因此要把栈挪移到 DDR 中,所以要重新设置栈,这是第二次(start.S 297-299 行);这里实际设置的栈的地址是 33E00000
,刚好在 uboot 的代码段的下面紧挨着。
(3) 为什么要再次设置栈?因为 DDR 已经初始化了,已经有大片内存可以用了,没必要再把栈放在 SRAM 中可怜兮兮的了;原来 SRAM 中内存大小空间有限,栈放在那里要注意不能使用过多的栈,否则栈会溢出,我们及时将栈迁移到 DDR 中,也是为了尽可能避免栈使用时候的小心翼翼。
感慨:uboot 的启动阶段主要技巧就在于,小范围内有限条件下的辗转腾挪。
2、再次判断当前地址以决定是否重定位
(1) 再次用相同的代码判断运行地址是在 SRAM 中还是 DDR 中,不过本次判断的目的不同(上次判断是为了决定是否要执行初始化时钟和 DDR 的代码
),这次判断是为了决定是否进行 uboot 的 relocate 重定向
。
(2) 冷启动时,当前情况是:uboot 的前一部分(16kb 或者 8kb)开机自动从 SD 卡加载到 SRAM 中正在运行,uboot 的第二部分(其实第二部分是整个 uboot 文件)还躺在 SD 卡的某个扇区开头的 N 个扇区中。此时 uboot 的第一阶段已经即将结束了(第一阶段该做的事基本做完了),结束之前要把第二部分(整个 uboot 文件)加载到 DDR 中链接地址处(0x33e00000),这个加载过程就叫重定位。
二、uboot 重定位详解
(1) 0xD0037488
这个内存地址在 SRAM 中,这个地址中的值是被硬件自动设置的。硬件根据我们实际电路中 SD 卡在哪个通道中,会将这个地址中的值设置为相应的数字。譬如我们从 SD0 通道启动时,这个值为 EB000000
;从 SD2 通道启动时,这个值为 EB200000
。
《S5PV210_iROM_ApplicationNote_Preliminary_20091126.pdf》:
《S5PV210_UM_REV1.1.pdf》:
(2) 我们在 start.S 的 260 行确定了从 MMCSD 启动,然后又在 278 行将 #BOOT_MMCSD
写入了 INF_REG3
寄存器中存储着。然后又在 322 行读出来,再和 #BOOT_MMCSD
去比较,确定是从 MMCSD 启动。最终跳转到 mmcsd_boot
函数中去执行重定位动作。
(3) 真正的重定位是通过调用 movi_bl2_copy
函数完成的,在 uboot/cpu/s5pc11x/movi.c 中。是一个 C 语言的函数。
(4) copy_bl2(2, MOVI_BL2_POS, MOVI_BL2_BLKCNT, CFG_PHY_UBOOT_BASE, 0);
分析参数:2 表示通道 2;MOVI_BL2_POS 是 uboot 的第二部分在 SD 卡中的开始扇区,这个扇区数字必须和烧录 uboot 时烧录的位置相同;MOVI_BL2_BLKCNT 是 uboot 的长度占用的扇区数;CFG_PHY_UBOOT_BASE 是重定位时将 uboot 的第二部分复制到 DDR 中的起始地址(33E00000)。
《S5PV210_iROM_ApplicationNote_Preliminary_20091126.pdf》:
三、start.S 解析8
1、什么是虚拟地址、物理地址
(1) 物理地址就是物理设备设计生产时赋予的地址。像裸机中使用的寄存器的地址就是 CPU 设计时指定的,这个就是物理地址。物理地址是硬件编码的,是设计生产时确定好的,一旦确定了就不能改了。
(2) 一个事实就是:寄存器的物理地址是无法通过编程修改的,是多少就是多少,只能通过查询数据手册获得并操作。坏处就是不够灵活。一个解决方案就是使用虚拟地址。
(3) 虚拟地址意思就是,在我们软件操作和硬件被操作之间增加一个层次,叫做虚拟地址映射层。有了虚拟地址映射后,软件操作只需要给虚拟地址,硬件操作还是用原来的物理地址
,映射层建立一个虚拟地址到物理地址的映射表。当我们软件运行的时候,软件中使用的虚拟地址在映射表中查询,得到对应的物理地址再发给硬件去执行(虚拟地址到物理地址的映射是不可能通过软件来实现的
)。
2、MMU 单元的作用
(1) MMU 就是 memory management unit,内存管理单元。MMU实际上是 SoC 中一个硬件单元,它的主要功能就是实现虚拟地址到物理地址的映射。
(2) MMU 芯片在 CP15 协处理器中进行控制,也就是说要操控 MMU 进行虚拟地址映射,方法就是对 cp15 协处理器的寄存器进行编程。
3、地址映射的额外收益1:访问控制
(1) 访问控制就是:在管理上对内存进行分块,然后每块进行独立的虚拟地址映射,然后在每一块的映射关系中同时还实现了访问控制(对该块可读、可写、只读、只写、不可访问等控制)。
(2) 回想在 C 语言中编程中经常会出现一个错误:Segmentation fault。实际上这个段错误就和 MMU 实现的访问控制有关。当前程序只能操作自己有权操作的地址范围(若干个内存块),如果当前程序指针出错访问了不该访问的内存块,则就会触发段错误。
4、地址映射的额外收益2:cache
(1) cache 的工作和虚拟地址映射有关系。
(2) cache 是快速缓存,意思就是比 CPU 慢但是比 DDR 块。CPU 嫌 DDR 太慢了,于是乎把一些 DDR 中常用的内容事先读取缓存在 cache 中,然后 CPU 每次需要找东西时先在 cache 中找。如果 cache 中有就直接用 cache 中的;如果 cache 中没有,才会去 DDR 中寻找。
参考阅读
四、start.S 解析9
1、使能域访问(cp15的c3寄存器)
(1) cp15协处理器内部有 c0 到 c15 共 16 个寄存器,这些寄存器每一个都有自己的作用。我们通过 mrc 和 mcr 指令来访问这些寄存器。所谓的操作 cp 协处理器其实就是操作 cp15 的这些寄存器。
(2) c3 寄存器在 mmu 中的作用是控制域访问。域访问是和 MMU 的访问控制有关的。
2、设置 TTB(cp15 的 c2 寄存器)
(1) TTB 就是 translation table base,转换表基地址。首先要明白什么是 TT(translation table转换表),TTB其实就是转换表的基地址。
(2) 转换表是建立一套虚拟地址映射的关键。转换表分 2 部分,表索引和表项。表索引对应虚拟地址,表项对应物理地址。一对表索引和表项构成一个转换表单元,能够对一个内存块进行虚拟地址转换。(映射表基本规定中,规定了内存映射和管理是以块为单位的,至于块有多大,要看你的 MMU 的支持和你自己的选择。在 ARM 中支持 3 种块大小,细表 1KB、粗表 4KB、段 1MB)。真正的转换表就是由若干个转换表单元构成的,每个单元负责1个内存块,总体的转换表负责整个内存空间(0 - 4G)的映射。
(3) 整个建立虚拟地址映射的主要工作就是建立这张转换表。
(4) 转换表是放置在内存中的,放置时要求起始地址在内存中要 xx 位对齐。转换表不需要软件去干涉使用,而是将基地址 TTB 设置到 cp15 的 c2 寄存器中,然后 MMU 工作时会自动去查转换表。
3、使能 MMU 单元(cp15 的 c1 寄存器)
(1) cp15 的 c1 寄存器的 bit 0 控制 MMU 的开关。只要将这一个 bit 置 1 ,即可开启 MMU 。开启 MMU 之后,上层软件层的地址就必须经过TT的转换才能发给下层物理层去执行。
4、找到映射表待分析
(1) 通过符号查找,确定转换表在 lowlevel_init.S 文件的 593 行。
五、start.S 解析10
宏观上理解转换表:整个转换表可以看作是一个 int 类型的数组,数组中的一个元素就是一个表索引和表项的单元。数组中的元素值就是表项,这个元素的数组下标就是表索引。
ARM 的段式映射中,长度为 1MB,因此一个映射单元只能管 1MB 内存,那我们整个 4G 范围内需要 4G/1MB = 4096 个映射单元,也就是说这个数组的元素个数是 4096.实际上我们做的时候并没有依次单个处理这 4096 个单元,而是把 4096 个分成几部分,然后每部分用 for 循环做相同的处理。
1、宏 FL_SECTION_ENTRY
.macro FL_SECTION_ENTRY base,ap,d,c,b
.word (\base << 20) | (\ap << 10) | \
(\d << 5) | (1<<4) | (\c << 3) | (\b << 2) | (1<<1)
.endm
我们可以看到,base << 20,意思就是,左移 20位,就是以 M (k,M,G…)字节为单位进行划分;
因此 base 如果是 5,FL_SECTION_ENTRY 5,ap,d,c,b
就映射起始地址为 5M(到 6M,因为每一个表项的大小是 1 M)的地址空间,后面的 ap,d,c,b
是映射的这段地址区域的访问控制信息。
2.
对于上面这段代码的理解:
.set __base,0
,把 __base 设置为 0。
.rept ... .endr
之间的语句,是一个循环;每次 __base 递增 1,循环次数为 0x100(256)。
base 的取值 | 映射表的“表项”(物理地址) | 映射表的“表索引”(虚拟地址,以 M 字节划分) |
---|---|---|
0 | 0x0000_0000 (0 << 20) | 0x0 (M) |
1 | 0x0010_0000 (1 << 20) | 0x1 (M) |
2 | 0x0020_0000 (2 << 20) | 0x2 (M) |
3 | 0x0030_0000 (3 << 20) | 0x3 (M) |
… | … | … (M) |
0xFF | 0x0FF0_0000 (0xFF << 20) | 0xFF (M) |
因此,这段循环就映射了一段物理地址空间: 0x0 ~ (0x1000,0000 - 1)。因此,这段循环映射的物理地址范围是:0 ~ 256M - 1,总共拥有的表索引是 0 ~ 255。
通过 uboot 的注释可以看到,这段地址区域,确实是给 iRAM 空间使用的。
3.
接着我们继续分析接下来的 uboot 代码。
base 的取值 | 映射表的“表项”(物理地址) | 映射表的“表索引”(虚拟地址,以 M 字节划分) |
---|---|---|
null | 0 | 0x100 (M) |
null | 0 | 0x101 (M) |
null | 0 | 0x102 (M) |
null | 0 | 0x103 (M) |
… | … | … (M) |
null | 0 | 0x1FF (M) |
虚拟地址范围 0x1000_0000 ~ 0x1FFF_FFFF,将其映射到物理地址 0 ,即无效地址。
如下图可以看到,物理地址 0x1000_0000 ~ 0x1FFF_FFFF 确实是保留区,不应该被访问。
所以将虚拟地址范围 0x1000_0000 ~ 0x1FFF_FFFF映射为无效地址,是符合逻辑的。
4.
base 的取值 | 映射表的“表项”(物理地址) | 映射表的“表索引”(虚拟地址,以 M 字节划分) |
---|---|---|
0x200 | 0x2000_0000 | 0x200 (M) |
0x201 | 0x2010_0000 | 0x201 (M) |
0x202 | 0x2020_0000 | 0x202 (M) |
0x203 | 0x2030_0000 | 0x203 (M) |
… | … | … (M) |
0x5FF | 0x5FF0_0000 | 0x5FF (M) |
虚拟地址范围 0x2000_0000 ~ 0x5FFF_FFFF,将其映射到物理地址 0x2000_0000 ~ 0x5FFF_FFFF 。
如下图可以看到,物理地址 0x2000_0000 ~ 0x5FFF_FFFF 是 DDR 内存区域。
关于 DRAM 内存空间的区域,uboot 指定的地址详情如下图。详情可以参考博文:链接地址。
5.
base 的取值 | 映射表的“表项”(物理地址) | 映射表的“表索引”(虚拟地址,以 M 字节划分) |
---|---|---|
null | 0 | 0x600 (M) |
null | 0 | 0x601 (M) |
null | 0 | 0x602 (M) |
null | 0 | 0x603 (M) |
… | … | … (M) |
null | 0 | 0x7FF (M) |
虚拟地址范围 0x6000_0000 ~ 0x7FFF_FFFF,将其映射到物理地址 0 ,即无效地址。
如下图可以看到,物理地址 0x6000_0000 ~ 0x7FFF_FFFF 确实是 uboot 中未设置的 DDR 区域,不应该被访问。
所以将虚拟地址范围 0x6000_0000 ~ 0x7FFF_FFFF 映射为无效地址,是符合逻辑的。
6.
base 的取值 | 映射表的“表项”(物理地址) | 映射表的“表索引”(虚拟地址,以 M 字节划分) |
---|---|---|
0x800 | 0x8000_0000 | 0x800 (M) |
0x801 | 0x8010_0000 | 0x801 (M) |
0x802 | 0x8020_0000 | 0x802 (M) |
0x803 | 0x8030_0000 | 0x803 (M) |
… | … | … (M) |
0xaFF | 0xaFF0_0000 | 0xaFF (M) |
虚拟地址范围 0x8000_0000 ~ 0xaFFF_FFFF,将其映射到物理地址 0x8000_0000 ~ 0xaFFF_FFFF 。
如下图可以看到,物理地址 0x8000_0000 ~ 0xaFFF_FFFF 是 SROMC_BANK 区域。
7.
base 的取值 | 映射表的“表项”(物理地址) | 映射表的“表索引”(虚拟地址,以 M 字节划分) |
---|---|---|
0xb00 | 0xb000_0000 | 0xb00 (M) |
0xb01 | 0xb010_0000 | 0xb01 (M) |
0xb02 | 0xb020_0000 | 0xb02 (M) |
0xb03 | 0xb030_0000 | 0xb03 (M) |
… | … | … (M) |
0xbFF | 0xbFF0_0000 | 0xbFF (M) |
虚拟地址范围 0xb000_0000 ~ 0xbFFF_FFFF,将其映射到物理地址 0xb000_0000 ~ 0xbFFF_FFFF 。
如下图可以看到,物理地址 0xb000_0000 ~ 0xbFFF_FFFF 是 ONENAND/NAND 区域。
8.
base 的取值 | 映射表的“表项”(物理地址) | 映射表的“表索引”(虚拟地址,以 M 字节划分) |
---|---|---|
0x300 | 0x3000_0000 | 0xc00 (M) |
0x301 | 0x3010_0000 | 0xc01 (M) |
0x302 | 0x3020_0000 | 0xc02 (M) |
0x303 | 0x3030_0000 | 0xc03 (M) |
… | … | … (M) |
0x3FF | 0x3FF0_0000 | 0xcFF (M) |
虚拟地址范围 0xc000_0000 ~ 0xcFFF_FFFF,将其映射到物理地址 0x3000_0000 ~ 0x3FFF_FFFF 。
如下图可以看到,物理地址 0x3000_0000 ~ 0x3FFF_FFFF 是 uboot 设置的 DMC0 区域。
9.
base 的取值 | 映射表的“表项”(物理地址) | 映射表的“表索引”(虚拟地址,以 M 字节划分) |
---|---|---|
0xd00 | 0xd000_0000 | 0xd00 (M) |
0xd01 | 0xd010_0000 | 0xd01 (M) |
0xd02 | 0xd020_0000 | 0xd02 (M) |
0xd03 | 0xd030_0000 | 0xd03 (M) |
… | … | … (M) |
0xFFF | 0xFFF0_0000 | 0xFFF (M) |
虚拟地址范围 0xd000_0000 ~ 0xFFFF_FFFF,将其映射到物理地址 0xd000_0000 ~ 0xFFFF_FFFF 。
如下图可以看到,物理地址 0xd000_0000 ~ 0xFFFF_FFFF 代表的区域。
总结
VA PA length
0-10000000 0-100' 256MB
10000000-20000000 0 256MB
20000000-60000000 20000000-60000000 1GB 512-1.5G
60000000-80000000 0 512MB 1.5G-2G
80000000-b0000000 80000000-b0000000 768MB 2G-2.75G
b0000000-c0000000 b0000000-c0000000 256MB 2.75G-3G
c0000000-d0000000 30000000-40000000 256MB 3G-3.25G
d-完 d-完 768MB 3.25G-4G
DRAM 的有效范围:
DMC0: 0x30000000 ~ 0x3FFFFFFF
DMC1: 0x40000000 ~ 0x4FFFFFFF
结论:虚拟地址映射只是把虚拟地址的 c0000000 开头的 256MB ,映射到了 DMC0 的 30000000 开头的 256MB 物理内存上去了。其他的虚拟地址空间根本没动,还是原样映射的。
思考:为什么配置时将链接地址设置为 c3e00000,因为这个地址将来会被映射到33e00000 这个物理地址。
六、start.S 解析11
1、再次设置栈
(1) 第三次设置栈。这次设置栈还是在 DDR 中,之前虽然已经在 DDR 中设置过一次栈了,但是本次设置栈的目的,是将栈放在比较合适(安全,紧凑而不浪费内存)的地方。
(2) 我们实际将栈设置在 uboot 起始地址上方 2MB 处,这样安全的栈空间是:2MB-uboot大小-0x1000= 1.8MB 左右。这个空间既没有太浪费内存,又足够安全。
2、清理bss
(1) 清理 bss 段代码和裸机中讲的一样。注意表示 bss 段的开头和结尾地址的符号是从链接脚本 u-boot.lds 得来的。
3、ldr pc, _start_armboot
(1) start_armboot
是 uboot/lib_arm/board.c
中,这是一个 C 语言实现的函数。这个函数就是 uboot 的第二阶段。
这句代码的作用,就是将 uboot 第二阶段执行的函数的地址传给 pc。实际上就是使用一个远跳转,直接跳转到 DDR 中的第二阶段开始地址处。
(2) 远跳转的含义就是,这句语句加载的地址和当前运行地址无关,而和链接地址有关
。因此这个远跳转可以实现,从 SRAM 中的第一阶段跳转到 DDR 中的第二阶段
。
(3) 这里这个远跳转 ldr pc, _start_armboot,就是 uboot 的第一阶段和第二阶段的分界线
。
4、总结:uboot的第一阶段做了哪些工作
(1) 构建异常向量表;
(2) 设置 CPU 为 SVC 模式;
(3) 关看门狗;
(4) 开发板供电置锁;
(5) 时钟初始化;
(6) DDR 初始化;
(7) 串口初始化并打印"OK";
(8) uboot 工程的重定位;
(9) 建立映射表并开启 MMU;
(10) 跳转到第二阶段(BL2);
三次设置 sp 栈指针
-
第一次:因为 DDR 尚未初始化,因此程序执行都是在 SRAM 中,所以在 SRAM 中分配了一部分内存作为栈。
-
第二次:因为 DDR 已经被初始化了,因此要把栈挪移到 DDR 中,所以要重新设置栈(start.S 297-299 行);这里实际设置的栈的地址是
33E00000
,刚好在 uboot 的代码段的下面紧挨着。为什么要再次设置栈?因为 DDR 已经初始化了,已经有大片内存可以用了,没必要再把栈放在 SRAM 中可怜兮兮的了;原来 SRAM 中内存大小空间有限,栈放在那里要注意不能使用过多的栈,否则栈会溢出,我们及时将栈迁移到 DDR 中,也是为了尽可能避免栈使用时候的小心翼翼。
-
第三次:这次设置栈还是在 DDR 中,之前虽然已经在 DDR 中设置过一次栈了,但是本次设置栈的目的,是将栈放在比较合适(安全,紧凑而不浪费内存)的地方。
我们实际
将栈设置在 uboot 起始地址上方 2MB 处
,这样安全的栈空间是:2MB-uboot大小-0x1000= 1.8MB 左右。这个空间既没有太浪费内存,又足够安全。
源自朱有鹏老师.