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は各命令のテストを終えた後、
- 汎用レジスタgp(x3)に1(pass)またはそれ以外(fail)を格納
- 例外を発生(ecall)
- mtvecに格納されているトラップベクタへ
- 例外の発生要因と発生時の特権モードの確認
- 特権モードが予期しないモードだった場合、gp = gp | 1337を計算し格納。
- 無限ループへ(終了)
という動作をします。
実装方針
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
で読み込むように指定します。
実行結果
まだ何十個もテスト残ってるけど。
コード全文
コードは今後も漸進的に更新されるので、以下のように-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で教えていただきました。ありがとうございます。
MEPCは「例外が発生したPC」を格納するという定義ですので値を勝手に進めたりしません。しかも+4 進めてしまうとC命令で例外が発生すると再実行できなくなります。
— msyksphinz_dev (@dev_msyksphinz) 2020年6月14日
また、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段パイプラインにするかが見積もりやすくなるはずです。
データハザードの解決
命令は上から下に実行されていきます。横軸は時間で、各命令のどのステージが進行中かがわかるようになっています。
例えばaddi命令。
- IF ステージ:命令フェッチ
- IDステージ:rs1,rs2決定
- EXステージ:ALUの入力と命令が確定
- MEMステージ:ALUからの演算結果(addiの場合は足し算の値)確定
- WBステージ:rdへALUからの演算結果代入
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ステージに結果をバイパスします。
他にデータフォワーディングが必要とされる状況が考えられるでしょうか。
データメモリにアクセスする命令でハザードが多発すると思われます
(例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)で動いているのでデータメモリの実装がおかしいはずはないのですが。。。
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となります。
密結合メモリをFPGAの内部に作り込む前提である場合はここは問題とはなりませんが、FPGA外部のSDRAMなどにデータメモリを持つ場合はこの実装では動かないでしょう。
今回の実装の場合、メモリ側からackが帰ってくるまで待つのが正しい実装と言えます。ackが帰ってくるまで永久にIDステージを繰り返します。*1
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 # 無限ループ
所感
ようやく5ステージパイプラインを持つCPUっぽくなってきました。さらにストールやフォワーディングが必要になった場合は今回のコードに追記する形でできると思いますので、そんなに辛くなさそう。
あと、ストールのところで「メモリリード信号が発行されてから、メモリからACK信号(データ送信済み)がくるまでmem_stall信号をtrueにする。」としていますが、ハザードは解決されたもののちょっとタイムロスが発生しているので、もう少し工夫してみようかなと思います。
コード全文
コードは今後も漸進的に更新されるので、以下のように-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関係は少しペースは落としていかなければならないと思いますが、引き続き続けていきますのでよろしくお願いいたします。
Chiselを使ったRISC-Vの勉強(6.パイプライン化とALUを使用する命令の見直し)
突然ですがいままでシングルラインで組んでいたCPUプロジェクトをいきなり5ステージパイプラインにしました。
いきなり!ステージ
パイプライン化に向けての準備
本来でしたら、シングルステージでまだ実装が終わっていないMEMステージまで実装後複数段パイプラインにするのが筋だとおもいます。
しかしプロジェクトを進めていく上で問題が発生していました。
- 機能が増えていくにつれて各ステージの境目がわからなくなってきていた
- バス上にメモリマップドなメモリがあり、そこから命令の読み込み、データ読み書きをする前提である
- シングルクロック未満(=非同期)でメモリへの読み込み、書き込みは現実的に不可能。例えばアドレスを与える→データを受け取るを考えても最低2クロック必要。
- したがってメモリアクセスが終了するまで次の命令に移行しないようストール機能が必須
- 併せて各機能で分離して動作させるためステージングも必要
- RV32Iの場合、なお、データメモリは8bit/16bit/32bit単位でアクセスができないといけない
- データメモリは32bitバスを前提としているのでバス幅調整に1ステージ分追加するかも見積もる必要がある。
したがってこの時点でシングルパイプラインで実装し続けるメリットは皆無であり、一旦パイプライン化してからデータメモリ、その他機能を追加し全容を把握するのが最適と言えます。なおMEMステージで実装するデータメモリは命令メモリと基本的RWは変わらないので32bit幅のアクセスだけできるようかなり大雑把に実装しています。
予定していたマルチパイプライン化にこの段階で取り組むことにしました。
参考にしたコード
rocket/src/main/scala/dpath.scala · fe9adfe71b90046d7d6b84d0c9fe033b06c9229d · CoDEx / rocket-chip · GitLab
本家のRocket-Chipのブランチから探しても同じものが取得できると思います。
ステージごとにかなり簡潔に記述されているので、自分のプロジェクトに導入する際にも分かりやすかったです。
パイプライン化をしたからと言って、いままでの資産が無駄になることはなく、必要とされるデータのレジスタをステージ分用意するだけです。
例えばCPUのパイプラインが5ステージあって、任意のデータが最終ステージまで必要とされているのであれば、データ x5 のレジスタが必要になるだけです。
任意のデータが3段目のステージまで必要とされているのであれば、データ x3のレジスタが必要、といった具合です。
また各ステージで前段の処理が終わっていないと次のステージに移行できないようであれは適宜ストール信号を発行できるようにします。
この部分は未完成な部分があるので、次回以降に取り扱いたいと思います。
行数がいきなり増えるのでびっくりしますが、各ステージで分けて考えられるようになるので、シングルステージよりも考えやすいと思います。
ここでパイプライン化する際に必須ではないですが併せて修正した部分を取り上げます。
ALUの変更
いままで算術、論理命令だけの演算をしていました。しかし条件分岐もおなじALUを使う事ができるはずです。実際同じ演算を行いますし、ALUと別に条件分岐ごとに比較演算機を外部に作るのは論理回路の浪費に思われます。
条件分岐の処理中に算術演算命令を並行して行うようなハザードが発生するステージはないからです。
そういう並行して動作する構成も面白そうですけどそれはまたの機会に。
というわけで、少し処理を追加する必要がありました。
以下の4つの演算です
- ALU_SEQ (入力された2つの値(in.a, in.b)は等しいか?)
- ALU_SNE (入力された2つの値(in.a, in.b)は等しくないか?)
- ALU_SGE (in.a >= in.bか?)
- ALU_SGEU (UInt(in.a) >= UInt(in.b)か?)
※ALU_SGE (io.op1 >= io.op2か?)、SGEU(SGEの符号なし)はすでに実装済みです。
以下のような実装です。
object ALU { def ALU_ADD: UInt = 0.U(5.W) def ALU_SLL: UInt = 1.U(5.W) def ALU_SEQ: UInt = 2.U(5.W) def ALU_SNE: UInt = 3.U(5.W) def ALU_XOR: UInt = 4.U(5.W) def ALU_SRL: UInt = 5.U(5.W) def ALU_OR: UInt = 6.U(5.W) def ALU_AND: UInt = 7.U(5.W) def ALU_COPY1: UInt = 8.U(5.W) def ALU_COPY2: UInt = 9.U(5.W) def ALU_SUB: UInt = 10.U(5.W) def ALU_SRA: UInt = 11.U(5.W) def ALU_SLT: UInt = 12.U(5.W) def ALU_SGE: UInt = 13.U(5.W) def ALU_SLTU: UInt = 14.U(5.W) def ALU_SGEU: UInt = 15.U(5.W) def ALU_X: UInt = 0.U(5.W) // BitPat("b????") def isSub(op: UInt): Bool = op >= ALU_COPY2 // need sub? def isCmp(op: UInt): Bool = op >=ALU_SLT // Compare op? def isCmpU(op: UInt): Bool = op >= ALU_SLTU // Compare unsigned? def isCmpI(op: UInt): Bool = op(0) // need inverse for compare? //noinspection ScalaStyle def isCmpEq(op: UInt): Bool = !op(3) // EQ or NEQ compare operation? } import ALU._ //noinspection ScalaStyle class ALU extends Module { val io = IO { new Bundle { val op1: UInt = Input(UInt(32.W)) val op2: UInt = Input(UInt(32.W)) val alu_op: UInt = Input(UInt(5.W)) val out: UInt = Output(UInt(32.W)) val cmp_out = Output(Bool()) } } // Shift val op2_inv: UInt = Mux(isSub(io.alu_op), ~io.op2, io.op2).asUInt() val sum: UInt = io.op1 + op2_inv + isSub(io.alu_op) val shamt: UInt = io.op2(4,0).asUInt val shin: UInt = Mux(io.alu_op === ALU_SRA || io.alu_op === ALU_SRL,io.op1,Reverse(io.op1)) val shift_r: UInt = (Cat(isSub(io.alu_op) & shin(31), shin).asSInt >> shamt)(31, 0) val shift_l: UInt = Reverse(shift_r) val slt: Bool = Mux(io.op1(31) === io.op2(31), sum(31), Mux(isCmpU(io.alu_op), io.op2(31), io.op1(31))) val cmp = isCmpI(io.alu_op) ^ Mux(isCmpEq(io.alu_op), (io.op1 ^ io.op2) === 0.U, slt) val w_out = Mux(io.alu_op === ALU_ADD || io.alu_op === ALU_SUB, sum, Mux(io.alu_op === ALU_SLT || io.alu_op === ALU_SLTU, cmp, Mux(io.alu_op === ALU_SRA || io.alu_op === ALU_SRL, cmp, Mux(io.alu_op === ALU_SLL, shift_l, Mux(io.alu_op === ALU_AND, io.op1 & io.op2, Mux(io.alu_op === ALU_OR, io.op1 | io.op2, Mux(io.alu_op === ALU_XOR, io.op1 ^ io.op2, Mux(io.alu_op === ALU_COPY1, io.op1 , io.op2)))))))) io.out := w_out io.cmp_out := cmp } }
実装にあたってはいつものriscv-miniを参考にしました。
github.com
今回追加した機能はmini-riscvには含まれてはいませんので、自前で追加実装する必要があります。
ド・モルガンの法則がわかってればなんてことはないでしょ、、、ってここを実装するのすんごい時間かかった。
高専の数学1の最後の方を20年ぶりくらいに必死に勉強し直す始末に。
みんな社会人になっても宇宙人になっても仙人になってもおじいさんおばあさんになっても数学の勉強やろうね。趣味すらできなくなるから。
chisel-iotestersでプローブする信号の追加
今のところ必要としている信号は以下のものです。適宜追加していきます。
- IF/IDステージ: pc , 機械語
- EXステージ:rs1, rs2, imm
- MEMステージ:ALU演算結果
- WBステージ: ALU演算結果とrd
アセンブラを組む
前回のものに少し手を加えただけです。
今回の動作確認における実質的な内容は変わらないので各行の日本語のコメントは省略します。
test.s
に以下を記述します
_start0: addi x29, x0, 10 # x29 = x0 + 10 = 10 addi x31, x29, 0xAA # x31 = x29 + 0xAA = 180 (= 0xB4) _label1: addi x1, x0, 1 # x1 = x0 + 1 = 1 addi x2, x0, 2 # x2 = x0 + 2 = 2 addi x3, x0, 3 # x3 = x0 + 3 = 3 addi x4, x0, 4 # x4 = x0 + 4 = 4 addi x5, x0, 5 # x5 = x0 + 5 = 5 addi x6, x0, 6 # x6 = x0 + 6 = 6 addi x7, x6, 7 # x7 = x6 + 7 = 13 (= 0x0D) addi x8, x0, 8 # x8 = x0 + 8 = 8 jal x4, _label4 # x4 => address(_label2), jump _label4 _label2: addi x9, x0, 9 # x9 = 9 addi x10, x0, 10 # x10= 0x0A addi x11, x0, 11 # x11= 0x0B _label3: jalr x0, x5,0 # jump to x5 (= _label5) _label4: addi x12, x0, 12 # x12= 0x0C jalr x5, x4, 4 # x5 => _label5, jump x4+4 (= _label2+4) _label5: jal x0, _label5 # forever loop nop
動作確認
make clean make
で動作確認を行う事ができます。IntelliJのユーザはTerminalで一旦上記コマンドを実行してからrunしてみてください。(runだけだと機械語ファイルがない場合エラーになる)
ここではおもにアセンブラファイル2行目のaddi x31, x29, 0xAA
に注目したいと思います。
x29の値(0xAが格納されている)とimmの値(0xAA)の足し算の結果をx31に書き戻すという命令です。
コード全文
コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
git clone http://github.com/panda5mt/KyogenRV -b 0.0.10.8 --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の勉強(5. 分岐命令の実装)
前回の記事と分けてしまったら内容がほとんど皆無になってしまいました。
ないようがないよう
(この辺りでみんなブラウザ閉じる)
年取るってこういうことなんだなぁ。
さて、本題です。
前回riscv-asからアセンブルした機械語をChiselプロジェクトにワンストップで導入する仕組みを構築しました。
分岐命令の相対アドレスを計算するのが大変なのと、immが命令タイプに応じて異なるため、ハンドアセンブルが厳しくなったからです。人為的ミスの低減にもなるはずです。
immの種類多すぎでは
今更感が非常に強いですが、immを整理しました。
命令セットのImmに対応するビット位置がまちまちなので、それをここで統一しました。
CSR関係のzimmもここで整理しました。なお、参考にしたのはこちら。
ざっとこんな感じです。riscv-miniとほとんど変わらない実装
object ImmGen { def apply(sel: UInt, inst: UInt): SInt = { val sign = Mux(sel === IMM_Z, 0.S, inst(31).asSInt) val b30_20 = Mux(sel === IMM_U, inst(30,20).asSInt, sign) val b19_12 = Mux(sel =/= IMM_U && sel =/= IMM_J, sign, inst(19,12).asSInt) val b11 = Mux(sel === IMM_U || sel === IMM_Z, 0.S, Mux(sel === IMM_J, inst(20).asSInt, Mux(sel === IMM_B, inst(7).asSInt, sign))) val b10_5 = Mux(sel === IMM_U || sel === IMM_Z, 0.U, inst(30,25)) val b4_1 = Mux(sel === IMM_U, 0.U, Mux(sel === IMM_S || sel === IMM_B, inst(11,8), Mux(sel === IMM_Z, inst(19,16), inst(24,21)))) val b0 = Mux(sel === IMM_S, inst(7), Mux(sel === IMM_I, inst(20), Mux(sel === IMM_Z, inst(15), 0.U))) Cat(sign, b30_20, b19_12, b11, b10_5, b4_1, b0).asSInt }
分岐命令
実装した分岐命令は以下に挙げる通りです。WLEN
は命令1ワードあたりのアドレスバイト長で、今回は4(=32bit)です。imm_X
はXタイプの即値immという意味です。
疑似コード
rd = pc + WLEN, pc = pc + imm_J
- jalr
疑似コード
rd = pc + WLEN, pc = rs1 + imm_I
- beq
疑似コード
if (rs1 == rs2) then pc = pc + imm_B else pc = pc + WLEN
- bne
疑似コード
if(rs1 =/= rs2)then pc = pc + imm_B else pc = pc + WLEN
- blt
疑似コード
if(rs1 < rs2) then pc = pc + imm_B else pc = pc + WLEN
- bltu
疑似コード
if(rs1.asUInt < rs2.asUInt) then pc = pc + imm_B else pc = pc + WLEN
- bge
疑似コード
if(rs1 >= rs2)then pc = pc + imm_B else pc = pc + WLEN
- bgeu
疑似コード
if(rs1.asUInt >= rs2.asUInt)then pc = pc + imm else pc = pc + WLEN
前回のコミットと比較し、すべての分岐命令においてALUを使うように修正しました。回路規模肥大化の抑制を期待できます。
バブルロジックの導入
next_inst_is_valid
が真の時はそのまま、偽の時は次アドレスの命令をバブルします。
JAL、JALR命令は特に条件なく分岐をしてくれるので、この命令があった場合は次にフェッチしている命令語をバブルします。
Bから始まる分岐命令の場合、条件が真となれば次段でフェッチしている命令をバブルします。偽の場合は分岐しませんので命令はそのまま残します。
// bubble logic next_inst_is_valid := true.B switch (id_ctrl.br_type) { is( BR_NE ) { when(val_rs1 =/= val_rs2) { next_inst_is_valid.:=(false.B)} // NEQ = true: bubble next inst & branch .otherwise { next_inst_is_valid.:=(true.B) } } is( BR_EQ ) { when(val_rs1 === val_rs2) { next_inst_is_valid.:=(false.B)} // EQ = true: bubble next inst & branch .otherwise { next_inst_is_valid.:=(true.B) } } is( BR_GE ) { when(val_rs1 > val_rs2) { next_inst_is_valid.:=(false.B)} // GE = true: bubble next inst & branc .otherwise { next_inst_is_valid.:=(true.B) } } is( BR_GEU ) { when(val_rs1.asUInt > val_rs2.asUInt) { next_inst_is_valid.:=(false.B)} // GE = true: bubble next inst & branch .otherwise { next_inst_is_valid.:=(true.B) } } is( BR_LT ) { when(val_rs1 < val_rs2){ next_inst_is_valid.:=(false.B)} // LT = true: bubble next inst & branch .otherwise { next_inst_is_valid.:=(true.B) } } is( BR_LTU ) { when(val_rs1.asUInt() < val_rs2.asUInt) { next_inst_is_valid.:=(false.B)} // LT = true: bubble next inst & branch .otherwise { next_inst_is_valid.:=(true.B) } } is( BR_J ) { next_inst_is_valid.:=(false.B) } // JAL is( BR_JR ) { next_inst_is_valid.:=(false.B) } // JALR }
命令語を間違えて解釈していた
Uが末尾につくアセンブラ命令。bltuやbgeu。
immがunsignedだと本気で誤解していました。
逝ってこいレジスタ。負が扱えないので帰ってこれない。すごい。作ってみてもいいかもですね。(ダメだろ)
正確には、rs1、rs2を比較時にunsignedとして扱う命令でした。ですよね。。immだったら、なんのためにauipc
命令あるんだよという話になります。
JAL/JALR命令のテスト
そんなこんないろいろ間違いを治しつつ、テストコードを書きました。
(ここに掲載したのはjalとjalrだけです。bから始まる分岐命令も一応動作を確認済みです)
早くriscv-testを走らせられるレベルになりたい。
_start0: nop nop _label1: addi x1, x0, 1 # x1 = x0 + 1 = 1 addi x2, x0, 2 # x2 = x0 + 1 = 2 addi x3, x0, 3 # x3 = x0 + 2 = 3 jal x4, _label4 # x4に次アドレス(_lavel2)を格納し, _label4へジャンプ _label2: addi x7, x0, 7 # x7 = 7 addi x8, x0, 8 # x8 = 8 (RETURN HERE) addi x9, x0, 9 # x9 = 9 addi x10, x0, 10 # x10= 0x0A addi x11, x0, 11 # x11= 0x0B _label3: jalr x0, x5,0 # x5に格納されているアドレス(= _label5)へジャンプ _label4: addi x12,x0,12 # x12= 0x0C jalr x5, x4, 4 # x5に次アドレス(_lavel5)を格納し, x4(= _label2) + 4のアドレス( addi x8, x0, 8のところ)へジャンプ→(addi x7, x0, 7)は実行されない _label5: jal x0, _label5 # 無限ループ
結果を見てみます。intelliJであれば画面下部のterminalのタブで
make clean make test
で動作します。
実行結果。
コード全文
コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
git clone http://github.com/panda5mt/KyogenRV -b 0.0.10.3 --depth 1 cd KyogenRV/
macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。
brew install coreutils
makefileでOSによってod/godをスイッチしています。
アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。
nano src/sw/test.s
次回はデータRAMの実装に入ります。
Chiselを使ったRISC-Vの勉強(4. プロジェクトへの機械語の読み込みの自動化)
今更ながら環境を整える
VSCodeにChiselプラグインを入れつつ頑張っていたのですが、IDEがオブジェクトやクラスの階層を把握していないため、補完機能が乏しいことなどから限界を感じてました。
いつも参考にさせていただいている、diningyo氏(id:diningyo-kpuku-jougeki)のブログ
Scalaの勉強 - 統合開発環境IntelliJ IDEAのインストール - ハードウェアの気になるあれこれ
というわけでIntelliJ IDEAに乗り換えました。すごく快適。最初っからこうしていればよかった。
機械語の生成
いままでCPUロジックに与えるhexはハンドアセンブルしていましたが、そろそろ限界を感じています。
例えば、JAL命令。
こんなの毎回のように手動アセンブラやってたら脳が停止してしまいます。それに、間違った機械語を記述していたのに気づかずデコーダを作ったら収拾がつかなくなります。
これに関してはみなさん工夫されているようです。
RISC-V(RV32I)のアセンブリから機械語への翻訳(簡易) - Qiita
a163236氏によるscala記述のアセンブラです。機械語がバイナリ文字列で出力されるので、コピペでChiselプロジェクトに貼り付けられるのが魅力。
さて、自分はscalaでアセンブラとか記述できないので、実直にriscv64-unknown-elf-asとodコマンドを用いる方向にしました。
参考1:Chisel-Templateを使ってオリジナルデザインを作ってみるチュートリアル (3. CPUのコアのDebug-Trace作成) - FPGA開発日記
参考2:【 od 】コマンド――ファイルを8進数や16進数でダンプする:Linux基本コマンドTips(93) - @IT
Linuxやwsl環境で32bit(=4byte)長ごとに改行された可読できる機械語を出力するには以下のようにします。(アセンブラファイルはtest.s、欲しいファイルはtest.hexという名前とします。)
riscv64-unknown-elf-as test.s -o test.o riscv64-unknown-elf-objcopy --gap-fill 0 -O binary test.o test.bin od -An -tx4 -w4 test.bin >> test.hex
test.hexはこんな感じになります。
00000013 00100093 00100113 00200193 00300213 00400293 00500313 00600393 00700413 01b00e13 01c00e93 ….
1行あたり32bitで、16進数表記です。
これならコピペしたり、scalaからSource.fromFile
でオープンすることも可能です。
このブログの最後に明記しますが、Makefileを記述したので、make asm
コマンドでワンストップで生成できます。
Scalaからこのファイルをオープンするにはどうしたらいいでしょう。
以下のようにしました。 間違ってたらご指摘ください。
val s = Source.fromFile("test.hex") var bufs :Array[String] = _ try { bufs = s.getLines.toArray } finally { s.close() } // このあと、Arrayから読み出すコードを記述する
ここまでのコード
コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。
brew install coreutils
BSD odと区別するためなのはわかってるつもりですけど、GNU odをgodってするのどうかと思いますね………。
どうでもいいですけど。
git clone http://github.com/panda5mt/KyogenRV -b 0.0.10 --depth 1 cd KyogenRV/
アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。
nano src/sw/test.s
------- 補足:riscv-gnu-toolchainの導入(まだ導入していない人向け) -------
今回のコードからriscv-gnu-toolchainが必須となります。
こちらからソースからビルドした方がいいかも。
GitHub - riscv/riscv-gnu-toolchain: GNU toolchain for RISC-V, including GCC
パッケージ管理がディストリビューションごとに違うので、依存ファイルの導入はMacの場合を書きます。
brew install python3 gawk gnu-sed gmp mpfr libmpc isl zlib expat coreutils
riscv-gnu-toolchainを導入します。
git clone --recursive https://github.com/riscv/riscv-gnu-toolchain cd riscv-gnu-toolchain git submodule update --init --recursive ./configure --prefix=/opt/riscv --enable-multilib make
PATHを/opt/riscv/binに通します。
nano ~/.bash_profile # または nano ~/.bashrc
.bashrcまたは.bash_profileに以下を追加
export PATH=$PATH:/opt/riscv/bin
source ~/.bash_profile
またはsource ~/.bashrc
でPATHを通します。
------- 補足終わり -------
保存後、プロジェクトのルートフォルダで
make asm
するとアセンブルがはじまります。
また依存関係の解決をMakefileにさせているのでtest.sに変更があった場合は
make test
したときに同時にアセンブルも行います。(riscv-gnu-toolchainが導入されていて、PATHが通っていることが前提です。)
ビルドが完了すると(project_root)/src/sw/test.hex
が生成されます。
話題は変わりますが、上記プロジェクトには分岐命令の一部を実装しています。
この分岐命令の話などは、また次回の記事で。
Chiselを使ったRISC-Vの勉強(3. ALUの実装とIDステージの改良)
githubを巡回してみていろいろな実装を見て作戦を練っていました。
これまでRISCVの命令セットを見る限り、opcode(7bit) + funct3(3bit)で判別して適当に実装できるかなと考えていました。(10bitなら単純計算で1023命令は用意できるし)
- ALUへの演算命令、2入力への変数指定、結果の格納先(汎用レジスタ、pc、データメモリなど)指定
- データメモリ書き込みイネーブル、アドレス指定、読み込みイネーブル、アドレス指定
- CSR周りのゴニョゴニョ
- CPUストール制御信号
- 分岐制御信号
をIDステージで書けばいいかなと書き始めていました。
マイクロコードの記述方法の調査
IDステージの実装方法を調べているといろいろな実装が見つかります。opcodeから実直にimm,rs1,rs2,funct3を分けて実装しているのが、diningyo氏 (id:diningyo-kpuku-jougeki) のdirv
https://github.com/diningyo/dirv/blob/master/src/main/scala/dirv/pipeline/Idu.scala
とっても簡潔に記述されています。
氏のdirvはステージごとに綺麗に分かれていてパイプラインを組むときにとても作業がしやすそうです。実際、dirvトップモジュールはそれぞれのステージをConnectするだけの記述になっています。
しかし、いろんなrisc-v実装を見てると、マイクロコードはほとんどの実装で示し合わせたようにこんな実装(全く全てが同じではないですが、雰囲気としては同じ)をしてました。
class IDecode { val table: Array[(BitPat, List[BitPat])] = Array( LW -> List(Y, BR_N , OP1_RS1, OP2_IMI , ALU_ADD , WB_MEM, REN_1, MEN_1, M_XRD, MT_W) , LB -> List(Y, BR_N , OP1_RS1, OP2_IMI , ALU_ADD , WB_MEM, REN_1, MEN_1, M_XRD, MT_B ), LBU -> List(Y, BR_N , OP1_RS1, OP2_IMI , ALU_ADD , WB_MEM, REN_1, MEN_1, M_XRD, MT_BU ), LH -> List(Y, BR_N , OP1_RS1, OP2_IMI , ALU_ADD , WB_MEM, REN_1, MEN_1, M_XRD, MT_H ), LHU -> List(Y, BR_N , OP1_RS1, OP2_IMI , ALU_ADD , WB_MEM, REN_1, MEN_1, M_XRD, MT_HU ), …… )}
分岐制御、ALUの入力指定、IMMの種類、ALUでの演算指定、メモリ書き戻し、レジスタ書き戻し、などの指定がListで格納されています。
本家Rocket-chipなどがこの実装だから、なのはわかってたのですが、いろいろ調べてました。
lowRISCというプロジェクトがあり、メモリなどのカスタマイズができるようです。その際にこの体裁で拡張やカスタマイズを進めていくようなのでこの手順を真似しておいた方が今後新たにSoCの設計するときに有利だと思いました。
参考:Adding HW/SW support for the load and store tag instructions · lowRISC: Collaborative open silicon engineering
命令デコード(ID)ステージの実装
自分が参考するにあたって、上記のような手法でIDステージを記述しており、かつChisel3.2以降*1に対応したものはないか、さがしていました。
これが良さげ。
GitHub - ucb-bar/riscv-mini: Simple RISC-V 3-stage Pipeline in Chisel
Chisel3で記述されていて、かなりシンプルでした。これを参考に組み立てていきます。
方針としては、
- RV32Iに対応しているアセンブラのIDステージのみ移植し、いま自作しているプロジェクトにフィットするよう書き換える。
- ALUは32bit固定にして、わかりやすく記述。今後対応しきれないようであれば都度書き換え。
という感じで行くことににしました。
ALUの実装はこんな感じです。へんなところで頑張らない。足し算引き算、(排他的)論理和、論理積、シフトくらい。
ざっと見てVerilogコードがなんとなくでも想像できる形。(一部抜粋)
class ALU extends Module { val io = IO (new Bundle { val op1 = Input(UInt(32.W)) val op2 = Input(UInt(32.W)) val alu_op = Input(UInt(4.W)) val out = Output(UInt(32.W)) }) val shamt = io.op2(4,0).asUInt val w_out = Wire(UInt(32.W)) w_out := MuxLookup(io.alu_op, io.op2, Seq( ALU_ADD -> (io.op1 + io.op2), ALU_SUB -> (io.op1 - io.op2), ALU_SRA -> (io.op1.asSInt >> shamt).asUInt, ALU_SRL -> (io.op1 >> shamt), ALU_SLL -> (io.op1 << shamt), ALU_SLT -> (io.op1.asSInt < io.op2.asSInt), ALU_SLTU -> (io.op1 < io.op2), ALU_AND -> (io.op1 & io.op2), ALU_OR -> (io.op1 | io.op2), ALU_XOR -> (io.op1 ^ io.op2), ALU_COPY1 -> io.op1)) io.out := w_out }
30行足らず。わーい。
このALUをCPU側のモジュールにインスタンシエートし、入力出力をIDステージのフラグから判別してやればOK
ALUの入力セレクタは今のところこんな実装。I-type命令じゃない即値immなどは真面目に実装していないのでこれから頑張る。
// ALU OP1 selector val ex_op1 = MuxLookup(id_ctrl.alu_op1, 0.U(32.W), Seq( OP1_RS1 -> rv32i_reg(idm.io.inst.rs1), OP1_IMU -> 0.U(32.W), // DUMMY OP1_IMZ -> 0.U(32.W) // DUMMY ) ) // ALU OP2 selector val ex_op2 = MuxLookup(id_ctrl.alu_op2, 0.U(32.W), Seq( OP2_RS2 -> rv32i_reg(idm.io.inst.rs2), OP2_IMI -> idm.io.inst.imm, OP2_IMS -> 0.U(32.W) // DUMMY ) ) // ALUの入力にConnectし、ALUのオペレーションを4bit長で指示 import ALU._ val alu = Module(new ALU) alu.io.alu_op := id_ctrl.alu_func alu.io.op1 := ex_op1 alu.io.op2 := ex_op2
ALUの出力先はpcとかデータメモリとか色々あるはずですが、今回はまだ汎用レジスタだけ実装しています。
when (rf_wen === REN_1){ // 汎用レジスタ書き込みイネーブルであれば when (rd_addr =/= 0.U){ rv32i_reg(rd_addr) := alu.io.out // 汎用レジスタに書き込み }.otherwise { // rd_addr = 0 rv32i_reg(0.U) := 0.U(32.W) // ゼロレジスタが指定されていたら、強制的にゼロを書き込む(nop命令などの用途) } }
ALUを使い、かつ汎用レジスタのみで読み書きが完結する命令はI,およびR-typeの命令です。
- rd,rs1,imm(12bit)を使う命令
- rd,rs1,imm(5bit)を使う命令
- rd,rs1,rs2を使う命令
それぞれから1命令づつピックアップしてハンドアセンブルして、結果が期待された値となっているか確認。
- addi
- slli
- add
をピックアップ。
下記のように、バイナリを記述して、汎用レジスタの結果を見ます。
(中略) 0x00A00593L, // addi x11,x0,10 (x11 = 0 + 10 = 10) 0x00259093L, // slli x1,x11,2 (x1 = x11 << 2 = 40) 0x00B00613L, // addi x12,x0,11 (x12 = 11) 0x00C581B3L, // add x3,x11,x12 (x3 = x11 + x12 = 10 + 11 = 21 = 0x15)
期待される結果は、
x1 = 40(= 0x28)
x3 = 21(= 0x15)
x11 = 10(= 0x0A)
x12 = 11( = 0x0B)
です。
結果:
コード全文
コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
git clone http://github.com/panda5mt/KyogenRV -b 0.0.9.1 --depth 1
*1:初期の頃に記述されたものはChisel2以前のバージョンで記述しているサンプルも多いため
Chiselを使ったRISC-Vの勉強(2)
汎用レジスタ(x0-x31)の実装
RISC-V(RV32I)の汎用レジスタは32bit長で32個あります。
https://content.riscv.org/wp-content/uploads/2017/05/riscv-spec-v2.2.pdf
まずRISC-Vの命令デコードには必須なのでこちらを用意します。
Chiselだとレジスタ宣言、ゼロ初期化まで1行でかけます。x0 = zero registerで固定ですが、めんどくさいのでx1-x31とまとめて確保しました。
val rv32i_reg = RegInit(VecInit(Seq.fill(32)(0.U(32.W)))) // x0 - x31:All zero initialized
デバッグトレース
今のところ、デバッグトレースで必要なものは何かを考え、テストオブジェクトに入出力できるようにしました。
- PC
- Program counterの読み書きができるようにしました。halt信号(後述)をtrueにしてから書き換え推奨。
- 命令メモリ
- 前回の記事で既に実装済み。
(ID)命令デコードの実装
力弱く、addiだけ実装しました。これを実装すると、上記で用意したx0-x31までのテストができるからです。
以下のようなアセンブラを記述します。
addi x1, x0, 0; // x1 = x0 + 0 (=0) addi x2, x0, 1; // x2 = x0 + 1 (=1) addi x3, x0, 2; // x3 = x0 + 2 (=2) addi x4, x0, 3; // x4 = x0 + 3 (=3) …. addi x31, x0, 1E; x31 = x0 + 0x1E (=30)
ここまで説明しておいてアレですけどアセンブラ流し込むスクリプトを記述してなかったので(おい)、、機械語で書きましょう。
AddiはRISC-Vの中でI-Typeの命令語で12bitの即値(imm)、1つの代入元(ソース)レジスタ(rs1)、1つの代入先(デスティネーション)レジスタ(rd)から構成されます。rs1はx0~x31、rdはx1~x31上にあるいずれかのレジスタ*1となります。
b????_????_????_????_?000_????_?001_0011
であれば良いことがわかります。16進数で表記すると、
0x00000093, // addi x1,x0,0 (x1 = x0 + 0 = 0) 0x00100113, // addi x2,x0,1 (x2 = x0 + 1 = 1) 0x00200193, // addi x3,x0,2 (x3 = x0 + 2 = 2) 0x00300213, // addi x4,x0,3 (x4 = x0 + 3 = 3) .... 0x01E00F93 // addi x31,x0,30
となります。うっひょ、マシン語とか超楽しいマジ無理。早くアセンブラからバイナリ流し込む機能実装しないと。
Chiselで機械語の判定はBitPatを使うと簡単にできます。
when (inst_code === BitPat("b????_????_????_????_?000_????_?001_0011")){ // ADDI = imm12=[31:20], src=[19:15], funct3=[14:12], rd=[11:7], opcode=[6:0] val dest = inst_code(11,7) // rd-pointer val imm = inst_code(31,20) // imm val src = inst_code(19,15) // src-pointer val dsrc = rv32i_reg(src) //rs when (0.U < dest && dest < 32.U){ //アドレスは1~31か? rv32i_reg(dest) := dsrc + imm } }
と言った感じです。
テストコードを書き直します。1部抜粋です。
poke(c.io.sw.halt, true.B) // CPUを停止 step(1) for (addr <- 0 to (memarray.length * 4 - 1) by 4){ poke(c.io.sw.wAddr, addr) poke(c.io.sw.wData, memarray(addr/4)) // 指定アドレスに機械語をロード println(f"write: addr = 0x${addr}%08X, data = 0x${memarray(addr/4)}%08X") step(1) } poke(c.io.sw.w_pc, 0) // プログラムカウンタをゼロに戻す=reset step(1) // fetch pc poke(c.io.sw.halt, false.B) // CPUを再開させる step(1) for (lp <- 0 to (memarray.length - 1) by 1){ val a = peek(c.io.sw.addr) val d = peek(c.io.sw.data) println(f"read : addr = 0x$a%08X, data = 0x$d%08X") // メモリ上の機械語のダンプ step(1) } step(1) poke(c.io.sw.halt, true.B) step(1) for (lp <- 0 to 31 by 1){ poke(c.io.sw.gAddr, lp) step(1) val d = peek(c.io.sw.gData) println(f"read : x$lp%2d = 0x$d%08X") //x0 ~ x31レジスタの内容をダンプ step(1) }
動作確認
コード全文
コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
git clone http://github.com/panda5mt/KyogenRV -b 0.0.4 --depth 1
きょうはここまで。