市面上采用RISC-V架构的CPU很多,且没有如X86那样高度细节的标准,故采用说明文档详细的SiFive Freedom U540-C000芯片来做介绍(下面统一称为FU540)。
FU540支持多种启动方式,且由MSEL针脚控制。
在了解启动流程之前,首先需要明确RISC-V的三种启动模式
- M-mode(Machine Mode)
- S-mode(Supervisor Mode)
- U-mode(User Mode)
在系统加电启动后会处于M-mode,有关启动模式将在下文详细讲解。
通常,RISC-V启动顺序流程包含以下几个阶段:
RISC-V上游引导流程类似。ROM是ZSBL。FSBL加载器是SoC专用的。将由Coreboot和/或U-Boot SPL替代。运行时是OpenSBI。它提供运行时服务。U-Boot是OpenSBI中的有效负载。
- Zeroth Stage Boot Loader(ZSBL),安装在板载的ROM中,处于M-mode
- First Stage Boot Loader(FSBL),brings up PPLs and DDR, 处于M-mode
- Berkeley Boot Loader(BBL),adds emulation for soft instructions,处于M-mode
- User Payload,包含软件,如Linux,处于S-mode或U-mode
ZSBL和FSBL均依照MSEL(Mode Select)的设置(这些引脚要接到零或电源,表示高电平或低电平)加载下一阶段的bootloader,具体参照附录A。
1.1 复位向量(第一条指令)
Z待加电后,所有核心都跳转到0x1004,在这个位置的内存包含如下内容:
Reset vector ROM
所有核心将依照复位地址和MSEL跳转,详细信息如下:
1.2 ZSBL(第0阶段Bootloader)
处于M-mode的ZSBL保存在maskROM 中地址为0x1_0000的位置,它负责从GPT中加载更为复杂的FSBL(寻找编号为5B193300-FC78-40CD-8002-E86C45580B47的GPT分区)。通过先加载GPT的头文件,然后一块一块(块大小为512bytes)的顺序地扫描GPT。加载过程结束后,FSBL被加载进地址为0x0800_0000的L2 LIM缓存中,随后,将执行FSBL阶段。
ZSBL通过MSEL阵脚的值来决定从哪里寻找FSBL所在的分区,详细信息参照附录B。
1.3 FSBL(第1阶段Bootloader)
处于M-mode的FSBL从L2 LIM地址为0x0800_0000的缓存上执行,它负责为在DDR上运行系统做准备,可大概分为如下的这些任务:
- 通过配置芯片上的PLL将核心频率变为1GHz
- 配置DDR PLL,PHY和DDR控制器
- 将GEM GXL TX PLL设置为123MHz并重置它
- 如果是外部PHY,重置它
- 从编号为2E54B353-1271-4842-806F-E436D6AF6985的GTP分区下载BBL
- 扫描OTP获取的芯片序列号
- 将DTB(硬件设备树)复制到DDR,填写FSBL版本,内存大小和MAC地址
- 启用16个中15道L2缓存线路(这样做将移除几乎所有的L2 LIM缓存)
- 跳转到DDR上地址为0x8000_0000的位置
同样,FSBL也参照MSEL的设置决定去哪里寻找BBL所在的分区,详细信息参照附录D。
1.4 BBL(第2阶段的Bootloader)
处于M-mode的BBL从DDR上地址为0x8000_0000的位置执行。它负责为如SBI(Supervisor Binary Interface)等RISC-V需要的,但没有被芯片本身实现的指令。在进行写操作的时候,BBL通常包含一个嵌入的Linux内核负载,当SBI被初始化后,它将跳转的Linux内核。
当ZSBL与FSBL都从QSPI(Quad SPI)加载下一阶段的boot-loader。然而,其中使用的协议并不确定,具体由MSEL决定。
1.5 用户负载(预示boot完成)
在这一阶段,boot基本完成,待执行如转载操作系统(S-mode)等任务后,运行用户空间软件程序(U-mode)。
1.6 什么是U-Boot
U-Boot属于一种bootloader,简单来说,其作用就是从flash中读出内核,随后加载在内存中,最终初始化并启动操作系统内核。
具体来说,可以分为下述几个方面:
1)U-Boot主要作用是用来启动操作系统内核。体现在uboot最后一句代码就是启动内核。
2)U-Boot还要负责部署操作系统内核。体现在uboot最后的传参。
3)U-Boot中还有操作Flash等板子上硬件的驱动。例如串口要打印,ping网络成功,擦除、烧写flash是否成功等。
4)U-Boot还得提供一个命令行界面供人来操作。很简单,至少你能看到。
1.7 什么是SBI和OpenSBI(Open Supervisor Binary Interface)
SBI是一种接口规范,提供了RISC-V标准的S-mode OS与M-mode的SEE(Supervisor Execution Environment)的接口,一般来说操作系统不会直接接触硬件,而是通过调用SBI的接口。
Open正如其名,是一个开源的SBI具体实现,通过遵守开源协议,任何用户都可以修改和使用。
有了OpenSBI和U-Boot加持,大致启动流程变为下图:
OpenSBI的初始化流程如下:
(1)底层初始化:
- 判断hart(Hardware Thread) id
- 代码重定位(判断_load_start与_start函数是否一致)
- 清除除保存设备树地址的寄存器的值
- 清除bss段
- 设置栈指针(预留栈空间)
- 读取设备树中的设备信息
- 设备树重定位,为U-Boot提供信息
- 至此,底层初始化结束,执行sbi_init,进行正式的初始化程序
(2)设备初始化:
首先判断是通过S-mode还是M-mode启动
- sbi_domain_init 初始化动态加载的镜像的模块
2.sbi_platform_early_init 平台的早期初始化
3.sbi_console_init 控制台初始化,从这里开始,就可以使用串口输出了。
4.sbi_platform_irqchip_init irq中断初始化
5.sbi_ipi_init 核间中断初始化
6.sbi_tlb_init mmu的tlb表的初始化
7.sbi_timer_init timer初始化
8.sbi_hsm_prepare_next_jump 准备下一级的boot
(3)二级boot跳转,如加载U-Boot或者Linux内核
1.8 详细解读RISC-V的启动模式
M-mode(Machine Mode)
M-mode是最底层的模式,也是每一个标准 RISC-V 处理器必须要实现的模式,它拥有最高权限,这意味着他将使用物理地址直接运行在硬件上。当cpu加电后,将处于M-mode。
机器模式具备拦截和处理异常的能力,并且可以访问所有其他模式下的控制状态寄存器CSR (Control Status Register)。
下表是对 RISC-V 机器模式下的控制状态寄存器的汇总:
寄存器 |
功能简要说明 |
Machine ISA Register (misa) |
用于指示当前处理器所支持的指令集模块。 |
Machine Vendor ID Register (mvendorid) |
用于指示供应商 ID 的寄存器 |
Machine Architecture ID Register (marchid) |
处理器核架构编号 |
Machine Implementation ID Register (mimpid) |
提供了处理器实现版本的唯一编码 |
Hart ID Register (mhartid) |
运行当前代码的硬件线程(hart)的 ID |
Machine Status Registers (mstatus and mstatush) |
机器模式下的状态寄存器 |
Machine Trap-Vector Base-Address Register (mtvec) |
配置发生异常后的入口地址 |
Machine Trap Delegation Registers (medeleg and mideleg) |
机器异常委托寄存器和机器中断委托寄存器 |
Machine Interrupt Registers (mip and mie) |
控制中断使能与查询中断的状态的寄存器 |
Hardware Performance Monitor |
硬件性能监控的CSR寄存器,用于监控CPU的内部运行情况。 |
Machine Counter-Enable Register (mcounteren) |
仅控制计数器的可访问性。读或写这个寄存器的行为不会影响底层计数器。 |
Machine Counter-Inhibit CSR (mcountinhibit) |
控制计数器是否递增 |
Machine Scratch Register (mscratch) |
用于机器模式下临时性的保存某些数据。 |
Machine Exception Program Counter (mepc) |
用于保存进入异常前的指令的 PC 值。 |
Machine Cause Register (mcause) |
用于保存进入异常的原因。 |
Machine Trap Value Register (mtval) |
用于保存进入异常时的地址或者指令 |
S-mode(Supervisor Mode)
Supervisor Mode(S-mode),如下图的RISC-V Privilege Levels中所示,是Machine Mode(M-mode)上两层的模式。这意味着操作系统调用OpenSBI的抽象来完成非常底端的操作。这样会使在开发板上开发一个操作系统变得容易一些。在此模式下,运行如操作系统内核的软件,并通过OpenSBI接口调用底层设备。
RISC-V Privilege Levels
上图我们看到介于S-mode与M-mode之间是保留的,这里正是为SBI接口留有的位置,通常来说,是下图的OpenSBI接口实现。
U-mode(User-Mode)
在此模式下,处于用户空间,运行用户程序。
事实上,RISC-V芯片的启动流程还在不断地快速演变,进化中,下图给出了演变的历程。
通过遵守开源协议,任何用户都可以修改和使用OpenSBI。面对这些复杂的情况,需要制定一系列规则来规范化 RISC-V 的启动规则。下图给出了启动规范标准的演变历程。
ARM架构中的Hypervisor与OpenSBI的对比
类比ARM64芯片,可以一一对应出RISC-V的启动模式,但实际上在RISC-V芯片中,并不存在Hypervisor模式来承上启下,反之,使用的是上文所提到的SBI(Supervisor Binary Interface)规范下的接口,一般是OpenSBI。
(ARM) (RISC-V )
在ARM架构中,Hypervisor层承担了虚拟化的作用,承担了如内存管理、设备模拟、设备分配、异常处理、指令捕获、虚拟异常管理、中断控制器管理、调度、上下文切换、内存转换、多个虚拟地址空间管理等非常多的功能。
相比之下,SBI在RISC-V架构中充当了BIOS和在操作系统运行时为上层提供底层抽象的作用,功能较少。不过SBI也在不断发展中,可能在将来SBI会去承担虚拟化的功能。
1.9 关键名词解释
1.9.1 DDR 中的DLL/PLL
简单来说,DDL(Delay Locked Loop)和PLL(Phase Locked Loop)是一种维持信号稳定的电路,使内存能够更加高效地传输数据。
下图给出了无DLL/PLL与有DLL/PLL的情况
DLL/PLL通过连续地比较两个信号的关系并提供反馈来保持他们之间地固定关系。
现代系统使用同步通信来实现往返于存储系统中DRAM的高数据传输速率。 同步通信的系统使用时钟信号作为时序参考,因此可以以与该参考已知的关系来发送和接收数据。 维持这种关系的困难在于工艺,电压和温度变化会改变时钟和数据信号之间的时序关系,从而导致时序裕量(timing margin)降低。 随着信号速度的增加,该问题变得更加严重,从而限制了系统以更高的速度通信数据的能力。
DLL 用于维护时钟信号和输出数据信号之间的正时关系。DLL 的一个关键要素是相位探测器,它检测时钟和输出数据之间的相位差异。相位探测器检测此相差,并通过低通滤波器向可变延迟线发送控制信息,该延迟线可调整内部时钟的正时以保持所需的正时关系(PLL 使用电压控制振荡器来调整此正时关系)。维持这两个信号之间的相位关系的一个困难是,向相位检测器提供反馈的循环必须考虑到输出逻辑和输出驱动程序的正时特征。这一点很重要,因为它估计了时钟和输出驱动因素驱动的数据之间的相位差异。为了实现这一目标,模仿输出逻辑和输出驱动程序的行为特征的电路入到此反馈回路中,以模拟当过程、电压和温度变化时的时间延迟和行为变化。通过DLL和PLL以这种方式保持时钟和输出数据之间的正时关系,可以提高计时间距,并解决了提高信号速度的重要限制。
PLL 类似于 DL,但也可用于拆分或乘以外部系统时钟频率,用于芯片的其他部分。PLL 可用于向 DRAM 的核心提供较慢的时钟频率,而接口则以更高的时钟频率运行。以这种方式使用的PLL使DRAM核心预装,允许DRAM核心以较慢的频率运行(提高DRAM产量),同时允许接口以更高的速度运行,以提高系统性能。
1.9.2 DDR中PHY
DDR PHY是指芯片存储器的高速接口物理层,是SOC和外界存储之间数据地址传输的一个重要通道。它主要基于时钟上下沿分别采集数据来达到提高传输速率的目的。
内存子系统一般理解包含下面这些部分:
在BIOS的代码中除了按照硬件手册和用户的选择填寄存器外,还存在着一部分内存初始化代码MRC(Memory Reference Code),也叫Memory Training代码,主要用来调整时序和提高信号完整性。内存IO的频率十分高,微小的误差也会被放大,Memory Training通过 “训练” 得到一组对齐、补偿和参考电压参数,来平衡和对冲线路的差异和信号的噪声。
PHY是物理接口的部分(其所处结构大致为DDR controller接口ßàPHYßà外部DDR接口),它包括了内存的Training所需要的物理层支持。因此它的主要功能是作为一个硬件接口,处理时序,将信号以一个较好的时序发出。
1.9.3 GEM GXL TX PLL中的PLL
(1)TX:内置时钟
(2)PLL:(Phase Locked Loop)相锁环
1.9.4 DTB (硬件设备树)
硬件设备树是一种描述硬件资源的数据结构,它通过bootloader将硬件资源传给内核,使得内核和硬件资源描述相对独立。
直观来说,在Windows中,DTB即为下图中 “我的电脑” 属性里的 “设备管理器”。
设备树由一系列被命名的节点及其属性构成,一般来说,其包含的信息如下:
- CPU的数量和类别
- 内存基地址和大小
- 总线和桥
- 外设连接
- 中断控制器和中断使用情况
- GPIO控制器和GPIO使用情况
它基本上就是画一棵电路板上CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出Linux内核中的相关设备。
设备树的组成包括三部分,DTC(Device Tree Compiler),DTS(Device Tree Source)和DTB(Device Tree Blob)。
DTS: dts文件是对Device Tree的描述,放置在内核的/arch/arm/boot/dts目录。一个*.dts文件对应一个ARM的machine。dts文件描述了一个板子的硬件资源。以前写在mach-xxx文件中的内容被转成了dts文件。
DTC: DTC为编译工具,它可以将.dts文件编译成.dtb文件。
DTB: DTC编译*.dts生成的二进制文件(.dtb),bootloader在引导内核时,会预先读取.dtb到内存,进而由内核解析。
DTS与DTB可以通过DTC与FDTDUMP进行编译与反编译。
1.9.4.1 使用U-Boot借助DTB启动内核
现今的内核版本使用了Device Tree:
- 内核不再包含对硬件的描述,它以二进制的形式单独存储在另外的位置
2)Bootloader需要加载两个二进制文件:内核镜像和DTB
- 内核镜像仍然是uImage或者zImage
- DTB文件在arch/arm/boot/dts中,每一个board对应一个.dts文件
3)Bootloader通过r2寄存器来传递DTB地址,通过修改DTB可以修改内存信息,kernel command line,以及潜在的其它信息
启动过程总的归纳为:
- kernel入口处获取到uboot传过来的.dtb镜像的基地址
- 通过early_init_dt_scan()函数来获取kernel初始化时需要的bootargs和cmd_line等系统引导参数
- 调用unflatten_device_tree函数来解析dtb文件,构建一个由device_node结构连接而成的单向链表,并使用全局变量of_allnodes保存这个链表的头指针
- 内核获取of_allnodes链表信息来初始化内核其他子系统、设备等
1.9.5 FSBL中的L2 LIM相关步骤解释
Enable 15 of the 16 L2 ways (this removes almost all of the L2 LIM memory)
Ways为L2缓存hash时允许冲突的长度,此处FSBL允许L2缓存拥有长度为15的hash冲突长度。
1.9.6 OpenSBI的作用与具体案例(openSBI下的RISC-V裸机实现串口输出)
OpenSBI不仅起到了引导启动的作用,还提供了M-mode转换到S-mode的实现,同时,在S-mode下的操作系统内核可以通过这一实现访问M-mode的服务。
随后会介绍如何利用该服务在控制台输出Hello。
准备工作:
- 编译openSBI
- 安装qemu
- 安装交叉编译工具链
- 完善工程目录
- ├──build.sh ##编译脚本 (编译了s与main.c,通过link.ld链接)
- ├──entry.s ##入口函数(执行的入口函数,设置了堆的地址,并跳转到main)
- ├──fw_bin ##可执行的固件脚本
- │├──fw_jump.elf ##opensbi
- │├──hello.elf ##编译完成的固件
- │└──run.sh ##直接运行的脚本
- ├──link.ld ##链接文件(规定了程序的布局)
- ├──main.c ##主函数(调用openSBI,可以在S-mode访问M-mode的串口输出服务。)
- ├──readme.md
- └──sbi.h##sbi调用api
(5)在此文件夹下输入./run.sh即能在控制台看到输出的Hello
在进行M-mode服务访问时,采用ECALL进行SYSCALL
在SYSCALL过程中,ECALL会使用a0 与a7寄存器,分别保存调用参数和调用号。
在RISC-V架构中,S-mode不直接参与时钟中断和软件中断,而是使用ECALL来请求M-mode设置定时器或在代理处理器中断。
openSBI提供的服务接口如下表(串行输出至控制台仅使用了sbi_console_putchar接口)
Appendix:
A. 依照MSEL,ZSBL和FSBL的下一阶段启动媒介
B. 被ZSBL加载的FSBL地址
C. RAMBUS DDR3 PHY结构图
D. 被FSBL加载的BBL的位置
Reference:
[1] SiFive FU540-C000 Manual, https://sifive.cdn.prismic.io/sifive/b5e7a29c-d3c2-44ea-85fb-acc1df282e21_FU540-C000-v1p3.pdf
[2] An Introduction to RISC-V Bootflow, https://crvf2019.github.io/pdf/43.pdf
[3] The Standardized Boot flow for RISC-V Platforms, http://crva.ict.ac.cn/crvs2020/index/slides/3-8.pdf
[4] RISC-V64 opensbi启动过程, https://cloud.tencent.com/developer/article/1758282
[5] uboot作用和功能, https://blog.csdn.net/yilongdashi/article/details/87968572
[6] uboot启动流程概述_关于riscv启动部分思考, https://blog.csdn.net/weixin_39530149/article/details/112312779
[7] DLL/PLL on a DRAM, https://www.rambus.com/dllpll-on-a-dram/
[8] 内存为什么要Training? 内存初始化代码为什么是BIOS中的另类?, https://zhuanlan.zhihu.com/p/107898009
[9] 设备树的基本概念,https://zhuanlan.zhihu.com/p/69188823
[10] opensbi下的riscv64裸机系列编程,http://www.elecfans.com/d/1445926.html
[11] Rambus DDR3 PHY IP, https://www.rambus.com/interface-ip/ddrn-phys/ddr3-phy/
[12] ARMv8虚拟化,https://www.cnblogs.com/LoyenWang/p/13584020.html