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

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

Chisel Bootcamp - Module3.1(5) - オプション付きのI/Oポート宣言

スポンサーリンク

前回の記事ではChisel BootcampのModule3.1でScalaの文法であるMatch文の復習とそれをChiselに適用するとどうなるかということについてを見ていった。

www.tech-diningyo.info

今日も引き続きModule3.1を見ていく。今日はオプション付きのIO宣言についてだ。

Module 3.1: ジェネレータ:パラメータ

オプション付きのI/Oポート宣言

ここでいうオプションはRTLのinput/outputをパラメタライズの対象とすることを言っている。例えばこのようなモジュールをVerilogで書くと以下の様にifdefdefineを使うことになる。

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 */

個人的にはあんまりやりたくない書き方の一つ。Verilogdefineは順序に気をつけないとめんどくさいし、何よりコードがわかりにくい。

例題:オプションを使ったオプション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.carryInpokeを使って値を与えてもエラーにならない。

[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が終わるかな、、といったところという感じで次回に続く。