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

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

Chisel Bootcamp - Module3.2(6) - 練習問題:Vecを使ったRISC-Vのレジスタファイル

スポンサーリンク

前回の記事ではChisel BootcampはChiselのコレクション型であるVecについて調べた。

www.tech-diningyo.info

今日は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引数に所望のぽーとしていを行う。読み出された値と引数のvalueexpectで比較して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が反映される
  • リード用のポートはパラメタライズ可能
    • リードは各ポートに与えたアドレスの値が同じサイクルで読み出せる

ここまでを踏まえて実装した自分の解答 - クリックすると開くので、見たくない場合は開かないように注意。

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

ちなみBootcampの解答では、32本のレジスタにとりあえず値を書いて、読み出し値を0にするという実装を行っていた。こちらのほうが問題文には忠実な気もする。

このレジスタファイルを回路をRTLにすると、結構な分量のコードになった。

長いのでここに貼るのはやめてgistにあげてあるので興味があればご覧になってほしい。

Module3.2練習問題のレジスタファイルのRTL

レジスタファイル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にプロジェクト一式を上げておいた。

github.com

これでModule3.2も終了。ようやく少しChiselが馴染んできた。でも次もModule3.2になってるな。。。Module3.2とModule3.3の幕間になるようで、Chiselに組み込まれている標準ライブラリの紹介を見ていく。