FPGA開発日記

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

AWSで動作するRISC-VデザインFireSimのカスタマイズ : Firechipにオリジナルデバイスを追加する

f:id:msyksphinz:20180617195844p:plain

FireSimについてある程度動かしたので、今度はカスタマイズをしてみたい。

というわけで、FireSimの実際のデザイン部分であるFireChipを使ってみることにした。

オリジナルのデザインを追加してみることにした。

メモリマップのレジスタを作る

参考にしたのは以下のチュートリアル

Memory-mapped Registers — FireSim documentation

ここで作るのは、外部からの情報を取ってきてメモリに書き込むデバイスだ(まどろっこしい書き方をしたが、つまりDMAだ)。 メモリマップされたレジスタを持っており、CPUはこのレジスタを使ってデバイスをコントロールすることができる。

まずはTLRegisterNodeオブジェクトを作って、RTLのレジスタを作成する。

  • src/main/scala/example/InputStream.scala
class InputStream(
    address: BigInt,
    val beatBytes: Int = 8)
    (implicit p: Parameters) extends LazyModule {

  val device = new SimpleDevice("input-stream", Seq("example,input-stream"))
  val regnode = TLRegisterNode(
    address = Seq(AddressSet(address, 0x3f)),
    device = device,
    beatBytes = beatBytes)

  lazy val module = new InputStreamModuleImp(this)
}

ここでTLRegisterNodeに対してパラメータを設定しているのだが、 - address : レジスタのアドレスとアドレスマスク。ここでは0x3fをマスクとして設定することで、64バイトのレジスタを宣言している。 - device : SimpleDevice型として作られたdeviceというオブジェクトを接続しているのだが、これはLinuxカーネルドライバで使用するためのもの。ここでは無視。 - beatBytes : TileLinkで規定されているデータ幅。この長さのレジスタが作られる。

ここではDMAを作るので、以下のレジスタがあれば良さそうだ。

  • addr : アドレス
  • len : 転送長
  • running : デバイスが実行中かどうか
  • complete : 転送が完了したか

実際のモジュールとしては、以下のようなInputStreamModuleImp を作成した。

  • src/main/scala/example/InputStream.scala
class InputStreamModuleImp(outer: InputStream) extends LazyModuleImp(outer) {
    val addrBits = 64
    val w = 64
    val io = IO(new Bundle {
        // Not used yet
        val in = Flipped(Decoupled(UInt(w.W)))
    }
    val addr = Reg(UInt(addrBits.W))
    val len = Reg(UInt(addrBits.W))
    val running = RegInit(false.B)
    val complete = RegInit(false.B)

    outer.regnode.regmap(
        0x00 -> Seq(RegField(addrBits, addr)),
        0x08 -> Seq(RegField(addrBits, len)),
        0x10 -> Seq(RegField(1, running)),
        0x18 -> Seq(RegField(1, complete)))
}

TileLinkのポートを作る

参考にしたのは以下。

DMA and Interrupts — FireSim documentation

つくるのはDMAなので、まずはTileLinkのポートを作らないと始まらない。これは先ほどのInputStreamに追加する。 TLClientNodeをモジュールとして追加した。

  • src/main/scala/example/InputStream.scala
class InputStream(
    address: BigInt,
    val beatBytes: Int = 8,
    val maxInflight: Int = 4)
    (implicit p: Parameters) extends LazyModule {

  val device = new SimpleDevice("input-stream", Seq("example,input-stream"))
  val regnode = TLRegisterNode(
    address = Seq(AddressSet(address, 0x3f)),
    device = device,
    beatBytes = beatBytes)
  val dmanode = TLClientNode(Seq(TLClientPortParameters(
    Seq(TLClientParameters(
      name = "input-stream",
      sourceId = IdRange(0, maxInflight))))))

  lazy val module = new InputStreamModuleImp(this)
}

TLClientNodeのパラメータとしてはとりあえず何となくわかりそうなものだが、ソースIDはTileLinkの識別IDを設定している。 これはどんな自由なモジュールでもこの調子で名前を付けていいのかしら?

次にDMAのステートマシンを作る。これも上記のチュートリアルの写経だが...

  • src/main/scala/example/InputStream.scala
class InputStreamModuleImp(outer: InputStream) extends LazyModuleImp(outer) {
  val (tl, edge) = outer.dmanode.out(0)
  val addrBits = edge.bundle.addressBits
  val w = edge.bundle.dataBits
  val beatBytes = (w / 8)

  val io = IO(new Bundle {
    val in = Flipped(Decoupled(UInt(w.W)))
  })

  val addr = Reg(UInt(addrBits.W))
  val len = Reg(UInt(addrBits.W))
  val running = RegInit(false.B)
...

やっていることとしては、IOポートから入ってくるデータを、tlポート(TileLinkポート)に出力している。

以下のコードが肝になるらしい?TileLinkのAポートでValidを出している。

  • src/main/scala/example/InputStream.scala
...
  io.in.ready := canIssue && tl.a.ready
  tl.a.valid  := canIssue && io.in.valid
  tl.a.bits   := edge.Put(
    fromSource = OHToUInt(xactOnehot),
    toAddress = addr,
    lgSize = log2Ceil(beatBytes).U,
    data = io.in.bits)._2
  tl.d.ready := running && xactBusy.orR
...

SoCへの組み込み

次に、SoC環境へのDMAコントローラの追加を行う。

参考にしたのは以下のチュートリアル

Connecting Devices to Bus — FireSim documentation

以下のようなtraitを作る。BaseSubsystemに対してデバイスを追加する。アドレスは 0x10017000 だ。

BaseSubsystemはシステムバスsbusペリフェラルバスのpbus、さらに割り込み用のバスとしてibusを持っている。 pbusレジスタノードに接続し、sbusをDMAノード、割り込みノードをibusに追加する。

  • src/main/scala/example/InputStream.scala
trait HasPeripheryInputStream { this: BaseSubsystem =>
  private val portName = "input-stream"
  val streamWidth = pbus.beatBytes * 8
  val inputstream = LazyModule(new InputStream(0x10017000, pbus.beatBytes))
  pbus.toVariableWidthSlave(Some(portName)) { inputstream.regnode }
  sbus.fromPort(Some(portName))() := inputstream.dmanode
  ibus.fromSync := inputstream.intnode
}
trait HasPeripheryInputStreamModuleImp extends LazyModuleImp {
  val outer: HasPeripheryInputStream

  val stream_in = IO(Flipped(Decoupled(UInt(outer.streamWidth.W))))
  outer.inputstream.module.io.in <> stream_in

  def connectFixedInput(data: Seq[BigInt]) {
    val fixed = Module(new FixedInputStream(data, outer.streamWidth))
    stream_in <> fixed.io.out
  }
}

そしてTopへの接続だ。さらにシミュレーション環境を接続して完了となる。

  • src/main/scala/example/Top.scala
class ExampleTopWithInputStream(implicit p: Parameters) extends ExampleTop
    with HasPeripheryInputStream {
  override lazy val module = new ExampleTopWithInputStreamModule(this)
}

class ExampleTopWithInputStreamModule(outer: ExampleTopWithInputStream)
  extends ExampleTopModuleImp(outer)
  with HasPeripheryInputStreamModuleImp
  • src/main/scala/example/Configs.scala
class WithFixedInputStream extends Config((site, here, up) => {
  case BuildTop => (clock: Clock, reset: Bool, p: Parameters) => {
    val top = Module(LazyModule(new ExampleTopWithInputStream()(p)).module)
    top.connectFixedInput(Seq(
      BigInt("1002abcd", 16),
      BigInt("34510204", 16),
      BigInt("10329999", 16),
      BigInt("92101222", 16)))
    top
  }
})

class FixedInputStreamConfig extends Config(
  new WithFixedInputStream ++ new BaseExampleConfig)

最後にVerilogを出力してみたのだが、FixedInputStreamがないと怒られる。たしかに、資料を探してもFixedInputStreamが見当たらない。自分で作らないといけないのかなあ?

$  make CONFIG=FixedInputStreamConfig
mkdir -p /home/msyksphinz/work/firechip/verisim/generated-src
cd /home/msyksphinz/work/firechip && java -Xmx2G -Xss8M -XX:MaxPermSize=256M -jar /home/msyksphinz/work/firechip/rocket-chip/sbt-launch.jar "runMain example.Generator  /home/msyksphinz/work/firechip/verisim/generated-src example TestHarness example FixedInputStreamConfig"
OpenJDK 64-Bit Server VM warning: ignoring option MaxPermSize=256M; support was removed in 8.0
[info] Loading project definition from /home/msyksphinz/work/firechip/project
[info] Loading settings from build.sbt ...
[info] Loading settings from build.sbt ...
[info] Loading settings from build.sbt ...
[info] Loading settings from build.sbt ...
[info] Loading settings from plugins.sbt ...
[info] Loading project definition from /home/msyksphinz/work/firechip/rocket-chip/project
[info] Loading settings from build.sbt ...
[info] Loading settings from build.sbt ...
[info] Loading settings from build.sbt ...
Using addons:
[info] Set current project to example (in build file:/home/msyksphinz/work/firechip/)
[warn] Multiple main classes detected.  Run 'show discoveredMainClasses' to see the list
[info] Compiling 3 Scala sources to /home/msyksphinz/work/firechip/target/scala-2.11/classes ...
[error] /home/msyksphinz/work/firechip/src/main/scala/example/InputStream.scala:30:28: not found: type FixedInputStream
[error]     val fixed = Module(new FixedInputStream(data, outer.streamWidth))
[error]                            ^
[error] one error found
[error] (Compile / compileIncremental) Compilation failed
[error] Total time: 6 s, completed Jun 26, 2018 11:18:11 PM
/home/msyksphinz/work/firechip/Makefrag:28: recipe for target '/home/msyksphinz/work/firechip/verisim/generated-src/example.TestHarness.FixedInputStreamConfig.fir' failed
make: *** [/home/msyksphinz/work/firechip/verisim/generated-src/example.TestHarness.FixedInputStreamConfig.fir] Error 1
f:id:msyksphinz:20180627003813p:plain

2018/06/28追加。MLで聞いてみるとFixedInputStreamの実装を教えてくれた。以下を追加した。

  • src/main/scala/example/InputStream.scala
class FixedInputStream(data: Seq[BigInt], w: Int) extends Module {
    val io = IO(new Bundle {
        val out = Decoupled(UInt(w.W))
    })

    val s_start :: s_send :: s_done :: Nil = Enum(3)
    val state = RegInit(s_start)

    val dataVec = VecInit(data.map(_.U(w.W)))
    val (sendIdx, sendDone) = Counter(io.out.fire(), data.size)

    io.out.valid := (state === s_send)
    io.out.bits := dataVec(sendIdx)

    when (state === s_start) { state := s_send }
    when (sendDone) { state := s_done }
}

コンパイルが完了すると、チュートリアルの通りにテストプログラムを作成し、コンパイルする。 実行すると、以下のようになった。正しく動いている。

$ ./simulator-example-FixedInputStreamConfig ../tests/input-stream.riscv
000000001002abcd
0000000034510204
0000000010329999
0000000092101222

何だこの出力は?と思うかもしれないが、これは最初にChiselでFixした入力信号なのでそれが出力されているだけだ。

  • src/main/scala/example/Config.scala
class WithFixedInputStream extends Config((site, here, up) => {
  case BuildTop => (clock: Clock, reset: Bool, p: Parameters) => {
    val top = Module(LazyModule(new ExampleTopWithInputStream()(p)).module)
    top.connectFixedInput(Seq(
      BigInt("1002abcd", 16),
      BigInt("34510204", 16),
      BigInt("10329999", 16),
      BigInt("92101222", 16)))
    top
  }
})

これだけでは単純にテストベクタが固まっているので、もっとカスタマイズして実行したい。