前回の続きで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
/otherwise
がWhenContext
クラスのメソッドとして実装されており、戻り値として自身のインスタンスを返却することで、メソッドの呼び出しを繋げているためです(実装が見たい方は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のswitch
はchisel3.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で回答がついていました。
この動作を確認しておきましょう。例題は先程の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のswitch
はVerilog 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]
マルチプレクサ系のライブラリは他にもありますので興味があればソースコードを覗いてみてください
ということでChiselの制御構文について、でした。
ここまでの話を抑えると、Chiselで回路の実装は可能になるくらいの状態なのですが、正直これだけだとあまり旨味が無いですね(´・ω・`)
ということで、次はChiselのaggregate型の一つであるVec
についてを紹介していく予定です。
第6回に続く。