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

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

ChiselのBundleとSomeを使ったデバッグ用インターフェースの作り方

スポンサーリンク

前回のChiselの記事ではBundleの基本的な使い方についてをまとめたみた。

www.tech-diningyo.info

今回は前回まとめたBundleを使ってモジュールのIOポートをオプション化する方法についてをまとめておく。

ChiselでIOポートのオプション化するには?

まずは基本的な話から再度確認しておく。 この話は実は今進めているChisel-Bootcampの中でも簡単な例が取り扱われている。
その時に”でもこれじゃ野暮ったいから、もっと楽に書けないかしら・・・”と思ったがその時は、自分のレベルが低すぎて答えが見つからなかった。
それから少し経って”あ、これでイケるんじゃん!”って気づいたので改めて、基本的なことからまとめてみようと思う。
因みに過去の記事は以下↓。

www.tech-diningyo.info

モジュールの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の宣言が滅茶めんどくさい。。

SomeBundleですっきり!!

これを書くために前回Bundleについてをまとめたと言っても過言ではない! ということでSomeBundle使うとすっきり書けます。 ただそれだけ(笑)

上の例だと以下のようになります。

// デバッグ用ポートのための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で構造化できるから、しといた方がいろいろ楽に書ける!

ということで、BundleSomeを使ったIOのオプション化の方法でした。