しかくいさんかく

解答略のメモ

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命令が実行可能な回路になっている。

f:id:kaitou_ryaku:20171203220411p:plain:w600

予告通り、Dフリップフロップが4個左側に並んでいる。このレジスタを上から順にa,b,c,dと命名した。

真ん中あたりに、MUXとかMとかDEMUXなどと書かれた台形の赤い素子がある。 MUXとMはマルチプレクサのことで、DEMUXはデマルチプレクサを表している。 今日はこいつらの解説がメインだ。

昨日の1bit反転CPUと比べると、線が全体的に四本になり、NOTゲートの代わりにマルチプレクサの入った回路になった。しかし本質は全く変わらない。 昨日も今日も、Dフリップフロップの出口(Q)を出発した線がぐるっと一周して入口(D)に帰還する回路であることに変わりはない。

マルチプレクサ

まずは最初の画像の右側にいるマルチプレクサ(Mと書かれた小さな台形)だが、その実装は

f:id:kaitou_ryaku:20171203220435g:plain:w300

左側が省略記号で、右側が基本論理ゲートによる実装を表している。

真理値表は

入力 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入力にすると、最初の図の左下に位置する大きな台形のマルチプレクサになる

f:id:kaitou_ryaku:20171203220451g:plain:w400

このマルチプレクサは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と書かれた大きな台形)を説明する。

f:id:kaitou_ryaku:20171203220504g:plain:w400

これは(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再考

マルチプレクサとデマルチプレクサが分かったので、いよいよ本題だ。 最初の回路を再掲するが、まずは赤い部分を見て欲しい。

f:id:kaitou_ryaku:20171203220516p:plain:w600

左下のMUXのあたりから線を辿っていくと

  1. 左下のMUXは、レジスタa,b,c,dの出力の中から1つを選出している。どれを選ぶかはSyが決める。
  2. 1で選ばれたデータが、レジスタaの出力と共にM(マルチプレクサ)に入っている。
  3. 2でどちらが選ばれるかは、右下のデマルチプレクサの出力が決める。
  4. 右下のデマルチプレクサのうちどれが1になるかは、Sxが決める。

つまり

という操作をクロックの立ち上がりのたびに実行している1

機械語ニーモニック

今回のCPUの挙動を具体的に考えたいので、Sx=00,Sy=01の場合を考察する。 一回のクロックで各レジスタがどのように代入されるかを現実に忠実に書き下すと

; 以下の4命令が、1回のクロックで同時実行される
mov a, b
mov b, b
mov c, c
mov d, d

ここで、mov a, bC言語でいうところの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のブロックは回路図のDEMUXMで書かれたあたりの線の繋がりを記述している。 最後の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を繋げて光らせれば良い。 この辺をうまくやるには制約ファイル等の説明が必要なのだが、面倒くさいので割愛。


  1. ところでSxやSyはどこに繋がってるの?という疑問がわくが、本質的な答えはメモリだ。メモリから「機械語」をとってきて、それをCPU内(デコーダーと呼ばれる)で加工し、今回の回路図のSxやSyに渡すのだ。今はまだメモリを説明していないし、ちゃんと話すにはプログラムカウンタの知識もいる。ここでは深く考えないことにしよう。とりあえずは人力で、クロックの度にSxやSyにうまく電圧を印加すると思って欲しい。