CPUの拡張

今回は、前回設計したCPUをいろいろ拡張してみましょう。

拡張1: データメモリ

まずは拡張として、演算結果などを保存するデータメモリを追加してみましょう。

アーキテクチャ概要



データメモリdmemは、命令が入っている命令メモリimemとば独立したメモリとし、 アドレスバス、データバスも別々とします。(いわゆるハーバード・アーキテクチャ) データメモリdmemとの信号線は以下の通りとします。 つまりdmemの動作は、大きく分けて次の2つとなります。 なおd2ioは、CPUからdmemへ向かう信号ですので、CPUにとっては出力、dmemにとっては入力となります。そのためこれがつながるCPU側はdout、dmem側はdinと名前をつけることにします。 同様にd2cpuは、CPU側がdin、dmem側がdoutとなります。

データメモリdmemのHDL設計

データメモリのHDL記述は、おおまかには次のようになります。
// Data Memory
module dmem(clk, wr, addr, din, dout);
    input clk, wr;
    input [3:0] addr, din;
    output [3:0] dout;
    reg [3:0] mem[0:15];
    
    always @(posedge clk) begin
        // operation
        // ....
    end
    assign dout = .....;
endmodule
ここでポイントは、5行目のメモリの実体である変数memの宣言です。 「reg [3:0]」とすることで、4ビット幅の変数(reg型)が宣言できますが、 これを「mem[0:15]」とすることで、15個分の配列として宣言しています。 すなわちmemは4ビット×15個の配列、ということになります。 ちなみにmem[0]は0番地のデータ4ビット、mem[0][3]は その0番地のデータの最上位ビット、となります。 これは次のような二次元配列と考えるとわかりやすいでしょう。
0番地mem[0][3]mem[0][2]mem[0][1]mem[0][0]←4ビットまとめてmem[0]
1番地mem[1][3]mem[1][2]mem[1][1]mem[1][0]←4ビットまとめてmem[1]
..................
15番地mem[15][3]mem[15][2]mem[15][1]mem[15][0]←4ビットまとめてmem[15]
演習3-1 データメモリdmemを設計し、その動作をシミュレーションで検証してみましょう。

CPUの命令の拡張:データメモリアクセス

追加したデータメモリを使うために、CPUの命令も拡張しておきましょう。 追加する命令はデータメモリへの読み書きということになりますが、 アクセスするデータメモリのアドレスは即値immで命令中に与えることにします。 またそのimmが示すアドレスのデータメモリの内容は「@imm」と表記することにします。 ここでは、以下のように命令を追加することにします。 (太字が追加した4命令)
命令(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番地へ分岐、それ以外は次の命令へ
1000mov r0, @immr0の内容をデータメモリのimm番地へ書き込み
1001mov r1, @immr1の内容をデータメモリのimm番地へ書き込み
1010mov @imm, r0データメモリのimm番地の内容をr0へ代入
1011mov @imm, r1データメモリのimm番地の内容をr1へ代入
前回設計した拡張前のCPUを参考に、これらの拡張を行うと 次のようになるかと思います。まずは一通り読んで概略を理解しましょう。
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, daddr, d2io;
    wire [3:0] iaddr, d2cpu, op, imm;
    wire [7:0] idata;
    wire [3:0] d3, d2, d1, d0;
    reg mem_wr, st, z;
    wire clk, rst;
    
    imem i0(iaddr, idata);
    dmem i1(clk, mem_wr, daddr, d2io, d2cpu);
    sw_clk iclk(CLK6, ~SW[0], clk);
    seg7 iseg7(CLK6, d3, d2, d1, d0, SG, SA);

    assign LED = {z, st, clk, 1'b0, op};
    assign d0 = r0, d1 = r1, d2 = iaddr, d3 = daddr;
    assign rst = ~SW[3];
    assign iaddr = pc;
    assign op = idata[7:4], imm = idata[3:0];
    always @(posedge clk or posedge rst) begin
        if (rst == 1'b1) begin
            pc <= 4'h0; st <= 1'b0; z <= 1'b0; mem_wr <= 1'b0; daddr <= 4'h0; r0 <= 4'h0; r1 <= 4'h0;
        end
        else begin
            if (st == 0) begin
                st <= 1'b1;
                mem_wr <= 1'b0;
                case (op)
                    // operation
                    // resister access, mem_wr setf for op=1000/1001
                    ....
                endcase

            end
            else begin
                st <= 1'b0;
                // memory read for op=1010/1011
                case (op)
                    ...
                endcase

                   // update pc
                if (op == 4'b0110) ...
            end
        end
    end
endmodule

演習3-2 sample_cpu2.zipをダウンロードして展開し、 この中のsample.vを参考に、これらの命令を拡張したCPUを 設計してみましょう。 データメモリdmemは、前回と同様に新しいVerilogHDLファイルmem.vを プロジェクトに追加し、この中に命令メモリimemの記述とともに 記述するとよいでしょう。 またこれに以下のようなプログラム(もっと長いプログラムでもよい)を 命令メモリimem内に記述して、これを実行させてみましょう。 (このプログラムでは、最終的にr1=1となることになる)
番地(iaddr) 命令(idata) 命令のニーモニック表記
0 0000 0001 mov 1, r0
1 1000 0011 mov r0, @3
2 1011 0011 mov @3, r1
3 0110 0011 jmp 3

拡張2: メモリマップドI/O



※この図は2か所、接続に間違いがあります。それがどこかを探してみましょう。

データメモリの一部を、LEDなどのI/Oデバイスのように扱うことを 「メモリマップド(Memory-mapped I/O)」と呼びます。 ここでは、データメモリの特定の番地にデータを書き込むと、 その値が直接LEDに表示されるような回路dioを設計してみましょう。

といってもメモリマップドI/Oの出力側(CPUから書き込まれる側)は、 基本的にはデータメモリの書き込み部分と全く同じです。 例えば次のような回路をdio.vとしてプロジェクトに追加しておきます。

module dio(clk, wr, addr, din, d_led);
    input clk, wr;
    input [3:0] addr, din;
    output [3:0] d_led;
    reg [3:0] d_led;
    
    always @(posedge clk) begin
        if (wr == 1'b1) if (addr == 4'hf) d_led <= din;
    end
endmodule
中を読むとわかるとおり、addr=4'hf (2進数で1111、10進数で15)番地への 「書き込み」に対しては、その書き込んだデータを 出力d_ledに出力する(そしてその値を保持する)、という動作をするような 記述になっています。 CPU本体のほうでは、次のようにこの回路dioを呼び出しておき、 アドレス、データにはdmemと同様に、アドレスdaddrと CPUからdmemへ向かうデータと同じd2ioをつないでおきます。 出力d_ledは、wireとして宣言をしておきましょう。
    wire [3:0] d_led;
    dio  i2(clk, mem_wr, daddr, d2io, d_led);
この出力d_ledは、7セグメントLEDやLEDのあいているところに assignでつないでおいて表示させることにします。

演習3-3 メモリマップドI/Oの回路を設計し、CPUとあわせて動作させてみましょう。 その動作を確認できる適当なプログラムをimemに格納させること。 またメモリマップドI/Oを拡張し、割り当て番地を変更したり 複数の書き込みデバイスや読み出しデバイスを設計して動作させてみましょう。

※ヒント:読み出しデバイスは、データメモリの特定のアドレスに 対する読み出しの際に、入力信号(例えばスイッチの値)を 読み出しデータd2cpuに返すようにすればよい。
ただしデータメモリdmemからCPUへ向かうデータd2cpuと、 このdioからCPUへ向かうデータd2cpuは同一の信号線であるため、 dmemとdioの両方がd2ioにデータを「出力」しようとすると、 「競合」(CPUへの読み出しデータ信号d2cpuに、データメモリdmemとこの読み出しデバイスの 2つが同時に値を出そうとすると、インプリメンテーション時にエラーが起こるか、 動作させられても最悪の場合故障する)することになる。 そのため、データメモリのd2cpuへの値の出力は アドレスdaddrがデータメモリ対象の値のときだけ、とし、 それ以外のときは次のように「接続していない(高インピーダンスZ)」という値を 出力するようにする。

  assign dout = (addr == 5)?(4'bZZZZ):(mem[addr]);
この例では、C言語での「?:」演算子と同様に、 addr=5の場合は4ビットのZ(高インピーダンス)を、 それ以外の場合はデータメモリの内容をd2cpuへ出力するようにしている。 高インピーダンスの意味は、次の図のように考えるとよい。

この例では、dmem側で、addr=5のときはd2cpuをZとするため、 このときだけはdio側でd2cpuへ値を出力してもよいことになる。 すなわちdio側では以下のように記述すればよいことになる。 (「....」の部分はdioからCPUへ渡したい値。例えばスイッチの値など)
  assign dout = (addr == 5)?(....):(4'bZZZZ);

戻る