|
|
RISC-V ISA の解説
|
|
|
============
|
|
|
|
|
|
ここでは、RISC-V(リスク ファイブ)という ISA (命令セット・アーキテクチャ)の解説を行います。
|
|
|
|
|
|
RISC-V は 2010年にカルフォルニア大学バークレイ校で開発が開始された、オープンな Reduced Instruction Set Computer (RISC) ISA です。
|
|
|
|
|
|
RISC という用語は 1980年ごろに登場しましたが、それ以降に得られた知見を取り入れて作られた、最先端の ISA が RISC-V です。
|
|
|
|
|
|
## RISC-V ISA の仕様書
|
|
|
|
|
|
[RISC-V Specifications](https://riscv.org/specifications/)
|
|
|
|
|
|
のページを見れば RISC-V のすべての仕様がわかります。
|
|
|
|
|
|
## 簡易版仕様書
|
|
|
|
|
|
[pexRISC-V_ISA.pdf](uploads/05b7acb606fc467a59c2aa7b06851b3c/pexRISC-V_ISA.pdf)
|
|
|
|
|
|
今回のプロセッサ実験では RISC-V のすべてを実装する必要はなく、上記pdfファイルに記載した命令のみを実装すれば必要十分です。
|
|
|
|
|
|
|
|
|
## 各命令ごとの解説
|
|
|
|
|
|
ちょっと紛らわしいですが、冗長になるのを防ぐため、rs1番のレジスタの値、という意味で`rs1`などと書くことにします。
|
|
|
|
|
|
### ADDI
|
|
|
|
|
|
rs1番のレジスタの値を読み出し、その値に即値で指定された整数を加算し、rd番のレジスタに書き込む命令です。
|
|
|
|
|
|
即値は符号拡張されます。即値は12bitしかないため、32bitの値として解釈する際、足りない上位ビットの部分を埋める必要がありますが、即値の最上位ビットと同じもので埋めるのが符号拡張です。
|
|
|
|
|
|
例えば、即値の部分が`111000111000`であれば、`11111111111111111111111000111000`に拡張されます。
|
|
|
|
|
|
これは二の補数で表された、負の数になっています。つまり、即値に負の数を指定することができます。
|
|
|
|
|
|
このため、SUBI 命令のようなものは存在しません。
|
|
|
|
|
|
rs1として、0を指定した場合、「零レジスタを読み出し、その値に即値で指定された整数を加算し、rd番のレジスタに書き込む」という動作になります。零レジスタを読み出した場合常に0が得られますから、これは実質的に即値をrd番のレジスタに書き込むという命令であるということになります。
|
|
|
|
|
|
また、即値が0である場合、「rs1番のレジスタの値を読み出し、その値に0を加算し、rd番のレジスタに書き込む」という動作になります。0を加算しても値は変わりませんから、これは実質的にrs1番のレジスタの値をrd番のレジスタにコピーする、という命令であるということになります。
|
|
|
|
|
|
### ADD, SUB
|
|
|
|
|
|
rs1番のレジスタの値とrs2番のレジスタの値を読み出し、その値同士の和・差を計算し、rd番のレジスタに書き込む命令です。
|
|
|
|
|
|
和の計算はrs1とrs2をどちらにおいても変わらないですが、差の計算方法は`rd = rs1 - rs2`です。
|
|
|
|
|
|
### XORI, ORI, ANDI
|
|
|
|
|
|
rs1番のレジスタの値を読み出し、その値に即値で指定された整数とビットごとに論理演算をし、rd番のレジスタに書き込む命令です。
|
|
|
|
|
|
即値はやはり符号拡張されるという点に注意する必要があります(MIPS などでは、論理演算の場合、即値は零拡張されることになっていますが、RISC-V では符号拡張です)。
|
|
|
|
|
|
符号拡張されることを活用すると、ビットごとに反転させる`NOT`命令を、`XORI x9, x8, -1`のように書くことができます(確かめてみましょう。ヒント:-1を二の補数で表すとどのような表現になる?)。
|
|
|
|
|
|
### XOR, OR, AND
|
|
|
|
|
|
rs1番のレジスタの値とrs2番のレジスタの値を読み出し、その値同士の排他的論理和・論理和・論理積を計算し、rd番のレジスタに書き込む命令です。演算はビットごとに行われます。
|
|
|
|
|
|
特に説明することはないでしょう。
|
|
|
|
|
|
### SLT, SLTI, SLTU, SLTIU
|
|
|
|
|
|
`SLT`は Set if Less Than の略です。rs1番のレジスタの値とrs2番のレジスタの値を読み出し、それらを二の補数表現された符号付き整数だと解釈し、`rs1<rs2`であれば1を、そうでなければ0を、rd番のレジスタに書き込む命令です。
|
|
|
|
|
|
`SLTI`の場合は、rs2番のレジスタの値の代わりに即値で指定した値が使われる点のみが異なります。
|
|
|
|
|
|
`SLTU`の場合、「それらを二の補数表現された符号付き整数だと解釈し、」の部分が「それらを二進数で表現された符号なし整数だと解釈し、」に変更される点のみが異なります。
|
|
|
|
|
|
`SLTIU`の場合は、それら両方の複合です。
|
|
|
|
|
|
`SLTIU`であっても即値は符号拡張される点に注意してください。つまり、`SLTIU x10, x9, 4294967290`のような、即値が非常に大きく $`2^{32}`$ に近い値である命令であると解釈することになります。
|
|
|
|
|
|
|
|
|
これらの中には、`<=`での比較や、`>`、`>=`での比較が含まれていないように思えるかもしれません。
|
|
|
|
|
|
しかし、`>`での比較は、レジスタオペランドの順番を変えることで実現が可能です。
|
|
|
|
|
|
また、`<=`や`>=`での比較は、`>`や`<`での比較結果の0と1を入れ替える(たとえば、1と`XORI`すればできます)ことで実現可能です。
|
|
|
|
|
|
即値との比較の場合はオペランドの順番を変えることができないので不可能に思えるかもしれませんが、即値を変更すれば同じ意味になるような命令を作ることができます。
|
|
|
|
|
|
例えば、`rd = (rs1 <= 5);`を実現したい場合、`rd = (rs1 < 6);`であると考え直すことで表現可能です。あるいは、`rd = (rs1 > 5);`を表現したい場合、`rd = 1 xor (rs <= 5);`であると考え直すことができるので、やはり`rd = 1 xor (rs < 6);`のように表現可能になります。
|
|
|
|
|
|
その他、`rd = (rs1 == 0);`のようなものを実現したい場合、一見関係がなさそうに思えますが`SLTIU`命令を使えば実現可能です。符号なし整数として1より小さい数は0のみであるため、1と比較することで目的を達成できます。
|
|
|
|
|
|
### SLL, SRL, SRA, SLLI, SRLI, SRAI
|
|
|
|
|
|
それぞれ、Shift Left Logical、Shift Right Logical、Shift Right Arithmeticの略であり、末尾にIがつくものはシフトする量を指定するものが、レジスタオペランドではなく即値になった命令です。rs1番のレジスタを読み出した値をrs2番のレジスタを読み出した値、あるいは即値のビット数だけシフトした結果をrd番のレジスタに書き込む動作をします。
|
|
|
|
|
|
logical left shift とは、ビット列を左にシフトすることを意味します。左とは、上位ビット方向を意味します。左にシフトし、レジスタの幅からはみ出した値は捨てられます。一方、右にできた空間は0で埋められます。計算結果が桁あふれを起こさない場合、符号なし整数の$`N`$ bitの左シフトは、その整数に$`2^N`$を掛けた符号なし整数を求めることに使えます。
|
|
|
|
|
|
logical right shift とは、ビット列を右にシフトすることを意味します。左右が逆転したこと以外は左シフトと同様です。符号なし整数の$`N`$ bitの左シフトは、その整数を$`2^N`$で割った値の商を符号なし整数として求めることに使えます。
|
|
|
|
|
|
arithmetic right shift とは、ビット列を右にシフトする別の方法です。logical right shift では左にできた空間を常に0で埋めていましたが、もとのビット列での最上位ビットと同じもので埋めるのがarithmetic right shiftです。符号付き整数の$`N`$ bitの左シフトは、その整数を$`2^N`$で割った時の商を求めることに使えます。小数点以下は0に向かって丸められるのではなく、負の無限大方向に丸められるなど、最近のC言語における割り算と必ずしも挙動が一致しない点には注意が必要です。
|
|
|
|
|
|
シフト量を指定する`rs2`は、下位5bitのみを符号なし整数として解釈して使用されます。
|
|
|
`rs2`が負の数の場合に逆方向にシフトできたり、あるいは`rs2`が大きな値だから32bit以上のシフトができたり、というようなことはありませんので注意してください。
|
|
|
|
|
|
### LUI
|
|
|
|
|
|
`LUI` は Load Upper Immediate の略であり、`ADDI`だけでは作ることのできない、大きな値をレジスタに入れることが目的の命令です。命令語中には、即値の12bit目から31bit目しか記述されませんが、残りの0bit目から11bit目は暗黙に0となります。rdレジスタには、その値がそのまま書き込まれます。
|
|
|
|
|
|
この命令と`ADDI`命令を組み合わせることで、二命令で任意の32bitの値をレジスタに入れることが可能になります。
|
|
|
|
|
|
### AUIPC
|
|
|
|
|
|
`AUIPC` は Add Upper immediate to PCの略であり、プログラムカウンタ(PC)に即値を加算した値をrd番のレジスタに書き込みます。`LUI`命令と同様、即値の下12bit分は暗黙に0になります。
|
|
|
|
|
|
使用されるPCの値は、この命令の存在するアドレスと同じ値になります。
|
|
|
|
|
|
また、この命令は単にrd番のレジスタに値を書き込むだけであり、PCを操作する命令ではない(つまり、PCは通常通り+4される)ことに注意します。
|
|
|
|
|
|
### JAL
|
|
|
|
|
|
`JAL` は Jump And Link の略であり、rd番のレジスタに`PC+4`を書き込むと同時に、PCの値を`PC+imm`に変更する命令です。
|
|
|
|
|
|
rdとして零レジスタを指定した場合、レジスタに値が書き込まれることはなく、単に捨てられます。
|
|
|
つまり、そのような場合は単にPCの値を`PC+imm`に変更する、`J` (Jump) 命令となります。
|
|
|
|
|
|
rdとして零レジスタ以外を指定すると、関数呼び出しを実現することができます。
|
|
|
つまり、今実行している関数を中断して他の関数を呼び出した後、その呼び出した関数が終了したときに戻ってくる
|
|
|
べきアドレス、つまり`JAL`命令の次のアドレスがrd番のレジスタに書き込まれます(一命令は 4 Byte なので、PC+4 が戻ってくるべきアドレスです)。
|
|
|
|
|
|
このような用途の場合、通常、rdとしては1を指定することが慣例となっています。このため、1番のレジスタは、リンクレジスタと呼ばれます。
|
|
|
|
|
|
この命令を使ってもレジスタにPCの値を書き込むことができますが、単にPCを得る目的では`AUIPC`命令が存在するため、そういった使い方は非推奨のようです。
|
|
|
|
|
|
### JALR
|
|
|
|
|
|
`JALR` は Jump And Link Register の略です。rd番のレジスタにPC+4を書き込むと同時に、PCの値をrs1番のレジスタを読み出した値に即値を加えた値に変更する命令です。
|
|
|
|
|
|
rdとして零レジスタを指定した場合、単にPCの値を`rs1+imm`に変更する、`JR` (Jump Register) 命令となります。`imm`を0として、関数からのリターンに使うことができます。rs1としては、`JAL`命令のデスティネーションレジスタであるはずの、1番レジスタを指定すればよいことになります。
|
|
|
|
|
|
関数ポインタを用いた関数の呼び出しも、この命令を使うことで実現が可能になります。
|
|
|
|
|
|
さらに、`AUIPC`命令で得た値と組み合わせることで、任意のアドレスにジャンプ、あるいは任意のアドレスに存在する関数を呼び出すことが可能です。`JAL`命令だけでは現在のPCから近い場所にしかジャンプ、あるいは関数コールができません。
|
|
|
|
|
|
### Beq, Bne, Blt, Bge, Bltu, Bgeu
|
|
|
|
|
|
最初の四つは、Branch if EQual、Branch if Not Equal、Branch if Less Than、Branch if Greater than or equal to の略であり、残りの二つはそれの Unsigned 版です。
|
|
|
rs1番のレジスタとrs2番のレジスタの値を読み込み、それらを比較し、条件が成立していればPCの値を`PC+imm`に変更する動作をします。
|
|
|
|
|
|
各命令で、「条件が成立する」というのは以下の条件を満たすことを意味します。
|
|
|
|
|
|
* `Beq`命令では、`rs1==rs2`が成り立った時
|
|
|
* `Bne`命令では、`rs1!=rs2`が成り立った時
|
|
|
* `Blt`命令では、`rs1`と`rs2`を符号付き整数とみなしたとき、`rs1<rs2`が成り立った時
|
|
|
* `Bge`命令では、`rs1`と`rs2`を符号付き整数とみなしたとき、`rs1>=rs2`が成り立った時
|
|
|
* `Bltu`命令では、`rs1`と`rs2`を符号なし整数とみなしたとき、`rs1<rs2`が成り立った時
|
|
|
* `Bgeu`命令では、`rs1`と`rs2`を符号なし整数とみなしたとき、`rs1>=rs2`が成り立った時
|
|
|
|
|
|
Less than or Equal to (LE) や Greater Than (GT) に相当する命令がないように思われますが、単にrs1とrs2を取り換えれば実現できます。
|
|
|
|
|
|
### SB, SH, SW
|
|
|
|
|
|
それぞれ Store Byte、Store Half word、Store Word の略です。そのアーキテクチャの設計者が決めたbit長のことをWord長といいます[^1]が、RISC-V では32bitです。そのため、Half word というのは16bitを意味します。
|
|
|
[^1]: 普通、Word長(ワードサイズ)と言ったらレジスタの幅のことを指しますが、命令ニーモニック中に出現するWordという単語の示すbit長は必ずしもそれと一致しません。例えば、x86_64というアーキテクチャではレジスタサイズは64bitですが、ワードサイズは(歴史的経緯から)16bitです。RISC-Vには64bit版もありますが、ワードサイズは32bit版と変わらず32bitです。意味不明ですが、単にそのアーキテクチャの設計者が"決めた"ものだと理解して暗記する以外に方法はありません。
|
|
|
|
|
|
rs1番のレジスタとrs2番のレジスタの値を読み込み、メモリアドレス`rs1+imm`に`rs2`の値の下位8/16/32bitをストアします。
|
|
|
|
|
|
RISC-Vはリトルエンディアンなので、`rs1`が0x00010000、`rs2`が0x12345678、`imm`が0の時、`SB`命令を実行するとメモリの0x00010000番地だけが0x78に変更されます。`SH`命令の場合、メモリの0x00010000番地が0x78、0x00010001番地が0x56に変更されます。`SW`命令の場合、メモリの0x00010000番地が0x78、0x00010001番地が0x56、0x00010002番地が0x34、0x00010003番地が0x12に変更されます。
|
|
|
|
|
|
MIPSはビッグエンディアンなので、これとは異なった挙動を示します。MIPSを前提に書かれた書籍を参照して間違えないように気を付けましょう。
|
|
|
|
|
|
命令語中で即値が書かれている場所が二か所に分離していますが、合わせて一つの即値を定義しています。これはソースレジスタ番号の指定を命令語中の同一箇所で行うことでプロセッサを高速化することが目的です。プロセッサの構成を考えてみると、レジスタの番号は即値よりも早く必要になります。そのため、即値のビット列の並び替えには時間的余裕があり、少し複雑化してもよいだろうという発想になっています。
|
|
|
|
|
|
### LB, LBU, LH, LHU, LW
|
|
|
|
|
|
それぞれ、Load Byte、Load Byte Unsigned、Load Half word、Load Half word Unsigned、Load Wordの略です。
|
|
|
rs1番のレジスタの値を読み込み、メモリアドレス`rs1+imm`から8/16/32bitのデータをロードし、rd番のレジスタに書き込みます。
|
|
|
|
|
|
レジスタに書き込む際、Uのついた方の命令は零拡張を行います。これは、8bit読み込んだがレジスタの幅は32bitなので、足りない部分がありますが、そこを0で埋めるという意味です。Uのついていない方の命令は符号拡張を行います。これは、埋める部分を読み込んだデータの最上位ビットと同じもので埋めるという意味です。
|
|
|
|
|
|
|
|
|
ストアした値がそのままロードできるようになっています。例えば、`rs1`が0x00010000、`imm`が0、メモリの0x00010000番地が0xcd、0x00010001番地が0xab、メモリの0x00010002番地が0x34、0x00010003番地が0x12である場合、
|
|
|
|
|
|
* `LB`命令を実行するとrd番のレジスタに0xffffffcdが書き込まれます。
|
|
|
* `LBU`命令を実行するとrd番のレジスタに0x000000cdが書き込まれます。
|
|
|
* `LH`命令を実行するとrd番のレジスタに0xffffabcdが書き込まれます。
|
|
|
* `LHU`命令を実行するとrd番のレジスタに0x0000abcdが書き込まれます。
|
|
|
* `LW`命令を実行するとrd番のレジスタに0x1234abcdが書き込まれます。
|
|
|
|
|
|
|
|
|
|
|
|
## おまけ (RV32M)
|
|
|
|
|
|
ここに書かれている命令は必ずしも実装する必要はありません。CoreMarkには乗算・除算が含まれているため、実装するとほかの人と差をつけられるかも?
|
|
|
|
|
|
### MUL
|
|
|
|
|
|
rs1番のレジスタとrs2番のレジスタの値を読み出し、その積の下位32bitをrd番のレジスタに書き込みます。符号付き整数を二の補数で表した場合、和や差は符号なし整数の場合と同じ回路を使えるという利点があるのでした。積の下位32bitに関してもこの特徴が成り立ちます。確かめてみましょう。
|
|
|
|
|
|
乗算回路は一から記述する必要はありません。今回の実験で使う FPGA には DSP (Digital Signal Processing)ブロックが搭載されています。これは、乗算を行う専用回路があらかじめ実装されていると考えてよいです。Verilog HDLで単に`a = b * c`のように記述すれば、自動で DSPブロックが使われ、ANDゲートと加算器をたくさん並べるのよりも高効率な実装となります。そうはいっても乗算回路は単純な加算などに比べて非常に遅い回路であるため、`MUL`命令を実装するとプロセッサのクロック周波数が下がってしまうかもしれません。どうすればよいか、考えてみましょう。
|
|
|
|
|
|
### MULH, MULHSU, MULHU
|
|
|
|
|
|
rs1番のレジスタとrs2番のレジスタの値を読み出し、その積の上位32bitをrd番のレジスタに書き込みます。32bitの整数同士の掛け算の結果は64bitになりますが、その上位32bitだということです。
|
|
|
|
|
|
* `MULH`命令は、`rs1`と`rs2`を符号付き整数として解釈し、結果を符号付き整数として計算します。
|
|
|
* `MULHSU`命令は、`rs1`を符号付き整数、`rs2`を符号なし整数として解釈し、結果を符号付き整数として計算します。
|
|
|
* `MULHU`命令は、`rs1`と`rs2`を符号なし整数として解釈し、結果を符号なし整数として計算します。
|
|
|
|
|
|
### DIV, DIVU
|
|
|
|
|
|
rs1番のレジスタとrs2番のレジスタの値を読み出し、その商をrd番のレジスタに書き込みます。
|
|
|
|
|
|
`DIV`命令の場合は`rs1`と`rs2`は符号付き整数であると解釈して計算します。結果は符号付き整数として求めます。`DIVU`命令の場合は`rs1`と`rs2`は符号なし整数であると解釈して計算します。結果は符号なし整数として求めます。ただし、`DIV`の場合、求めるべき商は、数学的な商ではなく、答えを実数で求めた後小数点以下を切り捨て(0への丸め)を行ったものになります。
|
|
|
|
|
|
除算は、オペランドの値によっては結果が定義されないことがあります。そういった場合でも、例外が発生することはなく、以下に示す結果になります。
|
|
|
|
|
|
* 零除算(`rs2`が0の場合):`DIV`命令であれば-1、`DIVU`命令であれば$`2^{32}-1`$
|
|
|
* オーバーフロー(`DIV`命令で`rs1`が$`-2^{31}`$かつ`rs2`が-1の時、数学的な結果は$`2^{31}`$だが表現できない):$`-2^{31}`$
|
|
|
|
|
|
### REM, REMU
|
|
|
rs1番のレジスタとrs2番のレジスタの値を読み出し、その剰余をrd番のレジスタに書き込みます。
|
|
|
|
|
|
`REM`命令の場合は`rs1`と`rs2`は符号付き整数であると解釈して計算します。結果は符号付き整数として求めます。`REMU`命令の場合は`rs1`と`rs2`は符号なし整数であると解釈して計算します。結果は符号なし整数として求めます。ただし、`REM`の場合、求めるべき商は、数学的な剰余ではなく、符号が被除数と同じ剰余です(`rs1`と`rs2`の絶対値をとってから剰余を計算した後、`rs1`と同じ符号にすればよいです)。
|
|
|
|
|
|
剰余演算の結果が定義されていない場合の挙動は以下の通りです。
|
|
|
|
|
|
* 零除算(`rs2`が0の場合):`rs1`
|
|
|
* オーバーフロー(`REM`命令で`rs1`が$`-2^{31}`$かつ`rs2`が-1の時):0
|
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|