RISCV start.S uboot 初始化总体框架解析
u-boot 运行在S-mode下
1 | + _start: |
上述过程中, riscv_send_ipi 发送ipi 中断给其他核, 其他核是在M-mode 的opensbi 下处理的ipi中断, 处理时会调用sbi_ipi_process_smode 函数将 MIP.SSIP 置位, 因为开启了mideleg的 MIP_SSIP 位, 会导致sip.SSIP 也会被置位. 返回给uboot后, 从核在secondary_hart_loop 中loop, 判断sip.SSIP & sie.SSIE, 这个时候都置位了, 从核跳出secondary_hart_loop, 执行后面的handle_ipi 函数
u-boot 设计规范
u-boot是一个bootloader,有些情况下,它可能位于系统的只读存储器(ROM或者flash)中,并从那里开始执行。
因此,这种情况下,在u-boot执行的前期(在将自己copy到可读写的存储器之前),它所在的存储空间,是不可写的,这会有两个问题:
- 堆栈无法使用,无法执行函数调用,也即C环境不可用。
- 没有data段(或者正确初始化的data段)可用,不同函数或者代码之间,无法通过全局变量的形式共享数据。
对于问题1,通常的解决方案是:
u-boot运行起来之后,在那些不需要执行任何初始化动作
即可使用的、可读写的存储区域,开辟一段堆栈(stack)空间。
一般来说,大部分的平台,都有自己的SRAM,可用作堆栈空间。如果实在不行,也有可借用CPU的data cache的方法(不再过多说明)。
对于问题2,解决方案要稍微复杂一些:
首先,对于开发者来说,在u-boot被拷贝到可读写的RAM(这个动作称作relocation)之前,永远不要使用全局变量。
其次,在relocation之前,不同模块之间,确实有通过全局变量的形式传递数据的需求。怎么办?这就是global data需要解决的事情。
为了在relocation前通过全局变量的形式传递数据,u-boot设计了一个巧妙的方法:
定义一个struct global_data类型的数据结构,里面保存了各色各样需要传递的数据
堆栈配置好之后,在堆栈开始的位置,为struct global_data预留空间(可参考后面的说明),并将开始地址(就是一个struct global_data指针)保存在一个寄存器中,后续的传递,都是通过保存在寄存器中的指针实现
board_init_f_alloc_reserve 的返回值(a0)就是global data的指针
前置的板级初始化操作
global data准备好之后,u-boot会执行前置的板级初始化动作, u-boot很有可能还在只读的存储器中
这个阶段写的代码需要follow u-boot的设计规范, 不能使用全局变量, 数据传递要使用gd 指针
进行各式各样的初始化动作。后面将会简单介绍一些和RISCV平台有关的、和平台的移植工作有关的、比较重要的API
arch_cpu_init
cpu级别的初始化操作,可以在需要的时候由CPU有关的code实现。
board_early_init_f
如果定义CONFIG_BOARD_EARLY_INIT_F,则调用board_early_init_f接口,执行板级的early初始化。平台的开发者可以根据需要,实现board_early_init_f接口,以完成特定的功能。
timer_init
初始化系统的timer。
该接口应该由平台或者板级的代码实现,初始化成功后,u-boot会通过其它的API获取当前的timestamp,后面用到的时候再详细介绍。
misc_init_f
如果使能了CONFIG_MISC_INIT_F,则调用misc_init_f执行misc driver有关的初始化。
dram_init
调用dram_init接口,初始化系统的DDR。dram_init应该由平台相关的代码实现。
如果DDR在SPL中已经初始化过了,则不需要重新初始化,只需要把DDR信息保存在global data中即可
DRAM空间的分配
DRAM初始化完成后,就可以着手规划u-boot需要使用的部分,如下图:
- 考虑到后续的kernel是在RAM的低端位置解压缩并执行的,为了避免麻烦,u-boot将使用DRAM的顶端地址,即gd->ram_top所代表的位置。其中gd->ram_top是由setup_dest_addr函数配置的。
- u-boot所使用的DRAM,主要分为三类:
- 各种特殊功能所需的空间,如log buffer、MMU page table、LCD fb buffer、trace buffer、等等;
- u-boot的代码段、数据段、BSS段所占用的空间(就是u-boot relocate之后的执行空间),由gd->relocaddr标示;
- 堆栈空间,从gd->start_addr_sp处递减。
- 特殊功能以及u-boot所需空间,是由reserve_xxx系列函数保留的,具体可参考source code,这里不再详细分析。
setup_reloc
计算relocation有关的信息,主要是 gd->reloc_off,计算公式如下:gd->reloc_off = gd->relocaddr - CONFIG_SYS_TEXT_BASE;
其中CONFIG_SYS_TEXT_BASE
是u-boot relocation之前在(只读)memory的位置(也是编译时指定的位置),gd->relocaddr是relocation之后的位置,因此gd->reloc_off代表u-boot relocation操作之后的偏移量,后面relocation时会用到。同时,该函数顺便把global data拷贝到了上图所示的“new global data”处
relocation的时间点,可以是“系统可读写memory始化完成之后“的任何时间点。根据u-boot当前的代码逻辑,是在board_init_f
执行完成之后,因为board_init_f中完成了很多relocation有关的准备动作
后置的板级初始化操作
relocate完成之后,真正的C运行环境才算建立了起来,接下来会执行“后置的板级初始化操作”,即board_init_r
函数。board_init_r和board_init_f的设计思路基本一样,也有一个很长的初始化序列—-init_sequence_r
,该序列中包含如下的初始化函数
- initr_trace,初始化并使能u-boot的tracing system,涉及的配置项有CONFIG_TRACE。
- initr_reloc,设置relocation完成的标志。
- initr_caches,使能dcache、icache等。
- initr_malloc,malloc有关的初始化。
- initr_dm,relocate之后,重新初始化DM,涉及的配置项有CONFIG_DM。
board_init
,具体的板级初始化,需要由board代码根据需要实现。- set_cpu_clk_info,Initialize clock framework,涉及的配置项有CONFIG_CLOCKS。
- initr_serial,重新初始化串口(不太明白什么意思)。
- initr_announce,宣布已经在RAM中执行,会打印relocate后的地址。
- board_early_init_r,由板级代码实现,涉及的配置项有CONFIG_BOARD_EARLY_INIT_R。
- arch_early_init_r,由arch代码实现,涉及的配置项有CONFIG_ARCH_EARLY_INIT_R。
- power_init_board,板级的power init代码,由板级代码实现,例如hold住power。
- initr_flash、initr_nand、initr_onenand、initr_mmc、initr_dataflash,各种flash设备的初始化。
- initr_env,环境变量有关的初始化。
- initr_secondary_cpu,初始化其它的CPU core。
- stdio_add_devices,各种输入输出设备的初始化,如LCD driver等。
- interrupt_init,中断有关的初始化。
- initr_enable_interrupts,使能系统的中断
- initr_status_led,状态指示LED的初始化,涉及的配置项有CONFIG_STATUS_LED、STATUS_LED_BOOT。
- initr_ethaddr,Ethernet的初始化,涉及的配置项有CONFIG_CMD_NET。
- board_late_init,由板级代码实现,涉及的配置项有CONFIG_BOARD_LATE_INIT。
- 等等…
- run_main_loop/main_loop,执行到main_loop,开始命令行操作。
厂商板级配置
board/<厂商>/
ex: board/thead/ice-c910/ice.c
通常只需要覆盖board_init, 配置时钟, 配置硬件资源
arch 相关修改
覆盖cleanup_before_linux
, 该函数会在跳转kernel 前执行
main 函数分析
一切就绪后进入交互状态,cli_init->cli_loop循环读取cmdline中的启动参数,将其存入console_buffer字符数组。若autoboot_command启动内核,则不会执行到cli_loop,若按键,则进入cli_loop函数,循环等待执行命令。
1 | + main_loop |
uboot中有一个小巧的命令解释器hush shell
,run_command_list只是对 hush shell 中的函数 parse_string_outer 进行了一层封装。parse_string_outer
函数调用了bush_shell的命令解释器, parse_stream_outer函数来解释bootcmd的命令,而环境变量bootcmd的启动命令用来设置linux必要的启动环境,board_run_command执行命令。然后加载和启动内核
bootcmd 可以理解为shell 命令行, 通常bootcmd必须包含几条命令
- 从flash的什么地方加载kernel到内存的什么位置, 与文件系统有关
- bootm 指令, 从内存的什么位置启动kernel
bootm
bootm这个命令用于启动一个操作系统镜像。它会从镜像文件的头部取得一些信息,这些信息包括:文件的基于的cpu架构、其操作系统类型、镜像类型、压缩方式、镜像文件在内存中的加载地址、镜像文件运行的入口地址、镜像文件名等。
紧接着bootm将镜像加载到指定的地址,如果需要的话,还会解压镜像并传递必要有参数给内核,最后跳到入口地址进入内核。
1 | U_BOOT_CMD( |
需要打开的宏
CONFIG_BOOTM_LINUX=y
CONFIG_CMD_BOOTM=y
uImage有两种格式
- Legacy-uImage,我们需要另外加载ramdisk和fdt到RAM上面
- FIT-uImage kernel镜像、ramdisk镜像和fdt都已经打包到FIT-uImage的镜像中了。
示例: 加载 kernel、ramdisk、fdtbootm 0x80008000 0x81000000 0x82000000
1 | + do_bootm "示例: argv[1]=0x80008000, arv[2]=0x81000000, argv[3]=0x82000000" |
状态说明
1 | BOOTM_STATE_START 开始执行 bootm 的一些准备动作。 |
在这些流程中,起传递作用的是 bootm_headers_t images
这个数据结构,有些流程是解析镜像,往这个结构体里写数据。
而跳转的时候,则需要使用到这个结构体里面的数据。
1 | typedef struct bootm_headers { |
LMB 的概念。LMB 是指 logical memory blocks,主要是用于表示内存的保留区域,主要有 fdt 的区域,ramdisk 的区域等等。
boot_prep_linux 主要的目的是修正 LMB,并把 LMB 填入到 fdt 中。
Gdb 调试
u-boot 重定位后很难继续用 gdb debug
使用”add-symbol-file” 命令重定位后,使用代码 offset = gd->relocaddr 重定位该符号:
#uboot_reloc_debug
1 | symbol-file build/u-boot --> only for "gd_t" definition |