lynxeyedの電音鍵盤

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

Chiselを使ったRISC-Vの勉強(13.FPGAへの実装1)

メモリを同期読み込み書き込みにする

製作してきたChiselプロジェクトをFPGAに実装してみました。
Chiselを使ったRISC-Vの勉強(1)から目標にしていたようにAvalon-MM Masterとして動作させようとすると、

  • 命令フェッチ時にデータ取得が1クロック遅れる
  • ロード動作時に1クロック遅れる

(= どちらもメモリ読み込み動作時にのみ1クロック遅れる)
という現象が生じていました。これはChiselで命令メモリ/データメモリをMem(同期書き込み・非同期読み込みメモリ)として実装していたからです。
命令メモリ、データメモリ共にSyncReadMemに置き換えました。
github.com

git clone http://github.com/panda5mt/KyogenRV -b 0.1.1 --depth 1 

超ミラクルスーパーウルトラ大規模改修工事の恐れがありましたが、パイプライン動作そのものを大きく変更するような変更がなかったため比較的小規模な改修ですみました。riscv-testsも変わらずパスしています。

この修正に伴い、追加で修正した分

  • メモリR/W動作をハンドシェイクからレイテンシ動作に変更

Avalon-MMでは指定されたレイテンシでメモリリードライトを行います。もしスレーブが期待されたレイテンシで応答できない場合、スレーブ側はホストへwaitrequest信号を発行します。
ですので、Chisel側も準じた実装に変更しました。レイテンシに対応したステージでR/W動作を行い、waitrequest信号がきたらストールするだけです。(かえってハンドシェイク方式より単純になりました)

FPGAへの実装テスト

ここでQuartus Primeの使い方を紹介するつもりはないので少し駆け足で説明します。
ハーバードアーキテクチャCPUソフトコアをAvalon-MM Masterとする場合、命令メモリとデータメモリの2つのMasterを持つと思います。
この二つは独立ではなく、Back Pressureに支配されて連携動作をします。(今回はキャッシュ非搭載のため、命令/データは都度フェッチします)

  1. ChiselプロジェクトからVerilogコードを生成させる。
  2. PlatformDesignerに読み込ませるためのトッププロジェクトを作り(今回はSystemVerilog)、上記コードをインスタンスする。
  3. 新規プロジェクトを作り、PDで上記CPU + On-chip Memory + IOを使ったプロジェクトを作る
  4. hexエディタで命令を書く
  5. SigmalTapで実行中のCPUの動作を確認

項目2.の信号はavalon-MM Master信号と下記のように対応させました。


f:id:Lynx-EyED:20200905134337p:plain
Avalon-MM Master 1チャネルに対し最低限必要なのは下記項目です。byteenebleはRISC-Vの仕様上必須です。

  • clock
  • reset
  • byteenable
  • waitrequest
  • address
  • read
  • readdata
  • write
  • writedata

PlatformDesignerに登録が終わったら、項目3に進んでいきます。
それぞれの接続は以下のようになりました。onchip_memoryは0x0000番地から、I/Oはデータメモリ開始番地0x8000に配置します。


f:id:Lynx-EyED:20200905134637p:plain

項目4.
以下は疑似コードです。これをアセンブラで記述します。今回書くコードの目的は命令/データメモリが正しく動作しているかの確認です。

uint32_t data;

*(*PIO_BASE_ADDRESS) = 0xAA; // PIO = 0xAA
data = *(*PIO_BASE_ADDRESS);
data = data + 1;
*(*PIO_BASE_ADDRESS) = data; // PIO = 0xAB

上記をアセンブラで記述します。

	lui 	x1, 0x08 		# x1=0x8000
	li	x2, 0xAA		# x2 = 0xAA
	sw	x2, 0(x1)		# dmem[0x8000] = 0xAA
	lw	x3, 0(x1)		# x3 = dmem[0x8000]
	addi x3, x3, 1		# x3 = x3 + 1
	sw	x3, 0(x1)		# dmem[0x8000] = x3
_loop:
	jal x0, _loop		# loop

と書いてもいいのですが、これだとデータメモリに格納される前にデータフォワーディングでRWされるデータがよしなに取り扱われてしまうので、適宜パイプライン段数以上のnopを入れてやります。そうすることで本当にデータメモリの読み書きが機能しているかを確認することができます。

	lui 	x1, 0x08 		# x1=0x8000
	li	x2, 0xAA		# x2 = 0xAA
	sw	x2, 0(x1)		# dmem[0x8000] = 0xAA
	nop
	nop
	nop
	nop
	nop
	lw	x3, 0(x1)		# x3 = dmem[0x8000]
	nop
	nop
	nop
	nop
	nop
	addi x3, x3, 1		# x3 = x3 + 1
	nop
	nop
	nop
	nop
	nop
	sw	x3, 0(x1)		# dmem[0x8000] = x3
_loop:
	jal x0, _loop		# loop

みたいな感じです。アセンブルして、得られた機械語をQuartusのHexエディタで記録保存、Onchip Memoryの初期値データとします。
SignalTapで動作状況を見てみました。


f:id:Lynx-EyED:20200905134729p:plain
ストア命令の時は0x8000番地、書き込みデータは0xAA、writeシグナルを発行しています。
ロード命令も動作しています。waitrequestにより1クロック待たされていますが、動作しています。

次回は

コードを手作業で入力するのが大変なので、、
インテルhex/アルテラmifファイルの生成スクリプトを書いて、実機でriscv-testsを動作できるようにしていきます。

Chiselを使ったRISC-Vの勉強(12. riscv-testsの全項目クリア)

riscv-testsクリア

riscv-testsのリグレッションテストを全てクリアしました。(fence.i命令は除外しました。RISC-Vの基本Iアーキテクチャでは必須ではなくなったからです。)


f:id:Lynx-EyED:20200725162528p:plain
クリアしたからと言って「バグがない」とは言えないので、ランダム実行テストをする必要があり現在テスト中です。それは追々記事に。
テストがfailしたもののほとんどはCSRに起因し、CSRレジスタフォワーディングの実装の間違いに起因するものでした。
これはトレース結果を見ればすぐにわかるので修正→全テストのやり直しを繰り返せばクリアできるものが仕上がっていきます。

ここでは覚書としてクリアするのに1日以上かかった項目を挙げていきます。

CSRの実装 ≒ 例外の実装

intel FPGAに実装する際、カスタム命令を実行するコプロセッサを接続する予定なのでCSRを結局外してしまうのですが、ISAに定められている例外を正しく実装する事はコプロセッサを作る際の参考にもなるため、CSRをできる限り正しく実装することには意味があると言えます。
RISC-Vはフラグを持っていないCPUなので例外に頼る必要もあると思われます。

躓いていたテストは以下の実装でした。

  • rv32mi-p-illegal
  • rv32mi-p-ma_addr
  • rv32mi-p-ma_fetch
  • rv32mi-p-shamt
  • rv32mi-p-illegal

illegalに関しては今回はRV32Iのみのアーキテクチャであり、RVC(圧縮命令)の実装をしていないので、16bit命令が来た際に正しく不正命令例外を発生させればよくクリアできました。

  • rv32mi-p-ma_addr

load/storeのミスアラインアドレスのテストです。
ここで例外が発生したときにmepc(発生時のpc)に加えて、mtval(発生する要因となった命令そのもの)を格納する実装をしました。

  • rv32mi-p-ma_fetch

CSRモジュールはEXステージの時のPC,ALUへの入力値(rs1,rs2,immなど)を与える実装にしています。ミスアラインが発生するのは分岐命令時です。分岐先に指定されているアドレスが不正なために例外が発生します。

  • jal, jalrは条件なく分岐
  • branch命令は条件成立で分岐

これが少し厄介な状況を生み出します。jal/jalr命令は分岐先を調べて即例外を発行できるのに対し、branchは少なくともmemステージ(=ALUから比較結果の回答待ち)まで待たないといけません。ですので以下のような対応にしました。

  • jal, jalrは不正なアドレスを確認したら即例外発行
  • branch命令はpcと命令を一旦仮に格納(mepc,mtval)→分岐後不正なpcであった場合仮格納していたpcと命令を復元し例外発生

としました。ここで、「jal/jalrもいっしょの挙動にすりゃいいじゃん」または「CSR動作中は本体CPUストールすれば?」となります。
後者は検討中です。CSR自体もパイプライン動作をしているので「CSRモジュールが動作中か否か」という条件を洗い出す作業とALUをCSRに引き渡すロジックの追加が必要になります。
前者は容易に思いつきますができません。理由は以下の通りです。

 jal rd, <jump_address>

とした場合、rdにはこの命令の次のpcが格納されます。不正なアドレスにジャンプした後に例外判定をすると、rdが更新された後になってしまうため、rdの値をこの命令実行前の状態に復元する必要があります。
できなくはないけどする必要は全くないですし、いらない32bitのレジスタと配線を増やしてFPGAの動作を低下させる必要はないですね。
この命令もそうですが、パイプラインのなるべく早い段階で例外を発見し処理すると不要なパイプラインレジスタを実装せずにすみ、論理回路消費を抑え、高速化に寄与します。

  • rv32mi-p-shamt

シフト命令で(※純粋なシフト命令のみのテストではない)テストパスできないなんてあるのか楽勝じゃん?とおもったら最後までPassできずに泣いていました。
躓いていたのはこの命令。

slli	a0,a0,0x20 // a0 = a0 << 32

32bitシフトしたらゼロにすりゃいいのでは?と思っていたら、そういう理由ではありませんでした。
RV32Iでこの命令の即値は5bitです。つまり、0x20という表現はできません。不正命令として例外吐く動作が正しい挙動でした。基本的すぎるミスでウケる。

と、こんな感じでようやくクリアできました。
ここまで到達するのに3ヶ月。長かったですね。

今後

ランダムテストを通しながら以下の独立した試行をやっていきます。

  • 一旦今の状態でFPGAに実装
  • メモリウォーキング、Sv32の実装
  • CSRを独自実装のコプロに置き換え、テストコードの作成
  • カスタム命令の設計
  • OoOの設計
  • RV64Iの実装

コード

ここまでのコード全文は以下で。
github.com


コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.1.0 --depth 1 

Chiselを使ったRISC-Vの勉強(11. riscv-testsの自動化)

riscv-testsのリグレッションテストコードを自動生成

あまり記事にするほど内容は厚くないのですが備忘録として。

一つのテストコードをpassできても、今までpassできていたものがfailしてしまうロジックを埋め込んでしまう可能性は否めません。
全てのリグレッションテストを通しでpassさせる必要があります。今回の構成の場合、マシンモード/メモリウォーキング非搭載/32bit整数のみ/圧縮命令なしなのでrv32ui-p-/rv32mi-p-に対応すれば良く、テストコードは少なめの48個です。

まぁ比較的少ないのですが、手動で書いているとしんどいのです。

"rv32ui-p-add.hex test using Driver.execute" should "be pass test." in {
	iotesters.Driver.execute(Array(), () => new CpuBus())(testerGen = c => {
		CpuBusTester(c, "src/sw/rv32ui-p-add.hex")
	}) should be (true)
}

こんな感じのコードをリグレッションテスト数だけ作り、今後64bit拡張、Sv39搭載なども考えているのでテストは増大することになります。今のうちにpythonで自動化することにしました。


参考にさせていただいたのはid:msyksphinz氏のブログ。
msyksphinz.hatenablog.com

方針を決めていきます。

  • 手作業で頑張る部分
    • riscv-testsをgit cloneし、kyogenrvのプロジェクト内にmakeするところまでは手動(これは1回実行すればいいはずなので)
  • 自動化した部分
    • フォルダ内のrv32ui-p-* rv32mi-p-*で始まるelfファイルを走査し、hexファイルを作る
    • 作成したhexファイルのScalaTestコードを自動生成
    • テストコード実行

使い方

readmeには記載したのですが、改めて。

  • kyogenrvを導入
git clone http://github.com/panda5mt/KyogenRV  
  • riscv-testsを導入します
git clone https://github.com/riscv/riscv-tests
cd riscv-tests
git submodule update --init --recursive
nano env/p/link.ld

プログラム (.text)セクションが0x00000000から開始されるように修正

SECTIONS
{
  . = 0x00000000;   # -> ここを修正
  .text.init : { *(.text.init) }
  . = ALIGN(0x1000);
  .tohost : { *(.tohost) }
  . = ALIGN(0x1000);
  .text : { *(.text) }
  . = ALIGN(0x1000);
  .data : { *(.data) }
  .bss : { *(.bss) }
  _end = .;
}
  • link.ldを上書きしriscv-testsをビルド
autoconf
./configure --prefix=<KyogenRVsプロジェクトのルートディレクトリ>/tests/
make
make install
cd ../
cd KyogenRV/
make clean
make riscv-tests

これでテストコードのhex生成からリグレッションテストまで一貫して行います。終了するまで3分ほどかかりました。長い。
ちょっと工夫しないとなぁ。

IntelliJGUIで実行すると便利

IntelliJ IDEAユーザの方はmake riscv-testsするのではなくterminalで

make tester-gen

すると、テストコード生成までで終えてくれます。あとはGUIで行う方がpass/failが分かりやすいと思います。
/src/test/scala/TestCoreAll.scalaが自動生成されたコードです。
下図のようにクリックし、[Run TestCoreAll]を選択します。


f:id:Lynx-EyED:20200715074632p:plain

実行中。プログレスバーも出て進行度がわかります。


f:id:Lynx-EyED:20200715074741p:plain

リグレッションテストがどこでfailしたかも一目瞭然。(下図の場合はfence.i命令テストでエラー)


f:id:Lynx-EyED:20200715074900p:plain
最終的に48個中5個failしています。

f:id:Lynx-EyED:20200715075040p:plain
修正していきます。

Chiselを使ったRISC-Vの勉強(10. Load/Store全命令の実装)

実装中のRV32I RISC-V CPUですが、5段パイプラインのまま継続するか、6段パイプラインに増やすか見積もるため、Load/Storeを真面目に実装していませんでした。(今まで対応していたのは32bit長のlw/swのみ)
結論から申しますと、5段パイプラインのまま継続することにしました。詳細は後述する「パイプライン数の見積もり」をご覧ください。また、RV32Iとして一定の完成度になりつつあると思えてきたので、riscv-testsによるリグレッションテストも実施しました。

.dataセクションにも書き込めるようにする

Load/Store命令の実装がISA仕様を満たしているかをtestするにはriscv-testsのうち以下のものをクリアしなければいけません。

  • rv32ui-p-sb
  • rv32ui-p-sh
  • rv32ui-p-sw
  • rv32ui-p-lb
  • rv32ui-p-lbu
  • rv32ui-p-lh
  • rv32ui-p-lhu
  • rv32ui-p-lw

Dumpファイルを見れば分かりますが、上記のコードでは.dataセクションに初期値を置くため、データメモリが実装されていないとテストにパスしません。
今回製作したchiselプロジェクトではchisel-iotestersでコードを命令バス経由でメモリに書き込みます。
データメモリもこの命令バスからでもアクセスできるよう改造しました。


f:id:Lynx-EyED:20200708231604p:plain
なお、Avalon-MMに実装する際はデータバス、命令バスそれぞれから命令メモリ、データメモリをアクセスできるように接続します。データ/命令のアービトレーションは優先度に応じて切り替えられますし、優先度を同一にして優先権をラウンドロビンで移譲する実装もあるので問題とはなりません。
ですので、アンチグリッドロジックの実装は完全にAvalon-MM任せにできるので行いません。intel FPGA便利。

byte/halfword/wordの読み書きに対応する

データメモリ、命令メモリとも32bitアドレス/32bit幅のみのアクセスしかできていませんでしたが、RV32Iの場合load/store命令はbyte(8bit)/halfword(16bit)/word(32bit)のアクセスができないといけません。
これもAvalon-MMバス上ではbyteenable信号によりアラインメントの制約があるものの、8/16/32bitアクセスが可能です。
例えば、下位8bitのみ書き込みがしたい場合、

  • writedata = (32bitデータ)
  • byteenable = 0b’0001

とすれば32bitのうち、下位8bitのみ書き込まれます。

  • writedata = (32bitデータ)
  • byteenable = 0b’1100

とすれば32bitのうち、上位16bitのみ書き込まれます。
参考:
https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/manual/mnl_avalon_spec.pdf


f:id:Lynx-EyED:20200708232004p:plain
Avalon-MM有能です。
Chisel上でのメモリ側の実装は8bit x 4レーンに分け、byteenable信号によって書き込むレーンを選択するロジックを追加しAvalon-MM接続時と同等の環境を用意しました。
CPU側から、データメモリへの書き込みロジック(Store命令)は以下の通りです。

// send bus write size
    io.w_dmem_dat.byteenable := DontCare
    //  mem_rs(1)
    when(mem_ctrl.mem_wr === M_XWR) { // Store命令か?
        when(mem_ctrl.mask_type === MT_B) { // byteライト
            switch(mem_alu_out(1, 0)){ // アドレスの下位2bitでアラインを判断
                is("b00".U){
                    io.w_dmem_dat.byteenable := "b0001".U
                    io.w_dmem_dat.data := mem_rs(1)
                }
                is("b01".U){
                    io.w_dmem_dat.byteenable := "b0010".U
                    io.w_dmem_dat.data := mem_rs(1) << 8.U
                }
                is("b10".U){
                    io.w_dmem_dat.byteenable := "b0100".U
                    io.w_dmem_dat.data := mem_rs(1) << 16.U
                }
                is("b11".U){
                    io.w_dmem_dat.byteenable := "b1000".U
                    io.w_dmem_dat.data := mem_rs(1) << 24.U
                }
            }
        }.elsewhen(mem_ctrl.mask_type === MT_H) { // halfwordライト
            switch(mem_alu_out(1, 0)){
                is("b00".U){
                    io.w_dmem_dat.byteenable := "b0011".U
                    io.w_dmem_dat.data := mem_rs(1)
                }
                is("b10".U){
                    io.w_dmem_dat.byteenable := "b1100".U
                    io.w_dmem_dat.data := (mem_rs(1) << 16.U)
                }
            }
        }.otherwise { // wordライト
            io.w_dmem_dat.byteenable := "b1111".U
            io.w_dmem_dat.data := mem_rs(1)
        }
    }

CPU側から、データメモリへの読み込みロジック(Load命令)は以下の通りです。

when(mem_ctrl.mem_wr === M_XRD) { // Load命令か?
        when(mem_ctrl.mask_type === MT_B) { // byteリード
            switch(mem_alu_out(1, 0)){
                is("b00".U){ wb_dmem_read_data := Cat(Fill(24, io.r_dmem_dat.data(7)), io.r_dmem_dat.data( 7, 0)) }
                is("b01".U){ wb_dmem_read_data := Cat(Fill(24, io.r_dmem_dat.data(15)),io.r_dmem_dat.data(15, 8)) }
                is("b10".U){ wb_dmem_read_data := Cat(Fill(24, io.r_dmem_dat.data(23)),io.r_dmem_dat.data(23,16)) }
                is("b11".U){ wb_dmem_read_data := Cat(Fill(24, io.r_dmem_dat.data(31)),io.r_dmem_dat.data(31,24)) }
            }
        }.elsewhen(mem_ctrl.mask_type === MT_BU) { // byteリード符号なし
            switch(mem_alu_out(1, 0)){
                is("b00".U){ wb_dmem_read_data := Cat(0.U(24.W), io.r_dmem_dat.data( 7, 0)) }
                is("b01".U){ wb_dmem_read_data := Cat(0.U(24.W), io.r_dmem_dat.data(15, 8)) }
                is("b10".U){ wb_dmem_read_data := Cat(0.U(24.W), io.r_dmem_dat.data(23,16)) }
                is("b11".U){ wb_dmem_read_data := Cat(0.U(24.W), io.r_dmem_dat.data(31,24)) }
            }
        }.elsewhen(mem_ctrl.mask_type === MT_H) { // halfwordリード
            switch(mem_alu_out(1, 0)){
                is("b00".U){ wb_dmem_read_data := Cat(Fill(16, io.r_dmem_dat.data(15)), io.r_dmem_dat.data( 15, 0)) }
                is("b10".U){ wb_dmem_read_data := Cat(Fill(16, io.r_dmem_dat.data(31)), io.r_dmem_dat.data( 31, 16)) }
            }
        }.elsewhen(mem_ctrl.mask_type === MT_HU) { // halfwordリード符号なし
            switch(mem_alu_out(1, 0)){
                is("b00".U){ wb_dmem_read_data := Cat(0.U(16.W), io.r_dmem_dat.data( 15, 0 )) }
                is("b10".U){ wb_dmem_read_data := Cat(0.U(16.W), io.r_dmem_dat.data( 31, 16)) }
                // others
                is("b01".U){ wb_dmem_read_data := 0.U }
                is("b11".U){ wb_dmem_read_data := 0.U }
            }
        }.otherwise {
            wb_dmem_read_data := io.r_dmem_dat.data
        }
    }.otherwise {
        wb_dmem_read_data := io.r_dmem_dat.data
    }

パイプライン数の見積もり

上記実装の試行錯誤をしながらパイプライン数の見積もりをしていました。
結論として、

  • Load命令をStore命令にくらべて極端に多用する場合は6段パイプライン(たとえばGPIOの入力を絶えずポーリングするなど)
  • それ以外の場合は5段パイプラインで十分そう

Load命令が連続すると、ストール時間が増えます。加えて

(中略)
Load命令
分岐命令

という順序の場合、Load命令が分岐命令によってフラッシュされないようストール時間がさらに1クロック分増えます。この処理を多用するポーリング動作の場合は6段に拡張した方が処理速度は上がります。
が、低速な8bitマイコンではないのでこういう処理ばかりに専念させないと思われます。割り込み使うし。
加えていうのであれば、そこまでレイテンシ気にする場合はパイプラインマシンにとってコストが高い分岐命令を使うな、というところにまで発展するので。。。
こう言った結論から、5段パイプラインのままにしました。

riscv-testsを通してみた

RV32Iに要求されているテストのうち、以下がパスできたものです。

  • rv32ui-p-add
  • rv32ui-p-addi
  • rv32ui-p-and
  • rv32ui-p-andi
  • rv32ui-p-auipc
  • rv32ui-p-beq
  • rv32ui-p-bge
  • rv32ui-p-bgeu
  • rv32ui-p-blt
  • rv32ui-p-bltu
  • rv32ui-p-bne
  • rv32ui-p-jal
  • rv32ui-p-jalr
  • rv32ui-p-lb
  • rv32ui-p-lbu
  • rv32ui-p-lh
  • rv32ui-p-lhu
  • rv32ui-p-lui
  • rv32ui-p-lw
  • rv32ui-p-or
  • rv32ui-p-ori
  • rv32ui-p-sb
  • rv32ui-p-sh
  • rv32ui-p-simple
  • rv32ui-p-sll
  • rv32ui-p-slli
  • rv32ui-p-slt
  • rv32ui-p-slti
  • rv32ui-p-sltiu
  • rv32ui-p-sltu
  • rv32ui-p-sra
  • rv32ui-p-srai
  • rv32ui-p-srl
  • rv32ui-p-srli
  • rv32ui-p-sub
  • rv32ui-p-sw
  • rv32ui-p-xor
  • rv32ui-p-xori
  • rv32mi-p-breakpoint
  • rv32mi-p-csr
  • rv32mi-p-mcsr
  • rv32mi-p-sbreak
  • rv32mi-p-scall

パスできなかったもの

  • rv32ui-p-fence_i
  • rv32mi-p-ma_addr
  • rv32mi-p-ma_fetch
  • rv32mi-p-illegal
  • rv32mi-p-shamt

なおfence.i命令はこの記事執筆時点(ISA v.2.1)ではRV32/64/128の基本I命令からは外されており、"Zifencei"extensionという拡張扱いになっています。
実装は必須ではありません。命令キャッシュとデータキャッシュのコヒーレント性を保つ唯一の命令ですが、実装コストの大きさ、およびunix系OSで命令フェッチした際のコヒーレンス維持命令としては実用に耐えないと見たようです。
詳しくは以下の、p31,32を参照。
https://github.com/riscv/riscv-isa-manual/releases/download/draft-20200611-d08e29e/riscv-spec.pdf
あと、rv32mi-p-ma_*のテストコード、dumpファイルを見ると16bit命令が埋め込まれているので圧縮命令なのかな?(ma=misalignedってこと?)今回Cには対応しないのでこれはパスしなくていいかなという気持ち。あるいは例外吐く処理を入れるべきかな。。調査中
ミスアライン例外を起こすのが正解でした。
対応します。

その他のものはCSRの実装が現状で不完全なため動作が確認できていませんが、OpenOCDなどデバッガを接続した際に必須となるものばかりなので、実装していきます。

コード全文

github.com

コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.10.25 --depth 1 
cd KyogenRV/

macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。

brew install coreutils

makefileでOSによってod/godをスイッチしています。
アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。

nano src/sw/test.s

Chiselを使ったRISC-Vの勉強(9.CSR:外部割り込みの実装)

外部割り込みを実装しました。
RISC-Vに実装する割り込みはCPU外部からの割り込みだけ(あるいは各割り込み入力をマルチプレクスしてここへ入力、または割り込み信号の判別だけできるようにして入力を増やしていく感じで)でいいかな、と思ってきた次第です。

ターゲットFPGAintel P.S.GのCyclone10LP/GX/MAX10シリーズを考えているのでペリフェラルの実装はPlatform Designerで行います。
各モジュールはCPUの外部かつAvalon-MM上に存在することになります。割り込みはこの外部からの信号のみ受け付けるようにします。

事の発端

タイマー割り込みの調査をしていました。

  • mtime : 時間計測用。一定のクロックでカウントアップ。RV32/RV64でともに64bit長
  • mtimecmp:時間比較用レジスタ。mtime >= mtimecmpでタイマー割り込み発動。RV32/RV64でともに64bit長

となっており、タイマー割り込み発火のためには以下のようにプログラムを記述します。

  • mtvecに割り込みトラップ先のアドレスを格納
  • mtimecmpに割り込み発動の時間を格納
  • mie.mtie=1にしてタイマー割り込みを許可

なるほどねーと
アセンブラを書いてみました。

addi    x1, x0, 0x3FF
csrrw   x0, mtimecmp, x1

コンパイルすると
Error: unknown CSR `mtimecmp'

???


f:id:Lynx-EyED:20200625193918p:plain

Machine time register (memory-mapped control register).

Machine time compare register (memory-mapped control register).

モリーマップドなのかーい!
Privileged Architecture Version 1.7を見るとまだmtime,mtimecmpは32bit長でCSRレジスタになっており、v.1.9あたりからメモリマップドに変わっています。特にhart>=2であれば(いわゆるマルチコアであれば)割り込み関連のSFRはメモリマップドにする方が全てのhartのSFRに素早くアクセスができ、得策といえます。一見、当たり前のことのなのですが、ちょっとびっくり。

ここで方針を変更します。
CSRからメモリマップドIOにダイレクトにアクセスするのは得策とは言えません。
CPUとリソースの取り合いになるからです。割り込み発動も遅くなります。それにこんな事で調停回路つくるの超めんどくさい。

せっかくメモリーマップドなので、メモリーマップドなタイマーモジュールを作ります。
このモジュールにアクセスするのにCPUがmtime,mtimecmpまたはそれ相当のSFRをRWするというわけです。
そしてこのモジュールがmtime >= mtimecmpとなったときにCSRレジスタMIP.MTIP=1となるようにします。

割り込み回路実装の検討

ここで外部割り込みでもタイマー割り込みでも仕組みが同じになってしまいました。
違いは割り込み要因が外部割り込みMIP.MEIP=1かタイマー割り込みMIP.MTIP=1かの違いです。

タイマー割り込み回路を作る前の素振りとして、外部割り込み回路を作ります。

外部割り込み回路実装

CPUへの実装をしていきます。

  • 割り込み入力用の1ビット信号を用意
  • 任意のタイミングで割り込みビット=1
  • 割り込み発生
  • mepcへ発生時のアドレスを格納
  • mtvecのアドレスに分岐

簡単です。
このとき、割り込み発生時に分岐命令処理中ですと割り込み信号による例外が握り潰されてしまうことがあるので、pc更新の論理回路が例外発生時の処理を最優先にするよう修正しています。

mepcがゼロになるんだが

mepcは前回のブログでも取り上げた通り例外発生時にいたpcを格納するCSRレジスタです。
実はむずかしいのはmepcの実装でした。理由は以下の通りです。

マルチパイプラインCPUなので、分岐中は実際にデコードや演算処理しないけど便宜上フェッチする(あとでnopにするかフラッシュする)命令があったり、ストール中は一旦pcをゼロにして、分岐完了後に正しいカウンターをフェッチして再カウントしたりします。
という訳で、いつ発動するかわからない割り込みは、場合によっては有り得ないpcを戻り値としてmepcへ格納してしまうことになりかねません。
対策としては、パイプラインが処理中の内容を監視し、ストール中か分岐中かを判別する。もしどちらかの状態であれば、その時点で有効だった最後のpcの値を戻り値としてmepcに格納します。


判別用にpc_invalidという信号を用意しました。

// program counter check
val pc_invalid: Bool = inst_kill_branch || (ex_pc === pc_ini)

割り込みが発動した場合かつ分岐中、ストール中(pc=0)はmepcにpcを格納しないようにしました。
この場合は最後に有効だったpcをmepcへ格納します。

それ以外の例外発生時、例えばecall命令などではどうなるかというと、自身が原因でストールが発生、次フェーズで例外要因の分岐が発動しているはずなので上記の処理したpcを戻り値として格納してはいけません。
例外発動するまえのpcの値が格納されるので正確ではなくなる。そのまま現在のpcをmepcへ格納するのが正しいといえます。

とまぁ、src/main/scala/core/csr.scalaを見てもらえればこの辺りは分かっていただけるかと思います。

実行結果

アセンブラを記述します。<0xXX>と書いてあるのはその命令のアドレスです。割り込みが発生すると_label_expc(=0x18番地に飛びます)

_label0:
        addi    x1, x0, 0x18        # <0x00>
        csrrw   x0, mtvec, x1       # <0x04> : mtvec = <0x18>
        lui     x1, 0x01
        srli    x1, x1, 1
        csrrw   x0, mie, x1         # enable external interrupt(mie.mtie = 1)
        jal     x0, _label1         # jump _label1

_label_expc:
        addi    x2, x0, 0xAA        # <0x18>
        csrr    x3, mepc            #
        csrr    x4, mcause          #
        jal     x0, _label_expc     # <0x24> : loop

_label1:
        addi    x5,  x0, 0xBB       # <0x28>

_label2:
        addi    x6,  x0, 0xCC       # <0x2C>
        jal     x0, _label2         # <0x30>

さて今回は、アセンブラを書いただけだと割り込みが発生したかどうかわかりません。
chisel-iotestersでCPUに擬似的に任意の時間に割り込み信号を注入します。(今回はlp==96としました。割り込み許可フラグが1になった後であればどのタイミングでもOK)

// src/main/scala/core.scalaの578行目付近
(中略)
if(lp == 96){
  poke(signal = c.io.sw.w_interrupt_sig, value = true.B)
}
 else{
  poke(signal = c.io.sw.w_interrupt_sig, value = false.B)
 }

実行後。


f:id:Lynx-EyED:20200625202401p:plain
x2 = 0xAAとなっているので無事割り込みトラッピングに成功しています。
またx3 = mepc = 0x2cとなっているので、割り込み発動時にaddi x6, x0, 0xCCを実行中だったことがわかります。
割り込み発動のタイミングによってはx3 = mepc = 0x30となる場合もあると思います。

コード全文

github.com

コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.10.21 --depth 1 
cd KyogenRV/

macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。

brew install coreutils

makefileでOSによってod/godをスイッチしています。
アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。

nano src/sw/test.s

Chiselを使ったRISC-Vの勉強(8.CSR:例外の実装と割り込みの準備)

riscv-testsをパスしたい

RV32Iと謳うにはriscv-testsのテストパタンセットのうちRV32I ISAに要求されているリグレッションテストをパスするのが望ましいと言えます。CSR周りはその限りではありません。(picorv32などの特権命令非搭載かつ例外処理が独自、という実装もある)

github.com
このパターンセットをパスすることにより、RISC-Vのアーキテクチャとして正しく実装されているという指標になります。
ここではriscv-testsのrv32ui-p-and.dumpからどのような挙動をするのかをみてみます。
実装が正しければ最終的にpass,正しくない実装があるとfailにジャンプします*1
開始アドレスはリンカスクリプトlink.ldを書き換えて0x0000_0000からはじまるようにしています。(詳細後述)

(略)
0000062c <fail>:
 62c:	0ff0000f          	fence
 630:	00018063          	beqz	gp,630 <fail+0x4>
 634:	00119193          	slli	gp,gp,0x1
 638:	0011e193          	ori	gp,gp,1
 63c:	05d00893          	li	a7,93
 640:	00018513          	mv	a0,gp
 644:	00000073          	ecall

00000648 <pass>:
 648:	0ff0000f          	fence
 64c:	00100193          	li	gp,1
 650:	05d00893          	li	a7,93
 654:	00000513          	li	a0,0
 658:	00000073          	ecall
 65c:	c0001073          	unimp
 660:	0000                	unimp
 662:	0000                	unimp

passでもfailでもecall命令を使い例外を発生させています。なおpassするとgp(x3) = 1となります。
ecallはmtvecに格納されているベクタアドレスに遷移します。

 118:	00000297          	auipc	t0,0x0
 11c:	eec28293          	addi	t0,t0,-276 # 4 <trap_vector>
 120:	30529073          	csrw	mtvec,t0

上記のように、mtvecにはtrap_vectorアドレスが格納されていることがわかります。

00000004 <trap_vector>:
   4:	34202f73          	csrr	t5,mcause
   8:	00800f93          	li	t6,8
   c:	03ff0863          	beq	t5,t6,3c <write_tohost>
  10:	00900f93          	li	t6,9
  14:	03ff0463          	beq	t5,t6,3c <write_tohost>
  18:	00b00f93          	li	t6,11
  1c:	03ff0063          	beq	t5,t6,3c <write_tohost>
  20:	00000f13          	li	t5,0
  24:	000f0463          	beqz	t5,2c <trap_vector+0x28>
  28:	000f0067          	jr	t5
  2c:	34202f73          	csrr	t5,mcause
  30:	000f5463          	bgez	t5,38 <handle_exception>
  34:	0040006f          	j	38 <handle_exception>

00000038 <handle_exception>:
  38:	5391e193          	ori	gp,gp,1337

0000003c <write_tohost>:
  3c:	00001f17          	auipc	t5,0x1
  40:	fc3f2223          	sw	gp,-60(t5) # 1000 <tohost>
  44:	ff9ff06f          	j	3c <write_tohost>

trap_vectorに遷移したのち例外発生要因mcauseを調べています。

  • mcause = 8:ユーザーモードでのecall
  • mcause = 9:スーパーバイザーモードでのecall
  • mcause = 11:マシンモードでのecall

となります。今回はマシンモードでの暫定実装ですのでecallでの遷移時は11以外は格納されません。
それ以外の場合、

gp = gp | 1337

が実行されます。ですのでtestに成功してもmcauseの要因がいい加減ですとgp=1が得られなく、テストは失敗に終わります。


なんで1337なんだろ。まあいいか。


なお、割り込み時もmcauseのMSBビット(RV32の場合はmcause[31]が1になる)以外、挙動は同じです。

と、ここまで考えてきたように、CSRを最低限riscv-testsをパスできるレベルで実装しなければいけなくなってきました。
riscv.org

もう一度まとめると、
riscv-testsは各命令のテストを終えた後、

  1. 汎用レジスタgp(x3)に1(pass)またはそれ以外(fail)を格納
  2. 例外を発生(ecall)
  3. mtvecに格納されているトラップベクタへ
  4. 例外の発生要因と発生時の特権モードの確認
  5. 特権モードが予期しないモードだった場合、gp = gp | 1337を計算し格納。
  6. 無限ループへ(終了)

という動作をします。

実装方針

CSRのうち、以下を実装しなきゃいけないことがわかります。めんどくさいなぁ。

・ecall命令
・mtvec
・mepc(例外発生したpcを格納)
・mcause
・mstatus(最低限。マシンモードのみ実装)

「めんどくさい」というのはここまで実装しておいて今更ですが、CSR周りはPrivileged ISAのバージョンアップ毎にガラリと仕様が変わる為、あまり頑張りたくない感じなのです。

これまでの先人たちの実装をgithubで探すと、ある機能を果たすレジスタ名が同じでアドレスだけ異なっていたり、読み書きの権限が異なっていたりとなかなかカオスです。

ただし、例外を実装してしまえば(誤解を恐れずにいうのであれば)汎用組込みマイコンとしてマストな機能である割り込みの実装ができたようなものなので…それをモチベーションに頑張ります。

参考にしたのはいつも通り、riscv-miniです。
github.com
しかし、今回目指している実装はPrivileged Architecture Version 1.11に準拠していますが、この実装はVersion 1.7に準拠している為、このままの実装はできません。(mtvecは読み込みしかできない実装だったし)
あと、いま制作しているプロジェクトは5ステージパイプラインですが、riscv-miniは3ステージです。データフォワーディングなどの仕組みが若干異なります。

riscv-testsの導入

基本的にREADMEにあるようにすれば導入できます。

git clone https://github.com/riscv/riscv-tests
cd riscv-tests

ここで命令メモリ開始アドレスが異なっている場合は修正します。

nano env/p/link.ld

自分の場合は開始アドレスが0x0000_0000だったので下記のように変更しました。

SECTIONS
{
  . = 0x00000000;   # ->ここを修正した
  .text.init : { *(.text.init) }
  . = ALIGN(0x1000);
  .tohost : { *(.tohost) }
  . = ALIGN(0x1000);
  .text : { *(.text) }
  . = ALIGN(0x1000);
  .data : { *(.data) }
  .bss : { *(.bss) }
  _end = .;
}

保存後ビルドします。

git submodule update --init --recursive
autoconf
./configure --prefix=$(インストールしたいフォルダ)
make
make install

prefixで指定したフォルダに何やらたくさん入りますが、欲しいバイナリは(インストールフォルダ)/share/riscv-tests/isaに格納されています。
今回はand命令のコードをテストしようと思います。拡張子なしでrv32ui-p-andというファイル名が存在していると思いますが、実態はelfファイルです。自分のプロジェクトで読み込めるようにオリジナルhexファイルに変換します。

riscv64-unknown-elf-objcopy --gap-fill 0 -O binary  rv32ui-p-and rv32ui-p-and.bin
od -An -v -tx4 -w4 rv32ui-p-and.bin > rv32ui-p-and.hex

これをsrc/main/scala/core/core.scalaで読み込むように指定します。

実行結果


f:id:Lynx-EyED:20200618204708p:plain
gp(x3) = 1となっているので成功です。and命令はちゃんと動くようだ。よかった。
まだ何十個もテスト残ってるけど。

コード全文

github.com

コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.10.20 --depth 1 
cd KyogenRV/

macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。

brew install coreutils

makefileでOSによってod/godをスイッチしています。
アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。

nano src/sw/test.s

mepcはどのアドレスを格納すべきなのか

分岐命令とジャンプ命令では、

rd = (分岐が起こったアドレス) + 4.U

脊髄反射で格納していました。mepcも同じように(例外発生アドレス)+4.Uを格納して良いのでしょうか。
twitterで教えていただきました。ありがとうございます。


また、specificationにも以下の記述があるとslackでも教えていただいきました。

When a trap is taken into M-mode, mepc is written with the virtual address of the instruction that was interrupted or that encountered the exception. Otherwise, mepc is never written by the implementation, though it may be explicitly written by software.

トラップが発生しマシンモードに遷移すると、mepcには割り込み命令や、例外を発生させた命令の仮想アドレスが書き込まれます。それ以外の場合は、ソフトウェアによって明示的に代入されることはあるかもしれませんが、実装によってmepcがwriteされることはありません。

なるほどー。なので、もし復帰する際は自分で例外発生したアドレスに戻るか、その次の命令のアドレスにいくか記述してやる必要があるとのこと。

おしまい。

*1:Compress命令はまた異なる挙動になります。今回C命令を実装しない方向で進めています

Chiselを使ったRISC-Vの勉強(7.ハザードの解決)

MEMステージの実装中です。
制御ハザードはすでに分岐命令を実装した時点で実装しており、パイプライン化をしても特筆すべき点はありません。
ここではデータハザードを直していきます。
データハザードの解決が終わると、MEMステージをこのままにするか、もう1ステージ増やして6段パイプラインにするかが見積もりやすくなるはずです。

データハザードの解決

命令は上から下に実行されていきます。横軸は時間で、各命令のどのステージが進行中かがわかるようになっています。


f:id:Lynx-EyED:20200531143535p:plain
ここで、正確な値の入力を必要とするのは、EXステージです。同じ汎用レジスタに連続でアクセスする場合、これが問題となることがあります。
例えばaddi命令。

  • IF ステージ:命令フェッチ
  • IDステージ:rs1,rs2決定
  • EXステージ:ALUの入力と命令が確定
  • MEMステージ:ALUからの演算結果(addiの場合は足し算の値)確定
  • WBステージ:rdへALUからの演算結果代入


f:id:Lynx-EyED:20200531144814p:plain
addiの結果代入先のrdが、次命令のrs1,rs2のいずれかになると大変困ります。
rd確定前に、次命令がrs1,rs2が確定しているべきステージ(IDステージ)を過ぎているからです。

例は少し変わりますが、以下のような命令が考えられます。

lui     x1,  0x08     # x1 = 0x08 << 12 = 0x8000
addi    x1,  x1, 0x04 # x1 = x1 + 0x04 = 0x8004となるべきだが.....?

このままだと、最終的なx1の値は0x04となってしまいます。x1の値がまだWBステージまで到達する前に次の命令のEXステージで計算が開始されてしまうからです。

データフォワーディング

ここでハザードが起こることは分かりました。
解決していきます。この場合はデータフォワーディングすることによってこの問題に対処します。
rdに結果が代入されるのはWBステージですが、一つ前のMEMステージで値は確定しています。ですのでMEMステージで次命令のEXステージに結果をバイパスします。


f:id:Lynx-EyED:20200531145458p:plain



他にデータフォワーディングが必要とされる状況が考えられるでしょうか。
データメモリにアクセスする命令でハザードが多発すると思われます
(例1)

addi  x2 x0, 0xAA  # x2 = 0xAA
sw    x2, 5(x31)   # dmem[x31+5] = x2
lw    x3, 5(x31)   # x3 = dmem[x31+5]

ここでx3は0xAAとなるべきです。先ほど、データハザードの対策を適切にしたはずなのでこの場合でも正しい値が出るはずです。
でも以下の場合はどうでしょうか。
(例2)

addi  x2 x0, 0xAA  # x1 = 0xAA
nop
sw    x2, 5(x31)   # dmem[x31+5] = x2
lw    x3, 5(x31)   # x3 = dmem[x31+5]

「nopも入れてるんだし当然動くでしょ」と思ってたのですが、x3=0。しばらくデータメモリの実装を疑いましたが異常はありませんでした。
というか(例1)で動いているのでデータメモリの実装がおかしいはずはないのですが。。。


f:id:Lynx-EyED:20200531151901p:plain
1行目の命令がWBステージに到達する前に3行目の命令はIDステージに突入しています。ですのでここではWBステージからMEMステージにデータフォワーディングしてあげる必要があります。


f:id:Lynx-EyED:20200531212006p:plain
この部分をコードで記述すると以下のようになりました。信号名は参考にしたrocket-chipのものと同等です。

val ex_reg_rs1_bypass: UInt = MuxCase(ex_rs(0), Seq(
        (ex_reg_raddr(0) =/= 0.U && ex_reg_raddr(0) === mem_reg_waddr && mem_ctrl.rf_wen === REN_1) -> mem_alu_out,
        (ex_reg_raddr(0) =/= 0.U && ex_reg_raddr(0) === wb_reg_waddr && wb_ctrl.rf_wen === REN_1 /*&& wb_ctrl.mem_en === MEN_1*/) -> io.r_dmem_dat.data
    ))
    val ex_reg_rs2_bypass: UInt = MuxCase(ex_rs(1), Seq(
        (ex_reg_raddr(1) =/= 0.U && ex_reg_raddr(1) === mem_reg_waddr && mem_ctrl.rf_wen === REN_1) -> mem_alu_out,
        (ex_reg_raddr(1) =/= 0.U && ex_reg_raddr(1) === wb_reg_waddr && wb_ctrl.rf_wen === REN_1 && wb_ctrl.mem_en === MEN_1) -> io.r_dmem_dat.data,
        (ex_reg_raddr(1) =/= 0.U && ex_reg_raddr(1) === wb_reg_waddr && ex_ctrl.rf_wen === REN_0 && ex_ctrl.mem_en === MEN_1) -> wb_alu_out
    ))

データストールによる解決

他にどんな場合があるでしょうか。
WBステージでないと値が確定できない命令があります。Load命令全般です。
この場合はMEMステージから値だけもらってフォワーディングする、といった技が使えません。ストールを行うことで対処します。

(Load命令 && 次命令がLoad命令が使用したアドレスをrs1,rs2に使う) → ストール信号生成

ストール信号を発行するとpcの更新がとまり、ストールしたいステージのレジスタの更新が停止することになります。今回、IDステージを停止させるので、以下の図のようにIDステージが2回実行され、その時のEXステージはNOPとなります。


f:id:Lynx-EyED:20200531213000p:plain
しかしこの実装はあまり筋が良いとは言えません。データが次のステージで必ず読み込める前提になっているからです。
密結合メモリをFPGAの内部に作り込む前提である場合はここは問題とはなりませんが、FPGA外部のSDRAMなどにデータメモリを持つ場合はこの実装では動かないでしょう。
今回の実装の場合、メモリ側からackが帰ってくるまで待つのが正しい実装と言えます。ackが帰ってくるまで永久にIDステージを繰り返します。*1

f:id:Lynx-EyED:20200531214100p:plain
このストール信号の部分のコードの記述は以下のようになります。

var mem_stall: Bool = RegInit(false.B)

when (ex_ctrl.mem_wr === M_XRD) {
        mem_stall := true.B
} .elsewhen(io.r_dmem_dat.ack === true.B) {
        mem_stall := false.B
}
// メモリリード信号が発行されてから、メモリからACK信号(データ送信済み)がくるまでmem_stall信号をtrueにする。 

load_stall := ((ex_reg_waddr === id_raddr(0) || ex_reg_waddr === id_raddr(1)) && (ex_ctrl.mem_en === MEN_1) && (ex_ctrl.mem_wr === M_XRD)) || mem_stall

テストコード

今回の実装を包括的に網羅するアセンブラを記述します。

_label1:
        lui     x1,  0x08           # x1 = 0x08 << 12
        addi    x1,  x1, 0x04       # x1 = x1 + 0x04 = 0x8004
        addi    x2,  x1, 0x00      #  x2 = x1 

        sw      x2, 12(x31)        #  dmem[x31 + 12] = x2 = 0x8004
        lw      x3, 12(x31)        #  x3 = dmem[x31 + 12] => 0x8004になっているべき


        addi    x4, x0, 0xAA       # x4 = 0xAA
        sw      x4, 12(x31)        # dmem[x31 + 12] = x4 = 0xAA
        lw      x5, 12(x31)        # x5 = dmem[x31 + 12] = 0xAA

        beq    x4, x5, _label2   # x4 = x5 なら_label2へジャンプ
        addi    x10, x0, 0x44       
        addi    x11, x0, 0x55



 _label2:
        jal x0, _label2             # 無限ループ

実行結果


f:id:Lynx-EyED:20200531215028p:plain
実行できています。
x10,x11に値が代入されてしまっている場合は、beq命令が正しく評価されていない=>前命令のload命令でx4が格納される前に実行されてしまった可能性があります。


所感

ようやく5ステージパイプラインを持つCPUっぽくなってきました。さらにストールやフォワーディングが必要になった場合は今回のコードに追記する形でできると思いますので、そんなに辛くなさそう。
あと、ストールのところで「メモリリード信号が発行されてから、メモリからACK信号(データ送信済み)がくるまでmem_stall信号をtrueにする。」としていますが、ハザードは解決されたもののちょっとタイムロスが発生しているので、もう少し工夫してみようかなと思います。

コード全文

github.com

コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.10.15 --depth 1 
cd KyogenRV/

macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。

brew install coreutils

makefileでOSによってod/godをスイッチしています。
アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。

nano src/sw/test.s

次回は?

この一ヶ月ほど、駆け足でやってきました。
コロナの影響で止まっていた製造関係も少しづつ再開が始まってきました。
通常作業がいつもの80%くらいまで復活してきています。*2


ですのでRISCV関係は少しペースは落としていかなければならないと思いますが、引き続き続けていきますのでよろしくお願いいたします。

*1:RAMが破壊されている場合も考慮した方がいい場合もあります。例外吐く処理を実装した方が最善かもしれません

*2:疫病の危機が去ったわけではありません。自分や他の人の健康や命に対し無頓着であってはなりません。しかし、このブログはそう言った話題に乗じてなにか政治的社会的プロパガンダを発したりはこれまでもしてきませんでしたしこれからも同様です。露ほども為にはならないかもしれませんが、面白いと思う人に面白いと思われるのならそれが本望です。うんこ。