• opensbi firmware源码分析(3)


    0. 序

    上一篇,这次我们从sbi_ipi_init接着看

    1. 初始化函数6:sbi_ipi_init

    该函数为实现核间软中断做初始化。核间软中断的硬件支持来自clint设备,把cilnt设备的相应比特位置1,即可触发对应hart的软中断。但是触发软中断的hart后续应该干嘛?这就需要额外的辅助信息了。

    opensbi的处理方法大致如下:有一个全局的ipi_event队列,队列中每个元素指向一个sbi_ipi_event_ops,这个结构体指示了发送ipi的额外动作(update, sync成员),以及收到ipi后如何处理(process成员)。v0.8版的opensbi共有三个预定义的sbi_ipi_event_ops:

    • ipi_smode_ops:用于实现sbi_send_ipi 这个sbi调用
    • ipi_halt_ops:用于实现sbi_shutdown这个sbi调用
    • tlb_ops:用于实现remote fence系列拓展

    sbi_ipi_event_create负责在全局的ipi_event队列中找到一个空指针,修改使其指向需要注册的sbi_ipi_event_ops,返回该指针在队列中的位置。在sbi_init中,冷启动的cpu为每个hart分配了一个ipi_data结构体,在发送软中断前设置ipi_data相应bit位为1,通过置1的位置在全局的ipi_event队列中找到相应的sbi_ipi_event_ops,调用其process函数来处理该软中断(具体的逻辑可以从看代码sbi_ipi_sendsbi_ipi_process函数)。

    /** IPI event operations or callbacks */
    struct sbi_ipi_event_ops {
    	/** Name of the IPI event operations */
    	char name[32];
    
    	/**
    	 * Update callback to save/enqueue data for remote HART
    	 * Note: This is an optional callback and it is called just before
    	 * triggering IPI to remote HART.
    	 */
    	int (* update)(struct sbi_scratch *scratch,
    			struct sbi_scratch *remote_scratch,
    			u32 remote_hartid, void *data);
    
    	/**
    	 * Sync callback to wait for remote HART
    	 * Note: This is an optional callback and it is called just after
    	 * triggering IPI to remote HART.
    	 */
    	void (* sync)(struct sbi_scratch *scratch);
    
    	/**
    	 * Process callback to handle IPI event
    	 * Note: This is a mandatory callback and it is called on the
    	 * remote HART after IPI is triggered.
    	 */
    	void (* process)(struct sbi_scratch *scratch);
    };
    
    static const struct sbi_ipi_event_ops *ipi_ops_array[SBI_IPI_EVENT_MAX];
    // 全局的ipi_event队列
    
    struct sbi_ipi_data {
    	unsigned long ipi_type;
    };
    int sbi_ipi_init(struct sbi_scratch *scratch, bool cold_boot)
    {
    	// 略去部分无关紧要的代码
    	int ret;
    	struct sbi_ipi_data *ipi_data;
    
    	if (cold_boot) {
    		ipi_data_off = sbi_scratch_alloc_offset(sizeof(*ipi_data),
    							"IPI_DATA");
    		ret = sbi_ipi_event_create(&ipi_smode_ops);
    		ipi_smode_event = ret;
    		ret = sbi_ipi_event_create(&ipi_halt_ops);
    		ipi_halt_event = ret;
    	} 
    	ipi_data = sbi_scratch_offset_ptr(scratch, ipi_data_off);
    	ipi_data->ipi_type = 0x00;
    
    	/* Platform init */
    	ret = sbi_platform_ipi_init(sbi_platform_ptr(scratch), cold_boot);
    
    	/* Enable software interrupts */
    	csr_set(CSR_MIE, MIP_MSIP);
    	// 因为clint的提供的核间中断是M态的软中断,所以需要开启MIP_MSIP位
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    sbi_platform_ipi_init函数则与之前是一样的逻辑,最终会选择到fdt_ipi_client这个driver。因为clint设备实际提供了软中断和时钟中断,这里只初始化了软中断部分相关的数据结构。关键的操作只有一步,就是记录clint设备的物理地址,这样就知道该把哪一位置1来触发核间中断。

    2. 初始化函数7:sbi_tlb_init

    该初始化函数也算是核间中断实现的一部分,主要负责前面谈到的remote fence

    int sbi_tlb_init(struct sbi_scratch *scratch, bool cold_boot)
    {
    	// 略去部分无关紧要的代码
    	if (cold_boot) {
    		tlb_sync_off = sbi_scratch_alloc_offset(sizeof(*tlb_sync),
    							"IPI_TLB_SYNC");
    		tlb_fifo_off = sbi_scratch_alloc_offset(sizeof(*tlb_q),
    							"IPI_TLB_FIFO");
    		tlb_fifo_mem_off = sbi_scratch_alloc_offset(
    				SBI_TLB_FIFO_NUM_ENTRIES * SBI_TLB_INFO_SIZE,
    				"IPI_TLB_FIFO_MEM");
    		ret = sbi_ipi_event_create(&tlb_ops);
    		// 这里注册了之前谈到的tlb_ops
    		tlb_event = ret;
    		tlb_range_flush_limit = sbi_platform_tlbr_flush_limit(plat);
    	} else {
    		if (!tlb_sync_off ||
    		    !tlb_fifo_off ||
    		    !tlb_fifo_mem_off)
    			return SBI_ENOMEM;
    		if (SBI_IPI_EVENT_MAX <= tlb_event)
    			return SBI_ENOSPC;
    	}
    
    	tlb_sync = sbi_scratch_offset_ptr(scratch, tlb_sync_off);
    	tlb_q = sbi_scratch_offset_ptr(scratch, tlb_fifo_off);
    	tlb_mem = sbi_scratch_offset_ptr(scratch, tlb_fifo_mem_off);
    
    	*tlb_sync = 0;
    
    	sbi_fifo_init(tlb_q, tlb_mem,
    		      SBI_TLB_FIFO_NUM_ENTRIES, SBI_TLB_INFO_SIZE);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    执行冷启动的cpu为每个hart分配了三块变量区域tlb_sync, tlb_fifo, tlb_fifo_mem,这些变量都是为实现remote fence准备的。

    struct sbi_fifo { // 对应变量 tlb_fifo
    	void *queue; // 指向 tlb_fifo_mem
    	spinlock_t qlock;
    	u16 entry_size;
    	u16 num_entries;
    	u16 avail;
    	u16 tail;
    };
    // 一个环形buffer,每个buffer元素是一个sbi_tlb_info,
    // sbi_tlb_info记录了remote fence的类型
    struct sbi_tlb_info { 
    	unsigned long start;
    	unsigned long size;
    	unsigned long asid;
    	unsigned long vmid;
    	unsigned long type;
    	struct sbi_hartmask smask; // 等待sync的hart的掩码
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    opensbi对remote fence的sbi调用的实现都是同步的,意味着调用返回后所有被请求的hart的同步都已被完成(我个人感觉这个东西也没办法做成异步?)
    基本的流程如下:

    • S态软件请求remote fence调用,陷入opensbi的sbi_ipi_send_many函数,sbi_ipi_send_many依次调用sbi_ipi_send,每次传入需要同步的hartid
    • sbi_ipi_send函数根据注册的ipi_event拿到tlb_ops,然后调用其update函数, 再发送核间软中断,最后回调tlb_opssync函数(这与上一节描述的机制一致)。
    • 简单来讲tlb_opsupdate就是向相应的hart的fifo buffer塞入一个sbi_tlb_info,指示具体的同步方式(虽然opensbi实际上会进行一些优化,在v0.8这个版本中,在塞入sbi_tlb_info前,会遍历该buffer查看是否一些sbi_tlb_info可以进行合并)
    • tlb_opssync函数负责等待同步完成:在需要同步的hart被触发软中断后,检查自己的ipi_data,据此从全局的ipi_event队列中拿到tlb_ops,调用它的process回调函数,process函数则负责根据队列中的sbi_tlb_info进行同步操作,同步完成后,查看smask成员,将smask中比特位为1对应的hart的tlb_sync变量置1,告知这些hart自己已经完成了同步(smask对应位为1表示相应的hart在tlb_opssync中等待该hart同步完成的消息)。
    • tlb_sync变量被置1后,等待的hart从sync函数返回,针对一个hart的remote fence结束,当sbi_ipi_send_many对所有需要同步的hart调用的sbi_ipi_send返回后(这里是串行的,一个sbi_ipi_send返回后再调用下一个),整个remote fence结束。

    3. 初始化函数8:sbi_timer_init

    int sbi_timer_init(struct sbi_scratch *scratch, bool cold_boot)
    {
    	// 略去部分无关紧要的代码
    	u64 *time_delta;
    	if (cold_boot) {
    		time_delta_off = sbi_scratch_alloc_offset(sizeof(*time_delta),
    							  "TIME_DELTA");
    	}
    
    	time_delta = sbi_scratch_offset_ptr(scratch, time_delta_off);
    	*time_delta = 0;
    
    	ret = sbi_platform_timer_init(plat, cold_boot);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    sbi_platform_timer_init和之前的sbi_platform_ipi_init函数如出一辙,负责初始化clint设备的时钟中断部分。

    clint_warm_timer_init函数的最后,设置了mtimecmp为-1,此时禁用了时钟中断,当S态软件调用sbi_set_timer时,时钟中断开启。

    int clint_warm_timer_init(void){
    	// ....
    	clint->time_wr(-1ULL,
    		       &clint->time_cmp[target_hart - clint->first_hartid]);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4. 初始化函数9:sbi_ecall_init

    struct sbi_ecall_extension {
    	struct sbi_dlist head;
    	unsigned long extid_start; // 这俩变量表示该sbi extension对应的
    	unsigned long extid_end;   // extension ID范围
    	int (* probe)(unsigned long extid, unsigned long *out_val);
    	int (* handle)(unsigned long extid, unsigned long funcid,
    		       unsigned long *args, unsigned long *out_val,
    		       struct sbi_trap_info *out_trap);
    };
    int sbi_ecall_init(void)
    {
    	int ret;
    
    	/* The order of below registrations is performance optimized */
    	ret = sbi_ecall_register_extension(&ecall_time);
    	ret = sbi_ecall_register_extension(&ecall_rfence);
    	ret = sbi_ecall_register_extension(&ecall_ipi);
    	ret = sbi_ecall_register_extension(&ecall_base);
    	ret = sbi_ecall_register_extension(&ecall_hsm);
    	ret = sbi_ecall_register_extension(&ecall_legacy);
    	ret = sbi_ecall_register_extension(&ecall_vendor);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    sbi_ecall_init负责组织一个双链表,链表中每个元素都是一个sbi_ecall_extension结构体,对应一个riscv sbi extension。比如前面提到的remote fence调用,对应的就是这里的ecall_rfence。在S态软件调用ecall陷入opensbi时,opensbi就会根据a7寄存器保存的extension id,在这个双链表中查找,来确定S态软件希望调用哪个extension。

    5. 初始化函数10:sbi_platform_final_init

    该函数负责做一些最后的首尾工作(主要是对设备树的内容进行一些调整),在generic平台下,会调用generic_final_init函数。

    static int generic_final_init(bool cold_boot)
    {
    	void *fdt;
    	int rc;
    
    	if (generic_plat && generic_plat->final_init) {
    		rc = generic_plat->final_init(cold_boot, generic_plat_match);
    		if (rc)
    			return rc;
    	}
    
    	if (!cold_boot)
    		return 0;
    
    	fdt = sbi_scratch_thishart_arg1_ptr();
    
    	fdt_cpu_fixup(fdt);
    	fdt_fixups(fdt);
    
    	if (generic_plat && generic_plat->fdt_fixup) {
    		rc = generic_plat->fdt_fixup(fdt, generic_plat_match);
    		if (rc)
    			return rc;
    	}
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    可以看到,如果之前没有被fw_platform_lookup_special截获,该函数就只会调用fdt_cpu_fixupfdt_fixups这两个函数,而对于暖启动的hart该函数则相当于空操作。

    fdt_cpu_fixup就是之前谈到的把opensbi无法启动的cpu(可能的情况有hartid大于128,该cpu在设备树中没有mmu-type这个property等等)在设备树中的状态修改为disable

    而在fdt_fixups中又调用了fdt_plic_fixupfdt_reserved_memory_fixup这两个函数。

    void fdt_fixups(void *fdt)
    {
    	fdt_plic_fixup(fdt, "riscv,plic0");
    
    	fdt_reserved_memory_fixup(fdt);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    前者负责把plic在设备中的interrupt-extended这个property中对应M态的外部中断的interrupt specifier修改为-1,在plic device tree binding中,-1表示禁用该context(因为S态软件没办法使用M态的中断)。而后者负责在设备树中增加一个/reserved-memory子节点,reserved-memory binding指示了一些物理内存区域需要OS特殊对待。在generic平台下,该函数在/reserved-memory下新增了一个子节点,指示OS不要使用opensbi所占用的这段物理内存。

    最后吐槽一下opensbi是如何修改设备树的。考虑到设备树在内存中是展平的,其实要修改是很麻烦的。opensbi采取了非常简单粗暴的做法。其中一个重要的辅助函数是fdt_open_intofdt_splice_

    fdt_open_into函数负责拓展strings block后面的free space的大小(参考dtb memory structure),实际干的事情就是修改fdt_header中的totalsize。而fdt_splice_函数负责平移设备树的内容。比如希望给某个node增加一个property,就把这个node后面的设备树内容向后平移相应的字节数,然后把property插入到多出的空隙中。每次对设备树的修改就需要把设备树的内容整体进行平移,这样做的效率是相当低的。

    6. the last work

    最后剩下的代码如下,wake_coldboot_harts负责把coldboot_done置1,这样暖启动的cpu从wait_for_coldboot函数中退出,陷入到sbi_hsm_hart_wait的循环等待中

    static void __noreturn init_coldboot(struct sbi_scratch *scratch, u32 hartid)
    {
    	// ....
    	wake_coldboot_harts(scratch, hartid);
    
    	init_count = sbi_scratch_offset_ptr(scratch, init_count_offset);
    	(*init_count)++;
    
    	sbi_hsm_prepare_next_jump(scratch, hartid);
    	sbi_hart_switch_mode(hartid, scratch->next_arg1, scratch->next_addr,
    			     scratch->next_mode, FALSE);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    最后,冷启动的cpu在sbi_hart_switch_mode中调用mret,控制权传递给下一级bootloader。a0寄存器存放冷启动cpu的hartida1存放设备树的地址。

    值得注意的是,在冷启动的cpu把控制权传递给bootloader时,此时暖启动的cpu仍处于sbi_hsm_hart_wait的循环等待中。依赖于bootloader或者OS在合适的时候调用sbi_hart_start来启动这些cpu。

    7. 总结

    整个firmware的工作可以总结为初始化各类M态的寄存器和自己相关的数据结构,为后面服务S态软件的各种请求做好准备。

    可以发现,目前riscv体系结构还是有很多问题的。从opensbi的实现中可以看出至少两点:

    • 时钟中断的处理:目前的riscv只定义了M态的时钟中断,每次时钟中断都需要陷入opensbi,由opensbi设置mip寄存器把时钟中断转发给S态。随后S态的软件又需要通过sbi_set_timer调用再次陷入opensbi,设置下次时钟中断的时间。不过前不久提出了riscv sstc extension,希望能够增加stimecmp寄存器来减少时钟中断的开销,貌似目前这个还没有加入到spec中。
    • remote fence机制:这个实现的开销实在太大了,对于像JVM这样的需要在运行时修改指令的程序,remote fence又是必需的,目前还不清楚是否有更好的解决方案。
  • 相关阅读:
    【视觉SLAM】Bags of Binary Words for Fast Place Recognition in Image Sequences
    《嵌入式 - 深入剖析STM32》STM32 启动流程详解(GCC)
    MFC Windows 程序设计[132]之打开按钮的启用与禁用
    二叉树层序遍历及判断完全二叉树
    Linux 软件安装目录
    【直播精彩回顾】Redis企业级数据库及欺诈检测方案!
    rosbag 详细使用
    DataX实现mysql全量数据同步到hdfs
    【2023研电赛】东北赛区一等奖作品:基于FPGA的小型水下无线光通信端机设计
    虚拟桌宠模拟器:VPet-Simulator,一个开源的桌宠软件, 可以内置到任何WPF应用程序
  • 原文地址:https://blog.csdn.net/passenger12234/article/details/126290115