Rocket Chipの記事で以下のように書いたのを試してみたという記事。
あとRocketChipで使われている
MultiIOModule
についても少し気になることがあるので、それは別途紹介する予定。
LazyModuleImp
Rocket Chipで使用されるLazyModuleImp
というモジュールがある。これは今の自分の理解ではRocket Chipの各種モジュールの実装本体が入っているもので、ここにIOポートの宣言と思える記述が含まれていた。(以下の記事参照)
このIOポートの宣言が含まれるモジュールから生成されたFIRRTLのIO部分を見ると以下のようになっている。
module ExampleRocketSystem : input clock : Clock input reset : UInt<1> output auto : {} output debug : output mem_axi4 : output mmio_axi4 : input l2_frontend_bus_axi4 :
これを見て以下の2点が不思議に思えた。
- 各ポート名に接頭辞"io_"がついてない」ということだった
- 幾つかのモジュールに同時に
IO()
でラップされた変数が存在している
少し気になったのでこのモジュールの実体を見てみると以下のようになっている。
class LazyModuleImp(val wrapper: LazyModule) extends MultiIOModule with LazyModuleImpLike { val (auto, dangles) = instantiate() }
MultiIOModule
といういかにも複数個のIOを宣言できそうなモジュールが使われていた。Rocket Chipの仕組みからして「MultiIOModule
を使うとこういうことが出来るのでは??」ということがあったので幾つか試してみようと思う。
MultiIOModule
まず先に記載しておくと、今回参照しているChiselのソースコードのバージョンは"3.1.8"だ。これを書いたのはChsiel3.2ではこのあたりの構成が結構変わっているからだ。
以下がMultiIOModule
の宣言になる。
object experimental { // scalastyle:ignore object.name ~略~ type MultiIOModule = chisel3.core.ImplicitModule
ということで"3.1.8"時点ではMultiIOModule
はchisel3.core.ImplicitModule
のエイリアスになっている。
/** Abstract base class for Modules, which behave much like Verilog modules. * These may contain both logic and state which are written in the Module * body (constructor). * * @note Module instantiations must be wrapped in a Module() call. */ abstract class ImplicitModule(implicit moduleCompileOptions: CompileOptions) extends UserModule { // Implicit clock and reset pins val clock: Clock = IO(Input(Clock())) val reset: Reset = IO(Input(Bool())) // Setup ClockAndReset Builder.currentClockAndReset = Some(ClockAndReset(clock, reset)) private[core] override def initializeInParent(parentCompileOptions: CompileOptions): Unit = { implicit val sourceInfo = UnlocatableSourceInfo super.initializeInParent(parentCompileOptions) clock := Builder.forcedClock reset := Builder.forcedReset } }
コードを見てもらうとわかるが、以下が目に留まるところ。
Module
では必要なval io
が無い(のでval io
作らなくていい)Module
ではval io
を宣言だけして、派生クラスでの実装を強制しているため
clock
/reset
とIO()
でラップされた変数が2つ存在している
個人的に大事なのは2番めのIO()
でラップされた変数が複数個存在していることで、この時点でExampleRocketSystem
のように複数個のIO()
が存在することも許容されることになる。
というか、そもそもIO()
でラップする変数は2つ以上合っちゃ駄目、なんてどこにも書いてなかったのにそうだと思いこんでいた。。
これを継承して使えば任意のIO端子名を作ることも出来そう(RTLにした時に"io_"なんちゃらでなくてもいいという意味)なので、幾つか簡単なモジュールを書いて試してみる。
簡単な例
まずはごく基本的な使い方からで、ただ単にMultiIOModule
を継承してIO()
でラップした変数を2つ用意し、その2つの変数間で接続をしてみる。
import chisel3._ import chisel3.experimental.MultiIOModule import chisel3.util._ class TestMod extends MultiIOModule { val io1 = IO(new Bundle { val in1 = Input(Bool()) val out1 = Output(Bool()) }) val io2 = IO(new Bundle { val in2 = Input(Bool()) val out2 = Output(Bool()) }) io1.out1 := io2.out2 io2.out2 := io1.in1 } object ElaborateTestMod extends App { Driver.execute(args, () => new TestMod) }
上記コードは問題なくエラボレートが成功し、以下のように意図通りのRTLが生成された。
- 生成されたRTL
module TestMod( input clock, input reset, input io1_in1, output io1_out1, input io2_in2, output io2_out2 ); // io1 <-> io2間での接続も問題なく出来る assign io1_out1 = io2_out2; // @[TestMod.scala 19:12] assign io2_out2 = io1_in1; // @[TestMod.scala 20:12] endmodule
Rocket Chipの実装から出来そうなことを試す
トレイトにポート宣言書いてMix-inでポートを定義
以下のようにMultiIOModule
を継承したトレイト内に所望のIO
を定義しておけば、継承するだけでIO
を追加できる。以下のコードは最初のサンプルをトレイト使って書いたもの。
trait IOTraitA extends MultiIOModule { val a = IO(new Bundle { val in1 = Input(Bool()) val out1 = Output(Bool()) }) } trait IOTraitB extends MultiIOModule { val b = IO(new Bundle { val in2 = Input(Bool()) val out2 = Output(Bool()) }) } class TestModB extends IOTraitA with IOTraitB { a.out1 := b.out2 b.out2 := a.in1 } object ElaborateTestModB extends App { Driver.execute(args, () => new TestModB) }
- 生成されたRTL
module TestModB( input clock, input reset, input a_in1, output a_out1, output b_out2 ); assign a_out1 = b_out2; // @[TestMod.scala 44:10] assign b_out2 = a_in1; // @[TestMod.scala 45:10] endmodule
IOポートのTraitをパラメタライズ
パラメタライズしたい場合は以下のようにトレイト内にパラメタライズ用の変数を宣言だけしておいて、継承時に確定させればOK。
trait IOTraitC1 extends MultiIOModule { val bits: Int lazy val io1 = IO(new Bundle { val in1 = Input(UInt(bits.W)) val out1 = Output(UInt(bits.W)) }) } trait IOTraitC2 extends MultiIOModule { // 初期値与えといてもいいが、その場合は // 派生クラス側でoverrideが必須 val bits: Int = 0 lazy val io2 = IO(new Bundle { val in2 = Input(UInt(bits.W)) val out2 = Output(UInt(bits.W)) }) } class TestModC extends IOTraitC1 with IOTraitC2 { // ここでio1を評価するとbitsが0扱いされる // io1.out1 := io1.out1 // trait側で値を入れてる場合は // override必須 override val bits = 10 io1.out1 := io1.out1 io2.out2 := io1.in1 //override val bits = 10 } object ElaborateTestModB extends App { Driver.execute(args, () => new TestModC) }
注意としては、以下の2点
- トレイト側でパラメタライズ用変数を参照する変数を
lazy
にすることlazy
が無いとインスタンス時に評価されてしまうことになり、その場合はbits
の値が0として扱われる。- エラーにならないの、これ。という気もするが。
- パラメタライズ用の変数をクラス側でオーバーライドする際にトレイト内の変数より先に宣言しておくこと。
- これも上記とほぼ同様の話のようだが、先に参照してしまうと、その時点での
bits
の値が使われる。
- これも上記とほぼ同様の話のようだが、先に参照してしまうと、その時点での
- 生成されたRTL
module TestModC( input clock, input reset, input [9:0] io1_in1, output [9:0] io1_out1, input [9:0] io2_in2, output [9:0] io2_out2 ); assign io1_out1 = io1_in1; // @[TestMod.scala 78:12] assign io2_out2 = io1_in1; // @[TestMod.scala 79:12] endmodule
これを使うと例えばデバッグ用のポートは完全に分離してすっきり!!とか言ったことが出来そうな気がしているので、そのへんは追って試してみようと思う。