前回の記事ではChisel Bootcampの引き続きModule3.1の学習を進め、Scalaの機能implicit
とそれをChiselに応用した例を学んだ。
Module3.1も7回目で、今日でついに終わりとなる。Module3.1の最後としてジェネレータの例を紹介する。
Module 3.1: ジェネレータ:パラメータ
ジェネレータの例
ここで紹介する例は1bitのミリ型のステートマシンだ。ベースになる仕様はWikipediaのこれ。
例題:ミリ型のステートマシン
まずはサクッとコードを記載。
// Mealy machine has case class BinaryMealyParams( // number of states nStates: Int, // initial state s0: Int, // function describing state transition stateTransition: (Int, Boolean) => Int, // function describing output output: (Int, Boolean) => Int ) { require(nStates >= 0) require(s0 < nStates && s0 >= 0) } class BinaryMealy(val mp: BinaryMealyParams) extends Module { val io = IO(new Bundle { val in = Input(Bool()) val out = Output(UInt()) }) val state = RegInit(UInt(), mp.s0.U) // output zero if no states io.out := 0.U for (i <- 0 until mp.nStates) { when (state === i.U) { when (io.in) { state := mp.stateTransition(i, true).U io.out := mp.output(i, true).U }.otherwise { state := mp.stateTransition(i, false).U io.out := mp.output(i, false).U } } } } // example from https://en.wikipedia.org/wiki/Mealy_machine val nStates = 3 val s0 = 2 def stateTransition(state: Int, in: Boolean): Int = { if (in) { 1 } else { 0 } } def output(state: Int, in: Boolean): Int = { if (state == 2) { return 0 } if ((state == 1 && !in) || (state == 0 && in)) { return 1 } else { return 0 } } val testParams = BinaryMealyParams(nStates, s0, stateTransition, output) class BinaryMealyTester(c: BinaryMealy) extends PeekPokeTester(c) { poke(c.io.in, false) expect(c.io.out, 0) step(1) poke(c.io.in, false) expect(c.io.out, 0) step(1) poke(c.io.in, false) expect(c.io.out, 0) step(1) poke(c.io.in, true) expect(c.io.out, 1) step(1) poke(c.io.in, true) expect(c.io.out, 0) step(1) poke(c.io.in, false) expect(c.io.out, 1) step(1) poke(c.io.in, true) expect(c.io.out, 1) step(1) poke(c.io.in, false) expect(c.io.out, 1) step(1) poke(c.io.in, true) expect(c.io.out, 1) } val works = iotesters.Driver(() => new BinaryMealy(testParams)) { c => new BinaryMealyTester(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.001] Elaborating design... [info] [0.748] Done elaborating. Total FIRRTL Compile Time: 224.3 ms Total FIRRTL Compile Time: 21.0 ms End of dependency graph Circuit state created [info] [0.001] SEED 1542973527755 test cmd2HelperBinaryMealy Success: 9 tests passed in 13 cycles taking 0.023659 seconds [info] [0.013] RAN 8 CYCLES PASSED SUCCESS!!
”サクッとコードを記載”として載せたが、このコードはここまでのエッセンスや新しい機能が色々詰まったものになっているので、それぞれ個別に見ていこうと思う。
case class BinaryMealyParams
まずはScalaの文法case class
を使ったステートの表現から。さも知ってるでしょ?風に出てきているが、きちんとは把握していないので、特徴について見ておこう。
ちなみにこのcase class
は以前にScalaの勉強をやった際に参考にしていたドワンゴの資料でも紹介されている。
今回参考にさせてもらったのはこちらの記事↓
どうも通常のクラスにプラスアルファで以下のような機能がついてくるとのこと。
- 各種の”便利な”メソッドが定義される
- "コンパニオンオブジェクトが定義される"ので
- 基本コンストラクタ引数全てが
val
で宣言されたフィールドになる
文法的には
case class ClassA()
のように通常のクラス宣言の前にキーワードcase
をつけるだけ。
では、今回のサンプルではどのように使われているのかをサンプルから抜粋して見ていく。
case class BinaryMealyParams
の定義
まずは定義部分をもう一度。
// まずはcase classの定義部分 // Mealy machine has case class BinaryMealyParams( // number of states nStates: Int, // initial state s0: Int, // function describing state transition stateTransition: (Int, Boolean) => Int, // function describing output output: (Int, Boolean) => Int ) { require(nStates >= 0) require(s0 < nStates && s0 >= 0) }
先程も書いたとおり、宣言にcase
をつけることでcase class
にしている。
各引数について確認してみよう。
nStates
/Int
型:ステート数s0
/Int型
:初期ステートstateTransition
/(Int, Boolean) => Int
: ステート遷移を表現した関数output
/(Int, Boolean) => Int
: 次のステートを出力する関数
最初の2つの引数は特に問題ないが、後の2つは若干面食らうかもしれない。この2つはステート遷移時に何をするかと次のステートを決定する処理を実装した関数が渡されてることになる。
それが以下の部分だ。
第3引数:stateTransition
用関数stateTransition
関数が渡せるという機能自体は他の言語でも見かけるものだが、Scalaでは引数の型宣言部分に渡す関数の引数と戻り値を指定して渡すことが出来る。その部分を抜粋したのが以下の関数だ。
// BinaryMealyParamsの第3引数 def stateTransition(state: Int, in: Boolean): Int = { if (in) { 1 } else { 0 } }
処理自体はただ単にin
がtrue
なら1を、そうでなければ0を返すだけのものになっている。
第4引数:output
用関数output
4番目の引数のための関数が、ステートマシンの遷移を制御するための関数になる。
この関数では、先ほど記載したWikipediaのステートマシンの遷移を関数の処理として実現していることがわかると思う。
// BinaryMealyParamsの第4引数 def output(state: Int, in: Boolean): Int = { if (state == 2) { return 0 } if ((state == 1 && !in) || (state == 0 && in)) { return 1 } else { return 0 } }
Chiselを使ったミリ型ステートマシン
ここまでに紹介したcase class
を使うと以下のようなコードでステートマシンを作るジェネレータが作れる!というのがこの章の要旨になる。
そのChiselのジェネレータのコードが以下のようなものだ。
class BinaryMealy(val mp: BinaryMealyParams) extends Module { val io = IO(new Bundle { val in = Input(Bool()) val out = Output(UInt()) }) val state = RegInit(UInt(), mp.s0.U) // output zero if no states io.out := 0.U for (i <- 0 until mp.nStates) { when (state === i.U) { when (io.in) { state := mp.stateTransition(i, true).U io.out := mp.output(i, true).U }.otherwise { state := mp.stateTransition(i, false).U io.out := mp.output(i, false).U } } } }
ここでのポイントは上記のジェネレータのコードには先のWikipediaのリンク先のステートマシンのステートや遷移に関する情報が一切入っていないことだ。
このモジュールでは以下のことのみを行い、実際のステートの表現を先のcase class
に切り出すことで、ステートマシンを一般化している。
- ステート管理用のレジスタの作成と初期化
for
文を使った各ステート時の入力に対してのアクションの呼び出し
一般化しているため、この仕様に則って先のcase class
のインスタンス時の引数を変更することで、1bit型のミリ型ステートマシンであればどのようなものであっても、このジェネレータ一つで生成することが可能になる。
case class
だとパターンマッチも出来るっぽいので、もっと柔軟な処理が出来そうな気がしなくもないので、この辺はおいおい試してみることにしよう。
ということでひとまずModule3.1についてはこれでおしまい。