一、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 字节划分)00x0000_0000 (0 << 20)0x0 (M)10x0010_0000 (1 << 20)0x1 (M)20x0020_0000 (2 << 20)0x2 (M)30x0030_0000 (3 << 20)0x3 (M)……… (M)0xFF0x0FF0_0000 (0xFF << 20)0xFF (M)

因此,这段循环就映射了一段物理地址空间: 0x0 ~ (0x1000,0000 - 1)。因此,这段循环映射的物理地址范围是:0 ~ 256M - 1,总共拥有的表索引是 0 ~ 255。

通过 uboot 的注释可以看到,这段地址区域,确实是给 iRAM 空间使用的。

3.

接着我们继续分析接下来的 uboot 代码。

base 的取值映射表的“表项”(物理地址)映射表的“表索引”(虚拟地址,以 M 字节划分)null00x100 (M)null00x101 (M)null00x102 (M)null00x103 (M)……… (M)null00x1FF (M)

虚拟地址范围 0x1000_0000 ~ 0x1FFF_FFFF,将其映射到物理地址 0 ,即无效地址。

如下图可以看到,物理地址 0x1000_0000 ~ 0x1FFF_FFFF 确实是保留区,不应该被访问。

所以将虚拟地址范围 0x1000_0000 ~ 0x1FFF_FFFF映射为无效地址,是符合逻辑的。

4.

base 的取值映射表的“表项”(物理地址)映射表的“表索引”(虚拟地址,以 M 字节划分)0x2000x2000_00000x200 (M)0x2010x2010_00000x201 (M)0x2020x2020_00000x202 (M)0x2030x2030_00000x203 (M)……… (M)0x5FF0x5FF0_00000x5FF (M)

虚拟地址范围 0x2000_0000 ~ 0x5FFF_FFFF,将其映射到物理地址 0x2000_0000 ~ 0x5FFF_FFFF 。

如下图可以看到,物理地址 0x2000_0000 ~ 0x5FFF_FFFF 是 DDR 内存区域。

关于 DRAM 内存空间的区域,uboot 指定的地址详情如下图。详情可以参考博文:链接地址。

5.

base 的取值映射表的“表项”(物理地址)映射表的“表索引”(虚拟地址,以 M 字节划分)null00x600 (M)null00x601 (M)null00x602 (M)null00x603 (M)……… (M)null00x7FF (M)

虚拟地址范围 0x6000_0000 ~ 0x7FFF_FFFF,将其映射到物理地址 0 ,即无效地址。

如下图可以看到,物理地址 0x6000_0000 ~ 0x7FFF_FFFF 确实是 uboot 中未设置的 DDR 区域,不应该被访问。

所以将虚拟地址范围 0x6000_0000 ~ 0x7FFF_FFFF 映射为无效地址,是符合逻辑的。

6.

base 的取值映射表的“表项”(物理地址)映射表的“表索引”(虚拟地址,以 M 字节划分)0x8000x8000_00000x800 (M)0x8010x8010_00000x801 (M)0x8020x8020_00000x802 (M)0x8030x8030_00000x803 (M)……… (M)0xaFF0xaFF0_00000xaFF (M)

虚拟地址范围 0x8000_0000 ~ 0xaFFF_FFFF,将其映射到物理地址 0x8000_0000 ~ 0xaFFF_FFFF 。

如下图可以看到,物理地址 0x8000_0000 ~ 0xaFFF_FFFF 是 SROMC_BANK 区域。

7.

base 的取值映射表的“表项”(物理地址)映射表的“表索引”(虚拟地址,以 M 字节划分)0xb000xb000_00000xb00 (M)0xb010xb010_00000xb01 (M)0xb020xb020_00000xb02 (M)0xb030xb030_00000xb03 (M)……… (M)0xbFF0xbFF0_00000xbFF (M)

虚拟地址范围 0xb000_0000 ~ 0xbFFF_FFFF,将其映射到物理地址 0xb000_0000 ~ 0xbFFF_FFFF 。

如下图可以看到,物理地址 0xb000_0000 ~ 0xbFFF_FFFF 是 ONENAND/NAND 区域。

8.

base 的取值映射表的“表项”(物理地址)映射表的“表索引”(虚拟地址,以 M 字节划分)0x3000x3000_00000xc00 (M)0x3010x3010_00000xc01 (M)0x3020x3020_00000xc02 (M)0x3030x3030_00000xc03 (M)……… (M)0x3FF0x3FF0_00000xcFF (M)

虚拟地址范围 0xc000_0000 ~ 0xcFFF_FFFF,将其映射到物理地址 0x3000_0000 ~ 0x3FFF_FFFF 。

如下图可以看到,物理地址 0x3000_0000 ~ 0x3FFF_FFFF 是 uboot 设置的 DMC0 区域。

9.

base 的取值映射表的“表项”(物理地址)映射表的“表索引”(虚拟地址,以 M 字节划分)0xd000xd000_00000xd00 (M)0xd010xd010_00000xd01 (M)0xd020xd020_00000xd02 (M)0xd030xd030_00000xd03 (M)……… (M)0xFFF0xFFF0_00000xFFF (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 左右。这个空间既没有太浪费内存,又足够安全。

源自朱有鹏老师.

精彩文章

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。