はじめに
第1章 そもそもChiselって?
第2章 環境構築
第3章 Scalaの基本
第4章 Chiselの基本
第5章 Chiselをもっと便利に使うために
第6章 簡単なモジュールを作ってみよう
あとがき
付録A 第6章で使用したテスト環境
本書を手にとっていただき、ありがとうございます。
この本は、Chiselというハードウェアを実装するための言語について、基本的な部分から少し応用的な部分までを解説した入門書です。
近年はますます設計の規模が大きくなり、かつ、設計にかけることのできる期間も短くなってきています。そんな中でこの課題を解決すべく、いろいろなアプローチがとられており、Chiselもそうした取り組みの中で登場したソリューションのひとつです。RISC-Vの実装(Rocket ChipやBOOM)や、Google社のEdge TPUの開発に使われたこともあり、Chiselの名前を耳にしたことがある方もいらっしゃると思います。
しかし、現状ではいざ「なんだか面白そうな言語だし、試してみるか!!」と思って調べてみても、あまり多くの情報が見つからない状態です。ましてや、日本語でまとまった情報となると推して知るべし、ですよね。
著者も「あんまり情報ないなー」と思いつつ始めてみましたが、触り始めて少しずつ理解が進むうちに「あれ、これめちゃくちゃ楽できるし、思っている以上に楽しい!!」と感じるようになりました。
そして「これもう少し世の中に発信したいなー」と思っている自分に気づき、「ならば本を書いてみよう」という運びとなりました。
Chiselを触り始めて2年程度が経過しましたが、まだ「Chisel完全に理解した!」とはとてもいえない状態です。ですが、この本にはChiselを触り始めてからの経験を振り返って、「これ、最初に知っておきたかったなぁ」と思う内容を、できる限り詰め込んで解説を試みました。
これから”Chiselを触ってみたい”という方に目を通していただければ、この上ない幸せです。少しでもChiselを触る人が増えて、コミュニティーが発展するといいな!
ということで「みんな、Chiselやろうぜ!!」
七夕 雅俊
この本は普段ハードウェアの設計に携わっている方に、手にとっていただくことを想定しています。そのため、次のような知識を持っていることが望ましいです。
1.基本的な論理回路の知識
2.Verilog-HDL or SystemVerilogを触ったことがある
「VHDLは??」という声が聞こえてきそうですが、現在のChiselは最終的にVerilog-HDLのRTLを生成することもあり、Verilog-HDLとしました。そのため、生成されるRTLコードの説明の際には、Verilog-HDLのコードを交えて解説をしています。
また、この後のScalaの文法の章でも改めて説明しますが、Scalaはオブジェクト指向と関数型言語の両面を併せ持つ、マルチパラダイム型言語です。そのため、これらのいずれかの種類の言語を触った経験があれば、いっそう理解が早まると思います。ちなみに、著者がScala&Chiselを触り始めたとき、C++やPythonといったオブジェクト指向言語の経験はありましたが、関数型言語にふれた経験はありませんでした。なので、「関数型言語なんてやったことないよー」という方も、恐れる必要はありません。
Chiselは現在も開発が盛んに行われている言語で、日々開発用のGitHubリポジトリーでは、各種の議論や機能の追加が行われています。最初に本書を執筆した時点で、Chisel 3.2.0の正式リリースが行われたため、Chisel 3.2.0で追加になった要素や、以前のバージョン(Chisel 3.1.8)と異なる点について取り扱いました。今回の改訂版の修正・加筆のタイミングでChisel 3.3.0が正式にリリースされたため、現時点では次に示すふたつのバージョンの安定版が存在しています。
・Chisel 3.2.4
・Chisel 3.3.0
Chisel 3.2.4はChisel 3.2.0の各種バグ等の修正がメインで、機能面で大きな変更は加えられていません。一方でChisel 3.3.0では機能の追加の他に、一部の処理が非推奨扱いに変更されるなど、ユーザー視点で見た場合にも影響があります。そのため、改訂版の執筆にあたり、次の確認と追記を行いました。
・Chisel 3.2.4 / Chisel 3.3.0で全サンプルコードの動作確認を実施
・Chisel 3.2.0 → 3.3.0で変更になったトピックを追加
内容へと入る前に、本書でのソースコードの表記について説明しておきます。
本書で記載するソースのコーディングスタイルについて、ここで説明しておきます。基本的にはScala Style Guide1と、GitHubに公開されているChisel Style Guide2の規約を踏襲した形となっています。
上記のスタイルに加えて、本書に掲載するChiselのコードでは、次のようにしています。これはもちろん、著者が独自にやっているだけの規則なので、ここでふれた2点については、従う必要はまったくありません。
サンプルコード中では次のように、Scalaの要素とChiselの要素を変数名で区別しています。
// Scalaの要素はキャメルケース
val someDataBits = 100
// Chiselの信号はスネークケース
val w_some_data = 0.U
これは回路のパラメタライズを行う際に、ScalaとChiselの要素をひと目で見分けやすくするためです。
Chiselのネット、レジスタ、モジュールのインスタンスについては、次のように接頭辞を付けて、区別しています。
// ネット(Wire)
val w_some_flag = false.B
// レジスタ(Reg)
val r_some_data = 0.U(8.W)
// モジュールのインスタンス
val m_some_module = Module(new SomeModule)
// Bundle(4.3.3 Bundleを参照)内の信号の宣言は接頭辞なし
class SomeBundle extends Bundle {
val some_input = Bool()
}
ChiselではVerilog-HDL/SystemVerilogとは異なり、ネットとレジスタの信号の記述の見た目が同じになります。またモジュールのインスタンスと同時に接続を行うような文法になっておらず、Scalaの変数にモジュールのインスタンスが格納されます。これを見分けるために、上記のような信号の命名規則を採用しました。
本文中に示すサンプルコードは、次に示すGitHubのリポジトリで公開しています。
・https://github.com/diningyo/introductory-guide-to-chisel
本文中で次のような形でファイル名を記載したコードブロックが登場します。このパスはGitHubのリポジトリのディレクトリパスを示すものなっています。
class SampleSource extends Module {
val io = IO(new Bundle {})
}
実行方法については、リポジトリに含まれる"README.md"をご参照ください。
本書に記載された内容は、情報の提供のみを目的としています。したがって、本書を用いた開発、製作、運用は、必ずご自身の責任と判断によって行ってください。これらの情報による開発、製作、運用の結果について、著者はいかなる責任も負いません。
Chiselで作られたものは有名ですが、Chisel自体の紹介ってあんまり見かけませんよね?本書はChiselという言語を解説した本(のつもり)なので、まずはChiselってどんな目的で作られていて、どんなことができる言語なのかについて、簡単に紹介します。
現在、ハードウェアの設計に使用されている言語、一般的にハードウェア記述言語(HDL)と呼ばれているものには、Verilog-HDLやVHDLがあります。これらの言語はもともと、ハードウェアのシミュレーション用の言語でした。そのため、いわゆる合成可能な記述は、これらの言語のサブセットとして推測されるものでした(FF推定記述と呼ばれるもの)。
また、これらの言語は、モダンなソフトウェア言語ではサポートされている、強力な抽象化の機能を持っていません。そのため、近年の複雑かつ大規模な設計作業に対して、生産性という面において、問題が顕在化してきています。近年ではSystemVerilogが登場し、型システムやパラメタライズによる生産性の向上が図られています。それでも、モダンなソフトウェア言語が保有する機能の多くは、サポートされていない状態です。
このような問題に対応するため、UC berkeleyが開発した新しいハードウェアの設計言語が、Chisel(Constructing Hardware In a Scala Embedded Language)です。ChiselはScalaの内部DSLとして設計されており、Scalaの持つ関数型言語とオブジェクト指向言語の特色を活かして、ハードウェアの設計を行うことが可能です。比較的新しい言語で、DAC2012において論文1が発表されています。
DAC2012で発表された際のChiselは、バージョンとしてはChisel 2.xになっていますが、現在では非推奨(deprecated)となっています。本書で取り扱うChisel3は、2015年の4月頃からGitHub上で開発がスタートし、執筆時点では次に示す2つの安定版が存在しています。
・Chisel 3.2.4(2020/4/24リリース)
・Chisel 3.3.0(2020/5/5リリース)
Chiselは抽象度的にはHDLと同じレイヤーで、扱うのはネットやレジスタといった論理回路のプリミティブなデータです。そのため、これまでのHDLと同等の使い方をすることも可能です。しかし、Scalaに備わったモダンな機能を使うことで、作成するモジュールをパラメタライズし、ひとつのChiselモジュールから、複数の異なったRTLを生成することが可能となっています。このため、Chiselの公式の見解では、HCL(Hardware Construction Language)と呼称されています。
生産性という面で、HLS(High Level Synthesis)を思い浮かべた方もいると思います。ではChiselはHLSなのか?というと、HLSとは明確に異なる、というのが著者の見解です。HLSは、実現したいアルゴリズムを記述し、ハードウェアに変換しますが、Chiselにはこのような機能は備わっていません。
その代わりに、ChiselではHDLと同等の記述をパラメタライズして抽象度を上げ、生産性を向上させることができます。これらのことを踏まえて、Chiselのポジションを図示すると、図1.1のようになるでしょうか。
だいたい、HDLとHLSの中間あたりに存在する、とイメージしてもらえるとよいかと思います。図ではどちらかといえば、HDLよりに配置しました。HLSは、実現したいアルゴリズムを回路に落としこむという、トップダウン的なイメージです。いっぽうChiselは、HDL起点でもっと柔軟に回路を構成するために機能を追加した、ボトムアップ的なアプローチであるように感じます。Chiselでは基本的な文法を使うと、HDLと等価なデザインを作ることができます。Chiselの持つパラメタライズの機能をフルに使うことで抽象度を高め、生産性を向上することが可能です。
Scalaやら関数型言語やら、耳馴染みのない言葉と思われた方もいるかもしれません。ですが、実際に設計するのはハードウェアです。まずはどんな言語なのか、簡単なサンプルで確認してみましょう。ソースの細かい部分は置いておいて、普段ご自身が設計に使用している言語と比較しながら、Chiselで記述するとどんな風に書けるのかを、見てみてください。
例題にするのは、ハードウェア設計では皆さんお馴染みのFIFOです。
import chisel3._
import chisel3.util._
/**
* FIFO リード側 I/O
*/
class FIFORdIO(bits: Int) extends Bundle {
val enable = Input(Bool())
val empty = Output(Bool())
val data = Output(UInt(bits.W))
}
/**
* FIFO ライト側 I/O
*/
class FIFOWrIO(bits: Int) extends Bundle {
val enable = Input(Bool())
val full = Output(Bool())
val data = Input(UInt(bits.W))
}
/**
* FIFO I/O
* @param bits データのビット幅
* @param depth FIFOの段数
* @param debug trueでデバッグモード
*/
class FIFOIO(bits: Int, depth: Int = 16, debug: Boolean = false)
extends Bundle {
val depthBits = log2Ceil(depth)
val wr = new FIFOWrIO(bits)
val rd = new FIFORdIO(bits)
val dbg = if (debug) { Some(Output(new Bundle {
val r_wrptr = Output(UInt(depthBits.W))
val r_rdptr = Output(UInt(depthBits.W))
val r_data_ctr = Output(UInt((depthBits + 1).W))
})) } else {
None
}
override def cloneType: this.type =
new FIFOIO(bits, depth, debug).asInstanceOf[this.type]
}
/**
* 単純なFIFO
* @param dataBits データのビット幅
* @param depth FIFOの段数
* @param debug trueでデバッグモード
*/
class FIFO(dataBits: Int = 8, depth: Int = 16, debug: Boolean = false)
extends Module {
// parameter
val depthBits = log2Ceil(depth)
def ptrWrap(ptr: UInt): Bool = ptr === (depth - 1).U
val io = IO(new FIFOIO(dataBits, depth, debug))
val r_fifo = RegInit(VecInit(Seq.fill(depth)(0.U(dataBits.W))))
val r_rdptr = RegInit(0.U(depthBits.W))
val r_wrptr = RegInit(0.U(depthBits.W))
val r_data_ctr = RegInit(0.U((depthBits + 1).W))
// リードポインタ
when(io.rd.enable) {
r_rdptr := Mux(ptrWrap(r_rdptr), 0.U, r_rdptr + 1.U)
}
// ライトポインタ
when(io.wr.enable) {
r_fifo(r_wrptr) := io.wr.data
r_wrptr := Mux(ptrWrap(r_wrptr), 0.U, r_wrptr + 1.U)
}
// データカウント
when (io.wr.enable && io.rd.enable) {
r_data_ctr := r_data_ctr
} .otherwise {
when (io.wr.enable) {
r_data_ctr := r_data_ctr + 1.U
}
when (io.rd.enable) {
r_data_ctr := r_data_ctr - 1.U
}
}
// IOとの接続
io.wr.full := r_data_ctr === depth.U
io.rd.empty := r_data_ctr === 0.U
io.rd.data := r_fifo(r_rdptr)
// テスト用のデバッグ端子の接続
if (debug) {
io.dbg.get.r_wrptr := r_wrptr
io.dbg.get.r_rdptr := r_rdptr
io.dbg.get.r_data_ctr := r_data_ctr
}
}
/**
* FIFOのRTL生成処理
*/
object ElaborateFIFO extends App {
chisel3.Driver.execute(Array(""), () => new chapter1.FIFO(16))
}
リスト1.1のコードの中のElaborateFIFOを、Chiselの開発環境上で実行して、Verilog-HDLのRTLを生成してみます。
[info] Compiling 1 Scala source to /xxxx/target/scala-2.12/classes ...
[info] Done compiling.
[warn] Multiple main classes detected. Run 'show discoveredMainClasses'
to see the list
[info] running Generator chapter1.FIFO
[info] [0.001] Elaborating design...
[info] [0.109] Done elaborating.
Total FIRRTL Compile Time: 515.7 ms
[success] Total time: 2 s, completed 2019/11/30 18:14:34
[IJ]sbt:chisel-samples>
エラボレートが通って、処理が成功したように見えますね?では、実際に生成されたRTLも確認してみましょう。
module FIFO(
input clock,
input reset,
input io_wr_enable,
input [7:0] io_wr_data,
input io_rd_enable,
output io_rd_empty,
output [7:0] io_rd_data,
output io_full
);
reg [7:0] r_fifo_0; // @[FIFO.scala 55:23]
reg [31:0] _RAND_0;
// ~レジスタ宣言は省略~
reg [7:0] r_fifo_15; // @[FIFO.scala 55:23]
reg [31:0] _RAND_15;
reg [3:0] r_rdptr; // @[FIFO.scala 56:24]
reg [31:0] _RAND_16;
reg [3:0] r_wrptr; // @[FIFO.scala 57:24]
reg [31:0] _RAND_17;
reg [4:0] r_data_ctr; // @[FIFO.scala 58:27]
reg [31:0] _RAND_18;
// 中間変数は以下のように_T_xx/_GEN_xxとなる
wire _T_1; // @[FIFO.scala 51:41]
wire [3:0] _T_3; // @[FIFO.scala 62:54]
wire _T_5; // @[FIFO.scala 51:41]
wire [3:0] _T_7; // @[FIFO.scala 68:54]
wire _T_9; // @[FIFO.scala 72:22]
wire [4:0] _T_11; // @[FIFO.scala 76:32]
wire [4:0] _T_13; // @[FIFO.scala 79:32]
wire [7:0] _GEN_38; // @[FIFO.scala 86:14]
// ~中略~
assign _T_1 = r_rdptr == 4'hf; // @[FIFO.scala 51:41]
assign _T_3 = r_rdptr + 4'h1; // @[FIFO.scala 62:54]
assign _T_5 = r_wrptr == 4'hf; // @[FIFO.scala 51:41]
assign _T_7 = r_wrptr + 4'h1; // @[FIFO.scala 68:54]
assign _T_9 = io_wr_enable & io_rd_enable; // @[FIFO.scala 72:22]
assign _T_11 = r_data_ctr + 5'h1; // @[FIFO.scala 76:32]
assign _T_13 = r_data_ctr - 5'h1; // @[FIFO.scala 79:32]
assign _GEN_38 = 4'h1 == r_rdptr ? r_fifo_1 : r_fifo_0; // @[FIFO.scala 86:14]
// r_fifoの選択記述も省略。ひたすら三項演算子が続くだけ。
assign _GEN_51 = 4'he == r_rdptr ? r_fifo_14 : _GEN_50; // @[FIFO.scala 86:14]
assign io_rd_empty = r_data_ctr == 5'h0; // @[FIFO.scala 85:15]
assign io_rd_data = 4'hf == r_rdptr ? r_fifo_15 : _GEN_51; // @[FIFO.scala 86:14]
assign io_full = r_data_ctr == 5'h10; // @[FIFO.scala 84:11]
// こんな感じでVerilator用のランダマイズの記述が入る(
`ifdef RANDOMIZE_GARBAGE_ASSIGN
`define RANDOMIZE
`endif
// レジスタは以下のように、ひとつのalways文に展開される
always @(posedge clock) begin
if (reset) begin
r_fifo_0 <= 8'h0;
end else if (io_wr_enable) begin
if (4'h0 == r_wrptr) begin
r_fifo_0 <= io_wr_data;
end
end
~中略~
if (reset) begin
r_fifo_15 <= 8'h0;
end else if (io_wr_enable) begin
if (4'hf == r_wrptr) begin
r_fifo_15 <= io_wr_data;
end
end
if (reset) begin
r_rdptr <= 4'h0;
end else if (io_rd_enable) begin
if (_T_1) begin
r_rdptr <= 4'h0;
end else begin
r_rdptr <= _T_3;
end
end
if (reset) begin
r_wrptr <= 4'h0;
end else if (io_wr_enable) begin
if (_T_5) begin
r_wrptr <= 4'h0;
end else begin
r_wrptr <= _T_7;
end
end
if (reset) begin
r_data_ctr <= 5'h0;
end else if (!(_T_9)) begin
if (io_rd_enable) begin
r_data_ctr <= _T_13;
end else if (io_wr_enable) begin
r_data_ctr <= _T_11;
end
end
end
endmodule
Chiselを使った開発のメリットのひとつに、Chiselに標準で備わっているテスト機構があります。こちらも見てみましょう。リスト1.4/リスト1.5に示すテストコードを準備します。
リスト1.4は、FIFOモジュールに対しての操作を実装したテストクラスです。テスト対象のモジュールの規模にもよりますが、実装しておくと、各テストをシンプルに記述できます。
/**
* FIFOの単体テストクラス
* @param c FIFOモジュールのインスタンス
*/
class FIFOUnitTester(c: FIFO) extends PeekPokeTester(c) {
/**
* アイドル
*/
def idle(): Unit = {
poke(c.io.rd.enable, false)
poke(c.io.wr.enable, false)
step(1)
}
/**
* FIFOにデータを書き込む
* @param data データ
*/
def push(data: BigInt): Unit = {
poke(c.io.wr.enable, true)
poke(c.io.wr.data, data)
step(1)
}
/**
* FIFOのデータを読みだし、期待値と比較
* @param exp 期待値
*/
def pop(exp: BigInt): Unit = {
expect(c.io.rd.data, exp)
poke(c.io.rd.enable, true)
step(1)
}
/**
* プッシュとポップを同時に行う
* @param data 設定するデータ
* @param exp 期待値
*/
def pushAndPop(data: BigInt, exp: BigInt): Unit = {
expect(c.io.rd.data, exp)
poke(c.io.rd.enable, true)
poke(c.io.wr.enable, true)
poke(c.io.wr.data, data)
step(1)
}
}
リスト1.5のコードは、FIFOの各テストを実装したテストクラスになります。ChiselではScalaTestを利用した、BDD(Behavior Driven Development)形式のテストを実装可能です。ここでは長くなるため、最初のテストのみを記載しています。
/**
* FIFOのテストクラス
*/
class FIFOTester extends ChiselFlatSpec {
val dutName = "chapter1.FIFO"
val dataBits = 8
val depth = 16
it should "ホストがpushを実行すると、FIFOにデータが書き込まれる [FIFO-000]" in {
val outDir = dutName + "-fifo-push"
val args = Array(
"--top-name", dutName,
"--target-dir", s"test_run_dir/$outDir",
"-tgvo=on"
)
Driver.execute(args, () => new FIFO(dataBits, depth, true)) {
c => new FIFOUnitTester(c) {
val setData = Range(0, 16).map(_ => floor(random * 256).toInt)
expect(c.io.rd.empty, true)
for ((data, idx) <- setData.zipWithIndex) {
push(data)
expect(c.io.rd.empty, false)
expect(c.io.rd.data, setData(0))
}
idle()
}
} should be (true)
}
}
これを実行するとリスト1.6に示すログが出力されて、テストをPASSしていることが確認できました。
[IJ]sbt:chisel-samples> testOnly chapter1.FIFOTester
[info] Compiling 1 Scala source to /xxxx/target/scala-2.12/test-classes ...
[warn] there were three feature warnings; re-run with -feature for details
[warn] one warning found
[info] Done compiling.
[info] [0.002] Elaborating design...
[info] [0.175] Done elaborating.
Total FIRRTL Compile Time: 468.3 ms
file loaded in 0.100940596 seconds, 136 symbols, 115 statements
[info] [0.001] SEED 1590890484965
test FIFO Success: 33 tests passed in 22 cycles in 0.053316 seconds 412.64 Hz
[info] [0.019] RAN 17 CYCLES PASSED
~~中略~~
[info] FIFOTester:
[info] - should ホストがpushを実行すると、FIFOにデータが書き込まれる [FIFO-000]
[info] - should ホストがpopを実行すると、FIFOからデータが読み出される [FIFO-001]
[info] - should pushとpopが同時に起きた場合、FIFOのデータ数は維持される [FIFO-002]
[info] - should FIFOの段数を超えるデータが設定されると、ポインタはオーバーラップする [FIFO-003]
[info] ScalaTest
[info] Run completed in 2 seconds, 156 milliseconds.
[info] Total number of tests run: 4
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 4, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 4, Failed 0, Errors 0, Passed 4
[success] Total time: 3 s, completed 2020/05/31 11:01:26
FIFOは、さまざまなシチュエーションで使用されるハードウェアのブロックなので、よく流用して再設計を行うことがあると思います。そのような場合に、なるべく手間を減らすために、パラメタライズを行うこともあると思います。Chiselの持つ機能を使うことで、強力なパラメタライズが可能となり、柔軟性のあるハードウェアのジェネレーターを作成できます。リスト1.1のFIFOでも、FIFOの段数やデータのビット幅をパラメタライズできるようしています。
/**
* 単純なFIFO
* @param dataBits データのビット幅
* @param depth FIFOの段数
* @param debug trueでデバッグモード
*/
class FIFO(dataBits: Int = 8, depth: Int = 16, debug: Boolean = false)
extends Module {
// parameter
val depthBits = log2Ceil(depth)
def ptrWrap(ptr: UInt): Bool = ptr === (depth - 1).U
val io = IO(new FIFOIO(dataBits, depth, debug))
val r_fifo = RegInit(VecInit(Seq.fill(depth)(0.U(dataBits.W))))
val r_rdptr = RegInit(0.U(depthBits.W))
Verilog-HDLやSystemVerilogを使っている方の中には、「これくらいならVerilog-HDL/SystemVerilogでもできる」という感想をお持ちになった方もいるかもしれません。その感想のとおりで、Chiselで行うパラメタライズの一部は、従来の設計言語においても行うことが可能です。本書の後半では、Verilog-HDLでは複雑になりがちなI/F(インターフェイス)のパラメタライズや、Scalaのクラスパラメーターを使って、モジュールで扱うデータをパラメタライズする方法などについても紹介します。