前回の記事ではChisel BootcampのModule3.1でScalaの文法であるMatch
文の復習とそれをChiselに適用するとどうなるかということについてを見ていった。
今日も引き続きModule3.1を見ていく。今日はオプション付きのIO宣言についてだ。
Module 3.1: ジェネレータ:パラメータ
オプション付きのI/Oポート宣言
ここでいうオプションはRTLのinput
/output
をパラメタライズの対象とすることを言っている。例えばこのようなモジュールをVerilogで書くと以下の様にifdef
とdefine
を使うことになる。
module HalfFullAdder( input a, input b, `ifdef HasCarry input carry `endif output s output carryOut ) wire [1:0] w_sum; `ifdef HasCarry assign w_sum = a + b + carry; `else assign w_sum = a + b; `endif assign s = w_sum[0]; assign carryOut = w_sum[1]; endmodule /* HalfFullAdder */
個人的にはあんまりやりたくない書き方の一つ。Verilogのdefine
は順序に気をつけないとめんどくさいし、何よりコードがわかりにくい。
例題:オプションを使ったオプションI/O端子
で、これをChiselで書くのがこの章の内容。
上記のVerilogモジュールと同様のものをChiselで書いたものが以下の例になる。
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) }
ほぼ見たまんまではあるのだが、val carryIn
の実装がif (hasCarry)
となっており、hasCarry
によってSome(Input(UInt(1.W)))
かNone
が返却されることになる。
Bundle
の処理で値がNone
となる場合のために、実際の計算処理時には
val sum = io.a +& io.b +& io.carryIn.getOrElse(0.U)
とあるように、Some.getOrElse
を使って処理することで計算に影響の無い0.U
が加算されることになり、加算処理自体は正しいものとなる。
テスト
上記をテストするコードはこちら。
class HalfAdderTester(c: HalfFullAdder) extends PeekPokeTester(c) { require(!c.hasCarry, "DUT must be half adder") // 0 + 0 = 0 poke(c.io.a, 0) poke(c.io.b, 0) expect(c.io.s, 0) expect(c.io.carryOut, 0) // 0 + 1 = 1 poke(c.io.b, 1) expect(c.io.s, 1) expect(c.io.carryOut, 0) // 1 + 1 = 2 poke(c.io.a, 1) expect(c.io.s, 0) expect(c.io.carryOut, 1) // 1 + 0 = 1 poke(c.io.b, 0) expect(c.io.s, 1) expect(c.io.carryOut, 0) } class FullAdderTester(c: HalfFullAdder) extends PeekPokeTester(c) { require(c.hasCarry, "DUT must be half adder") poke(c.io.carryIn.get, 0) // 0 + 0 + 0 = 0 poke(c.io.a, 0) poke(c.io.b, 0) expect(c.io.s, 0) expect(c.io.carryOut, 0) // 0 + 0 + 1 = 1 poke(c.io.b, 1) expect(c.io.s, 1) expect(c.io.carryOut, 0) // 0 + 1 + 1 = 2 poke(c.io.a, 1) expect(c.io.s, 0) expect(c.io.carryOut, 1) // 0 + 1 + 0 = 1 poke(c.io.b, 0) expect(c.io.s, 1) expect(c.io.carryOut, 0) poke(c.io.carryIn.get, 1) // 1 + 0 + 0 = 1 poke(c.io.a, 0) poke(c.io.b, 0) expect(c.io.s, 1) expect(c.io.carryOut, 0) // 1 + 0 + 1 = 2 poke(c.io.b, 1) expect(c.io.s, 0) expect(c.io.carryOut, 1) // 1 + 1 + 1 = 3 poke(c.io.a, 1) expect(c.io.s, 1) expect(c.io.carryOut, 1) // 1 + 1 + 0 = 2 poke(c.io.b, 0) expect(c.io.s, 0) expect(c.io.carryOut, 1) } val worksHalf = iotesters.Driver(() => new HalfFullAdder(false)) { c => new HalfAdderTester(c) } val worksFull = iotesters.Driver(() => new HalfFullAdder(true)) { c => new FullAdderTester(c) } assert(worksHalf && worksFull) // Scala Code: if works == false, will throw an error println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
パラメーターhasCarry
の状況に応じて、2つのテストクラスが用意されており、それぞれのテストコードが実装されているのがわかる。
- 実行結果
FullAdderTester
の方ではhasCarry
の設定により、carryIn
がインスタンス時に入力として実装されるので、c.io.carryIn
にpoke
を使って値を与えてもエラーにならない。
[info] [0.001] Elaborating design... [info] [0.765] Done elaborating. Total FIRRTL Compile Time: 222.7 ms Total FIRRTL Compile Time: 11.9 ms End of dependency graph Circuit state created [info] [0.001] SEED 1542428606329 test cmd2HelperHalfFullAdder Success: 8 tests passed in 5 cycles taking 0.017838 seconds [info] [0.007] RAN 0 CYCLES PASSED [info] [0.000] Elaborating design... [info] [0.005] Done elaborating. Total FIRRTL Compile Time: 14.2 ms Total FIRRTL Compile Time: 11.8 ms End of dependency graph Circuit state created [info] [0.000] SEED 1542428607691 test cmd2HelperHalfFullAdder Success: 16 tests passed in 5 cycles taking 0.013780 seconds [info] [0.010] RAN 0 CYCLES PASSED SUCCESS!!
RTLへの変換
最後に、このコードを実際のVerilogのRTLに変換したものを見てみよう。
hasCarry == false
の場合
ご覧の通り、moduleのI/O宣言部にcarryIn
が存在しないのがわかる。
module cmd2HelperHalfFullAdder( // @[:@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_13; // @[cmd2.sc 9:18:@8.4] wire [2:0] sum; // @[cmd2.sc 9:26:@9.4] wire _T_15; // @[cmd2.sc 10:14:@10.4] wire _T_16; // @[cmd2.sc 11:21:@12.4] assign _T_13 = io_a + io_b; // @[cmd2.sc 9:18:@8.4] assign sum = _T_13 + 2'h0; // @[cmd2.sc 9:26:@9.4]-ここがgetOrElseでNoneが選択された部分 assign _T_15 = sum[0]; // @[cmd2.sc 10:14:@10.4] assign _T_16 = sum[1]; // @[cmd2.sc 11:21:@12.4] assign io_s = _T_15; assign io_carryOut = _T_16; endmodule
hasCarry == true
の場合
確かに、carryin
が実体化しており、計算にもcarryin
を計算に含めるコードが生成されていることを確認できる。
module cmd2HelperHalfFullAdder( // @[:@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] -- io_carryinが含まれている output io_s, // @[:@6.4] output io_carryOut // @[:@6.4] ); wire [1:0] _T_15; // @[cmd2.sc 9:18:@8.4] wire [1:0] _GEN_0; // @[cmd2.sc 9:26:@9.4] wire [2:0] sum; // @[cmd2.sc 9:26:@9.4] wire _T_16; // @[cmd2.sc 10:14:@10.4] wire _T_17; // @[cmd2.sc 11:21:@12.4] assign _T_15 = io_a + io_b; // @[cmd2.sc 9:18:@8.4] // 以下がio_carryinの計算 assign _GEN_0 = {{1'd0}, io_carryIn}; // @[cmd2.sc 9:26:@9.4] assign sum = _T_15 + _GEN_0; // @[cmd2.sc 9:26:@9.4] assign _T_16 = sum[0]; // @[cmd2.sc 10:14:@10.4] assign _T_17 = sum[1]; // @[cmd2.sc 11:21:@12.4] assign io_s = _T_16; assign io_carryOut = _T_17; endmodule
例題:ビット幅0のWireによるオプションIO
上記の例題はSome
を使ったオプションIOの作り方になっていたが、まだ別の方法が存在する。
早速コードを見てみよう。
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 = Input(if (hasCarry) UInt(1.W) else 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) } println("Half Adder:") println(getVerilog(new HalfFullAdder(false))) println("\n\nFull Adder:") println(getVerilog(new HalfFullAdder(true)))
こちらの例では、if (hasCarry)
によってcarryIn
の値を切り替えるという処理自体は一緒なのだが、こちらの例ではビット幅をhasCarry = false
の際の値をUInt(0.U)
としてビット幅0としている。
このモジュールからVerilogのRTLを生成した結果は以下の様になる。
hasCarry == false
の場合
module cmd5HelperHalfFullAdder( // @[:@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; // @[cmd5.sc 9:18:@8.4] wire [2:0] sum; // @[cmd5.sc 9:26:@9.4] wire _T_16; // @[cmd5.sc 10:14:@10.4] wire _T_17; // @[cmd5.sc 11:21:@12.4] assign _T_15 = io_a + io_b; // @[cmd5.sc 9:18:@8.4] assign sum = _T_15 + 2'h0; // @[cmd5.sc 9:26:@9.4] assign _T_16 = sum[0]; // @[cmd5.sc 10:14:@10.4] assign _T_17 = sum[1]; // @[cmd5.sc 11:21:@12.4] assign io_s = _T_16; assign io_carryOut = _T_17; endmodule
hasCarry == true
の場合
module cmd5HelperHalfFullAdder( // @[:@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; // @[cmd5.sc 9:18:@8.4] wire [1:0] _GEN_0; // @[cmd5.sc 9:26:@9.4] wire [2:0] sum; // @[cmd5.sc 9:26:@9.4] wire _T_16; // @[cmd5.sc 10:14:@10.4] wire _T_17; // @[cmd5.sc 11:21:@12.4] assign _T_15 = io_a + io_b; // @[cmd5.sc 9:18:@8.4] assign _GEN_0 = {{1'd0}, io_carryIn}; // @[cmd5.sc 9:26:@9.4] assign sum = _T_15 + _GEN_0; // @[cmd5.sc 9:26:@9.4] assign _T_16 = sum[0]; // @[cmd5.sc 10:14:@10.4] assign _T_17 = sum[1]; // @[cmd5.sc 11:21:@12.4] assign io_s = _T_16; assign io_carryOut = _T_17; endmodule
見比べるとわかると思うが、生成されるVerilogのRTL自体は全く一緒になっている。
Bundleの処理は見ていないが、ここまでの動きから推測するとBundle内の各変数がNone
or ビット幅0の値となるとFIRRTLに出力されないような実装になっているっぽい(後で確認したいところ)
Outputをオプション化する方法-- 追記(2018/11/22) --
Outputポートをオプション化するには???という疑問が湧いたので試してみた。
結論としては以下のようなコードで実装することが出来るようだ。
Bundle
を使っているのは、ただ単にこうしたらどうなるんだろう??という疑問から使っただけ。
この直前の項目で確認したビット幅0のWireによるオプションIOは使いどころ無いのでは、、、と思ったけど、しっかり存在していた(笑)
class CpuDebugMonitor(val Debug: Boolean) extends Bundle { val reg_wrdata = if (Debug) Output(UInt(16.W)) else Output(UInt(0.W)) } class OptionIO(val Debug: Boolean) extends Module { val io = IO(new Bundle { val a = Input(UInt(1.W)) val b = Output(UInt(1.W)) val dbg = new CpuDebugMonitor(Debug) }) io.b := io.a // debug io.dbg.reg_wrdata := io.a }
上記Chiselコードをから生成されるRTLがDebug
の値によってどう変化するかを確認してみよう。
Debug == true
module cmd19HelperOptionIO( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input io_a, // @[:@6.4] output io_b, // @[:@6.4] output [15:0] io_dbg_reg_wrdata // @[:@6.4] ); assign io_b = io_a; assign io_dbg_reg_wrdata = {{15'd0}, io_a}; endmodule
Debug == false
module cmd19HelperOptionIO( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input io_a, // @[:@6.4] output io_b // @[:@6.4] ); assign io_b = io_a; endmodule
見ての通りでfalse
の場合にはoutputからio_dbg_reg_wrdata
が消えているのがわかると思う。
これでChiselのオプションにまつわる話は終わりで次回でやっとModule3.1が終わるかな、、といったところという感じで次回に続く。