1.riscv的多级启动引导流程介绍

之前的步骤都是板子的一些外设添加以及,上一节写了个简单的固件程序,目前已经实现了板子的cpu,MROM,SRAM,FLASH,DDR,UART和RTC

image-20240606105450786

参考:

  1. RISC-V体系结构的U-Boot引导过程_riscv uboot-CSDN博客
  2. RISC-V启动引导流程 spec - 方东信 - 博客园 (cnblogs.com)
  3. notes/多核启动基本逻辑 at master · wangzhou/notes (github.com)

三种主要的操作模式,riscv 规范定义了三种主要的操作模式

  1. U模式(用户模式),运行用户程序的模式,权限级别最低。不能直接访问I/O或特权指令或内核内存或其他进程。
  2. S模式(管理模式),大多数Linux内核或其他O/S运行的模式。通过I/O remap函数访问最特权的指令和I/O控制。内存管理单元可能打开或关闭。
  3. M模式(机器模式),机器模式:裸机程序/第一阶段引导加载程序和FSBL(First Stage Bootloader )在此模式下运行。FSBL以固件的形式存在

固件:固件(Firmware)是一种嵌入在硬件设备中的软件,用于控制硬件的功能和操作。它介于硬件和高级软件(如操作系统和应用程序)之间,提供基本的硬件抽象和控制。 通常存在ROM或者EEPROM,例如BIOS 或者 UEFI等,

uboot 可以运行在M模式或者 S模式下, 取决于它是否在SBI的固件初始化之前运行。

  1. SBISupervisor Binary Interface 是 S模式 和SEE之间的调用约定/接口。SEE`Supervisor Execution Environment S执行环境 ,其调用风格就像System call一样。OpenSBI是一个SBI实现,可以在不同的模式下与U-Boot一起使用。
  2. opensbi RISC-V Open Source Supervisor Binary Interface
  3. 如下图,ABI 是 应用与 S模式之间的接口约定, SBI 是 S模式与 M模式之间的接口约定

image-20240606114347807

多级启动流程

image-20240606115629524

  • 实线箭头代表加载的操作
  • 虚线箭头代表跳转操作

流程

  1. ROM上的代码负责对电源,时钟进行初始化设置,并且将loader的代码加载到SRAM上并跳转执行LOADER —- 对应到riscv_setup_rom_reset_vec
  2. LOADER 初始化DDR(其实也是一种ram),然后加载opensbi 固件到ddr,也可以直接跳转到bootloader执行
  3. 最后bootloader会加载os 并执行

流程引导的每个模式

  1. 每个步骤运行在哪个模式下,
  2. zsblZero Stage Boot Loader,运行在M模式下,事实上我们在qemu中是通过drive 将固件直接加载到了flash的地方,所以rom上不需要执行加载的操作
  3. fsbl,运行在M模式下,这里需要加载 opensbi固件,加载设备树,然后跳转opensbi执行
  4. opensbi,执行在M模式下
  5. 跳转执行uboot执行在S模式下
  6. os 运行在 S模式下

image-20240606152715952

内存布局

qemu模拟的riscv中,多核启动的流程都是先多个核竞争一个主核,由主核对共享资源进行初始化,然后其余从核进行自身的初始化

riscv_setup_rom_reset_vec 用于设置 RISC-V 处理器的复位向量地址。复位向量是处理器在复位(重启)后执行的第一条指令的地址。该函数通常用于配置处理器的启动过程,确保处理器在复位后从正确的地址开始执行代码。

fw_dynamic_info 结构体,这个结构体 包含了下一个阶段程序启动的地址,魔数,下一阶段CPU位于 S模式,初始化完毕后又调用rom_add_blob_fixed_as函数将fw_dynamic_info拷贝到rom的reset_vec之后,用于下一阶段的启动

此时内存的布局是这样的:

image-20240606151519375

上一章节,将启动固件加载在flash后的内存布局:

image-20240606152341573

image-20240606153011137

2.Opensbi介绍

SBI指的是 Supervisor Binary Interface,运行在 M模式下的程序,操作系统(S模式)通过SBI 来调用M模式的硬件资源 相当于上层系统运行时的系统调用 opensbi是一种开源sbi的实现

  • FW_PAYLOAD :下一引导阶段被作为 payload 打包进来,通常是 U-Boot 或 Linux。这是兼容 Linux 的 RISC-V 硬件所使用的默认 firmware 。
  • FW_JUMP :不直接包含下一个阶段的代码,跳转到一个固定地址,该地址上需存有下一个加载器。QEMU 的早期版本曾经使用过它。
  • FW_DYNAMIC :带有动态信息的固件,根据前一个阶段传入的信息加载下一个阶段。通常是 U-Boot SPL 使用它。现在 QEMU 默认使用 FW_DYNAMIC。

opensbi源码下载:Releases · riscv-software-src/opensbi (github.com)

下载1.2版本。

当前版本opensbi开发者倾向于不要让ic设计尚加入太多的板级支持代码,因此opensbi本身也需要加载一份设备树文件,opensbi通过解析设备树文件了解soc内部的硬件结构,进而使用标准的驱动代码对其进行配置使用。

因此我们目前可以确认:

  1. 首先系统从MROM 启动– BL0
  2. 然后跳转到 flash 的首地址 执行上一节编写的 lowlevelboot 程序 – BL1
  3. 然后将 flash 上的 opensbi程序和 所需要的设备树资源文件加载到ddr上并跳转执行 – BL2

基于qemu-riscv从0开始构建嵌入式linux系统ch5-2. 什么是多级BootLoader与opensbi(下) — 主页 (quard-star-tutorial.readthedocs.io)

设备树:设备树是从linux内核中广泛使用的一种设备描述文件,可以简化驱动代码的编写并提高驱动代码的复用率移植性,因此逐渐扩展到各个嵌入式平台级代码项目中

opensbi中fw_base.S汇编文件正是opensbi的启动所在

  1. .entry_start符号即为链接脚本中第一个代码段,上级loader程序加载完成后自然跳转到该地址指令执行。
  2. 首先启动代码进行判断非boot核心跳转_wait_for_boot_hart等待,boot核心先进行一次代码_relocate,可以发现如果opensbi如果不在自己的链接地址内运行,则会实现自身代码的拷贝到目标ram上运行,因此可以以类似spl的方式从flash中启动。当然我们因为已经使用了自己编写的loader程序,这段_relocate不会执行,
  3. 之后的流程是.bss段的清零和SP指针的初始化。接下来就是调用fw_platform_init函数,注意此时传入参数a0——hart ida1——fdt地址,a2,a3,a4均为上级loader程序的传入参数,这个函数由platform来实现如果不使用则该函数由弱定义空函数来代替,platform函数具体内容我们后面实现时再来看,此处暂时跳过。
  4. 接下来就是_scratch_init函数,scratch你可以认为就是另一个sp指针的东西,定义了一片内存用来存放一些数据,同栈一样,先进后出。_scratch_init其实是按顺序写入了sbi下一级程序的地址参数等信息,由工程内的预定于宏指定,其实这里对我们作用不大,因为我们使用设备树文件提供给opensbi来解析得到下一级启动地址等信息
  5. 在向下就是_fdt_reloc,和代码reloc类似,对fdt进行,我们的设计不会执行到这个,最后来到了_start_warm,此时boot核心将标志释放,其余等待在_wait_for_boot_hart的核心也将要跳转到_start_warm。_start_warm针对每一个核心复位寄存器建立自己的栈空间,配置trap异常等完成后调用sbi_init离开汇编代码的世界。

image-20240606210704400

3.Opensbi的移植

需要明确的:

  • 采用opensbi的固件是FW_JUMP,会被加载到DRAM0x80000000 处执行
  • 需要编写设备树编译将设备树的地址传递给Opensbirom上的fw_dynamic_info用不到
  • 需要编写在flash上运行代码将opensbi的固件加载到DRAM处 然后跳转执行

使用tree -d -L 2 查看源码目录的层次

image-20240606212336666

要为我们的quard_star 板卡 新增opensbi的支持,如下进行操作:

  1. 在 platform 文件下新建一个 名为 quard_star 的文件夹
  2. 在quard_star文件夹下新增三个文件Kconfigobjects.mkplatform.c
  3. 在quard_satr文件夹下新增一个文件夹configs,在configs目录下新建一个名为defconfig的文件

新增完的目录如下:

image-20240606213048514

quard_star/Kconfig

1
2
3
4
5
6
7
8
# SPDX-License-Identifier: BSD-2-Clause

config PLATFORM_QUARD_STAR
bool
select FDT
select FDT_DOMAIN
select FDT_PMU
default y

quard_star/objects.mk

配置固件为 jump 已经jump跳转地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#
# SPDX-License-Identifier: BSD-2-Clause
#

# Compiler flags
platform-cppflags-y =
platform-cflags-y =
platform-asflags-y =
platform-ldflags-y =

# Objects to build
platform-objs-y += platform.o

# Blobs to build
FW_JUMP=y
FW_TEXT_START=0x80000000
FW_JUMP_ADDR=0x0

quard_star/configs/defconfig

指定配置需要哪些硬件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
CONFIG_PLATFORM_ALLWINNER_D1=y
CONFIG_PLATFORM_ANDES_AE350=y
CONFIG_PLATFORM_RENESAS_RZFIVE=y
CONFIG_PLATFORM_SIFIVE_FU540=y
CONFIG_PLATFORM_SIFIVE_FU740=y
CONFIG_FDT_GPIO=y
CONFIG_FDT_GPIO_SIFIVE=y
CONFIG_FDT_I2C=y
CONFIG_FDT_I2C_SIFIVE=y
CONFIG_FDT_IPI=y
CONFIG_FDT_IPI_MSWI=y
CONFIG_FDT_IPI_PLICSW=y
CONFIG_FDT_IRQCHIP=y
CONFIG_FDT_IRQCHIP_APLIC=y
CONFIG_FDT_IRQCHIP_IMSIC=y
CONFIG_FDT_IRQCHIP_PLIC=y
CONFIG_FDT_RESET=y
CONFIG_FDT_RESET_ATCWDT200=y
CONFIG_FDT_RESET_GPIO=y
CONFIG_FDT_RESET_HTIF=y
CONFIG_FDT_RESET_SIFIVE_TEST=y
CONFIG_FDT_RESET_SUNXI_WDT=y
CONFIG_FDT_RESET_THEAD=y
CONFIG_FDT_SERIAL=y
CONFIG_FDT_SERIAL_CADENCE=y
CONFIG_FDT_SERIAL_GAISLER=y
CONFIG_FDT_SERIAL_HTIF=y
CONFIG_FDT_SERIAL_RENESAS_SCIF=y
CONFIG_FDT_SERIAL_SHAKTI=y
CONFIG_FDT_SERIAL_SIFIVE=y
CONFIG_FDT_SERIAL_LITEX=y
CONFIG_FDT_SERIAL_UART8250=y
CONFIG_FDT_SERIAL_XILINX_UARTLITE=y
CONFIG_FDT_TIMER=y
CONFIG_FDT_TIMER_MTIMER=y
CONFIG_FDT_TIMER_PLMT=y
CONFIG_SERIAL_SEMIHOSTING=y

quard_star/platform.c

设备树的理解:【Linux内核|驱动模型】设备树的展开unflatten_device_tree - 知乎 (zhihu.com)

fw_platform_init 函数,注意此时传入参数a0——hart ida1——fdt地址,a2,a3,a4均为上级loader程序的传入参数。

函数逻辑:

  1. 首先,通过解析设备树来获取平台的模型名称(”model” 属性),并将其存储在 platform.name 变量中。
  2. 接下来,在设备树的 “/cpus” 路径下遍历处理器节点,获取每个处理器的 hartid(处理器标识符)。
  3. 根据获取的 hartid,将其存储在 quard_star_hart_index2id 数组中,并增加 hart_count 变量的计数。
  4. 最后,设置 platform.hart_count 变量为 hart_count,表示平台上处理器的数量。
  5. 函数返回 arg1,即原始的设备树指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
unsigned long fw_platform_init(unsigned long arg0, unsigned long arg1,
unsigned long arg2, unsigned long arg3,
unsigned long arg4)
{
const char *model;
void *fdt = (void *)arg1;
u32 hartid, hart_count = 0;
int rc, root_offset, cpus_offset, cpu_offset, len;

root_offset = fdt_path_offset(fdt, "/");
if (root_offset < 0)
goto fail;
// 获取平台模型名称
model = fdt_getprop(fdt, root_offset, "model", &len);
if (model)
sbi_strncpy(platform.name, model, sizeof(platform.name));

cpus_offset = fdt_path_offset(fdt, "/cpus");
if (cpus_offset < 0)
goto fail;

fdt_for_each_subnode(cpu_offset, fdt, cpus_offset) {
rc = fdt_parse_hart_id(fdt, cpu_offset, &hartid);
if (rc)
continue;

if (SBI_HARTMASK_MAX_BITS <= hartid)
continue;

quard_star_hart_index2id[hart_count++] = hartid;
}

platform.hart_count = hart_count;

/* Return original FDT pointer */
return arg1;

fail:
while (1)
wfi();
}

platform_ops 结构体 其结构体类型为const struct sbi_platform_operations,用于指定平台的相关的操作函数

每个成员对应一个平台相关的操作函数,用于在opensbi初始化过程中进行特定的操作和配置,每个函数在相应的阶段被调用,以完成平台相关的初始化,配置和资源管理等工作

拓展:

.early_init 使用点号(.)在结构体初始化时指定成员变量的名称是一种称为“指定初始化器”(Designated Initializers)的语法这种语法允许你在初始化结构体时明确地指出要初始化的成员变量,从而提高代码的可读性和可维护性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const struct sbi_platform_operations platform_ops = {
.early_init = quard_star_early_init, //早期初始化,不需要
.final_init = quard_star_final_init, //最终初始化,需要
.early_exit = quard_star_early_exit, //早期退出,不需要
.final_exit = quard_star_final_exit, //最终退出,不需要
.domains_init = quard_star_domains_init, //从设备树填充域,需要
.console_init = fdt_serial_init, //初始化控制台
.irqchip_init = fdt_irqchip_init, //初始化中断
.irqchip_exit = fdt_irqchip_exit, //中断退出
.ipi_init = fdt_ipi_init, //中断通信
.ipi_exit = fdt_ipi_exit,
.pmu_init = quard_star_pmu_init, //电源配置
.pmu_xlate_to_mhpmevent = quard_star_pmu_xlate_to_mhpmevent,
.get_tlbr_flush_limit = quard_star_tlbr_flush_limit, //需要
.timer_init = fdt_timer_init,
.timer_exit = fdt_timer_exit,
};

每个函数的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
static int quard_star_early_init(bool cold_boot)
{

return 0;

}

static int quard_star_final_init(bool cold_boot)
{
void *fdt;

if (cold_boot)
fdt_reset_init();
if (!cold_boot)
return 0;

fdt = sbi_scratch_thishart_arg1_ptr();

fdt_cpu_fixup(fdt);
fdt_fixups(fdt);
fdt_domain_fixup(fdt);

return 0;
}

static void quard_star_early_exit(void)
{

}

static void quard_star_final_exit(void)
{

}

static int quard_star_domains_init(void)
{
return fdt_domains_populate(fdt_get_address());
}

static int quard_star_pmu_init(void)
{
return fdt_pmu_setup(fdt_get_address());
}

static uint64_t quard_star_pmu_xlate_to_mhpmevent(uint32_t event_idx,
uint64_t data)
{
uint64_t evt_val = 0;

/* data is valid only for raw events and is equal to event selector */
if (event_idx == SBI_PMU_EVENT_RAW_IDX)
evt_val = data;
else {
/**
* Generic platform follows the SBI specification recommendation
* i.e. zero extended event_idx is used as mhpmevent value for
* hardware general/cache events if platform does't define one.
*/
evt_val = fdt_pmu_get_select_value(event_idx);
if (!evt_val)
evt_val = (uint64_t)event_idx;
}

return evt_val;
}
static u64 quard_star_tlbr_flush_limit(void)
{
return SBI_PLATFORM_TLB_RANGE_FLUSH_LIMIT_DEFAULT;
}

platform结构体

1
2
3
4
5
6
7
8
9
10
struct sbi_platform platform = {
.opensbi_version = OPENSBI_VERSION, // 版本号
.platform_version = SBI_PLATFORM_VERSION(0x0, 0x01), // 平台的版本号
.name = "Quard-Star", // 平台名称
.features = SBI_PLATFORM_DEFAULT_FEATURES, // 平台默认特征
.hart_count = SBI_HARTMASK_MAX_BITS, // 平台的处理器(hart,riscv叫做hart)数量
.hart_index2id = quard_star_hart_index2id, // 指向处理器标识符数组的指针,用于索引到唯一的处理器标识符
.hart_stack_size = SBI_PLATFORM_DEFAULT_HART_STACK_SIZE, // 指定了每个 hart的默认堆栈大小
.platform_ops_addr = (unsigned long)&platform_ops // 平台操作函数的指针,用于指定平台操作函数的地址
};