CPUのアーキテクチャと基本設計

コンピュータの心臓部とも言えるCPU (Centrol Processing Unit、 MPU (Micro Processing Unit)とも呼ぶ)は、 メモリ内のプログラムを順に実行するわけですが、 これは、論理回路としてはステートマシン(状態遷移回路)と みなすことができます。 すなわち、命令に応じて次の動作が変わる、さらにその命令を 実行するステップであるメモリからの読み込み(Fetch)や 命令の解釈(Decode)なども、状態の遷移と考えることができます。

ここでは、皆さんが情報システム工学実験第2の「2-4.CPUの構成」で 使ったCPUと似たアーキテクチャのCPUを設計し、 実際にFPGA上で動作させてみましょう。

設計するCPUのアーキテクチャ

アーキテクチャ概要



今回設計するCPUは、上図のようなアーキテクチャとします。 主な仕様は以下の通りです。

命令セットの定義

実行できる命令は、4ビットのオペコード(op)に対して、命令中の即値(imm)を用いて次のように定義します。(実行命令の表記であるニーモニック表記とあわせて示す)
命令(op)ニーモニック表記動作内容
0000 mov imm, r0immをr0に代入
0001 mov imm, r1immをr1に代入
0010 add r0, imm, r0 r0+immをr0に代入
0011 add r0, imm, r1 r0+immをr1に代入
0100 add r1, imm, r0 r1+immをr0に代入
0101 add r1, imm, r1 r1+immをr1に代入
0110 jmp immimm番地へジャンプ(無条件分岐)
0111 jz immZフラグ=1ならばimm番地へ分岐、それ以外は次の命令へ
例えば、メモリから読み出した命令idataと命令のニーモニック表記、実行内容との対応は以下の通りとなります。 これらの命令は命令メモリ(Instruction Memory; imem)に格納されているとします。 (このimem内の命令の並びがプログラム、ということになります) 現在実行している命令が入っているアドレス(iaddr)は、 プログラムカウンタ(pc)の値、とします。

命令メモリは、アドレスiaddrを指定するとその番地に入っている 命令をidataとして返す回路、とみることができますので、 CPUが行うべき動作としては、idataの値に応じて レジスタに書き込んだり分岐したり、といった動作を行う、ことになります。

またレジスタr0, r1やpcの値などを外部から観測できるようにしたほうが 動作確認には便利ですので、7セグメントLED(4桁)や8個のLEDに 必要なものを表示させることにします。 なお全体の動作はクロック信号clkにあわせて行うとし、 このclkはスイッチSW[0]で作成することにします。 このクロックと7セグメントLED表示は、ライブラリとして peripheral.vを用意しておきますので、これを使います。

命令の実行サイクル

1つの命令を実行するのに、次の2ステップで実行する、と定義します。 (うまくやると1ステップで実行できるのですが、 今後の拡張のために2ステップ実行としておきます) なおstは、各ステップの状態を区別する変数とします。

CPUのHDL設計

CPUのHDL記述のテンプレート

プロジェクトのテンプレートsample_cpu.zipをダウンロードして展開しておきます。 このトップ階層のsample.vは次のようになっています。
module sample(CLK6, LED, SW, SG, SA);
    input CLK6;
    input [3:0] SW;
    output [7:0] LED, SG;
    output [3:0] SA;

    reg [3:0] r0, r1, pc; // register (r0/r1) & pc
    wire [3:0] iaddr, op, imm; // iaddr, op, imm
    wire [7:0] idata; // idata
    wire [3:0] d3, d2, d1, d0;
    reg st, z;
    wire clk, rst;
    
    imem i0(iaddr, idata); // instruction memory
    sw_clk iclk(CLK6, ~SW[0], clk); // clock
    seg7 iseg7(CLK6, d3, d2, d1, d0, SG, SA); // 7segment LED display

    assign rst = ~SW[3]; // SW[3]:reset
    assign op = idata[7:4], imm = idata[3:0];

    assign iaddr = pc;

    // LED display assignment
    assign LED = {z, st, clk, 1'b0, op};

    // 7segment display assignment
    assign d3 = 4'd0, d2 = iaddr, d1 = r1, d0 = r0;

    always @(posedge clk or posedge rst) begin
        if (rst == 1'b1) begin
            // reset
            pc <= 4'h0; st <= 1'b0; z <= 1'b0; r0 <= 4'h0; r1 <= 4'h0;
        end
        else begin
        // CPU state machine
        // ......
        end
    end
endmodule
ここでクロック生成のsw_clkと7セグメントLED制御の7segは、 peripheral.vの中で記述されていて、ここではそれを インスタンス呼び出しして利用しています。 (興味がある人はperipheral.vの中身をのぞいてみてください)

seg7は、4ビットのd0〜d3に与えられる値を各桁の7セグメントLEDに 自動的に表示します。 (d0が最下位(右端)、d3が最上位(左端)。値は0〜9/A〜Fの16進表記)

演習2-1 このsample.vで、7セグメントLEDの各桁と8個のLEDに表示される情報が 何になるかを理解しましょう。 また各インスタンス間の接続や使われている信号を理解しましょう。 (例えば命令opや即値immの作り方など)

命令メモリ

命令メモリは、与えられたアドレスiaddrに対して、 その番地のメモリの内容をidataに出力する回路、と考えることができます。 これは、見方を変えると、入力iaddrに応じて出力idataが決まる回路、 ということですので、組み合わせ論理回路と考えることができます。 すなわち、前回の2-4デコーダと同様に記述することができるでしょう。 すなわち概略は以下の通りになります。
// Instruction Memory
module imem(addr, data);
    input [3:0] addr;
    output [7:0] data;
    reg [7:0] data;
    always @(addr) begin
        case (addr)
            ....
        endcase
     end
endmodule

演習2-2 プロジェクトに新しいファイルimem.vをFile→New→(Verilogファイル)で追加し、 以下のようなプログラムを格納する命令メモリimemのVerilogHDL記述を 追加してみましょう。 またその動作をシミュレーションで確認しておきましょう。
番地(iaddr) 命令(idata) 命令のニーモニック表記
0 0000 0001 mov 1, r0
1 0001 0010 mov 2, r1
2 0110 0010 jmp 2
シミュレーションの行い方は、基本的には前回と同様ですが、今回与える信号はiaddrで4ビットですので、信号を加える(Insert)ときに、"BusWidth"(バス幅=何ビットかということ)を「4」としておけば、4ビットの値(10進数で0〜15)として信号を与えることができます。

※この場合のように、最上位階層の回路(この場合はsample)の中にある回路に対してシミュレーションを行うには、一時的にシミュレーションを行いたい回路を以下の手順で最上位階層(Top Level)にする必要があります。(シミュレーションが終わったら、本来の最上位階層のsampleを最上位階層に戻す)

また複数の回路に対して、別々の入力信号波形を使いたい場合は、以下のように個別に指定することができます。

↑最上位階層にしたい回路(この場合はimem)を選んで右クリック→"Set as Top-Level Entity"(これでこのimemが最上位階層になる)
この回路に対して、最初に作った入力信号波形(*.vwf)とは別の入力信号波形を与えてシミュレーションを行いたい場合は、その回路を右クリック→"Settings"から、カテゴリー"Simulator Settings"で"Simulation input"のところで使いたい入力信号波形ファイル(*.vwf)を指定すればよい。

※なお入力信号波形ファイルで信号を与える入力信号を入力するとき、信号の種類(Type)が"Input"以外(たとえばBuried)となってしまうことがあるようです。その場合は、信号名を右クリック→Propertyから、"Type"を"Input"に指定しておいてください。
"Input"
"Buried"

以下は、戻すときの手順。

↑戻すときは、まず現在の最上位階層のimemを選んで右クリック→"Settings"

↑Top-level entityのところに現在のimemが表示されているので、その右の"..."ボタンを押す

↑最上位階層に変更する回路を選ぶ画面になるので、本来の最上位階層であるsamplpeを選んでOKを押す

↑最初の階層構造である、最上位階層=sample、その中にimem等、の構造となる

CPUのHDL記述

さきほどのsample_cpu内のsample.vの、 最後の方のCPU state machine部分に、命令を実行する回路を記述すれば CPUとして完成します。 各命令の実行は、st=0状態とst=1状態を交互に繰り返すことになり、 各状態では次のような動作を行うことになります。 つまり、movやaddは「st=0でのレジスタへの代入」として、 分岐命令は「st=1でのpcへの代入」として記述することができることになります。

演習2-3 CPUの記述を完成させ、imemとあわせてインプリメンテーションし、 シミュレーションでの検証や実機での動作を確認してみましょう。

※(補足)CPU全体のシミュレーションを行うときには、クロック生成用の回路sw_clkにクロック信号(この実験で使っているFPGAボードでは6MHz)を与える必要があります。 (本来は7セグメントLED表示回路seg7にも必要)これは、sw_clkが「スイッチを押してから一定時間だけ出力clk=1とする」という動作のためにクロック信号CLK6を使っているためです。(詳しくはperipheral.v内のsw_clkの記述を参照) そのためシミュレーションでもこのCLK6を入力として与える必要がありますが、sw_clkの動作を見るとわかるように、CLK6の90万サイクル分だけclk=1とする、という動作のため、これをシミュレーションで検証するのは効率がよくありませんし現実的ではありません。

そこで、シミュレーション用にperipheral.v内に書いてあるsw_clkの記述をコメントアウトした上で次のように書き換え、CLK6を使わずにスイッチからクロック信号clkを作るようにしたうえでシミュレーションを行うとよいでしょう。

module sw_clk(CLK6, a, q);
  input CLK6, a;
  output q;
  assign q = a;
endmodule
もちろんシミュレーションが終わってFPGAボードに書き込むときには、sw_clkを戻すのをお忘れなく。

※(補足)CPU動作のシミュレーションでは、CPUを記述しているsampleの出力がLEDとSGとSA(いずれもLEDや7セグメントLEDの信号)のため、確認がしにくいです。(特にSAとSGはseg7によってダイナミック駆動される信号のため、どのような出力が出ているのか、がわかりにくい)
そこで、一時的に、シミュレーションで観測したい信号をsampleの出力として定義してシミュレーションを行う、という方法が便利です。 たとえば信号d0を観測するならば、以下のように記述します。

//module sample(CLK6, LED, SW, SG, SA);   // comment out
module sample(CLK6, LED, SW, SG, SA, d0); // added
    output [3:0] d0;                      // added
もちろんシミュレーションが終わってFPGAボードに書き込むときには、sw_clkを戻すのをお忘れなく。

いろいろなプログラムの実行

ここで設計したCPUは、命令が8種類しかありませんが、 これでもいろいろなプログラムを実行できます。 例えば次のようなプログラムを与えてみます。
番地(iaddr) 命令(idata) 命令のニーモニック表記
00000 0000mov 0, r0
10001 0011mov 3, r1
20010 0100add r0, 4, r0
30101 1111add r1, 15, r1
40111 0110jz 6
50110 0010jmp 2
60110 0110jmp 6

演習2-4 上記のプログラムの動作を理解し、imemの内容をこれにあわせて変更して プログラムを実行させ、結果を確認してみましょう。

※ヒント: 3番地の"add r1, 15, r1"は、「r1+15→r1」だが、 r1は4ビットで桁あふれは無視する。 例えばr1=3でこの命令を実行すると、r1+15=18(2進数で1 0010)だが 桁あふれは無視するのでr1=2となる。すなわち-1(減算)を行っていることになる。

演習2-5 これらの命令を使って独自プログラムを作成し、その動作を確認してみましょう。


戻る