執筆: H.T

はじめに

皆様こんにちは。

前編に引き続き、CPUについてのお話、実装編です。前回お読み頂いた方は、早く実装したくてウズウズしていることでしょうから、早速本題に入りましょう。

では、策定したISAを基に回路作りを始めます!

CPUの全体像

まずは、今回作成する回路の全体像を確認します。ALUやMUXなど、聞き慣れない言葉が多いかと思いますが、都度ご説明いたしますのでご安心ください。なお、今回はジャンプ命令を実装しませんのでご了承ください。

まずはこれを見て、なんとなくデータの流れを掴みましょう。データは図の左から右に、命令メモリ、レジスタ、ALU、データメモリへと流れます。最終的な演算結果がレジスタに書き戻されることに注意してください。

この図には「命令メモリ」と「データメモリ」という2種類のメモリが存在します。2つのメモリに、プログラムとデータを分けて保存しているのです。このような構成は、ハーバードアーキテクチャと呼ばれており、直感的に理解し易いため、教育用としてもよく用いられています。

大まかにそれぞれのユニットについて確認します。後々詳しくご説明いたしますので、さらっと読み飛ばしていただければ結構です。


PC(プログラムカウンタ)

データを保存するレジスタの一種で、「次に実行する命令のアドレス」が格納されています。この中の値は自動的に更新され、常に次の命令を指し示していると考えてください。

命令メモリ

その名の通り、プログラムが格納されているメモリです。PCが示しているアドレスにある命令を、制御装置やレジスタに送ります。

レジスタ

レジスタについては前回触れましたが、小さくて高速なメモリのことを指します。今回作成するCPUには8つの汎用レジスタを搭載する予定です。R形式の命令では、指定されたレジスタに格納されている値を、次のALUに送り、演算結果を格納します。

ALU(演算装置)

これも名前通り演算を行う装置です。レジスタや命令からの即値を受け取り、演算結果を出力します。

MUX(マルチプレクサ)

マルチプレクサはとても便利なユニットで、2つ以上のデータを受け取り、状況に応じてどちらか一方を選択して出力することができます。

制御装置

これらのユニットを制御するのが制御装置です。命令を読み取って、演算装置にどのような演算をさせるか、マルチプレクサでどちらのデータを出力するかなどを制御します。


個人的な意見ですが、CPUのような複雑な構造を考えるときに大切なのは、機能を抽象化して考えることだと思っています。図のようにCPU全体を見たときに、ALUというユニットがどういった仕組みで動いているかを知る必要はありません。必要なのは、ALUが何のために存在し、どのようなデータを受け取り、何を出力するかだけです。言い換えれば、インプットに対するアウトプットが動作仕様に則していれば、その内部がどのような方法で実装されていようが問題ないのです。

本稿以降では皆様に、CPU内部の各ユニットについて抽象的に理解していただければと思います。勿論これから詳細なロジックに触れていくわけですが、あくまで「理解できた」という実感を深めるための足がかりになればと考えています。多少読み飛ばしていただいても抽象的な理解は可能です。

ALU

まずは、CPUの主要な機能の一つ、演算を司るALUです。ある意味最もわかりやすいユニットでしょう。

前回策定した命令セットアーキテクチャ(ISA)によれば、ALUで実行可能な演算の種類は「加算」「減算」「乗算」「XOR」「AND」「OR」「シフトレフト」「シフトライト」の8つです。

OPCode命令種別詳細
0000add足し算(加算)
0001sub引き算(減算)
0010mul掛け算(乗算)
0011xorXOR
0100andAND
0101orOR
0110slビットを左にシフト(シフトレフト)
0111srビットを右にシフト(シフトライト)
1000addiレジスタの値と指定した値を足し算(即値加算)
1001subiレジスタの値と指定した値を引き算(即値減算)

ALUには、3つの入力と1つの出力が必要です。入力のうち2つは、演算対象となる値を受け取るためのものです。それぞれ、「InputA」「InputB」としておきます。出力1つは演算結果の出力用なので、「Output」としましょう。今回は8bitCPUということで、演算に利用できる値の幅(入出力ビット長)は8bitです。

もう1つの入力は、「制御信号」です。ALUは、制御装置からの指示に従って、どの演算を行うか切り替える機能が必要です。8つの機能切り替えには、3bitの制御信号を受け付ける必要があります。

まとめると、ALUは「InputA」「InputB」で与えられた2つの値に対して、制御信号に基づいた演算を行い、1つの値を出力します。

では、CircuitVerseで実装してみましょう。

まずは入出力と、前々回に作成した加算器を追加してみました。足し算なのか、引き算なのかという選択は、出力の直前にマルチプレクサで行います。つまり、内部的には加算、減算、乗算等あらゆる演算を行い、どの結果を出力するかを制御信号で指定しているのです。この場合、制御信号が “000” のときに加算結果を出力します。

あとはこのマルチプレクサに様々な演算器を追加していくだけです。

各演算器の詳細については割愛しますが、制御信号を切り替えることで、それぞれ動作していることを確認できます。ちなみに、3bitの制御信号はISAで策定した命令コードの下3桁に対応しています。add命令もaddi命令も足し算の命令ですが、なんと素晴らしいことにどちらも下3桁は ”000” なのです…!(最初から想定していただけ)

これは後で必要になってくるのですが、ALUに「同値判定」機能と専用の1bitアウトプットを付けることにします。2つの入力A、Bが同じ値であれば、アウトプットから1が出力されるという機能です。同値判定は以下のようなアルゴリズムで実装できます。

  1. InputA と InputB を引き算する
  2. 演算結果が0であるか確認する

ゼロである事を判定するため、アウトプット名は「zero」とします。

同値判定をわざわざALU内に作ったのは、既に減算器が存在しており都合が良いためです。後ほど詳しく説明しますが、同値判定は beq 命令を実装する際に必要になります。

これでALUは完成です。

今後、ALUの中身には触れませんので、以下のように抽象化して考えることにします。

順序回路とクロック

レジスタを作る前に、データの記憶を行う回路の仕組みについて見ていきましょう。

論理回路は、組み合わせ回路順序回路の2種類に分けることができます。組み合わせ回路とはすなわち、「与えられた入力に基づいて出力を決定する回路」です。今までやってきた加算器などは組み合わせ回路に属するものでした。

そして、レジスタのような記憶可能な回路は順序回路に属します。順序回路は「現在の内部状態と入力に基づいて、出力及び次の内部状態が決定する回路」です。現在だけでなく、過去の入力値が出力結果に影響します。なんらかの形で過去の入力を記憶しているわけです。

最も簡単な順序回路の1つ、FlipFlopを紹介します。

まず、入力Rの0を1に変えてみてください。すると、出力Q’が1に変わります。

次に、入力Rを0に戻します。この時、FlipFlopは内部状態を保存しているため、出力結果は変わりません。

今度はSの入力を1にしてみます。すると、出力Q’が0という内部状態が保存され、Sを戻しても出力は変化しません。

このように、FlipFlopはシーソーのように内部状態を保存します。

これを使いやすく発展させたものがD-FlipFlop回路です。

入力数は同じですが、用途が少し変わります。D-FlipFlopはCLK(クロック)信号がオンになっている時だけ、入力Dに基づいて内部状態を変更します。次にCLKがオンになるまでの間、内部状態は保存され続けます。

ちなみに、ある信号が有効になっていることをアサート、無効な場合をネゲートと言います。「クロック信号がアサートされると、レジスタの内部状態が更新される」というように使います。かっこいい言葉なので、積極的に使っていきたいと思います。

ところで、いきなり登場したこのクロック信号とは何なのでしょうか。

前々回の加算器編にて、デジタルとアナログについて説明しました。デジタル回路は、非連続な値を用いることでアナログ界にあるノイズを排除できるのでした。先に結論から言ってしまうと、クロック信号とは謂わば時間をデジタル化するためのものなのです。

組み合わせ回路には、時間という概念がありませんでした。現在の入力によって現在の出力が決定されるため、出力結果は過去の状態と切り離されているのです。ところが、順序回路は違います。過去の状態が出力に影響してくるということで、そこに時間の概念が生まれます。ここで初めて問題になるのが、「どこまでが過去で、どこからが現在であるか」です。

もちろんアナログ世界において、時間は連続的に流れており、今この瞬間のみが現在、それより前はすべて過去です。しかし、例えば信号が0から1に切り替わる一瞬の間、電圧は0でも1でもない不安定な状態になります。丁度その瞬間が”現在”となったとき、回路は正しい動作を保証することができません。

そこで生まれたのが、クロックという概念です。

クロック信号は、一定の周期でオンオフを繰り返す信号です。例えばレジスタであれば、クロックが1になったタイミングでのみデータを保存することで、確実な動作を保証できます。クロック信号をCPU中の必要な回路に発信することで、複数あるユニットがタイミングを同期しながら確実に動作することができます。離散的な時間で動作させることで、時間的なノイズから開放されるのです。設計者の立場で考えると、クロック信号がネゲートされている間はデータ信号が不安定であっても良いという、設計のゆとりが生まれます。

余談ですが、CPUの設計ごとに「1クロックあたりの最大命令実行数」は決まっています。つまり、クロック周波数が高いほどCPUの処理性能が高くなるわけですが、いくらでも速度をあげられるわけではありません。クロック周期を、CPUを構成するトランジスタのスイッチング時間(0から1に変化する速度)よりも短くしてしまうと、トランジスタが切り替わる前にクロック信号がアサートされてしまい、結果として正常な動作が行えないからです。

現代のハイエンドクラスのCPUは、5GHz(毎秒50億回)前後のクロック周波数で動作することができます。

レジスタ

それではレジスタ1つ分の回路を見ていきましょう。

D-FlipFlopをたくさんつなげただけです。Inputに値をセットし、CLKが1になると、レジスタに値が保存されます。

このCPUには8つのレジスタが入っているため、この8つをまとめて1つのレジスタファイルというユニットとして構成します。イメージとしては、レジスタファイルに対して「レジスタアドレス1番にこのデータを保存して(または格納されている値を出力して)」と命令すると、要望通りに動作してくれるようにしたいです。そのために、レジスタファイルには2つの出力と、5つの入力を持たせます。

なんだかごちゃごちゃしてきました。入出力信号数が多いので、役割紹介を兼ねて整理します。

入力信号名ビット数役割
ReadRegister13bit1つ目の読み込みレジスタアドレス
ReadRegister23bit2つ目の読み込みレジスタアドレス
WriteData8bit書き込みデータ
WriteRegister3bit書き込みレジスタアドレス
RegWrite1bitデータを書き込むか否か
出力信号名ビット数役割
ReadData18bit1つ目の読み込みデータ
ReadData28bit2つ目の読み込みデータ

これらの入出力の必要性について、最もレジスタを多用するR形式の命令を見ながら考えていきましょう。

この命令形式を見ると、1命令につきレジスタファイルから2つの値(被演算子)を取り出し、1つの値(演算結果)を格納しています。2つある出力は、このデータを取り出すための出口です。(ReadData1, ReadData2)さらに、どのデータを取り出すかを指定するために2つ、どこにデータを格納するかを指定するために1つの入力が必要です。(ReadRegister1, ReadRegister2, WriteRegister)

データ格納のために、書き込みデータの内容を受け付けるための入力があります。(WriteData)

RegWrite入力は、「レジスタへ値を格納すべきか否か」を指定する入力です。これは本来クロック信号の役割ですが、条件分岐(bne)命令やストア(sw)命令ではデータの格納を必要としないため、制御装置で命令を判別し、さらにクロック信号が印加されているときにアサートされるようなRegWrite信号を作り(後述)、入力します。

レジスタアドレス7番はゼロレジスタといって、常に0を返すようにしました。近年のプロセッサにはあまり見られない構成ですが、これだけ単純なCPUでは利用価値があります。

これでレジスタファイルが完成しました。

書き込みを行うには、WriteDataでデータを、WriteRegisterでレジスタアドレスを指定した後に、RegWriteをアサートします。

ReadRegisterでレジスタアドレスを指定することで、指定したアドレスに格納されているデータがReadDataに出力されます。

まとめ

今回はCPUの全体像を見つつ、ALUとレジスタを作成しました。CPUは主にこの2つを使って演算を行っているため、中核と言っても過言でない存在かと思います。

次回は最終章です。簡単なメモリ類を作成し、制御装置と合わせて今まで作ってきた回路同士を繋ぎ合わせていきます。

ではまたお会いしましょう。

参考書籍

David A. Patterson & John L. Hennessy,『コンピュータの構成と設計』.第5版.日経BP.2014

関連リンク

CircuitVerse

コンピュータ・アーキテクチャ関連記事一覧(ORENDA技術ブログ)