FPGA開発日記

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

最小構成で学ぶChisel & Diplomacy

Diplomacyを使ってみるために、最小構成を考えてみよう。上記の2つのPusher(リクエスト挿入するためのモジュール)を介してMemoryに接続する。

下記のソースコードはいくつか説明しておいた方が良い。

まず、pusher1, ifu, xbar, memoryLazyModuleとして宣言されている。pusher1と, xbar, memoryはちょっと中身が複雑なので説明ができないが、ifuは自作なので中身を見てみよう。

  • src/main/scala/core_complex/core_complex.scala
 package core_complex

 import chisel3._

 import freechips.rocketchip.config._
 import freechips.rocketchip.amba.ahb._
 import freechips.rocketchip.amba.apb._
 import freechips.rocketchip.amba.axi4._
 import freechips.rocketchip.subsystem.{BaseSubsystemConfig}
 import freechips.rocketchip.devices.tilelink._
 import freechips.rocketchip.tilelink._
 import freechips.rocketchip.util._
 import freechips.rocketchip.diplomacy._
 import freechips.rocketchip.diplomaticobjectmodel.logicaltree.{BusMemoryLogicalTreeNode, LogicalModuleTree, LogicalTreeNode}
 import freechips.rocketchip.diplomaticobjectmodel.model.{OMECC, TL_UL}

 class core_complex(ramBeatBytes: Int, txns: Int)(implicit p: Parameters) extends LazyModule {
   val pusher1 = LazyModule(new TLPatternPusher("pat1", Seq(
     new WritePattern(0x100, 0x2, 0x012345678L),
     new WritePattern(0x500, 0x2, 0x0abcdef01L),
     new ReadExpectPattern(0x100, 0x2, 0x012345678L),
     new ReadExpectPattern(0x500, 0x2, 0x0abcdef01L)
   )))

   val ifu    = LazyModule(new ifu("ifu"))

   val xbar   = LazyModule(new TLXbar)
   val memory = LazyModule(new TLRAM(AddressSet(0x02000, 0x0fff), beatBytes = ramBeatBytes))

   xbar.node := pusher1.node
   xbar.node := ifu.node
   memory.node := xbar.node

   lazy val module = new LazyModuleImp(this) {
     val io = IO(new Bundle {
       val finished = Output(Bool())
     })
     pusher1.module.io.run := true.B
     io.finished := pusher1.module.io.done
   }
 }
  • src/main/scala/ifu/ifu.scala
 package freechips.rocketchip.tilelink

 import chisel3._

 import freechips.rocketchip.config._
 import freechips.rocketchip.amba.ahb._
 import freechips.rocketchip.amba.apb._
 import freechips.rocketchip.amba.axi4._
 import freechips.rocketchip.subsystem.{BaseSubsystemConfig}
 import freechips.rocketchip.devices.tilelink._
 import freechips.rocketchip.tilelink._
 import freechips.rocketchip.util._
 import freechips.rocketchip.diplomacy._
 import freechips.rocketchip.diplomaticobjectmodel.logicaltree.{BusMemoryLogicalTreeNode, LogicalModuleTree, LogicalTreeNode}
 import freechips.rocketchip.diplomaticobjectmodel.model.{OMECC, TL_UL}

 class ifu(name: String)(implicit p: Parameters) extends LazyModule {
   val node = TLClientNode(Seq(TLClientPortParameters(Seq(TLClientParameters(name = name)))))

   lazy val module = new LazyModuleImp(this) {
     val io = IO(new Bundle {
       val run   = Input(Bool())
       val done  = Output(Bool())
       val error = Output(Bool())
     })

     val (out, edge) = node.out(0)

...
   }

object ifu
 {
   def apply(name: String)(implicit p: Parameters): TLInwardNode =
   {
     val pusher = LazyModule(new ifu(name))
     pusher.node
   }
 }

ifuは2つのセクションに分かれる。ifuクラスの先頭にnodeを宣言し、これはTLClientNode(つまりTileLinkのマスターである)である。そして次にLazyModuleImplを宣言してノードに接続するデジタル回路部分を宣言する。

moduleにの内部にノードの情報を引き込むために、val (out, edge) = node.out(0)としている。outはTileLinkの接続信号、Edgeはパラメータ情報として考えて良い。つまり、以下のようにout.aout.bのようにしてTileLinkのチャネルにアクセスが可能となる。

     val (out, edge) = node.out(0)

     val baseEnd = 0
     val (sizeEnd,   sizeOff)   = (edge.bundle.sizeBits   + baseEnd, baseEnd)
     val (sourceEnd, sourceOff) = (edge.bundle.sourceBits + sizeEnd, sizeEnd)
     val beatBytes = edge.bundle.dataBits

     val counter = RegInit(0.U((beatBytes * 8).W))

     val a = out.a
     val d = out.d
...
     a.valid := io.run && !finish
     a.bits := Mux(req_counter(4), get_pbits, put_pbits)

     d.ready := true.B

     io.done := finish

     // Tie off unused channels
     out.b.valid := false.B
     out.c.ready := true.B
     out.e.ready := true.B

core_complexに立ち戻ると、2つのマスターifupusherxbarを介して接続される。xbarアービトレーション機能を持っており、2つのマスターからの信号を制御してメモリに伝え、メモリからのレスポンスを正確に2つのマスターに返す。

   xbar.node := pusher1.node
   xbar.node := ifu.node
   memory.node := xbar.node

もうひとつ、core_complexにも内部にModuleImplが設けられており、ここに各LazyModuleの制御が書かれている。

class core_complex(ramBeatBytes: Int, txns: Int)(implicit p: Parameters) extends LazyModule {
...
     lazy val module = new LazyModuleImp(this) {
     val io = IO(new Bundle {
       val finished = Output(Bool())
     })
     pusher1.module.io.run := true.B
     io.finished := pusher1.module.io.done
   }

pusher1run信号に対して1をアサートし、core_complexモジュールに対してio.finished出力信号を定義したうえでpusher1の制御がすべて完了するとそれをアサートするようにしている。

このcore_complexインスタンス化するためのモジュールがTestHarnessで、これはModuleつまり普通のChiselのモジュールとして宣言する。

 class TestHarness()(implicit p: Parameters) extends Module {
   val io = IO(new Bundle {
     val success = Output(Bool())
   })

   val ldut = LazyModule(new core_complex(4, 5000))
   val dut = Module(ldut.module)

   io.success := dut.io.finished
 }

dut.ioつまりcore_complex.moduleの信号io.finishedを外部の信号io.successに接続している。これが無いとLazyModuleはTestHarnessの中でインスタンス化されることなく、なにもVerilogが生成されない。

そして、このTestHarnessからVerilogを生成するのがGeneratorオブジェクトだ。GeneratorオブジェクトがemitVerilogを読みだしてVerilogファイルを生成する。これにはChiselのエンジンおよびFIRRTLのエンジンが呼び出される(Chisel 3.3.0からはDriverではなくChiselStageの使用が推奨されているが、まだ3.2.2を使っているのでDriverのまま)。

 // See LICENSE.SiFive for license details.

 package core_complex

 import chisel3._
 import freechips.rocketchip.config.Config
 import freechips.rocketchip.config.Parameters
 import freechips.rocketchip.devices.debug.Debug
 import freechips.rocketchip.diplomacy.LazyModule
 import freechips.rocketchip.util.AsyncResetReg
 import chisel3.Driver
 import freechips.rocketchip.subsystem.BaseSubsystemConfig

 object Generator {
     final def main(args: Array[String]) {
         val verilog = Driver.emitVerilog(
             new TestHarness()(Parameters.empty)
         )
     }
 }

このGeneratorを呼び出すために、以下のようにコマンドライン上からタイプする。

$ TERM=xterm-color java -Xmx2G -Xss8M -XX:MaxPermSize=256M -jar ./sbt-launch.jar 'runMain core_complex.Generator'

TestHarness.vが生成されることが確認できた。

module TestHarness(
  input   clock,
  input   reset,
  output  io_success
);
  wire  ldut_clock; // @[test_harness.scala 19:19]
  wire  ldut_reset; // @[test_harness.scala 19:19]
  wire  ldut_io_finished; // @[test_harness.scala 19:19]
  core_complex ldut ( // @[test_harness.scala 19:19]
    .clock(ldut_clock),
    .reset(ldut_reset),
    .io_finished(ldut_io_finished)
  );
  assign io_success = ldut_io_finished; // @[test_harness.scala 21:14]
  assign ldut_clock = clock;
  assign ldut_reset = reset;
endmodule

実際には大量のモジュールが配置されて生成されているのだが、Chiselによりcore_complexと外部モジュールが正しく接続されていることが分かると思う。

module core_complex(
  input   clock,
  input   reset,
  output  io_finished
);
....
  TLPatternPusher pusher1 ( // @[core_complex.scala 18:27]
    .clock(pusher1_clock),
    .reset(pusher1_reset),
    .auto_out_a_ready(pusher1_auto_out_a_ready),
    .auto_out_a_valid(pusher1_auto_out_a_valid),
...
  ifu ifu ( // @[core_complex.scala 25:26]
    .clock(ifu_clock),
    .reset(ifu_reset),
    .auto_out_d_valid(ifu_auto_out_d_valid)
  );
  TLXbar xbar ( // @[core_complex.scala 27:26]
    .clock(xbar_clock),
  TLRAM memory ( // @[core_complex.scala 28:26]
    .clock(memory_clock),
    .reset(memory_reset),
    .auto_in_a_ready(memory_auto_in_a_ready),
    .auto_in_a_valid(memory_auto_in_a_valid),
    .auto_in_a_bits_opcode(memory_auto_in_a_bits_opcode),
...