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;
}

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;
}

执行冷启动的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的掩码
};

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;
}

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]);
}

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;
}

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;
}

可以看到,如果之前没有被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);
}

前者负责把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);
}

最后,冷启动的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的sepc只完整定义了M态的时钟中断如何处理(当然平台可以进行拓展,自定义其它的S态时钟中断的处理方式),对于S态,每次时钟中断都需要陷入opensbi,由opensbi设置mip寄存器把时钟中断转发给S态。随后S态的软件又需要通过sbi_set_timer调用再次陷入opensbi,设置下次时钟中断的时间。不过前不久提出了riscv sstc extension,希望能够增加stimecmp寄存器来减少时钟中断的开销,貌似目前这个还没有加入到spec中。
  • remote fence机制:这个实现的开销实在太大了,对于像JVM这样的需要在运行时修改指令的程序,remote fence又是必需的,目前还不清楚是否有更好的解决方案。
  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
CWMP(CPE WAN Management Protocol)是一种用于管理宽带设备的协议,而TR-069(Technical Report 069)是基于CWMP的规范。 CWMP/TR-069的分析涉及到底层协议的实现以及相关的功能模块。由于CWMP/TR-069的实现方式可能因厂商而异,所以我无法提供具体的分析。但是,我可以给你一些常见的功能模块,供你参考: 1. 数据模型(Data Model):CWMP/TR-069使用一种基于XML的数据模型来描述设备的参数和状态。码中会涉及到数据模型的解析、更新和存储。 2. 连接管理(Connection Management):CWMP/TR-069使用HTTP或者HTTPS协议与管理服务器进行通信。码中会涉及到建立连接、发送请求和接收响应等操作。 3. 参数配置(Parameter Configuration):CWMP/TR-069允许管理服务器远程配置设备的参数。码中会涉及到参数的读取、写入和验证等操作。 4. 远程诊断(Remote Diagnostics):CWMP/TR-069允许管理服务器对设备进行远程诊断。码中会涉及到收集诊断信息、发送诊断请求和解析诊断结果等操作。 5. 固件升级(Firmware Upgrade):CWMP/TR-069允许管理服务器远程升级设备的固件。码中会涉及到固件的下载、验证和安装等操作。 这些只是功能模块的一部分,CWMP/TR-069的分析需要深入研究具体实现的代码。希望这些信息能对你有所帮助!如果你有具体的问题,欢迎继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值