C言語で自作RISC-Vを動作させる
前回の記事でアセンブラでLチカ、Hello worldはできました。
今回はC言語で試してみました。
実用性を求めるとC記述でも動作することが重要ですが、なによりもハーバードアーキテクチャの場合、Load/Store動作のデバッグになると思います。関数にジャンプする時、main関数にreturnする時、容赦無くLoad/Storeの連発をするからです。
2,3個Load/store命令が連続しているようなコードでは動作(実際はしていないのだけれどもタイミングの功罪で動いているように見える)していても、数十回連続すると動作しない事態が起こり得ます。
Cでは容赦無くLoad/Store行うので、良い試験になります。
GCCの準備
いままでアセンブラのみ使っていたためまともな環境を構築していませんでした。
※ビルドに必要なパッケージ(automake autotools-dev curlなどなど)はすでに導入済みとします。
git clone https://github.com/riscv/riscv-gnu-toolchain cd riscv-gnu-toolchain ./configure --prefix=/opt/riscv --with-arch=rv32i sudo make
ここで、/opt/の権限によりますがsudo makeしないと不可解なエラーになります。permission deniedって出ればわかりやすいのですが。
.bashrcなどでPATHを通したら完成です。
$ riscv32-unknown-elf-gcc -v Using built-in specs. COLLECT_GCC=riscv32-unknown-elf-gcc COLLECT_LTO_WRAPPER=/opt/riscv/libexec/gcc/riscv32-unknown-elf/10.2.0/lto-wrapper Target: riscv32-unknown-elf Configured with: /mnt/c/riscv-gnu-toolchain/riscv-gcc/configure --target=riscv32-unknown-elf --prefix=/opt/riscv --disable-shared --disable-threads --enable-languages=c,c++ --with-system-zlib --enable-tls --with-newlib --with-sysroot=/opt/riscv/riscv32-unknown-elf --with-native-system-header-dir=/include --disable-libmudflap --disable-libssp --disable-libquadmath --disable-libgomp --disable-nls --disable-tm-clone-registry --src=.././riscv-gcc --disable-multilib --with-abi=ilp32 --with-arch=rv32i --with-tune=rocket 'CFLAGS_FOR_TARGET=-Os -mcmodel=medlow' 'CXXFLAGS_FOR_TARGET=-Os -mcmodel=medlow' Thread model: single Supported LTO compression algorithms: zlib gcc version 10.2.0 (GCC)
リンカ・スタートアップの準備
参考サイト。みつきん(id:mickey_happygolucky)さんのブログです。
mickey-happygolucky.hatenablog.com
ほぼそのままですが、.text領域と.bss領域がことなりますので、それを書き換え、stackの最終アドレスが0x7fff(ただし4-byte-alignment)になるようにスタートアップコードをなおしました。良記事でした。ありがとうございます。
linker.ld
OUTPUT_ARCH( "riscv" ) ENTRY(_start) SECTIONS { . = 0x00000000; .text.init : { *(.text.init) } .tohost : { *(.tohost) } .text : { *(.text) } .data : { *(.data) } .bss : { *(.bss) } _end = .; }
---------ここから:2021年1月18日加筆修正---------
スタートアップコードは少し修正しています。今回作成したRV32IはPrivillaged ISAの規格通り、Machine-mode timer(mtime)を64bitで実装しています。
下位32bitをget_timel()、上位32bitをget_timeh()で取得できるようにしました。
utils.S
.section .text.init; .global _start _start: lui sp, 0x08 call main j . .global dummy dummy: ret .global put32 put32: sw x11,(x10) ret .global get32 get32: lw x10,(x10) ret .global get_timel get_timel: csrr x10, time ret .global get_timeh get_timeh: csrr x10, timeh ret
Cを記述する
簡単なLチカコードです。こちらもみつきんさんのコードとあまり変わりませんが、タイマーmtimeを使用しています。
コードを記述する前にKyogenRVの現時点で最新のタグが付されているバージョンをcloneします。
git clone http://github.com/panda5mt/KyogenRV -b 0.2.6 --depth 1 make sdk
make sdk
でVerilog生成、テストコードのhexファイル生成、今回のテスト用Cコードコンパイルを一括で行います。
Scala/chisel環境のない方は実行しない方がいいと思います。すでにコンパイル済みのhexファイルが消えるので。
後述しますが、今回のCプロジェクトをコンパイルするだけの場合はmake c_all
を実行します。
KyogenRVプロジェクトルート/src/sw/main.c
を記述します。
github.com
void put32(unsigned int, unsigned int); unsigned int get32(unsigned int); unsigned int get_timel(void); unsigned int get_timeh(void); void dummy (void); #define GPIO_BASE 0x8000 #define XTAL_FREQ_KHZ 60000 // wait msec counter void wait_ms(unsigned int msec){ volatile unsigned int oldtime; oldtime = get_timel(); while((get_timel()-oldtime) < XTAL_FREQ_KHZ * msec); //1msec } // main function int main(int argc, char *argv[]) { while (1) { wait_ms(500); put32(GPIO_BASE, 0x55); wait_ms(500); put32(GPIO_BASE, 0xAA); } return 0; }
標準IOもなにもインクルードしていない状態です。これで動作します。
プロジェクトのルートに戻り、下記を実行します
make c_all
KyogenRVプロジェクトルート/src/sw/blinker.hex
が生成されたhexです。
Quartus Primeで使用するインテルhexを生成する場合は
プロジェクトのルートに戻り、下記を実行します
./mk_intel_hex.py
KyogenRVプロジェクトルート/fpga/chisel_generated/blinker_intel.hex
が生成されたインテルhexです。
前回のようにPlatformDesignerでon-chip ramの初期値に設定し、コンパイルすることでFPGA上で動作します。
動作風景
youtu.be
逆アセンブル
make c_reverse
で逆アセンブルできます。
どのようなコードになっているか見ることができます。
セクション .text.init の逆アセンブル: 00000000 <_start>: 0: 00008137 lui sp,0x8 4: 0a8000ef jal ra,ac <main> 8: 0000006f j 8 <_start+0x8> 0000000c <dummy>: c: 00008067 ret 00000010 <put32>: 10: 00b52023 sw a1,0(a0) 14: 00008067 ret 00000018 <get32>: 18: 00052503 lw a0,0(a0) 1c: 00008067 ret 00000020 <get_timel>: 20: c0102573 rdtime a0 24: 00008067 ret 00000028 <get_timeh>: 28: c8102573 rdtimeh a0 2c: 00008067 ret セクション .text の逆アセンブル: 00000030 <wait_ms>: 30: fd010113 addi sp,sp,-48 # 7fd0 <_end+0x7ee0> 34: 02112623 sw ra,44(sp) 38: 02812423 sw s0,40(sp) 3c: 03010413 addi s0,sp,48 40: fca42e23 sw a0,-36(s0) 44: fddff0ef jal ra,20 <get_timel> 48: 00050793 mv a5,a0 4c: fef42623 sw a5,-20(s0) 50: 00000013 nop 54: fcdff0ef jal ra,20 <get_timel> 58: 00050713 mv a4,a0 5c: fec42783 lw a5,-20(s0) 60: 40f706b3 sub a3,a4,a5 64: fdc42703 lw a4,-36(s0) 68: 00070793 mv a5,a4 6c: 00579793 slli a5,a5,0x5 70: 40e787b3 sub a5,a5,a4 74: 00279793 slli a5,a5,0x2 78: 00e787b3 add a5,a5,a4 7c: 00479713 slli a4,a5,0x4 80: 40f70733 sub a4,a4,a5 84: 00571793 slli a5,a4,0x5 88: 00078713 mv a4,a5 8c: 00070793 mv a5,a4 90: fcf6e2e3 bltu a3,a5,54 <wait_ms+0x24> 94: 00000013 nop 98: 00000013 nop 9c: 02c12083 lw ra,44(sp) a0: 02812403 lw s0,40(sp) a4: 03010113 addi sp,sp,48 a8: 00008067 ret 000000ac <main>: ac: fe010113 addi sp,sp,-32 b0: 00112e23 sw ra,28(sp) b4: 00812c23 sw s0,24(sp) b8: 02010413 addi s0,sp,32 bc: fea42623 sw a0,-20(s0) c0: feb42423 sw a1,-24(s0) c4: 1f400513 li a0,500 c8: f69ff0ef jal ra,30 <wait_ms> cc: 05500593 li a1,85 d0: 00008537 lui a0,0x8 d4: f3dff0ef jal ra,10 <put32> d8: 1f400513 li a0,500 dc: f55ff0ef jal ra,30 <wait_ms> e0: 0aa00593 li a1,170 e4: 00008537 lui a0,0x8 e8: f29ff0ef jal ra,10 <put32> ec: fd9ff06f j c4 <main+0x18>
---------ここまで:2021年1月18日加筆修正---------
この分量であれば、アセンブラでも追いかけられるレベルです。
SignalTap片手に波形を追いかけながらCPUデバッグをしました。
問題点:riscv-testsがChiselシミュレーション上で失敗する
今回の修正でchisel-iotester上ではriscv-testsが失敗するようになってしまいました。
もちろんCyclone10LP FPGA上では正しく動作します。
理由(だけ)は簡単です。メモリやメモリマップ上のAvalon-MMスレーブの挙動をChiselで完全にはシミュレートできていないからです。
スレーブデバイスのwaitrequest含めたAvalon-MMの挙動をChiselで完全シミュレートする価値があるかというと、目的と手段の入れ替わりが生じているように思えます。今のところその意義を感じていません。そんなことするのであれば一旦Verilogを生成してQuartusでBFMテストに流すべきと思います。
対処としては、下記のいずれか選択をすると思われます
- riscv-testsのChisel上でのシミュレーションは諦める(エラーのまま放置)。
- シミュレーションはパスできるようにする:AvalonMMに対応したバスブリッジを別ファイルで用意し、シミュレーション時は当該ファイルを除外、HDL生成時は含めて生成する
後者の方がスマートですね。あとはやる気の問題かな。