目次: Linux
OpenSBIのブート部分を調べます。OpenSBIはいくつか動作モードがあるのですが、payloadを持ったタイプを調べます。対象のファイル名はbuild/platform/generic/firmware/fw_payload.binもしくは.elfです。
今回はメモリマップについて調べます。
前回(2024年7月19日の日記参照)説明したとおり、バイナリのメモリマップを決めるのはリンカースクリプトfw_payload.elf.ldSです。FW_TEXT_STARTがメモリマップの開始アドレスですが、OpenSBI 1.5からはPIEになったためFW_TEXT_START=0x0です。したがって開始アドレスは0x0です。
リンカースクリプトを図に起こすとこんな感じです。
OpenSBIのgeneric向け、payload付きバイナリのメモリマップ
読み取り専用データ(.rodataセクションなど)に続く謎の隙間があります。これはPMP(Physical Memory Protection、RISC-Vのメモリ保護機構)の領域サイズを2のべき乗にするためにわざと空けている隙間です。
/* opensbi/firmware/fw_base.ldS */
/*
* PMP regions must be to be power-of-2. RX/RW will have separate
* regions, so ensure that the split is power-of-2.
*/
. = ALIGN(1 << LOG2CEIL((SIZEOF(.rodata) + SIZEOF(.text)
+ SIZEOF(.dynsym) + SIZEOF(.rela.dyn))));
PROVIDE(_fw_rw_start = .);
/* Beginning of the read-write data sections */
.data :
{
私の環境でビルドした場合は.textや.rodataセクションの合計サイズが0x31bc8でした。この値に最も近い2のべき乗の2^18 = 0x40000が選択されます。
読み書き用データ(.dataセクションなど)の後にも謎の隙間があります。これはペイロードを配置するアドレスがプラットフォームによって固定されているためです。
/* opensbi/firmware/fw_payload.ldS */
#ifdef FW_PAYLOAD_OFFSET
. = FW_TEXT_START + FW_PAYLOAD_OFFSET;
#else
. = ALIGN(FW_PAYLOAD_ALIGN);
#endif
.payload :
{
FW_PAYLOAD_OFFSETが定義されていれば、ペイロードの開始アドレスはFW_TEXT_START + FW_PAYLOAD_OFFSETです。OpenSBI 1.5ではFW_TEXT_STARTは0x0です。FW_PAYLOAD_OFFSETはplatformのMakefileで値が決まっていて、firmwareのMakefileでマクロとして定義されます。
# opensbi/platform/generic/objects.mk
ifeq ($(PLATFORM_RISCV_XLEN), 32)
# This needs to be 4MB aligned for 32-bit system
FW_PAYLOAD_OFFSET=0x400000
else
# This needs to be 2MB aligned for 64-bit system
FW_PAYLOAD_OFFSET=0x200000
endif
# opensbi/firmware/objects.mk
ifdef FW_PAYLOAD_OFFSET
firmware-genflags-$(FW_PAYLOAD) += -DFW_PAYLOAD_OFFSET=$(FW_PAYLOAD_OFFSET)
endif
以前(2024年7月19日の日記参照)_fw_endの後ろの領域をスタック領域やヒープ領域として使っていると説明しました、再掲すると、
この説明を読んで、領域を勝手に使ってよいのか?何か別の領域が続いているのではないか?と疑問に思われた方はするどいです。が、ご安心ください、読み書き用データとペイロードの間の隙間を使っています。
個人的にはFW_PAYLOAD_OFFSETが決め打ちなのが若干気になりますが、0x200000 = 2MBあれば、256hart分(256 x 4KB = 1MB)のスタック領域は余裕で確保できますし、読み取り専用+読み書き用データ領域のサイズもよほど変な実装を加えない限り1MBも行かないでしょう。たぶん……。
目次: Linux
OpenSBIのブート部分を調べます。OpenSBIはいくつか動作モードがあるのですが、payloadを持ったタイプを調べます。対象のファイル名はbuild/platform/generic/firmware/fw_payload.binもしくは.elfです。
今回はメモリマップについて調べます。調べている途中で気づいたのですがOpenSBI 1.4と1.5で変わっている部分があるので、その話もしたいと思います。
/* opensbi/firmware/fw_payload.elf.ldS */
OUTPUT_ARCH(riscv)
ENTRY(_start)
SECTIONS
{
#include "fw_base.ldS"
...
/* opensbi/firmware/fw_base.ldS */
. = FW_TEXT_START;
/* Don't add any section between FW_TEXT_START and _fw_start */
PROVIDE(_fw_start = .);
...
バイナリのメモリマップを決めるのはリンカースクリプトfw_payload.elf.ldSです。fw_base.ldSは各モード共通で使われるリンカースクリプトです。FW_TEXT_STARTがメモリマップの開始アドレスとなります。
FW_TEXT_STARTが定義されている場所はMakefileで、
# opensbi/firmware/objects.mk
ifdef FW_TEXT_START
firmware-genflags-y += -DFW_TEXT_START=$(FW_TEXT_START)
else
firmware-genflags-y += -DFW_TEXT_START=0x0
endif
FW_TEXT_STARTが定義されていればその値を使い、未定義ならば0x0です。
QEMU向けにビルドする場合はPLATFORM=genericと指定しました。このときMakefileはplatform/generic/objects.mkが使用されますが、FW_TEXT_STARTの定義がOpenSBI 1.4と1.5で違います。
# platform/generic/objects.mk (OpenSBI 1.4)
# Blobs to build
FW_TEXT_START=0x80000000
FW_DYNAMIC=y
FW_JUMP=y
# platform/generic/objects.mk (OpenSBI 1.5)
# Blobs to build
FW_DYNAMIC=y
FW_JUMP=y
OpenSBI 1.4ではplatform/generic/objects.mkにてFW_TEXT_START=0x80000000が定義されていました。firmware-genflags-yに追加されるオプションは前者(-DFW_TEXT_START=0x80000000)です。
OpenSBI 1.5ではplatform/generic/objects.mkにてFW_TEXT_STARTが定義されないため、firmware-genflags-yに追加されるオプションは後者(-DFW_TEXT_START=0x0)です。
QEMUのアドレス0x0にはメモリがないのになぜこの設定で動作するか?ですが、OpenSBI 1.4ではアドレス固定でしたが、OpenSBI 1.5ではPIE(Position Independent Executable、位置独立実行形式)でビルドされるため、OpenSBI 1.5はロードアドレスがどこであっても動作します。
実験してみるとわかりますが、FW_TEXT_STARTを変な値に変えても動作します。OpenSBI 1.4同様にFW_TEXT_START=0x80000000にしても動作します。
が、変更する意味はあまりないでしょう。私が思いつく範囲だと、gdbでPIEのシンボルを読みたい場合はsymbol-file (file) -o 0x80000000のようにしますが、FW_TEXT_START=0x80000000にしておくとこのコマンドを一々入力せず済む、くらいです。他にも何かあれば教えていただけると嬉しいです。
メモリマップと言いつつエントリアドレスしか説明していなかったので、次回はエントリアドレス以降のメモリマップも調べたいと思います。
目次: Linux
OpenSBIのブート部分を調べます。OpenSBIはいくつか動作モードがあるのですが、今回はペイロードを持ったタイプを調べます。対象のファイル名はbuild/platform/generic/firmware/fw_payload.binもしくは.elfです。
OpenSBIのエントリポイントは_startというアセンブラ実装のコードです。
//opensbi/firmware/fw_base.S
_start
_try_lottery
_bss_zero
fw_save_info() // firmware/fw_payload.S
//何もしない
fw_platform_init() // platform/generic/platform.cだが独自の実装もある(k210など)
_scratch_init
//scratch領域を確保する、先頭アドレスはtpレジスタに格納される
fw_next_arg1() // firmware/fw_payload.S
// FW_PAYLOAD_FDT_ADDRが定義済みならFW_PAYLOAD_FDT_ADDRを返す
// FW_PAYLOAD_FDT_OFFSETが定義済みなら_fw_start + FW_PAYLOAD_FDT_OFFSETを返す
// いずれも未定義ならばa1レジスタの値を返す
// 結果はscratch->next_arg1に格納
fw_next_addr() // firmware/fw_payload.S
// payload_binのアドレスを返す
// FW_PAYLOAD_PATHが定義済みなら.incbin FW_PAYLOAD_PATH
// 未定義ならwfi命令の無限ループ
// 結果はscratch->next_addrに格納
fw_next_mode() // firmware/fw_payload.S
// PRV_Sを返す
// 結果はscratch->next_modeに格納
fw_options()
// 0を返す
// 結果はscratch->optionsに格納
_start_warm
// tpの値をmscratchレジスタへ
_reset_regs
// mscratchレジスタを第一引数(a0レジスタ)にしてsbi_init()を呼ぶ
sbi_init(struct sbi_scratch *scratch)
結構長いですが、ブートに必要な情報を扱っている部分を抜粋してコメントを付け加えました。
ブート処理で重要なデータがstruct sbi_platform platformです。これは各platformごとに定義されていて、QEMU用のバイナリであればgeneric/platform.cの定義が使われるはずです。fw_platform_init()関数にて初期化されます。
//opensbi/platform/generic/platform.c
struct sbi_platform platform = {
.opensbi_version = OPENSBI_VERSION,
.platform_version =
SBI_PLATFORM_VERSION(CONFIG_PLATFORM_GENERIC_MAJOR_VER,
CONFIG_PLATFORM_GENERIC_MINOR_VER),
.name = CONFIG_PLATFORM_GENERIC_NAME,
.features = SBI_PLATFORM_DEFAULT_FEATURES,
.hart_count = SBI_HARTMASK_MAX_BITS,
.hart_index2id = generic_hart_index2id,
.hart_stack_size = SBI_PLATFORM_DEFAULT_HART_STACK_SIZE,
.heap_size = SBI_PLATFORM_DEFAULT_HEAP_SIZE(0),
.platform_ops_addr = (unsigned long)&platform_ops
};
Cの構造体をアセンブラのコードから参照するときはちょっと面倒で、
こんな処理を行う必要があります。Cの構造体を書き換えるとアセンブラ側で定義しているオフセットとずれてしまう問題があるため、
//opensbi/include/sbi/sbi_platform.h
/**
* Prevent modification of struct sbi_platform from affecting
* SBI_PLATFORM_xxx_OFFSET
*/
_Static_assert(
offsetof(struct sbi_platform, opensbi_version)
== SBI_PLATFORM_OPENSBI_VERSION_OFFSET,
"struct sbi_platform definition has changed, please redefine "
"SBI_PLATFORM_OPENSBI_VERSION_OFFSET");
(...以下同様に続く...)
全てのオフセットマクロに対してアサーションが書かれています。sbi_platform構造体が頻繁に変わることはないでしょうし、メンバもせいぜい10個くらいなので、手書き&気合で乗り切れます。
こういうオフセットマクロがあまりにも多い場合は、Cのコードからオフセットマクロを自動生成する場合もあります。OSのコードでたまに見かけますね。
もうひとつ重要なデータはスクラッチ領域(データ型はstruct sbi_scratch)です。アセンブラのコード_scratch_initにて領域の確保と値の設定が行われ、Cのコードではscratch->aaaaのような形で参照します。
まず領域の確保ですが、OpenSBIのバイナリの終端部分から下記のようにスタックとヒープ領域が確保されます。
スクラッチ領域はスタック領域の末尾に確保されます。またtpレジスタはスクラッチ領域の先頭アドレスとなります。この値は後でsbi_init()関数の第一引数に渡すために使います。
OpenSBIからペイロード(今回はLinuxをペイロードにしています)へ制御を移す部分のコードを調べます。
sbi_init()
init_coldboot() //1コアだけ : lib/sbi/sbi_init.c
sbi_AAAAAA_init() //初期化関数の名前はこんな感じ
//
//初期化、メッセージ表示など
//
sbi_hsm_hart_start_finish()
hsm_start_ticket_release()
sbi_hart_switch_mode()
// mepc = next_addr : payload_binのアドレス
// a0 = arg0 = hartid
// a1 = arg1 = next_arg1 : FDTのアドレス
// mretでmepcに飛ばす、つまりペイロードであるLinuxに飛ぶ
init_warmboot() //他のコア : lib/sbi/sbi_init.c
...
いろんな処理を行ないますが、ペイロードへ制御を移す部分はsbi_hsm_hart_start_finish()とsbi_hart_switch_mode()関数です。
//opensbi/lib/sbi/sbi_hsm.c
void __noreturn sbi_hsm_hart_start_finish(struct sbi_scratch *scratch,
u32 hartid)
{
unsigned long next_arg1;
unsigned long next_addr;
unsigned long next_mode;
struct sbi_hsm_data *hdata = sbi_scratch_offset_ptr(scratch,
hart_data_offset);
if (!__sbi_hsm_hart_change_state(hdata, SBI_HSM_STATE_START_PENDING,
SBI_HSM_STATE_STARTED))
sbi_hart_hang();
next_arg1 = scratch->next_arg1;
next_addr = scratch->next_addr;
next_mode = scratch->next_mode;
hsm_start_ticket_release(hdata);
sbi_hart_switch_mode(hartid, next_arg1, next_addr, next_mode, false); //★制御を移す関数★
}
//opensbi/lib/sbi/sbi_hart.c
void __attribute__((noreturn))
sbi_hart_switch_mode(unsigned long arg0, unsigned long arg1,
unsigned long next_addr, unsigned long next_mode,
bool next_virt)
{
#if __riscv_xlen == 32
unsigned long val, valH;
#else
unsigned long val;
#endif
switch (next_mode) {
case PRV_M:
break;
case PRV_S:
if (!misa_extension('S'))
sbi_hart_hang();
break;
case PRV_U:
if (!misa_extension('U'))
sbi_hart_hang();
break;
default:
sbi_hart_hang();
}
val = csr_read(CSR_MSTATUS);
val = INSERT_FIELD(val, MSTATUS_MPP, next_mode);
val = INSERT_FIELD(val, MSTATUS_MPIE, 0);
#if __riscv_xlen == 32
if (misa_extension('H')) {
valH = csr_read(CSR_MSTATUSH);
valH = INSERT_FIELD(valH, MSTATUSH_MPV, next_virt);
csr_write(CSR_MSTATUSH, valH);
}
#else
if (misa_extension('H'))
val = INSERT_FIELD(val, MSTATUS_MPV, next_virt);
#endif
csr_write(CSR_MSTATUS, val);
csr_write(CSR_MEPC, next_addr);
if (next_mode == PRV_S) {
if (next_virt) {
csr_write(CSR_VSTVEC, next_addr);
csr_write(CSR_VSSCRATCH, 0);
csr_write(CSR_VSIE, 0);
csr_write(CSR_VSATP, 0);
} else {
csr_write(CSR_STVEC, next_addr);
csr_write(CSR_SSCRATCH, 0);
csr_write(CSR_SIE, 0);
csr_write(CSR_SATP, 0);
}
} else if (next_mode == PRV_U) {
if (misa_extension('N')) {
csr_write(CSR_UTVEC, next_addr);
csr_write(CSR_USCRATCH, 0);
csr_write(CSR_UIE, 0);
}
}
//★各引数に入っている値は下記の通り★
//arg0 = hartid
//arg1 = scratch->next_arg1
//next_addr = scratch->next_addr
//next_mode = scratch->next_mode
//next_virt = false
register unsigned long a0 asm("a0") = arg0;
register unsigned long a1 asm("a1") = arg1;
__asm__ __volatile__("mret" : : "r"(a0), "r"(a1));
__builtin_unreachable();
}
レジスタa0とa1に引数を入れ、mepcレジスタにペイロード先頭のアドレス(scratch->next_addr)を設定してmretします。
fw_next_addr() // firmware/fw_payload.S
// payload_binのアドレスを返す
// FW_PAYLOAD_PATHが定義済みなら.incbin FW_PAYLOAD_PATH
// 未定義ならwfi命令の無限ループ
// 結果はscratch->next_addrに格納
なぜscratch->next_addrにペイロード先頭のアドレスが入っているのか?については再掲するとともに、該当部分のソースコードを掲載します。
//opensbi/firmware/fw_payload.S
.section .entry, "ax", %progbits
.align 3
.global fw_next_addr
/*
* We can only use a0, a1, and a2 registers here.
* The next address should be returned in 'a0'.
*/
fw_next_addr:
lla a0, payload_bin //★payload_binのアドレスを返す★
ret
...
.section .payload, "ax", %progbits
.align 4
.globl payload_bin
payload_bin:
#ifndef FW_PAYLOAD_PATH //★FW_PAYLOAD_PATHが未定義ならwfi命令の無限ループ★
wfi
j payload_bin
#else
.incbin FW_PAYLOAD_PATH //★FW_PAYLOAD_PATHが定義済みならFW_PAYLOAD_PATHで指定されたファイルをこの場所に展開★
#endif
//opensbi/firmware/fw_base.S
/* Store next address in scratch space */
MOV_3R s0, a0, s1, a1, s2, a2
call fw_next_addr
REG_S a0, SBI_SCRATCH_NEXT_ADDR_OFFSET(tp) //★結果はscratch->next_addrに格納★
MOV_3R a0, s0, a1, s1, a2, s2
最後にFW_PAYLOAD_PATHマクロはどこから来るかを紹介しておきます。
#opensbi/firmware/objects.mk
firmware-bins-$(FW_PAYLOAD) += fw_payload.bin
ifdef FW_PAYLOAD_PATH
FW_PAYLOAD_PATH_FINAL=$(FW_PAYLOAD_PATH)
else
FW_PAYLOAD_PATH_FINAL=$(platform_build_dir)/firmware/payloads/test.bin
endif
firmware-genflags-$(FW_PAYLOAD) += -DFW_PAYLOAD_PATH=\"$(FW_PAYLOAD_PATH_FINAL)\"
ビルド時に指定したFW_PAYLOAD_PATH変数が、Makefile内で-Dオプションとしてコンパイラに渡されてマクロとして定義されます。シンプルですね。
次回以降はメモリマップやデバイスツリーについて調べようと思います。
目次: Arduino
Debian Testingをアップデートしたところ先日ビルドしたKiCad 7.0が動かなくなり、今回はビルドすらできなくなりました。cmakeを実行するとTKIGESライブラリが見つからないと言われてしまいます。
(略) -- Found the following HarfBuzz libraries: -- HarfBuzz (required): /usr/lib/x86_64-linux-gnu/libharfbuzz.so -- Found HarfBuzz: /usr/include/harfbuzz (found version "8.3.0") -- Found Fontconfig: /usr/lib/x86_64-linux-gnu/libfontconfig.so (found version "2.15.0") -- Checking for module 'ngspice' -- Found ngspice, version 42 -- Found ngspice: /usr/include -- Found OCC: /usr/include/opencascade (found version "7.8.1") *** OpenCascade library missing *** Could not find a library for TKIGES at /usr/lib/x86_64-linux-gnu Verify your OpenCascade installation or pass CMake the library directory as '-DOCC_LIBRARY_DIR=<path>' CMake Error at cmake/FindOCC.cmake:192 (message): Call Stack (most recent call first): CMakeLists.txt:807 (find_package) -- Configuring incomplete, errors occurred!
エラーメッセージさん曰く、OpenCascadeのライブラリが見つからないとのこと。バージョンを確認するとDebian StableはOpenCascad 7.6.3、Debian TestingはOpenCascade 7.8.1でした。この間にlibTKIGES.soのライブラリ名が変わったようです。
OpenCascadeのソースコード(リポジトリへのリンク)のコミットログを見ていると、ライブラリ名が一気に変わった瞬間がありました。
commit bd651bbbd9e30fc5a73d32d11e0ea1a1821afd76 Author: dpasukhi <dpasukhi@opencascade.com> Date: Sun Nov 19 11:09:33 2023 +0000 0033531: Configuration - Rework DataExchange ToolKits organization Integrated DE plugin functionality. Reworked DE components: - TKDESTEP: Handling STEP file format. - TKDEOBJ: Handling OBJ file format. - TKDEIGES: Handling IGES file format. - TKDEGLTF: Handling GLTF file format. - TKDEVRML: Handling VRML file format. - TKDEPLY: Handling PLY file format. - TKDESTL: Handling STL file format. Reworked DE DRAW components: TKXSDRAWSTEP: Container for DE command to work with STEP. TKXSDRAWOBJ: Container for DE command to work with OBJ. TKXSDRAWIGES: Container for DE command to work with IGES. TKXSDRAWGLTF: Container for DE command to work with GLTF. TKXSDRAWVRML: Container for DE command to work with VRML. TKXSDRAWPLY: Container for DE command to work with PLY. TKXSDRAWSTL: Container for DE command to work with STL. TKXSDRAW rework to be base DRAW plugin to keep DE session and utils. Updated documentation Updated samples
TKIGES以外にも盛大に変わっていて、OpenCascadeを使っている人たちは全滅しているんじゃないかな?と思います。どこで変わったか調べるとv7.7.2とv7.8.0の間で変わったようですね。
$ git log V7_7_2..V7_8_0 | grep bd651b commit bd651bbbd9e30fc5a73d32d11e0ea1a1821afd76
コミットの中身を参考にしつつKiCadを修正します。
diff --git a/cmake/FindOCC.cmake b/cmake/FindOCC.cmake
index af249c9ce2..923ccb4c7a 100644
--- a/cmake/FindOCC.cmake
+++ b/cmake/FindOCC.cmake
@@ -45,7 +45,6 @@ set( OCC_LIBS
TKGeomAlgo
TKGeomBase
TKHLR
- TKIGES
TKLCAF
TKMath
TKMesh
@@ -55,18 +54,16 @@ set( OCC_LIBS
TKPrim
TKService
TKShHealing
- TKSTEP209
- TKSTEPAttr
- TKSTEPBase
- TKSTEP
- TKSTL
TKTObj
TKTopAlgo
TKV3d
- TKVRML
+ TKDESTL
+ TKDEVRML
TKXCAF
- TKXDEIGES
- TKXDESTEP
+ TKDE
+ TKDECascade
+ TKDEIGES
+ TKDESTEP
TKXMesh
TKXmlL
TKXml
OpenCascadeやKiCadの中身を全くわかっていないので、APIや実装が大きく変わっていたらお手上げでした。しかし幸いなことにライブラリ名が変わっただけなので、OpenCascadeのライブラリを探しているcmake/FindOCC.cmakeの修正だけで乗り切れました。
Debian TestingのKiCadは8.0系に移行しましたし、そろそろKiCad 7.0系を引きずるのをやめてバージョンアップを考えないといけないかなあ……?
目次: Linux
以前、Berkeley Bootloader(riskv-pkのbbl)を使用してRISC-V Linuxを立ち上げました(2019年2月26日の日記参照)が、今回はOpenSBIを使ってLinuxの起動を試みようと思います。
前回との違いはブートローダをbblからOpenSBIに変更したことです。
前回同様なので省略します。
前回同様にbusyboxと手作業でinitramfsを作りますが、ビルドディレクトリを使うように手順を変更しています。バージョンは1.35.0を使います(タグ名: 1_35_0)。
$ git clone https://git.busybox.net/busybox $ cd busybox $ mkdir build $ make \ O=build \ ARCH=riscv \ CROSS_COMPILE=riscv64-unknown-linux-gnu- \ menuconfig - Settings ---> [*] Build static binary (no shared libs) $ make \ O=build \ ARCH=riscv \ CROSS_COMPILE=riscv64-unknown-linux-gnu- \ all
ビルドに成功するとbusyboxという実行ファイルが生成されるはずです。次にinitramfsを作成します。基本的な流れはディレクトリに必要なファイルを配置し、cpioで固めるだけです。
$ mkdir initramfs-work-riscv $ mkdir initramfs-work-riscv/root $ cd initramfs-work-riscv/root $ mkdir bin $ cp ../../build/busybox bin/ $ ln -s busybox bin/sh $ ln -s bin/busybox init $ mkdir dev $ sudo cp -a /dev/tty* dev/ $ find . | cpio --format=newc -o > ../initramfs.cpio
横着してホストマシンのデバイスファイルをコピーしてます。RISC-V Linux上で何か操作したければinit, sh以外のシンボリックリンクも作った方が便利です。
前回との違いは下記のとおりです。linux-nextでも手順は同じですが、linux-nextはたまにデグレして動作せず話がややこしくなるので、linux本線の少し古めのバージョンを使います。
バージョンは6.9を使います(タグ名: v6.9)。
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git $ cd linux $ mkdir build $ make \ O=build \ ARCH=riscv \ CROSS_COMPILE=riscv64-unknown-linux-gnu- \ defconfig $ make \ O=build \ ARCH=riscv \ CROSS_COMPILE=riscv64-unknown-linux-gnu- \ menuconfig - General setup ---> (../busybox/initramfs-work-riscv/initramfs.cpio) Initramfs source file(s) $ make \ O=build \ ARCH=riscv \ CROSS_COMPILE=riscv64-unknown-linux-gnu- \ all
前回のようにexportで環境変数を設定しても良いですし、makeに逐一指定しても良いです。
ブートローダーにはOpenSBIを使います。バージョンは1.5を使います(タグ名: v1.5)。
$ git clone https://github.com/riscv-software-src/opensbi.git $ cd opensbi $ make \ CROSS_COMPILE=riscv64-unknown-linux-gnu- \ PLATFORM=generic \ FW_PAYLOAD_PATH=../linux/build/arch/riscv/boot/Image \ clean all
BBLもそうでしたけど、ブートローダーとカーネルは1つのバイナリになります。FW_PAYLOAD_PATHにはカーネルのバイナリ(vmlinuxではない)のパスを指定します。PLATFORMはSoCやボードの種類を指定します、QEMU向けの場合はgenericです。
エミュレータにはQEMUを使います。システムにインストールされているバージョンが古い場合があるので、ソースコードからビルドする方法も紹介します。QEMU実行時にオプション--netdev userを使うのであれば、QEMUをビルドする前にlibslirp-devをインストールしておく必要があります。
$ cd qemu $ mkdir build $ cd build $ ../configure \ --target-list=riscv32-softmmu,riscv32-linux-user,riscv64-softmmu,riscv64-linux-user \ --enable-debug \ --disable-docs $ ninja
バージョンはv9.0.2を使いました。target-listやdisable-docsはビルド時間の短縮のためで指定しなくても良いです。使わないアーキテクチャ用(ARMとか)のエミュレータがビルドされるためビルドに若干時間がかかります。enable-debugはQEMUをデバッグする可能性があれば指定してください。
ここまでの準備でブートローダーとLinuxカーネルの初期部分は確認できるはずですので、動作確認します。
$ cd qemu/build $ ./qemu-system-riscv64 \ -machine virt \ -nographic \ -bios none \ -chardev stdio,id=con,mux=on \ -serial chardev:con \ -mon chardev=con,mode=readline \ -smp 4 \ -kernel ../../opensbi/build/platform/generic/firmware/fw_payload.bin
うまくいっていればこんな感じになるはずです。
OpenSBI v1.5 ____ _____ ____ _____ / __ \ / ____| _ \_ _| | | | |_ __ ___ _ __ | (___ | |_) || | | | | | '_ \ / _ \ '_ \ \___ \| _ < | | | |__| | |_) | __/ | | |____) | |_) || |_ \____/| .__/ \___|_| |_|_____/|____/_____| | | |_| Platform Name : riscv-virtio,qemu Platform Features : medeleg Platform HART Count : 4 Platform IPI Device : aclint-mswi Platform Timer Device : aclint-mtimer @ 10000000Hz Platform Console Device : uart8250 Platform HSM Device : --- Platform PMU Device : --- Platform Reboot Device : syscon-reboot Platform Shutdown Device : syscon-poweroff Platform Suspend Device : --- Platform CPPC Device : --- Firmware Base : 0x80000000 Firmware Size : 357 KB Firmware RW Offset : 0x40000 Firmware RW Size : 101 KB Firmware Heap Offset : 0x4f000 Firmware Heap Size : 41 KB (total), 2 KB (reserved), 11 KB (used), 27 KB (free) Firmware Scratch Size : 4096 B (total), 416 B (used), 3680 B (free) Runtime SBI Version : 2.0 Domain0 Name : root Domain0 Boot HART : 0 Domain0 HARTs : 0*,1*,2*,3* Domain0 Region00 : 0x0000000000100000-0x0000000000100fff M: (I,R,W) S/U: (R,W) Domain0 Region01 : 0x0000000010000000-0x0000000010000fff M: (I,R,W) S/U: (R,W) Domain0 Region02 : 0x0000000002000000-0x000000000200ffff M: (I,R,W) S/U: () Domain0 Region03 : 0x0000000080040000-0x000000008005ffff M: (R,W) S/U: () Domain0 Region04 : 0x0000000080000000-0x000000008003ffff M: (R,X) S/U: () Domain0 Region05 : 0x000000000c400000-0x000000000c5fffff M: (I,R,W) S/U: (R,W) Domain0 Region06 : 0x000000000c000000-0x000000000c3fffff M: (I,R,W) S/U: (R,W) Domain0 Region07 : 0x0000000000000000-0xffffffffffffffff M: () S/U: (R,W,X) Domain0 Next Address : 0x0000000080200000 Domain0 Next Arg1 : 0x0000000082200000 Domain0 Next Mode : S-mode Domain0 SysReset : yes Domain0 SysSuspend : yes Boot HART ID : 0 Boot HART Domain : root Boot HART Priv Version : v1.12 Boot HART Base ISA : rv64imafdch Boot HART ISA Extensions : sstc,zicntr,zihpm,zicboz,zicbom,sdtrig,svadu Boot HART PMP Count : 16 Boot HART PMP Granularity : 2 bits Boot HART PMP Address Bits: 54 Boot HART MHPM Info : 16 (0x0007fff8) Boot HART Debug Triggers : 2 triggers Boot HART MIDELEG : 0x0000000000001666 Boot HART MEDELEG : 0x0000000000f0b509 [ 0.000000] Linux version 6.9.0 (katsuhiro@blackbird) (riscv64-unknown-linux-gnu-gcc (GCC) 14.1.0, GNU ld (GNU Binutils) 2.42.50.20240622) #1 SMP Tue Jul 16 18:44:37 JST 2024 [ 0.000000] random: crng init done [ 0.000000] Machine model: riscv-virtio,qemu [ 0.000000] SBI specification v2.0 detected [ 0.000000] SBI implementation ID=0x1 Version=0x10005 [ 0.000000] SBI TIME extension detected [ 0.000000] SBI IPI extension detected [ 0.000000] SBI RFENCE extension detected [ 0.000000] SBI SRST extension detected [ 0.000000] SBI DBCN extension detected [ 0.000000] efi: UEFI not found. ...
今後はOpenSBIやLinuxカーネルの動作を調べる予定です。
目次: Yocto
Yocto Scarthgap(5.0.1)のメモです。前回同様、コードを読んで依存関係を調べるのは大変時間が掛かるので、簡易的な調査手法として全ファイルを消してエラーを観察し、エラーの原因となるファイルを復活させて次ステップに進める手段を用います。
引き続きBitbakeを実行してエラーを見て、足りないファイルを元に戻していくという方法で各パーツや設定の動作を見ていきたいと思います。
$ bitbake core-image-sato ERROR: OE-core's config sanity checker detected a potential misconfiguration. Either fix the cause of this error or at your own risk disable the checker (see sanity.conf). Following is the list of potential problems / advisories: DISTRO 'poky' not found. Please set a valid DISTRO in your local.conf
新たなディストリビューション(DISTRO, distribution)という用語が出てきました。Yoctoのドキュメント(Creating Your Own Distribution - The Yocto Project)には特に用語の説明がないのですが、一般的なLinuxディストリビューションと同じ意味、つまり「Linuxカーネルと周辺ソフトウェアを1つにまとめたもの」を意味していると思われます。
ビルドディレクトリbuild/conf/local.confに下記の記述があり、デフォルトのディストリビューションはpokyとなります。
## build/conf/local.conf
...(略)...
# Default policy config
#
# The distribution setting controls which policy settings are used as defaults.
# The default value is fine for general Yocto project use, at least initially.
# Ultimately when creating custom policy, people will likely end up subclassing
# these defaults.
#
DISTRO ?= "poky"
ちなみに環境変数による設定は上書きできます。DISTROに限らず他の変数も同様です。
$ DISTRO=something bitbake core-image-sato ERROR: OE-core's config sanity checker detected a potential misconfiguration. Either fix the cause of this error or at your own risk disable the checker (see sanity.conf). Following is the list of potential problems / advisories: DISTRO 'something' not found. Please set a valid DISTRO in your local.conf
Yoctoのドキュメントによれば、conf/distroの下にある"distro名.conf"がdistribution configuration fileなので、poky用ならばconf/distro/poky.confが該当します。探すとmeta-poky/conf/distroの下にpoky.confがありました。このファイルも元に戻します。
$ bitbake core-image-sato ERROR: Unable to parse /home/katsuhiro/share/projects/oss/poky/bitbake/lib/bb/parse/parse_py/BBHandler.py Traceback (most recent call last): File "/home/katsuhiro/share/projects/oss/poky/bitbake/lib/bb/parse/parse_py/BBHandler.py", line 71, in inherit(files=['poky-sanity'], fn='configuration INHERITs', lineno=0, d=<bb.data_smart.DataSmart object at 0x7fb98c543910>, deferred=False): if not os.path.exists(file): > raise ParseError("Could not inherit file %s" % (file), fn, lineno) bb.parse.ParseError: ParseError in configuration INHERITs: Could not inherit file classes/poky-sanity.bbclass
再びクラスが見つからないと言われています。どうもpokyディストリビューションだけで使っているクラスがあるようですので、該当するファイルも合わせて元に戻します。
今回登場したファイル、ディレクトリは、
こんなとこ。次回はレシピを見ます。
目次: RISC-V
目次: 独自OS
DOOMのクローン実装prboom2を組み込み環境に移植する話です。前回は画面描画を1.5倍ほど高速化しました。
今まで「組み込み環境」とひとまとめにして呼んでいましたが、OSだけでもLinux、RTOS、もっとシンプルなものも含めると無数にあります。既存のOSの上で動かすのも大変興味深いですが、今回は自作OS(2022年12月14日の日記参照)の上で動かしてみたいと思います(リポジトリへのリンク)。
今まで下記の機能を削除したり、代替機能で置き換えてきたのはこのためです。動作させるボードやSoCにHW機能がない場合もあれば、自作OSにドライバを実装しておらずHW機能があっても自作OSから使う方法がないのです。
自作OSの特徴は下記のようにLinuxと同じlibcを採用して、Linuxのアプリケーションを移植しやすい仕組みになっていることです。速度の遅い組み込み環境で何度も動作確認するのは大変ですから、なるべくLinux上で検証できるようにした構成です。
今回のprboom2の移植の際もC言語のコードを弄る必要はありませんでした。ビルドシステムというかMakefileを別途作っていますが、なんでだったか忘れました……。
自作OSのランタイム側のビルドのための準備については、ドキュメント(リンク)を見ていただくとして。QEMU RISC-V 32bit向けに自作OSをビルドします。
$ cd baremetal_crt $ cmake ./ -G Ninja -B build \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_INSTALL_PREFIX=./test/sysroot/ \ -DARCH=riscv \ -DCROSS_COMPILE=riscv64-unknown-elf- \ -DCC=gcc \ -DDEFCONF=riscv_qemu_virt_32_xip $ ninja -C build install
次に改造prboom2をビルドします。前回まで説明してきた改造を全て適用したprboom2をGitHubに置いた(リンク)ので、このコードを使うと簡単です。実装が適当かつ書きかけのコードとかが残っていて美しくありませんが、細かいことは気にしないでください……。
$ cd prboom2 $ ./configure --disable-gl $ cd src/riscv $ make USE_NEWLIB=y USE_SYSROOT=(baremetal_crtのパス)/test/sysroot all
最初のconfigureはconfig.hを作成するために実行しています。特にconfig.hを変更する予定はないので固定で置いてしまえば良かったんじゃないか?と思いましたけど、まあいいでしょう。
$ qemu-system-riscv32 \ -machine virt \ -bios none \ -net none \ -nographic \ -chardev stdio,id=con,mux=on \ -serial chardev:con \ -mon chardev=con,mode=readline \ -smp 4 \ -s -kernel prboom2/src/riscv/prboom
こんな感じで動作確認できます。
描画速度を稼ぐため解像度を半分にしている以外、見た目が前回まで(つまりLinuxで動作させたとき)と同じなのであまり面白くないですね……。
目次: RISC-V
DOOMのクローン実装prboom2を組み込み環境に移植する話です。前回はUARTしかないショボい組み込み環境でも動作できるようにprboom2を改造しました。
Linuxの仮想端末は超速いので描画速度はあまり気になりませんが、組み込み機器に移植すると描画がめちゃくちゃ遅いことに気づくと思います。理由は簡単でUARTが遅いからです。UARTは9600bps〜115.2kbpsがよく使われる速度で、速いものでもせいぜい1.5Mbps程度です。
前回の実装では24bit色モードを使ったので、1つのピクセルを表現するために、エスケープシーケンス16バイト+スペース2バイト = 18バイト必要です。128x120の画面を1枚表示するのに276kB必要、9600bpsだと230秒(0.0043fps)、1.5Mbpsでも1.47秒(0.67fps)です。
端末の背景色設定は24bit色モードと256色モードがあります。256色モードの場合、エスケープシーケンス10バイト+スペース2バイト = 12バイトなので、24bit色より多少は軽量です。128x120の画面を1枚表示するのに184kB必要です。1.5Mbpsで0.98秒(1.01fps)ですね。
24bit色モードはエスケープシーケンスでR, G, Bの各値を指定できるのでどんな色でも指定できました。256色モードの場合はパレットの各色に番号が振られていて、エスケープシーケンスで番号を指定する仕組みです。カラーパレットは上記の配置で固定されていてパレットにない色は使えません。
DOOMの画面の各ピクセルの色を見て、256色モードパレットの色を指す番号を探す減色処理を追加します。256色モードのカラーパレット番号と色の関係は規則的です(※)から、それほど難しくないでしょう。
256色モードだとこんな感じです。24bit色版と比較するとややディティールが潰れていて黒っぽいですけど、まあまあ良さそうです。
(※)16-231番(216色)はR, G, Bそれぞれ6段階の組み合わせです。パレット番号を表す式は、16 + 36r + 6g + b (0 <= r, g, b <= 5) となっています。
もう少し軽くできないか考えてみましょう。今は毎回「背景色変更のエスケープシーケンス + スペース2つ」を出力していますが、左隣のピクセルと色が同じピクセルであれば、背景色変更をする必要はないです。
つまり左隣と同じ色のピクセルが連続する限りエスケープシーケンスの出力を省略できます。非常に単純なRLE(Run-Length Encoding)の一種とも言えましょう。
デモ開始直後のところがわかりやすいですが、256色モードで描画すると真っ黒になってしまう画面が散見されます。
DOOMの画面は比較的暗い色が多いですが、256色モードのパレットは暗い色から明るい色まで均等に割り振られているため、暗い色の表現力が低いです。単純に近似すると画面の色がほぼ真っ黒になります。
この問題を回避するために暗い色をグレースケールに置き換えます。グレースケールと暗い赤、緑、青は違う色ですが、暗い色の色味は区別しづらいのでグレースケールに置き換えてもさほど不自然にはなりません。
こんな感じです。良く見ると壁の暗い緑がグレーになっていたり若干変ですが、総じて悪くない仕上がりです。これで256色モードが活用でき、24bit色モードに比べて描画速度が1.5倍以上になりました。
今まではずっとLinuxの上で作業してきましたが、次回でようやく組み込み機器に移植します。
目次: RISC-V
DOOMのクローン実装prboom2を組み込み環境に移植する話です。前回はLinux上で動作確認しました。今回は想定する組み込み環境と移植作戦&移植です。
想定する組み込み環境は下記のとおりです。
改造元にするコードは何でも良いですけど、私は動作するコードから出発した方がやりやすいので、前回Linux上で動作を確認した実装(src/SDL/*)をベースに改造します。移植にあたって障害となりそうなものは、
この辺の機能が移植先の組み込み環境に存在しないことです。順番に対処しましょう。
SDL(Simple DirectMedia Layer、公式サイトへのリンク、リポジトリへのリンク)はC/C++から簡単にグラフィクスや音声、各種入力機能が利用できる、素敵なライブラリです。が、しかし移植先の環境にSDLなどという高等なものはないのでSDLを使用している箇所は削除します。
メイン(src/SDL/i_main.c)、サウンド(src/SDL/i_sound.c)、画面(src/SDL/i_video.c)とジョイスティック入力(src/SDL/i_joy.c)はSDL.hをインクルードか、SDLの構造体やマクロを使用していてコンパイルエラーになるので、エラーになる箇所を全て削除します。
サウンド(src/SDL/i_sound.c)、とジョイスティック入力(src/SDL/i_joy.c)は動作しなくても問題を起こさないように、常に成功もしくは無意味な値を返すように改造します。
ゲームエンジンを動作させるにはprboom2ビルド時に生成されるprboom.wadとIWADファイル(オリジナルのDOOMのWADを指してこのように呼ぶそうです)の2つのファイルが必要です。が、移植先の環境にファイルなどという高尚なものはないので、下記の作戦でファイルアクセスなしでも動作するように改造します。
ファイルアクセスを行うコードはシステム(src/SDL/i_system.c)に実装されていますので、ファイルアクセスしている実装を全部静的配列へのアクセスに書き換えます。他の場所(src/d_main.c, src/w_wad.c)のファイルアクセスしている箇所も同様に置き換えます。
ファイルアクセス機能を置き換えたので、ファイル(DOOM.WADとprboom.wad)をC言語の静的配列に変換する方法も作りましょう。
変換方法は色々あると思いますが、昔作った「CmakeでバイナリをC言語の配列に変換する」やつを使います(2023年6月16日の日記参照)。こやつは適当に作った割に意外と便利です。
画面描画を削除するとDOOMが動いているかどうかわかりません。移植先の環境に画面を描画するような高尚な機能はないので、UARTとANSIエスケープシーケンスを利用して画面描画の代わりとします。
UARTの出力を受け取る端末には文字を出力する以外にもたくさんの機能があります。端末の各種機能のなかから「背景色の設定+空白の出力」をピクセル描画の代わりとして利用します。端末によって対応する機能は異なり(ECMA-48やANSI X3.64仕様が有名)ますが、今回の実装ではANSI X3.64のカーソル移動、文字色の変更機能を使用します。大抵の端末ソフトが実装しているはずです。たぶん。
端末の文字は縦長(縦:横=2:1くらい、フォント依存です)が多いため、Space 2文字でだいたい正方形になります。つまり背景色変更 + Spece 2文字 = 1ピクセルとすれば画面の描画ができるという寸法です。普通の文字サイズだと1ピクセルが大きくなりすぎるので、端末側のフォントサイズ設定で調整しましょう。
背景色を変更するエスケープシーケンスはこんな感じ。
24bit色モードであれば1つのピクセルを表現するために、エスケープシーケンス16バイト+スペース2バイト = 18バイト必要になります。極めて効率が悪いですが、とりあえず動かすために先に進みましょう。
方針が決まったところで画面描画の実装(src/SDL/i_video.c)を改造します。prboom2の画面はフルカラーではなくパレット描画なので、パレット操作をする部分も合わせて改造します。
prboom2が動作している様子(ウインドウはPuTTY、画面サイズ128x120、フォントサイズ5pt)
実際描画してみるとこんな感じです。ウインドウタイトルを見るとわかりますが、SDLによる描画ではなくPuTTYという端末ソフトウェアの画面です。
とりあえず動きました。やったね。画面描画が遅い問題は、次回以降対処していこうと思います。
目次: RISC-V
DOOMというFPS(First Person Shooter、一人称視点のシューティングゲーム)があります。1993年にid Softwareが開発したロングセラーシリーズです。オリジナルのid Softwareによる実装も2012年にオープンソースになりました(リポジトリへのリンク)。
日本ではそれほどでもない気がしますが、海外ではDOOMの人気は絶大で海外エンジニアの間ではゲーム機ではない電子機器(プリンタ、カメラ、ATMまで)でDOOMを動作させる改造が非常に人気のようです。
DOOM移植の良いところとしては、3D描画のゲームなので画面が派手で目を引きます。それでいて昔のゲームなので、しょぼいスペックの機器でもそこそこ動きます。操作せずに放っておいてもデモが実行される点も素晴らしいです。
今回はオリジナルのDOOMではなくprboom2(リポジトリへのリンク)というクローン実装を使います。prboom2はLinuxで簡単に動作確認できて楽です。いいですね。
DOOMはゲームエンジンなので実行時にはゲームデータを指定しなければなりません。ゲームデータは*.WADファイル(Where's All Dataの略らしい)に格納されていて、オリジナルDOOMのWADファイルのことをIWAD(Internal WAD)と呼ぶようです。IWADはDOOMの製品版に付属しているはずです、見たことがないから知らないですけど。本来はオリジナルDOOMを購入しDOOM.WADを持ってくるべきですが、手に入れる手段がなさそうです……。
ネット検索するとIWADを公開しているサイトがあるのでどうにかして手に入れてください。Doom Wiki(リンク)にDOOM.WADのMD5 hashが掲載されているので、入手したIWADファイルがどのバージョンか(あるいは壊れていないか)確認しましょう。
今回はIWADファイルはDOOM.WAD、バージョン1.8のもので試します。
まずは普通に動かしてみます。ビルドは簡単でbootstrapとconfigureを実行してmakeするだけです。SDLがインストールされていない環境の場合はlibsdl2-devのインストールが必要かもしれません。
$ cd prboom2 $ ./bootstrap $ ./configure $ make $ ./src/prboom -iwad DOOM.WAD
DOOM同様prboom2もゲームエンジンなので実行時にはゲームデータを指定しなければなりません。ビルド時に勝手に生成されるprboom.wadファイルと、ゲームデータが入ったIWADファイル(オリジナルのDOOM、freedoomなど)の2つが必要です。prboom.wadはカレントディレクトリに置けば勝手に見つけます。IWADファイルはオプション-iwadで指定しましょう。
音声なしで起動したければ-nosound、フルスクリーンではなくウインドウモードが良ければ-nofullscreenオプションが使えます。フルスクリーンとウインドウモードの切り替えはゲーム内のオプションから設定することもできます。
確認だけで終わってしまいました。次回から組み込み環境への移植を始めたいと思います。
< | 2024 | > | ||||
<< | < | 07 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
- | 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 | - | - | - |
合計:
本日: