32-bit RISC-V Linux をビルドして Spike で動かす
最近は自作 32-bit RISC-V CPU で Linux を動かしていた。その前段階として 32-bit RISC-V Linux をビルドして Spike で動かしたのでそのときの備忘録。
Spike でただ Linux を動かすだけなら Buildroot の Spike 用の設定ファイルを使えば簡単なのだが (ただし、ビルド時間が恐ろしく長い...)、今回は以下のような諸事情があって一からビルドした。
- OpenSBI に手を加えて自作 CPU 向けの firmware を追加したい
- 命令セットとして RV32IMA_Zicsr_Zifencei_Zicntr を使いたい
- デフォルトだと RV32IMAFDC_Zicsr_Zifencei_Zicntr_Zihpm (RV32GC)
- 自作 CPU では単精度/倍精度浮動小数点拡張 (F/D) と圧縮命令拡張 (C)、ハードウェアパフォーマンスカウンタ拡張 (Zihpm) をサポートしていない
大まかな手順としては、まず Buildroot で initrd を生成し、次に Linux をビルドして Image を生成、その次に OpenSBI をビルドして firmware を生成、最後にそれを Spike に渡して Linux を動かすという流れになる。
なお、Buildroot と Linux のビルド方法は自作 CPU のものと同じ。自作 CPU では OpenSBI だけ少し修正して Linux を動かしている。
Spike で 32-bit RISC-V Linux を動かすときは以下の資料が参考になる。
環境
- Docker で
amd64/ubuntu:latestの環境を用意してコマンドの動作確認をした。
Architecture: x86_64 OS : Ubuntu 24.04.1 LTS
前準備
- 環境変数などの設定。
$ mkdir $HOME/work && export WORK=$_ $ mkdir $WORK/riscv32 && export RISCV32=$_ $ export PATH=$RISCV32/bin:$PATH $ sudo apt update && sudo apt upgrade -y
RISC-V GNU Compiler Toolchain
- RISC-V GNU Compiler Toolchain をインストールする。
- Buildroot、Linux、OpenSBI を RV32IMA_Zicsr_Zifencei_Zicntr 向けにコンパイルするためのクロスコンパイラ。
- RISC-V GNU Compiler Toolchain - GitHub
$ cd $WORK $ sudo apt install -y autoconf automake autotools-dev curl python3 python3-pip libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev ninja-build git cmake libglib2.0-dev libslirp-dev $ git clone https://github.com/riscv/riscv-gnu-toolchain $ cd riscv-gnu-toolchain $ ./configure --prefix=$RISCV32 --with-arch=rv32ima_zicsr_zifencei_zicntr --with-abi=ilp32 $ make linux -j $(nproc)
Spike
- Spike をインストールする。
- Spike は RISC-V の公式が提供している命令セットシミュレータ (Instruction Set Simulator, ISS)。
- Spike RISC-V ISA Simulator - GitHub
$ cd $WORK $ sudo apt install -y device-tree-compiler $ git clone https://github.com/riscv-software-src/riscv-isa-sim.git $ mkdir riscv-isa-sim/build && cd $_ $ ../configure --prefix=$RISCV32 --with-target=riscv32-unknown-linux-gnu $ make -j $(nproc) $ make install
Buildroot
- 組み込みシステム用の Linux 環境を構築するプロセスを簡素化、自動化してくれるもの。
- 今回は初期 RAM ディスク (initrd・initramfs) を生成するためだけに使用する。
- Buildroot を使用すると root 権限を使わなくても initrd が作れて便利。
- 初期 RAM ディスクは本命のルートファイルシステムをマウントするために使用するミニファイルシステムらしい。
- Spike は cpio 形式の initrd を使うのがデフォルトらしいので慣習に従っておく。
- 参考) https://www.kernel.org/doc/Documentation/filesystems/ramfs-rootfs-initramfs.txt
- https://buildroot.org/downloads/
$ cd $WORK $ sudo apt install -y cpio libncurses-dev rsync unzip wget $ wget https://buildroot.org/downloads/buildroot-2024.11-rc2.tar.xz $ tar Jxvf buildroot-2024.11-rc2.tar.xz $ cd buildroot-2024.11-rc2 $ make menuconfig - Target options ---> - Target Architecture (RISCV) ---> - Target Architecture Variant (Custom architecture) ---> - [*] Integer Multiplication and Division (M) - [*] Atomic Instructions (A) - Target Architecture Size (32-bit) ---> - Toolchain ---> - Toolchain type (External toolchain) ---> - ($(RISCV32)) Toolchain path - (riscv32-unknown-linux-gnu) Toolchain prefix - External toolchain kernel headers series (6.6.x) ---> - External toolchain C library (glibc) ---> - [*] Toolchain has SSP support? - [*] Toolchain has SSP strong support? - [ ] Toolchain has RPC support? - [*] Toolchain has C++ support? - [*] Toolchain has Fortran support? - [*] Toolchain has OpenMP support? - Filesystem images ---> - [*] cpio the root filesystem (for use as an initial RAM filesystem) - Compression method (gzip) ---> - [ ] tar the root filesystem $ make
$WORK/buildroot-2024.11-rc2/output/images/にrootfs.cpioとrootfs.cpio.gzが生成される。- 末尾に
.gzがついているのがgzipで圧縮されたもの。今回はこちらを使用する。
Linux
- 最新版の Linux Kernel 6.12.1 (2024/12/01 時点) をビルドする。
- 今回は
tinyconfigと32-bit.configを使用する。 - 参考) https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/README.rst
- 参考) RISC-VでLinuxの動くマルチコアをつくるのにやったこと (2. ソフト編)
- Linux - GitHub
- https://kernel.org/
$ cd $WORK $ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.1.tar.xz $ tar Jxvf linux-6.12.1.tar.xz $ cd linux-6.12.1 $ make mrproper $ make ARCH=riscv CROSS_COMPILE=riscv32-unknown-linux-gnu- tinyconfig $ make ARCH=riscv CROSS_COMPILE=riscv32-unknown-linux-gnu- 32-bit.config $ make ARCH=riscv CROSS_COMPILE=riscv32-unknown-linux-gnu- menuconfig - General setup ---> - [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support - ($(WORK)/buildroot-2024.11-rc2/output/images/rootfs.cpio.gz) Initramfs source file(s) - [*] Support initial ramdisk/ramfs compressed using gzip (NEW) - [ ] Support initial ramdisk/ramfs compressed using bzip2 - [ ] Support initial ramdisk/ramfs compressed using LZMA - [ ] Support initial ramdisk/ramfs compressed using XZ - [ ] Support initial ramdisk/ramfs compressed using LZO - [ ] Support initial ramdisk/ramfs compressed using LZ4 - [ ] Support initial ramdisk/ramfs compressed using ZSTD - [*] Configure standard kernel features (expert users) ---> - [*] Enable support for printk - [*] Enable futex support - Platform type ---> # - [*] Symmetric Multi-Processing # Enable if using multi-core - [ ] Emit compressed instructions when building Linux # You can disable it by disabling “Boot options ---> UEFI runtime support” first - Unaligned Accesses Support (Emulate unaligned access where system support is missing) ---> - Boot options ---> - [ ] UEFI runtime support - [*] Permit falling back to parsing riscv,isa for extension support by default - Executable file formats ---> - [*] Kernel support for ELF binaries - [*] Write ELF core dumps with partial segments (NEW) - [*] Kernel support for scripts starting with #! - [ ] Kernel support for flat binaries - [*] Kernel support for MISC binaries - [*] Enable core dump support - Device Drivers ---> - Generic Driver Options ---> - [*] Maintain a devtmpfs filesystem to mount at /dev - [*] Automount devtmpfs at /dev, after the kernel mounted the rootfs - Character devices ---> - [*] Enable TTY - [*] Virtual terminal (NEW) - [*] Enable character translations in console (NEW) - [*] Support for console on virtual terminal (NEW) - [ ] Support for binding and unbinding console drivers (NEW) - [*] Unix98 PTY support (NEW) - [*] Legacy (BSD) PTY support (NEW) - (256) Maximum number of legacy PTY in use (NEW) - [*] Allow legacy TIOCSTI usage (NEW) - [*] Automatically load TTY Line Disciplines (NEW) - Serial drivers ---> - [*] Early console using RISC-V SBI - [*] RISC-V SBI console support - File systems ---> - Pseudo filesystems ---> - [*] /proc file system support - [ ] /proc/kcore support (NEW) - [*] Sysctl support (/proc/sys) (NEW) - [*] Enable /proc page monitoring (NEW) - [ ] Include /proc/<pid>/task/<tid>/children file (NEW) - [*] sysfs file system support - Kernel hacking ---> - printk and dmesg options ---> - [*] Show timing information on printks $ make ARCH=riscv CROSS_COMPILE=riscv32-unknown-linux-gnu- -j $(nproc)
$WORK/linux-6.12.1/にvmlinux、$WORK/linux-6.12.1/arch/riscv/boot/にImageが生成される。vmlinuxは ELF ファイルであり、Imageはvmlinuxをobjcopy -O binary vmlinuxしたバイナリファイル。- OpenSBI には
Imageを payload として渡す。 - Linux のアセンブリが見たい場合は
riscv32-unknown-linux-gnu-objdump -d vmlinux > vmlinux.dumpなどとしてディスアセンブリするといい。
OpenSBI
- RISC-V Open Source Supervisor Binary Interface (OpenSBI)
- RISC-V Supervisor Binary Interface (SBI) のオープンソース実装が OpenSBI らしい。
- Machine-mode で実行されるプラットフォーム固有のファームウェア。
- Supervisor-mode からシステムコール形式で呼び出すことができる。
- ブートローダー的な役割もある。
- 先ほど生成した Linux の
Imageを payload として渡す。 - 参考) OpenSBI Deep Dive - Western Digital
- OpenSBI - GitHub
$ cd $WORK $ git clone https://github.com/riscv-software-src/opensbi.git $ cd opensbi $ make CROSS_COMPILE=riscv32-unknown-linux-gnu- PLATFORM_RISCV_XLEN=32 PLATFORM_RISCV_ABI=ilp32 PLATFORM_RISCV_ISA=rv32ima_zicsr_zifencei_zicntr PLATFORM=generic FW_PAYLOAD_PATH=$WORK/linux-6.12.1/arch/riscv/boot/Image
$WORK/opensbi/build/platform/generic/firmware/にfw_payload.elfが生成される。- これが Buildroot の initrd、Linux Kernel、OpenSBI をすべて含んだ ELF ファイル。
- これを Spike で実行する。
Running Linux on Spike
- Spike で
fw_payload.elfを実行して Linux を動かす。 - exit する方法: Ctrl + c → q → Enter
$ spike --isa=rv32ima_zicsr_zifencei_zicntr -m256 --bootargs 'root=/dev/ram console=hvc0 earlycon=sbi' $WORK/opensbi/build/platform/generic/firmware/fw_payload.elf OpenSBI v1.5-137-ga387a8d ____ _____ ____ _____ / __ \ / ____| _ \_ _| | | | |_ __ ___ _ __ | (___ | |_) || | | | | | '_ \ / _ \ '_ \ \___ \| _ < | | | |__| | |_) | __/ | | |____) | |_) || |_ \____/| .__/ \___|_| |_|_____/|____/_____| | | |_| Platform Name : ucbbar,spike-bare Platform Features : medeleg Platform HART Count : 1 Platform IPI Device : aclint-mswi Platform Timer Device : aclint-mtimer @ 10000000Hz Platform Console Device : uart8250 Platform HSM Device : --- Platform PMU Device : --- Platform Reboot Device : htif Platform Shutdown Device : htif Platform Suspend Device : --- Platform CPPC Device : --- Firmware Base : 0x80000000 Firmware Size : 321 KB Firmware RW Offset : 0x40000 Firmware RW Size : 65 KB Firmware Heap Offset : 0x47000 Firmware Heap Size : 37 KB (total), 2 KB (reserved), 10 KB (used), 24 KB (free) Firmware Scratch Size : 4096 B (total), 244 B (used), 3852 B (free) Runtime SBI Version : 2.0 Domain0 Name : root Domain0 Boot HART : 0 Domain0 HARTs : 0* Domain0 Region00 : 0x10000000-0x10000fff M: (I,R,W) S/U: (R,W) Domain0 Region01 : 0x80040000-0x8005ffff M: (R,W) S/U: () Domain0 Region02 : 0x02080000-0x020bffff M: (I,R,W) S/U: () Domain0 Region03 : 0x80000000-0x8003ffff M: (R,X) S/U: () Domain0 Region04 : 0x02000000-0x0207ffff M: (I,R,W) S/U: () Domain0 Region05 : 0x0c000000-0x0cffffff M: (I,R,W) S/U: (R,W) Domain0 Region06 : 0x00000000-0xffffffff M: () S/U: (R,W,X) Domain0 Next Address : 0x80400000 Domain0 Next Arg1 : 0x82200000 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 : rv32ima Boot HART ISA Extensions : zicntr,sdtrig Boot HART PMP Count : 16 Boot HART PMP Granularity : 2 bits Boot HART PMP Address Bits: 32 Boot HART MHPM Info : 0 (0x00000000) Boot HART Debug Triggers : 4 triggers Boot HART MIDELEG : 0x00000222 Boot HART MEDELEG : 0x0004b109 [ 0.000000] Linux version 6.12.1 (root@bbe998f21521) (riscv32-unknown-linux-gnu-gcc () 14.2.0, GNU ld (GNU Binutils) 2.43.1) #1 Sun Dec 1 16:38:35 JST 2024 [ 0.000000] OF: fdt: Ignoring memory range 0x80000000 - 0x80400000 [ 0.000000] Machine model: ucbbar,spike-bare [ 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] earlycon: sbi0 at I/O port 0x0 (options '') [ 0.000000] printk: legacy bootconsole [sbi0] enabled [ 0.000000] OF: reserved mem: 0x80000000..0x8003ffff (256 KiB) nomap non-reusable mmode_resv1@80000000 [ 0.000000] OF: reserved mem: 0x80040000..0x8005ffff (128 KiB) nomap non-reusable mmode_resv0@80040000 [ 0.000000] Zone ranges: [ 0.000000] Normal [mem 0x0000000080400000-0x000000008fffffff] [ 0.000000] Movable zone start for each node [ 0.000000] Early memory node ranges [ 0.000000] node 0: [mem 0x0000000080400000-0x000000008fffffff] [ 0.000000] Initmem setup node 0 [mem 0x0000000080400000-0x000000008fffffff] [ 0.000000] Falling back to deprecated "riscv,isa" [ 0.000000] riscv: base ISA extensions aim [ 0.000000] riscv: ELF capabilities aim [ 0.000000] Kernel command line: root=/dev/ram console=hvc0 earlycon=sbi [ 0.000000] Dentry cache hash table entries: 32768 (order: 5, 131072 bytes, linear) [ 0.000000] Inode-cache hash table entries: 16384 (order: 4, 65536 bytes, linear) [ 0.000000] Built 1 zonelists, mobility grouping on. Total pages: 64512 [ 0.000000] mem auto-init: stack:all(zero), heap alloc:off, heap free:off [ 0.000000] SLUB: HWalign=64, Order=0-1, MinObjects=0, CPUs=1, Nodes=1 [ 0.000000] NR_IRQS: 64, nr_irqs: 64, preallocated irqs: 0 [ 0.000000] riscv-intc: 32 local interrupts mapped [ 0.000000] clocksource: riscv_clocksource: mask: 0xffffffffffffffff max_cycles: 0x24e6a1710, max_idle_ns: 440795202120 ns [ 0.000000] sched_clock: 64 bits at 10MHz, resolution 100ns, wraps every 4398046511100ns [ 0.000090] Console: colour dummy device 80x25 [ 0.000110] Calibrating delay loop (skipped), value calculated using timer frequency.. 20.00 BogoMIPS (lpj=40000) [ 0.000130] pid_max: default: 32768 minimum: 301 [ 0.000165] Mount-cache hash table entries: 1024 (order: 0, 4096 bytes, linear) [ 0.000185] Mountpoint-cache hash table entries: 1024 (order: 0, 4096 bytes, linear) [ 0.016100] ASID allocator using 9 bits (512 entries) [ 0.020135] Memory: 249124K/258048K available (1482K kernel code, 476K rwdata, 178K rodata, 3849K init, 184K bss, 8688K reserved, 0K cma-reserved) [ 0.020220] devtmpfs: initialized [ 0.020630] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645041785100000 ns [ 0.020655] futex hash table entries: 256 (order: -1, 3072 bytes, linear) [ 0.021395] clocksource: Switched to clocksource riscv_clocksource [ 0.029425] workingset: timestamp_bits=30 max_order=16 bucket_order=0 [ 0.037245] riscv-plic: plic@c000000: mapped 31 interrupts with 1 handlers for 2 contexts. [ 0.070295] printk: legacy console [hvc0] enabled [ 0.070295] printk: legacy console [hvc0] enabled [ 0.070330] printk: legacy bootconsole [sbi0] disabled [ 0.070330] printk: legacy bootconsole [sbi0] disabled [ 0.077680] clk: Disabling unused clocks [ 0.211175] Freeing unused kernel image (initmem) memory: 3848K [ 0.211210] Kernel memory protection not selected by kernel config. [ 0.211240] Run /init as init process Saving 256 bits of non-creditable seed for next boot Starting syslogd: OK Starting klogd: OK Running sysctl: OK Starting network: ip: socket: Function not implemented ip: socket: Function not implemented FAIL Starting crond: OK crond (busybox 1.36.1) started, log level 8 Welcome to Buildroot buildroot login: root root login[60]: root login on 'console' # uname -a uname -a Linux buildroot 6.12.1 #1 Sun Dec 1 16:38:35 JST 2024 riscv32 GNU/Linux # cat /proc/cpuinfo cat /proc/cpuinfo processor : 0 hart : 0 isa : rv32ima_zicntr_zicsr_zifencei_zihpm mmu : sv32 mvendorid : 0x0 marchid : 0x5 mimpid : 0x0 hart isa : rv32ima_zicntr_zicsr_zifencei_zihpm
- Spike で Linux 動いた!
Spike を RV32 のプログラム向けにビルドする
Spike (riscv-isa-sim) は RISC-V の命令セットシミュレータで、(比較的) 手軽に RISC-V のプログラムを試す上でとても便利なツール。
Spike はデフォルトで RV64 のプログラム向けにビルドされるが、今回は Spike を RV32 のプログラム向けにビルドする。また、合わせて必要となる RISC-V のコンパイラ (riscv-gnu-toolchain) とプロキシカーネル (riscv-pk) をビルドし、Hello world プログラムを動かす。
RISC-V の開発環境は基本的に Linux だが、macOS や Windows の人向けに Docker を利用したビルド方法についても触れる。
- Spike RISC-V ISA Simulator - GitHub
- RISC-V GNU Compiler Toolchain - GitHub
- RISC-V Proxy Kernel and Boot Loader - GitHub
インストール
Spike と RISC-V GNU Compiler Toolchain、RISC-V Proxy Kernel をそれぞれインストールする。
基本的には GitHub のインストール方法に書かれていることと同じだが、configure のオプションが異なるので注意。
特に、Spike の configure で --with-target=riscv32-unknown-elf を指定し忘れると Spike が pk を見つけられなくなり、毎回 pk のフルパスを記述するか、pk のバイナリをカレントディレクトリにコピーしなければならなくなるので注意。
↓ 忘れるとこんな感じのエラーが出力される。
$ spike pk hello terminate called after throwing an instance of 'std::runtime_error' what(): could not open pk; searched paths: . (current directory) /opt/riscv/riscv64-unknown-elf/bin/ (based on configured --prefix and --with-target)
以下は Linux (Ubuntu) の人向けのインストール方法。WORK と RISCV のディレクトリはどこでもいいので適宜置き換える。
$ sudo apt update && sudo apt upgrade -y $ export WORK="$HOME/src" $ export RISCV="/opt/riscv" $ export PATH="$RISCV/bin:$PATH" $ mkdir -p $WORK # install riscv-gnu-toolchain $ sudo apt install -y autoconf automake autotools-dev curl python3 python3-pip libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev ninja-build git cmake libglib2.0-dev libslirp-dev $ cd $WORK $ git clone https://github.com/riscv/riscv-gnu-toolchain $ cd riscv-gnu-toolchain $ ./configure --prefix=$RISCV --with-arch=rv32gc --with-abi=ilp32 $ make -j $(nproc) # install riscv-pk $ cd $WORK $ git clone https://github.com/riscv-software-src/riscv-pk.git $ cd riscv-pk $ mkdir build $ cd build $ ../configure --prefix=$RISCV --host=riscv32-unknown-elf $ make -j $(nproc) $ sudo make install # install riscv-isa-sim (spike) $ sudo apt install -y device-tree-compiler $ cd $WORK $ git clone https://github.com/riscv-software-src/riscv-isa-sim.git $ cd riscv-isa-sim $ mkdir build $ cd build $ ../configure --prefix=$RISCV --with-target=riscv32-unknown-elf $ make -j $(nproc) $ sudo make install
macOS や Windows、Linux の人で Docker を使う場合の Dockerfile は以下の通り。
FROM ubuntu:latest ENV HOME="/root" ENV WORK="$HOME/src" ENV RISCV="/opt/riscv" ENV PATH="$RISCV/bin:$PATH" RUN apt-get update && apt-get install -y \ autoconf \ automake \ autotools-dev \ bc \ bison \ build-essential \ cmake \ curl \ device-tree-compiler \ flex \ gawk \ git \ gperf \ libexpat-dev \ libglib2.0-dev \ libgmp-dev \ libmpc-dev \ libmpfr-dev \ libtool \ ninja-build \ patchutils \ python3 \ python3-pip \ texinfo \ zlib1g-dev \ && rm -rf /var/lib/apt/lists/* # install riscv-gnu-toolchain WORKDIR $WORK RUN git clone https://github.com/riscv/riscv-gnu-toolchain WORKDIR $WORK/riscv-gnu-toolchain RUN ./configure --prefix=$RISCV --with-arch=rv32gc --with-abi=ilp32 RUN make -j $(nproc) RUN make distclean # install riscv-pk WORKDIR $WORK RUN git clone https://github.com/riscv-software-src/riscv-pk.git WORKDIR $WORK/riscv-pk/build RUN ../configure --prefix=$RISCV --host=riscv32-unknown-elf RUN make -j $(nproc) RUN make install RUN make distclean # install riscv-isa-sim (spike) WORKDIR $WORK RUN git clone https://github.com/riscv-software-src/riscv-isa-sim.git WORKDIR $WORK/riscv-isa-sim/build RUN ../configure --prefix=$RISCV --with-target=riscv32-unknown-elf RUN make -j $(nproc) RUN make install RUN make distclean WORKDIR $HOME
Docker の使い方
Docker のインストールは Install Docker Engine - Docker Docs から。あるいは macOS の人は homebrew (Docker - Homebrew Formulae) から。brew install --cask docker コマンドを実行して Docker をインストールする。
インストールが終わったら Docker Engine を起動しておく。
Dockerfile のあるディレクトリで以下のコマンドを実行すると Docker image が生成される。
$ docker build . -t spike_env
Docker image の生成が終了したら以下のコマンドで確認する。
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE spike_env latest 2fa01e5c77eb 4 hours ago 15.2GB ubuntu latest 35a88802559d 3 weeks ago 78MB
コンテナを動かして中に入るためには以下のコマンドを実行する。
$ docker run -it spike_env
コンテナの外に出るためにはプロンプトに exit とタイプしてから Enter を押すか、ctrl + d を押す。
Hello world
Spike のインストールが終わったので Hello, world プログラムを動かしてみる。
hello.c に Hello world プログラムを記述する。
// hello.c #include <stdio.h> int main() { printf("Hello, world!\n"); return 0; }
RISC-V GNU Compiler Toolchain で hello.c をコンパイルし、実行ファイルとして hello を出力する。
$ riscv32-unknown-elf-gcc -o hello hello.c
Spike で hello を実行する。
$ spike --isa=rv32gc pk hello Hello, world!
Spike で Hello world プログラムを動かすことができた。
ここで、pk は RISC-V Proxy Kernel のこと。Spike は Spike だけでは入出力関連のシステムコールを実行できないため、これらの処理を Proxy Kernel を通してホスト OS 側で行うようにしている。
Hello world の内側
RISC-V で Hello world プログラムを動かすことができたが、ここまでではホストの環境で Hello world を実行したときとの違いがわからない。
なので、ここからは RISC-V の Hello world プログラムの内側をのぞいてみる。
まずは Hello world プログラムのアセンブリを見てみる。
以下のコマンドを実行すると Hello world プログラムの RISC-V アセンブリが hello.dump に出力される。
$ riscv32-unknown-elf-objdump -d hello > hello.dump
hello.dump はこんな感じになっている。RISC-V の仕様書 (RISC-V ISA manual - GitHub) を見てもらうと、RISC-V の命令が並んでいるなぁと思ってもらえるかも。
hello: file format elf32-littleriscv Disassembly of section .text: 00010094 <exit>: 10094: 1141 addi sp,sp,-16 10096: 4581 li a1,0 10098: c422 sw s0,8(sp) 1009a: c606 sw ra,12(sp) 1009c: 842a mv s0,a0 1009e: 7dc000ef jal 1087a <__call_exitprocs> 100a2: d481a783 lw a5,-696(gp) # 13988 <__stdio_exit_handler> 100a6: c391 beqz a5,100aa <exit+0x16> 100a8: 9782 jalr a5 100aa: 8522 mv a0,s0 100ac: 19c020ef jal 12248 <_exit> 000100b0 <register_fini>: 100b0: 00000793 li a5,0 100b4: c791 beqz a5,100c0 <register_fini+0x10> 100b6: 6549 lui a0,0x12 100b8: 99450513 addi a0,a0,-1644 # 11994 <__libc_fini_array> 100bc: 08d0006f j 10948 <atexit> 100c0: 8082 ret 000100c2 <_start>: 100c2: 00004197 auipc gp,0x4 100c6: b7e18193 addi gp,gp,-1154 # 13c40 <__global_pointer$> 100ca: d4818513 addi a0,gp,-696 # 13988 <__stdio_exit_handler> 100ce: 07018613 addi a2,gp,112 # 13cb0 <__BSS_END__> 100d2: 8e09 sub a2,a2,a0 100d4: 4581 li a1,0 100d6: 2d61 jal 1076e <memset> 100d8: 00001517 auipc a0,0x1 100dc: 87050513 addi a0,a0,-1936 # 10948 <atexit> 100e0: c519 beqz a0,100ee <_start+0x2c> 100e2: 00002517 auipc a0,0x2 100e6: 8b250513 addi a0,a0,-1870 # 11994 <__libc_fini_array> 100ea: 05f000ef jal 10948 <atexit> 100ee: 2d19 jal 10704 <__libc_init_array> 100f0: 4502 lw a0,0(sp) 100f2: 004c addi a1,sp,4 100f4: 4601 li a2,0 100f6: 20b1 jal 10142 <main> 100f8: bf71 j 10094 <exit> ...
次に、Spike が Hello world プログラムを実行したときの命令列をながめてみる。
以下のコマンドを実行すると、Spike が実行した命令列や命令の実行結果のコミット履歴などが hello.log に出力される。
$ spike --isa=rv32gc -l --log-commits --log=hello.log pk hello
hello.log はこんな感じになっている。全部で 515,083 行。スタートアップルーチンやら pk の処理などでやたら複雑になっていそう。
core 0: 0x00001000 (0x00000297) auipc t0, 0x0 core 0: 3 0x00001000 (0x00000297) x5 0x00001000 core 0: 0x00001004 (0x02028593) addi a1, t0, 32 core 0: 3 0x00001004 (0x02028593) x11 0x00001020 core 0: 0x00001008 (0xf1402573) csrr a0, mhartid core 0: 3 0x00001008 (0xf1402573) x10 0x00000000 core 0: 0x0000100c (0x0182a283) lw t0, 24(t0) core 0: 3 0x0000100c (0x0182a283) x5 0x80000000 mem 0x00001018 core 0: 0x00001010 (0x00028067) jr t0 core 0: 3 0x00001010 (0x00028067) core 0: 0x80000000 (0x1f80006f) j pc + 0x1f8 core 0: 3 0x80000000 (0x1f80006f) core 0: 0x800001f8 (0x00000093) li ra, 0 core 0: 3 0x800001f8 (0x00000093) x1 0x00000000 core 0: 0x800001fc (0x00000113) li sp, 0 core 0: 3 0x800001fc (0x00000113) x2 0x00000000 core 0: 0x80000200 (0x00000193) li gp, 0 core 0: 3 0x80000200 (0x00000193) x3 0x00000000 core 0: 0x80000204 (0x00000213) li tp, 0 core 0: 3 0x80000204 (0x00000213) x4 0x00000000 core 0: 0x80000208 (0x00000293) li t0, 0 core 0: 3 0x80000208 (0x00000293) x5 0x00000000 core 0: 0x8000020c (0x00000313) li t1, 0 ...
ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE.
pip をアップデートしようとしたらハッシュが合わなくて怒られた。
$ pip install -U pip
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Requirement already satisfied: pip in /usr/lib/python3/dist-packages (20.3.4)
Collecting pip
WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProtocolError('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))': /simple/pip/pip-24.0-py3-none-any.whl
Downloading https://www.piwheels.org/simple/pip/pip-24.0-py3-none-any.whl (2.1 MB)
|█████████████████████████▋ | 1.7 MB 3.4 kB/s eta 0:02:03
ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE. If you have updated the package versions, please update the hashes. Otherwise, examine the package contents carefully; someone may have tampered with them.
pip from https://www.piwheels.org/simple/pip/pip-24.0-py3-none-any.whl#sha256=da6b1e839e2fe6c03034a8987bc271d271cf32328e67126349d2eda92aabf3c3:
Expected sha256 da6b1e839e2fe6c03034a8987bc271d271cf32328e67126349d2eda92aabf3c3
Got 9397efdf4b412e8108527dfb1cbab0d40bf462a9e581abc7a5c62cf29affb3b8
$ pip cache purge や $ pip install --no-cache-dir -U pip などでキャッシュを消すと上手くいく、などと書かれている記事もあるが上手くいかない。
どうやら、エラー文に書かれている whl ファイルを wget や curl などで取得し、$ pip install <whl_file> とすると上手くいくらしい。
$ wget https://www.piwheels.org/simple/pip/pip-24.0-py3-none-any.whl --2024-04-20 23:19:49-- https://www.piwheels.org/simple/pip/pip-24.0-py3-none-any.whl Resolving www.piwheels.org (www.piwheels.org)... 2a00:1098:0:80:1000:3b:1:1, 2a00:1098:0:82:1000:3b:1:1, 46.235.225.189, ... Connecting to www.piwheels.org (www.piwheels.org)|2a00:1098:0:80:1000:3b:1:1|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 2110226 (2.0M) Saving to: ‘pip-24.0-py3-none-any.whl’ pip-24.0-py3-none-any.whl 74%[===================================================================================> ] 1.50M 5.87KB/s in 4m 21s 2024-04-20 23:24:17 (5.90 KB/s) - Read error at byte 1572864/2110226 (Error decoding the received TLS packet.). Retrying. --2024-04-20 23:24:18-- (try: 2) https://www.piwheels.org/simple/pip/pip-24.0-py3-none-any.whl Connecting to www.piwheels.org (www.piwheels.org)|2a00:1098:0:80:1000:3b:1:1|:443... connected. HTTP request sent, awaiting response... 206 Partial Content Length: 2110226 (2.0M), 537362 (525K) remaining Saving to: ‘pip-24.0-py3-none-any.whl’ pip-24.0-py3-none-any.whl 100%[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++============================>] 2.01M 5.17KB/s in 95s 2024-04-20 23:25:58 (5.50 KB/s) - ‘pip-24.0-py3-none-any.whl’ saved [2110226/2110226] $ pip install pip-24.0-py3-none-any.whl Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple Processing ./pip-24.0-py3-none-any.whl Installing collected packages: pip Successfully installed pip-24.0
Verilator で波形ファイルを出力する
Verilator は最速の Verilog シミュレータかつ lint ツール。オープンソースのツールであり、機能が優れているため、様々なプロジェクトで利用されている (Welcome to Verilator - Veripool)。
Verilator の大きな特徴として、Verilog を C++ にコンパイルしてからシミュレーションすることが挙げられる。また、設計した回路のテストベンチを C++ で記述できるため、Verilog だけでは困難なシミュレーションを行えるという特徴もある。
Verilator で波形ファイルを出力する方法には以下の2通りの方法がある。
1つ目の方法は他の Verilog シミュレータを利用したことがある人にとっては親しみやすい。2つ目の方法は C++ のみでテストベンチを記述するときに向いている。これらの2通りの方法について、サンプルプロジェクトを例にそれぞれ説明する。
今回は C++ のテストベンチを利用する場合について紹介する。Verilog のファイルだけを使ってシミュレーションする場合は、verilator --binary --trace <verilog_file> などとし、後述する $dumpfile や $dumpvars を追加すれば波形ファイルを出力できる。
一次資料を読みたい方は以下がおすすめ。
- How do I generate waveforms (traces) in C++? - Verilator User's Guide
- verilator/examples/make_tracing_c - GitHub
動作環境
OS release : Ubuntu 22.04.3 LTS Verilator version : Verilator 5.019 devel rev v5.018-75-g39d9bd4d4
サンプルプロジェクト
サンプルプロジェクトのディレクトリ構成は以下のようになっている。
. ├── Makefile ├── sim_main.cpp ├── top.v └── counter.v
sim_main.cpp が Verilator 用のテストベンチを記述した C++ ファイル、top.v が Verilog のシミュレーション用の記述をした Verilog ファイル、counter.v がカウンタ回路を記述した Verilog ファイル。Makefile はソースコードをビルドして動かすために利用する。それぞれのファイルの内容を以下に示す。
Makefile
#=========================================================== # Sources #----------------------------------------------------------- SRCS += $(wildcard *.v) CXX_SRCS += $(wildcard *.cpp) #=========================================================== # Verilator #----------------------------------------------------------- VERILATOR ?= verilator VL_TOPNAME := Vtop VERILATOR_FLAGS += --cc --exe --build VERILATOR_FLAGS += --prefix $(VL_TOPNAME) VERILATOR_INPUT += $(CXX_SRCS) $(SRCS) #=========================================================== # Build rules #----------------------------------------------------------- .PHONY: default build run clean default: build run build: $(VERILATOR) $(VERILATOR_FLAGS) $(VERILATOR_INPUT) run: obj_dir/$(VL_TOPNAME) clean: rm -rf obj_dir rm -f *.vcd
sim_main.cpp
#include <verilated.h> #include "Vtop.h" int main (int argc, char **argv) { VerilatedContext *contextp = new VerilatedContext; contextp->commandArgs(argc, argv); Vtop *top = new Vtop{contextp}; top->clk_i = 0; top->rst_ni = 0; while (!contextp->gotFinish()) { contextp->timeInc(1); top->clk_i = !top->clk_i; if (!top->clk_i) { if (contextp->time()>5) { top->rst_ni = 1; } } top->eval(); } top->final(); delete top; return 0; }
top.v
module top ( input wire clk_i , input wire rst_ni ); wire [31:0] cntr; counter counter0 ( .clk_i (clk_i ), .rst_ni(rst_ni), .cntr_o(cntr ) ); always @(negedge clk_i) begin $write("%10d\n", cntr); if (cntr==20) begin $finish; end end endmodule
counter.v
module counter ( input wire clk_i , input wire rst_ni, output wire [31:0] cntr_o ); reg [31:0] cntr; assign cntr_o = cntr; always @(posedge clk_i) begin if (!rst_ni) begin cntr <= 0; end else begin cntr <= cntr+1; end end endmodule
1. Verilog ファイルに $dumpfile や $dumpvars を追加して波形ファイルを出力する方法
まずは1つ目の方法について説明する。変更を加えるファイルは sim_main.cpp、top.v、Makefile の3つ。
sim_main.cpp に波形の出力を有効化するための処理を追加する。以下のソースコードにおいて、コメントで <- Added と記述した行が新たに追加したコード。
// sim_main.cpp #include <verilated.h> #include "Vtop.h" int main (int argc, char **argv) { VerilatedContext *contextp = new VerilatedContext; contextp->commandArgs(argc, argv); contextp->traceEverOn(true); // <- Added
top.v に $dumpfile や $dumpvars を追加する。$dumpfile は波形ファイルの出力先を指定するものであり、$dumpvars は波形の出力を行うモジュールの階層を指定するもの。
// top.v module top ( input wire clk_i , input wire rst_ni ); initial begin // <- Added $dumpfile("dump.vcd"); // <- Added $dumpvars(0); // <- Added end // <- Added
Makefile に波形ファイルを出力するための Verilator オプション --trace を追加する。--trace は VCD (Value Change Dump) 形式の波形ファイルを出力するために使用される。
# Makefile VERILATOR_FLAGS += --cc --exe --build VERILATOR_FLAGS += --prefix $(VL_TOPNAME) VERILATOR_FLAGS += --trace # <- Added VERILATOR_INPUT += $(CXX_SRCS) $(SRCS)
あとは make を実行すればシミュレーションが行われ、波形ファイル dump.vcd が出力される。波形を見るには gtkwave dump.vcd を実行すればよい。

2. C++ ファイルから直接波形を出力する方法
次に、2つ目の方法について説明する。変更を加えるファイルは sim_main.cpp、Makefile の2つ。
sim_main.cpp に波形ファイルを出力するために必要なヘッダーファイル、波形の出力を有効化するための処理、波形ファイルのオープン、書き込み、クローズの処理を追加する。
// sim_main.cpp #include <verilated.h> #include <verilated_vcd_c.h> // <- Added #include "Vtop.h" int main (int argc, char **argv) { VerilatedContext *contextp = new VerilatedContext; contextp->commandArgs(argc, argv); contextp->traceEverOn(true); // <- Added Vtop *top = new Vtop{contextp}; top->clk_i = 0; top->rst_ni = 0; VerilatedVcdC *tfp = new VerilatedVcdC; // <- Added top->trace(tfp, 0); // <- Added tfp->open("dump.vcd"); // <- Added while (!contextp->gotFinish()) { contextp->timeInc(1); top->clk_i = !top->clk_i; if (!top->clk_i) { if (contextp->time()>5) { top->rst_ni = 1; } } top->eval(); tfp->dump(contextp->time()); // <- Added } tfp->close(); // <- Added top->final(); delete tfp; // <- Added delete top; return 0; }
Makefile に波形ファイルを出力するための Verilator オプション --trace を追加する。
# Makefile VERILATOR_FLAGS += --cc --exe --build VERILATOR_FLAGS += --prefix $(VL_TOPNAME) VERILATOR_FLAGS += --trace # <- Added VERILATOR_INPUT += $(CXX_SRCS) $(SRCS)
あとは1つ目の方法と同様に make を実行すればシミュレーションが行われ、波形ファイル dump.vcd が出力される。波形を見るには gtkwave dump.vcd を実行すればよい。