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

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

Chisel Bootcamp - Module3.1(7) - case classを使ったステートマシン・ジェネレータ

スポンサーリンク

前回の記事ではChisel Bootcampの引き続きModule3.1の学習を進め、Scalaの機能implicitとそれをChiselに応用した例を学んだ。

www.tech-diningyo.info

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の勉強をやった際に参考にしていたドワンゴの資料でも紹介されている。

今回参考にさせてもらったのはこちらの記事↓

qiita.com

どうも通常のクラスにプラスアルファで以下のような機能がついてくるとのこと。

  • 各種の”便利な”メソッドが定義される
  • "コンパニオンオブジェクトが定義される"ので
    • apply:newなしでインスタンス生成が可能
    • unapply:コンストラクタパターンを使ったパターンマッチが可能
  • 基本コンストラクタ引数全てが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
  }
}

処理自体はただ単にintrueなら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に切り出すことで、ステートマシンを一般化している。

  1. ステート管理用のレジスタの作成と初期化
  2. for文を使った各ステート時の入力に対してのアクションの呼び出し

一般化しているため、この仕様に則って先のcase classインスタンス時の引数を変更することで、1bit型のミリ型ステートマシンであればどのようなものであっても、このジェネレータ一つで生成することが可能になる。

case classだとパターンマッチも出来るっぽいので、もっと柔軟な処理が出来そうな気がしなくもないので、この辺はおいおい試してみることにしよう。

ということでひとまずModule3.1についてはこれでおしまい。