コンテンツにスキップ

命令メモリ

命令メモリ

ここまで、データメモリは接続できましたが、4 + 3 - 2 + 1を実現するための処理内容はテストベンチから入力していました。 処理内容を保持するための命令メモリというものを導入して、接続します。 命令メモリの構造自体は、データメモリと同様ですが、役割が異なるため、内容が異なります。 まずは、どういうものを命令としてメモリに保持すると良いかを考えてみます。

処理と命令

テストベンチに書かれた処理内容は、以下の 3 つの信号線への入力でした。 それぞれを下の表にまとめます。 テストベンチの data address は、test_address <= test_address + ADDR_W'h01;という形でインクリメントしていましたが、ここでは実際の値に直しています。

処理 selector write enable data address
1 OP_THROUGH_B DISABLE ADDR_W'h00
2 OP_ADD DISABLE ADDR_W'h01
3 OP_SUB DISABLE ADDR_W'h02
4 OP_ADD DISABLE ADDR_W'h03
5 OP_THROUGH_A ENABLE ADDR_W'h04

この表のように、演算と対象となるデータをひとまとめにしたものを命令と呼び、それぞれを以下のように区別します。

  • 演算を表す部分:オペコード(op-code)。ここでは、selector と write enable
  • 対象となるデータを表す部分:オペランド(operand)。ここでは、data address

上記の表をより人間にわかりやすい形にしてみましょう。 具体的には、selector や write enable の値を直接指定するより、実際にやりたいことをベースに処理に名前を付けてみます。 たとえば、処理 1 で実際にやりたいことは、メモリからのデータのロードでしたので、LDという名前にしてみます。

  • 処理 1 をLDと命名
selector write enable わかりやすい表記(ニーモニック)
OP_THROUGH_B DISABLE LD

このように、処理に人間がわかりやすいような名前を付けたものをニーモニックと呼びます。 また、ニーモニックとデータを合わせたものをアセンブラ表記処理の実際のビットコードを並べたものを機械語と呼びます。

  • テストベンチの処理 1 のアセンブラ表記
命令(ニーモニック) データ
LD 0
  • 機械語(13bit)
selector write enable data address
0010 0 0000 0000

もっとも単純な実装では、機械語をメモリに入れて、順に呼び出すことで処理を実現できます。これで命令メモリの設計は完了です。

プログラムカウンタ

命令メモリの中のどの処理の実行するかを指定するために、プログラムカウンタを使います。 つまり、プログラムカウンタの値に従って、命令メモリから指定されたアドレスの命令が読み出されます。 今回は単に値を1つずつインクリメントするだけなので、命令メモリに格納された命令を上から順に実行していきます。

4 + 3 - 2 + 1をアセンブラ表記と機械語で表記したものを以下の表にまとめます。

処理 アセンブラ表記 機械語
1 LD 0 0010 0 0000 0000
2 ADD 1 0111 0 0000 0001
3 SUB 2 1000 0 0000 0010
4 ADD 3 0111 0 0000 0011
5 ST 4 1001 1 0000 0100

この表の内容を、そのままメモリの内容として保持すれば、命令メモリの完成です。

構成概要

ここまで述べた命令メモリの構成を見ていきます。

命令メモリ自体は単なるメモリなので組み合わせ回路ですが、プログラムカウンタはクロックに同期してインクリメントしなければなりません。 命令メモリの読み出し部分は垂れ流しだといろいろ大変なので、レジスタを取り付けます。 また、`` また、命令メモリに書き込みは行いませんので、データメモリとは異なり、write enable や input 信号は省きます。

次に、命令メモリには selector や write enable の値がごちゃまぜに入っていますので、それぞれを分解する必要があります。 具体的には、メモリから取ってきたデータを以下のように分解し、それぞれを各信号線に接続します。

  • 12~9bit 目 → selector
  • 8bit 目 → write enable
  • 7~0bit 目 → data address

全体としては、以下の図のような回路になります。

なお、命令メモリの中身は、データメモリと同様にファイル imem.datに書き出して利用します。

実装

以下に、命令メモリの実装を示します。全体の挙動は、次の通りです。

  1. クロックに同期して、プログラムカウンタがインクリメントされている
  2. プログラムカウンタの値に従った、imem のアドレスが常に出力されている

命令メモリのモジュール(simple_instruction_memory.v)は、前述のデータメモリよりも簡単な構造になっています。 両者の異なる部分は以下のとおりです。

  • write enable や input 信号がなくなっている
simple_instruction_memory.v 命令メモリのモジュール(未完成)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
`include "parameters.vh"

module simple_instruction_memory(
    input clk, input rst_n,
    output reg [`IMEM_W-1:0] dataout
    );

    reg [`ADDR_W-1:0] program_counter;
    reg [`IMEM_W-1:0] imem [0:`DEPTH-1];

    // read raw data from file (only for simulation)
    initial begin
        $readmemb("imem.dat", imem);
    end

    // プログラムカウンタ
    always @(posedge clk or negedge rst_n)
    begin
        //ここに処理を追加してね
    end

    // プログラムカウンタに従った、命令メモリの読み出し
    always @(posedge clk or negedge rst_n)
    begin
        if(!rst_n) dataout <= 0;
        else
        //ここに処理を追加してね
    end

endmodule

回路合成結果


図:simple_instruction_memory.vのDigitalJS Onlineによる合成結果[^1]。右下の命令メモリimemのアドレス(addr)にプログラムカウンタのレジスタが接続されている。また、プログラムカウンタは常に+1する回路となっている。結果はこちら

テストベンチは以下のようになります。今回は、テストベンチ側でメモリから取ってきたデータを分解しています。 具体的には、17 行目で{}構文を使って命令メモリモジュールの出力をそれぞれの信号線に分解して受け取っています。

initial beginの内部はこれまでのものよりもシンプルな形になっています。 #STEPを使って時間を進めているだけです。 これは、検証するモジュールに入力信号がなく、クロックのみに従って動くためです。 また、毎クロック変化する出力を確認したいので、$monitor文で信号の変化を観測します。

test_simple_instruction_memory.v 命令メモリのモジュールのテストベンチ
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/* test bench */
`timescale 1ns/1ps
`include "parameters.vh"

module testbench_simple_instruction_memory;

    parameter STEP = 10;
    reg clk, rst_n;
    reg [`ADDR_W-1:0] test_address;
    wire [`IMEM_W-1:0] test_dataout;

    wire [3:0] test_selector;
    wire test_write_enable;
    wire [7:0] test_datamemory_address;

    // 命令メモリの出力を、それぞれの信号に分解
    assign {test_selector, test_write_enable, test_datamemory_address} = test_dataout;

    // インスタンス化
    simple_instruction_memory sim_0(.clk(clk), .rst_n(rst_n),
            .dataout(test_dataout)
            );

    // clockの生成
    always #(STEP/2) begin
             clk <= ~clk;
    end

    // メモリを全て表示する
    task dump_mem;
        integer i;
        begin
            $display("---DUMP MEM---");
            for(i = 0; i < `DEPTH; i = i + 1) begin
                $display("%3d, %b", i, sim_0.imem[i]);
            end
            $display("--------------");
        end
    endtask

    initial begin
        $dumpfile("testbench_simple_instruction_memory.vcd");
        $dumpvars(0, testbench_simple_instruction_memory);
        clk <= `DISABLE;
        rst_n <= `ENABLE_N;

        dump_mem();

        #STEP
        rst_n <= `DISABLE_N;
        #STEP
        $monitor("%b, %b, %b", test_selector, test_write_enable, test_datamemory_address);
        #(STEP*5)

        rst_n <= `ENABLE_N;
        #STEP
        $finish;
    end
endmodule
imem.dat 命令メモリの中身
1
2
3
4
5
0010_0_0000_0000 // LD 0
0111_0_0000_0001 // ADD 1
1000_0_0000_0010 // SUB 2
0111_0_0000_0011 // ADD 3
1001_1_0000_0100 // ST 4

ビルド&実行コマンド

iverilog ./testbench_simple_instruction_memory.v ./simple_instruction_memory.v
vvp ./a.out
テストベンチの出力結果
$ iverilog ./testbench_simple_instruction_memory.v ./simple_instruction_memory.v && vvp ./a.out
WARNING: ./simple_instruction_memory.v:14: $readmemb(imem.dat): Not enough words in the file for the    requested range [0:7].
VCD info: dumpfile testbench_simple_instruction_memory.vcd opened for output.
---DUMP MEM---
  0, 0010000000000
  1, 0111000000001
  2, 1000000000010
  3, 0111000000011
  4, 1001100000100
  5, xxxxxxxxxxxxx
  6, xxxxxxxxxxxxx
  7, xxxxxxxxxxxxx
--------------
0010, 0, 00000000
0111, 0, 00000001
1000, 0, 00000010
0111, 0, 00000011
1001, 1, 00000100
./testbench_simple_instruction_memory.v:55: $finish called at 60000 (1ps)
テストベンチの波形

演習

  1. 以下の動作をするような命令列を作り、命令メモリimem.datを作成してみる。なお、dmemはデータメモリのこと。
    1. dmemのアドレス0x0番地を読む
    2. dmemのアドレス0x2番地の値を掛け算する
    3. dmemのアドレス0x4番地の値を引き算する
    4. dmemのアドレス0x6番地の値を足し算する
    5. dmemのアドレス0x8番地に値を格納する
  2. dmemの内容が以下の形であったときの、上記の命令列の計算結果は何か?
    7
    6
    5
    4
    3
    2
    1
    

命令メモリの接続

作成した命令メモリを、ALU_DMEM 回路に接続します。 これで、ここまでに個別に作成した以下の構成要素を接続した簡単なアキュムレータ・マシンが完成です。

  • ALU: alu4.v
  • データパス回路: simple_datapath.v
  • データメモリ: simple_memory.v
  • 命令メモリ: simple_instruction_memory.v

構成概要

全体の構成図は以下のようになり、接続関係は以下の点がポイントです。 こうすることで、命令メモリからの出力に従って、ALU で指定の演算を行ったり、データメモリを読み書きできます。

  • 命令メモリの出力を、ALU_DMEM 回路の 3 つの入力に接続

動作としては、クロックに従って命令メモリが読み出され、それに従って ALU やデータメモリが動く、という形です。 なお、回路全体には、クロック信号以外の外部入力が存在ない自律的な回路です。

実装

構成概要に従って、命令メモリと ALU_DMEM 回路に接続します。

以下のテストベンチで、構成概要と同じになるようにそれぞれを接続しますが、一部未完成です。 演習として、未完成の部分を完成させてみましょう。 なお、命令メモリには、これまでと同様に4 + 3 - 2 + 1をやる命令が入っています。

testbench_accumulator.v アキュムレータのモジュールのテストベンチ(未完成)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
`timescale 1ns/1ps
`include "parameters.vh"

module testbench_accumulator;

    parameter STEP = 10;
    reg clk, rst_n;

    wire [`DATA_W-1:0] alu2dmem;
    wire [`DATA_W-1:0] dmem2alu;
    wire [12:0] imem_dataout;

    wire [12:5] imem2dmem_address;
    wire [3:0] imem2alu_selector;
    wire imem2dmem_write_enable;

    simple_datapath simple_datapath_1(.clk(clk), .rst_n(rst_n),
                                    // ここを完成させてね
                                    );

    simple_memory simple_memory_1(.clk(clk), .rst_n(rst_n),
                                    // ここを完成させてね
                                    );

    simple_instruction_memory simple_instruction_memory_1(.clk(clk), .rst_n(rst_n),
                                    // ここを完成させてね
                                    );

    // clockの生成
    always #(STEP/2) begin
             clk <= ~clk;
    end

    // データメモリを全て表示する
    task dump_dmem;
        integer i;
        begin
            $display("---DUMP DATA MEM---");
            for(i = 0; i < `DEPTH; i = i + 1) begin
                $display("%3d, %h", i, simple_memory_1.dmem[i]);
            end
            $display("-------------------");
        end
    endtask

    // 命令メモリを全て表示する
    task dump_imem;
        integer i;
        begin
            $display("---DUMP Instruction MEM---");
            for(i = 0; i < `DEPTH; i = i + 1) begin
                $display("%3d, %b", i, simple_instruction_memory_1.imem[i]);
            end
            $display("-------------------");
        end
    endtask


    initial begin
        $dumpfile("testbench_accumulator.vcd");
        $dumpvars(0, testbench_accumulator);
        clk <= `DISABLE;
        rst_n <= `ENABLE_N;

        dump_imem();
        dump_dmem();

        #STEP
        rst_n <= `DISABLE_N;
        $display("pc |  sel, we, data addr | alu2dmem, dmem2alu");
        // いい感じに表示させてみてね
        $monitor("%h | %b, %2b, %9b | %8h, %8h",
                            );

        #(STEP*10)
        dump_dmem();
        rst_n <= `ENABLE_N;
        $finish;
    end
endmodule

ビルド&実行コマンド

iverilog alu4.v simple_datapath.v simple_instruction_memory.v simple_memory.v testbench_accumulator.v
vvp ./a.out

演習

  1. 構成概要に従って、テストベンチを完成させる
  2. 計算の途中と結果が正しいかを、テストベンチで確認
  3. 22 + 7 * 6 = 64を実現してみる
    • データメモリには22,7,6の順で値が入っているものとする
  4. 学年 + 組 * 番号をやってみる
    • データメモリには学年,組,番号の順で値が入っているものとする

ここまでで、簡単なアキュムレータ・マシンは完成です。 全体の動作としては、まず、プログラムカウンタに従って上から順番に命令メモリを読み出します。 次に、命令メモリに書かれた処理とデータメモリのアドレスを読み出し、計算を実行しています。