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

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

Chiselで作るNIC - (2)- Decoder

スポンサーリンク

前回に引き続きChiselで作るお試しNICの話。 2回目はDecoder部分について。

NICDecoder

前回記載したブロック図中のDecoder部分から。名前はNICDecoderにしてます。

ソースコード

まずはソースコードを。

// See LICENSE for license details.

import chisel3._
import chisel3.util._

/**
  * NICDecoderのIOクラス
  * @param numOfOutput 出力ポートの数
  */
class NICDecoderIO(numOfOutput: Int) extends Bundle {
  val in = Flipped(Decoupled(new NICBaseData(numOfOutput)))
  val out = Vec(numOfOutput, Decoupled(new NICBaseData(numOfOutput)))

  override def cloneType: this.type =
    new NICDecoderIO(numOfOutput).asInstanceOf[this.type]
}

/**
  * NICDecoder
  * @param numOfOutput 出力ポートの数
  */
class NICDecoder(numOfOutput: Int, sliceEn: Boolean)
  extends Module {
  val io = IO(new NICDecoderIO(numOfOutput))

  val q = Queue(io.in, 1, !sliceEn, !sliceEn)

  val chosen_readies = Seq.fill(numOfOutput)(
    (Wire(Bool()), Wire(Bool())))

  for ((out_port, idx) <- io.out.zipWithIndex) {
    // qの出力のdstがポートのインデックスと一致すれば選択された状態
    val chosen = q.bits.dst === idx.U

    out_port <> q
    out_port.valid := chosen && q.valid

    // out側の各readyを格納
    chosen_readies(idx)._1 := chosen
    chosen_readies(idx)._2 := out_port.ready
  }

  // in側のreadyの接続
  q.ready := MuxCase(false.B, chosen_readies)

}

コメント読めば大体わかる気がしますが、解説していきます。

IO部分

以下のような形になってます。

class NICDecoderIO(numOfOutput: Int) extends Bundle {
  val in = Flipped(Decoupled(new NICBaseData(numOfOutput)))
  val out = Vec(numOfOutput, Decoupled(new NICBaseData(numOfOutput)))

  override def cloneType: this.type =
    new NICDecoderIO(numOfOutput).asInstanceOf[this.type]
}

上記の様にDecoupledオブジェクトでChiselのData型のインスタンスをくるむと、そのデータをbitsという変数にコピーし、ready/valid端子を追加したものが返却されます(型自体はDecoupledIO)。DecoupledIOMasterポート視点の入出力になっているため、Decoderの入力側として使用するためFlippedでポートの入出力を反転させています。

定義場所の関係で上記のソースコードには入っていませんがNICBaseDataの宣言は以下のようなものです。前回の"バスの仕様"に書いたデータに相当するものをBundleでまとめたものですね。

/**
  * 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]
}

ということで上記のNICDecoderIO.ioは以下のような構造を持つ変数になります。

  • in.valid : 入力
  • in.ready : 出力
  • in.bits.dst : 入力
  • in.bits.data : 入力

出力側はパラメータを使用して、所望のポート数のVecになっているだけですね。

入力用のレジスタスライス

ソースコード的には以下の部分です。以下のインスタンス処理時にio.inenq側に接続しています。

  val q = Queue(io.in, 1, !sliceEn, !sliceEn)

以前の記事で調べた内容をまとめましたが、Queueの第3/第4引数の指定を変えてやると、Queueenq側のvalid/readyの制御を組み合わせ論理にするか、レジスタを入れるかを変更することが出来ます。

この第3/第4引数をパラメータのsliceEnで制御してやることでレジスタを入れるかを決定しています。

出力のvalidの選択

以下が出力のvalid端子の制御処理です。

先ほど書いたとおりio.outVecになっているためforループで処理が可能です。

今回は出力先のポートの選択を以下の条件にしています。

  • io.in.bits.dstが出力ポートがのインデックスに一致

この処理を示しているのがchosen信号です。

  for ((out_port, idx) <- io.out.zipWithIndex) {
    // qの出力のdstがポートのインデックスと一致すれば選択された状態
    val chosen = q.bits.dst === idx.U

    // qのdeq側と出力ポートを接続
    out_port <> q
    out_port.valid := chosen && q.valid

Queuedeq(出力)側の接続は以下の2段階で処理を行います。

  1. deqの信号とoutを全部接続
  2. validのみchosenを使って再接続

Chiselでは最後に接続した信号が最終的な接続になるので、上記のようにすることでvalid以外の信号は<>でまとめて接続した後に、制御の変更が必要なvalidのみ接続を変更することが可能です。

Queuedeq.readyの制御

Queuedeq側のready信号にはdst信号によって選択されたポートのreadyが反映される必要があります。

その条件を満たすためにchosen_readies信号に各ポートの選択情報とready信号を接続してタプルでまとめて管理しています。

このようにして作ったchosen_readiesSeq[(Bool, Data)]という形になるので、そのままMuxCaseに渡すことが可能です。

  val chosen_readies = Seq.fill(numOfOutput)(
    (Wire(Bool()), Wire(Bool())))

  for ((out_port, idx) <- io.out.zipWithIndex) {
    // ~略~

    // out側の各readyを格納
    chosen_readies(idx)._1 := chosen
    chosen_readies(idx)._2 := out_port.ready
  }

  // in側のreadyの接続
  q.ready := MuxCase(false.B, chosen_readies)

NICDecoderの動作時の波形

最後にこのNICDecoderの動作の波形を。

f:id:diningyo-kpuku-jougeki:20190721213157p:plain
NICDecoderの動作波形

文中で言及したQueueの動作に関する記事は以下です。興味があればご覧ください。