オール・トランジスタ4ビットCPUの製作とFPGA開発
[Vol.5 ステート・マシンと命令デコーダの設計]

ALU,レジスタ,I/Oなどをトランジスタ・レベルで手作りし,さらにFPGAにも実装

著者:別府 伸耕(リニア・テック) / 企画:ZEPエンジニアリング /


【Index】

Vol.1 ノイマン型CPUの設計

Vol.2 CPUのレジスタとI/Oの設計

Vol.3 Lチカで学ぶFPGA開発体験

Vol.4 CPUのROM,PC,ALUの設計

Vol.5 ステート・マシンと命令デコーダの設計

Vol.6 CPUの全体統合とプログラムの実行

オール・トランジスタ1738石!CPU組み立てキット(ロボット用パーツ付き) CPU1738好評発売中

ステート・マシンの設計

ステート・マシンの役割

CPUが1つの命令を実行する一連の流れを「命令サイクル」(instruction cycle)あるいは「マシン・サイクル」(machine cycle)といいます.これから設計する「ステート・マシン」は,命令サイクルのステップごとにCPUの動作を切り替えて全体の流れを制御する役割を果たします.

CPUの命令サイクル

一般的なCPUの命令サイクルは,図1に示す4つのステップから構成されます.最初の「命令フェッチ」(instruction fetch)でROMから命令を読み込みます.次の「命令デコード」(instruction decode)では読み込んだ命令を解読(デコード)し,CPU内部の信号経路を切り替えます.続く「命令実行」(instruction execute)で命令を実行します.これは,ALUによる演算などが該当します.最後の「ライト・バック」(write back)で演算結果をレジスタやメモリに書き込みます.

なお,命令の種類によっては「命令実行」と「ライト・バック」の区別が難しい場合もあります(これから作るCPUのデータ転送命令など).また,1つ目の命令を実行しながら2つ目の命令を読み込むことで処理時間の短縮を狙う「パイプライン」(pipeline)という手法もあります.いずれにしても,CPU動作の基本は上記の4ステップとなります.

図1 一般的なCPUの命令サイクル

これから作るCPUの命令サイクル

今回のCPUでは,回路を簡単にするためにROMが出力するデータをレジスタなどに保存せず,そのまま命令デコーダに接続する設計にしています.そのため,「命令フェッチ」と「命令デコード」が同時に行われます.また,命令デコードと同時にALUに対する制御信号入力やデータ入力が行われるので,ALUによる「命令実行」も同時に行われます.その結果として,図2に示すように命令をROMから読み出したタイミングで「フェッチ」,「デコード」,「実行」が一度に行われることになります.これに続くクロックでレジスタに演算結果を書き込む「ライト・バック」が行われ,1連の命令サイクルが終了します.

これ以降は,CPUが「フェッチ」,「デコード」,「実行」を行っている状態を「状態0」と呼び,「ライト・バック」を行っている状態を「状態1」と呼ぶことにします.

図2 今回作るCPUの命令サイクル

ステート・マシンの動作:ジャンプなし命令

ステート・マシンの仕様を具体的に検討します.

これから作るCPUの命令は,PC(プログラム・カウンタ)が出力するアドレスを指定した値に上書きする「ジャンプ命令」(“JUMP”,“JNC”,“JNZ”)と,それ以外の「ジャンプなし命令」に分けられます.まずは,「ジャンプなし命令」を実行するときの挙動について考えます.

ジャンプなし命令を実行中のPCとIDの挙動

図3において,PCの出力“$Q$”はROMのアドレス端子に接続される信号です.

また,命令デコーダ(ID)の“$PC\_LD$”出力(後述)はPCの“$LD$”端子に接続される信号で,“$PC\_LD = 1$”のときだけPCに対する上書き操作が許可されます.ここでは「ジャンプなし命令」を考えているので,図3では常に“$PC\_LD = 0$”となっています.

ステート・マシンの“$PC\_UP$”出力および“$LD\_EN$”出力

図3におけるステート・マシンの“$PC\_UP$”出力は,PCの“$UP$”端子に接続される信号です.“$PC\_UP = 1$”のときはPCがアドレス出力を“$+1$”(インクリメント)します.また,ステート・マシンの“$LD\_EN$”出力はIDの“$LD\_EN$”端子(後述)に接続されるもので,“$LD\_EN = 1$”のときだけ各レジスタ(Aレジスタ,Bレジスタ,OUTポート)に対する書き込みが許可されます.

ジャンプなし命令におけるCPUの状態遷移

図3の最初の時点ではCPUはリセットされた直後であり,PCの出力は“0000”,CPUは「状態0」であるとします.このとき,CPUはROMの「0番地」に書き込まれているデータに対して「フェッチ」,「デコード」,「実行」を行います.また,「状態0」では“$LD\_EN = 1$”となっているので,次のクロックの立ち上がりでCPUは「ライト・バック」を行います.すなわち,「状態1」に遷移します.

「状態1」のときはステート・マシンが“$PC\_UP = 1$”を出力しているので,次のクロックの立ち上がりでPCの出力は”$+1$”されて“0001”になります.また,CPUは「状態0」に戻ってROMの「1番地」の命令を読み込んで実行します.

CPUは以上の動作を繰り返して,次々と命令を処理していきます.

図3 「ジャンプなし命令」を実行するときの各ブロックの出力

ステート・マシンの動作:ジャンプ命令

続いて,CPUが「ジャンプ命令」を実行するときのようすについて考えます.ここでは,図4に示すようにROMの「0番地」の内容がジャンプ命令“JUMP 0100”(ROMの4番地にジャンプせよ)であるとします.最初の「状態0」の時点でIDはジャンプ命令を解読し,“$PC\_LD = 1$”を出力します.すると,次のクロックの立ち上がりでPC内のレジスタに新しい値“0100”が上書きされ,CPUはROMの4番地にジャンプします.ジャンプした後のステート・マシンは再び「状態0」になり,ROMの4番地の命令に対して「フェッチ」,「デコード」,「実行」を行います.

以上のことから,ジャンプ命令を実行するときは「状態0」$\to$「状態0」という具合に,状態0が連続することになります.

図4 「ジャンプ命令」を実行するときの各ブロックの出力

ステート・マシンの状態遷移図

ここまでの内容をまとめると,ステート・マシンの挙動を図5のように表すことができます.

このような図を「状態遷移図」(state transition diagram)といいます.

実行する命令がジャンプ命令か否かで,IDが出力する“$PC\_LD$”の値が変化します.「ジャンプなし命令」のときは“$PC\_LD = 0$”となります.ステート・マシンはこれを受けて,「状態0」の次に「状態1」となる動作をします.「状態1」になった後は必ず「状態0」に戻ります.

これに対して,「ジャンプ命令」のときは“$PC\_LD = 1$”となります.この場合,ステート・マシンは「状態0」の後に再び「状態0」になります.

これから,図5の状態遷移図を実現するようにステート・マシンの回路を設計します.

図5 これから作るCPUのステート・マシンの状態遷移図

ステート・マシンの入出力仕様

図6に,ステート・マシンの入出力端子を示します.

“$PC\_LD$”は先に説明したとおりステート・マシンの動作を制御する入力端子です.また“$\overline{HALT}$”は“HALT”(ホールト)命令を実行するときに“0”を入力する端子で,このときCPUは動作を停止します.

なお,今回の設計ではステート・マシンのブロックの中に「クロック回路」と「リセット回路」も実装することにします.これにともない,図6にはクロック信号の出力端子“$CK$”およびリセット信号の出力端子“$\overline{RST}$”を追加しています.

図6 ステート・マシンの入出力端子

ステート・マシンの内部ブロック図

図7にステート・マシンの内部ブロック図を示します.

“FF1”(フリップフロップ1)は“HALT”命令を実行するときにクロックを止める役割を担います.なお,“$\overline{HALT}$”信号と“$PC\_UP$”信号のNORをとっているのは,「状態0」$\to$「状態1」($PC\_UP = 0 \to PC\_UP = 1$)という遷移におけるクロックでHALT動作を実行するためです.

FF2は,“$PC\_LD = 0$”のときはT-FFとして動作して「状態0」と「状態1」を行き来します.また,“$PC\_LD = 1$”のときは常に“0”を保持して「状態0」にとどまります. このことから,“$PC\_UP$”信号の値(FF2の出力$Q$)はCPUの「状態」に相当することがわかります.

“CLOCK”はクロック信号を生成する回路ブロック,“RESET”はリセット信号を生成する回路ブロックを表しています.図中の“$CK$”ノードの電圧はクロック信号としてCPU全体に供給され,“$\overline{RST}$”ノードの電圧はリセット信号としてCPU全体に供給されます.

図7 ステート・マシンの内部ブロック図

ステート・マシンを論理ゲート・レベルで作る

図7のブロック図をもとにして論理ゲート・レベルでステート・マシンの回路を作ると,図8のようになります.FF1およびFF2は,例によって「非同期リセット付きポジティブ・エッジ・トリガ型D-FF」で構成しています.

クロック信号を発生させる“CLOCK”のブロックは,NOTゲートによる発振回路で構成しました.発振回路の周波数を決めるキャパシタの容量は,スイッチで切り替えられるようにしてあります.キャパシタのスイッチをすべてONにするとおよそ2 Hz,すべてOFFにするとおよそ200 kHzのクロックが得られます.

また,クロック信号は手動のスイッチで入力することもできます.スイッチの出力はシュミット・トリガで整形してから利用するようにしています.

リセット信号を発生させる“RESET”のブロックでも,リセット用のスイッチが発生させる波形をシュミット・トリガで整形してCPU全体に供給しています.

図8 ステート・マシンの論理ゲート・レベルの回路図

写真1は,図8に示したステート・マシンの回路を「ディジタル回路ブロック」で作ったものです.これがCPUキット“CPU1738”の「STATE MACHINE基板」となります.論理ゲート数は20個,使用しているトランジスタ数は100個です.

写真1 CPUキット“CPU1738”の「STATE MACHINE基板」

ステート・マシンをFPGA上に実装する

FPGA内に実装するステート・マシンの仕様検討

ここまで考えてきた「トランジスタ・レベル」の回路では,CPU全体に対して供給するクロック信号およびリセット信号を「ステート・マシン」のブロックに含めていました.これに対して,FPGAを使う場合は外部からクロック信号やリセット信号を与えるのが一般的です.そこで,外部からこれらの信号を受け取るように,ステート・マシンの仕様を変更することにします.

図9に,これからFPGA内に実装するステート・マシンの入出力端子を示します.入力端子として,クロック端子“$CK$”およびリセット端子“$\overline{RST}$”を用意しています.また,“HALT”命令を実行する回路は上位階層(トップ・モジュール)に実装するので,ここでは“$\overline{HALT}$”端子を用意していません.

図9 FPGAに実装するステート・マシンの入出力端子

Verilogソース・コード

図9の仕様にしたがってステート・マシンを実装するためのVerilog記述例をリスト1に示します.“HALT”命令に関係するフリップフロップは上位階層に置くので,ここではCPUのステート(状態0か状態1)を管理するフリップフロップ“state”だけを用意しています.出力“$PC\_UP$”および“$LD\_EN$”は,いずれもこのフリップフロップの出力を引き出したものになっています.

`default_nettype none

module STATE_MACHINE
(
	input wire clk,
	input wire n_rst,
	input wire pc_ld,
	
	output wire ld_en,
	output wire pc_up
);

reg state;

always@(posedge clk, negedge n_rst)
begin
	if(~n_rst)
		state <= 1'b0;
	else if (pc_ld)
		state <= 1'b0;
	else
		state <= ~state;
end

assign ld_en = ~state;
assign pc_up = state;

endmodule

`default_nettype wire



リスト1 ステート・マシンのVerilogソース・コード“STATE\_MACHINE.v”

ステート・マシンの動作を論理シミュレーションで確認する

リスト1で定義したモジュール“STATE\_MACHINE”の動作を論理シミュレーションで確認してみます.リスト2にテスト・ベンチの記述例を示します.

“state\_machine”という名前でステート・マシンのインスタンスを作っています. 検証内容は簡単で,入力“$PC\_LD$”の値に対する出力“$LD\_EN$”および“$PC\_UP$”の変化を確かめるだけです.

“$PC\_LD = 0$”(ジャンプなし命令に相当)のときは,クロックが入るごとに“state”レジスタの値が反転(これにともない“$LD\_EN$”および“$PC\_UP$”も反転)する動作になるはずです.また,“$PC\_LD = 1$”(ジャンプ命令に相当)のときは常に“$LD\_EN = 1$”および“$PC\_UP = 0$”となるはずです.

リスト3のスクリプト・ファイルを利用してModelSimでシミュレーションを実行すると,図10に示す結果が得られます.問題なく動作していることが確認できます.

`timescale 1us/10ns

module tb_state_machine();

//wire, register declaration
reg clk;
reg n_rst;
reg [7:0] cycle_cnt;

reg pc_ld;
wire ld_en;
wire pc_up;

//clock signal
initial clk = 1'b0;
always #0.5
	clk = ~clk;

//reset signal
initial begin
	n_rst = 1'b0;
	#1;
	n_rst = 1'b1;
end


//pc load signal
initial begin
	pc_ld = 1'b0;
	#4;
	pc_ld = 1'b1;
end

//cycle count
initial cycle_cnt = 0;
always @ (posedge clk)
	cycle_cnt = cycle_cnt + 8'd1;

//stop
always @ (*)
if(cycle_cnt == 8'd10)
	$stop;

//STATE_MACHINE instance
STATE_MACHINE state_machine
(
	.clk(clk),
	.n_rst(n_rst),
	.pc_ld(pc_ld),
	.ld_en(ld_en),
	.pc_up(pc_up)
);

endmodule



リスト2 ステート・マシンのテスト・ベンチ“tb\_state\_machine.v”
#transcript window setting
transcript on

#delete "rtl_work" directory
if {[file exists rtl_work]} {
	vdel -lib rtl_work -all
}

#create the design library
vlib rtl_work

#define a mapping between logical and physical library name
vmap work rtl_work

#compile the Verilog files
vlog  -vlog01compat -work work \
	+incdir+../../ \
	../../STATE_MACHINE.v \
	./tb_state_machine.v

#invoke the simulator
vsim -L altera_mf_ver -c work.tb_state_machine

#wave window setting
add wave -divider TEST_BENCH
add wave -bin sim:/tb_state_machine/clk
add wave -bin sim:/tb_state_machine/n_rst
add wave -unsigned sim:/tb_state_machine/cycle_cnt

add wave -divider STATE_MACHINE_INPUT
add wave -bin sim:/tb_state_machine/state_machine/pc_ld


add wave -divider STATE_MACHINEOUTPUT
add wave -bin sim:/tb_state_machine/state_machine/ld_en
add wave -bin sim:/tb_state_machine/state_machine/pc_up


#save all signal
log -r *

#run simulation
run -all

#wave window zoom setting
wave zoom full



リスト3 ステート・マシンのシミュレーションのためのスクリプト・ファイル“tb\_state\_machine.do”
図10 ステート・マシンのシミュレーション結果

命令デコーダ(ID)の設計

IDの入出力仕様

命令デコーダ(ID)は,ROMから読み込んだ命令に応じてCPU全体の制御信号を生成する回路です.ここでいう制御信号とは,いままで設計した“A REG”や“B REG”などの回路ブロックにおける“$LD$”信号や“$OE$”信号などが該当します.

図11に,これから作るIDの入出力端子を示します.“$D_0$”から“$D_3$”はROMから読み出した命令を入力する端子です.また“$C$”および“$Z$”にはALUが出力する演算結果のフラグを入力します.“$LD\_EN$”はステート・マシンから来る信号で,各レジスタに書き込みを許可するときに“1”になります.IDは,これらの信号にもとづいて各ブロックを制御するための信号を出力します.なお,図11における信号名は“[回路ブロック名]\_[その回路ブロックにおける端子名]”という形式にしています.

図11 IDの入出力端子

各命令を実行するときのCPUの動作

図12および図13に,各命令を実行するときのデータの流れを示します.IDは,これらの図における太い矢印が通過する経路の回路ブロックをアクティブにします.なお,“※”印がついている経路は動作とは関係ありませんが,IDの回路を簡略化するために有効にしています.

図12 命令実行時のデータの流れ(1)
図13 命令実行時のデータの流れ(2)

IDの真理値表

図12および図13の動作を実現するIDの真理値表を作ると,表1のようになります.なお,“$PC\_LD = 1$”となる条件は少しややこしいので,別途考えることにします.

表1において“※”印がついている信号線は,ステート・マシンが出力する“$LD\_EN$”信号とのANDをとって出力します.これはCPUのステートが「状態1」のときだけレジスタに対する書き込みを許可するためです.また,表1における“(1)”という表記はIDの回路を簡略化するために“1”にしていることを意味しています.

表1 IDの真理値表(※印がついている信号線は“$LD\_EN$”とANDをとって出力する)

“$PC\_LD=1$”となる条件

続いて,PCの“$LD$”端子に入力する“$PC\_LD$”信号について考えます.

さきほどステート・マシンを設計したときに考えたとおり,CPUがジャンプ命令を実行するときだけ“$PC\_LD = 1$”とするのでした.このCPUには“JUMP”,“JNC”,“JNZ”という3つのジャンプ命令があるので,それぞれの条件について1つずつ確認することにします.

まず,“JUMP”命令のときは無条件に“$PC\_LD=1$”とします.“JNC”命令は“Jump if Not Carry”(ALUの$C$フラグが“0”ならばジャンプする)という意味なので,IDの入力が“$C=0$”の時だけ“$PC\_LD=1$”を出力するようにします.また,“JNZ”命令は“Jump if Not Zero”(ALUの$Z$フラグが“0”ならばジャンプする)という意味なので,IDの入力が“$Z=0$”のときだけ“$PC\_LD=1$”を出力するようにします.

以上のことから,“$PC\_LD=1$”を出力する入力信号の組み合わせをまとめると表2のようになります.

表2 IDが“$PC\_LD = 1$”を出力するときの入力の組み合わせ

IDを論理ゲート・レベルで作る

以上の内容を実現するIDの回路を論理ゲート・レベルで作ると,図1のようになります. これはあくまで一例で,何通りもの実装方法が考えられます. もっと最適化して,トランジスタ数を減らすことも可能です.

ここでは回路図の読みやすさとトランジスタ数のバランスを考えて図14の形にまとめました.

図14 IDの論理ゲート・レベルの回路図

写真2は,図14に示したIDの回路を「ディジタル回路ブロック」で作ったものです.これがCPUキット“CPU1738”の「ID基板」となります.論理ゲート数は50個,使用トランジスタ数は216個です.

写真2 CPUキット“CPU1738”の「ID基板」

IDをFPGA上に実装する

Verilogソース・コード

リスト4に,IDの機能を実現するためのVerilogの記述例を示します.表1および表2に示す真理値表のとおりに動作するように,functionや条件演算子を利用して記述しています.

真理値表から図14の回路図を起こして,さらに論理ゲートで実装するのはかなり骨が折れる作業でした.これに対して,FPGAを使えば簡潔なVerilog記述によってスマートに論理回路を実装することができます(FPGAは便利ですね).

`default_nettype none

module ID
(
	input wire [3:0] d,
	input wire ld_en,
	input wire c,
	input wire z,
	
	output wire a_reg_ld,

	output wire b_reg_ld,
	output wire b_reg_oe, //"ROM_OE" is "~B_REG_OE"

	output wire alu_as,
	output wire alu_oe, //"IN_OE" is "~ALU_OE"
	output wire alu_ld,
	output wire alu_mux_a,
	output wire alu_mux_b,

	output wire out_ld,

	output wire pc_ld,

	output wire n_halt
);

//=====================================
//A REG Load Enable
//=====================================
assign a_reg_ld = func_a_reg_ld(d, ld_en);
function func_a_reg_ld;
	input [3:0] d;
	input ld_en;
	begin 
		casex(d)
			4'b00x0: func_a_reg_ld = ld_en;
			4'b01xx: func_a_reg_ld = ld_en;
			4'b1011: func_a_reg_ld = ld_en;
			default: func_a_reg_ld = 1'b0;
		endcase
	end
endfunction

//=====================================
//B REG Load Enable
//=====================================
assign b_reg_ld = func_b_reg_ld(d, ld_en);
function func_b_reg_ld;
	input [3:0] d;
	input ld_en;
	begin 
		casex(d)
			4'b0001: func_b_reg_ld = ld_en;
			4'b0011: func_b_reg_ld = ld_en;
			default: func_b_reg_ld = 1'b0;
		endcase
	end
endfunction

//=====================================
//B REG Output Enable
//=====================================
assign b_reg_oe = func_b_reg_oe(d);
function func_b_reg_oe;
	input [3:0] d;
	begin 
		casex(d)
			4'b001x: func_b_reg_oe = 1'b1;
			4'b010x: func_b_reg_oe = 1'b1;
			4'b100x: func_b_reg_oe = 1'b1;
			4'b1011: func_b_reg_oe = 1'b1;
			default: func_b_reg_oe = 1'b0;
		endcase
	end
endfunction

//=====================================
//ALU ADD/SUB
//=====================================
assign alu_as = func_alu_as(d);
function func_alu_as;
	input [3:0] d;
	begin 
		casex(d)
			4'b01x1: func_alu_as = 1'b1;
			default: func_alu_as = 1'b0;
		endcase
	end
endfunction

//=====================================
//ALU Output Enable
//=====================================
assign alu_oe = (d == 4'b1011) ? 1'b0 : 1'b1;

//=====================================
//ALU Load Enable
//=====================================
assign alu_ld = func_alu_ld(d, ld_en);
function func_alu_ld;
	input [3:0] d;
	input ld_en;
	begin 
		casex(d)
			4'b01xx: func_alu_ld = ld_en;
			default: func_alu_ld = 1'b0;
		endcase
	end
endfunction

//=====================================
//ALU MUX A
//=====================================
assign alu_mux_a = func_alu_mux_a(d);
function func_alu_mux_a;
	input [3:0] d;
	begin 
		casex(d)
			4'b0011: func_alu_mux_a = 1'b1;
			4'b01xx: func_alu_mux_a = 1'b1;
			4'b1000: func_alu_mux_a = 1'b1;
			default: func_alu_mux_a = 1'b0;
		endcase
	end
endfunction

//=====================================
//ALU MUX B
//=====================================
assign alu_mux_b = func_alu_mux_b(d);
function func_alu_mux_b;
	input [3:0] d;
	begin 
		casex(d)
			4'b0011: func_alu_mux_b = 1'b0;
			4'b1000: func_alu_mux_b = 1'b0;
			default: func_alu_mux_b = 1'b1;
		endcase
	end
endfunction

//=====================================
//OUT REG Load Enable
//=====================================
assign out_ld = func_out_ld(d, ld_en);
function func_out_ld;
	input [3:0] d;
	input ld_en;
	begin 
		casex(d)
			4'b100x: func_out_ld = ld_en;
			4'b1010: func_out_ld = ld_en;
			default: func_out_ld = 1'b0;
		endcase
	end
endfunction

//=====================================
//Program Counter Load Enable
//=====================================
assign pc_ld = func_pc_ld(d, c, z);
function func_pc_ld;
	input [3:0] d;
	input c;
	input z;
	begin 
		casex(d)
			4'b1100: func_pc_ld = 1'b1;
			4'b1101: func_pc_ld = ~c;
			4'b1110: func_pc_ld = ~z;
			default: func_pc_ld = 1'b0;
		endcase
	end
endfunction

//=====================================
//HALT
//=====================================
assign n_halt = (d == 4'b1111)? 1'b0 : 1'b1;

endmodule

`default_nettype wire



リスト4 IDのVerilogソース・コード“ID.v”

IDの動作を論理シミュレーションで確認する

リスト4で定義したモジュール“ID”の動作を論理シミュレーションで確認してみます.リスト5にテスト・ベンチの記述例を示します.

“id”という名前でIDのインスタンスを作っています.ここでは,すべての制御線がアクティブになるように“$LD\_EN = 1$”,“$C= 0$”,“$Z = 0$”としてシミュレーションを実行しています.もし“$LD\_EN = 0$”とした場合は,常に“$A\_REG\_LD = 0$”,“$B\_REG\_LD = 0$”,“$OUT\_LD = 0$”となります.また,“$C = 1$”や“$Z=1$”とした場合は“JNC”命令や“JNZ”命令を読み込んでも“$PC\_LD$”が“1”になることはありません.ここではこれらの条件におけるシミュレーション結果を省略しますが,実際にシミュレーションを行えば問題なく動作することを確認できます.

リスト6のスクリプト・ファイルを利用してModelSimでシミュレーションを実行すると,図15に示す結果が得られます.問題なく動作していることが確認できます.

`timescale 1us/10ns

module tb_id();

//wire, register declaration
reg clk;
reg [7:0] cycle_cnt;

reg [3:0] d;
reg ld_en;
reg c;
reg z;

wire a_reg_ld;
wire b_reg_ld;
wire b_reg_oe;
wire alu_as;
wire alu_oe;
wire alu_ld;
wire alu_mux_a;
wire alu_mux_b;
wire out_ld;
wire pc_ld;
wire n_halt;

//clock signal
initial clk = 1'b0;
always #0.5
	clk = ~clk;

//data (instruction)
initial d = 0;
always @ (posedge clk)
	d = d + 4'd1;

//register load signal
initial begin
	ld_en = 1'b1;
end

//carry flag from ALU
initial begin
	c = 1'b0;
end

//zero flag from ALU
initial begin
	z = 1'b0;
end

//cycle count
initial cycle_cnt = 0;
always @ (posedge clk)
	cycle_cnt = cycle_cnt + 8'd1;

//stop
always @ (*)
if(cycle_cnt == 8'd16)
	$stop;

//ID instance
ID id
(
	.d(d),
	.ld_en(ld_en),
	.c(c),
	.z(z),

	.a_reg_ld(a_reg_ld),

	.b_reg_ld(b_reg_ld),
	.b_reg_oe(b_reg_oe),

	.alu_as(alu_as),
	.alu_oe(alu_oe),
	.alu_ld(alu_ld),
	.alu_mux_a(alu_mux_a),
	.alu_mux_b(alu_mux_b),

	.out_ld(out_ld),
	
	.pc_ld(pc_ld),

	.n_halt(n_halt)
);

endmodule



リスト5 IDのテスト・ベンチ“tb\_id.v”
#transcript window setting
transcript on

#delete "rtl_work" directory
if {[file exists rtl_work]} {
	vdel -lib rtl_work -all
}

#create the design library
vlib rtl_work

#define a mapping between logical and physical library name
vmap work rtl_work

#compile the Verilog files
vlog  -vlog01compat -work work \
	+incdir+../../ \
	../../ID.v \
	./tb_id.v

#invoke the simulator
vsim -L altera_mf_ver -c work.tb_id

#wave window setting
add wave -divider TEST_BENCH
add wave -bin sim:/tb_id/clk
add wave -unsigned sim:/tb_id/cycle_cnt

add wave -divider ID_INPUT
add wave -bin sim:/tb_id/id/d
add wave -bin sim:/tb_id/id/ld_en
add wave -bin sim:/tb_id/id/c
add wave -bin sim:/tb_id/id/z

add wave -divider ID_AREG_OUTPUT
add wave -bin sim:/tb_id/id/a_reg_ld

add wave -divider ID_BREG_OUTPUT
add wave -bin sim:/tb_id/id/b_reg_ld
add wave -bin sim:/tb_id/id/b_reg_oe

add wave -divider ID_ALU_OUTPUT
add wave -bin sim:/tb_id/id/alu_as
add wave -bin sim:/tb_id/id/alu_oe
add wave -bin sim:/tb_id/id/alu_ld
add wave -bin sim:/tb_id/id/alu_mux_a
add wave -bin sim:/tb_id/id/alu_mux_b

add wave -divider ID_OUT_OUTPUT
add wave -bin sim:/tb_id/id/out_ld

add wave -divider ID_PC_OUTPUT
add wave -bin sim:/tb_id/id/pc_ld

add wave -divider ID_HALT_OUTPUT
add wave -bin sim:/tb_id/id/n_halt

#save all signal
log -r *

#run simulation
run -all

#wave window zoom setting
wave zoom full

リスト6 IDのシミュレーションのためのスクリプト・ファイル“tb\_id.do”
図15 IDのシミュレーション結果

(c)2021 Nobuyasu Beppu