5日目: [CPU] レジスタの複数化とマルチプレクサ
この記事はひとりでCPUとエミュレータとコンパイラを作る Advent Calendar 2017の5日目の記事です。
昨日は1bitのCPUを導入した。
要するにCPUとは、クロックが立ち上がるたびに、計算した値を変数に代入するループであった。 変数はDフリップフロップを用いて実装されており、そのように情報を記録する素子をレジスタと呼ぶのだった。
昨日はCPUの簡単な例を出すのが目的だったので、1bitレジスタ1個のCPUを考えた。 変数はaしかなかった。 今日はレジスタ4個、つまりa,b,c,dの4変数が扱える回路を考える。だいぶCPUらしくなるぞ~
1bitレジスタ4個のCPU
今日の目標はこの回路を理解することだ。 実はこの回路、4個のレジスタに対しmov命令が実行可能な回路になっている。
予告通り、Dフリップフロップが4個左側に並んでいる。このレジスタを上から順にa,b,c,dと命名した。
真ん中あたりに、MUXとかMとかDEMUXなどと書かれた台形の赤い素子がある。 MUXとMはマルチプレクサのことで、DEMUXはデマルチプレクサを表している。 今日はこいつらの解説がメインだ。
昨日の1bit反転CPUと比べると、線が全体的に四本になり、NOTゲートの代わりにマルチプレクサの入った回路になった。しかし本質は全く変わらない。 昨日も今日も、Dフリップフロップの出口(Q)を出発した線がぐるっと一周して入口(D)に帰還する回路であることに変わりはない。
マルチプレクサ
まずは最初の画像の右側にいるマルチプレクサ(Mと書かれた小さな台形)だが、その実装は
左側が省略記号で、右側が基本論理ゲートによる実装を表している。
真理値表は
入力 S | 出力 Y |
---|---|
0 | A |
1 | B |
つまりマルチプレクサとは、A,Bの2つの入力データから出力を1つ選ぶゲートである。 A,Bを選ぶための線がSで、出力がYだ。 これを3入力1出力のマルチプレクサという。
A,B,Sの電圧が変わると即座にYの電圧も変わるので、マルチプレクサは組み合わせ回路である。 これをHDLで記述すると
module multiplexer_AB ( input A, input B, input S, output Y, ); always_comb begin if (S) Y = B; else Y = A; end endmodule
このようにalways_comb
中でif文を使って書ける。
次に、マルチプレクサを拡張してみる。
A ,Bの2入力をA,B,C,Dの4入力にすると、最初の図の左下に位置する大きな台形のマルチプレクサになる
このマルチプレクサは6入力1出力となるが、入力6本のうちの4本はデータ線A,B,C,Dで、残り2本はどのデータを選ぶか指定するための線S1,S2となっている。 S1, S2をまとめてSとした絵が、最初の回路図に描かれている。 混乱を避けるために、最初の回路図ではSの線上に斜線を引いて2と書いた。この表記は今後も出てくる。
真理値表は
入力 S1,S2 | 出力 Y |
---|---|
0,0 | A |
0,1 | B |
1,0 | C |
1,1 | D |
HDLでこのマルチプレクサを記述すると
module multiplexer_ABCD ( input A, input B, input C, input D, input [1:0] S, output Y, ); always_comb begin case (S) 2'b00 Y = A; 2'b01 Y = B; 2'b10 Y = C; 2'b11 Y = D; endcase end endmodule
Sが配列で与えられcase文が使用されているが意味はわかるだろう。
2'b00
は、ビットサイズが2で、binary表示(2進数表示)で00
で表される値という意味だ。
こういうコードを見たときには、直近のABCD入力のマルチプレクサの動画を思い浮かべて欲しい。 動画中では、Sの値に応じてABCDの1つをYに繋いでいることを強調したつもりだ。
デマルチプレクサ
最初の画像の右下にいるデマルチプレクサ(DEMUXと書かれた大きな台形)を説明する。
これは(S1,S2)の2進数を使い、4本の出力のうち1本を1にして、他は全て0にする組み合わせ回路である。
真理値表は
入力 S1,S2 | 出力 A,B,C,D |
---|---|
0,0 | 1,0,0,0 |
0,1 | 0,1,0,0 |
1,0 | 0,0,1,0 |
1,1 | 0,0,0,1 |
回路図と真理表を見れば意味はわかると思う。
HDLで記述すると、組み合わせ回路(always_comb
)であることに注意して
module demultiplexer_ABCD ( input [1:0] S, output A, output B, output C, output D, ); always_comb begin case (S) 2'b00 begin A = 1'b1; B = 1'b0; C = 1'b0; D = 1'b0; end 2'b01 begin A = 1'b0; B = 1'b1; C = 1'b0; D = 1'b0; end 2'b10 begin A = 1'b0; B = 1'b0; C = 1'b1; D = 1'b0; end 2'b11 begin A = 1'b0; B = 1'b0; C = 1'b0; D = 1'b1; end endcase end endmodule
冒頭のCPU再考
マルチプレクサとデマルチプレクサが分かったので、いよいよ本題だ。 最初の回路を再掲するが、まずは赤い部分を見て欲しい。
左下のMUXのあたりから線を辿っていくと
- 左下のMUXは、レジスタa,b,c,dの出力の中から1つを選出している。どれを選ぶかはSyが決める。
- 1で選ばれたデータが、レジスタaの出力と共にM(マルチプレクサ)に入っている。
- 2でどちらが選ばれるかは、右下のデマルチプレクサの出力が決める。
- 右下のデマルチプレクサのうちどれが1になるかは、Sxが決める。
つまり
という操作をクロックの立ち上がりのたびに実行している1。
機械語とニーモニック
今回のCPUの挙動を具体的に考えたいので、Sx=00
,Sy=01
の場合を考察する。
一回のクロックで各レジスタがどのように代入されるかを現実に忠実に書き下すと
; 以下の4命令が、1回のクロックで同時実行される mov a, b mov b, b mov c, c mov d, d
ここで、mov a, b
はC言語でいうところのa = b;
のようなもので、aにbを代入するという理解でよい。
b, c, dについては自分自身を代入しており、普通のプログラミング言語に慣れた身からすると無意味な操作に映るだろう。
しかしCPUの場合、自分自身を代入することは「値を保持する」という操作に対応する。
昨日説明したように、各レジスタはクロック立ち上がりの度に値を保持するか変えるかの二択を迫られているのだ。
従ってCPUの挙動を現実に忠実に表現するには、4つのレジスタa,b,c,dのmov
命令を書くのが原理的に正しい。
しかし、わざわざmov b, b
とかmov c, c
と書くのは面倒くさい。
値の保持は、いわばデフォルトの挙動だからあえて書く必要もないだろう。
ということで、普通は保持命令を簡略化して書く
mov a, b
この形式でCPUの挙動を表現する言語をアセンブリ言語という。 またアセンブリ言語で書かれたCPUの1命令(つまり1行)をニーモニックという。 またSxとSyの値を機械語という。 ニーモニックと機械語の対応関係は
機械語 | ニーモニック | 説明 |
---|---|---|
0000 | nop | 何もしない |
0001 | mov a, b | aをbで上書き |
0010 | mov a, c | aをcで上書き |
0011 | mov a, d | aをdで上書き |
0100 | mov b, a | bをaで上書き |
0101 | nop | 何もしない |
0110 | mov b, c | bをcで上書き |
0111 | mov b, d | bをdで上書き |
1000 | mov c, a | cをaで上書き |
1001 | mov c, b | cをbで上書き |
1010 | nop | 何もしない |
1011 | mov c, d | cをdで上書き |
1100 | mov d, a | dをaで上書き |
1101 | mov d, b | dをbで上書き |
1110 | mov d, c | dをcで上書き |
1111 | nop | 何もしない |
機械語0000
はニーモニックmov a, a
になるかと思いきや、nop
となっている。
このときは全レジスタが変更されないので、つまり何もしないno-operationという命令を実行したとみなせる。
回路全体の記述
最後に今回の回路全体をHDLで記述しておく
// movとリセット命令を持つCPU module mov_only_cpu( input CLOCK, input [1:0] Sx, input [1:0] Sy); logic a, b, c, d; // Dフリップフロップ logic y; // ワイヤ logic next_a, next_b, next_c, next_d; // ワイヤ // コピー元のレジスタをxに繋ぐ always_comb begin case (Sy) 2'b00: y = a; 2'b01: y = b; 2'b10: y = c; 2'b11: y = d; endcase end // レジスタ更新用のワイヤを作る always_comb case (Sx) 2'b00: begin next_a = y; next_b = b; next_c = c; next_d = d; end 2'b01: begin next_a = a; next_b = y; next_c = c; next_d = d; end 2'b10: begin next_a = a; next_b = b; next_c = y; next_d = d; end 2'b11: begin next_a = a; next_b = b; next_c = c; next_d = y; end endcase end // レジスタの更新 always_ff @(posedge CLOCK) begin a <= next_a; b <= next_b; c <= next_c; d <= next_d; end endmodule
少し長いが、基本は昨日の1bit反転CPUのHDLコードと同じだ。 あのコードからNOTゲートを抜いて、レジスタをa,b,c,dの4個に増やし、マルチプレクサとデマルチプレクサを加えたにすぎない。
最初のalways_comb
のブロックは回路図のMUX
で書かれたあたりの線の繋がりを記述している。
二番目のalways_comb
のブロックは回路図のDEMUX
とM
で書かれたあたりの線の繋がりを記述している。
最後のalways_ff
のブロックは回路図の左端でワイヤがDフリップフロップに接続している箇所を記述している。
どうでもいい話
今書いた回路全体のHDLには、CPUとして重要な機能が2つ欠落している。
レジスタの初期化
現状のHDLコードでは、a,b,c,dの値を交換することはできるけど、「aの値を1にする」という操作ができない。 a,b,c,dに値をセットする機能はリセット機能と呼ばれている。 実際のPCは、電源ボタンを押したらCPUのリセット機能が走るように作られている(多分)。
値の表示
a,b,c,dの値を交換したところで、それが我々の目に見えなければ意味がない。 現状のHDLコードでは、a,b,c,dの値を外部から確認する術がない。 実際のPCは、CPUはメモリにつながっており、その中でVRAMと呼ばれる部分に値を書き込むことでディスプレイを光らせることができる。 FPGAでレジスタの値を手軽に見るには、レジスタとLEDを繋げて光らせれば良い。 この辺をうまくやるには制約ファイル等の説明が必要なのだが、面倒くさいので割愛。