ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

ChiselのMultiIOModuleを使ったポートのカスタマイズ

スポンサーリンク

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点が不思議に思えた。

  1. 各ポート名に接頭辞"io_"がついてない」ということだった
  2. 幾つかのモジュールに同時に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"時点ではMultiIOModulechisel3.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/resetIO()でラップされた変数が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点

  1. トレイト側でパラメタライズ用変数を参照する変数をlazyにすること
    1. lazyが無いとインスタンス時に評価されてしまうことになり、その場合はbitsの値が0として扱われる。
    2. エラーにならないの、これ。という気もするが。
  2. パラメタライズ用の変数をクラス側でオーバーライドする際にトレイト内の変数より先に宣言しておくこと。
    1. これも上記とほぼ同様の話のようだが、先に参照してしまうと、その時点での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

これを使うと例えばデバッグ用のポートは完全に分離してすっきり!!とか言ったことが出来そうな気がしているので、そのへんは追って試してみようと思う。