RISCVのM拡張を実装した

2024/02/23に公開

はじめに

これまで書籍(RISCVとChiselで学ぶ はじめてのCPU自作)のコードを写経してRISCVの命令セットを実行できるようになりました。
同書の写経も一通り終わったので自分で未実装の機能を追加していくことにしました。

ということで、まずは標準M拡張(掛け算割り算)を実装することにしました。

実装内容

仕様

仕様は以下の7章を参考にしました。
https://drive.google.com/file/d/1s0lZxUZaa7eV_O0_WsZzaurFLLww7ou5/view?pli=1

対応内容概要

以下を対応しました。

  • 命令セットの追加
  • デコード処理の追加
  • 演算処理の追加
  • ライトバック処理の追加

演算処理の追加

chiselでは基本的にはscalaの演算子を用いて一発で乗算、除算を計算できます。
乗算、除算はそれぞれ独立した回路として実装されることが一般的らしいので(by「プロセッサを支える技術」)、それぞれ独立したモジュールとして作成しました。

Multiplyer.scala
class Multiplyer extends Module{
  val io = IO(
    new Bundle{
      val exe_fun = Input(UInt(WORD_LEN.W))
      val op1_data = Input(UInt(WORD_LEN.W))
      val op2_data = Input(UInt(WORD_LEN.W))
      val calced = Output(UInt(WORD_LEN.W))
    }
  )
...
}
Divider.scala
class Divider extends Module{
  val io = IO(
    new Bundle{
      val exe_fun = Input(UInt(WORD_LEN.W))
      val op1_data = Input(UInt(WORD_LEN.W))
      val op2_data = Input(UInt(WORD_LEN.W))
      val calced = Output(UInt(WORD_LEN.W))
    }
  )
  ...
}
Core.scala
class Core extends Module{
  ...

  //mul
  val multiplyer = Module(new Multiplyer())
  multiplyer.io.exe_fun := exe_fun
  multiplyer.io.op1_data := op1_data
  multiplyer.io.op2_data := op2_data
  mul_out := multiplyer.io.calced

  //div
  val divider = Module(new Divider())
  divider.io.exe_fun := exe_fun
  divider.io.op1_data := op1_data
  divider.io.op2_data := op2_data
  div_out := divider.io.calced
}

乗算

乗算で気を付けなくていけないのは、ビット幅です。32bit*32bitは64bitになり、MULは下位32bitを、MULHは上位32bitを返す仕様となっています。
そこで、一度両オペランドの値を64bitにキャストしてから所望の32bit分を抜き出すという処理にしました。

また、各オペランドがUInt型、SInt型の場合がそれぞれ異なる演算指令として定義されている(MULH, MULHU, MULHSU)ので、入力オペランド(UInt型として定義)をSInt型にキャストした変数を用意し、演算指令に応じて計算に使用する値を切替えます。

Multiplyer.scala
  ...
    var op1_64_U = UInt(DWORD_LEN.W); //DWORD_LEN=64
    op1_64_U = io.op1_data;
    var op2_64_U = UInt(DWORD_LEN.W);
    op2_64_U = io.op2_data;

    val op1_64_S = op1_64_U.asSInt;
    val op2_64_S = op2_64_U.asSInt;

    io.calced := MuxCase(0.U(WORD_LEN.W), Seq(
      (io.exe_fun === ALU_MUL) -> (op1_64_U * op2_64_U)(31, 0).asUInt,
      (io.exe_fun === ALU_MULH) -> ((op1_64_S * op2_64_S)>>32.U(WORD_LEN.W)).asUInt,  //WORD_LEN=32
      (io.exe_fun === ALU_MULHSU) -> ((op1_64_S * op2_64_U)>>32.U(WORD_LEN.W)).asUInt,
      (io.exe_fun === ALU_MULHU) -> ((op1_64_U * op2_64_U)>>32.U(WORD_LEN.W)).asUInt,   
    ))

除算

除算の場合は0割に注意しないといけません。この場合の仕様も定められています。
The quotient of division by zero has all bits set, and the remainder of division by zero equals the dividend
(商は全ビット1、余りは割られる数と同じ)

ということで割る数が0か否かで場合分けします。

Divider.scala

  val op1_data_s = io.op1_data.asSInt;
  val op2_data_s = io.op2_data.asSInt;

  io.calced := MuxCase(0.U(WORD_LEN.W), Seq(
    (io.exe_fun === ALU_DIV) -> (devide_s(op1_data_s, op2_data_s)),
    (io.exe_fun === ALU_REM) -> (rem_s(op1_data_s, op2_data_s)),
  ))

  def devide_s(op1:chisel3.SInt, op2:chisel3.SInt) : chisel3.UInt = {
    
    val retS = Mux(op2 === 0.S(WORD_LEN.W), 0xFFFFFFFF.S(WORD_LEN.W), (op1/op2));
    return retS.asUInt
  }

  def rem_s(op1:chisel3.SInt, op2:chisel3.SInt) : chisel3.UInt = {
    
    val retS = Mux(op2 === 0.S(WORD_LEN.W), op1, (op1 % op2));
    return retS.asUInt
  }

DIVU、REMUも同様です。

試験

乗算

以下のような試験用Cコードを作成しました。

mul.c
#include <stdio.h>

int main(){

  asm volatile("li a0, 1");
  asm volatile("li a1, 1");
  asm volatile("slli a0, a0, 17");
  asm volatile("slli a1, a1, 20");
  asm volatile("addi a0, a0, 7");
  asm volatile("addi a1, a1, 3");
  asm volatile("mul a2, a1, a0");
  
  asm volatile("unimp");  //これが試験の終わりとテストコードで定義している

  return 0;
}

やっているのは0x20007 * 0x100003です。
下位32bitだけ取り出せていることの確認のために結果が32bit以上になるようにしています。

dumpされたアセンブリは以下のようになります(asm volitileだからほぼそのまま)

...
00000000 <main>:
   0:	00100513          	li	a0,1
   4:	00100593          	li	a1,1
   8:	01151513          	slli	a0,a0,0x11
   c:	01459593          	slli	a1,a1,0x14
  10:	00750513          	addi	a0,a0,7
  14:	00358593          	addi	a1,a1,3
  18:	02a58633          	mul	a2,a1,a0
  1c:	c0001073          	unimp
  20:	00000513          	li	a0,0
  24:	00008067          	ret
...

このCコードをhex化してメモリに読み込ませ、クロックを進めるだけのテストコードを実行します。
Core.scalaで各変数をprintfで出力し、正しい値が変数に入っているのか確認します。
以下が結果です。

pcがプログラムカウンタ、rs1_data、rs2_data、wb_dataが'mul wb_data,rs1_data,rs2_data'の各オペランド値です。
上記のdumpファイルと比較すると、pc=0x18で正しく乗算が行われている(下位32bitが抽出できている)ことが分かります。

同様にmulhも確認します。
試験用Cコードは以下です。

#include <stdio.h>

int main(){

  asm volatile("li a0, 1");
  asm volatile("li a1, 1");
  asm volatile("slli a0, a0, 17");
  asm volatile("slli a1, a1, 20");
  asm volatile("mulhu a2, a1, a0");
  
  asm volatile("unimp");

  return 0;
}

0x20007 * 0x100003を計算します。
dumpは以下のようになります。

00000000 <main>:
   0:	00100513          	li	a0,1
   4:	00100593          	li	a1,1
   8:	01151513          	slli	a0,a0,0x11
   c:	01459593          	slli	a1,a1,0x14
  10:	02a5b633          	mulhu	a2,a1,a0
  14:	c0001073          	unimp
  18:	00000513          	li	a0,0
  1c:	00008067          	ret

実行結果は以下のようになりました。

pc = 0x10にて乗算結果の上位32bitが抽出されているのが分かります。

除算

同様の手法で確認します。
まずはMUL

試験用Cコード

int main(){

  asm volatile("li a0, 3");
  asm volatile("li a1, 7");
  asm volatile("divu a2, a1, a0");
  
  asm volatile("unimp");

  return 0;
}

dumpファイル

   0:	00300513          	li	a0,3
   4:	00700593          	li	a1,7
   8:	02a5d633          	divu	a2,a1,a0
   c:	c0001073          	unimp
  10:	00000513          	li	a0,0
  14:	00008067          	ret

実行結果

7/3 = 2が計算できています。

次はREM
Cコード

int main(){

  asm volatile("li a0, 3");
  asm volatile("li a1, 7");
  asm volatile("rem a2, a1, a0");
  
  asm volatile("unimp");

  return 0;
}

dump

00000000 <main>:
   0:	00300513          	li	a0,3
   4:	00700593          	li	a1,7
   8:	02a5e633          	rem	a2,a1,a0
   c:	c0001073          	unimp
  10:	00000513          	li	a0,0
  14:	00008067          	ret

実行結果

7%3 = 1が計算できています。

0割の場合も試験します。
DIVの結果

REMの結果

7 ÷ 3 = 0xFFFFFFFF 余り 7
が計算できています。

最後に

コードは以下に挙げてあります。
https://github.com/mr16048/mycpu/tree/M-extension

演算自体は簡単にできた(ハード的に実装するのは大変ですが)のですが、低レイヤなだけあってビット数や型を意識しないといけないのが思ったより苦労しました。

なお、同じ割り算に対してdivとremを連続して書けば内部で結合して1回の演算で商と余りを両方求める、という仕様もあるようですが、これは未実装です。
まずは複数の演算をまとめる、という部分の仕組みをつくるところからです、、、

参考文献

書籍
RISC-VとChiselで学ぶ はじめてのCPU自作
プロセッサを支える技術

webサイト
RISCV仕様
Chisel Data Types

細かいハマりポイントなどは自ブログで解説しています
[Chisel]整数型のキャスト
[scala/chisel] value >> is not a member of (chisel3.Bool, chisel3.UInt)
[Chisel]条件分岐について

Discussion