前回に引き続きChiselで作るお試しNICの話。 2回目はDecoder部分について。
NICDecoder
前回記載したブロック図中のDecoder部分から。名前はNICDecoder
にしてます。
ソースコード
まずはソースコードを。
- NICDecoder.scala
// 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
)。DecoupledIO
はMaster
ポート視点の入出力になっているため、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.in
をenq
側に接続しています。
val q = Queue(io.in, 1, !sliceEn, !sliceEn)
以前の記事で調べた内容をまとめましたが、Queue
の第3/第4引数の指定を変えてやると、Queue
のenq
側のvalid
/ready
の制御を組み合わせ論理にするか、レジスタを入れるかを変更することが出来ます。
この第3/第4引数をパラメータのsliceEn
で制御してやることでレジスタを入れるかを決定しています。
出力のvalid
の選択
以下が出力のvalid
端子の制御処理です。
先ほど書いたとおりio.out
はVec
になっているため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
Queue
のdeq
(出力)側の接続は以下の2段階で処理を行います。
deq
の信号とout
を全部接続valid
のみchosen
を使って再接続
Chiselでは最後に接続した信号が最終的な接続になるので、上記のようにすることでvalid
以外の信号は<>
でまとめて接続した後に、制御の変更が必要なvalid
のみ接続を変更することが可能です。
Queue
のdeq.ready
の制御
Queue
のdeq
側のready
信号にはdst
信号によって選択されたポートのready
が反映される必要があります。
その条件を満たすためにchosen_readies
信号に各ポートの選択情報とready
信号を接続してタプルでまとめて管理しています。
このようにして作ったchosen_readies
はSeq[(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
の動作の波形を。
文中で言及したQueue
の動作に関する記事は以下です。興味があればご覧ください。