コンピュータアーキテクチャとRISC-V CPUの構造について見ていきましょう。

本記事の内容は浦項工科大学校のCSED311の内容を基に、 授業で扱われなかったいくつかの内容を追加して構成しました。

アーキテクチャとは?

コンピュータアーキテクチャは、ハードウェアとソフトウェアの間のインターフェース規約です。 具体的にはCPUが理解する命令セット(ISA、Instruction Set Architecture)を 中心に、以下の内容を定義します。

  • 命令(インストラクション):どのような演算(加算、分岐、メモリアクセスなど)をサポートし、どのような形式で 表現するか
  • レジスタ:レジスタがいくつあり、それぞれ何ビットか、それぞれの用途は何か
  • メモリモデル:アドレス空間のサイズ、バイト順序(エンディアン)
  • 例外/割り込み処理:エラーや外部イベントをどのように処理するか

アーキテクチャは実際のハードウェア的な実装とは無関係に同一に維持される抽象層 です。コンパイラがこの規約だけを見て当該アーキテクチャに合うコードを生成すれば、その アーキテクチャを実装したどのチップでも動作します。そのため、製造元が異なっても アーキテクチャが同じであればユーザーは同一のOSとアプリケーションを実行できます。(AMDと Intelのx86-64 CPUを想像できます)

一方、マイクロアーキテクチャはアーキテクチャという「約束」を実際のハードウェアで実現する内部 設計です。パイプライン、アウトオブオーダー実行、分岐予測などの最適化や詳細な実装が マイクロアーキテクチャに該当します。

アーキテクチャの種類

コンピュータアーキテクチャの主な種類には、フォン・ノイマンアーキテクチャ、ハーバードアーキテクチャ、 データフローアーキテクチャなどがあります。

フォン・ノイマンアーキテクチャ

フォン・ノイマンアーキテクチャはジョン・フォン・ノイマンが提案したコンピュータの基本構造で、今日ほぼ すべての汎用コンピュータの基盤となっています。フォン・ノイマンアーキテクチャ以前のコンピュータ(ENIACなど)は プログラムを変更するには物理的に配線を変更する必要がありました。

当時ENIACを開発したモークリーとエッカートは後続コンピュータEDVACを設計しながら、プログラムと データを同じメモリに格納する方法を構想します。こうすることで、プログラムをメモリに ロードするだけで一つの配線で異なる作業を実行できるという利点があります。 (その後フォン・ノイマンがこのアイデアをまとめた報告書を単独著者として配布したことで、 この構造の名前がフォン・ノイマンアーキテクチャとなりました。)

フォン・ノイマンアーキテクチャはCPUがメモリから命令を取得し、解釈し、実行する 過程を繰り返してプログラムを実行します。この時、命令とデータが同じメモリ、同じ データバスを共有するため、CPUがいくら高速であってもメモリ帯域幅によって 性能が制限されるフォン・ノイマンボトルネック(Von Neumann Bottleneck)が発生します。この 理由から、現代のCPUはフォン・ノイマンアーキテクチャを基盤としていますが、伝統的なフォン・ノイマン アーキテクチャの構造をそのまま使用せず、以下のハーバードアーキテクチャの構造と適切に 組み合わせて使用しています。

ハーバードアーキテクチャ

ハーバードアーキテクチャはフォン・ノイマンアーキテクチャと同時期にハーバード大学のMark Iコンピュータに由来する アーキテクチャです。Mark Iは命令をパンチテープから、データを機械式カウンタから 別々に読み取っていたため、フォン・ノイマンアーキテクチャとは異なり命令メモリとデータメモリを 物理的に完全に分離した構造を持ちます。

ハーバードアーキテクチャは命令とデータのバスを区別して命令と データを同時に読み取ることができるため、スループットが高いという利点があります。 しかし二つのメモリ間の容量を柔軟に分割できず、プログラムを命令メモリに移す 別のメカニズムが必要です。現代ではマイクロコントローラ(Arduinoに搭載されるAVRなど)や DSP(デジタル信号処理器)のような一部の分野でハーバードアーキテクチャが利用されています。

修正ハーバードアーキテクチャ

修正ハーバードアーキテクチャは現代のCPUの大部分が採用している、フォン・ノイマンアーキテクチャと ハーバードアーキテクチャの間の折衷案です。L1キャッシュレベルでI-cacheとD-cacheを分離して ハーバードアーキテクチャの高い帯域幅という利点を取り、それ以下のメモリは統合して フォン・ノイマンアーキテクチャの柔軟性を維持します。CPU側からはL1キャッシュのメモリが 命令とデータに分離されているためハーバードアーキテクチャのように動作しますが、キャッシュを直接 扱わないコンパイラやプログラマの観点からは一つのアドレス空間としてコンピュータを扱うことが できます。

データフローアーキテクチャ

データフローアーキテクチャは上記のハーバードアーキテクチャやフォン・ノイマンアーキテクチャとは根本的に 異なるアーキテクチャパラダイムです。フォン・ノイマン方式はプログラムカウンタが「次に実行する 命令」を順次指定します。データフローアーキテクチャはこの フォン・ノイマン方式の順次的な演算実行という限界を脱却するために設計された アーキテクチャで、プログラムカウンタがありません。 データフローアーキテクチャではプログラムカウンタが順次的にどの命令を実行するか 指定しなくても、必要なデータがすべて準備されると命令が即座に実行されます。プログラムの実行 順序をプログラマやハードウェアが決めるのではなく、データ自体が実行 フローを決定するのです。

データフローアーキテクチャではプログラムは順次的な命令リストではなく、有向 グラフで表現されます。(a + b) * (c - d)のような演算を実行する時、加算ノードと 減算ノードは互いに依存関係がないため同時に実行されます。二つの演算の結果がどちらも乗算 ノードに到着すると乗算演算が発火(fire)します。明示的にプログラマが並列 演算を指定しなくても、自動的にすべての演算が並列的に実行されるのです。

1970年代に初めて登場したデータフローアーキテクチャは1980-1990年代に次世代コンピュータとして 活発に研究されましたが、様々な限界により商用化には失敗しました。データフロー アーキテクチャに必要な演算がハードウェア的に非常にコストが高かったこと、大規模データ 構造を扱う際の困難さ、既存のすべてのソフトウェアインフラが順次実行モデルを 前提として作成されているという複合的な問題のためでした。

純粋なデータフローコンピュータは現在は姿を消しましたが、アイデアは現代まで残り 受け継がれています。フォン・ノイマンモデル内でのアウトオブオーダー実行(OoO)構造、FPGAでの 回路設計などでデータフローアーキテクチャの設計思想が受け継がれており、最近ではAI アクセラレータプロセッサがデータフロー原理をハードウェアに再び適用しており、汎用 コンピューティングマシンとしては商用化されませんでしたが、様々な特殊目的ハードウェアで データフローアーキテクチャを利用しようとする研究が続いています。

ISA

ISAはInstruction Set Architectureの略で、ソフトウェアとハードウェアの間の インターフェースと言えます。ISAは以下のような事項を決定します。

  • 命令セットにどのような演算が存在するか、各命令のエンコーディング形式はどうか
  • ハードウェアが直接サポートするデータ型にはどのようなものがあるか、バイト 順序はどうか(リトルエンディアン、ビッグエンディアン)
  • レジスタ構成とレジスタの数、ビット幅と用途
  • アドレッシングモード(Addressing Mode)と命令がオペランドの位置をどのように 指定するか
  • メモリモデルのアドレス空間サイズ
  • どのような状況(ゼロ除算、ページフォールト、不正な命令)で例外が発生するか、 割り込みはどのように処理されるか
  • 権限レベルと保護モード/カーネルモードの区分、各モードでアクセス可能なレジスタと 命令の範囲、仮想メモリ関連部分

一方、ISAの下に位置するマイクロアーキテクチャに関連する部分(クロック速度、パイプライン 段数、キャッシュ構造など)とソフトウェア層の規約(ABI、メモリマップ、ファームウェア インターフェース)はISAが指定しません。

Programmer Visible StateはISAでプログラマ(コンパイラ)が命令を通じて直接 読み書きできるすべてのハードウェア状態です。プログラムの意味(semantics)はこの 状態の変化で定義されます。具体的には汎用レジスタ、プログラムカウンタ、スタック ポインタ、フラグレジスタ、メモリアドレス空間全体などが該当します。

これとは反対に見えない状態(microarchitectural state)はキャッシュ内容、分岐 予測器のヒストリ、リオーダーバッファ、リザベーションステーション、物理レジスタファイル、TLBエントリ などがあります。これらは性能にのみ影響を与え、プログラムの論理的な結果を変えることは ありません。

同じISAを実装したどのチップでもProgrammer Visible Stateの変化が同一であれば 正確な実装であり、その他の内部状態は自由に異なることができます。

ISAが提供する命令を機能別に分類すると以下のように分けることができます。

  • 算術/論理演算(Arithmetic/Logical):レジスタや即値に対して演算を 実行します。結果は通常レジスタに格納され、副次的にフラグが更新されます。
  • データ移動(Data Transfer/Memory):レジスタとメモリ間、またはレジスタ間で データを移動します。
  • 制御フロー(Control Flow):プログラムカウンタを変更して実行フローを変えます。 無条件分岐(Jump)、条件分岐(Branch)、関数呼び出し/復帰などがあります。
  • システム命令(System/Privileged):OSやハードウェアの制御のための 命令です。ユーザーモードからカーネルへの移行、割り込み制御などがあります。現代の マイクロプロセッサではこのような命令の大部分はカーネルモードでのみ実行可能で、 ユーザーモードで実行すると例外が発生します。

これらの命令は必ず**アトミックに(Atomicity)**実行されなければなりません。一つの 命令はProgrammer Visible Stateを複数変更できますが、このような中間変更 過程はプログラマに公開されてはなりません。実際のハードウェアではパイプラインで レジスタを先に書き込み、PCを後で更新するといった形で命令を段階的に処理することが ありますが、これはMicroarchitectural Stateであり、Programmer Visible Stateではありません。

ISAの歴史

EDSAC

EDSACは最初のストアドプログラムコンピュータの一つで、非常に単純なISAを持って います。

命令は5ビットopcode + 予約ビット + 10ビットメモリアドレス(n)で構成され、単一アキュムレータ (Accumulator)構造です。レジスタはAcc一つだけで、すべての演算はこのAcc レジスタを中心に行われます。命令の例は以下の通りです。

  • A n:M[n]ACCに加算します
  • T n:ACCの内容をM[n]に移し、ACCをクリアします
  • E n:ACC>=0ならば、M[n]にジャンプします
  • I n:テープから次の文字を読み取り、M[n]が指すアドレスに格納します。
  • Z :プログラムを停止してベルを鳴らします。

すべてのメモリアドレスが命令にハードコーディングされている点が特徴です。しかしこれでは 以下のような問題が発生します。

  1. コードで関数を実行する場合、関数は呼び出し元に復帰しなければなりません。 しかしEDSACは復帰アドレスが命令にハードコーディングされているため、 同じ関数を複数箇所から呼び出す場合、復帰アドレスの指定が困難でした。
  2. 配列要素にアクセスする際、配列のアドレスが命令にハードコーディングされているため、ループを 回しながら配列にアクセスするには毎回命令自体を修正してアドレスを変更する必要がありました。

ISAの発展

このような問題を解決するため、後代のCPUは様々な方式で進化してきました。

一つ目に、レジスタはアキュムレータレジスタ一つだけを使用する構造から、プログラムのアドレス 指定のためのレジスタが追加され、現代では汎用レジスタ(GPR、General Purpose Register)が多数追加されてすべてのレジスタをどのような用途でも使用できるように なりました。

二つ目に、命令に様々なオペランドを指定できるようになりました。EDSACのように 命令が暗黙的なオペランド(ACC)を持ち、明示的オペランドは一つだけの構造を MonadicOP inout, in2のようにオペランドが2つで入力の一つが結果で 上書きされる構造をDyadicOP out, in1, in2のように出力と入力2つをすべて 明示する構造をTriadicと呼びます。

後述するCISCアーキテクチャではADD [mem], regのように命令一つでメモリアクセスと演算を 同時に実行できる一方、RISCアーキテクチャではメモリアクセスはload/storeのみで 行います。(load-store architecture)

三つ目に、命令に様々なメモリアドレスを指定できるようになりました。

  • Absoulte - LD rt, 100:EDSACと同様にアドレスが命令に定数として指定される方式です。
  • Register Indirect - LD rt, (r_base):レジスタに格納されている値をアドレスとして 使用します。ポインタのデリファレンスがこの方式です。
  • Displaced - LD rt, offset(r_base):レジスタの値に定数オフセットを加えて アドレスを作ります。構造体アクセスに有用です。
  • Indexed - LD rt, (r_base, r_index):二つのレジスタの値を加えてアドレスを 作ります。
  • Memory Indirect - LD rt, ((r_base)):メモリアドレスから値を読み取り、その値を 再びアドレスとして使用します。二重ポインタのデリファレンスに該当します。
  • Auto increment/decrement - LDR rt, (r_base):レジスタの値をアドレスとして 使用しつつ、アクセス前後にレジスタの値を自動的に増加/減少させます。配列を 順次走査する時に有用です。

RISCの登場

初期のCPUの発展は、単純だったISAを徐々に複雑にし、命令セットに様々な 機能を追加する形で進められました。当時はアセンブリで直接プログラミングすることが 一般的であり、多様な命令を追加することでプログラマのコーディングを簡便にすることが できました。また、メモリのサイズや性能が限られていたため、頻繁に使用される複雑な 演算を一度に処理することが有用でした。

しかし、その後コンパイラ技術が発展し、プログラマが直接複雑なアセンブリ命令を 使用してプログラミングするケースはほとんどなくなりました。人が直接使用することを 想定して作られた複雑な命令はコンパイラが最適化しにくく、命令ごとに 実行時間やメモリアクセス回数が大きく異なるためパイプラインを効率的に設計することも 困難でした。

このような背景でRISC(Reduced Instruction Set Computer)が登場します。すべての 命令のサイズは同じ大きさで、メモリアクセスは必ず指定された命令で、各命令は 単純にしつつレジスタを十分に提供するなど、命令セットを単純化するという 思想で作られたRISC CPUは、複雑さをハードウェアからコンパイラに移し、ハードウェアを 単純にする代わりに、最適化の負担は発展したコンパイラに委ねます。

RISCはパイプラインを容易に構築でき、クロックを上げるにも有利でした。 ハードウェアが単純なのでトランジスタをキャッシュ、分岐予測器など性能に貢献する箇所に 投資でき、検証とデバッグが容易で設計サイクルも短かったです。

RISCと対比して、RISC以前のアーキテクチャ(x86、M68K、Z80など)を CISC(Complex Instruction Set Computer)と呼びますが、 M68K、Z80など多くのCISCアーキテクチャは消滅しましたが、x86は巨大なソフトウェアエコシステムのため 消滅せず、代わりに外部ISAはCISC、内部的にはRISCという折衷案を採用して、 複雑な命令をCPU内部でRISCに分解して処理する設計を導入しました。

現代のRISCアーキテクチャの代表格としてはARMがありますが、ARMは初期に簡単な 設計による高い電力効率(消費電力あたりの性能比)を武器に新しい分野であるモバイル市場に 素早く参入し、2020年代以前まではRISCアーキテクチャはモバイル、組み込みなどの低消費電力 設計で、CISCアーキテクチャは高消費電力を費やす代わりに高い性能を必要とする ワークステーションやデスクトップ、サーバー環境で利用されてきました。

しかし2020年代以降、Apple SiliconでM1チップを発売したことでこの状況が大きく 逆転しました。AppleのM1等のARM CPUはRISCアーキテクチャベースのCPUが高い性能とCISC CPUより改善された電力効率で急速に市場シェアを伸ばしており、Apple以外にもARM Windows/Linuxノートパソコンが販売されるなど、モバイル市場以外でもRISCアーキテクチャが シェアを少しずつ伸ばしています。

RISC-V

RISC-Vは2010年にUC Berkeleyで始まったオープンソースISAです。1981年に設計された 最初のRISCプロセッサであるRISC-I、RISC-IIに過去30年間の教訓を反映してレガシーなしに クリーンなRISC ISAをゼロから設計しました。(Vは Berkeleyで開発された5番目のRISC 設計という意味です。RISC-I、RISC-II、SOAR、SPUR、RISC-V)

RISC-Vは特定企業に依存するARMやx86とは異なり、誰でも無料でRISC-V ISAを 実装したプロセッサを製造でき、スタートアップ、大学、大企業すべてが自由にチップを設計 できます。また、モジュール式設計を利用して一つの巨大なISAではなく、最小限の 命令を持つ小さなISAに必要な拡張を付加する構造で開発されました。

RISC-VのProgrammer Visible State

RISC-Vには基本的にx0からx31まで32個の整数汎用レジスタがあります。 x0は常に0に固定された状態ですが、これはx0を0に固定することで様々な 動作を既存の命令で表現でき、命令の種類数を減らせるためです。 (例えば、NOPをADDI x0, x0, 0で、即値ロードをADDI x1, x0, 42で表現 できます。)

また、汎用レジスタに含まれないPCレジスタと浮動小数点演算を提供するF/D 拡張に含まれる浮動小数点レジスタなどが存在します。

メモリはバイトアドレッシング方式で、基本的にリトルエンディアン方式です。メモリの すべてのバイトはProgrammer Visible Stateであり、基本的にリトルエンディアンアラインメントを 使用します。

RISC-Vの拡張体系

RISC-Vは小さな基本ISAに必要な拡張を組み合わせて使用する構造です。

基本となるISAは以下の通りです。

  • RV32I:32ビット整数基本。整数算術/論理、load/storeなど最小限の 命令のみを持ちます。
  • RV64I:64ビット整数基本。RV32Iに64ビット演算とダブルワード命令が 追加されます。
  • RV128I:128ビット整数基本。ただし64ビットより大きなアドレス空間がいつ必要になるかは まだ不明確なため、仕様が完全に確定(frozen)されていない状態です。
  • RV32ERV64E:組み込み用縮小ISA。レジスタを32個から16個に削減して超小型 マイクロコントローラのコストを下げます。

ここに以下のような標準Extensionが存在します。

  • M:整数乗算、除算:MULMULHなどハードウェア乗算器のネイティブ整数 乗算演算命令を追加します。超小型組み込み機器ではハードウェア乗算なしに 乗算と除算をビット演算など他の演算で代替できるため、乗算と 除算が別の拡張として分離されています。
  • A:アトミック演算:マルチコア環境でコア間メモリ共有のための 命令です。メモリへの書き込みや読み取りをアトミックに実行したり、メモリを 予約する機能などが定義されています。
  • F:32ビット浮動小数点演算:FPUによるハードウェア浮動小数点演算を 追加します。32個の浮動小数点レジスタf0-f31と浮動小数点演算の結果を 記録するフラグであるfcsrが追加されます。
  • DQ:それぞれ64ビット倍精度浮動小数点演算と128ビット4倍精度 浮動小数点演算命令を追加します。
  • C:よく使う命令を16ビットでエンコードできるようにします。コードサイズを 削減できるため、フラッシュメモリのサイズが大きく制限される組み込み環境で使用 できます。
  • V:ベクトル拡張。SIMDと類似した様々なベクトル演算をサポートします。
  • H:ハイパーバイザー拡張。ハイパーバイザーがサポートする仮想化層をサポートします。

これ以外に単一アルファベットの代わりに、拡張名の前にZを付けた細分化された拡張もあります。

  • Zicsr:CSRアクセス命令
  • Zifencei:命令キャッシュ同期、自己修正コード、JITコンパイル時のI-Cacheと D-Cacheの一貫性保証
  • ZbaZbbZbcZbs:アドレス計算高速化、基本ビット演算、キャリーレス 乗算などの詳細ビット操作命令
  • Zfinx:浮動小数点演算を整数レジスタで実行
  • Ztso:Total Store Orderingメモリモデル保証。x86バイナリ変換時に有用
  • Zicond:条件付き演算。分岐なしに条件付きで値を選択

非常に多様な拡張が存在するため、ソフトウェア互換性のためによく使用される 拡張をまとめて定義するRISC-Vプロファイルが存在します。 例えばRVA20U64はI、M、A、F、D、C、Zicsr、Zifencei拡張を 必須として持つように定義されています。

RISC-V命令フォーマット

RISC-Vは6種類の基本命令フォーマットを定義します。RV32Iですべて32ビット固定 長であり、opcode、rd、rs1、rs2の位置がフォーマット間で最大限一貫して配置され、 デコーディングを単純にします。

R-Type

レジスタ間演算に使用されます。

[31:25]  [24:20]  [19:15]  [14:12]  [11:7]  [6:0]
funct7   rs2      rs1      funct3   rd      opcode
7ビット    5ビット    5ビット    3ビット    5ビット   7ビット

rs1、rs2の二つのソースレジスタで演算し、結果をrdに格納します。funct7funct3が具体的な演算の種類を区別します。

例:ADD x1, x2, x3:x1 <- x2 + x3

I-Type

即値演算、load、JALRなどに使用されます。

[31:20]     [19:15]  [14:12]  [11:7]  [6:0]
imm[11:0]   rs1      funct3   rd      opcode
12ビット      5ビット    3ビット    5ビット   7ビット

12ビット即値が符号拡張(sign-extend)されて使用されます。R-typeからfunct7rs2の位置が即値に置き換わった形式です。

例:ADDI x1, x2, 10:x1 <- x2 + 10

R-typeからfunct7がなくなったため、若干の命令の違いが存在します。例えば R-typeでfunct7で区別されるADD/SUBの場合、I-typeでは負の即値を 使えばよいので命令自体が不要となり省かれています。また、Immediate fieldを分割して細分 命令を区別するケースもあります。 (RV32I基準で即値が最大5ビットまでしか必要ないSLLI、SRLI、SRAIなどのシフト 命令は、先頭7ビットを命令指定子として、残り5ビットをシフト即値として 使用します。)

メモリから値を取得するLoad命令もI-Typeですが、opcodeの位置にLoadを 意味する0000011を入れ、rs1をベースに、12ビットimmはオフセットとして 使用します。funct3に応じて32ビットを読むLW、16ビットを読んで符号拡張するLH、 16ビットを読んでゼロ拡張するLHU、8ビットを読むLBLBUなどが区別されます。

また、即値は常に12ビットに制限されます。プログラムで使用する即値は 通常小さな値であり、大きな定数が必要なケースは通常まれであるため、 最適化のための意図的な設計です。どうしても大きな定数が 必要な場合は後述するU-type命令やAUIPCなどを使用して値を生成します。 これは一つの命令ですべてを行おうとせず、簡単な複数の命令の組み合わせで 解決しようというRISCの思想に従ったものです。

S-Type

メモリに値を格納するのに使用されます。

[31:25]   [24:20]  [19:15]  [14:12]  [11:7]   [6:0]
imm[11:5] rs2      rs1      funct3   imm[4:0] opcode
7ビット     5ビット    5ビット    3ビット    5ビット    7ビット

結果をレジスタに書き込まないためrdがなく、代わりにrdの位置に即値の下位 ビットを、funct7の位置に即値の上位ビットを入れます。

例:SW x3, 12(x2)MEM[x2 + 12] <- x3

B-Type(SB-Type)

条件分岐に使用されます。

[31]     [30:25]    [24:20]  [19:15]  [14:12]  [11:8]   [7]     [6:0]
imm[12]  imm[10:5]  rs2      rs1      funct3   imm[4:1] imm[11] opcode
1ビット    6ビット      5ビット    5ビット    3ビット    4ビット    1ビット   7ビット

S-Typeと類似していますが、即値のビット配置が異なります。即値が分岐オフセットとして 使用され、RISC-V命令は最小16ビット単位でアラインされるため、分岐オフセットの最下位 ビットは常に0であるためエンコードしません。したがって13ビット範囲の分岐が可能です。

例:BEQ x1, x2, offsetx1 == x2ならPC <- PC + offset

ビット配置が独特にねじれている理由は、S-typeと最大限多くのビット位置を共有して ハードウェアを単純化するためです。このようなS-Typeとの類似性からB-Typeを SB-Typeと呼ぶこともあります。

U-Type

上位20ビット即値を使用する命令です。

[31:12]     [11:7]  [6:0]
imm[31:12]  rd      opcode
20ビット      5ビット   7ビット

20ビット即値が上位20ビットに配置され、下位12ビットは0で埋められます。I-Type 命令で最大12ビットまでの即値を使用できるため、これを補完するために 作られた命令です。

  • LUI(Load Upper Immediate):rd <- imm << 12、大きな定数を作る時にADDIと 組み合わせて使用します。0x12345678を作る時、LUI x1, 0x12345で上位20ビットを 埋め、ADDI x1, x1, 0x678で下位12ビットを埋めます。
  • AUIPC(Add Upper Immediate to PC):rd <- PC + (imm << 12)、PC相対アドレス 計算に使用され、JALとloadと組み合わせて現在位置から±2GB範囲のアドレスを作ることが できます。

J-Type(UJ-Type)

無条件ジャンプに使用されます。

[31]     [30:21]    [20]     [19:12]    [11:7]  [6:0]
imm[20]  imm[10:1]  imm[11]  imm[19:12] rd      opcode
1ビット    10ビット     1ビット    8ビット      5ビット   7ビット

20ビット即値をジャンプオフセットとして使用し、B-Typeと同様にbit0は常に0であるため エンコードしません。したがって21ビット範囲(±1MB)のジャンプが可能です。

B-Typeと類似して、U-Typeとビット位置を最大限共有するように設計されています。 そのためUJ-Typeと呼ばれることもあります。

RV32I命令セット

整数算術/論理(R-type)

命令動作説明
ADD rd, rs1, rs2rd ← rs1 + rs2加算
SUB rd, rs1, rs2rd ← rs1 - rs2減算
AND rd, rs1, rs2rd ← rs1 & rs2ビットAND
OR rd, rs1, rs2rd ← rs1 | rs2ビットOR
XOR rd, rs1, rs2rd ← rs1 ^ rs2ビットXOR
SLL rd, rs1, rs2rd ← rs1 << rs2[4:0]論理左シフト
SRL rd, rs1, rs2rd ← rs1 >> rs2[4:0]論理右シフト(0埋め)
SRA rd, rs1, rs2rd ← rs1 >>> rs2[4:0]算術右シフト(符号維持)
SLT rd, rs1, rs2rd ← (rs1 < rs2) ? 1 : 0符号あり比較
SLTU rd, rs1, rs2rd ← (rs1 < rs2) ? 1 : 0符号なし比較

即値算術/論理(I-type)

命令動作説明
ADDI rd, rs1, immrd ← rs1 + sext(imm)即値加算
ANDI rd, rs1, immrd ← rs1 & sext(imm)即値AND
ORI rd, rs1, immrd ← rs1 | sext(imm)即値OR
XORI rd, rs1, immrd ← rs1 ^ sext(imm)即値XOR
SLLI rd, rs1, shamtrd ← rs1 << shamt即値論理左シフト
SRLI rd, rs1, shamtrd ← rs1 >> shamt即値論理右シフト
SRAI rd, rs1, shamtrd ← rs1 >>> shamt即値算術右シフト
SLTI rd, rs1, immrd ← (rs1 < sext(imm)) ? 1 : 0即値符号あり比較
SLTIU rd, rs1, immrd ← (rs1 < sext(imm)) ? 1 : 0即値符号なし比較

上位即値(U-type)

命令動作説明
LUI rd, immrd ← imm << 12上位20ビットロード
AUIPC rd, immrd ← PC + (imm << 12)PC基準上位20ビット加算

Load(I-type)

命令funct3動作説明
LB rd, imm(rs1)000rd ← sext(MEM8[rs1 + sext(imm)])バイトロード(符号拡張)
LH rd, imm(rs1)001rd ← sext(MEM16[rs1 + sext(imm)])ハーフワードロード(符号拡張)
LW rd, imm(rs1)010rd ← MEM32[rs1 + sext(imm)]ワードロード
LBU rd, imm(rs1)100rd ← zext(MEM8[rs1 + sext(imm)])バイトロード(ゼロ拡張)
LHU rd, imm(rs1)101rd ← zext(MEM16[rs1 + sext(imm)])ハーフワードロード(ゼロ拡張)

Store(S-type)

命令funct3動作説明
SB rs2, imm(rs1)000MEM8[rs1 + sext(imm)] ← rs2[7:0]バイト格納
SH rs2, imm(rs1)001MEM16[rs1 + sext(imm)] ← rs2[15:0]ハーフワード格納
SW rs2, imm(rs1)010MEM32[rs1 + sext(imm)] ← rs2ワード格納

条件分岐(B-type)

命令funct3動作説明
BEQ rs1, rs2, offset000if (rs1 == rs2) PC ← PC + offset等しければ分岐
BNE rs1, rs2, offset001if (rs1 != rs2) PC ← PC + offset異なれば分岐
BLT rs1, rs2, offset100if (rs1 < rs2) PC ← PC + offset符号あり未満
BGE rs1, rs2, offset101if (rs1 >= rs2) PC ← PC + offset符号あり以上
BLTU rs1, rs2, offset110if (rs1 < rs2) PC ← PC + offset符号なし未満
BGEU rs1, rs2, offset111if (rs1 >= rs2) PC ← PC + offset符号なし以上

ジャンプ

命令タイプ動作説明
JAL rd, offsetJ-typerd ← PC+4, PC ← PC + offset直接ジャンプ(関数呼び出し)
JALR rd, rs1, immI-typerd ← PC+4, PC ← (rs1 + sext(imm)) & ~1間接ジャンプ(関数復帰等)

メモリ順序

命令説明
FENCE pred, succメモリアクセス順序保証(pred/succ:I、O、R、Wの組み合わせ)

システム命令

命令動作説明
ECALL環境呼び出しトラップ発生システムコール(OS移行)
EBREAKデバッグトラップ発生ブレークポイント

以前はZicsrZifencei拡張がRV32Iに含まれていましたが、2.1で別の 拡張として分離されたため、現在は合計40個の命令がRV32Iを構成しています。

RISC-V ABIコンベンション

レジスタコンベンション

ISAレベルでは強制されませんが、すべてのシステムで推奨されるレジスタの使用 コンベンションです。x0 = 0だけが唯一ISAで強制されるものであり、それ以外はハードウェア の観点では同等です。実際にこのようなコンベンションを遵守するのはABI、Calling Conventionの役割です。

しかし、ほとんどのコンパイラ、OS、デバッガがこのようなコンベンションを遵守しているため、 特殊なベアメタル組み込みで外部ライブラリすら使用しない非常に特殊な 場合を除いては、このコンベンションを遵守する必要があります。

レジスタABI名用途Caller/Callee saved
x0zero常に0(ISAレベルで固定)
x1ra復帰アドレス(Return Address)Caller
x2spスタックポインタ(Stack Pointer)Callee
x3gpグローバルポインタ(Global Pointer)
x4tpスレッドポインタ(Thread Pointer)
x5t0一時レジスタCaller
x6t1一時レジスタCaller
x7t2一時レジスタCaller
x8s0 / fp保存レジスタ / フレームポインタCallee
x9s1保存レジスタCallee
x10a0関数引数1 / 戻り値1Caller
x11a1関数引数2 / 戻り値2Caller
x12a2関数引数3Caller
x13a3関数引数4Caller
x14a4関数引数5Caller
x15a5関数引数6Caller
x16a6関数引数7Caller
x17a7関数引数8Caller
x18s2保存レジスタCallee
x19s3保存レジスタCallee
x20s4保存レジスタCallee
x21s5保存レジスタCallee
x22s6保存レジスタCallee
x23s7保存レジスタCallee
x24s8保存レジスタCallee
x25s9保存レジスタCallee
x26s10保存レジスタCallee
x27s11保存レジスタCallee
x28t3一時レジスタCaller
x29t4一時レジスタCaller
x30t5一時レジスタCaller
x31t6一時レジスタCaller