RISC-V 64架构专题二(D1芯片SPL启动分析)

一、启动模式

通过查看D1-H芯片手册,可知芯片内核有一段片上内存,且其支持以下几种引导介质启动:

  1. SD card
  2. eMMC
  3. SPI NOR Flash
  4. 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段的作用大致为:

  1. 跳转到真正的代码段运行;
  2. 定义一些平台、模式、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);

由上述代码可知,在进行跳转时,进行了如下操作:

  1. 初始化spi接口与spi nand flash,同时读取spi nand flash中的下阶段启动包到0x41000000内存地址处;
  2. 解析启动包中的头部信息,并将0x41000000内存中的opensbi与uboot镜像等拷贝到ddr运行地址处;
  3. 判断是否存在opensbi,如果存在则跳转到opensbi开始运行。

各个镜像的运行地址都存储在启动包的头部信息中,实际地址如下图所示:

地址分布结构图如下:

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

相关推荐
嵌入式小企鹅17 小时前
RISC-V车规专委会成立、AI模型集中开源、半导体产能加速爬坡
人工智能·学习·ai·程序员·算力·risc-v·半导体
国科安芯18 小时前
空间激光通信系统中抗辐射 MCU 芯片应用研究
单片机·嵌入式硬件·架构·risc-v·安全性测试
极创信息2 天前
信创领域五种主流CPU架构(X86 / ARM / RISC-V / MIPS / LoongArch)
java·arm开发·数据库·spring boot·mysql·软件工程·risc-v
嵌入式小企鹅3 天前
CPU需求变化、RISC-V安全方案、DeepSeek V4适配、太空算力动态
人工智能·驱动开发·华为·开源·算力·risc-v
国科安芯4 天前
商业航天与航空安全场景下抗辐射 MCU 选型、应用实践及发展趋势
单片机·嵌入式硬件·无人机·cocos2d·risc-v
国科安芯5 天前
空间辐射环境下抗辐射 MCU 可靠性机理及航空安全应用研究综述
单片机·嵌入式硬件·macos·无人机·cocos2d·risc-v
国科安芯5 天前
航空安全关键系统抗辐射 MCU 加固技术、工程实现与典型应用
单片机·嵌入式硬件·无人机·cocos2d·risc-v
Captain_Data5 天前
AI 12小时设计CPU完整解析:从219字到RISC-V内核的技术突破
人工智能·python·ai·大模型·芯片设计·risc-v
圆山猫7 天前
[RISCV] 用 Rust 写一个 RISC-V BootROM:从 QEMU 到真实硬件(2)
rust·risc-v
嵌入式小企鹅7 天前
算力价值重估、AI编程模型齐开源、RISC-V融资15亿
人工智能·学习·ai·程序员·risc-v·前沿科技·太空算力