第2回 プロセッサの設計1
本日の講義内容
- 設計環境
- SystemVerilog入門
- RISC-V ISA入門
- 汎用プロセッサの設計 (1)
設計環境
SystemVerilogを用いて記述したRTL設計が正常に動作するか検証するために、本講義では、HDLシミュレータである、Cadence Xceliumを利用します。このツールや、これが導入された設計環境の詳細については、別途資料を用いて説明します。もし環境の不具合等で利用できない場合、他の導入しやすいHDLシミュレータとして
などがあります。これらを利用すると良いでしょう。基本的にお好みの環境で問題ありません。ただし、次回以降におこなう論理合成や配置配線については、こちらで準備した環境上で実行する必要があります。アカウントにログインできることをぜひ確認しておいてください。なお、他のHDLシミュレータの例として、Siemens ModelSim/Questaといった商用ツールも同様の機能を持っています。これらは今後VLSI設計に携わる中で使う機会があるかもしれません。
SystemVerilog 101
本講義では、SystemVerilogを用いて汎用プロセッサのRTL設計を記述していきます。SystemVerilogはVerilog HDLの後継、かつ上位互換にあたるもので、文法の多くはVerilog HDLと共通しています。しかし、SystemVerilogでは型や制御構文が強化され、安全で読みやすいコードが書けるようになっています。
SystemVerilogには、設計、検証においてそれぞれ利点があります。本講義で触れるのはその一部に留まり、また設計の方が中心になります。しかし、SystemVerilogは本来、設計と検証の統一言語を目指しており、検証用の高度な機能も多数加えられています。
大規模なVLSIを精緻に検証する必要がある場合、あるいは、そうしたVLSIを構成するために、複数のIPへと分割し、統一的な検証をおこなう必要がある場合などは、そのような検証機能が重要になります。SystemVerilogに基づいて標準化された検証フレームワークであるUVM (Universal Verification Methodology) の存在については、特に覚えておくと良いでしょう。
SystemVerilog手習い
HDLは回路図をテキストで記述するための言語で、ソフトウェアのように手続きを書くのではなく、回路そのものを定義するのでした。Verilog/SystemVerilogにおける回路部品はmodule
という箱のようなものとして定義され、入力信号input
と出力信号output
を持つのでした。
SystemVerilogには、logic
という型が存在し、信号は基本的にlogic
として宣言すればよいです。
演習: AND回路の設計
以下のコードに追記して、4-bitのAND回路を設計しましょう。
SystemVerilogでは、複雑な組み合わせ回路は、always_comb
の中でif
文やcase
文を利用することで記述できます。Verilogと異なり、組み合わせ回路専用のalways
があることで、より安全に回路を記述することができます。
演習: 比較回路の設計
以下のコードに追記して、a
とb
を比較し、a > b
のとき1
を出力する比較回路を設計しましょう。
また、case
文については、unique
キーワードを併せて利用することで、分岐条件が同時に2つ真になることが無いよう処理系で保証することができます。
演習: セレクタ回路の設計
以下のコードに追記して、sel
に応じて次のようなy
を出力するセレクタ回路を設計しましょう。
sel
: 00 ->y
: 0001sel
: 01 ->y
: 0010sel
: 10 ->y
: 0100sel
: 11 ->y
: 1000
なお、簡単な条件の組み合わせ回路であれば、三項演算子? :
を用いてassign
で書くこともできます。
演習: 最大値回路の設計
以下のコードに追記して、2つの4-bit信号a
, b
を比較し、大きい方の値を出力する回路を設計しましょう。三項演算子を使いましょう。
順序回路は、always_ff
の中で記述します。このとき、代入には通常ノンブロッキング代入<=
を用います。always_ff
の中に複数の代入文を記述するとしましょう。通常、依存関係を持たない複数の変数に対して、クロック遷移と同時に並列に値が代入されることを期待すると思います。こうした動作を実現するのがノンブロッキング代入です。複数のノンブロッキング代入文がある場合には同時に代入がおこなわれます。一方、ブロッキング代入=
では、ある代入文の実行が終了するまで、後続の代入文は実行されません。つまり、ブロックします。そのため、代入に順序関係が生じ、順序回路に対して適用すると意図しない動作となることがあります。
演習: DFF回路の設計
以下のコードに追記して、同期リセット機能付きのDFF回路を設計しましょう。
また、SystemVerilogでは、定数としてparameter
やlocaoparam
を使うことができます。前者はmodule
の外部から指定可能なものであり、後者はmodule
の内部だけで使う定数です。
最後に簡単なステートマシンを作ってみましょう。
演習: ステートマシンの設計
以下のコードに追記して、次のような仕様を持つステートマシンを設計しましょう。
- 状態は、OFF->ON->OFF->ON->OFF->......と変わる
- 入力: toggle (1のとき状態がトグルする)
- 出力: toggle_out (現在 ON 状態なら 1)
こたえ
このステートマシンを検証するためのテストベンチの記述を示します。テストベンチには検証用特有の記述がいくつか存在するのでした。
特に、assert
は動作中に条件が満たされていることをチェックするために有用で、以下の例でも使用しています。
演習: テストベンチを使ったシミュレーション
上のtb_toggle_fsm.svを用いてシミュレーションしてみましょう。
Xceliumを使用する場合は、%xrun tb_toggle_fsm.sv toggle_fsm.sv
というコマンドになります。
コーディング規約
SystemVerilogの記述スタイルとしては、たとえば、lowRISC Verilog Coding Style Guideが参考になるでしょう。シンプルかつ可読性が高く、論理合成可能なRTL設計を記述しやすいと思います。OpenTitanのような大規模プロジェクトで採用されています。
具体的には、
- 各ファイルには1つの
module
のみを定義し、ファイル名は "module
名.sv" とする - 信号には
logic
を使う (wire
やreg
は使わない) - クロック駆動の記述は
always_ff
、組み合わせ回路にはalways_comb
を使う- 簡単な組み合わせ回路は
assign
を使って記述してもok
- 簡単な組み合わせ回路は
case
文ではunique
を使用- 定数には
parameter
やlocalparam
を使う (define
は使わない)
あたりをまずは守ってみると良いでしょう。
SystemVerilogの簡単な歴史
Verilog HDLは、 Automated Integrated Design Systems (後のGateway Design Automation) が開発したVerilogやVerilog-XLというシミュレータ用の記述言語として誕生しています。その後、Verilog-XLはCadenceの物となりました。本講義の設計環境で用いているCadence XceliumはVerilog-XLに様々な機能を統合し、進化させたものです。一方、SystemVerilogはCo-Design Automationが1999年に発表したSuperlogが原型となっています。Verilog HDLが設計・検証それぞれにおいて抱えていた弱点の克服を見据え、Verilog HDLとC言語それぞれの利点を取り入れるという方針で開発が進められたようです。SystemVerilogは2005年にIEEEによって標準化され、現在にいたっています。なお、規格としてのVerilog HDLはSystemVerilogへと統合されており、SystemVerilog (IEEE 1800) が唯一の後継というかたちになっています。
RISC-V ISA
RISC-V (リスク・ファイブ) 命令セットアーキテクチャ (ISA) は、2010年ごろ、UC Berkeleyで開発が開始されたオープンなISAです。当時、多くのISAは商用ライセンスが必要で、アカデミックな研究や教育に使いづらかったため、オープンで自由に使えるISAとして設計されました。
RISCの原則に基づいて、シンプルかつ洗練された設計となっているほか、モジュラ形式という特徴もあります。これは、命令セットが、
- RV32I (基本整数命令)
- RV32M (乗除算命令)
- RV32A (アトミック命令)
- RV32F (単精度浮動小数点命令)
- RV32D (倍精度浮動小数点命令)
- RV32C (圧縮命令)
といったように基本命令セットの他に様々な拡張命令モジュールへと分割されており、必要に応じてこれらを組み合わせることができるというものです。なお、RV32I or RV64Iに対して、MAFD, Zicsr, Zifenceiといった拡張を施したものをRV32G or RV64Gと呼び、よく用いられる汎用的な命令セットになっています。
現在は、RISC-V Internationalという非営利団体がこうした仕様を管理しており、公式な各種仕様や周辺ツールについては
に情報がまとまっています。
以降では、最もシンプルな32-bit ISA、RV32Iについて見ていきましょう。本講義ではこのRV32Iを用いてプロセッサを設計していきます。非常にシンプルですが、この命令セットだけの実装でもCやRustといった高級言語で記述したソフトウェアをきちんとプロセッサ上で動作させることができます。
RISC-V RV32I
RISC-V RV32Iの中で今回実装していく命令をある程度の大きさで分類すると、
- 算術演算命令
- 論理演算命令
- 比較命令
- シフト命令
- LUI命令/AUIPC命令
- ロード命令
- ストア命令
- 条件分岐命令
- ジャンプ命令
といったものがあり、合計37種類の命令になります。これらについて、順に説明していきます。

まず、算術演算命令と論理演算命令です。各命令は32bitから成っており、下位7bitで命令種別を表し、その他のフィールドで、
rs1
: ソースレジスタ番号1 (5bit)rs2
: ソースレジスタ番号2 (5bit)rd
: デスティネーションレジスタ番号 (5bit)imm
: 即値
といったものが記述されています。ここで、レジスタ番号は32個存在するレジスタのうちどこに格納された値を演算に用いるか、あるいはどこに演算結果を書き込むかを表しています。5bitで32種類のレジスタいずれかをを指定します。即値は、命令に直接埋め込まれている定数で、この値も演算に用いられます。
それぞれの命令の内容に沿った演算がこうしたソースレジスタの値や即値を用いて実行され、デスティネーションレジスタへと書き込まれます。RISC-V ISAでは、基本的に即値は、32-bitの値へと符号拡張されてから演算に用いられます。ここで符号拡張とは、符号ビットを保ったまま拡張する方法で、たとえば最上位ビットが0なら左側に0を埋めていき、1なら1を埋めていくのでした。また、論理演算命令では、ビット毎に演算がおこなわれます。

比較命令は、各種比較の結果としてデスティネーションレジスタに0か1の値を書き込みます。
シフト命令では、シフトの量として即値やrs2の下位5bitのみが利用されることに注意が必要です。右シフト命令にはシフトの結果として空いた左側のビットに0を埋める論理右シフト命令と、空いた左側のビットに符号ビット (最上位ビット) を埋める算術右シフト命令があります。

LUIとAUIPCは少し特殊な命令です。LUIは20bitの即値を上位20bitとして下位12bitはゼロで埋め、レジスタに書き込む命令です。これに続いてADDIなどで下位bitを加えることで、32bitの大きな定数を作ることができます。AUIPCは20bitの即値を上位20bitとしてPC (プログラムカウンタ) の値に足し、デスティネーションレジスタに書き込みます。後述するJALRと組み合わせることで、任意の32bit相対アドレスへのジャンプを可能にします。

ロード命令とストア命令は、メモリのrs1+imm
で指定されたアドレスに対してロードやストアを実行します。それぞれ、1バイトや2バイト、4バイトと取り扱うデータの単位が異なる複数の命令が存在します。また、ロード命令についてはロードしてきたデータの取り扱いとして、符号拡張とゼロ拡張 (左側を0で埋める) の2種類が存在します。

条件分岐命令は、値の比較の結果を元に分岐をおこなう命令です。分岐する場合の分岐先アドレスはpc+imm
になります。ジャンプ命令では、JALの場合pc+imm
、JALRの場合pc+rs1
というアドレスに、それぞれジャンプします。ここでデスティネーションアドレスには、pc+4
という値を書き込みます。
Open Question: RISC-V ISAにおける即値の順番はどうしてこんなにバラバラになっているのでしょうか?
なお、RV32Iには他に下記のような命令が存在します。
- FENCE
- マルチスレッド、マルチコア環境、あるいは外部デバイスなどが存在する場合にメモリアクセス順序を制御するための命令
- ECALL/EBREAK
- 例外を発生させOSやデバッガへ制御を引き渡すための命令
これらはプロセッサ単体をベアメタル環境 (OSなどが無い環境) で動作させる際には考慮の必要が無いため今回は触れませんが、ISAの、システムソフトウェアとハードウェアとの界面という役割を考える上では非常に重要な命令です。システムソフトウェアに興味のある方は、これらやZicsr拡張命令についてその詳細を知っておくとよいでしょう。
また、RISV-Vのレジスタファイルは1個のゼロレジスタ (常に0の値をとるレジスタ) と31個の汎用レジスタにより構成されます。この各レジスタの役割はRISC-Vのアプリケーションバイナリインタフェース (ABI) 規約の中で定められています。

規約の詳細については前述した公式サイトで知ることができます。こちらについてもソフトウェア以上のレイヤとの関係が知りたい方はぜひ確認しておくと良いと思います。
DEC Alpha
DEC Alpha ISAは1990年代にDigital Equipment Corporation(DEC) が開発したRISC ISAです。先行する命令セットであるVAXと比較してRISCの特徴を数多く導入し、高性能化を追及しました。シンプルな固定長命令、豊富なレジスタ、ロードストア型アーキテクチャといった設計思想は、近年のRISC-Vにも通じるものであり、ISAの簡素さが高効率な実装につながるという理念は、当時すでに明確に現れていました。Alpha ISAに基づく汎用プロセッサである21164や21264は、洗練されたマイクロアーキテクチャに基づいて、非常なエンジニアリングコストをつぎ込んで回路やレイアウトを最適化しています。その一端は、たとえば、"High-Performance Microprocessor Design," JSSC1998.で説明されるタイミングクリティカルなパスの手作業による回路・配線調整や、電源のカスタム設計に見ることができます。こうしたカスタム設計による恩恵は、0.35μm CMOSプロセスにおいて、最大動作周波数が600MHzという値から定量的に窺い知ることもできます。今後、ある程度の規模のディジタル回路を論理合成で設計する際、この値を思い出してみると良いでしょう。現状の先端汎用プロセッサの大部分は、スタンダードセルの論理合成や配置配線によって設計されています。フロアプランや電源設計について、あるいは、ボトルネックとなるような特定の個別回路について、局所的には手のかかった最適化がおこなわれていることでしょう。しかし、回路設計とプロセス設計との分業が進んだこと、また、設計における主要な目標が消費エネルギーや熱密度の低減へと移ってきたこともあり、Alphaのプロセッサ群を思わせる種類の最適化は実施されていないのではないかと想像します。
汎用プロセッサの設計 (1)
汎用プロセッサの全体構成
本講義ではまず、シングルサイクルで動作するプロセッサを想定して各種機能ブロックの設計を進め、やがて全体のパイプライン化やマイクロアーキテクチャ最適化に取り組んでいきます。

具体的には、上図に示すような構成のプロセッサを設計していきます。全体を眺めると配線が複雑に見えますが、個別に動作を見ていくと非常にシンプルです。最初は、詳細なクロック駆動のタイミングなどを考慮せず、プロセッサ全体の動作の流れや各機能ブロックの働きについて見ていきましょう。

まず、プログラムカウンタ (PC) によるフェッチアドレスの指定です。PCは次に実行する命令のアドレスを出力します。クロック毎に次に実行するアドレスへと出力を更新していきます。条件分岐命令やジャンプ命令の無い場合には、分岐フラグ (br_taken
) が立たず、pc
に4を足したものが次のアドレスになります。分岐が実行される際には、br_taken
が立ち、分岐先アドレスbr_addr
の値が次のアドレスとなります。このbr_addr
については後のステージで計算されたものが入力されています。

次に、命令メモリからのフェッチです。PCの指すアドレスpc
に格納された32bitの命令列insn
が読み出され、後段のレジスタファイルや命令デコーダへと入力されます。

その後、命令列insn
はレジスタファイルと命令デコーダへと入力されます。レジスタファイルには命令列の指定するデスティネーションレジスタ番号rd
やソースレジスタ番号rs1
、rs2
が入力されます。それぞれに対応するレジスタの値として、rs1_val
やrs2_val
を出力します。また、命令デコーダは、insn
を入力として
- 符号拡張した即値:
imm[31:0]
- ALUの演算内容選択信号:
alu_code
- ALUへの入力選択信号:
alu_op1_sel
,alu_op2_sel
といったものを生成します。これによってALUで所望の演算を実行できるようにします。
また、
- ストア命令フラグ:
is_store
- ロード命令フラグ:
is_load
によって、データメモリへの書き込みの有無やデータメモリからの読み出しの有無を判定します。

これを受けて、ALUを用いて演算が実行されます。rs1_val
やrs2_val
、imm
といった信号から、命令に応じて生成されたalu_op1_sel
やalu_op2_sel
によって選択されたものがALUへと入力されます。演算の種別もalu_code
で適切なものが選択されます。また、ALUは分岐の有無br_taken
についても判定します。並行して、分岐先のアドレスbr_addr
も計算されます。

それから、データメモリへのアクセスがおこなわれます。ここでは、ALUによって計算されたアドレスに応じて、ロードやストアがおこなわれます。ストア命令フラグis_store
が立っている場合、このアドレスにデータrs2_val
が書き込まれます。

最後に、レジスタファイルへの書き戻しが実行されます。ロード命令フラグis_load
が立っている場合はデータメモリからロードしてきた値が、それ以外の場合はALUの計算結果alu_result
が、書き込まれます。
このように、個別のステージ毎に見てみると、プロセッサはシンプルに動いています。また、今PC以降に見てきた処理のステージは、典型的な5ステージパイプライン構成におけるそれと概ね一致しています。
ALUの設計
具体的にプロセッサの各機能ブロックの設計を進めていきましょう。最初は、ALUです。ALU (Arithmetic Logic Unit) では、RISC-V ISAにおける加減算、論理演算、比較、シフトといった演算が実行されます。デコーダによって生成された制御信号に応じた演算を、2つの入力信号に対して、実行します。
演習: ALUの設計
以下のコードを追記修正して、ALUを設計しましょう。
また、設計したALUについて、シミュレーションで動作を検証してみましょう。
演習: ALUの検証
テストベンチを作成して、ALUを検証しましょう。 assertを使うと良いと思います。
RISC-V ISAに基づくオープンソース汎用プロセッサ
RISC-V ISAはオープンに公開された仕様であり、ライセンスフリーで使用できるため、それに基づいて実装されたプロセッサは、自由に設計・公開することが可能です。例として、非営利団体であるOpenHW Groupや、ETH ZürichやUniversity of Bolognaのチームが中心となったPULP Platformなどから多様なプロセッサのRTL設計が公開されています、こちらでは実際に製造されたプロセッサチップについての情報も公開されています。
次回予告
本日はここまでです。次回は、汎用プロセッサの設計の続きをおこなっていきます。興味のある方は汎用プロセッサのマイクロアーキテクチャについて予習してみてください。