この記事は、木更津高専 Advent Calendar 20215日目の記事です。
前の記事:入門じゃない固有値/固有ベクトル(固有値の諸定理)
次の記事:友達の研究室にArduinoで動くカードキーを設置した話
はじめに
最近、RISC-VとChiselで学ぶ はじめてのCPU自作という本を始めました。
この本は、最近流行っているRISC-VのCPUを、ChiselというScalaを元にしたハードウェア記述言語で自作する本です。ソフトウェア上のシミュレーションのみを行うのでFPGA等のハードウェアを準備する必要がない分とっつきやすいかもしれません。
第三部のパイプラインの実装まで終わり、できたところまでをFPGAに実装しようとして詰まってしまったので、とりあえずここまでの感想を残しておこうと思います。
第1部 基礎知識
1章・2章は論理回路やコンピュータアーキテクチャの知識があれば読み飛ばしても大丈夫そうですが、面白いコラムがあったりしたので自分は読みました。
3章はChiselの入門で、一通り目を通したら文法がわからない時に戻ってくるという感じでOKだと思います。自分で拡張を加えようとするときはchisel-bookが日本語訳されているのでこちらも参照するといいかもしれません。
第2部 簡単なCPUの実装
ここからCPUの実装に入ります。開発環境はDockerで構築します。Dockerに慣れていなかったのですが、しっかり説明があったので大丈夫でした。(結構便利だったのでこれからも使っていきたいかも)
このあたりは段々と使える命令が増えていったり、実装がスマートになっていくのが楽しいです。個人的にはデコーダーがListLookupで綺麗にまとまったのが気持ちいいポイントでした。
あと、Chiselのビットパターンが以下のようにDon't Careを書くことができて便利だなーと思ったのですが、Verilogにもcasex文/casez文という物があるらしいことをあとから知りました。
val ADD = BitPat("b0000000??????????000?????0110011")
21章ではRISC-V向けのビルドツール(gcc・ldなど)の使い方も書かれているので参考になると思います。
第3部 パイプライン化
知識としてパイプラインの仕組みは知っていましたが、各種ハザードの解消方法がいまいち理解できていなかったのでとても勉強になりました。
パイプラインレジスタを挟んだだけではハザードが発生していたCPUがだんだんパイプラインに対応していくところがおもしろポイントです。ストール時に信号が染み渡っていく感じ?がハードウェアを作ってる感があってよかったです。
感想はここまで。。。
実機で動かしたい
ひとまず命令を実行できるものはできたのでFPGA上で動かしたい欲が出てきました。
ド素人なので、Verilogコードを吐き出してFPGAに書き込むだけ!と思っていましたが全くそんなことはなくてめっちゃ詰まってます…
ということで、ここから先は今詰まっているところのメモです。
Verilogへの変換
Verilog化するには以下のようなクラスを定義します。
object CpuTop extends App {
(new chisel3.stage.ChiselStage).emitVerilog(new Top(), Array("--target-dir", "generated"))
}
sbt "runMain [package名].CpuTop"
を実行するとgeneratedディレクトリの下にTop.vが生成されました。
chisel-bookにはchisel3.Driver.execute()
で生成できるとありましたが、非推奨だということでsbtに怒られます…
emitVerilog()
ではなくemitSystemVerilog()
にするとSystemVerilogも生成できるみたいです。
生成された1200行くらいのVerilogコードを眺めるとChiselでは隠れていたwire文やらassign文やらが大量に生成されていて目ン玉が飛び出ました。Chiselありがたや。
とりあえずVerilogの生成はできました。でもここで問題発生。
メモリの初期化問題
Memory.scalaのloadMemoryFromFile()
の部分がTop.Memory.mem.vに出力され、Vivadoで読み込むと最後のbind文が文法エラーになっていました。
module BindsTo_0_Memory(
input clock,
input [31:0] io_imem_addr,
output [31:0] io_imem_inst,
input [31:0] io_dmem_addr,
output [31:0] io_dmem_rdata,
input io_dmem_wen,
input [31:0] io_dmem_wdata
);
initial begin
$readmemh("program.hex", Memory.mem);
end
endmodule
// ↓ ??????
bind Memory BindsTo_0_Memory BindsTo_0_Memory_Inst(.*);
どうやらSystemVerilogにはbindというキーワードがあるみたいなのですが、よくわかりません…(調べ中)
同期メモリの使用
もう一つ問題発生。ここまでMem()
でメモリを生成してきましたが、このままだとメモリがレジスタとして生成されロジックを大量に消費してしまうみたいです。この問題はSyncReadMem()
を使ってFPGA上のメモリリソースを使うようにすれば解決できるようなので変更を加えます。
//val mem = Mem(16384, UInt(8.W))
val mem = SyncReadMem(16384, UInt(8.W))
これで問題解決ではなく、FPGA上の同期メモリはアドレスを指定して即データがもらえるわけではなく数クロック待たされます。
そのため、
- データが有効かのフラグをCPU・メモリに実装する
- データが有効でない場合はストールする
ように変更を加える必要があります。(作業中)
このままだと命令フェッチで毎クロックストールするので、0クロックで読み込むことのできるキャッシュを追加したいです。(願望)
おわり
すごく雑な記事になってしまいましたが最後まで見ていただきありがとうございます。
初心者なので間違ったことを言っていたら優しく教えていただけると嬉しいです。
色々と調べながら試行錯誤中なので出来上がったらしっかりした記事を書くかもしれないです。