前回の続きでChiselの文法入門編その6
今回はChiselにおける拡張型であるVec
とBundle
の2つについてまとめて書こうを思ったのですが、思いの外膨らんだので、Vec
メインにしました。
Chisel入門編〜その6:Vec〜
その2でChiselの”型”として、以下を紹介しましたが実はChiselにはまだ幾つか”型”が存在しています。
今回のその6ではその残りの型の中からVec
とBundle
についての概要を紹介し、主にVec
の使い方についてをまとめます。
一言で書くとBundle
とVec
は共にChiselの基本の型を束ねる目的で使用される拡張型といえる存在です。
Verlog HDL及びSystem Verilogで言うと、以下のようなものに置き換えることが可能です。
Vec
- メモリ配列Bundle
- 構造体
Vec
とBundle
の位置づけ
これについては以下の画像を見てもらうと分かりやすいと思います。
ご覧いただいたとおりでBool
/UInt
といった型はBits
クラスを継承しているのに対して、Vec
/Bundle
はそれぞれAggregate
/Record
クラスを継承したものになっています。
これらのクラスは内部にData
型のインスタンスをSeq
やListMap
に入れて管理しており、複数のData
型をまとめて扱うことが出来るようになっています。
これだけだとScalaのSeq
等と一緒じゃん。。となるのですがVec
は実際の回路上で動的に変化する要素に対して使えるところに大きな違いがあります。
では使い方についてを見ていこうと思います。
Vec
型
冒頭に記載した通りVec
はVerilog HDLにおけるメモリ配列に相当する概念です。
すぐ上に書いたとおりScalaだとSeq
と似たデータ型ですが、Chiselにおいてはこの2つは、以下のように評価のタイミングが異なっています。
- Scalaの
Seq
: Chiselのエラボレート時に評価され、ハードウェア要素が固定される。
val addr = Wire(UInt(2.W)) val a = Seq.fill(4)(RegInit(0.U(32.W))) // 以下はエラーになる: // → Seqで宣言したRegInitの配列の要素参照に // Chiselのハードウェア要素であるaddrを使っているため a(addr) := io.wrdata
- Chiselの
Vec
: 回路の動作中の信号を使って評価される。
val addr = Wire(UInt(2.W)) val a = Reg(Vec(4, UInt(32.W))) a(addr) := io.wrdata
早い話がChiselのVec
を使うと通常の論理回路におけるアドレス信号等の信号を使ってVec
で宣言したChiselのハードウェア要素に動的にアクセスが可能になるということです。
使い分けとしては以下のように考えると良いと思います。
- Scalaのcollection型:エラボレート時に回路構成が決まるもの(例えばマルチプレクサの入力数とか、フィルタの段数とか)
- ChiselのVec:論理回路の動作中に他のChiselのハードウェア要素に応じて参照する場所が変化するもの(レジスタファイルのアドレスとか)
Vec
の宣言
基本形
Vec
の宣言は以下のような形になります。
val a = Vec(<要素数>, <Chiselの型のインスタンス>)
要素数10で8bitのUInt
のVec
を宣言すると以下のような形になります。
val a = Vec(10, UInt(8.W))
なお、Vec
もChiselの”ハードウェア要素”ではなくChiselの"型"になるので、実際にハードウェアとして作成する論理回路の一部にするためには”ハードウェア要素”でラッピングする必要があります。
上記の”要素数10で8bitのUInt
のVec
”をハードウェアとしてインスタンスする場合は以下のような感じです。
Wire
の場合
val wireVecA = Wire(Vec(10, UInt(8.W)))
Reg
の場合
val wireVecA = Reg(Vec(10, UInt(8.W)))
Chiselを書き始めた時はこの区別がついてなかったので、以下のようにVec
を外側にしてエラーが発生しまくってました。。。
- 注意:以下はエラーになる例です
val wireVecA = Vec(10, Wire(UInt(8.W))) // エラー
エラーメッセージは前回紹介したコレ↓
[error] (run-main-5) chisel3.core.Binding$ExpectedChiselTypeException: vec type 'chisel3.core.UInt@a' must be a Chisel type, not hardware
初期値が必要な場合
さて上記でVec
の宣言の仕方は分かったと思いますが、ここで以下のような場合はどうするの??と思った方がいるのではないでしょうか?
- Q:初期値付きのレジスタ
RegInit
や既存の信号を受けるRegNext
の場合も一緒で良いのか?
この問についての答えですが、以下のようになります。
- A:ちょっと宣言の仕方が変わります。
早速見てみましょう。
RegInit
を使う場合
val vecRegInit = RegInit(VecInit(Seq.fill(10)(0.U)))
見てお気づきになられたと思いますが、上記の例では全て0.U
でレジスタが初期化されます。
このやり方は以下のChiselのwikiで発見しました。
RegNext
を使う場合
RegNext
は2回目で紹介したとおり、特定の信号をレジスタ受けする場合に使用しますが、初期値のあり/なしを選択することが可能です。これらのケースについてそれぞれの記述を見てみましょう。
val vecReg = Reg(Vec(10, UInt(8.W))) // 初期値なしの場合 val vecRegNextWoInit = RegNext(vecReg) // 元のハードウェア要素のVecを入れればOK // 初期値ありの場合 val vecRegNextWithInit = RegNext(vecReg, VecInit(Seq.fill(10)(0.U(8.W))) // 別途特定の値の初期化したVecInitが必要
ここでは全てのレジスタを固定値0.U
で生成するためにSeq.fill
を使っていますが、それぞれ別の値で初期化したい場合にはVecInit
に渡すSeq
をその値で埋めたものを渡せばOKです。
レジスタファイルの例で見るVec
の使い方
例えばVerilog HDLでCPUで使うようなレジスタファイルを作成するケースを考えてみます。
その場合、メモリ配列を使って以下のような感じになると思います。
使う場合コードは以下のようにgenerate
構文を使って処理するようなイメージになると思います。
module reg_file ( input clk, input [4:0] rdaddr_0, output [31:0] rddata_0, input [4:0] rdaddr_1, output [31:0] rddata_0, input [4:0] wraddr, input wren, input [31:0] wrdata ); reg [31:0] regs[0:31]; always @(posedge clk) begin if (wren) begin regs[wraddr] <= wrdata; end end rddata_0 = regs[rdaddr_0]; rddata_1 = regs[rdaddr_1]; endmodule
- Chiselで書くレジスタファイル
同じものをChiselで書いてみると以下のようになります。
class RegFile extends Module { val io = IO(new Bundle { val rdaddr_0 = Input(UInt(5.W)) val rddata_0 = Output(UInt(32.W)) val rdaddr_1 = Input(UInt(5.W)) val rddata_1 = Output(UInt(32.W)) val wren = Input(Bool()) val wraddr = Input(UInt(5.W)) val wrdata = Input(UInt(32.W)) }) val regFileVec = RegInit(VecInit(Seq.fill(10)(false.B))) when (io.wren) { regFileVec(io.wraddr) := io.wrdata } io.rddata_0 := regFileVec(io.rdaddr_0) io.rddata_1 := regFileVec(io.rdaddr_1) }
ほぼ見た目一緒ですね(´・ω・`)
せっかくVec
が使えるようになったのでリード系の端子もまとめてしまいましょう。
class RegFile extends Module { val io = IO(new Bundle { val rdaddr = Input(Vec(2, UInt(5.W))) val rddata = Output(Vec(2, UInt(32.W))) val wren = Input(Bool()) val wraddr = Input(UInt(5.W)) val wrdata = Input(UInt(32.W)) }) val regFileVec = RegInit(VecInit(Seq.fill(32)(false.B))) when (io.wren) { regFileVec(io.wraddr) := io.wrdata } for ((rddata, addr) <- io.rddata.zip(io.rdaddr)) { rddata := regFileVec(addr) } }
もう少しScalaっぽくするためにfor
ループ部分を高階関数を使って書き直すと以下のようになります。
ですがコレくらいのものだと、上の例のようにzip
で作ったイテレータをfor
ループで回したほうがわかりやすいですね^^;
io.rddata.zip(io.rdaddr).foreach {
case (data, addr) => data := regFileVec(addr)
}
因みに”Verilog HDLのメモリ配列に相当する要素”と書きましたが、これは文法上の扱いだけでChiselのVec
はRTLに変換するとVec
に指定したChiselのハードウェア要素にインデックスが付与された形でベタにRTL上に展開されます。
以下は上のRegFile
の要素数を4に変更して生成したRTLです。
module RegFile( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [4:0] io_rdaddr_0, // @[:@6.4] output [31:0] io_rddata_0, // @[:@6.4] input [4:0] io_rdaddr_1, // @[:@6.4] output [31:0] io_rddata_1, // @[:@6.4] input io_wren, // @[:@6.4] input [4:0] io_wraddr, // @[:@6.4] input [31:0] io_wrdata // @[:@6.4] ); reg regFileVec_0; // @[cmd18.sc 12:27:@13.4] reg [31:0] _RAND_0; reg regFileVec_1; // @[cmd18.sc 12:27:@13.4] reg [31:0] _RAND_1; reg regFileVec_2; // @[cmd18.sc 12:27:@13.4] reg [31:0] _RAND_2; reg regFileVec_3; // @[cmd18.sc 12:27:@13.4] reg [31:0] _RAND_3; wire [1:0] _T_63; // @[:@15.6] wire _regFileVec_T_63; // @[cmd18.sc 15:27:@16.6] wire _GEN_0; // @[cmd18.sc 15:27:@16.6] wire _GEN_1; // @[cmd18.sc 15:27:@16.6] wire _GEN_2; // @[cmd18.sc 15:27:@16.6] wire _GEN_3; // @[cmd18.sc 15:27:@16.6] wire _GEN_4; // @[cmd18.sc 14:18:@14.4] wire _GEN_5; // @[cmd18.sc 14:18:@14.4] wire _GEN_6; // @[cmd18.sc 14:18:@14.4] wire _GEN_7; // @[cmd18.sc 14:18:@14.4] wire [1:0] _T_67; // @[:@18.4] wire _GEN_9; // @[cmd18.sc 18:15:@19.4] wire _GEN_10; // @[cmd18.sc 18:15:@19.4] wire _GEN_11; // @[cmd18.sc 18:15:@19.4] wire [1:0] _T_71; // @[:@20.4] wire _GEN_13; // @[cmd18.sc 19:15:@21.4] wire _GEN_14; // @[cmd18.sc 19:15:@21.4] wire _GEN_15; // @[cmd18.sc 19:15:@21.4] assign _T_63 = io_wraddr[1:0]; // @[:@15.6] assign _regFileVec_T_63 = io_wrdata[0]; // @[cmd18.sc 15:27:@16.6] assign _GEN_0 = 2'h0 == _T_63 ? _regFileVec_T_63 : regFileVec_0; // @[cmd18.sc 15:27:@16.6] assign _GEN_1 = 2'h1 == _T_63 ? _regFileVec_T_63 : regFileVec_1; // @[cmd18.sc 15:27:@16.6] assign _GEN_2 = 2'h2 == _T_63 ? _regFileVec_T_63 : regFileVec_2; // @[cmd18.sc 15:27:@16.6] assign _GEN_3 = 2'h3 == _T_63 ? _regFileVec_T_63 : regFileVec_3; // @[cmd18.sc 15:27:@16.6] assign _GEN_4 = io_wren ? _GEN_0 : regFileVec_0; // @[cmd18.sc 14:18:@14.4] assign _GEN_5 = io_wren ? _GEN_1 : regFileVec_1; // @[cmd18.sc 14:18:@14.4] assign _GEN_6 = io_wren ? _GEN_2 : regFileVec_2; // @[cmd18.sc 14:18:@14.4] assign _GEN_7 = io_wren ? _GEN_3 : regFileVec_3; // @[cmd18.sc 14:18:@14.4] assign _T_67 = io_rdaddr_0[1:0]; // @[:@18.4] assign _GEN_9 = 2'h1 == _T_67 ? regFileVec_1 : regFileVec_0; // @[cmd18.sc 18:15:@19.4] assign _GEN_10 = 2'h2 == _T_67 ? regFileVec_2 : _GEN_9; // @[cmd18.sc 18:15:@19.4] assign _GEN_11 = 2'h3 == _T_67 ? regFileVec_3 : _GEN_10; // @[cmd18.sc 18:15:@19.4] assign _T_71 = io_rdaddr_1[1:0]; // @[:@20.4] assign _GEN_13 = 2'h1 == _T_71 ? regFileVec_1 : regFileVec_0; // @[cmd18.sc 19:15:@21.4] assign _GEN_14 = 2'h2 == _T_71 ? regFileVec_2 : _GEN_13; // @[cmd18.sc 19:15:@21.4] assign _GEN_15 = 2'h3 == _T_71 ? regFileVec_3 : _GEN_14; // @[cmd18.sc 19:15:@21.4] assign io_rddata_0 = {{31'd0}, _GEN_11}; assign io_rddata_1 = {{31'd0}, _GEN_15}; always @(posedge clock) begin if (reset) begin regFileVec_0 <= 1'h0; end else begin if (io_wren) begin if (2'h0 == _T_63) begin regFileVec_0 <= _regFileVec_T_63; end end end if (reset) begin regFileVec_1 <= 1'h0; end else begin if (io_wren) begin if (2'h1 == _T_63) begin regFileVec_1 <= _regFileVec_T_63; end end end if (reset) begin regFileVec_2 <= 1'h0; end else begin if (io_wren) begin if (2'h2 == _T_63) begin regFileVec_2 <= _regFileVec_T_63; end end end if (reset) begin regFileVec_3 <= 1'h0; end else begin if (io_wren) begin if (2'h3 == _T_63) begin regFileVec_3 <= _regFileVec_T_63; end end end end endmodule
Chiselの記述によってRTLに変換した際に凄まじいif
文のネストを含むRTLが生成されることがありますので、変換したRTLはチェックすることをお勧めします(前にRISC-VのレジスタファイルをVec
で生成したら32個のレジスタ分だけif
文のネストが出来たりしました)。
上記の例で見た様にChiselのVec
型は同じ型のIOポートを纏めるためにも使用できます。
これを使うことでマルチプレクサやアービターのような回路をすっきりさせることが出来るようになります。
ここの例ではまだ適用していませんが、次に紹介予定のBundle
と組み合わせることで更に柔軟な表現が可能となり、Chiselで実装をするメリットが大きくなってくると個人的には思っています。
ということでChiselのaggregate型の一つVec
の解説でした。
次回はもうひとつのaggregate型であるBundle
について紹介していく予定です。
第7回に続く。