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

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

Chisel Bootcamp - Module2.2 (1) - 組み合わせ回路

スポンサーリンク

前回の記事ではChisel BootcampのModule2.1の学習を終えた。

www.tech-diningyo.info

今回はModule2.2に入って学習を続けていく。内容は『組み合わせ回路』だ。

Module 2.2: 組み合わせ回路

モチベーション

Module2.1の時と同じく、まずはモチベーションから見ていく。前回同様、これについては丸々引用させてもらうことにして訳したものを掲載する。

このセクションではChiselの要素を使ってどのように組み合わせ回路を実装するかについて見ていく。以下に示すChiselの基本的な3つの型がどのように接続され、動作するのかについて詳しく説明していく。

  1. UInt : unsigned integer (符号なし整数型)
  2. SInt : signed integer(符号付き整数型)
  3. Bool : true or false (ブーリアン型)

Chiselの全ての変数はScalavalとして宣言されることを知っておいてほしい。ハードウェア自体は一度定義されて以降決して変化しないので、構築時に決してScalavarを使わないこと。それらの値が変化するのはハードウェアが動作している時だけである。Wireはパラメタライズされた型としての使用は許可される。

次はModule2.1と同じくChiselのセットアップになるのだが、同様のコードを実行するだけなので割愛。

共通の演算子

まずは空のModuleを作ってどのようにそれが構築されるかを見ていく。

class MyModule extends Module {
  val io = IO(new Bundle {
    val in  = Input(UInt(4.W))
    val out = Output(UInt(4.W))
  })
}

上記とまとめると以下のようになる。

  • ChiselのModuleを継承したクラスを作ると、それはVerilogModuleに対応したものにマッピングされる
  • MyModuleは一つの入力/出力を持っていて、bit幅は4-bitのUIntになる

例題:ScalaとChiselの演算子で一緒に見えるもの

上記の空モジュールに処理を追加していく。

class MyModule extends Module {
  val io = IO(new Bundle {
    val in  = Input(UInt(4.W))
    val out = Output(UInt(4.W))
  })

  val two  = 1 + 1
  println(two)
  val utwo = 1.U + 1.U
  println(utwo)
  
  io.out := io.in
}
println(getVerilog(new MyModule))

上記のコード実行すると以下の出力が得られる。

[info] [0.001] Elaborating design...
2
chisel3.core.UInt@14
[info] [0.756] Done elaborating.
Total FIRRTL Compile Time: 334.7 ms
module cmd3HelperMyModule( // @[:@3.2]
  input        clock, // @[:@4.4]
  input        reset, // @[:@5.4]
  input  [3:0] io_in, // @[:@6.4]
  output [3:0] io_out // @[:@6.4]
);
  assign io_out = io_in;
endmodule

追加したことについてまとめると、2つのval変数を作ったが、それらは以下のようなことを示している。

  1. 1つ目はScalaInt型を2つ足しあわせ、それをprintlnで出力したので整数"2"が表示される
  2. 2つ目のvalはChiselのUInt型を足しあわせているので、printlnで出力をするとハードウェアのノードとして見えるので、その型の名前とポインタ(chisel3.core.UInt@d)が出力される

なお、上記のChiselのUIntとして扱われたコード1.Uは型のキャスト扱いとなりこの場合においてはInt(1)がChiselのUIntリテラルに変換されている。

モジュールの出力は何かによってドライブされる必要があるので、ここでは入力信号をそのまま出力に接続している。その結果このモジュールはModule2.1で作ったpassthroughモジュールになる。

非対応の動作

ここでは先程出てきた1.U1を出すと何が起こるかについて見ていく。

class MyModuleTwo extends Module {
  val io = IO(new Bundle {
    val in  = Input(UInt(4.W))
    val out = Output(UInt(4.W))
  })

  val twotwo = 1.U + 1
  println(twotwo)
  
  io.out := io.in
}
println(getVerilog(new MyModule))

見ての通りval twotwoにおいて1.U1が加算されているが、このコードを実行すると以下のようにコンパイルに失敗する。

cmd4.sc:7: type mismatch;
 found   : Int(1)
 required: chisel3.core.UInt
  val twotwo = 1.U + 1
                     ^Compilation Failed

セクション名の通り、この足し算はIntUIntの足し算であり、このような足し算は非対応となるのでコンパイラエラーが発生した。Scalaは強力に型付けされた言語なのでどのような方であっても明示的なキャストが必須となる。演算子を用いて演算を行う場合には常に型に気をつけたほうが良さそう。

他のChiselの演算子

他の演算子として

  • 減算
  • 乗算

がある。

これらはいずれもunsinged integerにおいては期待したとおりに動く。さて例を見てみよう

class MyOperators extends Module {
  val io = IO(new Bundle {
    val in      = Input(UInt(4.W))
    val out_add = Output(UInt(4.W))
    val out_sub = Output(UInt(4.W))
    val out_mul = Output(UInt(4.W))
  })

  io.out_add := 1.U + 4.U
  io.out_sub := 2.U - 1.U
  io.out_mul := 4.U * 2.U
}
println(getVerilog(new MyOperators))

出力は以下のようになる。

[info] [0.000] Elaborating design...
[info] [0.012] Done elaborating.
Total FIRRTL Compile Time: 41.0 ms
module cmd4HelperMyOperators( // @[:@3.2]
  input        clock, // @[:@4.4]
  input        reset, // @[:@5.4]
  input  [3:0] io_in, // @[:@6.4]
  output [3:0] io_out_add, // @[:@6.4]
  output [3:0] io_out_sub, // @[:@6.4]
  output [3:0] io_out_mul // @[:@6.4]
);
  wire [3:0] _T_15; // @[cmd4.sc 9:21:@8.4]
  wire [2:0] _T_16; // @[cmd4.sc 9:21:@9.4]
  wire [2:0] _T_19; // @[cmd4.sc 10:21:@11.4]
  wire [2:0] _T_20; // @[cmd4.sc 10:21:@12.4]
  wire [1:0] _T_21; // @[cmd4.sc 10:21:@13.4]
  wire [4:0] _T_24; // @[cmd4.sc 11:21:@15.4]
  assign _T_15 = 3'h1 + 3'h4; // @[cmd4.sc 9:21:@8.4]
  assign _T_16 = _T_15[2:0]; // @[cmd4.sc 9:21:@9.4]
  assign _T_19 = 2'h2 - 2'h1; // @[cmd4.sc 10:21:@11.4]
  assign _T_20 = $unsigned(_T_19); // @[cmd4.sc 10:21:@12.4]
  assign _T_21 = _T_20[1:0]; // @[cmd4.sc 10:21:@13.4]
  assign _T_24 = 3'h4 * 3'h2; // @[cmd4.sc 11:21:@15.4]
  assign io_out_add = {{1'd0}, _T_16};
  assign io_out_sub = {{2'd0}, _T_21};
  assign io_out_mul = _T_24[3:0];
endmodule

中間変数_T_Nが定義され、その中で書く計算が行われているのがわかると思う。

上記のChiselコードをテストするためのコードは例えば以下のような物になる。

class MyOperatorsTester(c: MyOperators) extends PeekPokeTester(c) {
  expect(c.io.out_add, 5)
  expect(c.io.out_sub, 1)
  expect(c.io.out_mul, 8)
}
assert(Driver(() => new MyOperators) {c => new MyOperatorsTester(c)})
println("SUCCESS!!")

Module2.1の時とは異なり、PeekPokeTesterを継承して使っているが、基本的な動きは一緒なので割愛。

因みにまだ他にも演算子はあるんだが、それはいずれ出てくるはずなのでまたの機会に一緒にまとめようと思う。

マルチプレクサと連結

Chiselではこれまでに出てきた3つの演算(加算、減算、乗算)の他に以下の演算子が用意されている

  • mux
  • concatenation

まずは早速例から。

class MyOperatorsTwo extends Module {
  val io = IO(new Bundle {
    val in      = Input(UInt(4.W))
    val out_mux = Output(UInt(4.W))
    val out_cat = Output(UInt(4.W))
  })

  val s = true.B
  io.out_mux := Mux(s, 3.U, 0.U) // should return 3.U, since s is true
  io.out_cat := Cat(2.U, 1.U)    // concatenates 2 (b10) with 1 (b1) to give 5 (101)
}

println(getVerilog(new MyOperatorsTwo))
class MyOperatorsTwoTester(c: MyOperatorsTwo) extends PeekPokeTester(c) {
  expect(c.io.out_mux, 3)
  expect(c.io.out_cat, 5)
}
assert(Driver(() => new MyOperatorsTwo) {c => new MyOperatorsTwoTester(c)})
println("SUCCESS!!")
  • 実行結果
[info] [0.000] Elaborating design...
[info] [0.022] Done elaborating.
Total FIRRTL Compile Time: 25.9 ms
module cmd6HelperMyOperatorsTwo( // @[:@3.2]
  input        clock, // @[:@4.4]
  input        reset, // @[:@5.4]
  input  [3:0] io_in, // @[:@6.4]
  output [3:0] io_out_mux, // @[:@6.4]
  output [3:0] io_out_cat // @[:@6.4]
);
  assign io_out_mux = 4'h3;
  assign io_out_cat = 4'h5;
endmodule

[info] [0.000] Elaborating design...
[info] [0.007] Done elaborating.
Total FIRRTL Compile Time: 12.1 ms
Total FIRRTL Compile Time: 14.3 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1539612763352
test cmd6HelperMyOperatorsTwo Success: 2 tests passed in 5 cycles taking 0.006764 seconds
[info] [0.005] RAN 0 CYCLES PASSED
SUCCESS!!

上記コードとテストを見れば動きは一目瞭然だが、一応ざっとまとめてみると以下のようになる。

  • Muxは3項演算子の動き
  • Catは2つ引数を受け取り、MSB→LSBの順に連結する
  • コード中のtrue.BはChiselでブーリアンを表す際に好んで使われる記法

因みに上記のMyOperatorsTwoVerilogに変換して見ると以下のようになる。

[info] [0.000] Elaborating design...
[info] [0.004] Done elaborating.
Total FIRRTL Compile Time: 12.8 ms
module cmd6HelperMyOperatorsTwo( // @[:@3.2]
  input        clock, // @[:@4.4]
  input        reset, // @[:@5.4]
  input  [3:0] io_in, // @[:@6.4]
  output [3:0] io_out_mux, // @[:@6.4]
  output [3:0] io_out_cat // @[:@6.4]
);
  assign io_out_mux = 4'h3;
  assign io_out_cat = 4'h5;
endmodule

Muxについてはtrue.Bで選択しているので"3"が、Catverilog的に書くと{2'b10, 1'b1}となり"5"がそれぞれの出力端子に出力される。これはFIRRTLがより簡潔な回路を生成するために、明らかに不要な回路を削除したためだ。

これでModule2.2のレクチャーは終了で、残りは練習問題となるのだがそれはまた後日。