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

$ 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

$ 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

$ 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.cpiorootfs.cpio.gz が生成される。
  • 末尾に .gz がついているのが gzip で圧縮されたもの。今回はこちらを使用する。

Linux

$ 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 ファイルであり、Imagevmlinuxobjcopy -O binary vmlinux したバイナリファイル。
  • OpenSBI には Image を payload として渡す。
  • Linuxアセンブリが見たい場合は riscv32-unknown-linux-gnu-objdump -d vmlinux > vmlinux.dump などとしてディスアセンブリするといい。

OpenSBI

$ 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 だが、macOSWindows の人向けに Docker を利用したビルド方法についても触れる。



インストール

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

macOSWindowsLinux の人で 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.cHello 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 ファイルを wgetcurl などで取得し、$ 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 の大きな特徴として、VerilogC++コンパイルしてからシミュレーションすることが挙げられる。また、設計した回路のテストベンチを C++ で記述できるため、Verilog だけでは困難なシミュレーションを行えるという特徴もある。

Verilator で波形ファイルを出力する方法には以下の2通りの方法がある。

  1. Verilog ファイルに $dumpfile$dumpvars を追加し、iverilog などと同様な形式で波形ファイルを出力する方法
  2. C++ ファイルから直接波形ファイルを出力する方法

1つ目の方法は他の Verilog シミュレータを利用したことがある人にとっては親しみやすい。2つ目の方法は C++ のみでテストベンチを記述するときに向いている。これらの2通りの方法について、サンプルプロジェクトを例にそれぞれ説明する。

今回は C++ のテストベンチを利用する場合について紹介する。Verilog のファイルだけを使ってシミュレーションする場合は、verilator --binary --trace <verilog_file> などとし、後述する $dumpfile$dumpvars を追加すれば波形ファイルを出力できる。


一次資料を読みたい方は以下がおすすめ。

動作環境

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.vVerilog のシミュレーション用の記述をした 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.cpptop.vMakefile の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.cppMakefile の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 を実行すればよい。

参考文献