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

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

Chiselの文法 - 入門編 〜その5:Chiselの制御構文〜

スポンサーリンク

前回の続きでChiselの文法入門編その5
5回目はChiselの制御構文についてです。

Chisel入門編〜その5:Chiselの制御構文〜

今回はChiselの制御構文についてです。Chiselとして追加されている構文は以下の3つになります。

  • when/elsewhen/otherwise
  • switch/is
  • Mux/MuxCase : これは厳密にはChiselの文法では無く、ライブラリとして提供されるオブジェクトになります これはChiselの構文では無く各種論理回路の基本的なブロックを提供するutilの中のマルチプレクサの機能を提供するオブジェクトになります(2019/06/02コメントにて指摘を頂いたので修正しました)

もうこれだけでもあんまり紹介する必要も無い気がしますが、解説していきたいと思います。

when/elsewhen/otherwise

これはVerilog HDLではそれぞれif/else if/elseに相当するChiselの構文です
使い方はVerilog HDLのそれぞれの要素を置き換えるだけでほぼOKです。

when (<条件式→Bool()を返却する式(第4回の比較式とか)>) {
  // 処理1:whenが成立した場合の処理
} .elsewhen (<条件式>) {
  // 処理2:whenが成立せず、elsewhenの条件が成立した場合の処理
} .otherwise {
  // 処理3:when/elsewhenが成立しなかった場合の処理
}

例えば以下のVerilog HDLのコードを例として、Chiselのコードに書き換えてみます。

wire [1:0] a;
reg [1:0] b;

always @* begin
    if (a == 2'b00) begin
        b = 2'b01;
    end
    else if (a == 2'b01) begin
        b = 2'b10;
    else begin
        b = 2'b11;
    end
end

Chiselで書き直すと以下のようになります。

class WhenStatement extends Module {
  val io IO(new Bundle {
    val a = Input(UInt(2.W))
  })

  val a = io.a
  val b = WIre(UInt(2.W))

  /**
    * ほぼVerilog HDLと同じ感じで書けます。
    * elsewhen/otherwiseの前につく
    * "."は必須なので注意してください
    */
  when (a === "b00".U) {
    b := "b01".U
  } .elsewhen (a === "b01".U) {
    b := "b10".U
  } .otherwise {
    b := "b11".U
  }

  printf("b = %b", b)
}

コメントに入れた通りelsewhen/otherwiseの前の"."は必須なのでご注意ください。
これはelsewhen/otherwiseWhenContextクラスのメソッドとして実装されており、戻り値として自身のインスタンスを返却することで、メソッドの呼び出しを繋げているためです(実装が見たい方はWhen.scalaをどうぞ)

  • 実行結果
[info] [0.000] Elaborating design...
[info] [0.046] Done elaborating.
Total FIRRTL Compile Time: 3.6 ms
Total FIRRTL Compile Time: 2.8 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1559396618768
[info] [0.001]   POKE io_a <- 0
[info] [0.001] STEP 0 -> 1
b = 2'b1
[info] [0.002]   POKE io_a <- 1
[info] [0.002] STEP 1 -> 2
b = 2'b10
[info] [0.002]   POKE io_a <- 2
[info] [0.002] STEP 2 -> 3
b = 2'b11

switch/is

これはVerilog HDLで言うcase/endcaseに相当する構文です。
以下のような形で使用します。
なおChiselのswitchchisel3.util以下に存在しているため、使用時にはutil以下のimportが必要です。
必要なのはutil.switch/util.is/util.SwitchContextの3つなので、以下のコードのようにutil一式をまとめてimportしたほうが良いと思います。
#筆者はimport chisel3.util.{switch, is}とだけimportした際に謎のエラー(SwitchContextがimportされていないのが原因でした)に悩まされました。

// これ大事。Chisel書くときはchisel3._とchisel3.util._は
// まるっとimportで良いと思う。
import chisel3.util._

switch (<Chiselのハードウェア要素>) {
  is (<定数値1>) {
    // 定数値1の時の処理
  }
  is (Iterable[<定数値>]) {
    // Iterable[<定数値>]の時の処理
  }
  ...
  is (<定数値M>, <定数値N>) {
    // 定数値M/Nの時の処理
  }
}

ご覧頂いたとおりで、何となくVerilog HDLのcaseに近いことがわかるかと思います。
ただ上記の2番目/3番目の例に記載したようにChiselのisは複数の値をまとめて扱うことが可能です。
因みにVerilog HDLで言うdefault項は文法としては存在しませんが、記述することは可能です(後述します)。

以下のVerilog HDLのステートマシン記述を例に見てみます。

localparam  s_IDLE = 2'b00;
localparam  s_PREPARE = 2'b01;
localparam  s_DO_SOMTHING = 2'b11;

reg [1:0] r_state;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        r_state <= s_IDLE;
    end
    else begin
        case (r_state)
            s_IDLE : begin
                if (run) begin
                    r_state <= s_PREPARE;
                end
            end
            s_PREPARE :     r_state <= s_DO_SOMTHING;
            s_DO_SOMTHING : r_state <= s_IDLE;
            default :       r_state <= s_IDLE;
        endcase
    end
end

上記をChiselで書き直すと以下のようになります。

class SwitchStatement extends Module {
  val io = IO(new Bundle {
    val run = Input(Bool())
  })

  // これはEnum使ったほうが良いけど
  // ここではとりあえずこの定義で。
  val sIdle = "b00".U(2.W) // 2.Wを宣言しないと初期化時にRegInitのビット数が1bitになる
  val sPrepare = "b01".U(2.W)
  val sDoSomething = "b10".U(2.W)

  val state = RegInit(sIdle)

  switch (state) {
    is (sIdle) {
      when (io.run) {
        state := sPrepare
      }
    }
    is (sPrepare) {
      state := sDoSomething
    }
    is (sDoSomething) {
      state := sIdle
    }      
  }
  printf("state = 2'b%b\n", state)
}
  • 実行結果
End of dependency graph
Circuit state created
[info] [0.000] SEED 1559399129944
[info] [0.000] STEP 0 -> 2
state = 2'b0
state = 2'b0
[info] [0.001]   POKE io_run <- 1 // io.runにtrueを設定
[info] [0.001] STEP 2 -> 3
state = 2'b1 // ステートが遷移(sIdle → sPrepare)
[info] [0.002]   POKE io_run <- 0
[info] [0.002] STEP 3 -> 6
state = 2'b10
state = 2'b0 // io.runがfalseなのでsIdleで待機
state = 2'b0

switchのデフォルト項

前述したとおりChiselの文法的にはVerilog HDLのdefaultに相当する文法は存在しませんが、以下の方法でdefault項の動作を記述することが可能です。

<Chiselのハードウェア要素> := <デフォルト項の値>
switch (<Chiselのハードウェア要素>) {
  is (<定数値1>) {
    // 定数値1の時の処理
  }
}

Chiselでは”同じ<ハードウェア要素>へのアサインは最後に評価されたものが有効になる”という規則があります。
このため上記のように記載しておくと、まず最初にデフォルト項相当の設定が行われた後にswitchの評価が行われることになりswitchの中のisにマッチしなかった場合には最初に設定した値が有効になるため、結果としてデフォルト項が実現可能です。
これは以下のgithubのissueで回答がついていました。

github.com

この動作を確認しておきましょう。例題は先程のVerilog HDLのコードと同様にして、ステートマシン内部のアサイン処理を定義したステート以外の値(=="b11".U)にしてみます。
あとついでに各ステートの宣言をEnumを使って書きなおしてますので、その宣言について触れておきます。

  • Enumの宣言 以下の様になります。各ステートの値は左から連番で0, 1, 2, ... Nとなります。
    Enumを使って宣言したステートはビット幅が<ステート数: Int>から計算される値になるので、そのままRegInit等の初期値に使ってもビット数が足りなくなるといったことは起きません。
val <ステート0> :: <ステート1> :: ... :: <ステートN> :: Nil = Enum(<ステート数: Int>)
  • デフォルト項の挙動を確認するコード
class SwitchStatement extends Module {
  val io = IO(new Bundle {
    val run = Input(Bool())
  })

  // Enum使って書き直し
  val sIdle :: sPrepare :: sDoSomething :: Nil = Enum(3)

  val state = RegInit(sIdle)

  state := sIdle // デフォルト項
  switch (state) {
    is (sIdle) {
      when (io.run) {
        state := sPrepare
      }
    }
    is (sPrepare) {
      state := sDoSomething
    }
    is (sDoSomething) {
      state := "b11".U // 存在しないステートを設定
    }      
  }
  printf("state = 2'b%b\n", state)
}
  • 実行結果
Circuit state created
[info] [0.000] SEED 1559400184667
[info] [0.000] STEP 0 -> 2
state = 2'b1
state = 2'b10 // sDoSomethingステートに遷移
[info] [0.001]   POKE io_run <- 1
[info] [0.001] STEP 2 -> 3
state = 2'b11 // 存在しないステートに遷移
[info] [0.002]   POKE io_run <- 0
[info] [0.002] STEP 3 -> 6
state = 2'b1  // デフォルト項が有効になりsPrepareに遷移
state = 2'b10 // 以降はずっとsPrepare→sDoSomething→存在しないステートを繰り返す
state = 2'b11

ChiselのswitchVerilog HDLのcaseに相当するものですが、エラボレートを行いRTLに変換した際には以下の様にif文を使った処理に変換されます。

  always @(posedge clock) begin
    if (reset) begin
      state <= 2'h0;
    end else begin
      if (_T_8) begin
        if (io_run) begin
          state <= 2'h1;
        end
      end else begin
        if (_T_9) begin
          state <= 2'h2;
        end else begin
          if (_T_10) begin
            state <= 2'h0;
          end
        end
      end
    end
    `ifndef SYNTHESIS
    `ifdef PRINTF_COND
      if (`PRINTF_COND) begin
    `endif
        if (_T_13) begin
          $fwrite(32'h80000002,"state = 2'b%b\n",state); // @[cmd72.sc 27:9:@28.6]
        end
    `ifdef PRINTF_COND
      end
    `endif
    `endif // SYNTHESIS
  end

switchを使ってWireの信号に値を設定する場合

以下のChiselのコードはswitchで参照しているstateが3bitで、それに対してのステートがs0~s7なのでswitch内の記述で全てのケースがカバーされています。 そのためエラボレートが通りそうな気がしますが、このコードはエラーになります。

class SwitchStatement extends Module {
  val io = IO(new Bundle {
    val state = Input(UInt(3.W))
  })

  val s0 :: s1 :: s2 :: s3 :: s4 :: s5 :: s6 :: s7 :: Nil = Enum(8)

  val state = io.state
  val wren = Wire(Bool())
  val wrdata = Wire(UInt(8.W))

  switch (state) {
    is (s0) {
      wren := false.B
      wrdata := 0.U
    }
    // is (<定数値M>, <定数値N>) - これに相当する記述
    is (s1, s2, s3) {
      wren := true.B
      wrdata := 1.U
    }
    // is (Iterable[<定数値>]) - これに相当する記述
    is (Seq(s4, s5, s6, s7)) {
      wren := true.B
      wrdata := 2.U
    }      
  }
  printf("state = 2'b%b\n", state)
  printf("(wren, wrdata) = (1'b%b, 3'h%x)\n", wren, wrdata)
}
  • エラボレート結果 スタックトレースが長いので関連部分のみを切りだして掲載したのが以下のエラーログです。
firrtl.passes.PassExceptions:
firrtl.passes.CheckInitialization$RefNotInitializedException:  @[cmd10.sc 9:18:@8.4] : [module cmd10HelperSwitchStatement]  Reference wren is not fully initialized.
   @[Conditional.scala 39:67:@33.8] : node _GEN_0 = mux(_T_25, UInt<1>("h1"), VOID) @[Conditional.scala 39:67:@33.8] // muxの第2引数が"VOID"になっている
   @[Conditional.scala 39:67:@21.6] : node _GEN_2 = mux(_T_16, UInt<1>("h1"), _GEN_0) @[Conditional.scala 39:67:@21.6]
   @[Conditional.scala 40:58:@11.4] : node _GEN_4 = mux(_T_9, UInt<1>("h0"), _GEN_2) @[Conditional.scala 40:58:@11.4]
   : wren <= _GEN_4
firrtl.passes.CheckInitialization$RefNotInitializedException:  @[cmd10.sc 10:20:@9.4] : [module cmd10HelperSwitchStatement]  Reference wrdata is not fully initialized.
   @[Conditional.scala 39:67:@33.8] : node _GEN_1 = mux(_T_25, UInt<2>("h2"), VOID) @[Conditional.scala 39:67:@33.8] // muxの第2引数が"VOID"になっている
   @[Conditional.scala 39:67:@21.6] : node _GEN_3 = mux(_T_16, UInt<1>("h1"), _GEN_1) @[Conditional.scala 39:67:@21.6]
   @[Conditional.scala 40:58:@11.4] : node _GEN_5 = mux(_T_9, UInt<1>("h0"), _GEN_3) @[Conditional.scala 40:58:@11.4]
   : wrdata <= _GEN_5
firrtl.passes.PassException: 2 errors detected!

コメントを入れた通り、wren/wrdataのMUXの記述に"VOID"が入っており、これによって全てのケースで値が設定されていないというエラーが発生しています。
どうもswitchを変換する際には特にビット幅に対してのチェック等は行われていないようで、switchの中でWireに値を設定する場合には機械的switchブロックの外で設定された値が上記のVOID部分に設定されるようです。
そのためWireを使ったswitch節を作成する場合には、switch節の外でデフォルト項の設定を行っておく必要があります。
上記の例ではswitchの外側でwren/wrdataへのアサインを追加するか、wren/wrdataの宣言時にWireInitを使って初期化しておくとエラーはなくなります。
これはwhenの場合でもotherwise項が存在しないWireで、初期値の設定がない場合には同じようにエラーが発生します。
なおRegの場合は”直前の値を保持”という動作になる関係でエラーは起きません。

class SwitchStatement extends Module {
  val io = IO(new Bundle {
    val state = Input(UInt(2.W))
  })

  val s0 :: s1 :: s2 :: s3 :: Nil = Enum(4)

  val state = io.state
  val wren = Wire(Bool())
  val wrdata = WireInit(0.U(8.W))

  wren := false.B
  switch (state) {
    is (s0) {
      wren := false.B
      wrdata := 0.U
    }
    is (s1, s2, s3) {
      wren := true.B
      wrdata := 1.U
    }
  }
  printf("state = 2'b%b\n", state)
  printf("(wren, wrdata) = (1'b%b, 3'h%x)\n", wren, wrdata)
}

Mux/MuxCase

冒頭に書いたとおり厳密にはChiselの文法ではありませんが、Verilog HDLの3項演算子に相当するものなのでここで合わせて紹介します。

使い方は以下のようになります。

val a = Mux(<条件式>, <条件式が成立した場合の値>, <条件式が不成立の場合の値>)
val b = MuxCase(<第2項のIterableの条件が不成立だった場合の値>, Seq[(条件式, 設定値)])

例としては以下のような形になります。

class MuxObj extends Module {
  val io = IO(new Bundle {
    val cond = Input(UInt(2.W))
  })

  val sIdle :: sPrepare :: sDoSomething :: Nil = Enum(3)

  val muxVal = Mux(io.cond === sIdle, 0.U, 1.U)
  val muxCaseVal = MuxCase(0.U, Seq(
      // scalaは -> でTupleが作れる
      (io.cond === sIdle)        -> 2.U,
      // 因みに以下のように->の左辺は単一の変数以外の場合は()つけないとエラー
      // io.cond === sPrepare :: Error
      (io.cond === sPrepare)     -> 3.U,
      (io.cond === sDoSomething) -> 4.U
      ))


  printf("muxVal     = 2'b%b\n", muxVal)
  printf("muxCaseVal = 2'b%b\n", muxCaseVal)
}
  • 実行結果
[info] [0.000] SEED 1559401651129
[info] [0.000]   POKE io_cond <- 0
[info] [0.001] STEP 0 -> 1
muxVal     = 2'b0
muxCaseVal = 2'b10
[info] [0.001]   POKE io_cond <- 1
[info] [0.002] STEP 1 -> 2
muxVal     = 2'b1
muxCaseVal = 2'b11
[info] [0.002]   POKE io_cond <- 2
[info] [0.002] STEP 2 -> 3
muxVal     = 2'b1
muxCaseVal = 2'b100
[info] [0.003]   POKE io_cond <- 3
[info] [0.003] STEP 3 -> 4
muxVal     = 2'b1
muxCaseVal = 2'b0

なおMux/MuxCaseはRTLに変換するとVerilog HDLの3項演算子になります。

  wire  _T_7; // @[cmd81.sc 9:28:@8.4]
  wire  muxVal; // @[cmd81.sc 9:19:@9.4]
  wire  _T_13; // @[cmd81.sc 12:16:@11.4]
  wire  _T_15; // @[cmd81.sc 13:16:@12.4]
  wire [2:0] _T_17; // @[Mux.scala 61:16:@13.4]
  wire [2:0] _T_18; // @[Mux.scala 61:16:@14.4]
  wire [2:0] muxCaseVal; // @[Mux.scala 61:16:@15.4]
  wire  _T_21; // @[cmd81.sc 17:9:@17.4]
  // muxVal
  assign _T_7 = io_cond == 2'h0; // @[cmd81.sc 9:28:@8.4]
  assign muxVal = _T_7 ? 1'h0 : 1'h1; // @[cmd81.sc 9:19:@9.4]
  // muxCaseVal
  assign _T_13 = io_cond == 2'h1; // @[cmd81.sc 12:16:@11.4]
  assign _T_15 = io_cond == 2'h2; // @[cmd81.sc 13:16:@12.4]
  assign _T_17 = _T_15 ? 3'h4 : 3'h0; // @[Mux.scala 61:16:@13.4]
  assign _T_18 = _T_13 ? 3'h3 : _T_17; // @[Mux.scala 61:16:@14.4]
  assign muxCaseVal = _T_7 ? 3'h2 : _T_18; // @[Mux.scala 61:16:@15.4]
  assign _T_21 = reset == 1'h0; // @[cmd81.sc 17:9:@17.4]

マルチプレクサ系のライブラリは他にもありますので興味があればソースコードを覗いてみてください

github.com

ということでChiselの制御構文について、でした。 ここまでの話を抑えると、Chiselで回路の実装は可能になるくらいの状態なのですが、正直これだけだとあまり旨味が無いですね(´・ω・`)
ということで、次はChiselのaggregate型の一つであるVecについてを紹介していく予定です。
第6回に続く。