一、启动模式
通过查看D1-H芯片手册,可知芯片内核有一段片上内存,且其支持以下几种引导介质启动:
- SD card
- eMMC
- SPI NOR Flash
- SPI NAND Flash
并且D1芯片通过GPIO pin和eFuse模块来选择引导介质类型。那么D1芯片又是如何从这些引导介质中将fsbl程序load出来并运行的呢?
1.1、BROM地址空间分配
首先查看芯片内部的地址分布:

从手册上看到基地址0~0x20000的128KB地址空间没有被分配出来,应该是用作D1芯片的内部BROM使用,但是对于实际BROM使用了多少空间,我们并不确定,也不需要关心。并且在启动过程中,D1-H开始从地址0x0获取第一个指令开始运行,而此处地址正是BROM所在位置处。
1.2、BROM的作用
BROM系统分为两部分:固件交换启动(FEL)模块和媒介启动模块。FEL负责将外部数据写入本地NVM(非易失性存储器),比如将fsbl烧录到启动媒介中;媒介启动模块负责从NVM加载一个有效和合法的BOOT0并运行,比如将启动媒介中的fsbl加载到soc内部ram中运行。
D1上有一个FEL引脚,当soc上电检测到此引脚拉低时,BROM就会进入到FEL功能模块,进入FEL升级流程;而当FEL上电拉高时,将进入到正常启动流程。
而在正常启动流程中,有一个boot mode可以用于选择,是使用gpio来决定启动介质,还是eFuse来决定启动介质:

不过通过我查看文档,并未发现BOOT_MODE应该由谁来决定的。不知是由soc特定引脚输入状态来决定,还是BROM固件自身决定的?如果有小伙伴知道的,请在评论区指正。
- GPIO选择启动介质
如果是由GPIO引脚来决定启动介质,则还会有两个GPIO引脚来选择启动介质:

- eFuse选择启动介质
如果是由eFuse来决定启动介质,则是通过eFuse_Boot_Select_Cfg配置组来进行选择的。它们被分为4组,每组为3位;也就是说有eFuse_Boot_Select_Cfg1~eFuse_Boot_Select_Cfg4的4组启动介质配置,并且eFuse_Boot_Select_Cfg1配置的优先级最高,eFuse_Boot_Select_Cfg4的启动介质优先级最低。在启动过程中,应该就是依次按照配置组的优先级顺序来依次进行启动。
其中每组的配置方法如下表所示:

eFuse_Boot_Select_Cfg1~eFuse_Boot_Select_Cfg4配置组的参数值,可以在SID module寄存器的位[27:16]进行读取(寄存器地址为: 0x03006210)。
由于我也没有在手册上找当相关eFuse_Boot_Select_Cfg配置组的配置方法,所以目前我也默认为由BROM固件中进行的配置。如果有小伙伴知道的,同样请在评论区指正。
二、启动过程
启动模式完成设置后,现在假设我们启动媒介设置为spi nand flash。即在上电之后,BROM首先从spi nand flash中将fsbl读取到D1芯片内部sram中运行,并通过fsbl代码进行DDR的初始化;最后由fsbl将opensbi读取到DDR中开始执行。
通过查看芯片内部的地址分布,可以看出D1芯片内部sram可用空间大小为:160KB。所以fsbl程序的大小也需要控制在此容量大小以内。全志为D1芯片提供了SPL代码作为fsbl程序,其运行所在的空间就位于此芯片内部的sram中,基地址为0x20000。通过查看链接脚本和boot0镜像的段描述信息,可得上述结论:

由上图可知,在代码段开始还有一个.head段,其大小为0x3c8字节。
2.1、head段作用分析
通过查看main/boot0_head.c文件,发现仅定义了一个boot0_file_head_t BT0_head 结构体变量,并将其链接到0x20000地址处。因此D1芯片运行SPL的第一句代码就是BT0_head 结构体中的第一个4字节成员JUMP_INSTRUCTION宏。
cpp
#define BROM_FILE_HEAD_SIZE (sizeof(boot0_file_head_t) & 0x00FFFFF)
#define BROM_FILE_HEAD_BIT_10_1 ((BROM_FILE_HEAD_SIZE & 0x7FE) >> 1)
#define BROM_FILE_HEAD_BIT_11 ((BROM_FILE_HEAD_SIZE & 0x800) >> 11)
#define BROM_FILE_HEAD_BIT_19_12 ((BROM_FILE_HEAD_SIZE & 0xFF000) >> 12)
#define BROM_FILE_HEAD_BIT_20 ((BROM_FILE_HEAD_SIZE & 0x100000) >> 20)
#define BROM_FILE_HEAD_SIZE_OFFSET ((BROM_FILE_HEAD_BIT_20 << 31) | \
(BROM_FILE_HEAD_BIT_10_1 << 21) | \
(BROM_FILE_HEAD_BIT_11 << 20) | \
(BROM_FILE_HEAD_BIT_19_12 << 12))
// 计算出boot0_file_head_t结构体大小,并跳转到BT0_head头之后的地址
#define JUMP_INSTRUCTION (BROM_FILE_HEAD_SIZE_OFFSET | 0x6f)
const boot0_file_head_t BT0_head = {
{
/* jump_instruction*/
JUMP_INSTRUCTION, // 宏定义,机器码0x3c80006f
BOOT0_MAGIC,
STAMP_VALUE,
......
},
......
};
其中JUMP_INSTRUCTION宏是跳转机器码(即0x3c80006f),其含义为跳转到当前PC值 + 0x3c8地址处,也就是0x203c8。这个地址刚好是BT0_head 结构体之后的位置处,也是代码段的起始位置。
因此head段的作用大致为:
- 跳转到真正的代码段运行;
- 定义一些平台、模式、ddr、串口相关信息等。
所以此部分头信息非常重要,需要针对自己板卡硬件来进行定义,否则会导致SPL启动失败。对于哪吒D1开发板来说,我将系统正常时nand中SPL前0x3c8个字节的数据读出来,并替换掉我编译出来的bin文件头部后,能正常启动,否则会启动失败。(因此我怀疑全志SDK在进行编译打包整体镜像时,将SPL镜像的头部进行了替换)
2.2、启动汇编分析
SPL的真正入口点在boot0_entry.S汇编文件中的_start。
cpp
_start:
/*disable interrupt*/
csrw mie, zero
/*enable theadisaee*/
li t1, EN_THEADISAEE
csrs mxstatus, t1
/*invaild ICACHE/DCACHE/BTB/BHT*/
li t2, 0x30013
csrs mcor, t2
li sp, CONFIG_NBOOT_STACK
jal clear_bss
jal main
j .
clear_bss:
la t0, __bss_start
la t1, __bss_end
clbss_1:
sw zero, 0(t0)
addi t0, t0, REGBYTES
blt t0, t1, clbss_1
ret
主要作用为关闭中断、无效指令与数据cache、设置0x48000为栈顶(栈大小为0x20000)、清bss段以及跳转到c环境的main函数中。
2.3、main函数分析
根据BT0_head结构体中的描述信息对uart串口、debug打印等级、PLL、DDR、Dcache、堆分配器进行初始化,最后再根据下一阶段启动目标的信息,进行加载opensbi等下级启动目标镜像进行跳转启动。
目前堆的基地址为0x40800000,大小为16MB。
接下来将着重分析跳转部分相关功能,这个与启动流程密切相关,需要着重分析。而其余串口、DDR和PLL初始化等操作,仅需了解即可。
cpp
// 初始化spi nand,同时从spi nand中读取后续启动包到0x41000000地址处的内存中
status = load_package();
if (status == 0)
// 从0x41000000拷贝镜像到对应的运行内存地址处
load_image(&uboot_base, &optee_base, &monitor_base, &rtos_base, &opensbi_base);
else
goto _BOOT_ERROR;
update_uboot_info(uboot_base, optee_base, monitor_base, rtos_base, dram_size,
pmu_type, uart_input_value, key_input);
mmu_disable( );
printf("Jump to second Boot.\n");
if (opensbi_base) {
// 跳转到opensbi中执行,同时传入a0、a1参数,a1为uboot的运行地址
boot0_jmp_opensbi(opensbi_base, uboot_base);
} else if (monitor_base) {
struct spare_monitor_head *monitor_head =
(struct spare_monitor_head *)((phys_addr_t)monitor_base);
monitor_head->secureos_base = optee_base;
monitor_head->nboot_base = uboot_base;
boot0_jmp_monitor(monitor_base);
} else if (optee_base)
boot0_jmp_optee(optee_base, uboot_base);
else if (rtos_base) {
printf("jump to rtos\n");
boot0_jmp(rtos_base);
}
else
boot0_jmp(uboot_base);
while(1);
由上述代码可知,在进行跳转时,进行了如下操作:
- 初始化spi接口与spi nand flash,同时读取spi nand flash中的下阶段启动包到0x41000000内存地址处;
- 解析启动包中的头部信息,并将0x41000000内存中的opensbi与uboot镜像等拷贝到ddr运行地址处;
- 判断是否存在opensbi,如果存在则跳转到opensbi开始运行。
各个镜像的运行地址都存储在启动包的头部信息中,实际地址如下图所示:

地址分布结构图如下:

最后SPL执行完毕后,将会跳转到opensbi开始执行,至此SPL的启动流程就分析完毕。