lynxeyedの電音鍵盤

MBDとFPGAと車載で使うデバイスの備忘録

Chiselを使ったRISC-Vの勉強(3. ALUの実装とIDステージの改良)

githubを巡回してみていろいろな実装を見て作戦を練っていました。
これまでRISCVの命令セットを見る限り、opcode(7bit) + funct3(3bit)で判別して適当に実装できるかなと考えていました。(10bitなら単純計算で1023命令は用意できるし)

  1. ALUへの演算命令、2入力への変数指定、結果の格納先(汎用レジスタ、pc、データメモリなど)指定
  2. データメモリ書き込みイネーブル、アドレス指定、読み込みイネーブル、アドレス指定
  3. CSR周りのゴニョゴニョ
  4. CPUストール制御信号
  5. 分岐制御信号

をIDステージで書けばいいかなと書き始めていました。

マイクロコードの記述方法の調査

IDステージの実装方法を調べているといろいろな実装が見つかります。opcodeから実直にimm,rs1,rs2,funct3を分けて実装しているのが、diningyo氏 (id:diningyo-kpuku-jougeki) のdirv
https://github.com/diningyo/dirv/blob/master/src/main/scala/dirv/pipeline/Idu.scala
とっても簡潔に記述されています。
氏のdirvはステージごとに綺麗に分かれていてパイプラインを組むときにとても作業がしやすそうです。実際、dirvトップモジュールはそれぞれのステージをConnectするだけの記述になっています。

しかし、いろんなrisc-v実装を見てると、マイクロコードはほとんどの実装で示し合わせたようにこんな実装(全く全てが同じではないですが、雰囲気としては同じ)をしてました。

class IDecode  {
    val table: Array[(BitPat, List[BitPat])] = Array(
    LW      ->  List(Y, BR_N  , OP1_RS1, OP2_IMI , ALU_ADD ,  WB_MEM, REN_1, MEN_1, M_XRD, MT_W) ,
    LB      ->  List(Y, BR_N  , OP1_RS1, OP2_IMI , ALU_ADD ,  WB_MEM, REN_1, MEN_1, M_XRD, MT_B ),
    LBU     ->  List(Y, BR_N  , OP1_RS1, OP2_IMI , ALU_ADD ,  WB_MEM, REN_1, MEN_1, M_XRD, MT_BU ),
    LH      ->  List(Y, BR_N  , OP1_RS1, OP2_IMI , ALU_ADD ,  WB_MEM, REN_1, MEN_1, M_XRD, MT_H ),
    LHU     ->  List(Y, BR_N  , OP1_RS1, OP2_IMI , ALU_ADD ,  WB_MEM, REN_1, MEN_1, M_XRD, MT_HU ),
……
)}

分岐制御、ALUの入力指定、IMMの種類、ALUでの演算指定、メモリ書き戻し、レジスタ書き戻し、などの指定がListで格納されています。
本家Rocket-chipなどがこの実装だから、なのはわかってたのですが、いろいろ調べてました。

lowRISCというプロジェクトがあり、メモリなどのカスタマイズができるようです。その際にこの体裁で拡張やカスタマイズを進めていくようなのでこの手順を真似しておいた方が今後新たにSoCの設計するときに有利だと思いました。
参考:Adding HW/SW support for the load and store tag instructions · lowRISC: Collaborative open silicon engineering

命令デコード(ID)ステージの実装

自分が参考するにあたって、上記のような手法でIDステージを記述しており、かつChisel3.2以降*1に対応したものはないか、さがしていました。

これが良さげ。
GitHub - ucb-bar/riscv-mini: Simple RISC-V 3-stage Pipeline in Chisel

Chisel3で記述されていて、かなりシンプルでした。これを参考に組み立てていきます。
方針としては、

  • RV32Iに対応しているアセンブラのIDステージのみ移植し、いま自作しているプロジェクトにフィットするよう書き換える。
  • ALUは32bit固定にして、わかりやすく記述。今後対応しきれないようであれば都度書き換え。

という感じで行くことににしました。

ALUの実装はこんな感じです。へんなところで頑張らない。足し算引き算、(排他的)論理和論理積、シフトくらい。
ざっと見てVerilogコードがなんとなくでも想像できる形。(一部抜粋)

class ALU extends Module {
    val io = IO (new Bundle 
    {
        val op1     = Input(UInt(32.W))
        val op2     = Input(UInt(32.W))
        val alu_op  = Input(UInt(4.W))
        val out     = Output(UInt(32.W))
    })
    val shamt = io.op2(4,0).asUInt
    val w_out = Wire(UInt(32.W))

    w_out := MuxLookup(io.alu_op, io.op2, Seq(
      ALU_ADD  -> (io.op1 + io.op2),
      ALU_SUB  -> (io.op1 - io.op2),
      ALU_SRA  -> (io.op1.asSInt >> shamt).asUInt,
      ALU_SRL  -> (io.op1 >> shamt),
      ALU_SLL  -> (io.op1 << shamt),
      ALU_SLT  -> (io.op1.asSInt < io.op2.asSInt),
      ALU_SLTU -> (io.op1 < io.op2),
      ALU_AND  -> (io.op1 & io.op2),
      ALU_OR   -> (io.op1 | io.op2),
      ALU_XOR  -> (io.op1 ^ io.op2),
      ALU_COPY1 -> io.op1))
    
    io.out := w_out
}

30行足らず。わーい。
このALUをCPU側のモジュールにインスタンシエートし、入力出力をIDステージのフラグから判別してやればOK
ALUの入力セレクタは今のところこんな実装。I-type命令じゃない即値immなどは真面目に実装していないのでこれから頑張る。

 // ALU OP1 selector
    val ex_op1 = MuxLookup(id_ctrl.alu_op1, 0.U(32.W),
        Seq(
            OP1_RS1 -> rv32i_reg(idm.io.inst.rs1),
            OP1_IMU -> 0.U(32.W),   // DUMMY 
            OP1_IMZ -> 0.U(32.W)    // DUMMY
        )
    )
    // ALU OP2 selector
    val ex_op2 = MuxLookup(id_ctrl.alu_op2, 0.U(32.W),
        Seq(
            OP2_RS2 -> rv32i_reg(idm.io.inst.rs2),
            OP2_IMI -> idm.io.inst.imm,   
            OP2_IMS -> 0.U(32.W)    // DUMMY
        )
    )


// ALUの入力にConnectし、ALUのオペレーションを4bit長で指示
import ALU._
    val alu = Module(new ALU)
    alu.io.alu_op := id_ctrl.alu_func
    alu.io.op1 := ex_op1
    alu.io.op2 := ex_op2

ALUの出力先はpcとかデータメモリとか色々あるはずですが、今回はまだ汎用レジスタだけ実装しています。

when (rf_wen === REN_1){ // 汎用レジスタ書き込みイネーブルであれば
        when (rd_addr =/= 0.U){
            rv32i_reg(rd_addr) := alu.io.out // 汎用レジスタに書き込み
        }.otherwise { // rd_addr = 0
            rv32i_reg(0.U) := 0.U(32.W) // ゼロレジスタが指定されていたら、強制的にゼロを書き込む(nop命令などの用途)
        }  
    }

ALUを使い、かつ汎用レジスタのみで読み書きが完結する命令はI,およびR-typeの命令です。


f:id:Lynx-EyED:20200428205047p:plain
その中から3タイプに分けました。

  • rd,rs1,imm(12bit)を使う命令
  • rd,rs1,imm(5bit)を使う命令
  • rd,rs1,rs2を使う命令

それぞれから1命令づつピックアップしてハンドアセンブルして、結果が期待された値となっているか確認。

  • addi
  • slli
  • add

をピックアップ。
下記のように、バイナリを記述して、汎用レジスタの結果を見ます。

(中略)
0x00A00593L, // addi x11,x0,10 (x11 = 0 + 10 = 10)
0x00259093L, // slli x1,x11,2 (x1 = x11 << 2 = 40)
0x00B00613L, // addi x12,x0,11 (x12 = 11)
0x00C581B3L, // add x3,x11,x12 (x3 = x11 + x12 = 10 + 11 = 21 = 0x15)

期待される結果は、
x1 = 40(= 0x28)
x3 = 21(= 0x15)
x11 = 10(= 0x0A)
x12 = 11( = 0x0B)
です。
結果:


f:id:Lynx-EyED:20200428205023p:plain

コード全文

コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.9.1 --depth 1 

*1:初期の頃に記述されたものはChisel2以前のバージョンで記述しているサンプルも多いため