于2022年3月12日2022年3月12日由Sukuna发布
随着嵌入式的发展,如今的嵌入式IC也基本上转向了SOC的方式,因而其IC内部不仅仅是一颗cpu核,可能包含各种各样的其他IP,因而相关的上层软件也需要针对性的划分不同的功能域,操作域,安全域等上层应用。为了能支持复杂而碎片化的应用需求,SOC的Boot阶段延伸出了数级的BootLoader,为行文简单,我们把第一级BootLoader称作BL0,下一级则为BL1、BL2……等。
我们使用的RISCV架构进行的此次开发学习,那么RISCV为了能统一设计思路,提出了标准的SBI规范,SBI即为 (RISC-V Supervisor Binary Interface),SBI作为直接运行在系统M模式(机器模式)下的程序,向上层OS提供了统一的系统调用环境,SBI程序拥有最高的权限,可以访问所有的硬件资源,同时其控制PMP等硬件级的权限管理单元,将系统划分为多个域(domain)以供上层不同的安全等级的多操作系统使用并不会造成数据侵入破坏。如下图所示,SBI位于S模式和M模式之间。
而RISCV官方在给出SBI标准规范的同时,也开发了一套开源代码opensbi,代码仓库位于github上。我们这里也就不需要自己编写符合SBI规范的固件代码,只需要移植opensbi即可,那么我们就将这个版本代码加入我们的开发环境,并添加相关的paltform。
板子的ROM引导一开始的LOADER.,LOADER过后就是OpenSBI,接着就是BOOTLOADER,最后就是加载OS,把OS投入运行.
目前opensbi的源码结构是比较简单清晰的,除了sbi目录内是核心文件,utils主要是用于设备树解析libfdt以及其他的驱动文件,当然这里的驱动都是使用设备树(设备树是从linux内核中广泛使用的一种设备描述文件,可以简化驱动代码的编写并提高驱动代码的复用率移植性,因此逐渐扩展到各个嵌入式平台级代码项目中)的方式的驱动。剩下的就是platform目录,用来定义属于板卡特有的代码。
首先是fw_base.lds,定义了fw_jump固件的链接脚本,当然此链接脚本是要经过c预处理宏展开后而使用的,但是不影响我们阅读。FW_TEXT_START定义了固件运行的的起始地址,然后依次放置text段,ro段,data段,bss段,bss段后将被用作栈空间.
/*
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2019 Western Digital Corporation or its affiliates.
*
* Authors:
* Anup Patel <anup.patel@wdc.com>
*/
. = FW_TEXT_START;
/* Don't add any section between FW_TEXT_START and _fw_start */
PROVIDE(_fw_start = .);
. = ALIGN(0x1000); /* Need this to create proper sections */
/* Beginning of the code section */
.text :
{
PROVIDE(_text_start = .);
*(.entry)
*(.text)
. = ALIGN(8);
PROVIDE(_text_end = .);
}
. = ALIGN(0x1000); /* Ensure next section is page aligned */
/* End of the code sections */
/* Beginning of the read-only data sections */
. = ALIGN(0x1000); /* Ensure next section is page aligned */
.rodata :
{
PROVIDE(_rodata_start = .);
*(.rodata .rodata.*)
. = ALIGN(8);
PROVIDE(_rodata_end = .);
}
/* End of the read-only data sections */
/* Beginning of the read-write data sections */
. = ALIGN(0x1000); /* Ensure next section is page aligned */
.data :
{
PROVIDE(_data_start = .);
*(.sdata)
*(.sdata.*)
*(.data)
*(.data.*)
*(.readmostly.data)
*(*.data)
. = ALIGN(8);
PROVIDE(_data_end = .);
}
.dynsym : {
PROVIDE(__dyn_sym_start = .);
*(.dynsym)
PROVIDE(__dyn_sym_end = .);
}
.rela.dyn : {
PROVIDE(__rel_dyn_start = .);
*(.rela*)
. = ALIGN(8);
PROVIDE(__rel_dyn_end = .);
}
. = ALIGN(0x1000); /* Ensure next section is page aligned */
.bss :
{
PROVIDE(_bss_start = .);
*(.sbss)
*(.sbss.*)
*(.bss)
*(.bss.*)
. = ALIGN(8);
PROVIDE(_bss_end = .);
}
/* End of the read-write data sections */
. = ALIGN(0x1000); /* Need this to create proper sections */
PROVIDE(_fw_end = .);
接着就是fw_base.S这个汇编文件,由于这个汇编文件特别长我就不一性贴出来.(图片仅供参考)
首先就是_start这个段:
_start:
/* Find preferred boot HART id */
MOV_3R s0, a0, s1, a1, s2, a2
call fw_boot_hart
add a6, a0, zero
MOV_3R a0, s0, a1, s1, a2, s2
li a7, -1
beq a6, a7, _try_lottery
/* Jump to relocation wait loop if we are not boot hart */
bne a0, a6, _wait_relocate_copy_done
首先判断一下是不是boot核心启动,如果是boot核心的话就执行fw_boot_hart函数,如果不是boot核心的话就跳转到_wait_relocate_copy_done,当然看代码也知道就是去执行_wait_for_boot_hart.其中fw_boot_hart是在fw_jump.S里面定义的函数.
_wait_for_boot_hart:
li t0, BOOT_STATUS_BOOT_HART_DONE
lla t1, _boot_status
REG_L t1, 0(t1)
/* Reduce the bus traffic so that boot hart may proceed faster */
nop
nop
nop
bne t0, t1, _wait_for_boot_hart
这个_wait_for_boot_hart本质上就是等待boot核加载完成,加载完成后就跳转到_start_warm了,看代码就知道这个程序正在观察_boot_status的情况.
如果是boot核的话就可以执行一系列boot的操作.可以发现如果opensbi如果不在自己的链接地址内运行,则会实现自身代码的拷贝到目标ram上运行,因此可以以类似spl的方式从flash中启动。但是在拷贝的时候要注意得获得拷贝的权限,就是执行了一遍_try_lottery.
#ifdef FW_PIN
....
#else:
/* Relocate if load address != link address */
_relocate:
lla t0, _link_start
REG_L t0, 0(t0)
lla t1, _link_end
REG_L t1, 0(t1)
lla t2, _load_start
REG_L t2, 0(t2)
sub t3, t1, t0
add t3, t3, t2
beq t0, t2, _relocate_done//地址相同,不用做
lla t4, _relocate_done
sub t4, t4, t2
add t4, t4, t0
blt t2, t0, _relocate_copy_to_upper//不同,直接开始移动
在搬迁执行完成的时候,我们已经确保所有的代码都已经加载到属于它的地址上,我们就可以从给定的地址开始执行我们的程序.
_relocate_done:
/*
* Mark relocate copy done
* Use _boot_status copy relative to the load address
*/
lla t0, _boot_status
#ifndef FW_PIC
lla t1, _link_start
REG_L t1, 0(t1)
lla t2, _load_start
REG_L t2, 0(t2)
sub t0, t0, t1
add t0, t0, t2
#endif
li t1, BOOT_STATUS_RELOCATE_DONE
REG_S t1, 0(t0)
fence rw, rw
/* At this point we are running from link address */
/* Reset all registers for boot HART */
li ra, 0
call _reset_regs
/* Zero-out BSS */
lla s4, _bss_start
lla s5, _bss_end
之后的流程是.bss段的清零和SP指针的初始化,这一步初始化堆栈.
_bss_zero:
REG_S zero, (s4)
add s4, s4, __SIZEOF_POINTER__
blt s4, s5, _bss_zero
/* Setup temporary trap handler */
lla s4, _start_hang
csrw CSR_MTVEC, s4
/* Setup temporary stack */
lla s4, _fw_end
li s5, (SBI_SCRATCH_SIZE * 2)
add sp, s4, s5
/* Allow main firmware to save info */
MOV_5R s0, a0, s1, a1, s2, a2, s3, a3, s4, a4
call fw_save_info
MOV_5R a0, s0, a1, s1, a2, s2, a3, s3, a4, s4
执行完代码的拷贝,.bss段的清零以及SP指针的初始化,接着开始执行平台的初始化,就是调用fw_platform_init函数即可,如果函数是没有定义的,那么就直接执行下方弱定义的空函数即可.这个platform init需要我们在编译open-sbi的时候指定何种paltform,指定了platform的种类才能找到指定的init函数,注意此时传入参数a0——hart id,a1——fdt地址,a2,a3,a4均为上级loader程序的传入参数.
#ifdef FW_FDT_PATH
/* Override previous arg1 */
lla a1, fw_fdt_bin
#endif
/*
* Initialize platform
* Note: The a0 to a4 registers passed to the
* firmware are parameters to this function.
*/
MOV_5R s0, a0, s1, a1, s2, a2, s3, a3, s4, a4
call fw_platform_init
add t0, a0, zero
MOV_5R a0, s0, a1, s1, a2, s2, a3, s3, a4, s4
add a1, t0, zero
/* Preload HART details
* s7 -> HART Count
* s8 -> HART Stack Size
*/
lla a4, platform
接着执行_scratch_init,scratch你可以认为就是另一个sp指针的东西,定义了一片内存用来存放一些数据,同栈一样,先进后出。_scratch_init其实是按顺序写入了sbi下一级程序的地址参数等信息.
对于scratch_init,它执行完之后会转而执行_fdt_reloc_again这个东西,其实也是relocate的意思,只不过这个是relocate fdt文件而已
_fdt_reloc_again:
REG_L t3, 0(t0)
REG_S t3, 0(t1)
add t0, t0, __SIZEOF_POINTER__
add t1, t1, __SIZEOF_POINTER__
blt t1, t2, _fdt_reloc_again
_fdt_reloc_done:
/* mark boot hart done */
li t0, BOOT_STATUS_BOOT_HART_DONE
lla t1, _boot_status
REG_S t0, 0(t1)
fence rw, rw
j _start_warm
准备到这里一切就就绪了,这个时候boot核会释放信号,所有核会一起执行_start_warm操作,_start_warm针对每一个核心复位寄存器建立自己的栈空间,配置trap异常等完成后调用sbi_init离开汇编代码的世界.最后一步就是调用sbi_init离开汇编代码.
sbi_init.c是sbi核心代码的开始,sbi_init函数主要分为冷启动和热启动,无论那种,主要功能均为对设备树进行解析,初始化相关硬件设备,比较重要的如irq,tlb,ipi,ecall,domain,PMP,timer,console,总之建立系统调用,配置硬件权限,定义系统mmio以及内存区域的划分,就是sbi要做的事情,完成后,所有domain从其boot核心启动到下级程序sbi的任务就完成了。其中init的调用顺序是这样的:sbi_domain_init–>sbi_hsm_init–>sbi_domain_finalize–>sbi_platform_final_init–>sbi_hsm_prepare_next_jump–>sbi_hart_switch_mode
在platform文件夹里面存储着很多个platform的信息,对于不同的platform有下面这个结构体来申明:
const struct sbi_platform_operations platform_ops = {
.nascent_init = generic_nascent_init,
.early_init = generic_early_init,
.final_init = generic_final_init,
.early_exit = generic_early_exit,
.final_exit = generic_final_exit,
.domains_init = generic_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 = generic_pmu_init,
.pmu_xlate_to_mhpmevent = generic_pmu_xlate_to_mhpmevent,
.get_tlbr_flush_limit = generic_tlbr_flush_limit,
.timer_init = fdt_timer_init,
.timer_exit = fdt_timer_exit,
};
struct sbi_platform platform = {
.opensbi_version = OPENSBI_VERSION,
.platform_version = SBI_PLATFORM_VERSION(0x0, 0x01),
.name = "Generic",
.features = SBI_PLATFORM_DEFAULT_FEATURES,
.hart_count = SBI_HARTMASK_MAX_BITS,
.hart_index2id = generic_hart_index2id,
.hart_stack_size = SBI_PLATFORM_DEFAULT_HART_STACK_SIZE,
.platform_ops_addr = (unsigned long)&platform_ops
};
//在sifive_fu740.c中有这个定义,在genetic中我们就是调用这个结构题对platform结构体进行修改的.
const struct platform_override sifive_fu740 = {
.match_table = sifive_fu740_match,
.tlbr_flush_limit = sifive_fu740_tlbr_flush_limit,
.final_init = sifive_fu740_final_init,
};
这个platform主要定义了各种信息以及各种处理函数的名字.
这里我就只强调下如下两个函数,fw_platform_init主要是通过fdt读取一些配置到platform结构中以灵活的通过设备树传入硬件信息,也是为了让platform代码适配vender厂商的一大类的板卡。final_init只是调用了fdt库的一些函数,但是十分重要,他针对平台初始化完成后将设备树做以修订为platform执行完毕后的状态信息,反应的设备真实的配置,是传入fdt与platform代码共同作用后的真正的设备信息。
比如说就用genetic来做例子,在这个函数主要通过一个override的结构体进行比较,首先先筛选出对应的override结构体,接着重新写入即可.
fw_platform_lookup_special(fdt, root_offset);
model = fdt_getprop(fdt, root_offset, "model", &len);
if (model)
sbi_strncpy(platform.name, model, sizeof(platform.name) - 1);
if (generic_plat && generic_plat->features)
platform.features = generic_plat->features(generic_plat_match);