前回の記事ではChisel BootcampはChiselのコレクション型であるVec
について調べた。
今日はModule3.2の締めくくりとして練習問題を見ていく。
Module 3.2: ジェネレータ:コレクション
Chiselのコレクション型の使用例
ここではChiselのVec
を使った練習問題に取り組んでいく。
練習問題: 32-bit RISC-Vプロセッサ
練習問題なので、これまでどおりこの部分は丸々引用。
レジスタファイルはプロセッサを作る上で重要なブロックだ。これはレジスタの配列でリード/ライトポートを使って読み書きが出来る。これらのポートはアドレスとデータフィールドで構成される。
RISC-VのISAは複数の構成を持つが、その中で最もシンプルなのはRV32Iである。RV32Iは32本の32-bitレジスタ配列を持つ。レジスタ配列のインデックス0番はライトをしたとしてもリードをすると常に"0"が読み出される。 (これは0を得るのに便利である)
RV32I向けに単一のライトポートとパラメタライズされたリードポートを備えたレジスタファイルを実装してみよう。
ライトは
wen
(write enable)がアサートされた場合にのみ有効となる。
以下が練習問題用の提供されているレジスタファイルのスケルトンになる。
class RegisterFile(readPorts: Int) extends Module { require(readPorts >= 0) val io = IO(new Bundle { val wen = Input(Bool()) val waddr = Input(UInt(5.W)) val wdata = Input(UInt(32.W)) val raddr = Input(Vec(readPorts, UInt(5.W))) val rdata = Output(Vec(readPorts, UInt(32.W))) }) // A Register of a vector of UInts // fromBits(0.U) is a bit of a hack to have reg reset to zero val reg = RegInit( Vec(32, UInt(32.W)).fromBits(0.U) ) }
テスト用のコード
chisel3.iotesters.Driver(() => new RegisterFile(2) ) { c => new PeekPokeTester(c) { def readExpect(addr: Int, value: Int, port: Int = 0): Unit = { poke(c.io.raddr(port), addr) expect(c.io.rdata(port), value) } def write(addr: Int, value: Int): Unit = { poke(c.io.wen, 1) poke(c.io.wdata, value) poke(c.io.waddr, addr) step(1) poke(c.io.wen, 0) } // everything should be 0 on init for (i <- 0 until 32) { readExpect(i, 0, port = 0) readExpect(i, 0, port = 1) } // write 5 * addr + 3 for (i <- 0 until 32) { write(i, 5 * i + 3) } // check that the writes worked for (i <- 0 until 32) { readExpect(i, if (i == 0) 0 else 5 * i + 3, port = i % 2) } }}
なんか色々それっぽいテストコードになってきてるな。特に難しいことは無いけど、このまんまだと書くことが殆ど無いので、少しだけ解説。
テストの構成
ざっくりと以下の3ブロックで構成されている
readExpect
: テスト用のリード関数と期待値比較write
: 実装したレジスタファイルへの書き込み関数- テスト本体
readExpect
ごくシンプルなリードタスクで、アドレスを指定するとレジスタファイルの指定したアドレスの値が同じサイクルで返却される。読みだすリードポートはデフォルトでは0番になるので、他のポートを使用する場合には第3引数に所望のぽーとしていを行う。読み出された値と引数のvalue
をexpect
で比較してOK/NGを判定する。
system verilogでタスク書くと以下のイメージになる。
task readExpect(input [4:0] addr, input int value, input int port = 0); io_raddr[port] = addr; if (io_rdata[port] != value) begin $error("read data(%x) != value(%x)", io_rdata[port], value); end endtask // readExpect
write
こちらもごくシンプルなライトタスク。実行するとwen
に1が入力されてaddr
で指定されるレジスタにvalue
の値が反映される。実行後にstep(1)
を呼び出すことによって、シミュレーション上の時間を1cycle進め、wen
を0に落として処理が完了となる。
system verilogでのタスクは以下の感じ。
task write(input [4:0] addr, input int value); io_wen = 1'b1; io_waddr = addr; io_wdata = value; @(posedge clk); io_wen = 1'b0; endtask // write
実装するレジスタファイルの仕様について
問題文及び、テストコードから実装するレジスタファイルの仕様を箇条書きすると以下の様になる。
仕様をまとめると以下のようになるだろうか。
- レジスタファイルのレジスタ本数は32
- 各レジスタのビット幅は32-bit
- レジスタ0はライトを行っても、読み出し値は0となる
- ライト用のポートは1つ
- ライトは
wen
を1にした際のアドレスのレジスタにwdata
が反映される
- ライトは
- リード用のポートはパラメタライズ可能
- リードは各ポートに与えたアドレスの値が同じサイクルで読み出せる
レジスタはすでに これを実際に実行してみると、以下のようにテストにパスすることが確認できる。 ちなみBootcampの解答では、32本のレジスタにとりあえず値を書いて、読み出し値を0にするという実装を行っていた。こちらのほうが問題文には忠実な気もする。 このレジスタファイルを回路をRTLにすると、結構な分量のコードになった。 長いのでここに貼るのはやめてgistにあげてあるので興味があればご覧になってほしい。 ここまでを踏まえて実装した自分の解答 - クリックすると開くので、見たくない場合は開かないように注意。
class RegisterFile(readPorts: Int) extends Module {
require(readPorts >= 0)
val io = IO(new Bundle {
val wen = Input(Bool())
val waddr = Input(UInt(5.W))
val wdata = Input(UInt(32.W))
val raddr = Input(Vec(readPorts, UInt(5.W)))
val rdata = Output(Vec(readPorts, UInt(32.W)))
})
// A Register of a vector of UInts
// fromBits(0.U) is a bit of a hack to have reg reset to zero
val reg = RegInit( Vec(32, UInt(32.W)).fromBits(0.U) )
// write
when(io.wen) {
when (io.waddr != 0.U) {
reg(io.waddr) := io.wdata
}
}
// output
for (i <- 0 until readPorts) {
io.rdata(i) := reg(io.raddr(i))
}
}
Vec
で確保されているので、後はライトとリードに対してどうするかという部分を実装すればOK。[info] [0.000] Elaborating design...
[deprecated] cmd5.sc:13 (1 calls): fromBits is deprecated: "fromBits is deprecated, use asTypeOf instead"
[deprecated] cmd5.sc:17 (1 calls): $bang$eq is deprecated: "Use \'=/=\', which avoids potential precedence problems"
[warn] There were 2 deprecated function(s) used. These may stop compiling in a future release, you are encouraged to fix these issues.
[warn] Line numbers for deprecations reported by Chisel may be inaccurate, enable scalac compiler deprecation warnings by either:
[warn] In the sbt interactive console, enter:
[warn] set scalacOptions in ThisBuild ++= Seq("-unchecked", "-deprecation")
[warn] or, in your build.sbt, add the line:
[warn] scalacOptions := Seq("-unchecked", "-deprecation")
[info] [0.089] Done elaborating.
Total FIRRTL Compile Time: 93.7 ms
Total FIRRTL Compile Time: 59.6 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1543720587405
test cmd5HelperRegisterFile Success: 96 tests passed in 37 cycles taking 0.099074 seconds
[info] [0.088] RAN 32 CYCLES PASSED
レジスタファイルVerilog版
Chisel版をRTLした場合Vec
がそのまま全展開されており、それがコードが長くなった原因ではある。
だが、Verilogだとメモリ配列使っても書いてもChiselのコードよりはどうしても長くなってしまう。
module regfile ( input clk ,input rst_n ,input wen ,input [4:0] waddr ,input [31:0] wdata ,input [4:0] raddr_0 ,input [4:0] raddr_1 ,input [31:0] rdata_0 ,input [31:0] rdata_1 ); reg [31:0] regs[0:31]; assign rdata_0 = regs[raddr_0]; assign rdata_1 = regs[raddr_1]; generate genvar i; for (i = 0; i < 32; i++) begin : GEN_REGFILE always@(posedge clk or negedge rst_n) begin if (!rst_n) begin regs[i] <= {32{1'b0}}; end else if (wen) begin if ((waddr == i) && (waddr !={5{1'b0}})) begin regs[i] <= wdata; end end end end endgenerate endmodule // reg_file
やっぱりalways
文やらリセット周りの部分があるからこればっかりは仕方ないか。それにポートのビット幅ならともかく、ポートの端子自体をパラメタライズって出来る気がしないし。
とりあえず上記verilog版はシミュレーション環境含めて、以下のgithubにプロジェクト一式を上げておいた。
これでModule3.2も終了。ようやく少しChiselが馴染んできた。でも次もModule3.2になってるな。。。Module3.2とModule3.3の幕間になるようで、Chiselに組み込まれている標準ライブラリの紹介を見ていく。