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

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

Rocket ChipのGeneratorのソースの解析メモ(6) - ExampleRocketSystemのIOポート

スポンサーリンク

前回はとりあえずLazyModuleを使った最小のモジュールを作ってみる、という話をまとめた。 その際に以下の要素があればLazyModuleを使ったモジュールを作れるということがわかった。

  1. Parametersから派生したモジュールのパラメータクラス
  2. LazyModuleから派生したモジュールの皮でモジュール間の最終的な接続はここっぽい?
  3. LazyModuleImpから派生したモジュールの実装本体

上記のようにLazyModuleImpが実装本体っぽいという当たりがついたので、実際にRocket Chipの実装の解析が少し進むかも!!ということでコードを呼んでみた内容をまとめていく。

例のRocketシステム (ExampleRocketSystem)

例の、、、と書くとおかしな感じが漂うけど、今回見ていきたいのはTestHarnessクラス内でインスタンスされているExampleRocketSystemだ。 知りたいことは色々あるのだが、今回は

  • そもそもモジュールのIOポートの宣言ってどこなの??

という部分にしぼって解析をしたいと思う。ということで実装部分を見てみよう。

  • system/ExampleRocketSystem.scala
/** Example Top with periphery devices and ports, and a Rocket subsystem */
class ExampleRocketSystem(implicit p: Parameters) extends RocketSubsystem
    with HasAsyncExtInterrupts
    with CanHaveMasterAXI4MemPort
    with CanHaveMasterAXI4MMIOPort
    with CanHaveSlaveAXI4Port
    with HasPeripheryBootROM {
  override lazy val module = new ExampleRocketSystemModuleImp(this)

~略~

}

class ExampleRocketSystemModuleImp[+L <: ExampleRocketSystem](_outer: L) extends RocketSubsystemModuleImp(_outer)
    with HasRTCModuleImp
    with HasExtInterruptsModuleImp
    with CanHaveMasterAXI4MemPortModuleImp
    with CanHaveMasterAXI4MMIOPortModuleImp
    with CanHaveSlaveAXI4PortModuleImp
    with HasPeripheryBootROMModuleImp
    with DontTouch

"~略~"と書いたが省略するほど長くはなくて、内部のポートやら接続を行うと思われるメソッドが幾つか呼び出されるだけになっていて、このExampleRocketSystem自体にはほぼ何も実装が無い。

ExampleRocketSystemのIOポートを探す

構成的には継承してる各種Trait群が実装実体なのだとは思うのだが、闇雲に追うのも大変なのでまずはmakeによって生成されたコードで何がポートに出てきているかを確認しておく。 細かい信号は接頭辞となる部分だけ端折って載せます(RTL生成の元コードがFIRRTLのコードだからChiselのソースと対応してなくて面倒だったのでFIRRTLの方のコード)。

  module ExampleRocketSystem :
    input clock : Clock
    input reset : UInt<1>
    output auto : {}
    output debug :
    output mem_axi4 :
    output mmio_axi4 :
    input l2_frontend_bus_axi4 :

中身的には上記のようになってて

  • debug関連のI/F
  • メモリへのAXI4 I/F
  • MMIOへのAXI4 I/F
  • L2用のバスへのAXI4 IF

がくっついてるという感じ。上記の下3つが対応しているのはExampleRocketSystemにMix-inされている以下の3つだと思われるのでこれらを確認していく。

    with CanHaveMasterAXI4MemPort
    with CanHaveMasterAXI4MMIOPort
    with CanHaveSlaveAXI4Port

    with CanHaveMasterAXI4MemPortModuleImp
    with CanHaveMasterAXI4MMIOPortModuleImp
    with CanHaveSlaveAXI4PortModuleImp

CanHaveMasterAXI4MemPort / CanHaveMasterAXI4MemPortModuleImp

まずはソースコードを。

trait CanHaveMasterAXI4MemPort { this: BaseSubsystem =>
  val module: CanHaveMasterAXI4MemPortModuleImp

  val memAXI4Node = p(ExtMem).map { case MemoryPortParams(memPortParams, nMemoryChannels) =>
    val portName = "axi4"
    val device = new MemoryDevice

    val memAXI4Node = AXI4SlaveNode(Seq.tabulate(nMemoryChannels) { channel =>
      val base = AddressSet(memPortParams.base, memPortParams.size-1)
      val filter = AddressSet(channel * mbus.blockBytes, ~((nMemoryChannels-1) * mbus.blockBytes))

      AXI4SlavePortParameters(
        slaves = Seq(AXI4SlaveParameters(
          address       = base.intersect(filter).toList,
          resources     = device.reg,
          regionType    = RegionType.UNCACHED, // cacheable
          executable    = true,
          supportsWrite = TransferSizes(1, mbus.blockBytes),
          supportsRead  = TransferSizes(1, mbus.blockBytes),
          interleavedId = Some(0))), // slave does not interleave read responses
        beatBytes = memPortParams.beatBytes)
    })

    memAXI4Node := mbus.toDRAMController(Some(portName)) {
      AXI4UserYanker() := AXI4IdIndexer(memPortParams.idBits) := TLToAXI4()
    }

    memAXI4Node
  }
}

/** Actually generates the corresponding IO in the concrete Module */
trait CanHaveMasterAXI4MemPortModuleImp extends LazyModuleImp {
  val outer: CanHaveMasterAXI4MemPort

  val mem_axi4 = outer.memAXI4Node.map(x => IO(HeterogeneousBag.fromNode(x.in)))
  (mem_axi4 zip outer.memAXI4Node) foreach { case (io, node) =>
    (io zip node.in).foreach { case (io, (bundle, _)) => io <> bundle }
  }

  def connectSimAXIMem() {
    (mem_axi4 zip outer.memAXI4Node).foreach { case (io, node) =>
      (io zip node.in).foreach { case (io, (_, edge)) =>
        val mem = LazyModule(new SimAXIMem(edge, size = p(ExtMem).get.master.size))
        Module(mem.module).io.axi4.head <> io
      }
    }
  }
}

CanHaveMasterAXI4MMIOPort / CanHaveMasterAXI4MMIOPortModuleImp

同様にAXI4MMIOについても確認していく。 こちらも先程と同様mmio_axi4IOでラップされている。

/** Adds a AXI4 port to the system intended to master an MMIO device bus */
trait CanHaveMasterAXI4MMIOPort { this: BaseSubsystem =>
  private val mmioPortParamsOpt = p(ExtBus)
  private val portName = "mmio_port_axi4"
  private val device = new SimpleBus(portName.kebab, Nil)

  val mmioAXI4Node = AXI4SlaveNode(
    mmioPortParamsOpt.map(params =>
      AXI4SlavePortParameters(
        slaves = Seq(AXI4SlaveParameters(
          address       = AddressSet.misaligned(params.base, params.size),
          resources     = device.ranges,
          executable    = params.executable,
          supportsWrite = TransferSizes(1, params.maxXferBytes),
          supportsRead  = TransferSizes(1, params.maxXferBytes))),
        beatBytes = params.beatBytes)).toSeq)

  mmioPortParamsOpt.map { params =>
    mmioAXI4Node := sbus.toFixedWidthPort(Some(portName)) {
      (AXI4Buffer()
        := AXI4UserYanker()
        := AXI4Deinterleaver(sbus.blockBytes)
        := AXI4IdIndexer(params.idBits)
        := TLToAXI4())
    }
  }
}

/** Actually generates the corresponding IO in the concrete Module */
trait CanHaveMasterAXI4MMIOPortModuleImp extends LazyModuleImp {
  val outer: CanHaveMasterAXI4MMIOPort
  val mmio_axi4 = IO(HeterogeneousBag.fromNode(outer.mmioAXI4Node.in))

  (mmio_axi4 zip outer.mmioAXI4Node.in) foreach { case (io, (bundle, _)) => io <> bundle }

  def connectSimAXIMMIO() {
    (mmio_axi4 zip outer.mmioAXI4Node.in) foreach { case (io, (_, edge)) =>
      // test harness size capped to 4KB (ignoring p(ExtMem).get.master.size)
      val mmio_mem = LazyModule(new SimAXIMem(edge, size = 4096))
      Module(mmio_mem.module).io.axi4.head <> io
    }
  }
}

CanHaveSlaveAXI4Port / CanHaveSlaveAXI4PortImp

こちらはRocketChipが備えるAXIのSlaveポートの記述で先程と同様にl2_frontend_bus_axi4IOでラップされている。

/** Adds an AXI4 port to the system intended to be a slave on an MMIO device bus */
trait CanHaveSlaveAXI4Port { this: BaseSubsystem =>
  private val slavePortParamsOpt = p(ExtIn)
  private val portName = "slave_port_axi4"
  private val fifoBits = 1

  val l2FrontendAXI4Node = AXI4MasterNode(
    slavePortParamsOpt.map(params =>
      AXI4MasterPortParameters(
        masters = Seq(AXI4MasterParameters(
          name = portName.kebab,
          id   = IdRange(0, 1 << params.idBits))))).toSeq)

  slavePortParamsOpt.map { params =>
    fbus.fromPort(Some(portName), buffer = BufferParams.default) {
      (TLWidthWidget(params.beatBytes)
        := AXI4ToTL()
        := AXI4UserYanker(Some(1 << (params.sourceBits - fifoBits - 1)))
        := AXI4Fragmenter()
        := AXI4IdIndexer(fifoBits))
    } := l2FrontendAXI4Node
  }
}

/** Actually generates the corresponding IO in the concrete Module */
trait CanHaveSlaveAXI4PortModuleImp extends LazyModuleImp {
  val outer: CanHaveSlaveAXI4Port
  val l2_frontend_bus_axi4 = IO(HeterogeneousBag.fromNode(outer.l2FrontendAXI4Node.out).flip)
  (outer.l2FrontendAXI4Node.out zip l2_frontend_bus_axi4) foreach { case ((bundle, _), io) => bundle <> io }
}

AXIを使うモジュールの一般形?

こうやって3つくらい並べると、大体共通してる部分があるのでこの辺はお作法的に捉えても使う分には問題なさそうでもある。 ざっくり今回見てきたAXIの3つのモジュールについてその共通部分を括りだして一般化??してみると、以下のような感じだろうか(まだわからない部分を含む。。ご容赦ください)

trait <TraitPort> { this: <LazyModuleの派生クラス> =>
  private val <ポートのパラメータ変数> = p(<対応するポートのField>)
  private val portName = "mmio_port_axi4"
  private val device = new SimpleBus(portName.kebab, Nil)

  val <ノード変数> = <AXI4MasterNode or AXI4SlaveNode>(
    <ポートのパラメータ変数>.map(params =>
      <AXI4SlavePortParameters or AXI4MasterPortParameters>(
        slaves = Seq(AXI4SlaveParameters(
          // 対応する設定
        )

  <ポートのパラメータ変数>.map { params =>
    mmioAXI<ノード変数> := <接続(ここはまだ追えてない)> {
      // ノード間の接続の関係っぽい??
    }
  }
}

trait <TraitPortModuleImp> extends LazyModuleImp {
  val outer: <TraitPort>
  val <ポート名> = IO(HeterogeneousBag.fromNode(outer.<ノード変数>.<in or out>))

  (<ポート名> zip outer.<ノード変数>.<in or out>) foreach { case (io, (bundle, _)) => io <> bundle }

  def <connect***>() {
    (<ポート名> zip outer.<ノード変数>.<in or out>) foreach { case (io, (_, edge)) =>
      // 必要な処理
      // connectSimAXIMMIOの場合ならSimAXIMemを実体化して<ポート名>と接続
    }
  }
}

ということでIOポートについてはこれらがFIRRTLを経てRTLになっていると思って良さそう。 ここまでに見てきた2つの***Impクラスで定義されているconnect***メソッドはTestHarness内でExampleRocketSystem.moduleを評価した後に実行されている。 つまりExampleRocketSystem.moduleを評価してmoduleを取り出したタイミングでは最終的な意味での配線は行われていない感じになってる、、、ように思える。 次はそのへんの接続部分に着目して解析を進めていこうと思う。 あとRocketChipで使われているMultiIOModuleについても少し気になることがあるので、それは別途紹介する予定。