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

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

Chiselの文法 - 入門編 〜その8:回路のビット幅のパラメタライズとrequire〜

スポンサーリンク

Chiselの文法入門の続きで今回は第8回目
前回までの記事でChiselの基本的な要素についての解説を一通り終えました。
今回からはChiselを使って論理回路を設計する際のメリットである回路のパラメタライズについての基本的な部分を紹介していきます。

Chisel入門編〜その8:回路のパラメタライズ:その1〜

前回までの記事でChiselの基本的な要素についての解説を一通り終えました。
そこまでをご覧になられて「これならSystem Verilogでも同じこと出来るでしょ」と思われた方もいると思います。
そのご意見は至極最もな話で、前回までの範囲でChiselを書いていても記述量的にはせいぜい2/3くらいになるのがいいとこだと思います。
Chiselで各メリットを真の意味で享受するには、今回紹介するパラメタライズを使った回路のカスタマイズが出来てナンボだと個人的には思っています。これを使いこなせれば、ひとつのChiselのコードから様々な派生をしたRTLを生成出来る!!はず、、、ですが、筆者自身もここは色々試している最中です。
このパラメタライズをゴリゴリに推し進めたのがRISC-Vの一実装であるRocket-ChipやFreedomのような各種ボード向けのシステムになります、、、、がこの辺はChiselの内部まで踏み込んで理解しないと、読むことすらままならない感じになります。。。
#筆者も絶賛格闘中。deplomacyパターンむずい。
こんな感じなので、まだあんまり使いこなせてはいない部分もあるのですが、回路のパラメタライズについて、例を示しながら見ていきたいと思います。

簡単なパラメタライズの例

ということで、早速簡単な例から見ていきます。
ここでご紹介するのはVerilog HDL等で行うparameterの伝搬に相当する処理をChiselで書くとどうなるのか??という話です。
Verilog HDLだとこんなのに相当するやつです。

module Memory
  #(
    parameter ADDR_WID = 16,
    parameter DATA_WID = 16
  )
  (
    input [ADDR_WID-1:0] addr,
    input [DATA_WID-1:0] data
    // ... 他のポート
  )

  // 回路記述
endmodule

ビット幅の調整

まずはよくやるビット幅のパラメタライズの例を示します。
基本的にパラメタライズを行う場合は、各モジュール等のクラス宣言にパラメータを追加して、その値でパラメタライズを実施します。
例えば以下のような形です。

class Memory(addrWid: Int, dataWid: Int) extends Module {
  val io = IO(new Bundle {
    val addr = Input(UInt(addrWid.W))
    val rden = Input(Bool())
    val wren = Input(Bool())
    val rddata = Output(UInt(dataWid.W))
    val wrdata = Input(UInt(dataWid.W))
  })

  // 紹介してなかったけど、ChiselにはMemというオブジェクトがあり
  // これはVerilog HDLのメモリ配列にそのままマッピングされます
  // 書式はMem(<要素数>, <データ型>)
  // 以下の例では、要素数1024でUInt(8.W)のメモリが生成されます。
  val mem = Mem(1024, UInt(dataWid.W))

  when (io.wren) {
    // アクセスの仕方はVecと一緒
    mem(io.addr) := io.wrdata
  }

  io.rddata := Mux(io.rden, mem(io.addr), 0.U)

  // ビット幅のチェック
  println(f"addr width = ${io.addr.getWidth}")
  println(f"data width = ${io.rddata.getWidth}")
}

上記のようにしておき、モジュールのインスタンス時に所望のビット幅を与えることでビット幅のパラメタライズが可能です。
上記の回路をエラボレートして各ビット幅がどうなるかを確認してみます。

  • エラボレート用のメイン
    • forループでビット幅を変更してエラボレートを行います。
object Elaborate extends App {
  for (addrWid <- 15 to 16; dataWid <- 16 to 17) {
    // エラボレート時にパラメータを与える
    chisel3.Driver.execute(Array(""), () => new Memory(addrWid, dataWid))
  }
}
  • エラボレート結果
[info] [0.000] Elaborating design...
addr width = 15
data width = 16
[info] [0.004] Done elaborating.
Total FIRRTL Compile Time: 76.2 ms
[info] [0.000] Elaborating design...
addr width = 15
data width = 17
[info] [0.002] Done elaborating.
Total FIRRTL Compile Time: 11.5 ms
[info] [0.000] Elaborating design...
addr width = 16
data width = 16
[info] [0.002] Done elaborating.
Total FIRRTL Compile Time: 14.9 ms
[info] [0.000] Elaborating design...
addr width = 16
data width = 17
[info] [0.003] Done elaborating.
Total FIRRTL Compile Time: 15.6 ms

結果についてはご覧の通りでループして生成した各エラボレート結果毎にビット幅が変化しているのが確認できると思います。

ビット幅の自動計算

上記のメモリの場合はどちらかというと、決まったアドレス幅を与えるよりは「メモリのサイズからビット幅を自動で計算」したいという方が多いと思います。
System Verilogでは$clog2を使って計算できますがChiselでもこのようなニーズに備えて計算用のユーティリティとしてlog2Ceilが用意されています。

import scala.math.pow
import chisel3.util._

/**
 * chisel3.util.Log2Ceilのサンプル
 */
object UseLog2Ceil extends App {
  val size = Range(1, 16).map(pow(2, _).toInt) // [2, 4, 8, ... 65536]

  // log2Ceil(<data: Int>)でビット幅が計算できる
  size.foreach(size => println(f"bit width = ${log2Ceil(size)}"))
}
  • 実行結果
bit width = 1
bit width = 2
bit width = 3
bit width = 4
bit width = 5
bit width = 6
bit width = 7
bit width = 8
bit width = 9
bit width = 10
bit width = 11
bit width = 12
bit width = 13
bit width = 14
bit width = 15
bit width = 16

パラメタライズに対しての制約の付与

このように色々パラメタライズ出来る!!となってしまうと、設計者の意図を超えたパラメータが指定されるケースも出てきてしまいます。
Scalaにはクラスパラメータ等をチェックするためにrequireというメソッドが用意されており、これを使うことでモジュールクラスのインスタンス生成時に、パラメータのチェックを行うことが可能です。
使い方はScalaassertと一緒で以下のようになります。

require(<条件式>, [<発火時のメッセージ>])

早速先ほどのサンプルにrequireを追加して確認してみます。
メモリのサイズが1024固定になっているので、メモリの幅がメモリのサイズ以上になって欲しいので、それを確認してみました。

  • requireを使ったパラメータの限定
class Memory(addrWid: Int, dataWid: Int) extends Module {
  val io = IO(new Bundle {
    val addr = Input(UInt(addrWid.W))
    val rden = Input(Bool())
    val wren = Input(Bool())
    val rddata = Output(UInt(dataWid.W))
    val wrdata = Input(UInt(dataWid.W))
  })

  // アドレスのチェック
  require(pow(2, addr).toInt > 1024,
   "Memory size is 1024, so addr bit width must be greater than equal to 10")

  val mem = Mem(1024, UInt(dataWid.W))

  when (io.wren) {
    mem(io.addr) := io.wrdata
  }

  io.rddata := Mux(io.rden, mem(io.addr), 0.U)

  // ビット幅のチェック
  println(f"addr width = ${io.addr.getWidth}")
  println(f"data width = ${io.rddata.getWidth}")
}
  • アドレスビット幅が9の場合のエラボレート結果
    • IllegalArgumentExceptionがスローされる
[info] [0.000] Elaborating design...
java.lang.IllegalArgumentException: requirement failed:
Memory size is 1024, so bit width must be greater than equal to 10
  scala.Predef$.require(Predef.scala:224)
  ammonite.$sess.cmd19$Helper$Memory.<init>(cmd19.sc:14)
  ammonite.$sess.cmd19$Helper$$anonfun$4.apply(cmd19.sc:31)
  ammonite.$sess.cmd19$Helper$$anonfun$4.apply(cmd19.sc:31)
  chisel3.core.Module$.do_apply(Module.scala:49)
  chisel3.Driver$$anonfun$elaborate$1.apply(Driver.scala:93)
  chisel3.Driver$$anonfun$elaborate$1.apply(Driver.scala:93)
  chisel3.internal.Builder$$anonfun$build$1.apply(Builder.scala:297)
  chisel3.internal.Builder$$anonfun$build$1.apply(Builder.scala:295)
  scala.util.DynamicVariable.withValue(DynamicVariable.scala:58)
  chisel3.internal.Builder$.build(Builder.scala:295)
  chisel3.Driver$.elaborate(Driver.scala:93)
  chisel3.Driver$.execute(Driver.scala:140)
  chisel3.Driver$.execute(Driver.scala:202)
  ammonite.$sess.cmd19$Helper.<init>(cmd19.sc:31)
  ammonite.$sess.cmd19$.<init>(cmd19.sc:7)
  ammonite.$sess.cmd19$.<clinit>(cmd19.sc:-1)
  • アドレスビット幅が10の場合のエラボレート結果
    • 正常にエラボレートされる
[info] [0.000] Elaborating design...
addr width = 10
data width = 8
[info] [0.002] Done elaborating.
Total FIRRTL Compile Time: 7.4 ms

パラメタライズを始めると、かなり自由度が上がるのでこのrequireを使ったチェックはしっかり入れておいた方が良いと思います。

今日はキリが良いので、ここまでします。
次回はパラメタライズ編その2として、IOポートのカスタマイズについてを紹介します。