RISCVのM拡張を実装した
はじめに
これまで書籍(RISCVとChiselで学ぶ はじめてのCPU自作)のコードを写経してRISCVの命令セットを実行できるようになりました。
同書の写経も一通り終わったので自分で未実装の機能を追加していくことにしました。
ということで、まずは標準M拡張(掛け算割り算)を実装することにしました。
実装内容
仕様
仕様は以下の7章を参考にしました。
対応内容概要
以下を対応しました。
- 命令セットの追加
- デコード処理の追加
- 演算処理の追加
- ライトバック処理の追加
演算処理の追加
chiselでは基本的にはscalaの演算子を用いて一発で乗算、除算を計算できます。
乗算、除算はそれぞれ独立した回路として実装されることが一般的らしいので(by「プロセッサを支える技術」)、それぞれ独立したモジュールとして作成しました。
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))
}
)
...
}
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))
}
)
...
}
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型にキャストした変数を用意し、演算指令に応じて計算に使用する値を切替えます。
...
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か否かで場合分けします。
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コードを作成しました。
#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
が計算できています。
最後に
コードは以下に挙げてあります。
演算自体は簡単にできた(ハード的に実装するのは大変ですが)のですが、低レイヤなだけあってビット数や型を意識しないといけないのが思ったより苦労しました。
なお、同じ割り算に対して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