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

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

Chiselで作るNIC - (4)- トップモジュール

スポンサーリンク

前回に引き続きChiselで作るお試しNICの話。 最後はDecoder/ArbiterをインスタンスするTopブロックについて。

NICTop

N個のDecoderとM個のArbiterを含む最上位階層。名前はNICTopにしてます。

ソースコード

まずはソースコードを。

// See LICENSE for license details.

import chisel3._
import chisel3.util._

/**
  * NIC用のデータクラス
  * @param numOfPort ポート数
  */
class NICBaseData(numOfPort: Int) extends Bundle {
  val dst = UInt(log2Ceil(numOfPort).W)
  val data = UInt(32.W)

  override def cloneType: this.type =
    new NICBaseData(numOfPort).asInstanceOf[this.type]
}

/**
  * ポートのパラメータ用クラス
  * @param sliceEn レジスタスライス設定
  */
class NICPortParams(val sliceEn: Boolean)

/**
  * NICのパラメータ用クラス
  * @param inParams 入力側の設定
  * @param outParams 出力側の設定
  */
class NICParams(
  val inParams: Seq[NICPortParams],
  val outParams: Seq[NICPortParams]
) {
  val numOfInput = inParams.length
  val numOfOutput = outParams.length
}

/**
  * NICTopのIOクラス
  * @param p NICの設定パラメータ
  */
class NICIO(val p: NICParams) extends Bundle {

  val numOfInput = p.inParams.length
  val numOfOutput = p.outParams.length
  val in = Vec(numOfInput,
    Flipped(Decoupled(new NICBaseData(numOfInput))))
  val out = Vec(numOfOutput,
    Decoupled(new NICBaseData(numOfOutput)))

  override def cloneType: this.type =
    new NICIO(p).asInstanceOf[this.type]
}

/**
  * NIC
  * @param p NICの設定パラメータ
  */
class NICTop(p: NICParams) extends Module {
  val io = IO(new NICIO(p))

  val decs = p.inParams.map(
    dp => Module(new NICDecoder(p.numOfOutput, dp.sliceEn)))
  val arbs = p.outParams.map(
    ap => Module(new NICArbiter(p.numOfInput, ap.sliceEn)))

  // io.in(n) <-> NICDecoder(n).io.in
  for ((in_port, dec) <- io.in zip decs) {
    dec.io.in <> in_port
  }

  // dec.io.out(M) <-> arb.io.in(N)
  for ((dec, i) <- decs.zipWithIndex; (arb, j) <- arbs.zipWithIndex) {
    dec.io.out(j) <> arb.io.in(i)
  }

  // io.out(N) <-> arb.io.out(N)
  for ((arb, out_port) <- arbs zip io.out) {
    out_port <> arb.io.out
  }
}

ここは結構書ける部分があるかも。

パラメタライズ用のパラメータ

この部分が今回一番試してみたかった部分。 NIC用のパラメータとしてNICParamsというクラスを定義して、それに渡すのはSeq[NICPortParams]というパラメータクラスのシーケンスにしてみた。

このようにしておくことでNIC内部の各Decoder/Arbiterの設定を個々に変更できるかという部分を確認してみる。

/**
  * ポートのパラメータ用クラス
  * @param sliceEn レジスタスライス設定
  */
class NICPortParams(val sliceEn: Boolean)

/**
  * NICのパラメータ用クラス
  * @param inParams 入力側の設定
  * @param outParams 出力側の設定
  */
class NICParams(
  val inParams: Seq[NICPortParams],
  val outParams: Seq[NICPortParams]
) {
  val numOfInput = inParams.length
  val numOfOutput = outParams.length
}

IOポート

次にIOポート用のクラスについてだが、NICになるので前回/前々回のDecoderとArbiterが入ることもありin/outともにVecになっている。

class NICIO(val p: NICParams) extends Bundle {

  val numOfInput = p.inParams.length
  val numOfOutput = p.outParams.length
  val in = Vec(numOfInput,
    Flipped(Decoupled(new NICBaseData(numOfInput))))
  val out = Vec(numOfOutput,
    Decoupled(new NICBaseData(numOfOutput)))

  override def cloneType: this.type =
    new NICIO(p).asInstanceOf[this.type]
}

Decoder/Arbiterのインスタンス

以下がDecoder/Arbiterのインスタンス部分だ。

  val decs = p.inParams.map(
    dp => Module(new NICDecoder(p.numOfOutput, dp.sliceEn)))
  val arbs = p.outParams.map(
    ap => Module(new NICArbiter(p.numOfInput, ap.sliceEn)))

NICTopのパラメータに渡ってきているNICParamsの中のDecoder/Arbiter用のパラメータが入っているので、これをmapを使ってひとつずつ取り出して、モジュールインスタンスしたものをdecs/arbsに格納することで所望の数のモジュールをインスタンスしている。

各モジュールの接続

最後に作ったdecs/arbsを接続する処理が以下のようになる。

  // io.in(n) <-> NICDecoder(n).io.in
  for ((in_port, dec) <- io.in zip decs) {
    dec.io.in <> in_port
  }

  // dec.io.out(M) <-> arb.io.in(N)
  for ((dec, i) <- decs.zipWithIndex; (arb, j) <- arbs.zipWithIndex) {
    dec.io.out(j) <> arb.io.in(i)
  }

  // io.out(N) <-> arb.io.out(N)
  for ((arb, out_port) <- arbs zip io.out) {
    out_port <> arb.io.out
  }

基本的な考え方は

  • 接続するポート同士を同時に取り出すためにzipを使ってforループで処理をする

ということだ。

今回は

  • decs(0)のM個の出力をarbs(0..M).in(0)に
  • decs(1)のM個の出力をarbs(0..M).in(1)に

という感じでdecsの出力ポートのインデックスとarbs(M)に入力のインデックスを一致させるようにループで処理を行い、接続した。

テスト

とりあえず簡単に動きを見てみたかったので、以下のようなテストを作成した。

// See LICENSE for license details.

import chisel3._
import chisel3.iotesters._

import scala.util.Random

/**
  * NICTopのユニットテスト用クラス
  * @param c テスモジュール
  */
class NICTopUnitTester(c: NICTop) extends PeekPokeTester(c) {

  val in = c.io.in
  val out = c.io.out

  val r = new Random(1)

  def idle(cycle: Int = 1): Unit = {
    for (in_port <- in) {
      poke(in_port.valid, false)
      poke(in_port.bits.dst, 0)
      poke(in_port.bits.data, 0)
    }

    for (out_port <- out) {
      poke(out_port.ready, true)
    }

    step(cycle)
  }

  /**
    * データ送信
    * @param src 入力ポート番号
    * @param dst 出力ポート番号
    * @param data 送信データ
    */
  def sendData(src: Int, dst: Int, data: BigInt): Unit = {
    poke(in(src).valid, true)
    poke(in(src).bits.dst, dst)
    poke(in(src).bits.data, data)
  }

  /**
    * データの期待値比較
    * @param dst 出力ポート番号
    * @param data 送信データ
    */
  def compareData(dst: Int, data: BigInt): Unit = {
    expect(out(dst).bits.dst, dst)
    expect(out(dst).bits.data, data)

    for ((out_port, idx) <- out.zipWithIndex) {
      val validExp = if (dst == idx) true else false
      expect(out_port.valid, validExp)
    }
  }
}

/**
  * NICTopのテストクラス
  */
class NICTopTester extends BaseTester {

  behavior of "NICTop"

  val testArgs = baseArgs :+ "-tn=NICTop"
  val numOfInput = 3
  val numOfOutput = 4

  /**
    * NICのパラメータ生成
    * @param numOfInput 入力ポート数
    * @param numOfOutput 出力ポート数
    * @param inSliceEn 入力のレジスタスライス設定
    * @param outSliceEn 出力のレジスタスライス設定
    * @return NICの入出力のパラメータ
    */
  def getPortParams(numOfInput: Int,
                    numOfOutput: Int,
                    inSliceEn: Boolean = false,
                    outSliceEn: Boolean = false
                   ): NICParams = {
    val inParams = Seq.fill(numOfInput)(new NICPortParams(inSliceEn))
    val outParams = Seq.fill(numOfOutput)(new NICPortParams(outSliceEn))

    new NICParams(inParams, outParams)
  }

  it should "io.in(N).validをHighにした時のio.in(N).dstに従って出力ポートが選択される" in {
    val p = getPortParams(numOfInput, numOfOutput)
    iotesters.Driver.execute(
      testArgs :+ "-td=test_run_dir/NICTop-000",
      () => new NICTop(p)) {
      c => new NICTopUnitTester(c) {

        idle(10)

        // 入力のnumOfInput * 出力の numOfOutput 回だけループし
        // 全ての入力ポート x 出力ポートへの転送を確認
        for (src <- 0 until numOfInput; dst <- 0 until numOfOutput) {
          val data = intToUnsignedBigInt(r.nextInt())
          sendData(src, dst, data)
          compareData(dst, data)

          step(1)
          poke(in(src).valid, false)

          step(1)

          idle(10)
        }
      }
    } should be (true)
  }
}

動作波形

上記のテスト実行時の動作波形は以下のようになった。 きちんと各入力ポートからの4回の送信がout(0), out(1), out(2), out(3)に振り分けられているのがわかるかと思う。

f:id:diningyo-kpuku-jougeki:20190724232459p:plain
NICTopの動作波形

4回位の記事で簡単なプロトコルを使ってNICを作ってみた。今回はNICPortParamsの中身はレジスタスライスを入れるかどうかだけの設定だが、ここにアドレスマップの設定等を入れてやれば、任意のアドレス範囲でアクセス先を制御することなども出来ると思う。 ということで、簡単なNICを作ってみた話でした。