コグノスケ


link 未来から過去へ表示(*)  link 過去から未来へ表示

link もっと前
2024年9月24日 >>> 2024年9月11日
link もっと後

2024年9月24日

OpenSBIを調べる - CPUの列挙

目次: Linux

以前、OpenSBIがRISC-V CPUの拡張機能をどのように認識し有効にするか調べました。今回はどのようにCPUの数を認識するか調べます。OpenSBIのプラットフォームは今まで同様にgenericを使います。

実はそんなに難しくなく、デバイスツリーの/cpuノードを読みに行くだけのようです。/cpuノードの例としてQEMU virtマシン、4CPUで起動したときにQEMUが生成するデバイスツリーを下記に示します。長いのでCPU_2, 3は省略しています。名前やラベル、regが2や3になるだけです。

デバイスツリーのCPUノードの例
/ {
	cpus {
		#address-cells = <1>;
		#size-cells = <0>;
		timebase-frequency = <10000000>;

		cpu0: cpu@0 {
			compatible = "riscv";
			reg = <0>;
			device_type = "cpu";
			riscv,cbop-block-size = <64>;
			riscv,cboz-block-size = <64>;
			riscv,cbom-block-size = <64>;
			riscv,isa-extensions = "i", "m", "a", "f", "d", "c", "h",
				"zic64b", "zicbom", "zicbop", "zicboz",
				"ziccamoa", "ziccif", "zicclsm", "ziccrse",
				"zicntr", "zicsr", "zifencei", "zihintntl",
				"zihintpause", "zihpm", "za64rs", "zawrs",
				"zfa", "zca", "zcd", "zba", "zbb", "zbc", "zbs",
				"ssccptr", "sscounterenw", "sstc", "sstvala",
				"sstvecd", "svadu";
			riscv,isa-base = "rv64i";
			riscv,isa = "rv64imafdch_zic64b_zicbom_zicbop_zicboz_ziccamoa_ziccif_zicclsm_ziccrse_zicntr_zicsr_zifencei_zihintntl_zihintpause_zihpm_za64rs_zawrs_zfa_zca_zcd_zba_zbb_zbc_zbs_ssccptr_sscounterenw_sstc_sstvala_sstvecd_svadu";
			mmu-type = "riscv,sv57";

			status = "okay";

			cpu0_intc: interrupt-controller {
				compatible = "riscv,cpu-intc";
				interrupt-controller;
				#interrupt-cells = <1>;
			};
		};

		cpu1: cpu@1 {
			compatible = "riscv";
			reg = <1>;
			device_type = "cpu";
			riscv,cbop-block-size = <64>;
			riscv,cboz-block-size = <64>;
			riscv,cbom-block-size = <64>;
			riscv,isa-extensions = "i", "m", "a", "f", "d", "c", "h",
				"zic64b", "zicbom", "zicbop", "zicboz",
				"ziccamoa", "ziccif", "zicclsm", "ziccrse",
				"zicntr", "zicsr", "zifencei", "zihintntl",
				"zihintpause", "zihpm", "za64rs", "zawrs",
				"zfa", "zca", "zcd", "zba", "zbb", "zbc", "zbs",
				"ssccptr", "sscounterenw", "sstc", "sstvala",
				"sstvecd", "svadu";
			riscv,isa-base = "rv64i";
			riscv,isa = "rv64imafdch_zic64b_zicbom_zicbop_zicboz_ziccamoa_ziccif_zicclsm_ziccrse_zicntr_zicsr_zifencei_zihintntl_zihintpause_zihpm_za64rs_zawrs_zfa_zca_zcd_zba_zbb_zbc_zbs_ssccptr_sscounterenw_sstc_sstvala_sstvecd_svadu";
			mmu-type = "riscv,sv57";

			status = "okay";

			cpu1_intc: interrupt-controller {
				compatible = "riscv,cpu-intc";
				interrupt-controller;
				#interrupt-cells = <1>;
			};
		};

		cpu2: cpu@2 {
			/* 省略 */
		};

		cpu3: cpu@3 {
			/* 省略 */
		};

OpenSBIがCPU数を確認するコードは下記のようになっています。

OpenSBIがCPU数を確認するコード

// opensbi/platform/generic/platform.c

unsigned long fw_platform_init(unsigned long arg0, unsigned long arg1,
				unsigned long arg2, unsigned long arg3,
				unsigned long arg4)
{
	const char *model;
	void *fdt = (void *)arg1;    //★★デバイスツリーのアドレス、レジスタa1を使ってOpenSBIに渡す★★
	u32 hartid, hart_count = 0;
	int rc, root_offset, cpus_offset, cpu_offset, len;

	//...

	cpus_offset = fdt_path_offset(fdt, "/cpus");
	if (cpus_offset < 0)
		goto fail;

	fdt_for_each_subnode(cpu_offset, fdt, cpus_offset) {
		//★★device_typeがcpu、regプロパティに値が入っていれば、CPUのノードとみなす★★
		rc = fdt_parse_hart_id(fdt, cpu_offset, &hartid);
		if (rc)
			continue;

		if (SBI_HARTMASK_MAX_BITS <= hartid)
			continue;

		//★★statusプロパティがokかokayなら有効なCPUとみなす★★
		if (!fdt_node_is_enabled(fdt, cpu_offset))
			continue;

		//★★ハート数を1つ増やす★★
		//★★hartidは連番とは限らないため、hartindexとhartidの対応表を作る★★
		generic_hart_index2id[hart_count++] = hartid;
	}

	//★★platform領域に記録する★★
	platform.hart_count = hart_count;
	platform.heap_size = fw_platform_calculate_heap_size(hart_count);
	platform_has_mlevel_imsic = fdt_check_imsic_mlevel(fdt);

デバイスツリー(正確にはFDT: flattened device tree)の/cpuノードを見て、子ノードがCPUの定義であり、有効ならばhart_countを+1するシンプルなコードです。hartidは連番とは限らないため、hartindex(0からCPU数 - 1までの連番)とhartidの対応表generic_hart_index2id[]も同時に作ります。

無効なCPUはどうなるか?

無効なCPUの扱いも見ておきます。先程のコードからわかる通りstatus = "disabled"つまり無効なCPUの場合は、hartindexとhartidの対応表(platform.hart_index2id[])にhartidが載りません。例としてQEMUにてCPU 4つで起動し、デバイスツリーでCPU 2だけstatus = "disabled"にしたときのhartindexとhartidの対応表をダンプします。

4CPU, CPU2だけ無効のときのhartindexとhartidの対応表
(gdb) p platform
$11 = {opensbi_version = 65541, platform_version = 1,
  name = "riscv-virtio,qemo", '\000' <repeats 46 times>, features = 2,
  hart_count = 3, hart_stack_size = 8192, heap_size = 39936, reserved = 0,
  platform_ops_addr = 2148010200, firmware_context = 0,
  hart_index2id = 0x80083700 <generic_hart_index2id>}

(gdb) p generic_hart_index2id
$15 = {0, 1, 3, 0 <repeats 125 times>}

全CPUが有効ならば0, 1, 2, 3となりますが、CPU 2が無効なので0, 1, 3となっていることがわかります。

次にメインCPUがサブCPUを起こしに行くコードを見ます。アセンブラなので若干分かりづらいですが、hartindexとhartidの対応表に自CPUのhartidが登録されていればCPUを起動し、登録されていない場合はCPUを起動しません。

OpenSBIがサブCPUを起こす処理

// opensbi/firmware/fw_base.S

_fdt_reloc_done:
	//★★メインCPUはこちら★★
	//★★初期化処理が終わるとここに到達する★★
	//★★_boot_statusにBOOT_STATUS_BOOT_HART_DONEを書き込むとサブCPU側がループを抜ける★★
	/* mark boot hart done */
	li	t0, BOOT_STATUS_BOOT_HART_DONE
	lla	t1, _boot_status
	fence	rw, rw
	REG_S	t0, 0(t1)
	j	_start_warm

	//★★サブCPUはこちら★★
	//★★_boot_statusを読みながらループで待っている★★
	/* waiting for boot hart to be done (_boot_status == 2) */
_wait_for_boot_hart:
	li	t0, BOOT_STATUS_BOOT_HART_DONE
	lla	t1, _boot_status
	REG_L	t1, 0(t1)
	/* Reduce the bus traffic so that boot hart may proceed faster */
	div	t2, t2, zero
	div	t2, t2, zero
	div	t2, t2, zero
	bne	t0, t1, _wait_for_boot_hart

_start_warm:
	/* Reset all registers except ra, a0, a1, a2, a3 and a4 for non-boot HART */
	li	ra, 0
	call	_reset_regs

	/* Disable all interrupts */
	csrw	CSR_MIE, zero

	//★★platform.hart_index2idを見て、hartidと一致する要素があるか確かめる★★
	/* Find HART count and HART stack size */
	lla	a4, platform
#if __riscv_xlen > 32
	lwu	s7, SBI_PLATFORM_HART_COUNT_OFFSET(a4)
	lwu	s8, SBI_PLATFORM_HART_STACK_SIZE_OFFSET(a4)
#else
	lw	s7, SBI_PLATFORM_HART_COUNT_OFFSET(a4)
	lw	s8, SBI_PLATFORM_HART_STACK_SIZE_OFFSET(a4)
#endif
	//★★s9はplatform.hart_index2id[0]のアドレス★★
	REG_L	s9, SBI_PLATFORM_HART_INDEX2ID_OFFSET(a4)

	/* Find HART id */
	csrr	s6, CSR_MHARTID

	//★★platform.hart_index2idがNULLだったら処理を止める★★
	/* Find HART index */
	beqz	s9, 3f
	li	a4, 0

	//★★a4 = hartindex
	//★★a5 = platform.hart_index2id[hartindex]の値(hartindexに対応するhartid)
	//★★s6 = CPUのhartid
	//★★s7 = hart数
	//★★s9 = platform.hart_index2id[hartindex]のアドレス
1:
#if __riscv_xlen > 32
	lwu	a5, (s9)
#else
	lw	a5, (s9)
#endif
	//★★hartidとplatform.hart_index2id[n]が一致していたら、ループを抜ける★★
	beq	a5, s6, 2f

	//★★s9を進めて、platform.hart_index2id[0], [1], [2], ...を順に調べる★★
	add	s9, s9, 4
	add	a4, a4, 1
	blt	a4, s7, 1b
	//★★s6 = hartindex
2:	add	s6, a4, zero
	//★★hartindexがhart数を超えているか?
	//★★  超える  : CPUが無効である、サブCPUを起動しない -> _start_hangへ
	//★★  超えない: CPUが有効である、サブCPUを起動する -> ブランチ命令の先へ
3:	bge	s6, s7, _start_hang

	//...

OpenSBIのロゴが出たあたりでブレークして各スレッドの実行している関数名を見ると、下記のようになります。

無効なCPU 2が_start_hang()で止まっている様子
(gdb) info thr
  Id   Target Id                    Frame
  1    Thread 1.1 (CPU#0 [halted ]) sbi_hsm_hart_wait (scratch=0x8008c000, hartid=0)
    at opensbi/lib/sbi/sbi_hsm.c:177
  2    Thread 1.2 (CPU#1 [running]) 0x0000000080020ffe in fdt32_to_cpu (x=50331648)
    at opensbi/lib/utils/libfdt/libfdt_env.h:57
* 3    Thread 1.3 (CPU#2 [halted ]) _start_hang ()
    at opensbi/firmware/fw_base.S:409
  4    Thread 1.4 (CPU#3 [halted ]) sbi_hsm_hart_wait (scratch=0x80088000, hartid=3)
    at opensbi/lib/sbi/sbi_hsm.c:177

Thread Id 3つまりCPU 2(※)だけ_start_hang()に居ることがわかりますね。

(※)GDBのThread Idは1スタートなのでThread Id 1, 2, 3, 4がCPU 0, 1, 2, 3に相当します。

編集者:すずき(2024/10/17 04:19)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2024年9月17日

Yoctoの並列Fetchでエラーが起きるとき

目次: Yocto

Yoctoは特に何も指定せずにビルドすると全スレッドを使おうとします。この挙動はコンパイルだけでなくFetchでも同様のようです。最近のCPUは一般向けでも8〜16スレッドが珍しくないので、何も考えずにbitbakeを実行すると16接続同時にFetchを試みて、サーバー側の同時接続数制限か何かに引っかかってエラーで落ちます。

このエラーが鬱陶しくて困っていたのですが、並列数を2〜4程度(サーバーに蹴られない接続数)に制限して、最初にFetchだけ実行してしまうとよさそうです。

並列数を2に制限してFetchのみ実行
BB_NUMBER_THREADS=2 bitbake core-image-sato --runall=fetch

一度Fetchが終わればその後はFetchしませんから、並列数を16でも32でも好きな数にして良いです。

編集者:すずき(2024/09/21 23:22)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2024年9月14日

OpenSBIを調べる - scratch領域の詳細

目次: Linux

今回はOpenSBIのコード内に頻出するscratch領域について調べます。OpenSBI内で設定値を格納するために何かと登場するscratch領域ですが、hartごとに用意されている領域であることくらいしか知りません。具体的にどこに配置されるのか?どんな領域があるのか?を調べます。

まずscratch領域の場所を調べましょう。アセンブラで書かれている_hartid_to_scratch()関数を読み解くのがわかりやすいでしょう。

scratch領域のアドレスを得るコード

// opensbi/lib/sbi/sbi_scratch.c

int sbi_scratch_init(struct sbi_scratch *scratch)
{
	u32 i, h;
	const struct sbi_platform *plat = sbi_platform_ptr(scratch);

	for (i = 0; i < plat->hart_count; i++) {
		h = (plat->hart_index2id) ? plat->hart_index2id[i] : i;
		hartindex_to_hartid_table[i] = h;
		//★★scratch領域の先頭アドレスはhartindex_to_scratch_table[]に保存されている★★
		hartindex_to_scratch_table[i] =
			((hartid2scratch)scratch->hartid_to_scratch)(h, i);
	}

	last_hartindex_having_scratch = plat->hart_count - 1;

	return 0;
}


// opensbi/firmware/fw_base.S

	.globl _hartid_to_scratch
_hartid_to_scratch:
	/*
	 * a0 -> HART ID (passed by caller)
	 * a1 -> HART Index (passed by caller)
	 * t0 -> HART Stack Size
	 * t1 -> HART Stack End
	 * t2 -> Temporary
	 */
	lla	t2, platform
#if __riscv_xlen > 32
	lwu	t0, SBI_PLATFORM_HART_STACK_SIZE_OFFSET(t2)
	lwu	t2, SBI_PLATFORM_HART_COUNT_OFFSET(t2)
#else
	lw	t0, SBI_PLATFORM_HART_STACK_SIZE_OFFSET(t2)
	lw	t2, SBI_PLATFORM_HART_COUNT_OFFSET(t2)
#endif
	sub	t2, t2, a1    /* hart数 - hart indexに */
	mul	t2, t2, t0    /* スタックサイズを掛ける */
                              /* index 0が末尾側、index 1が末尾の1つ手前となる */
	lla	t1, _fw_end   /* OpenSBIバイナリの終端、メモリマップを参照 */
	add	t1, t1, t2    /* _fw_endの後ろがスタック領域 */
	li	t2, SBI_SCRATCH_SIZE    /* 現状はSBI_SCRATCH_SIZE = 0x1000 */
	sub	a0, t1, t2    /* スタック領域の末尾がscratch領域 */
	ret

コードを見る限り、scratch領域の場所を知るにはhartindexが必要です。hartindexはhartに関係する何かの配列のindexに使う値です。0〜hart数 - 1の値を取ります。

なぜhartidをindex代わりにしないのか?理由はRISC-Vのhartidがindex値として適していないからです。hartidは0から始まる連続した値とは限らず、非連続でも構いませんし、非常に大きな値をとっても構いません(推奨はされませんが)。そのためhartindexのような別概念を持ち出す必要があります。ちなみにhartindexとhartidの関係はhartindex_to_hartid_table[]に保持されています。

例えばhart数4、hartindex 0だとしますと、スタック領域はhart数分つまり4つあります。scratch領域の先頭は
(4 - 0) * hart_stack_size - SBI_SCRATCH_SIZE
なので、4つ目のスタック領域の末尾 - 0x1000です。図示したほうがわかりやすいでしょう。


スタック領域とscratch領域の関係

アドレスの高位側からhartindex 0, 1, 2, ...用のスタックおよびscratch領域が並んでいて、hartindex_to_hartid_table[]にhartindexとhartidの関係が保存されており、hartindex_to_scratch_table[]にhartindexとscratch領域の先頭アドレスの関係が保存されています。

scratch領域の中身

以上でscratch領域の場所はわかりました。次は領域の内部を調べましょう。コードを見るとscratch領域は2つの機能があります。

  • 先頭にstruct sbi_scratchが存在
  • 以降はsbi_scratch_alloc_offset()関数で確保するための領域

先頭に構造体があって、その後の領域はOpenSBI内で自由に確保して使う方式です。領域の確保にはsbi_scratch_alloc_offset()関数を使用します。コードはこんな感じ。

scratch領域にメモリを確保するコード

// opensbi/lib/sbi/sbi_scratch.c

unsigned long sbi_scratch_alloc_offset(unsigned long size)
{
	u32 i;
	void *ptr;
	unsigned long ret = 0;
	struct sbi_scratch *rscratch;

	/*
	 * We have a simple brain-dead allocator which never expects
	 * anything to be free-ed hence it keeps incrementing the
	 * next allocation offset until it runs-out of space.
	 *
	 * In future, we will have more sophisticated allocator which
	 * will allow us to re-claim free-ed space.
	 */

	if (!size)
		return 0;

	size += __SIZEOF_POINTER__ - 1;
	size &= ~((unsigned long)__SIZEOF_POINTER__ - 1);

	spin_lock(&extra_lock);

	if (SBI_SCRATCH_SIZE < (extra_offset + size))
		goto done;

	//★★extra_offsetは最初struct sbi_scratchの末尾を指している★★
	//★★sbi_scratch_alloc_offset()が成功するたびに高位側のアドレスにズレていく★★
	ret = extra_offset;
	extra_offset += size;

done:
	spin_unlock(&extra_lock);

	if (ret) {
		for (i = 0; i <= sbi_scratch_last_hartindex(); i++) {
			rscratch = sbi_hartindex_to_scratch(i);
			if (!rscratch)
				continue;
			ptr = sbi_scratch_offset_ptr(rscratch, ret);
			sbi_memset(ptr, 0, size);
		}
	}

	return ret;
}

柔軟で面白い仕組みですがscratch領域内に保存された情報がどこにあるか分かりづらく、コードを読む側としては嫌な作りとも言えます。下記の図にscratch領域にメモリを確保するとどうなるか?を示します。


scratch領域の初期状態


100バイトメモリ確保した後のscratch領域の状態

初期状態と100バイトのメモリ確保を行った後で、scratch領域のメモリマップとextra_offsetが高位側のアドレスにズレる様子を表しました。

scratch領域にメモリを確保する人たち

実際にはたくさんの領域が確保されます。下記はsbi_scratch_alloc_offset()を呼んでscratch領域にメモリを確保し、どんな種類のデータを何バイト確保しているか?の一覧表です。

offsetを保存する変数名 データ型 サイズ
domain_hart_ptr_offset pointer 8
entry_count_offset pointer 8
init_count_offset pointer 8
hart_data_offset struct sbi_hsm_data 56
hart_features_offset struct sbi_hart_features40
fdt_isa_bitmap_offset unsigned long[1] ※要素数=hart数 / 648
shs_ptr_off pointer 8
sse_inject_fifo_off struct sbi_fifo 24
sse_inject_fifo_mem_offstruct sse_ipi_inject_data[5]20
phs_ptr_offset pointer 8
hart_state_ptr_offset pointer 8
plic_ptr_offset pointer 8
plic_mcontext_offset long 8
plic_scontext_offset long 8
ipi_data_off struct sbi_ipi_data8
mswi_ptr_offset pointer 8
tlb_sync_off atomic_t8
tlb_fifo_off struct sbi_fifo24
tlb_fifo_mem_off pointer 8
time_delta_off u64 8
mtimer_ptr_offset pointer 8
fwft_ptr_offset pointer 8

表の1列目はsbi_scratch_alloc_offset()の返り値を保存している変数名です。hart_features_offsetを例にして、領域の確保を行っているコードと、領域のアドレスを取得するコードを見ましょう。

scratch領域の確保と参照をしているコードの例

// opensbi/lib/sbi/sbi_hart.c

int sbi_hart_init(struct sbi_scratch *scratch, bool cold_boot)
{
	int rc;

	/*
	 * Clear mip CSR before proceeding with init to avoid any spurious
	 * external interrupts in S-mode.
	 */
	csr_write(CSR_MIP, 0);

	if (cold_boot) {
		if (misa_extension('H'))
			sbi_hart_expected_trap = &__sbi_expected_trap_hext;

		//★★scratch領域に領域を確保する方法★★
		//★★領域を確保して、scratch領域先頭からのオフセットアドレス(=返り値)を★★
		//★★hart_features_offsetに保存★★
		hart_features_offset = sbi_scratch_alloc_offset(
					sizeof(struct sbi_hart_features));
		if (!hart_features_offset)
			return SBI_ENOMEM;
	}

	rc = hart_detect_features(scratch);
	if (rc)
		return rc;

	return sbi_hart_reinit(scratch);
}

bool sbi_hart_has_extension(struct sbi_scratch *scratch,
			    enum sbi_hart_extensions ext)
{
	//★★scratch領域に確保した領域を参照する方法★★
	struct sbi_hart_features *hfeatures =
			sbi_scratch_offset_ptr(scratch, hart_features_offset);

	if (__test_bit(ext, hfeatures->extensions))
		return true;
	else
		return false;
}

端的に言うと領域の確保にsbi_scratch_alloc_offset()関数、領域の参照にsbi_scratch_offset_ptr()関数を使えば良いです。

複数hartの場合は問題ない?

結論から言うと問題は起きません。領域確保時と領域参照時に分けて考えましょう。

最初にscratch領域のメモリを確保する場合です。これは単純で、初期化を担当する1つのhartだけがscratch領域のメモリ確保を行えば、同じ領域をダブって確保してしまう問題は発生しません。

次にscratch領域を参照する場合です。全てのhartが同じオフセットを使用すれば、重なったりはみ出たりする問題は発生しません。例えばsbi_scratch_alloc_offset()関数で確保したメモリ領域Aがありサイズ16、オフセット120だとしたら、全hartのscratch領域にメモリ領域Aが存在していて、同じサイズとオフセットであるとみなします。全hartのscratch領域の大きさが同じだからできる芸当ですね。

これで謎に包まれたOpenSBIのscratch領域も多少は晴れたはずです。たぶん……。

編集者:すずき(2024/09/22 11:23)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2024年9月13日

OpenSBIを調べる - OpenSBIとRISC-V ISA extensions

目次: Linux

今回はOpenSBIがRISC-Vの拡張機能をどのように認識し有効にするか調べます。

RISC-Vはたくさんの拡張命令と拡張機能が存在し、CPUやHWの実装によって対応する機能が異なります。そのためCPU/HWがRISC-Vのどの規格に対応しているのかを表す方法が必要です。RISC-Vの規格では命令セットと拡張機能をまとめて"rv64imafdc_aaa_bbb"のような文字列で表します。この文字列の名前がわからないのが困ったところですが、仕様書ではISA extensionsと書かれていることが多いようなので、以降はISA extensionsと表記します。

当初ISA extensionsは"rv64imafdc"のような「rv + ビット幅(32 or 64) + アルファベットの羅列(1文字=1つの拡張を表す)」だけの素朴なルールでしたが、際限なく増えるRISC-Vの拡張機能はこんなルールでは表せません。現在は"rv64imafdc_aaa_bbb_ccc"のようなアンダースコア + 拡張名を列車のように繋げるルール(RISC-V Unprivileged Architecture: Chapter 36. ISA Extension Naming Conventions)が採用されています。

例えばQEMUの仮想CPUのISA extensionsを見ると、

QEMUの仮想CPUのISA extensions
rv64imafdch_zic64b_zicbom_zicbop_zicboz_
ziccamoa_ziccif_zicclsm_ziccrse_
zicntr_zicsr_zifencei_zihintntl_
ihintpause_zihpm_za64rs_zawrs_
zfa_zca_zcd_zba_
zbb_zbc_zbs_ssccptr_
sscounterenw_sstc_sstvala_sstvecd_
svadu

本来はとても長い1行ですが、見づらいため4つごとに改行を入れています。もはやISA extensionsは魔法の呪文か暗号の様相を呈していて、どこか一文字だけ間違えていたとしても気付ける自信がありません。

OpenSBIが拡張機能を認識する方法

拡張機能の認識方法を調べます。答えを先に書くとデバイスツリーのcpuノードのriscv,isaプロパティの長い長い文字列を頭から解析しています。割と力技でした。

デバイスツリーのCPUノード、riscv,isaプロパティの例

/ {
	cpus {
		cpu0: cpu@0 {
			device_type = "cpu";
			reg = <0>;
			compatible = "riscv";
			riscv,isa-base = "rv64i";
			riscv,isa = "rv64imafdc_zicsr_zifencei_zba_zbb_zbs";  //★★これ★★

			status = "okay";
			//...

ISA extensionsの文字列解析をする関数はfdt_parse_isa_one_hart()関数です。

fdt_parse_isa_one_hart()関数の呼び出し履歴
sbi_init @ lib/sbi/sbi_init.c
  init_coldboot @ lib/sbi/sbi_init.c
    sbi_hart_init @ lib/sbi/sbi_hart.c
      hart_detect_features @ lib/sbi/sbi_hart.c
        sbi_platform_extensions_init @ include/sbi/sbi_platform.h
          sbi_platform_ops(plat)->extensions_init(hfeatures);
          -> generic_extensions_init @ platform/generic/platform.c
            fdt_parse_isa_extensions @ lib/utils/fdt/fdt_helper.c
              fdt_parse_isa_all_harts @ lib/utils/fdt/fdt_helper.c
                fdt_parse_isa_one_hart @ lib/utils/fdt/fdt_helper.c

GenericプラットフォームをQEMU用のデバイスツリーで動作させた際の呼び出し経路はこんな感じです。3つ上位のfdt_parse_isa_all_harts()関数を見ると、

ISA extensionsを解析する処理と結果を格納する部分のコード

// opensbi/platform/generic/platform.c

static int generic_extensions_init(struct sbi_hart_features *hfeatures)
{
	int rc;

	//★★第3引数hfeatures->extensionsにISA extensionsの解析結果を書き込む★★
	//★★hfeaturesはscratch領域のhart_features_offsetバイト目、手元の環境だと192バイト目★★

	/* Parse the ISA string from FDT and enable the listed extensions */
	rc = fdt_parse_isa_extensions(fdt_get_address(), current_hartid(),
				      hfeatures->extensions);

	if (rc)
		return rc;

	if (generic_plat && generic_plat->extensions_init)
		return generic_plat->extensions_init(generic_plat_match,
						     hfeatures);

	return 0;
}


// opensbi/lib/utils/fdt/fdt_helper.c

int fdt_parse_isa_extensions(void *fdt, unsigned int hartid,
			unsigned long *extensions)
{
	int rc, i;
	unsigned long *hart_exts;
	struct sbi_scratch *scratch;

	if (!fdt_isa_bitmap_offset) {
		fdt_isa_bitmap_offset = sbi_scratch_alloc_offset(
					sizeof(*hart_exts) *
					BITS_TO_LONGS(SBI_HART_EXT_MAX));
		if (!fdt_isa_bitmap_offset)
			return SBI_ENOMEM;

		//★★この関数がISA extensionsの解析結果を置く場所は★★
		//★★scratch領域 + fdt_isa_bitmap_offsetの場所★★
		rc = fdt_parse_isa_all_harts(fdt);
		if (rc)
			return rc;
	}

	scratch = sbi_hartid_to_scratch(hartid);
	if (!scratch)
		return SBI_ENOENT;

	//★★scratch領域 + fdt_isa_bitmap_offsetの場所を取得する★★
	hart_exts = sbi_scratch_offset_ptr(scratch, fdt_isa_bitmap_offset);

	//★★ISA extensionsの解析結果は、hfeatures->extensionsに全てまとめられる★★
	for (i = 0; i < BITS_TO_LONGS(SBI_HART_EXT_MAX); i++)
		extensions[i] |= hart_exts[i];
	return 0;
}


// opensbi/lib/utils/fdt/fdt_helper.c

static int fdt_parse_isa_all_harts(void *fdt)
{
	u32 hartid;
	const fdt32_t *val;
	unsigned long *hart_exts;
	struct sbi_scratch *scratch;
	int err, cpu_offset, cpus_offset, len;

	if (!fdt || !fdt_isa_bitmap_offset)
		return SBI_EINVAL;

	cpus_offset = fdt_path_offset(fdt, "/cpus");
	if (cpus_offset < 0)
		return cpus_offset;

	fdt_for_each_subnode(cpu_offset, fdt, cpus_offset) {
		err = fdt_parse_hart_id(fdt, cpu_offset, &hartid);
		if (err)
			continue;

		if (!fdt_node_is_enabled(fdt, cpu_offset))
			continue;

		//★★device treeのriscv,isaプロパティを見る★★
		val = fdt_getprop(fdt, cpu_offset, "riscv,isa", &len);
		if (!val || len <= 0)
			return SBI_ENOENT;

		scratch = sbi_hartid_to_scratch(hartid);
		if (!scratch)
			return SBI_ENOENT;

		//★★解析結果を置く場所はscratch領域 + fdt_isa_bitmap_offsetの場所★★
		hart_exts = sbi_scratch_offset_ptr(scratch,
						   fdt_isa_bitmap_offset);

		//★★riscv64imafdc_zicsr_zifencei_...のような文字列を解析する★★
		err = fdt_parse_isa_one_hart((const char *)val, hart_exts);
		if (err)
			return err;
	}

	return 0;
}

デバイスツリーのcpusノードを探して、子ノード(cpu@0など)のriscv,isaプロパティを読み出して、文字列解析するfdt_parse_isa_one_hart()関数に渡していることがわかります。

解析結果はscratch領域(OpenSBIのhartごとの情報を保持しておくメモリ領域、スタック領域の末尾に確保される)のfdt_isa_bitmap_offsetに置かれ、最終的には別のscratch領域にあるfeatures->extensionsにまとめられます。scratch領域の詳細が気になるところですが、また別の機会に調べることにします。

OpenSBIが拡張機能を有効にする方法

拡張機能はリセット直後は無効になっているものもあるので、有効にする必要があります。RISC-V Privileged Architectureではmenvcfgレジスタがあり、適切なビットフィールドに値をセットして機能の有効/無効を切り替える場合があります。有効/無効が設定できない機能もあって、統一感がないです……。

例えばS-mode用のタイマーコンペアレジスタ(stimecmpレジスタ)を定義しているSSTC拡張では、menvcfgのビット63 STCE(STimecmp Enable)フィールドに1をセットすると機能が有効になります。機能を有効にせずstimecmpにアクセスするとillegal instruction例外が発生します。

OpenSBIのコードでmenvcfgレジスタに値を設定する関数は下記のmstatus_init()です。

拡張機能を有効にするためmenvcfgレジスタに値を設定するコード

// opensbi/lib/sbi/sbi_hart.c

/**
 * Check whether a particular hart extension is available
 *
 * @param scratch pointer to the HART scratch space
 * @param ext the extension number to check
 * @returns true (available) or false (not available)
 */
bool sbi_hart_has_extension(struct sbi_scratch *scratch,
			    enum sbi_hart_extensions ext)
{
	struct sbi_hart_features *hfeatures =
			sbi_scratch_offset_ptr(scratch, hart_features_offset);

	if (__test_bit(ext, hfeatures->extensions))
		return true;
	else
		return false;
}

static void mstatus_init(struct sbi_scratch *scratch)
{
	int cidx;
	unsigned long mstatus_val = 0;
	unsigned int mhpm_mask = sbi_hart_mhpm_mask(scratch);
	uint64_t mhpmevent_init_val = 0;
	uint64_t menvcfg_val, mstateen_val;

//...

	if (sbi_hart_priv_version(scratch) >= SBI_HART_PRIV_VER_1_12) {
		menvcfg_val = csr_read(CSR_MENVCFG);
#if __riscv_xlen == 32
		menvcfg_val |= ((uint64_t)csr_read(CSR_MENVCFGH)) << 32;
#endif

#define __set_menvcfg_ext(__ext, __bits)				\
		if (sbi_hart_has_extension(scratch, __ext))		\
			menvcfg_val |= __bits;

		/*
		 * Enable access to extensions if they are present in the
		 * hardware or in the device tree.
		 */

		//★★sbi_hart_has_extension()関数で拡張機能の一覧hfeatures->extensionsのビットをテストする★★
		//★★拡張機能が存在しているなら、menvcfgの相当するビットをセットする★★
		__set_menvcfg_ext(SBI_HART_EXT_ZICBOZ, ENVCFG_CBZE)
		__set_menvcfg_ext(SBI_HART_EXT_ZICBOM, ENVCFG_CBCFE)
		__set_menvcfg_ext(SBI_HART_EXT_ZICBOM,
				  ENVCFG_CBIE_INV << ENVCFG_CBIE_SHIFT)
#if __riscv_xlen > 32
		__set_menvcfg_ext(SBI_HART_EXT_SVPBMT, ENVCFG_PBMTE)
#endif
		__set_menvcfg_ext(SBI_HART_EXT_SSTC, ENVCFG_STCE)
		__set_menvcfg_ext(SBI_HART_EXT_SMCDELEG, ENVCFG_CDE);
		__set_menvcfg_ext(SBI_HART_EXT_SVADU, ENVCFG_ADUE);

#undef __set_menvcfg_ext

		/*
		 * When both Svade and Svadu are present in DT, the default scheme for managing
		 * the PTE A/D bits should use Svade. Check Svadu before Svade extension to ensure
		 * that the ADUE bit is cleared when the Svade support are specified.
		 */

		if (sbi_hart_has_extension(scratch, SBI_HART_EXT_SVADE))
			menvcfg_val &= ~ENVCFG_ADUE;

		csr_write(CSR_MENVCFG, menvcfg_val);    //★★menvcfgを設定、拡張機能が有効になる★★
#if __riscv_xlen == 32
		csr_write(CSR_MENVCFGH, menvcfg_val >> 32);
#endif

		/* Enable S-mode access to seed CSR */
		if (sbi_hart_has_extension(scratch, SBI_HART_EXT_ZKR)) {
			csr_set(CSR_MSECCFG, MSECCFG_SSEED);
			csr_clear(CSR_MSECCFG, MSECCFG_USEED);
		}
	}

	/* Disable all interrupts */
	csr_write(CSR_MIE, 0);

	/* Disable S-mode paging */
	if (misa_extension('S'))
		csr_write(CSR_SATP, 0);
}

以上がOpenSBIによって拡張機能が認識&有効になる仕掛けです。ISA extensionsを間違えるとこの仕組みは動かないので、ISA extensionsの長くて難しくて間違いやすい記述はどうすれば良いの?と疑問が湧きます。しかしその答えはOpenSBIにはなさそうですね……。

ちなみにmenvcfgの詳細はPrivileged Architecture(GitHubにソースコードがあります、PDFはReleasesからダウンロード可能)に記載されています。なんちゃら拡張が使う予定のビットフィールドなのでそちらを参照せよと書いていることも多く、各々の拡張機能の仕様書を行脚することになるでしょう。

編集者:すずき(2024/10/25 02:20)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



link もっと前
2024年9月24日 >>> 2024年9月11日
link もっと後

管理用メニュー

link 記事を新規作成

<2024>
<<<09>>>
1234567
891011121314
15161718192021
22232425262728
2930-----

最近のコメント5件

  • link 24年10月1日
    すずきさん (10/06 03:41)
    「xrdpで十分動作しているので、Wayl...」
  • link 24年10月1日
    hdkさん (10/03 19:05)
    「GNOMEをお使いでしたら今はWayla...」
  • link 24年10月1日
    すずきさん (10/03 10:12)
    「私は逆にVNCサーバーに繋ぐ使い方をした...」
  • link 24年10月1日
    hdkさん (10/03 08:30)
    「おー、面白いですね。xrdpはすでに立ち...」
  • link 14年6月13日
    2048player...さん (09/26 01:04)
    「最後に、この式を出すのに紙4枚(A4)も...」

最近の記事3件

  • link 24年10月28日
    すずき (10/30 23:49)
    「[Linuxからリモートデスクトップ] 目次: Linux開発用のLinuxマシンの画面を見るにはいろいろな手段がありますが、...」
  • link 23年4月10日
    すずき (10/30 23:46)
    「[Linux - まとめリンク] 目次: Linux関係の深いまとめリンク。目次: RISC-V目次: ROCK64/ROCK...」
  • link 24年10月24日
    すずき (10/25 02:35)
    「[ONKYOからM-AUDIOのUSB DACへ] 目次: PCかれこれ10年以上(2013年3月16日の日記参照)活躍してく...」
link もっとみる

こんてんつ

open/close wiki
open/close Linux JM
open/close Java API

過去の日記

open/close 2002年
open/close 2003年
open/close 2004年
open/close 2005年
open/close 2006年
open/close 2007年
open/close 2008年
open/close 2009年
open/close 2010年
open/close 2011年
open/close 2012年
open/close 2013年
open/close 2014年
open/close 2015年
open/close 2016年
open/close 2017年
open/close 2018年
open/close 2019年
open/close 2020年
open/close 2021年
open/close 2022年
open/close 2023年
open/close 2024年
open/close 過去日記について

その他の情報

open/close アクセス統計
open/close サーバ一覧
open/close サイトの情報

合計:  counter total
本日:  counter today

link About www2.katsuster.net
RDFファイル RSS 1.0

最終更新: 10/30 23:49