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

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

Chisel Bootcamp - Module2.2 (2) - 練習問題

スポンサーリンク

前回の記事ではChisel BootcampのModule2.2の学習を終え、残りは練習問題のみというところまで進めた。

www.tech-diningyo.info

今回はModule2.2の残りの練習問題を見ていく。

Module 2.2: 組み合わせ回路

練習問題

これを始めるにあたって以下の注意が記されている。

この練習問題を解くために、Chisel cheetsheetを参照する必要があるかもしれない

このcheatsheetだが、Chiselの文法がとっても綺麗にまとめてあるので、手元に置いておくと開発のお供に役に立つこと間違い無しだ。 是非参照することをオススメする。

では練習問題へ。。

なお、これまでの記事においても練習問題を載せる場合は問題についてはそれなりに訳した本文を載せ、答えについては基本隠すようにしてきた。 これ以降の記事でも、この方針に従って進めていこうと思う。

練習問題:MAC

MACの機能((A*B)+C)を持ったChiselのモジュールを作成し、テストにパスするようにせよ。

以前の練習問題と同じく、部分的に不完全なコード(以下の???)が用意してあって、その不完全な部分を埋める課題となっている。

class MAC extends Module {
  val io = IO(new Bundle {
    val in_a = Input(UInt(4.W))
    val in_b = Input(UInt(4.W))
    val in_c = Input(UInt(4.W))
    val out  = Output(UInt(8.W))
  })

  ???
}
class MACTester(c: MAC) extends PeekPokeTester(c) {
  val cycles = 100
  import scala.util.Random
  for (i <- 0 until cycles) {
    val in_a = Random.nextInt(16)
    val in_b = Random.nextInt(16)
    val in_c = Random.nextInt(16)
    poke(c.io.in_a, in_a)
    poke(c.io.in_b, in_b)
    poke(c.io.in_c, in_c)
    expect(c.io.out, in_a*in_b+in_c)
  }
}
assert(Driver(() => new MAC) {c => new MACTester(c)})
println("SUCCESS!!")

以下は自分が試した解答。見たくない場合は展開しないようにご注意を。

解答

class MAC extends Module {
  val io = IO(new Bundle {
    val in_a = Input(UInt(4.W))
    val in_b = Input(UInt(4.W))
    val in_c = Input(UInt(4.W))
    val out  = Output(UInt(8.W))
  })

  val mul = io.in_a * io.in_b
  io.out := mul + io.in_c
}
class MACTester(c: MAC) extends PeekPokeTester(c) {
  val cycles = 100
  import scala.util.Random
  for (i <- 0 until cycles) {
    val in_a = Random.nextInt(16)
    val in_b = Random.nextInt(16)
    val in_c = Random.nextInt(16)
    poke(c.io.in_a, in_a)
    poke(c.io.in_b, in_b)
    poke(c.io.in_c, in_c)
    expect(c.io.out, in_a*in_b+in_c)
  }
}
assert(Driver(() => new MAC) {c => new MACTester(c)})
println("SUCCESS!!")

そして出力

[info] [0.001] Elaborating design...
[info] [0.703] Done elaborating.
Total FIRRTL Compile Time: 220.6 ms
Total FIRRTL Compile Time: 11.7 ms
End of dependency graph
Circuit state created
[info] [0.001] SEED 1539871064250
test cmd2HelperMAC Success: 100 tests passed in 5 cycles taking 0.039588 seconds
[info] [0.031] RAN 0 CYCLES PASSED
SUCCESS!!

練習問題:アービター

以下の回路はFIFOのデータを2つのパラレルな処理ユニットへ調停する。FIFOと処理ユニット(PEs)はready-validのインターフェースによって通信する。PEがreadyの際にFIFOのデータが送信されるようにアービターを設計せよ。

なお両方のPEがreadyの場合にはPE0が優先されるものとする。アービターはどちらか一方のPEがreadyの場合にはFIFOにデータを要求するようにすることを忘れないこと。また、データがvalidになる前にPEがreadyになったことを通知することを待つ必要がある。この練習問題を解くためにはおそらくバイナリ演算子が必要になるだろう。

という問題だ。

”以下の回路”はこれ↓。

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

まとめると以下のようになる。

  • 以下の仕様を満たすアービターを設計せよ
    • PEがreadyの際にFIFOのデータをPEに供給する
    • 両方のPEがreadyの場合はPE0に優先権がある
    • FIFOのデータがreadyになる前にPEがreadyになった場合は、PEへのvalidを待つ必要がある

これに対するコードは以下。

class Arbiter extends Module {
  val io = IO(new Bundle {
    // FIFO
    val fifo_valid = Input(Bool())
    val fifo_ready = Output(Bool())
    val fifo_data  = Input(UInt(16.W))
    
    // PE0
    val pe0_valid  = Output(Bool())
    val pe0_ready  = Input(Bool())
    val pe0_data   = Output(UInt(16.W))
    
    // PE1
    val pe1_valid  = Output(Bool())
    val pe1_ready  = Input(Bool())
    val pe1_data   = Output(UInt(16.W))
  })

  ???  
}
class ArbiterTester(c: Arbiter) extends PeekPokeTester(c) {
  import scala.util.Random
  val data = Random.nextInt(65536)
  poke(c.io.fifo_data, data)
  
  for (i <- 0 until 8) {
    poke(c.io.fifo_valid, (i>>0)%2)
    poke(c.io.pe0_ready,  (i>>1)%2)
    poke(c.io.pe1_ready,  (i>>2)%2)

    expect(c.io.fifo_ready, i>1)
    expect(c.io.pe0_valid,  i==3 || i==7)
    expect(c.io.pe1_valid,  i==5)
    
    if (i == 3 || i ==7) {
      expect(c.io.pe0_data, data)
    } else if (i == 5) {
      expect(c.io.pe1_data, data)
    }
  }
}
assert(Driver(() => new Arbiter) {c => new ArbiterTester(c)})
println("SUCCESS!!")

”バイナリ演算子が必要になるだろう”とのことなのでcheetsheetをここで確認しておく。

使いそうなのは以下ですかね。

Chisel Explanation Width
!x Logical NOT 1
x && y Logical AND 1
`x y` | Logical OR | 1

以下は自分が試した解答。繰り返しになるが見たくない場合は展開しないようにご注意を。

解答

class Arbiter extends Module {
  val io = IO(new Bundle {
    // FIFO
    val fifo_valid = Input(Bool())
    val fifo_ready = Output(Bool())
    val fifo_data  = Input(UInt(16.W))
    
    // PE0
    val pe0_valid  = Output(Bool())
    val pe0_ready  = Input(Bool())
    val pe0_data   = Output(UInt(16.W))
    
    // PE1
    val pe1_valid  = Output(Bool())
    val pe1_ready  = Input(Bool())
    val pe1_data   = Output(UInt(16.W))
  })

  io.fifo_ready := (io.pe0_ready || io.pe1_ready)
  io.pe0_valid := io.fifo_valid && io.pe0_ready
  io.pe0_data := io.fifo_data
  io.pe1_valid := io.fifo_valid && (!io.pe0_valid) && io.pe1_ready
  io.pe1_data := io.fifo_data
}
class ArbiterTester(c: Arbiter) extends PeekPokeTester(c) {
  import scala.util.Random
  val data = Random.nextInt(65536)
  poke(c.io.fifo_data, data)
  
  for (i <- 0 until 8) {
    poke(c.io.fifo_valid, (i>>0)%2)
    poke(c.io.pe0_ready,  (i>>1)%2)
    poke(c.io.pe1_ready,  (i>>2)%2)

    expect(c.io.fifo_ready, i>1)
    expect(c.io.pe0_valid,  i==3 || i==7)
    expect(c.io.pe1_valid,  i==5)
    
    if (i == 3 || i ==7) {
      expect(c.io.pe0_data, data)
    } else if (i == 5) {
      expect(c.io.pe1_data, data)
    }
  }
}
assert(Driver(() => new Arbiter) {c => new ArbiterTester(c)})
println("SUCCESS!!")

そして出力

[info] [0.000] Elaborating design...
[info] [0.010] Done elaborating.
Total FIRRTL Compile Time: 19.0 ms
Total FIRRTL Compile Time: 14.6 ms
End of dependency graph
Circuit state created
[info] [0.001] SEED 1539872134453
test cmd6HelperArbiter Success: 27 tests passed in 5 cycles taking 0.010229 seconds
[info] [0.009] RAN 0 CYCLES PASSED
SUCCESS!!

練習問題:パラメタライズ版加算器(オプション)

オプション扱いなのだが、一応やってみる。

ということで問題文は以下。

この追加の練習問題はChiselの最も強力な機能の一つでもある、パラメタライゼーションを見せることになる。これを試すために以下の機能を持つパラメタライズ版加算器を設計してみよう

  • オーバフロー発生時に、以下のいずれかを持つ加算器
    • 飽和させる
    • 結果を切り捨てる

まず最初にModuleを見てみよう。Moduleに渡したパラメータにsaturateと呼ばれるBooleanデータがある。これはScalaBooleanでありChiselのBoolではないので、単一のハードウェアで上記の2つの機能をもったものは作れない。しかしジェネレータをつるくことによって、飽和機能を持った加算器と切り捨て機能を持った加算器の両方を作成することが出来る。これらの機能はコンパイル時に決定される

次に加算器の入出力が4-bitのUIntであることに注目してほしい。Chiselはビルドインのビット幅推論機を持っている。もしあなたがcheetsheetを見ているなら、通常の加算におけるビット幅は2つの入力の最大のビット幅になるのを見つけただろう。これはつまり

scala val sum = io.in_a + io.in_b

という演算は、4-bit同士の演算であり、加算後の値は4-bitの入力に合わせて切り捨てられることを意味する。加算処理時に飽和するかどうかを確認すべきであれば、計算結果が5-bitの信号となる場所を用意する必要がある。これは加算時に+&を使用することで可能になるということがcheetsheetに載っているはずだ。

scala val sum = io.in_a +& io.in_b

最後に、4-bitのUIntの信号に5-bitのUIntの信号を接続するとデフォルトの動きとしてMSBが切り捨てられる。これを利用することにてって、非飽和の加算器の結果を切り捨てることが出来る。

上記で触れられているオーバーフローのビットを含んだ加算はcheetsheetでは以下のように記載されている。

Chisel Explanation Width
+& Additon max(w(x),w(y))+1
-& Subtraction max(w(x),w(y))+1

見てのとおりだが、演算結果のビット幅が演算時の2つのデータの最大のビット幅+1になる。

さて問題のコードを見てみよう。

class ParameterizedAdder(saturate: Boolean) extends Module {
  val io = IO(new Bundle {
    val in_a = Input(UInt(4.W))
    val in_b = Input(UInt(4.W))
    val out  = Output(UInt(4.W))
  })

  ???
}
class ParameterizedAdderTester(c: ParameterizedAdder, saturate: Boolean) extends PeekPokeTester(c) {
  // 100 random tests
  val cycles = 100
  import scala.util.Random
  import scala.math.min
  for (i <- 0 until cycles) {
    val in_a = Random.nextInt(16)
    val in_b = Random.nextInt(16)
    poke(c.io.in_a, in_a)
    poke(c.io.in_b, in_b)
    if (saturate) {
      expect(c.io.out, min(in_a+in_b, 15))
    } else {
      expect(c.io.out, (in_a+in_b)%16)
    }
  }
  
  // ensure we test saturation vs. truncation
  poke(c.io.in_a, 15)
  poke(c.io.in_b, 15)
  if (saturate) {
    expect(c.io.out, 15)
  } else {
    expect(c.io.out, 14)
  }
}
for (saturate <- Seq(true, false)) {
  assert(Driver(() => new ParameterizedAdder(saturate)) {c => new ParameterizedAdderTester(c, saturate)})
}
println("SUCCESS!!")

解答

class ParameterizedAdder(saturate: Boolean) extends Module {
  val io = IO(new Bundle {
    val in_a = Input(UInt(4.W))
    val in_b = Input(UInt(4.W))
    val out  = Output(UInt(4.W))
  })

  val sum = io.in_a +& io.in_b
  if (saturate) {
    io.out := Mux(sum > 15.U, 15.U, sum)  // 飽和したのでクリップ
  }
  else {
    io.out := sum                         // sumを入れることでMSBであるbit[4]をカット
  }
}
class ParameterizedAdderTester(c: ParameterizedAdder, saturate: Boolean) extends PeekPokeTester(c) {
  // 100 random tests
  val cycles = 100
  import scala.util.Random
  import scala.math.min
  for (i <- 0 until cycles) {
    val in_a = Random.nextInt(16)
    val in_b = Random.nextInt(16)
    poke(c.io.in_a, in_a)
    poke(c.io.in_b, in_b)
    if (saturate) {
      expect(c.io.out, min(in_a+in_b, 15))
    } else {
      expect(c.io.out, (in_a+in_b)%16)
    }
  }
  
  // ensure we test saturation vs. truncation
  poke(c.io.in_a, 15)
  poke(c.io.in_b, 15)
  if (saturate) {
    expect(c.io.out, 15)
  } else {
    expect(c.io.out, 14)
  }
}
for (saturate <- Seq(true, false)) {
  assert(Driver(() => new ParameterizedAdder(saturate)) {c => new ParameterizedAdderTester(c, saturate)})
}
println("SUCCESS!!")

出力結果は以下の通り

[info] [0.000] Elaborating design...
[info] [0.052] Done elaborating.
Total FIRRTL Compile Time: 12.9 ms
Total FIRRTL Compile Time: 11.5 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1539874024771
test cmd7HelperParameterizedAdder Success: 101 tests passed in 5 cycles taking 0.015463 seconds
[info] [0.016] RAN 0 CYCLES PASSED
[info] [0.000] Elaborating design...
[info] [0.004] Done elaborating.
Total FIRRTL Compile Time: 7.5 ms
Total FIRRTL Compile Time: 5.2 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1539874024877
test cmd7HelperParameterizedAdder Success: 101 tests passed in 5 cycles taking 0.009467 seconds
[info] [0.009] RAN 0 CYCLES PASSED
SUCCESS!!

これでModule2.2も全て終了。次回からはModule2.3に取り組んでいこう。https://www.tech-diningyo.info/entry/2018/10/18/235201