データメモリ
データの保持とメモリ
シンプル・データパス回路では、22 + 7 * 6 = 64
のような、算術の順番を考慮した計算がそのままではできません。これは、7 * 6
の部分の途中結果を保持する場所がないためです。また、計算の順番も保持したり考慮できないため、テストベンチの方でそれを考慮しなければならないので、実用的ではありません。
保持すべきデータ
このためには、どうにかして計算の順番と計算データ、中間データに加え、それらの場所を保持する必要があります。具体的には、以下の 2 種類を保持する必要があります。
- 計算データ自体。途中結果。出力結果
- 演算子。中間結果の場所
これらをメモリを利用して解決してみましょう。
データや演算子はそのままメモリに保持する形で問題ありませんが、
中間結果の場所は、そのデータが保持されているアドレスを保持します。
なお、演算の順番は、演算子を順番に処理する形にします。
上記の 2 つを保持するために、以下の 2 種類のメモリを導入すると良さそうです。
- データメモリ:計算データ自体。途中結果。出力結果
- 命令メモリ:演算子。中間結果の場所
まずは、一般的な読み書き可能なメモリを設計・実装していきましょう。
メモリ (RAM) の構成概要
メモリ自体は単純な表のようなもので、データを保持するとともに、データの場所を表すアドレスがあります。メモリには幅 (width) と深さ (depth) があり、それぞれが以下の役割を持ちます。
- メモリの幅 :1 つのデータの大きさ。16bit とか
- メモリの深さ:保持できるデータの量。256 個とか
- メモリの容量:幅 x 深さ。16bit * 256 個 = 4096bit
- メモリのアドレス:表の番地
読み書きを行う単純なメモリの構成図を以下に示します。
これは、任意のアドレスに読み書きが可能なランダムアクセス・メモリ(RAM: Random Access Memory)になります。
clock
に加えて、write enable
という確実に書き込みを行う信号線がついています。
使い方は以下のようになります。回路全体は、クロックに同期して読み書きが行われます。
読み出した値は次のサイクルで出てくるので、注意が必要です。
- 読み出し:
data address
を指定すると、output
が読み出せます。(垂れ流し形式)
- 書き込み:
input
とdata address
を指定して、write enable
をアサートすると、目的のアドレスに入力値が書き込まれます。
データメモリの実装
ここでは、RAM の単純な実装とテストベンチを示します。
このテストベンチでは、アドレスとデータを指定してwrite_mem
関数でデータの書き込みを検証します。
write_mem
関数は、前述の使い方に従って実際の書き込みを行います。
メモリは、テストベンチではdmem.dat
というファイルで表現されます。
メモリの中身を独立のファイルとして別に記述し、$readmemh
という関数で読み込みます。
今回は、dmem.dat
というファイルに初期値を書いています。
なお、メモリをテストベンチに含まないのは、メモリは論理回路とは異なる SRAM などの特別な回路を使うため、論理合成の対象としないからです。
Verilog HDL によるメモリの実装は以下になりますが、一部未完成です。
演習として、未完成の部分を完成させてみましょう。
simple_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 | `include "parameters.vh"
module simple_memory(
input clk, input rst_n,
input [`DATA_W-1:0] datain,
input [`ADDR_W-1:0] address,
input write_enable,
output wire [`DATA_W-1:0] dataout
);
// main body of memory
reg [`DATA_W-1:0] dmem [0:`DEPTH-1];
// read raw data from file (only for simulation)
initial begin
$readmemh("dmem.dat", dmem);
end
// 書き込み処理
always @(posedge clk)
begin
// ここに処理を書いてね
end
// 読み出し処理(dmemのaddressをdataoutに垂れ流し)
// ここに処理を書いてね
endmodule
|
testbench_simple_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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120 | /* test bench */
`timescale 1ns/1ps
`include "parameters.vh"
module testbench_simple_memory;
parameter STEP = 10;
reg clk, rst_n;
reg [`DATA_W-1:0] test_datain;
reg [`ADDR_W-1:0] test_address;
wire [`DATA_W-1:0] test_dataout;
reg test_write_enable;
integer i;
simple_memory sm_0(.clk(clk), .rst_n(rst_n),
.datain(test_datain),
.address(test_address),
.write_enable(test_write_enable),
.dataout(test_dataout)
);
// clockの生成
always #(STEP/2) begin
clk <= ~clk;
end
// メモリの中身を全て表示する
task dump_mem;
integer i;
begin
$display("-----");
for(i = 0; i < `DEPTH; i = i + 1) begin
$display("%3d, %h", i, sm_0.dmem[i]);
end
$display("-----");
end
endtask
// メモリ書き込み
// Usage: write_mem(`ENABLE, `ADDR_W'h01, `DATA_W'h8);
task write_mem;
input set_write_enable;
input [`ADDR_W-1:0] set_address;
input [`DATA_W-1:0] set_datain;
begin
// ここに処理を書いてね
end
endtask
// メモリ読み出し
// Usage: read_mem(`ADDR_W'h01, test_dataout);
task read_mem;
input [`ADDR_W-1:0] set_address;
input [`DATA_W-1:0] set_datain;
begin
// ここに処理を書いてね
end
endtask
initial begin
$dumpfile("testbench_simple_memory.vcd");
$dumpvars(0, testbench_simple_memory);
clk <= `DISABLE;
#(STEP/2)
rst_n <= `ENABLE_N;
$display("Initial data in memory");
dump_mem();
#STEP
rst_n <= `DISABLE_N;
// TEST write_mem
$display("TEST write_mem");
#STEP
write_mem(`ENABLE, `ADDR_W'h01, `DATA_W'h11);
#STEP
test_write_enable <= `DISABLE;
dump_mem();
#STEP
write_mem(`ENABLE, `ADDR_W'h02, `DATA_W'h12);
#STEP
test_write_enable <= `DISABLE;
#STEP
write_mem(`ENABLE, `ADDR_W'h03, `DATA_W'h13);
#STEP
test_write_enable <= `DISABLE;
dump_mem();
// TEST read_mem
$display("TEST read_mem");
$display("-----");
#STEP
read_mem(`ADDR_W'h00);
#STEP
$display("%3d, %h", test_address, test_dataout);
#STEP
read_mem(`ADDR_W'h01);
#STEP
$display("%3d, %h", test_address, test_dataout);
#STEP
read_mem(`ADDR_W'h02);
#STEP
$display("%3d, %h", test_address, test_dataout);
#STEP
read_mem(`ADDR_W'h03);
#STEP
$display("%3d, %h", test_address, test_dataout);
#(STEP*1)
rst_n <= `ENABLE_N;
#(STEP*2)
$finish;
end
endmodule
|
dmem.dat
メモリの中身
回路合成結果
図:simple_memory.v
のDigitalJS Onlineによる合成結果。右下あたりのdmem
がデータメモリ。結果はこちら
演習
- 構成概要に書かれた使い方に従って、実装とテストベンチを完成させる
- データが読み書きされているかを、テストベンチで確認
また、想定されるテストベンチの出力結果と波形は以下になります。(回答例的なやつ)
想定される、テストベンチの出力結果
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 | $ iverilog testbench_simple_memory.v simple_memory.v && ./a.out
WARNING: simple_memory.v:16: $readmemh(dmem.dat): Not enough words in the file for the requested range [0:7].
VCD info: dumpfile testbench_simple_memory.vcd opened for output.
Initial data in memory
-----
0, 0004
1, 0003
2, 0002
3, 0001
4, xxxx
5, xxxx
6, xxxx
7, xxxx
-----
TEST write_mem
-----
0, 0004
1, 0011
2, 0002
3, 0001
4, xxxx
5, xxxx
6, xxxx
7, xxxx
-----
-----
0, 0004
1, 0011
2, 0012
3, 0013
4, xxxx
5, xxxx
6, xxxx
7, xxxx
-----
TEST read_mem
-----
0, 0004
1, 0011
2, 0012
3, 0013
./testbench_simple_memory.v:122: $finish called at 185000 (1ps)
|
想定される、テストベンチの波形
細かい話
本講義資料のデータメモリーは、出力部分が単なる配線(wire)でレイテンシが0で出てくる形になっており、SRAMのような挙動をしています(仮にSRAM挙動と呼びます)。
データメモリーの出力部分にレジスタ(reg)を挟んで値をラッチした、DRAMのような挙動にすることも一般的です(仮にDRAM挙動と呼びます)。
DRAM挙動の場合は、レイテンシがあるためタイミングに注意が必要です。
本講義では内容が複雑になりすぎるため、タイミングに関する話にはあまり言及しません。
そのため、今回のデータメモリはSRAM挙動のものを利用しています。
また、DRAM挙動では講義の後半の方で全体を接続した場合には、命令メモリ(imem)のselectorとのタイミングの調整も必要になります。
具体的には、SRAM形式のdmemを利用するとレイテンシ1でALUに値が伝搬しますが、selectorはimemからレイテンシ0でALUに伝搬します。
このタイミングのずれを調整するために、命令メモリのselectorにレジスタを1つ挟んで、タイミング調整をしなければならなくなります。
なお、dmemの出口部分にレジスタを挟んだ場合は、タイミング調整のために、selectorの方にもレジスタを挟まなければなりません。
レジスタの場所は、dmemの黄色い四角の中に置くか、もしくは黄色と緑の四角の間に置くかが考えられますが、一般的にはどちらでもよく、設計者の判断となります。
データメモリの接続
それでは、作成したデータメモリを ALU(シンプル・データパス回路)に取り付けてみましょう。
接続したものをALU_DMEM 回路と呼ぶことにします。
構成概要
全体の構成図は以下のようになり、接続関係は以下の 2 つがポイントです。
- シンプル・データパス回路の
input_b
に、メモリのoutput
を接続
- シンプル・データパス回路の
result
に、メモリのinput
を接続
また、データメモリからデータを ALU に入力する処理をロード、ALU からデータメモリにデータを入力する処理をストアと言います。
Accumulator の結果が、ALU のA
とメモリのinput
の両方につながっていることにも注意してください。一見、ALU から出てきた結果がすべてメモリに書き込まれてしまうように見えます。実際には、write enable
信号がアサートされたときにだけ結果がストアされるので、問題はありません。
実装
以下のテストベンチで、alu4.v
とsimple_datapath.v
を接続しますが、一部未完成です。 演習として、未完成の部分を完成させてみましょう。
なお、まだデータを保持するデータメモリしか作成していですので、命令と Write Enable 信号はテストベンチから直接入力します。
テストベンチの検証内容(initial begin
のブロック)は以下のとおりです。
4 + 3 - 2 + 1
をやりたい
- データメモリからアドレスを指定してデータをロード
- テストベンチから入力された命令を ALU で実行
- ALU が実行する命令には、ロードしたデータと Accumulator の値を用いる
- 最後に結果をデータメモリにストア
testbench_alu_dmem.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 | `timescale 1ns/1ps
`include "parameters.vh"
module testbench_alu_dmem;
parameter STEP = 10;
reg clk, rst_n;
reg [`SEL_W-1:0] test_sel;
reg [`ADDR_W-1:0] test_address;
reg test_write_enable;
// ここに必要なwire/regを宣言してね
simple_datapath simple_datapath_1(.clk(clk), .rst_n(rst_n),
// ここを完成させてね
);
simple_memory simple_memory_1(.clk(clk), .rst_n(rst_n),
// ここを完成させてね
);
initial begin
$dumpfile("testbench_alu_dmem.vcd");
$dumpvars(0, testbench_alu_dmem);
clk <= 0;
rst_n <= 0;
#STEP
rst_n <= `DISABLE_N;
test_write_enable <= `DISABLE;
#STEP clk <= 1;
test_sel <= `OP_THROUGH_B;
test_address <= `ADDR_W'h00;
#STEP clk <= 0;
$display("test_sel:%b, dataout", test_sel, alu2dmem, simple_memory_1.dmem[test_address]);
#STEP clk <= 1;
test_sel <= `OP_ADD;
test_address <= test_address + `ADDR_W'h01;
#STEP clk <= 0;
$display("test_sel:%b, dataout", test_sel, alu2dmem, simple_memory_1.dmem[test_address]);
#STEP clk <= 1;
test_sel <= `OP_SUB;
test_address <= test_address + `ADDR_W'h01;
#STEP clk <= 0;
$display("test_sel:%b, dataout", test_sel, alu2dmem, simple_memory_1.dmem[test_address]);
#STEP clk <= 1;
test_sel <= `OP_ADD;
test_address <= test_address + `ADDR_W'h01;
#STEP clk <= 0;
$display("test_sel:%b, dataout", test_sel, alu2dmem, simple_memory_1.dmem[test_address]);
#STEP clk <= 1;
test_sel <= `OP_THROUGH_A;
test_write_enable <= `ENABLE;
test_address <= test_address + `ADDR_W'h01;
#STEP clk <= 0;
#STEP clk <= 1;
#STEP clk <= 0;
$display("dmem[%h] = %h", test_address, simple_memory_1.dmem[test_address]);
#(STEP*3)
$finish;
end
endmodule
|
ビルド&実行コマンド
コマンドは以下のとおりで、これまでに作成した alu4.v
、dmem.dat
、simple_datapath.v
、simple_memory.v
、parameters.vh
は、再利用できます。
iverilog ./simple_datapath.v simple_memory.v testbench_alu_dmem.v ./alu4.v
vvp ./a.out
gtkwave testbench_alu_dmem.vcd
演習
- 構成概要に従って、テストベンチを完成させる
- データが読み書きされているかを、テストベンチで確認
22 + 7 * 6 = 64
を実現してみる(22->7->6の順番でdmem.dat
に値を入れること。テストベンチのtest_sel
やtest_address
をいじって実現すること)
学年 + 組 * 番号
をやってみる
また、想定されるテストベンチの出力結果と波形は以下になります。(回答例的なやつ)
想定される、テストベンチの出力結果
| WARNING: simple_memory.v:15: $readmemh: Standard inconsistency, following 1364-2005.
WARNING: simple_memory.v:15: $readmemh(dmem.dat): Not enough words in the file for the requested range [0:7].
VCD info: dumpfile testbench_alu_dmem.vcd opened for output.
test_sel:0001, dataout x 4
test_sel:0110, dataout 4 3
test_sel:0111, dataout 7 2
test_sel:0110, dataout 5 1
dmem[04] = 0006
|
想定される、テストベンチの波形