前回のChiselの記事ではBundle
の基本的な使い方についてをまとめたみた。
今回は前回まとめたBundle
を使ってモジュールのIOポートをオプション化する方法についてをまとめておく。
ChiselでIOポートのオプション化するには?
まずは基本的な話から再度確認しておく。
この話は実は今進めているChisel-Bootcampの中でも簡単な例が取り扱われている。
その時に”でもこれじゃ野暮ったいから、もっと楽に書けないかしら・・・”と思ったがその時は、自分のレベルが低すぎて答えが見つからなかった。
それから少し経って”あ、これでイケるんじゃん!”って気づいたので改めて、基本的なことからまとめてみようと思う。
因みに過去の記事は以下↓。
モジュールのIOポートのオプション化ーその1
以下のコードは上に貼った過去記事のもの。
クラスパラメータとScalaにあるOption
型の一つであるSome
を使うことでポートをオプション化することが出来る。
また以下の例ではcarryIn
がある場合ない場合の計算に対応するためにOption
型のメソッドgetOrElse
を使い、carryIn
が存在しない場合には0.U
が加算されるようにしている。
class HalfFullAdder(val hasCarry: Boolean) extends Module { val io = IO(new Bundle { val a = Input(UInt(1.W)) val b = Input(UInt(1.W)) val carryIn = if (hasCarry) Some(Input(UInt(1.W))) else None val s = Output(UInt(1.W)) val carryOut = Output(UInt(1.W)) }) val sum = io.a +& io.b +& io.carryIn.getOrElse(0.U) io.s := sum(0) io.carryOut := sum(1) }
上記のコードをクラスパラメータ"hasCarry"を変更してインスタンスして生成したRTLを見ると以下のようにcarryIn
ポートの有無や、それに応じたsum
の計算が変更されているのがわかると思う。
なおこれはgetOrElse
を使って計算式を一つにまとめたからでRTL的に0.U
の加算が気に食わなければ、普通にif
文で切り替えてももちろんOK。
hasCarry == true
の場合
[info] [0.001] Elaborating design... [info] [0.682] Done elaborating. Total FIRRTL Compile Time: 249.8 ms module cmd3HelperHalfFullAdder( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input io_a, // @[:@6.4] input io_b, // @[:@6.4] input io_carryIn, // @[:@6.4] / carryInが存在している output io_s, // @[:@6.4] output io_carryOut // @[:@6.4] ); wire [1:0] _T_15; // @[cmd3.sc 9:18:@8.4] wire [1:0] _GEN_0; // @[cmd3.sc 9:26:@9.4] wire [2:0] sum; // @[cmd3.sc 9:26:@9.4] wire _T_16; // @[cmd3.sc 10:14:@10.4] wire _T_17; // @[cmd3.sc 11:21:@12.4] assign _T_15 = io_a + io_b; // @[cmd3.sc 9:18:@8.4] // 以下の2行がcarryInの計算部分 assign _GEN_0 = {{1'd0}, io_carryIn}; // @[cmd3.sc 9:26:@9.4] assign sum = _T_15 + _GEN_0; // @[cmd3.sc 9:26:@9.4] assign _T_16 = sum[0]; // @[cmd3.sc 10:14:@10.4] assign _T_17 = sum[1]; // @[cmd3.sc 11:21:@12.4] assign io_s = _T_16; assign io_carryOut = _T_17; endmodule
hasCarry == false
の場合
[info] [0.000] Elaborating design... [info] [0.008] Done elaborating. Total FIRRTL Compile Time: 21.5 ms module cmd3HelperHalfFullAdder( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input io_a, // @[:@6.4] input io_b, // @[:@6.4] // carryInが存在しない output io_s, // @[:@6.4] output io_carryOut // @[:@6.4] ); wire [1:0] _T_13; // @[cmd3.sc 9:18:@8.4] wire [2:0] sum; // @[cmd3.sc 9:26:@9.4] wire _T_15; // @[cmd3.sc 10:14:@10.4] wire _T_16; // @[cmd3.sc 11:21:@12.4] assign _T_13 = io_a + io_b; // @[cmd3.sc 9:18:@8.4] // carryInが存在しないため2'h0が加算される。 assign sum = _T_13 + 2'h0; // @[cmd3.sc 9:26:@9.4] assign _T_15 = sum[0]; // @[cmd3.sc 10:14:@10.4] assign _T_16 = sum[1]; // @[cmd3.sc 11:21:@12.4] assign io_s = _T_15; assign io_carryOut = _T_16; endmodule
モジュールのIOポートのオプション化ーその2
こちらも以前書いた記事の付け足し部分で見た方法
class HalfFullAdder(val hasCarry: Boolean) extends Module { val io = IO(new Bundle { val a = Input(UInt(1.W)) val b = Input(UInt(1.W)) val carryIn = if (hasCarry) Input(UInt(1.W)) else Input(UInt(0.W)) val s = Output(UInt(1.W)) val carryOut = Output(UInt(1.W)) }) val sum = io.a +& io.b +& io.carryIn io.s := sum(0) io.carryOut := sum(1) }
オプション化対象のポートのビット幅が0
になるようにすると、そのポートはRTLの生成時に削除される。
以下が生成されたRTL。見てわかる通りSome
を使った場合と同様のRTLが生成されている。
hasCarry == true
の場合
module cmd17HelperHalfFullAdder( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input io_a, // @[:@6.4] input io_b, // @[:@6.4] input io_carryIn, // @[:@6.4] output io_s, // @[:@6.4] output io_carryOut // @[:@6.4] ); wire [1:0] _T_15; // @[cmd17.sc 9:18:@8.4] wire [1:0] _GEN_0; // @[cmd17.sc 9:26:@9.4] wire [2:0] sum; // @[cmd17.sc 9:26:@9.4] wire _T_16; // @[cmd17.sc 10:14:@10.4] wire _T_17; // @[cmd17.sc 11:21:@12.4] assign _T_15 = io_a + io_b; // @[cmd17.sc 9:18:@8.4] assign _GEN_0 = {{1'd0}, io_carryIn}; // @[cmd17.sc 9:26:@9.4] assign sum = _T_15 + _GEN_0; // @[cmd17.sc 9:26:@9.4] assign _T_16 = sum[0]; // @[cmd17.sc 10:14:@10.4] assign _T_17 = sum[1]; // @[cmd17.sc 11:21:@12.4] assign io_s = _T_16; assign io_carryOut = _T_17; endmodule
hasCarry == false
の場合
module cmd17HelperHalfFullAdder( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input io_a, // @[:@6.4] input io_b, // @[:@6.4] output io_s, // @[:@6.4] output io_carryOut // @[:@6.4] ); wire [1:0] _T_15; // @[cmd17.sc 9:18:@8.4] wire [2:0] sum; // @[cmd17.sc 9:26:@9.4] wire _T_16; // @[cmd17.sc 10:14:@10.4] wire _T_17; // @[cmd17.sc 11:21:@12.4] assign _T_15 = io_a + io_b; // @[cmd17.sc 9:18:@8.4] assign sum = _T_15 + 2'h0; // @[cmd17.sc 9:26:@9.4] assign _T_16 = sum[0]; // @[cmd17.sc 10:14:@10.4] assign _T_17 = sum[1]; // @[cmd17.sc 11:21:@12.4] assign io_s = _T_16; assign io_carryOut = _T_17; endmodule
FIRRTLレベルでの比較
既に見てきたとおりSome
を使ってもビット幅が0になるようにしても上記で生成されたRTLは一緒になる。
Chisel3ではVerilogのRTLを生成する際にはFIRRTLという中間表現を生成した後に、それをVerilogに変換していいる。ということでFIRRTLレベルではどうだろう??という疑問が湧いたので一応確認してみる。
Some
を使った場合
hasCarry == true
の場合
;buildInfoPackage: chisel3, version: 3.1.0, scalaVersion: 2.11.12, sbtVersion: 1.1.1, builtAtString: 2018-04-17 19:22:56.455, builtAtMillis: 1523992976455 circuit cmd15HelperHalfFullAdder : module cmd15HelperHalfFullAdder : input clock : Clock input reset : UInt<1> @ carryInが存在している output io : {flip a : UInt<1>, flip b : UInt<1>, flip carryIn : UInt<1>, s : UInt<1>, carryOut : UInt<1>} node _T_15 = add(io.a, io.b) @[cmd15.sc 9:18] node sum = add(_T_15, io.carryIn) @[cmd15.sc 9:26] node _T_16 = bits(sum, 0, 0) @[cmd15.sc 10:14] io.s <= _T_16 @[cmd15.sc 10:8] node _T_17 = bits(sum, 1, 1) @[cmd15.sc 11:21] io.carryOut <= _T_17 @[cmd15.sc 11:15]
hasCarry == false
の場合
;buildInfoPackage: chisel3, version: 3.1.0, scalaVersion: 2.11.12, sbtVersion: 1.1.1, builtAtString: 2018-04-17 19:22:56.455, builtAtMillis: 1523992976455 circuit cmd15HelperHalfFullAdder : module cmd15HelperHalfFullAdder : input clock : Clock input reset : UInt<1> @ carryInが存在しない output io : {flip a : UInt<1>, flip b : UInt<1>, s : UInt<1>, carryOut : UInt<1>} node _T_13 = add(io.a, io.b) @[cmd15.sc 9:18] node sum = add(_T_13, UInt<1>("h00")) @[cmd15.sc 9:26] node _T_15 = bits(sum, 0, 0) @[cmd15.sc 10:14] io.s <= _T_15 @[cmd15.sc 10:8] node _T_16 = bits(sum, 1, 1) @[cmd15.sc 11:21] io.carryOut <= _T_16 @[cmd15.sc 11:15]
ビット幅を0にした場合
hasCarry == true
の場合
;buildInfoPackage: chisel3, version: 3.1.0, scalaVersion: 2.11.12, sbtVersion: 1.1.1, builtAtString: 2018-04-17 19:22:56.455, builtAtMillis: 1523992976455 circuit cmd17HelperHalfFullAdder : module cmd17HelperHalfFullAdder : input clock : Clock input reset : UInt<1> @ carryInはUIntの1bitの信号 output io : {flip a : UInt<1>, flip b : UInt<1>, flip carryIn : UInt<1>, s : UInt<1>, carryOut : UInt<1>} node _T_15 = add(io.a, io.b) @[cmd17.sc 9:18] node sum = add(_T_15, io.carryIn) @[cmd17.sc 9:26] node _T_16 = bits(sum, 0, 0) @[cmd17.sc 10:14] io.s <= _T_16 @[cmd17.sc 10:8] node _T_17 = bits(sum, 1, 1) @[cmd17.sc 11:21] io.carryOut <= _T_17 @[cmd17.sc 11:15]
hasCarry == false
の場合
;buildInfoPackage: chisel3, version: 3.1.0, scalaVersion: 2.11.12, sbtVersion: 1.1.1, builtAtString: 2018-04-17 19:22:56.455, builtAtMillis: 1523992976455 circuit cmd17HelperHalfFullAdder : module cmd17HelperHalfFullAdder : input clock : Clock input reset : UInt<1> @ carryInはUIntの0bitの信号 output io : {flip a : UInt<1>, flip b : UInt<1>, flip carryIn : UInt<0>, s : UInt<1>, carryOut : UInt<1>} node _T_15 = add(io.a, io.b) @[cmd17.sc 9:18] node sum = add(_T_15, io.carryIn) @[cmd17.sc 9:26] node _T_16 = bits(sum, 0, 0) @[cmd17.sc 10:14] io.s <= _T_16 @[cmd17.sc 10:8] node _T_17 = bits(sum, 1, 1) @[cmd17.sc 11:21] io.carryOut <= _T_17 @[cmd17.sc 11:15]
という結果になった。 なので、
Some
を使うと、FIRRTL生成時に不要な端子は削除される- ビット幅を0にする手法を使うと、Verilogの生成時に端子が削除される
という挙動になっているようだ。
出力ポートのオプション化
ここまでの例は入力ポートのオプション化に関する例しかなかったので、出力ポートのオプション化についても見ておく。
Some
を使っ出力ポートのオプション化
Some
を使う場合は以下のようにすればいい。
class OptionOutput(option: Boolean) extends Module { val io = IO(new Bundle { val in = Input(Bool()) val out = Output(Bool()) val optOut = if (option) Some(Output(Bool())) else None // ポート宣言は一緒 }) io.out := io.in // option == trueの時にのみ実体化するので、if文でくくる if (option) { io.optOut.get := io.in } }
実はこの方法に気づけなくて野暮ったい書き方になるなぁ、、、、と悩んでいました。。
ビット幅を0にする場合
これは以前の記事でも紹介済み&入力の場合と扱いは一緒でOK
class OptionOutput(option: Boolean) extends Module { val io = IO(new Bundle { val in = Input(Bool()) val out = Output(Bool()) // ポート宣言は一緒 val optOut = if (option) Output(Bool()) else Output(UInt(0.W)) }) io.out := io.in // bit幅0を使う場合はif文は不要 io.optOut := io.in }
Some
とビット幅0のif
文のあるなしの違いを表しているのが、先にみたFIRRTL上の表現になる。
Some
の場合はFIRRTL生成時には既にoptOut
ポートが存在しないためif
文でくくってインスタンス時に切り替えないとエラーが発生するが、ビット幅0の場合はFIRRTL上はポートが存在するためエラーにはならない。
複数のポートをまとめてオプション化する方法
長々書いてきましたが、これが今日の本題&やっと気づけたやり方。と言っても既に本記事で書いた内容でもあるので簡単に。
前に紹介したビット幅0にする方法で行う複数ポートのオプション化
例えばChiselのテスターを使って内部の信号を比較するようなケースやデバッグのためにトレースしたい、、といったことを考えた際に、それらのポートは対象モジュールのIOに引き出してやる必要がある。
これは実際の回路としては不要になるため、オプション化して最終のRTLからは削除するのが普通だと思う。
これを以前に紹介したビット幅を0にする手法でやると、以下のように野暮ったい感じに・・・。
class Top(debug: Boolean) extends Module { val io = IO(new Bundle { val in = Input(Bool()) val out = Output(Bool()) val dbgOut1 = if (debug) Output(Bool()) else Output(UInt(0.W)) val dbgOut2 = if (debug) Output(UInt(1.W)) else Output(UInt(0.W)) val dbgOut3 = if (debug) Output(UInt(4.W)) else Output(UInt(0.W)) val dbgOut4 = if (debug) Output(UInt(6.W)) else Output(UInt(0.W)) val dbgOut5 = if (debug) Output(Bool()) else Output(UInt(0.W)) }) io.out := io.in io.dbgOut1 := false.B io.dbgOut2 := 0x1.U io.dbgOut3 := 0x10.U io.dbgOut4 := 0x10.U io.dbgOut5 := true.B }
以下が上記のChiselコードから生成されるRTL
// debug == true module cmd24HelperTop( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input io_in, // @[:@6.4] output io_out, // @[:@6.4] output io_dbgOut1, // @[:@6.4] output io_dbgOut2, // @[:@6.4] output [3:0] io_dbgOut3, // @[:@6.4] output [5:0] io_dbgOut4, // @[:@6.4] output io_dbgOut5 // @[:@6.4] ); assign io_out = io_in; assign io_dbgOut1 = 1'h0; assign io_dbgOut2 = 1'h1; assign io_dbgOut3 = 4'h0; assign io_dbgOut4 = 6'h10; assign io_dbgOut5 = 1'h1; endmodule // debgu == false module cmd25HelperTop( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input io_in, // @[:@6.4] output io_out // @[:@6.4] ); assign io_out = io_in; endmodule
先に書いたように、所望の結果にはなる。 なるんだけどIOの宣言が滅茶めんどくさい。。
Some
とBundle
ですっきり!!
これを書くために前回Bundle
についてをまとめたと言っても過言ではない!
ということでSome
とBundle
使うとすっきり書けます。
ただそれだけ(笑)
上の例だと以下のようになります。
// デバッグ用ポートのためのBundle class DbgIO extends Bundle { val out1 = Output(Bool()) val out2 = Output(UInt(1.W)) val out3 = Output(UInt(4.W)) val out4 = Output(UInt(6.W)) val out5 = Output(Bool()) } class Top(debug: Boolean) extends Module { val io = IO(new Bundle { val in = Input(Bool()) val out = Output(Bool()) val dbgPort = if (debug) Some(new DbgIO) else None }) io.out := io.in // デバッグ有効時のポート接続 if (debug) { io.dbgPort.get.out1 := false.B io.dbgPort.get.out2 := 0x1.U io.dbgPort.get.out3 := 0x10.U io.dbgPort.get.out4 := 0x10.U io.dbgPort.get.out5 := true.B } }
これだとif
文でくくったこともあり、ブロックで構造化されていい感じ。
デバッグに使いたい信号との接続は書く必要はあるが、Bundle
を使っているため上層に引き渡す際には<>
を使って接続が可能になる。
例えばこんな感じ↓。
class DbgIO extends Bundle { val out1 = Output(Bool()) val out2 = Output(UInt(1.W)) val out3 = Output(UInt(4.W)) val out4 = Output(UInt(6.W)) val out5 = Output(Bool()) } class Top(debug: Boolean) extends Module { val io = IO(new Bundle { val in = Input(Bool()) val out = Output(Bool()) // debug == trueの時のみDbgIOをインスタンスしてSomeに入れる val dbgPort = if (debug) Some(new DbgIO) else None }) io.out := io.in // デバッグ有効時のポート接続 if (debug) { io.dbgPort.get.out1 := false.B io.dbgPort.get.out2 := 0x1.U io.dbgPort.get.out3 := 0x10.U io.dbgPort.get.out4 := 0x10.U io.dbgPort.get.out5 := true.B } } // TopTop階層でTopをインスタンスして接続する class TopTop(debug: Boolean) extends Module { val io = IO(new Bundle { val in = Input(Bool()) val out = Output(Bool()) val in2 = Input(Bool()) val dbgPort = if (debug) Some(new DbgIO) else None }) // Chiselで書いたTopモジュールのインスタンス化部分 // 大事な文法なのにやり方に触れた覚えがないな、これ val top = Module(new Top(debug)) // TopTopとTopでIOが異なるので<>で接続できなの単体で接続 top.io.in := io.in io.out := top.io.out // dbgPortはBundleで構造化されているので<>をつかって接続が可能になる io.dbgPort.get <> top.io.dbgPort.get }
結局長々書いたが要点は実は以下の1点な気がした。
Bundle
で構造化できるから、しといた方がいろいろ楽に書ける!
ということで、Bundle
とSome
を使ったIOのオプション化の方法でした。