lynxeyedの電音鍵盤

MBDとFPGAと車載で使うデバイスの備忘録

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 sdkVerilog生成、テストコードの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

アセンブル

CPUデバッグ時にはアセンブラでみた方が良いと思います。

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生成時は含めて生成する

後者の方がスマートですね。あとはやる気の問題かな。