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

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

Chisel Bootcamp - Module2.3 (2) - フロー制御(Wire)

スポンサーリンク

前回の記事でChisel BootcampのModule2.3に入りChiselのwhen/elsewhen/otherwiseについて勉強した。

www.tech-diningyo.info

今日からは引き続きModule2.3を進めていくが、今日扱うのChiselのWireについてだ。

Module 2.3: フロー制御

Wire宣言

前回の記事で扱ったwhenは”評価時に値を返さない”と書いた。今回出てくるChiselのWireはこの問題に対しての方法の一つとして存在している。Wire:=を使った接続の際の左辺/右辺のどちらに使ってもよいものとして定義されている。

という風に書いてあるのだが、いまいちピンとこないのでとりあえず早速例を見ていこう

Wireを使った4つの入力のソート

作るものは簡単で4つの入力をソートするモジュールになる。とても丁寧に図が付いているのでそれも使わせていただいた。以下のように4つの入力が出力される際にソートされるように並べ替える必要がある。なお図中の線の色は以下の意味を持っている。

  • 赤の線:左の値が小さい
  • 黒の線:左の値が大きいので、値がスワップされる

f:id:diningyo-kpuku-jougeki:20181020233516p:plain

これをChiselで実装すると以下のようになる。

/** Sort4 sorts its 4 inputs to its 4 outputs */
class Sort4 extends Module {
  val io = IO(new Bundle {
    val in0 = Input(UInt(16.W))
    val in1 = Input(UInt(16.W))
    val in2 = Input(UInt(16.W))
    val in3 = Input(UInt(16.W))
    val out0 = Output(UInt(16.W))
    val out1 = Output(UInt(16.W))
    val out2 = Output(UInt(16.W))
    val out3 = Output(UInt(16.W))
  })

  val row10 = Wire(UInt(16.W))
  val row11 = Wire(UInt(16.W))
  val row12 = Wire(UInt(16.W))
  val row13 = Wire(UInt(16.W))

  when(io.in0 < io.in1) {
    row10 := io.in0            // preserve first two elements
    row11 := io.in1
  }.otherwise {
    row10 := io.in1            // swap first two elements
    row11 := io.in0
  }

  when(io.in2 < io.in3) {
    row12 := io.in2            // preserve last two elements
    row13 := io.in3
  }.otherwise {
    row12 := io.in3            // swap last two elements
    row13 := io.in2
  }

  val row21 = Wire(UInt(16.W))
  val row22 = Wire(UInt(16.W))

  when(row11 < row12) {
    row21 := row11            // preserve middle 2 elements
    row22 := row12
  }.otherwise {
    row21 := row12            // swap middle two elements
    row22 := row11
  }

  val row20 = Wire(UInt(16.W))
  val row23 = Wire(UInt(16.W))
  when(row10 < row13) {
    row20 := row10            // preserve middle 2 elements
    row23 := row13
  }.otherwise {
    row20 := row13            // swap middle two elements
    row23 := row10
  }

  when(row20 < row21) {
    io.out0 := row20            // preserve first two elements
    io.out1 := row21
  }.otherwise {
    io.out0 := row21            // swap first two elements
    io.out1 := row20
  }

  when(row22 < row23) {
    io.out2 := row22            // preserve first two elements
    io.out3 := row23
  }.otherwise {
    io.out2 := row23            // swap first two elements
    io.out3 := row22
  }
}

// verify the inputs are sorted
class Sort4Tester(c: Sort4) extends PeekPokeTester(c) {
  poke(c.io.in0, 3)
  poke(c.io.in1, 6)
  poke(c.io.in2, 9)
  poke(c.io.in3, 12)
  expect(c.io.out0, 3)
  expect(c.io.out1, 6)
  expect(c.io.out2, 9)
  expect(c.io.out3, 12)

  poke(c.io.in0, 13)
  poke(c.io.in1, 4)
  poke(c.io.in2, 6)
  poke(c.io.in3, 1)
  expect(c.io.out0, 1)
  expect(c.io.out1, 4)
  expect(c.io.out2, 6)
  expect(c.io.out3, 13)
    
  poke(c.io.in0, 13)
  poke(c.io.in1, 6)
  poke(c.io.in2, 4)
  poke(c.io.in3, 1)
  expect(c.io.out0, 1)
  expect(c.io.out1, 4)
  expect(c.io.out2, 6)
  expect(c.io.out3, 13)
}

// Here's the tester
val works = iotesters.Driver(() => new Sort4) {
c => new Sort4Tester(c)
}
assert(works) // Scala Code: if works == false, will throw an error
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
println(getVerilog(new Sort4))

そうか、これまで使ってたのはScalavalだった。。 だから、初期化は出来ても代入が出来ないので今回みたいな処理が出来ないんだ。ピンとこないと書いたけどやっと理解できた。

上記のコードを実行すると以下のような結果が得られる。

[info] [0.000] Elaborating design...
[info] [0.018] Done elaborating.
Total FIRRTL Compile Time: 39.5 ms
Total FIRRTL Compile Time: 24.8 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1539525355736
test cmd6HelperSort4 Success: 12 tests passed in 5 cycles taking 0.012863 seconds
[info] [0.007] RAN 0 CYCLES PASSED
SUCCESS!!
[info] [0.000] Elaborating design...
[info] [0.009] Done elaborating.
Total FIRRTL Compile Time: 44.9 ms
module cmd6HelperSort4( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [15:0] io_in0, // @[:@6.4]
  input  [15:0] io_in1, // @[:@6.4]
  input  [15:0] io_in2, // @[:@6.4]
  input  [15:0] io_in3, // @[:@6.4]
  output [15:0] io_out0, // @[:@6.4]
  output [15:0] io_out1, // @[:@6.4]
  output [15:0] io_out2, // @[:@6.4]
  output [15:0] io_out3 // @[:@6.4]
);
  wire  _T_25; // @[cmd6.sc 18:15:@12.4]
  wire [15:0] row10; // @[cmd6.sc 18:25:@13.4]
  wire [15:0] row11; // @[cmd6.sc 18:25:@13.4]
  wire  _T_26; // @[cmd6.sc 26:15:@21.4]
  wire [15:0] row12; // @[cmd6.sc 26:25:@22.4]
  wire [15:0] row13; // @[cmd6.sc 26:25:@22.4]
  wire  _T_29; // @[cmd6.sc 37:14:@32.4]
  wire [15:0] row21; // @[cmd6.sc 37:23:@33.4]
  wire [15:0] row22; // @[cmd6.sc 37:23:@33.4]
  wire  _T_32; // @[cmd6.sc 47:14:@43.4]
  wire [15:0] row20; // @[cmd6.sc 47:23:@44.4]
  wire [15:0] row23; // @[cmd6.sc 47:23:@44.4]
  wire  _T_33; // @[cmd6.sc 55:14:@52.4]
  wire [15:0] _GEN_8; // @[cmd6.sc 55:23:@53.4]
  wire [15:0] _GEN_9; // @[cmd6.sc 55:23:@53.4]
  wire  _T_34; // @[cmd6.sc 63:14:@61.4]
  wire [15:0] _GEN_10; // @[cmd6.sc 63:23:@62.4]
  wire [15:0] _GEN_11; // @[cmd6.sc 63:23:@62.4]
  assign _T_25 = io_in0 < io_in1; // @[cmd6.sc 18:15:@12.4]
  assign row10 = _T_25 ? io_in0 : io_in1; // @[cmd6.sc 18:25:@13.4]
  assign row11 = _T_25 ? io_in1 : io_in0; // @[cmd6.sc 18:25:@13.4]
  assign _T_26 = io_in2 < io_in3; // @[cmd6.sc 26:15:@21.4]
  assign row12 = _T_26 ? io_in2 : io_in3; // @[cmd6.sc 26:25:@22.4]
  assign row13 = _T_26 ? io_in3 : io_in2; // @[cmd6.sc 26:25:@22.4]
  assign _T_29 = row11 < row12; // @[cmd6.sc 37:14:@32.4]
  assign row21 = _T_29 ? row11 : row12; // @[cmd6.sc 37:23:@33.4]
  assign row22 = _T_29 ? row12 : row11; // @[cmd6.sc 37:23:@33.4]
  assign _T_32 = row10 < row13; // @[cmd6.sc 47:14:@43.4]
  assign row20 = _T_32 ? row10 : row13; // @[cmd6.sc 47:23:@44.4]
  assign row23 = _T_32 ? row13 : row10; // @[cmd6.sc 47:23:@44.4]
  assign _T_33 = row20 < row21; // @[cmd6.sc 55:14:@52.4]
  assign _GEN_8 = _T_33 ? row20 : row21; // @[cmd6.sc 55:23:@53.4]
  assign _GEN_9 = _T_33 ? row21 : row20; // @[cmd6.sc 55:23:@53.4]
  assign _T_34 = row22 < row23; // @[cmd6.sc 63:14:@61.4]
  assign _GEN_10 = _T_34 ? row22 : row23; // @[cmd6.sc 63:23:@62.4]
  assign _GEN_11 = _T_34 ? row23 : row22; // @[cmd6.sc 63:23:@62.4]
  assign io_out0 = _GEN_8;
  assign io_out1 = _GEN_9;
  assign io_out2 = _GEN_10;
  assign io_out3 = _GEN_11;
endmodule

Verilogで考えると

  • Scalaval : 単純なassign
  • ChiselのWireassign文に3項演算子使って選択した信号を入れるようなケース

ってくらいの認識でとりあえず大丈夫そう。

この辺はどっかのタイミングでVerilog→Chiselの比較表みたいのを作ってみて把握するのが良いかな。

テストモジュールの書き換え

このWireのトピックの最後は”この4入力のソートモジュールのテストに使ったテスターはもっといい感じに書き換えれるよ”という話題で締めくくられている。

その”いい感じ”のテスターを作るにはScalaListを使って以下のように書くそうだ。

// verify the all possible ordering of 4 numbers are sorted
class BetterSort4Tester(c: Sort4) extends PeekPokeTester(c) {
  List(1, 2, 3, 4).permutations.foreach { case i0 :: i1 :: i2 :: i3 :: Nil =>
    println(s"Sorting $i0 $i1 $i2 $i3")
    poke(c.io.in0, i0)
    poke(c.io.in1, i1)
    poke(c.io.in2, i2)
    poke(c.io.in3, i3)
    expect(c.io.out0, 1)
    expect(c.io.out1, 2)
    expect(c.io.out2, 3)
    expect(c.io.out3, 4)
  }
}


// Here's the tester
val works = iotesters.Driver(() => new Sort4) {
c => new BetterSort4Tester(c)
}
assert(works) // Scala Code: if works == false, will throw an error
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!

確かに、、、だいぶシンプルになっている。流れとしては、以下の感じ。

  1. 1 - 4までを要素に持つListを作成
  2. Listpermutationsメソッド使ってListの要素を元に順列作ってIteratorを返却
    1. permutationsメソッドの詳細については以下のリンクを参照
    2. https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html#permutations:Iterator[Repr]
  3. 2.のIteratorforeachにわたって、Listのデータがi0-i3マッピングされる
  4. i0-i3pokeに渡してテスト実行
    1. モジュールの機能がソートなので、テストは固定値でOK

こういうテストの作り方が出来ると、総当りのテストとかも簡単に作れるのでいいですな。総当りじゃなくても、テストの入力データ&期待値を作っといてそれをループで処理することも出来るし。

Module2.3の学習自体はこれで終わりで、残りは練習問題となる。それは次の記事で行うことにして、今日はここまで。