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

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

Chisel Bootcamp - Module2.3 (3) - フロー制御(練習問題)

スポンサーリンク

前回の記事でChisel BootcampのModule2.3の学習自体が終わったところまで進んだ。

www.tech-diningyo.info

今回はModule2.3の練習問題に取り組んでいく。

Module 2.3: フロー制御

練習問題

多項式の計算回路

ということでModule2.3の練習問題に取り組んでいく。

ここでの練習問題は、以下の3つの多項式についてのものとなる。

  • x^ 2 - 2x +1
  • 2x^ 2 + 6x + 3
  • 4x^ 2 - 10x -5

これまでの練習問題と少し違っているのは、すぐにChiselのモジュールの作成に入らずにいくつかのステップを踏んで進めていくとことだ。

こうすることの意味はTDDによる開発を意識することにあるようだ。

ステップ1 - Scalaで各多項式を関数化

まずは最初のステップとして、Scalaで上記3つの多項式に対するチェックをパスできる多項式の計算処理を実装する。

def poly0(x: Int): Int = ???
def poly1(x: Int): Int = ???
def poly2(x: Int): Int = ???

assert(poly0(0) == 1)
assert(poly1(0) == 3)
assert(poly2(0) == -5)

assert(poly0(1) == 0)
assert(poly1(1) == 11)
assert(poly2(1) == -11)

見ての通りで、3つのpoly0~poly2の宣言のみが用意されており、処理自体は未実装となっている。またその後のassert文で簡単なチェックが出来るようになっている。

これまで同じく、下記は自分で実行した際の解答になるので見たくない場合は開かないように注意。

解答 - クリックすると開くでので、見たくない場合は開かないように注意。

def poly0(x: Int): Int = (x * x) - (2 * x) + 1
def poly1(x: Int): Int = (2 * x * x) + (6 * x) + 3
def poly2(x: Int): Int = (4 * x * x) - (10 * x) - 5

assert(poly0(0) == 1)
assert(poly1(0) == 3)
assert(poly2(0) == -5)

assert(poly0(1) == 0)
assert(poly1(1) == 11)
assert(poly2(1) == -11)

出力結果は以下の通り

defined function poly0
defined function poly1
defined function poly2

assert文が発火しなかったので、ただ単にpoly0~poly2が定義されたというメッセージが出るのみとなる。

ステップ2 - Scalaで各多項式の関数をまとめる

ステップ2では、ステップ1のpoly~poly2を一つのpoly関数にまとめていく。どの多項式を計算するかを選択するための引数selectが追加されている。

このステップもScalaで実装を行うように指示があるので、それに従おう。

def poly(select: Int, x: Int): Int = {
  ???
}

assert(poly(1, 0) == 3)
assert(poly(1, 1) == 11)
assert(poly(2, 1) == -11)

解答 - クリックすると開くでので、見たくない場合は開かないように注意。

def poly(select: Int, x: Int): Int = {
  val square = x * x
  if (select == 0) {
    square - (2 * x) + 1
  } else if (select == 1) {
    (2 * square) + (6 * x) + 3
  } else {
    (4 * square) - (10 * x) - 5
  }
}

assert(poly(1, 0) == 3)
assert(poly(1, 1) == 11)
assert(poly(2, 1) == -11)

あんまり説明文読んでなかったので普通に書きなおしたが、Bootcampの解答はselectの値に応じて先程のpoly0~poly2をそのまま呼ぶ形になっていた。(もともとの指示は"select the polynomial"になってる)

出力結果は以下の通り。これも先ほどと一緒でassertが発火していないので、polyが定義されたというメッセージのみ。

defined function poly

ステップ3 - Chiselでモジュールを作成

いよいよChiselのモジュールとして、Sort4モジュールを作成してみる。

指示もシンプルで、

以下のテンプレートを使って自分の回路を作ってみよう

となっている。

// compute the polynomial
class Polynomial extends Module {
  val io = IO(new Bundle {
    val select = Input(UInt(2.W))
    val x = Input(SInt(32.W))
    val fOfX = Output(SInt(32.W))
  })
    
  val result = Wire(SInt(32.W))  
  val square = Wire(SInt(32.W))  
  
  ???

  io.fOfX := result  
}

// verify that the computation is correct
class PolynomialTester(c: Polynomial) extends PeekPokeTester(c) {
  for(x <- 0 to 20) {
    for(select <- 0 to 2) {
      poke(c.io.select, select)
      poke(c.io.x, x)
      expect(c.io.fOfX, poly(select, x))
    }
  }
}

// Test Polynomial
val works = Driver(() => new Polynomial) {
  c => new PolynomialTester(c)
}
assert(works)        // Scala Code: if works == false, will throw an error
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!

解答 - クリックすると開くでので、見たくない場合は開かないように注意。

// compute the polynomial
class Polynomial extends Module {
  val io = IO(new Bundle {
    val select = Input(UInt(2.W))
    val x = Input(SInt(32.W))
    val fOfX = Output(SInt(32.W))
  })
    
  val result = Wire(SInt(32.W))  
  val square = Wire(SInt(32.W))  
  
    square := io.x * io.x
    
    when(io.select === 0.U) {
        result := square - (2.S * io.x) + 1.S
    }.elsewhen (io.select === 1.U) {
        result := (2.S * square) + (6.S * io.x) + 3.S
    }.otherwise {
        result := (4.S * square) - (10.S * io.x) - 5.S
    }

  io.fOfX := result  
}

// verify that the computation is correct
class PolynomialTester(c: Polynomial) extends PeekPokeTester(c) {
  for(x <- 0 to 20) {
    for(select <- 0 to 2) {
      poke(c.io.select, select)
      poke(c.io.x, x)
      expect(c.io.fOfX, poly(select, x))
    }
  }
}

// Test Polynomial
val works = Driver(() => new Polynomial) {
  c => new PolynomialTester(c)
}
assert(works)        // Scala Code: if works == false, will throw an error
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!

出力結果は以下の通り

[info] [0.000] Elaborating design...
[info] [0.106] Done elaborating.
Total FIRRTL Compile Time: 39.7 ms
Total FIRRTL Compile Time: 32.9 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1540117201319
test cmd5HelperPolynomial Success: 63 tests passed in 5 cycles taking 0.028744 seconds
[info] [0.024] RAN 0 CYCLES PASSED
SUCCESS!!

自分で1から書いてみるまであんまり意識しなかったけど、

// 間違い
when (条件1) {
  条件1がTrueの時の処理
} elsewhen (条件2) {
  条件2がTrueの時の処理
} otherwise {
  それ以外の時の処理    
}

じゃなくて、

// 正解
when (条件1) {
  条件1がTrueの時の処理
} .elsewhen (条件2) {
  条件2がTrueの時の処理
} .otherwise {
  それ以外の時の処理    
}

なのね(elsewhen/otherwiseの前に.が必要)

同じようなのがもうひとつあって、

// 間違い
when (io.in == 1.U)

じゃなくて

// 正解
when (io.in === 1.U)

になる(”一致する”を表すときは=が3つ)

Chiselのceahtsheetにも丁寧に"Equalitytriple equals"と書いてあった。

ステートマシン

次の練習問題はハードウェアの制御には欠かせないステートマシンだ。ここは問題分がしっかり書いてあるのでまとめて訳して引用させてもらう。

カルノー図を使っったステートマシンの最適化は面倒だが、それは合成ツールが解決してくれる。しかし、その結果生成されるのは直感的でなく読みづらいコードになりがちだ。だからここでは、Chiselのフロー制御と最終接続の法則を使うことで、もっと直感的なコードを書いてみよう。

グラッドの生徒はその過程において4つの状態を通過する。その4つはIdle、Coding、 Writing、Graduatingだ。これらのステートの遷移は3つの入力によって起きる。すなわち、コーヒー(Coffee)、 アイデアが浮かんだ時(Ideas)、彼らのアドバイザーからの進捗に対するプレッシャー(Pressure)だ。一度卒業(Graduate)すると、状態はIdleに戻る。このFSMダイアグラムを描くと以下の図になる。ラベルのついていない遷移(入力が無いときなど)は、現在のステートにとどまらずにIdleに戻る。入力の優先順位は、

  1. Coffee
  2. Ideas
  3. Pressure

の順だ。

そのため、IdleステートにおいてCoffeeとPressureの入力があった倍には、生徒はCodingステートに遷移する。

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

ステップ1:テスト作成

以下の準備されたコードの"???"に必要なコードを実装しよう。仕様については前項で引用した文と画像に従ったものを作れば良い。

// state map
def states = Map("idle" -> 0, "coding" -> 1, "writing" -> 2, "grad" -> 3)

// life is full of question marks
def gradLife (state: Int, coffee: Boolean, idea: Boolean, pressure: Boolean): Int = {
  var nextState = states("idle")
  ???
  nextState
}

// some sanity checks
(0 until states.size).foreach{ state => assert(gradLife(state, false, false, false) == states("idle")) }
assert(gradLife(states("writing"), true, false, true) == states("writing"))
assert(gradLife(states("idle"), true, true, true) == states("coding"))
assert(gradLife(states("idle"), false, true, true) == states("idle"))
assert(gradLife(states("grad"), false, false, false) == states("idle"))

解答 - クリックすると開くでので、見たくない場合は開かないように注意。

// state map
def states = Map("idle" -> 0, "coding" -> 1, "writing" -> 2, "grad" -> 3)

// life is full of question marks
def gradLife (state: Int, coffee: Boolean, idea: Boolean, pressure: Boolean): Int = {
  var nextState = states("idle")
  if (state == states("idle")) {
    if (coffee) {
      nextState = states("coding")
    } else if (idea) {
      nextState = states("idle")
    } else if (pressure) {
      nextState = states("writing")
    }
  } else if (state == states("coding")) {
    if (coffee) {
      nextState = states("coding")
    } else if (pressure || idea){
      nextState = states("writing")
    }
  } else if (state == states("writing")) {
    if (coffee || idea) {
      nextState = states("writing")
    } else if (pressure) {
      nextState = states("grad")
    }
  } else {
    nextState = states("idle")
  }
   
  nextState
}

// some sanity checks
(0 until states.size).foreach{ state => assert(gradLife(state, false, false, false) == states("idle")) }
assert(gradLife(states("writing"), true, false, true) == states("writing"))
assert(gradLife(states("idle"), true, true, true) == states("coding"))
assert(gradLife(states("idle"), false, true, true) == states("idle"))
assert(gradLife(states("grad"), false, false, false) == states("idle"))

出力結果は以下の通り

defined function states
defined function gradLife

ステップ1:ChiselでFSM作成

ここも本文を引用させていただく。

まだ順序回路を学んでいないので、先にScalaの関数で作ったgradLifeのように、現在のステートはModuleへの入力として与え、その次に遷移するステートを出力する回路を作成することにする。

Chiselはステートマシンをマッピングするのに適したEnumという関数を持っている。これを使うことで、各ステートをUIntリテラルとして扱える。

Chiselの一致判断は3つの=になることを忘れないように!

うん、イコール3つは、さっきまさにハマったやつだね。

// life gets hard-er
class GradLife extends Module {
  val io = IO(new Bundle {
    val state = Input(UInt(2.W))
    val coffee = Input(Bool())
    val idea = Input(Bool())
    val pressure = Input(Bool())
    val nextState = Output(UInt(2.W))
  })
    
  val idle :: coding :: writing :: grad :: Nil = Enum(4)
  
  io.nextState := idle
  ???
}

// verify that the hardware matches the golden model
class GradLifeSim(c: GradLife) extends PeekPokeTester(c) {
  for (state <- 0 to 3) {
    for (coffee <- List(true, false)) {
      for (idea <- List(true, false)) {
        for (pressure <- List(true, false)) {
          poke(c.io.state, state)
          poke(c.io.coffee, coffee)
          poke(c.io.idea, idea)
          poke(c.io.pressure, pressure)
          expect(c.io.nextState, gradLife(state, coffee, idea, pressure))
        }
      }
    }
  }
}

// Test
val works = Driver(() => new GradLife) {c => new GradLifeSim(c)}
assert(works)        // Scala Code: if works == false, will throw an error
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!

この練習問題は、先ほどScalaで実装したgradLifeをそのまんまChiselのコードに置き換えればそれでオッケー。

解答 - クリックすると開くでので、見たくない場合は開かないように注意。

// life gets hard-er
class GradLife extends Module {
  val io = IO(new Bundle {
    val state = Input(UInt(2.W))
    val coffee = Input(Bool())
    val idea = Input(Bool())
    val pressure = Input(Bool())
    val nextState = Output(UInt(2.W))
  })
    
  val idle :: coding :: writing :: grad :: Nil = Enum(4)
  
  io.nextState := idle
    
  when (io.state === idle) {
    when (io.coffee) {
      io.nextState := coding 
    } .elsewhen (io.idea) {
      io.nextState := idle
    } .elsewhen (io.pressure) {
      io.nextState := writing
    }
  } .elsewhen (io.state === coding) {
    when (io.coffee) {
      io.nextState := coding 
    } .elsewhen (io.idea || io.pressure) {
      io.nextState := writing
    }
  } .elsewhen (io.state === writing) {
    when (io.coffee || io.idea) {
      io.nextState := writing
    } .elsewhen (io.pressure) {
      io.nextState := grad
    }
  } .otherwise {
    io.nextState := idle
  }
}

// verify that the hardware matches the golden model
class GradLifeSim(c: GradLife) extends PeekPokeTester(c) {
  for (state <- 0 to 3) {
    for (coffee <- List(true, false)) {
      for (idea <- List(true, false)) {
        for (pressure <- List(true, false)) {
          poke(c.io.state, state)
          poke(c.io.coffee, coffee)
          poke(c.io.idea, idea)
          poke(c.io.pressure, pressure)
          expect(c.io.nextState, gradLife(state, coffee, idea, pressure))
        }
      }
    }
  }
}

// Test
val works = Driver(() => new GradLife) {c => new GradLifeSim(c)}
assert(works)        // Scala Code: if works == false, will throw an error
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!

出力結果は以下の通り

[info] [0.000] Elaborating design...
[info] [0.067] Done elaborating.
Total FIRRTL Compile Time: 22.1 ms
Total FIRRTL Compile Time: 18.6 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1540131941742
test cmd10HelperGradLife Success: 32 tests passed in 5 cycles taking 0.013625 seconds
[info] [0.012] RAN 0 CYCLES PASSED
SUCCESS!!

これでModule2.3も全て終了。次の記事からModule2.4に入っていよいよ順序回路を勉強していく。