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

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

Chisel Bootcamp - 幕間(3) - ChiselのArbiter

スポンサーリンク

前回の記事ではChiselの標準ライブラリからQueueを紹介した。

www.tech-diningyo.info

今日も引き続きChiselの標準ライブラリを紹介していく。今日はChiselのアービターだ。

Module 3.2幕間: Chiselの標準ライブラリ

アービター

Chiselの標準ライブラリには2つのアービターが存在する。いずれのアービターも各々のモジュールの優先順位にした以外N個のDecoupledIOの入力から選択された一つのDecoupledIOを出力する。

  • Arbiter:インデックスの小さいほうが優先されるタイプのアービター
  • RRArbiter:ラウンド・ロビンで出力が選択されるタイプのアービター

なお標準ライブラリのアービターはいずれも組み合わせ回路で構成されることに注意が必要だ。

サンプルコード

早速サンプルコードを見ていこう。

Driver(() => new Module {
    // Example circuit using a priority arbiter
    val io = IO(new Bundle {
      val in = Flipped(Vec(2, Decoupled(UInt(8.W))))
      val out = Decoupled(UInt(8.W))
    })
    // Arbiter doesn't have a convenience constructor, so it's built like any Module
    val arbiter = Module(new Arbiter(UInt(8.W), 2))  // 2 to 1 Priority Arbiter
    arbiter.io.in <> io.in
    io.out <> arbiter.io.out
  }) { c => new PeekPokeTester(c) {
    poke(c.io.in(0).valid, 0)
    poke(c.io.in(1).valid, 0)
    println(s"Start:")
    println(s"\tin(0).ready=${peek(c.io.in(0).ready)}, in(1).ready=${peek(c.io.in(1).ready)}")
    println(s"\tout.valid=${peek(c.io.out.valid)}, out.bits=${peek(c.io.out.bits)}")
    poke(c.io.in(1).valid, 1)  // Valid input 1
    poke(c.io.in(1).bits, 42)
    // What do you think the output will be?
    println(s"valid input 1:")
    println(s"\tin(0).ready=${peek(c.io.in(0).ready)}, in(1).ready=${peek(c.io.in(1).ready)}")
    println(s"\tout.valid=${peek(c.io.out.valid)}, out.bits=${peek(c.io.out.bits)}")
    poke(c.io.in(0).valid, 1)  // Valid inputs 0 and 1
    poke(c.io.in(0).bits, 43)
    // What do you think the output will be? Which inputs will be ready?
    println(s"valid inputs 0 and 1:")
    println(s"\tin(0).ready=${peek(c.io.in(0).ready)}, in(1).ready=${peek(c.io.in(1).ready)}")
    println(s"\tout.valid=${peek(c.io.out.valid)}, out.bits=${peek(c.io.out.bits)}")
    poke(c.io.in(1).valid, 0)  // Valid input 0
    // What do you think the output will be?
    println(s"valid input 0:")
    println(s"\tin(0).ready=${peek(c.io.in(0).ready)}, in(1).ready=${peek(c.io.in(1).ready)}")
    println(s"\tout.valid=${peek(c.io.out.valid)}, out.bits=${peek(c.io.out.bits)}")
} }

前回のQueueと同じくテスターにモジュールの実装を直接渡している。このコードを実行すると(当然だが)テストにパスする結果が得られる。テスターに渡したアービターは2つの入力のDecoupledIOを持っており、各々のvalid端子を0x1にすることで転送の要求を行う。上記のテストコードではvalidを立てた際にどちらのDecoupledIOの値が出力されるかをチェックしている。

[info] [0.001] Elaborating design...
[info] [0.708] Done elaborating.
Total FIRRTL Compile Time: 230.5 ms
Total FIRRTL Compile Time: 39.3 ms
End of dependency graph
Circuit state created
[info] [0.001] SEED 1544881672950
[info] [0.002] Start:
[info] [0.003]     in(0).ready=1, in(1).ready=1
[info] [0.004]     out.valid=0, out.bits=117
[info] [0.005] valid input 1:
[info] [0.006]     in(0).ready=1, in(1).ready=1
[info] [0.007]     out.valid=1, out.bits=42
[info] [0.007] valid inputs 0 and 1:
[info] [0.008]     in(0).ready=1, in(1).ready=0
[info] [0.009]     out.valid=1, out.bits=43
[info] [0.010] valid input 0:
[info] [0.011]     in(0).ready=1, in(1).ready=0
[info] [0.011]     out.valid=1, out.bits=43
test cmd2Helperanonfun1anon2 Success: 0 tests passed in 5 cycles taking 0.025763 seconds
[info] [0.013] RAN 0 CYCLES PASSED

最初に書いたとおり、Arbiter組み合わせ論理で構成されるため、テストの実行結果の最後にあるようにサイクルが経過していないことがわかる。

使い方

以下が最初のサンプルコードからモジュールの定義部分のみを抜粋したものだ。

new Module {
    // Example circuit using a priority arbiter
    val io = IO(new Bundle {
      val in = Flipped(Vec(2, Decoupled(UInt(8.W))))
      val out = Decoupled(UInt(8.W))
    })
    // Arbiter doesn't have a convenience constructor, so it's built like any Module
    val arbiter = Module(new Arbiter(UInt(8.W), 2))  // 2 to 1 Priority Arbiter
    arbiter.io.in <> io.in
    io.out <> arbiter.io.out
}

使用するときは

  1. Module(new Arbiter("データのビット幅", "入力の数"))アービターインスタンス
  2. 各々の端子を<>で接続

となる。

Arbiterから生成されるRTL

確認のために上記のテストに入力したモジュールから生成されるRTLを見ておこう。

生成されるモジュールは以下の2つだ。

  • Arbiter : Chiselの標準ライブラリAribterから生成されたアービター
  • cmd4Helperanonfun1anon2 : Arbiterインスタンスしているラッパー階層のモジュール

必要なところには適宜コメントを入れたつもりなので、そちらも一緒に見てほしい。

module Arbiter( // @[:@3.2]
  output       io_in_0_ready, // @[:@6.4]
  input        io_in_0_valid, // @[:@6.4]
  input  [7:0] io_in_0_bits, // @[:@6.4]
  output       io_in_1_ready, // @[:@6.4]
  input        io_in_1_valid, // @[:@6.4]
  input  [7:0] io_in_1_bits, // @[:@6.4]
  input        io_out_ready, // @[:@6.4]
  output       io_out_valid, // @[:@6.4]
  output [7:0] io_out_bits // @[:@6.4]
);
  wire [7:0] _GEN_1; // @[Arbiter.scala 121:27:@10.4]
  wire  _T_70; // @[Arbiter.scala 31:78:@14.4]
  wire  _T_72; // @[Arbiter.scala 129:19:@17.4]
  wire  _T_74; // @[Arbiter.scala 130:19:@19.4]
  wire  _T_75; // @[Arbiter.scala 130:31:@20.4]
    
  // 出力のデータ選択部分 - インデックスの小さい"0"が優先される
  assign _GEN_1 = io_in_0_valid ? io_in_0_bits : io_in_1_bits; // @[Arbiter.scala 121:27:@10.4]
    
  // io_in_1_validの生成
  assign _T_70 = io_in_0_valid == 1'h0; // @[Arbiter.scala 31:78:@14.4]
  assign _T_72 = _T_70 & io_out_ready; // @[Arbiter.scala 129:19:@17.4]
  assign _T_74 = _T_70 == 1'h0; // @[Arbiter.scala 130:19:@19.4]
    
  // io_out_validの生成 - _T_70 == 1'h0なのでio_in_valid == 1'h1
  assign _T_75 = _T_74 | io_in_1_valid; // @[Arbiter.scala 130:31:@20.4]
  assign io_in_0_ready = io_out_ready;
  assign io_in_1_ready = _T_72;
  assign io_out_valid = _T_75;
  assign io_out_bits = _GEN_1;
endmodule
module cmd4Helperanonfun1anon2( // @[:@23.2]
  input        clock, // @[:@24.4]
  input        reset, // @[:@25.4]
  output       io_in_0_ready, // @[:@26.4]
  input        io_in_0_valid, // @[:@26.4]
  input  [7:0] io_in_0_bits, // @[:@26.4]
  output       io_in_1_ready, // @[:@26.4]
  input        io_in_1_valid, // @[:@26.4]
  input  [7:0] io_in_1_bits, // @[:@26.4]
  input        io_out_ready, // @[:@26.4]
  output       io_out_valid, // @[:@26.4]
  output [7:0] io_out_bits // @[:@26.4]
);
  wire  Arbiter_io_in_0_ready; // @[cmd4.sc 8:25:@28.4]
  wire  Arbiter_io_in_0_valid; // @[cmd4.sc 8:25:@28.4]
  wire [7:0] Arbiter_io_in_0_bits; // @[cmd4.sc 8:25:@28.4]
  wire  Arbiter_io_in_1_ready; // @[cmd4.sc 8:25:@28.4]
  wire  Arbiter_io_in_1_valid; // @[cmd4.sc 8:25:@28.4]
  wire [7:0] Arbiter_io_in_1_bits; // @[cmd4.sc 8:25:@28.4]
  wire  Arbiter_io_out_ready; // @[cmd4.sc 8:25:@28.4]
  wire  Arbiter_io_out_valid; // @[cmd4.sc 8:25:@28.4]
  wire [7:0] Arbiter_io_out_bits; // @[cmd4.sc 8:25:@28.4]
    
  // newで宣言したArbiterがインスタンされる
  Arbiter Arbiter ( // @[cmd4.sc 8:25:@28.4]
    .io_in_0_ready(Arbiter_io_in_0_ready),
    .io_in_0_valid(Arbiter_io_in_0_valid),
    .io_in_0_bits(Arbiter_io_in_0_bits),
    .io_in_1_ready(Arbiter_io_in_1_ready),
    .io_in_1_valid(Arbiter_io_in_1_valid),
    .io_in_1_bits(Arbiter_io_in_1_bits),
    .io_out_ready(Arbiter_io_out_ready),
    .io_out_valid(Arbiter_io_out_valid),
    .io_out_bits(Arbiter_io_out_bits)
  );
    
  // "<>"で接続したとおりにRTL上で結線される。
  assign io_in_0_ready = Arbiter_io_in_0_ready;
  assign io_in_1_ready = Arbiter_io_in_1_ready;
  assign io_out_valid = Arbiter_io_out_valid;
  assign io_out_bits = Arbiter_io_out_bits;
  assign Arbiter_io_in_0_valid = io_in_0_valid;
  assign Arbiter_io_in_0_bits = io_in_0_bits;
  assign Arbiter_io_in_1_valid = io_in_1_valid;
  assign Arbiter_io_in_1_bits = io_in_1_bits;
  assign Arbiter_io_out_ready = io_out_ready;
endmodule

ArbiterRRArbiterを使ったサンプル

ただ単に例を見るだけというのも味気ない&RRArbiterを使ってないので、両方のアービターを使って優先順位の選択を切り替え可能にしたアービター・ジェネレータを作ってみる。

class MyArbiter(InputNum: Int, DataWidth: Int, Type: Int = 0) extends Module {
  val io = IO(new Bundle {
    val in = Flipped(Vec(InputNum, Decoupled(UInt(DataWidth.W))))
    val out = Decoupled(UInt(DataWidth.W))
  })
    
  var arb = Module
  if (Type == 1) {
    val arb = Module(new Arbiter(UInt(DataWidth.W), InputNum))
    arb.io.in <> io.in
    io.out <> arb.io.out
  }
  else {
    val arb = Module(new RRArbiter(UInt(DataWidth.W), InputNum))
    arb.io.in <> io.in
    io.out <> arb.io.out
  }
}

一応、所望の機能にはなるんだけど、これだとarbの結線部分が野暮ったいんだよな。。。なんか方法はあるんだろうけどまだよくわからん。。。あとで調べることにしよう。

因みに上記でRRArbiterを有効にしてRTLを生成すると、以下の様に直前に選択された入力を保持するようなRTLが生成されていた。

module RRArbiter( // @[:@3.2]
  input        clock, // @[:@4.4]
  output       io_in_0_ready, // @[:@6.4]
  input        io_in_0_valid, // @[:@6.4]
  input  [7:0] io_in_0_bits, // @[:@6.4]
  output       io_in_1_ready, // @[:@6.4]
  input        io_in_1_valid, // @[:@6.4]
  input  [7:0] io_in_1_bits, // @[:@6.4]
  input        io_out_ready, // @[:@6.4]
  output       io_out_valid, // @[:@6.4]
  output [7:0] io_out_bits, // @[:@6.4]
  output       io_chosen // @[:@6.4]
);
  reg  lastGrant; // @[Reg.scala 11:16:@14.4]
  reg [31:0] _RAND_0;
  wire  _GEN_4; // @[Arbiter.scala 41:16:@11.4]
  wire [7:0] _GEN_5; // @[Arbiter.scala 41:16:@11.4]
  wire  _T_79; // @[Decoupled.scala 37:37:@13.4]
  wire  _GEN_6; // @[Reg.scala 12:19:@15.4]
  wire  grantMask_1; // @[Arbiter.scala 67:57:@19.4]
  wire  validMask_1; // @[Arbiter.scala 68:83:@21.4]
  wire  _T_84; // @[Arbiter.scala 31:68:@23.4]
  wire  _T_88; // @[Arbiter.scala 31:78:@25.4]
  wire  _T_90; // @[Arbiter.scala 31:78:@26.4]
  wire  _T_94; // @[Arbiter.scala 72:50:@30.4]
  wire  _T_95; // @[Arbiter.scala 60:21:@31.4]
  wire  _T_96; // @[Arbiter.scala 60:21:@33.4]
  wire  _GEN_7; // @[Arbiter.scala 77:27:@35.4]
  wire  choice; // @[Arbiter.scala 79:25:@38.4]
  assign _GEN_4 = io_chosen ? io_in_1_valid : io_in_0_valid; // @[Arbiter.scala 41:16:@11.4]
  assign _GEN_5 = io_chosen ? io_in_1_bits : io_in_0_bits; // @[Arbiter.scala 41:16:@11.4]
  assign _T_79 = io_out_ready & io_out_valid; // @[Decoupled.scala 37:37:@13.4]
  assign _GEN_6 = _T_79 ? io_chosen : lastGrant; // @[Reg.scala 12:19:@15.4]
  assign grantMask_1 = 1'h1 > lastGrant; // @[Arbiter.scala 67:57:@19.4]
  assign validMask_1 = io_in_1_valid & grantMask_1; // @[Arbiter.scala 68:83:@21.4]
  assign _T_84 = validMask_1 | io_in_0_valid; // @[Arbiter.scala 31:68:@23.4]
  assign _T_88 = validMask_1 == 1'h0; // @[Arbiter.scala 31:78:@25.4]
  assign _T_90 = _T_84 == 1'h0; // @[Arbiter.scala 31:78:@26.4]
  assign _T_94 = grantMask_1 | _T_90; // @[Arbiter.scala 72:50:@30.4]
  assign _T_95 = _T_88 & io_out_ready; // @[Arbiter.scala 60:21:@31.4]
  assign _T_96 = _T_94 & io_out_ready; // @[Arbiter.scala 60:21:@33.4]
  assign _GEN_7 = io_in_0_valid ? 1'h0 : 1'h1; // @[Arbiter.scala 77:27:@35.4]
  assign choice = validMask_1 ? 1'h1 : _GEN_7; // @[Arbiter.scala 79:25:@38.4]
  assign io_in_0_ready = _T_95;
  assign io_in_1_ready = _T_96;
  assign io_out_valid = _GEN_4;
  assign io_out_bits = _GEN_5;
  assign io_chosen = choice;
  // 最後に選択された入力を保持
  always @(posedge clock) begin
    if (_T_79) begin
      lastGrant <= io_chosen;
    end
  end
endmodule

後で調べることは残ってるんだけど、Arbiterの紹介としてはこれでおしまい。

次回は”ステートレス”な機能からビット単位の操作を行うユーティリティを紹介する予定。