ARM
环境:
处理器架构:arm64
uboot版本:uboot-2020.01
内核源码:linux-5.0
ubuntu版本:20.04.1
ATF版本:2.1
代码阅读工具:vim+ctags+cscope
一般嵌入式系统使用的都是对称多处理器(Symmetric Multi-Processor, SMP)系统,包含了多个cpu, 这几个cpu都是相同的处理器,如4核Contex-A53。但是在系统 启动阶段他们的地位并不是相同的,其中core0是主cpu(也叫引导处理器),其他core是从cpu(也叫辅处理器),引导cpu负责执行我们的启动加载程序如uboot,以及初始化内核,系统初始化完成之后主core会启动从处理器。
一般主处理器启动从处理器有以下三种:
(1).ACPI
**(2).spin-table **
(3).PSCI
第一种ACPI是高级配置与电源接口(Advanced Configuration and Power Interface)一般在x86平台用的比较多,而后两种spin-table(自旋表)和PSCI(电源状态协调协议 Power State Coordination)会在arm平台上使用,本系列 主要讲解后两种 。主要内容分为上下两篇如下:
上篇:
1.开场白
2.cpu启动的一些基本概念
3.支持spin-table情况
下篇:
4.支持psci情况
5.从处理器启动进入内核世界之后做了些什么
6.最后说两句
1)cpu启动的含义:cpu可以从内存中取指、译码、执行,当然内存可以是soc片内的sram,也可以是ddr。
2)我们要知道,程序为何可以在多个cpu上并发执行:他们有各自独立的一套寄存器,如:程序计数器pc,栈指针寄存器sp,通用寄存器等,可以独自 取指、译码、执行,当然内存和外设资源是共享的,多核环境下当访问临界区 资源一般 自旋锁来防止竞态发生。
3)soc启动流程:soc启动的一般会从片内的rom, 也叫bootrom开始执行第一条指令,这个地址是系统默认的启动地址,会在bootrom中由芯片厂家固化一段启动代码来加载启动bootloader到片内的sram,启动完成后的bootloader除了做一些硬件初始化之外做的最重要的事情是初始化ddr,因为sram的空间比较小所以需要初始化拥有大内存 ddr,最后会从网络/usb下载 或从存储设备分区上加载内核到ddr某个地址,为内核传递参数之后,然后bootloader就完成了它的使命,跳转到内核,就进入了操作系统内核的世界。
4)linux内核启动流程:bootloader将系统的控制权交给内核之后,他首先会进行处理器架构相关初始化部分,如设置异常向量表,初始化mmu(之后内核就从物理地址空间进入了虚拟地址空间的世界,一切是那么的虚无缥缈,又是那么的恰到好处)等等,然后会清bss段,设置sp之后跳转到C语言部分进行更加复杂通用的初始化,其中会进行内存方面的初始化,调度器初始化,文件系统等内核基础组件 初始化工作,随后会进行关键的从处理器的引导过程,然后是各种实质性的设备驱动的初始化,最后 创建系统的第一个用户进程init后进入用户空间执行用户进程宣誓内核初始化完成,可以进程正常的调度执行。
5)系统初始化阶段大多数都是主处理器做初始化工作,所有不用考虑处理器并发情况,一旦从处理器被bingup起来,调度器和各自的运行队列准备就绪,多个任务就会均衡到各个处理器,开始了并发的世界,一切是那么的神奇。
了解了关于cpu启动的一些基本概念,下面开始我们的正题,讲解arm64常用的两种cpu启动方式。首先,我们来看一下比较简单的自旋表的方式启动从处理器。
从bootloader说起(以uboot为例):首先,上电后主处理器和从处理器都会启动,执行uboot,从uboot的_start的汇编代码开始执行,主处理器在uboot中欢快的执行后启动内核,进入内核执行,而从处理器会执行到spin_table_secondary_jump中(注意:之前执行的代码,设置的寄存器都是各cpu独立的寄存器)
arch/arm/cpu/armv8/start.S:
19 .globl _start
20 _start:
...
151 #if defined(CONFIG_ARMV8_SPIN_TABLE) && !defined(CONFIG_SPL_BUILD)
152 branch_if_master x0, x1, master_cpu //判断是否为主cpu(core0),是跳转到master_cpu,否则往下走
153 b spin_table_secondary_jump //跳转执行
...
arch/arm/cpu/armv8/spin_table_v8.S:
9 ENTRY(spin_table_secondary_jump)
10 .globl spin_table_reserve_begin
11 spin_table_reserve_begin:
12 0: wfe
13 ldr x0, spin_table_cpu_release_addr
14 cbz x0, 0b
15 br x0
16 .globl spin_table_cpu_release_addr
17 .align 3
18 spin_table_cpu_release_addr:
19 .quad 0
20 .globl spin_table_reserve_end
21 spin_table_reserve_end:
22 ENDPROC(spin_table_secondary_jump)
在spin_table_secondary_jump中:首先会 执行wfe指令,使得从处理器睡眠等待 。如果被唤醒,则从处理器会判断spin_table_cpu_release_addr这个地址是否为0,为0则继续跳转到wfe处继续睡眠,否则跳转到spin_table_cpu_release_addr指定的地址处执行。
那么这个地址什么时候会被设置呢?答案是:主处理器在uboot中读取设备树的相关节点属性获得,我们来看下如何获得。 执行路径为:
do_bootm_linux
- >boot_prep_linux
- >image_setup_linux
- >image_setup_libfdt
- >arch_fixup_fdt
- >spin_table_update_dt
在spin_table_update_dt函数中做了几件非常重要的事情:
arch/arm/cpu/armv8/spin_table.c:
11 int spin_table_update_dt(void *fdt)
12 {
13 int cpus_offset, offset;
14 const char *prop;
15 int ret;
16 unsigned long rsv_addr = (unsigned long)&spin_table_reserve_begin;
17 unsigned long rsv_size = &spin_table_reserve_end -
18 &spin_table_reserve_begin;
19 //获取设备树的cpus节点的偏移
20 cpus_offset = fdt_path_offset(fdt, "/cpus");
21 if (cpus_offset < 0)
22 return -ENODEV;
23 //寻找每一个device_type属性为cpu的节点
24 for (offset = fdt_first_subnode(fdt, cpus_offset);
25 ¦ offset >= 0;
26 ¦ offset = fdt_next_subnode(fdt, offset)) {
27 prop = fdt_getprop(fdt, offset, "device_type", NULL);
28 if (!prop || strcmp(prop, "cpu"))
29 continue;
30
31 /*
32 ¦* In the first loop, we check if every CPU node specifies
33 ¦* spin-table. Otherwise, just return successfully to not
34 ¦* disturb other methods, like psci.
35 ¦*///获得enable-method属性,比较属性值是否为 "spin-table"(即是使用自旋表启动方式)
36 prop = fdt_getprop(fdt, offset, "enable-method", NULL);
37 if (!prop || strcmp(prop, "spin-table"))
38 return 0;
39 }
40
41 for (offset = fdt_first_subnode(fdt, cpus_offset);
42 ¦ offset >= 0;
43 ¦ offset = fdt_next_subnode(fdt, offset)) {
//找到cpu节点
44 prop = fdt_getprop(fdt, offset, "device_type", NULL);
45 if (!prop || strcmp(prop, "cpu"))
46 continue;
47 //重点:设置cpu-release-addr属性值为spin_table_cpu_release_addr的地址!
48 ret = fdt_setprop_u64(fdt, offset, "cpu-release-addr",
49 (unsigned long)&spin_table_cpu_release_addr);
50 if (ret)
51 return -ENOSPC;
52 }
53 //设置设备树的保留内存 :添加一个内存区域为16和17行描述的地址范围(这是物理地址)
54 ret = fdt_add_mem_rsv(fdt, rsv_addr, rsv_size);
55 if (ret)
56 return -ENOSPC;
57
58 printf(" Reserved memory region for spin-table: addr=%lx size=%lx\\n",
59 ¦ rsv_addr, rsv_size);
60
61 return 0;
62 }