FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

「RISC-VとChiselで学ぶはじめてのCPU自作」を献本頂きました

最近発売になった、「RISC-VとChiselで学ぶはじめてのCPU自作」を献本頂きました。@ciniml 様、ありがとうございます。

私はChiselもRISC-Vもそこそこ知っていたので、完全に門外漢というわけではない。昨日入手したばかりなので詳細に読み込んでいるわけではないが、ざっくりと流れを読んでみた。

まずは、かずあるCPU自作の本の中で、「RISC-V」にターゲットを当て、「開発言語をChiselとする」ものはかなり限られる。 今秋発売になる予定のHarris & Harris がどの言語を使うつもりなのかが分からないが、Chisel+RISC-Vの自作CPU本で日本初であることは間違いない。

「なぜSystemVerilogではなくChiselで書いたのか」という所も知りたかったがざっと読んだ限りは書いていなかった。個人的にはChiselでセセコマシイCPUのパイプラインなどを書くのは、少し役者不足じゃないか(SiFiveに失礼だが)と思うところもある。ChiselがSiFive以外に普及しない理由もここらへんにある気がする。 (ちなみに、解説してあるChiselの文法は基本的なものなのでこれでRocket-Chipが読めるようになるわけではない。念のため)。

仮にSystemVerilogで同じ内容を書くと、5段パイプラインくらいなら、自作CPU本はかなりの数が出てきているので目新しさが無くなってしまう。そういう意味で、本書は少しニッチなところを狙っているようにも思える(ベクトル命令を解説しているあたりも、キワキワを攻めたいという意図を感じる)。

本書は大きく分けて以下のような構成になっていた。

  • コンピュータアーキテクチャについて
  • ScalaとChiselについて
  • シングルステージのCPUの実装
  • パイプラインCPUの実装
  • ベクトル命令の一部を実装
  • カスタム命令の実装

(個人的には、途中で出てくるDockerの説明が非常に役に立った。Docker全く知らなかったので)。

本書には「シミュレーション波形」というものが全く登場しない

自作CPUといえばRTLシミュレーションからの波形確認でしょ!というのは定番すぎるかもしれないが、本書では驚くべきことに「RTLシミュレーション」とか「波形」というものが一切登場しない。

最初からChiselTestを使っているあたり、最初からVerilogを生成してRTLシミュレーションすることなんて全く考えていないと思う。すべての動作確認はテキストメッセージの確認だけで行われているのは、かなり新しい。 (このことについては本書内でコメントされていなかったが、著者のポリシーだろうか?)

波形を使わない、というのはメリットとデメリットがあって:

  • デメリット: CPUが動いているという実感がわかない
  • メリット: 何となく敷居が低い

だからFPGAとかそういう話にもならないし、ハードウェア設計経験者なら「本当にこれ回路になってんのかいな?」という不安は残る気がする。まあそれがChiselである1

そういう意味では、ハードウェア本に多い図やタイミングダイアグラムなども本書はほぼ無いに等しい。最初の実装ではどのモジュールを実装しているのか一発で想像できなかった。まあそれはしっかりと読みこめば分かる。

という訳で、本書はChiselというキワキワの言語を使ってRISC-Vの自作CPUというキワキワを攻める、かなりマニアックな本だが、RISC-Vに興味がある人はぜひ読んでみると面白いと思う。


  1. ちゃんと前書きに「物足りなく感じてしまうかもしれない方々:HDLやプロセッサの設計知識がある人」と書いてあった。

自作CPUをVivado Simulatorでシミュレーションするための試行

これまで基本的に自作CPUのシミュレーションはVerilatorを使っていた。これには理由がある。 - ある程度の規模のデザインならばコンパイル時間も気にならない。 - SpikeとのDPI接続などの機能が豊富 - かなり攻めたSystemVerilogの構文もサポートしている

しかし最初の項目「デザインのコンパイル時間」が耐えきれないレベルになってきた。 シミュレーションする対象の規模がかなり大きくなってきており、1回のコンパイルで30分以上かかるようになっている。

さすがにまずいのでVivado Simulatorの検討を始めた。

しかしVivado Simulatorの方もかなり問題があり、

  • WDBという意味不明な波形フォーマットをサポートしているビューアがVivado Simulatorしかない(FSTに対応してほしい)
  • Vivado Simulatorの波形ビューアがかなりイマイチ

一応Vivado SimulatorにSpikeとのステップ実行も併せてVerilatorの環境をそのまま移行できた。DPI-Cの取り扱いについてはXSCコマンドを使ってどうにかする。

 XSC_ARGS += -mt off
=# XSC_ARGS += -v 1
 XSC_ARGS += -compile
 XSC_ARGS += --cppversion 14
 XSC_ARGS += $(CPP_FILELIST)
 XSC_ARGS += -gcc_link_options "-L/usr/lib64 -lelf $(addprefix -I, $(INC_DIR))"
 XSC_ARGS += $(addprefix -gcc_compile_options "-I, $(addsuffix ", $(INC_DIR)))
 XSC_ARGS += $(addprefix -gcc_compile_options "-D, $(addsuffix ", $(DEFINE)))

 XSC2_ARGS += -mt off
=# XSC2_ARGS += -v 1
 XSC2_ARGS += -compile
 XSC2_ARGS += --cppversion 14
 XSC2_ARGS += $(ELF_LOADER)
 XSC2_ARGS += -gcc_link_options "-L/usr/lib64 -lelf $(addprefix -I, $(INC_DIR))"
 XSC2_ARGS += $(addprefix -gcc_compile_options "-I, $(addsuffix ", $(INC2_DIR)))
 XSC2_ARGS += $(addprefix -gcc_compile_options "-D, $(addsuffix ", $(DEFINE)))

 XSC_LINK_ARGS += -shared
 XSC_LINK_ARGS += -gcc_link_options "-L/usr/lib64 $(addprefix -L, $(INC_DIR)) -lelf -lriscv -lsoftfloat -ldisasm -lfesvr -lfdt -ldl -lpthread $(addprefix -I, $(INC_DIR))"
=# XSC_LINK_ARGS += -v 1

 sim_vivado: $(DECODE_FILES) $(FILELIST)
     $(XSC) $(XSC2_ARGS) | tee sim.log
     $(XSC) $(XSC_ARGS) | tee -a sim.log
     $(XSC) $(XSC_LINK_ARGS) | tee -a sim.log
     $(XVLOG) $(XVLOG_ARGS) ../src/riscv64_pkg.sv ../src/msrh_standard_conf_pkg.sv -f $(FILELIST) $(FILE_TEST) $(FILE_SRC) | tee -a sim.log
     $(XELAB) $(XELAB_ARGS) tb | tee -a sim.log
     DUMP_TYPE=$(DUMP) $(XSIM) $(XSIM_ARGS) "work.tb" | tee -a sim.log

XSCでSpike付きのライブラリをコンパイルできるようになるまで苦労したが、どうにか動くようになった。 ただしVivado波形データを見るのは相変わらずつらいなあ...

VCDも一応出力できるけど、VerilatorのFSTの階層と比べるとかなり使いずらい。これは使い続けるかどうか、迷うなあ...

AMBA5 の CHI (Coherence Hub Interface)についての調査 (6. コヒーレンスプロトコル2)

developer.arm.com

developer.arm.com

インターコネクト間のプロトコルフローについて

次に、CHIのプロトコルフローについて見ていく。

Read Transaction flows

Read transactions with DMT and without snoops

DMTというのはDirect Memory Transferであるということを思い出しておく。つまりリクエスタの要求に対して、データはHomeノードを介さず直接変換される。

  1. RN-FがHN-FにReadリクエストを送る
  2. HN-FがSN-FにReadリクエストを送る
    • ReadリクエストのIDフィールドは送られるデータレスポンスをベースにしている。データはリクエスタもしくはHN-Fに返される。
  3. SN-FはデータレスポンスをRN-Fに直接返信する
  4. 最初のリクエストがReadSharedの場合、RN-FはHN-FにCompAckを送信する。
f:id:msyksphinz:20210815231655p:plain:w400

この図を見てわかる通り、RN-Fのキャッシュラインの状態はI(Invalid)からUC(Unique Clean)状態になる。

Read transactions with DMT and with snoops

DMTにおけるスヌープ付きのリードリクエストについてフローを確認する。このためには、ReadSharedトランザクションを送出する。

下記のダイアグラムにおいて、ノードの役割は以下のとおりである。

  • RN-F0 : リクエストノード。リクエストを送出する。
  • RN-F1 : リクエストノード。今回はリクエストを送出せず、ホームノードからスヌープを受け取る。
  • HN-F : ホームノード。スヌープ処理とスレーブへのリクエスト送出を取り扱う。
  • SN-F : スレーブノード。データリクエストを受け取りレスポンスを返す。

いかのような順序でフローが実行される。

  1. RN-F0がHN-Fにリードリクエストを送出する。
  2. HN-FがスヌープリクエストをRN-F1に送出する。
  3. RN-F1からスヌープレスポンスを受け取り、RN-F1にデータが存在しないことを確認した後、HN-FがSN-Fに対してリードリクエストを送出する。
  4. SN-FはデータレスポンスをRN-F0に送出する。
  5. RN-F0はCompAckをHN-Fに送出し、ReadSharedのリクエストを完了させる。
f:id:msyksphinz:20210817235553p:plain:w400

read transaction with DCT

DCTはダイレクトキャッシュ転送(Direct Cache Transfer)を用いるタイプの転送について。

ここでのポイントは、RN-F1のスヌープリクエストに対するレスポンスが、直接RN-F0に返されるということである。

  1. RN-F0はホームノードHN-FにReadSharedを送出する。
  2. HN-FはSnpSharedFwdを送出して、RN-F1に対してフォワーディングスヌープリクエストを転送する。
  3. RN-F1のキャッシュライン状態はUCからSCに更新される。
  4. RN-F1はCompData_SCをRN-F0に転送する。
  5. RN-F1はSnpResp_SC_Fwded_SC スヌープをリクエストをHN-Fに送出する。つまり、
    • データはリクエスタにフォワードされた。
    • 最終的なキャッシュライン状態はSCに変更された。
    • 新たにキャッシュラインを取得したノードはそのキャッシュ状態をSCに変更した。
  6. CompDataレスポンスをRN-F0が送信し、HN-Fは転送を完了する。
f:id:msyksphinz:20210817235618p:plain:w400

RISC-Vの命令拡張に関するポリシに関して

RISC-Vの仕様書を一から読みなおすことを地道に続けている。RISC-Vの仕様自体についてはあまり言及することはないのだが、仕様書には面白いことに「RISC-Vの拡張」関する取り決めをした章が設けられている。"Chapter26 Extending RISC-V"がそれに相当する。

そもそもRISC-Vは、「ベースISA」である"RV32I", "RV64I",に対して拡張を搭載することで機能を拡充する、というポリシを取っている。 つまり、最も基本的な整数演算命令以外はすべて拡張機能であると決められている。この拡張機能には大きく2種類が取り決められていて、

  • 標準拡張 (Standard Extension) : MAFDQLCBTPVがそれに相当する。ハードウェア除算、浮動小数点、アトミック拡張などがそれに相当する。非常に一般的な拡張命令群で、これらは互いに命令エンコーディング空間内で衝突しないように設計されている。
  • 非標準拡張 (Non-standard Extension) : 標準拡張よりもより高度に専門化された拡張で、他の標準拡張や非標準拡張と命令エンコーディング空間がぶつかってしまう可能性もある。

RISC-Vでは32ビット命令長の命令は下位2ビットが2'b11になっており、16ビット命令は2'b00, 2'b01, 2,b10に割り当てられている。そういう意味で、RISC-Vでは下位の2ビットを「プレフィックス」と呼んでいる。 RISC-Vはリトルエンディアンなので、下位の2ビットは一般的に先にフェッチされるため、最も低いビットにプレフィックスが挿入されているという訳だ。

例えば、アトミック拡張"A"は下位の7ビットが"0101111"となっており、これがアトミックのメジャーオペコードであるということを示している。 さらにfunct3フィールドの3ビットを使ってより細かなエンコーディングを行われるが、この3ビットも残りの22ビット(32-7-3=22)のプレフィックスととらえることができる。

f:id:msyksphinz:20210817002001p:plain

さらに"グリーンフィールド拡張"と"ブラウンフィールド拡張"という分類もされている。 グリーンフィールド拡張は、新しい命令エンコーディング空間を用いて定義される拡張機能のこと。ブラウンフィールド拡張は既存のエンコーディング空間に対して特定のビットに制限を付けることで定義される拡張となり、既存のエンコーディング空間を利用するため必ず親となるグリーンフィールド拡張が存在している。

例えば、ベースISAは30ビットの空間を持つグリーンフィールド拡張であり、FDQ浮動小数点拡張はベースISAに対する30ビットのブラウンフィールド拡張であると考えることができる。 このとき、A拡張は32ビットから下位の7ビットを除いた空間である25ビットにエンコーディングされたグリーンフィールド空間であると考えることができる。

以下の図はちょっとややこしいが、

f:id:msyksphinz:20210817002102p:plain
  • RV32I / RV64I はグリーンフィールド拡張(エンコーディング空間は30ビット)
  • A拡張はグリーンフィールド拡張(エンコーディング空間は25ビット)
  • F拡張はブラウンフィールド拡張で、親となる拡張はI命令
  • D拡張はブラウンフィールド拡張で、親となる拡張はF拡張
  • Q拡張はブラウンフィールド拡張で、親となる拡張はD拡張
  • M拡張はブラウンフィールド拡張で、親となる拡張はI拡張

"No new state"というのは新しいステート(まあ端的にいえばレジスタ)が追加されるかどうかで、FDQは浮動小数レジスタとフラグが追加されるので"Add state"に分類され、A拡張とM拡張は新しいステートが追加されないので"No new state"と分類されていると考えれば良かろう。

AMBA5 の CHI (Coherence Hub Interface)についての調査 (5. キャッシュ状態とコヒーレンスプロトコル)

developer.arm.com

developer.arm.com

CHIのコヒーレンスプロトコルについて

キャッシュラインの取りうる状態

キャッシュラインの状態の種類は、以下のようになっている。

状態 説明 補足?
I (Invalid) キャッシュラインはキャッシュ中に存在していない Invalid状態?
UC (Unique Clean) キャッシュラインはキャッシュ中に存在しており、更新されていない。
同じキャッシュラインは他のキャッシュに存在していない
スヌープに対するレスポンスとして、Homeノードに対してレスポンスする必要がある。
Exclusive状態?
UCE (Unique Clean Empty) キャッシュラインはキャッシュ中に存在している。
同じキャッシュラインはほかのキャッシュに存在していない
どのデータバイトもValidではない
スヌープに対するレスポンスとして、Homeノードに対してレスポンスする必要がある。
UD (Unique Dirty) キャッシュラインはほかのキャッシュには存在していない
メモリに対してキャッシュラインは更新されてる。
スヌープに対するレスポンスとして、Homeノードに対してレスポンスする必要がある。
Modified状態?
UDP (Unique Dirty Partial) キャッシュラインはほかのキャッシュには存在していない
メモリに対してキャッシュラインは一部更新されている。
キャッシュラインは吐き出されると、次のレベルのメモリに対してマージされる必要がある。
スヌープに対するレスポンスとして、Homeノードに対してレスポンスする必要がある。
SC (Shared Clean) キャッシュラインは他のキャッシュにも存在している可能性がある。
キャッシュラインはメモリに対して更新されている可能性がある。
メモリに対する掃き出しの責任を負う必要がない。
Shared状態?
SD (Shared Dirty) キャッシュラインは他のキャッシュにも存在している可能性がある。
キャッシュラインはメモリに対して更新されている可能性がある。
メモリに対する掃き出しの際、データを次のレベルのメモリに受け渡す責任を持つ。
Owner状態?

リクエストの種類

リクエス 説明
Read Requests データをリクエスタに返す。
システム中の他のエージェントに対してデータが移動される可能性がある。
キャッシュ状態は更新される可能性がある。
他のキャッシュの状態も更新される可能性がある。
Dataless Requests データはリクエスタに返されない。
システム中の他のエージェントに対してデータが移動される可能性がある。
キャッシュ状態は更新される可能性がある。
他のキャッシュの状態も更新される可能性がある。
Write RequestsとCombined Write Requets リクエスタからデータを移動する
システム中の他のエージェントに対してデータが移動される可能性がある。
キャッシュ状態は更新される可能性がある。
他のキャッシュの状態も更新される可能性がある。
Atomic Requests リクエスタからデータを移動する
いくつかのリクエストに関しては、リクエスタにデータレスポンスが返される。
システム中の他のエージェントに対してデータが移動される可能性がある。
キャッシュ状態は更新される可能性がある。
他のキャッシュの状態も更新される可能性がある。
Other Requests システム中でデータの移動は発生しない
分散仮想メモリ(DVM: Distributed Virtual Memory)メンテナンスなどの補助として使用される。
以降のRead Requestに対するメモリコントローラのウォームアップに使用される。

インターコネクト間のプロトコルフローについて

次に、CHIのプロトコルフローについて見ていく。

Read Transaction flows

Read transactions with DMT and without snoops

DMTというのはDirect Memory Transferであるということを思い出しておく。つまりリクエスタの要求に対して、データはHomeノードを介さず直接変換される。

  1. RN-FがHN-FにReadリクエストを送る
  2. HN-FがSN-FにReadリクエストを送る
    • ReadリクエストのIDフィールドは送られるデータレスポンスをベースにしている。データはリクエスタもしくはHN-Fに返される。
  3. SN-FはデータレスポンスをRN-Fに直接返信する
  4. 最初のリクエストがReadSharedの場合、RN-FはHN-FにCompAckを送信する。
f:id:msyksphinz:20210815231655p:plain

この図を見てわかる通り、RN-Fのキャッシュラインの状態はI(Invalid)からUC(Unique Clean)状態になる。

JSONベースで書ける波形表示ツールWaveDromがとても便利だった (2)

少し前にWaveDromというJSONで波形を表示するツールについて調査していたのだが、ある程度使いこなすことが出来るようになってきた。 今作っている自作RISC-V CPUの仕様書を書くために波形を書いているのだが、結構便利に使うことが出来ている。

例えば、以下のようなJSONファイルを書けば自動的にPNGSVGを作ることが出来た。

f:id:msyksphinz:20210815003855p:plain

ポイントとしては、

  • []を使って階層的に波形を管理できる。
  • nodeedgeを使って波形の間に矢印とコメントを出力することができる。
  • hscaleへ波形の横幅を調整できる。ただし整数のみらしい。

というあたりを使いこなせば、十分に役に立つ気がしている。

AMBA5 の CHI (Coherence Hub Interface)についての調査 (4. オーダリング制約)

developer.arm.com

developer.arm.com

CHIのオーダリングの章を読んでいたのだが、正直概念的な話が多すぎて良く分からん。

CHIのオーダリングについて

CHIがオーダリングの要求をサポートするためのプロトコルのメカニズムについて説明する。

  • マルチコピーアトミック性
  • 完了レスポンスとオーダリング
  • 完了Acknowlegdement
  • トランザクションオーダリング

マルチコピーアトミック性

同じ場所への書き込みはすべてシリアライズする必要がある。つまり、いくつかのリクエストについてはすべての書き込みが見えていないとしても、すべてのリクエスターからは同じ順序に見えている必要がある。

全てのリクエスターが書き込みを監視するできるようになるまで、その場所の読み込み要求に対する値は、その書き込んだ値は見えない。

完了レスポンスとオーダリング

トランザクションが後続のトランザクションとの順序性を保証するために、同じエージェントもしくは異なるエージェントからの要求について、Comp, RespSepData, CompData, Persistのレスポンスを操作する制約がある。しかしこのリストをいくら読んでも全く想像がつかないので、これについては読み飛ばしたい。

完了Acknowledgement

直訳する。

リクエスターが発行したトランザクションと、異なるリクエスターからのトランザクションによって引き起こされたSnoopトランザクションの相対的な順序は、完了Acknowledgementの使用によって制御されます。これにより、Requesterからのトランザクションの後にオーダーされたSnoopトランザクションが、トランザクションレスポンスの後に受信されることが保証されます。

うーん?良く分からん。

トランザクションオーダリング

RN, HNペアとHN-I, SN-Iペアに対するリクエストのオーダリングのメカニズムについて定義している。HN-FとSN-Fのペアについてのオーダリングはオーダーフィールドが使用される。

  • リクエストオーダ:同一エージェントから同じアドレス場所への複数のトランザクションに対するオーダリングを保証する。
  • エンドポイントオーダ:同一エージェントから同じエンドポイントアドレス領域への複数のトランザクションに対するオーダリングを保証する。
  • 順序付き書き込みの監視:システム中の他のエージェントからの、単一エージェントからのの書き込みトランザクションのシーケンスの監視について保証する。
  • リクエスト受け入れ:CompleetrがAcknowlegdementを返すのは、リクエストが受け入れられた場合のみであることを示す。

いくつかのリクエストについての確認

  • ReadNoSnp : 他のマスターからのスヌープが必要ない場合の読み込みリクエストを行いたい場合に使用する。レスポンスはスレーブノードからダイレクトに返される。
  • ReadOne, ReadOnceCleanInvalid, ReadOnceMakeInvalidトランザクションは他のマスターに対するスヌープが必要な時に使用する。リクエスターは自分自身のキャッシュに対してキャッシュラインを割り当てるつもりがないときに使用する。