4連休の課題としてFPGAで簡単なCPUを作っているので、その進捗を記録しておきます。
RISC-V (RV32I) の作成
とりあえず今回は確実に動くCPUを作ることを目標にしました。
パイプラインなどは実装せず、フェッチ→デコード→実行→メモリ・アクセス→書き戻しの5段階にそれぞれ1クロック使って、1命令に5クロックかける設計になっています。
命令セットにはRISC-Vの一番基本的な構成であるRV32Iを採用しましたが、簡単のため特権命令や割り込み周りは省略しました。
ハードウェアには以前使ったDigilentのBasys 3を使って、Vivadoで開発しました。
kivantium.hateblo.jp
あまり工夫したところはないので実装の詳細は説明しません。ソースコードはここに置いてあります。
github.com
RISC-V向けのGCCはriscv-gnu-toolchain
というリポジトリで公開されています。以前はriscv-tools
というリポジトリ以下で公開されていたものが移動したらしいので、古い記事を参考にするときは気をつけてください。
github.com
READMEに従ってインストールするだけですが、configureで32bit用に設定する必要があります。ソースコードだけで10GB近くあるのでディスク容量に注意してください。
sudo apt-get install autoconf automake autotools-dev curl python3 libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev
git clone --recursive https://github.com/riscv/riscv-gnu-toolchain
./configure --prefix=/opt/riscv32 --with-arch=rv32im --with-abi=ilp32d
make linux
/opt/riscv32
以下にインストールされるのでPATHを通しておきます。
export PATH=/opt/riscv32/bin:$PATH
バイナリの作成
簡単な例として、フィボナッチ数列の第10番目の項を再帰で求めるプログラムを実行することにします。test.c
を以下の通り作成します。main関数を抜けないようにするために最後に無限ループを実行しています。
int fib(int n) {
if(n <= 1) return 1;
return fib(n-1) + fib(n-2);
}
int main() {
fib(10);
for(;;) {}
return 0;
}
これを自作CPUで動くようにコンパイルします。
普通にコンパイルしてしまうと未実装の命令を使った初期化ルーチンが走ってしまうので、まずはそれを無効にします。start.S
を以下の通り作成します。
.section .text.init;
.globl _start
_start:
call main
何もせずにmainを呼び出すアセンブリになっています。
次に、命令を0番地から実行するように指定します。link.ld
を以下の通り作成します。
OUTPUT_ARCH( "riscv" )
ENTRY(_start)
SECTIONS
{
. = 0x00000000;
.text.init : { *(.text.init) }
.tohost : { *(.tohost) }
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
_end = .;
}
以下のようにしてバイナリを生成します。
riscv32-unknown-elf-gcc -march=rv32i -c -o start.o start.S
riscv32-unknown-elf-gcc -march=rv32i -c -o test.o test.c
riscv32-unknown-elf-ld test.o start.o -lc -L/opt/riscv32/riscv32-unknown-elf/lib/ -Tlink.ld -nostartfiles -static -o test.elf
riscv32-unknown-elf-objcopy -O binary test.elf test.bin
hexdump -v -e '1/4 "%08x" "\n"' test.bin > test.hex
最後に出来上がるtest.hex
には、CPUが実行する命令列が16進数で書かれています。
074000ef
fe010113
00112e23
(中略)
00a00513
f7dff0ef
0000006f
ELF形式のファイルに対してobjdump
を実行すると逆アセンブルした結果を見ることができます。
$ riscv32-unknown-elf-objdump -d test.elf
test.elf: ファイル形式 elf32-littleriscv
セクション .text.init の逆アセンブル:
00000000 <_start>:
0: 074000ef jal ra,74 <main>
セクション .text の逆アセンブル:
00000004 <fib>:
4: fe010113 addi sp,sp,-32
8: 00112e23 sw ra,28(sp)
c: 00812c23 sw s0,24(sp)
10: 00912a23 sw s1,20(sp)
14: 02010413 addi s0,sp,32
18: fea42623 sw a0,-20(s0)
1c: fec42703 lw a4,-20(s0)
20: 00100793 li a5,1
24: 00e7c663 blt a5,a4,30 <fib+0x2c>
28: 00100793 li a5,1
2c: 0300006f j 5c <fib+0x58>
30: fec42783 lw a5,-20(s0)
34: fff78793 addi a5,a5,-1
38: 00078513 mv a0,a5
3c: fc9ff0ef jal ra,4 <fib>
40: 00050493 mv s1,a0
44: fec42783 lw a5,-20(s0)
48: ffe78793 addi a5,a5,-2
4c: 00078513 mv a0,a5
50: fb5ff0ef jal ra,4 <fib>
54: 00050793 mv a5,a0
58: 00f487b3 add a5,s1,a5
5c: 00078513 mv a0,a5
60: 01c12083 lw ra,28(sp)
64: 01812403 lw s0,24(sp)
68: 01412483 lw s1,20(sp)
6c: 02010113 addi sp,sp,32
70: 00008067 ret
00000074 <main>:
74: ff010113 addi sp,sp,-16
78: 00112623 sw ra,12(sp)
7c: 00812423 sw s0,8(sp)
80: 01010413 addi s0,sp,16
84: 00b00513 li a0,11
88: f7dff0ef jal ra,4 <fib>
8c: 0000006f j 8c <main+0x18>
C言語で書いた通り、再帰でフィボナッチ数を求めた後、`8c'を無限ループするプログラムになっていることが確認できます。
生成した16進数の命令列はSystemVerilogの$readmemh
を利用して命令メモリに埋め込んでいます(ソースコード)。コードを変更するたびに論理合成をやり直す必要がありますが、命令列を外部から読み込ませるのは面倒なのでこうしました。
実機での動作
関数の引数と戻り値が入るa0
レジスタの値を10進数で7セグLEDに表示する回路を組みました。しばらく計算した後、最終的な関数の返り値である89が表示されます。(動作を見やすくするためにクロックを10万分周しています)
今後の課題
xv6が去年からRISC-Vに対応したそうなので、xv6が動くようなCPUが作れると良いです。
OSを動かすためには割り込みなどの特権命令やスーパーバイザーモードでの仮想アドレスを実装しないといけないみたいので道のりは険しそうですが……
参考文献
ツールチェインの使い方が特に参考になりました。
github.com
上のFPGAマガジンで実装されているRISC-Vです(本誌からリンクされてなかった……)
1命令を5クロックで実行する設計はここから持ってきました(このコードではデコーダーやALUが順序回路になっていますが、自分の実装では組み合わせ回路になっています。2クロック無駄になっていますが、簡単のためです……)
mindchasers.com
ツールチェインのコンパイルオプションはここを参考にしました。