オール・トランジスタ4ビットCPUの製作とFPGA開発
[Vol.4 CPUのROM,PC,ALUの設計]

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好評発売中

ROMの設計

ROMの入出力仕様

ROMは,CPUが実行するプログラムを格納するための回路です.図1に,これから作るROMの入出力端子の仕様を示します.

今回作るCPUは4ビットなので,それに合わせて4ビットのアドレス入力($A_0$から$A_3$)を持つROMを作ることにします.この場合,ROMのアドレス空間は$2^4 = 16$番地となります.

ROMのデータ線は8本($D_0$から$D_7$)とします.データの下位4ビットはCPUが扱う数値データで,いわゆる「オペランド」に相当します.この下位4ビットのデータ線は「算術論理演算回路」(ALU)の入力に接続されます.一方,データの上位4ビットはCPUの動作を決定する「命令」のデータであり,「オペコード」に相当する部分となります.この上位4ビットのデータ線は「命令デコーダ」(ID)に接続されます.

図1 ROMの入出力仕様
「ライト・エディション」を選択

ROMとBレジスタの出力の衝突を防ぐ工夫

今回のCPUのアーキテクチャでは,「Bレジスタ(B REG)の出力」と「ROMのデータ出力の下位4ビット」がALUの入力(の片方)を共有するようなバス構成となっています.BレジスタとROMの出力端子が同じ配線に接続されているため,出力状態の組み合わせによっては出力部のトランジスタに過大な電流が流れて破損する恐れがあります.そこで,ROMのデータ出力の下位4ビットには「トライ・ステート・バッファ」を付けて出力部をバスから電気的に切り離せるようにしておきます.図2の“OE”(アウト・イネーブル)端子は,このトライ・ステート・バッファの制御端子です.

なお,Bレジスタを設計するときも出力部にはトライ・ステート・バッファを入れていました.これにより,常にBレジスタかROMのどちらか片方の回路だけ出力を許可する(そうなるようにIDを設計する)ことで,これらの回路の出力部分が破損することを防げます.

ROMの内部ブロック図

スイッチ・マトリクス

図2に,ROMの内部ブロック図を示します.ROMを作るためには,「電源を切ってもデータを保持できる回路」が必要です.一般的な製品ではフローティング・ゲート型のMOSFETによるフラッシュ・メモリや,電流で配線を焼き切ってデータを記憶するヒューズ型の回路が使用されますが,今回は単純に「スイッチ」を使うことにします.8ビットのDIPスイッチを16個並べて,8ビット$\times$16番地の「スイッチ・マトリクス」を構成します.このスイッチ・マトリクスが今回のROMにおいて「データ記憶」を担うブロックとなります.

4 to 16デコーダ

今回のROMには,アドレスとして“0”(2進数で“0000”)から“15”(2進数で“1111”)までの値が入力されます.このアドレス入力に応じてスイッチ・マトリクスの中から適切なスイッチを1つだけ選択し,データを出力することが求められます.この動作を実現するための回路が「4 to 16デコーダ」です.4 to 16デコーダは,「4ビットの入力信号を16本の出力信号に変換する回路」です.2進数の入力(4ビット)の値に応じて16本ある出力信号線のうち1本だけが“1”になり,それ以外の信号線は“0”になります.

トライ・ステート・バッファ

ROMの出力部分にはトライ・ステート・バッファを入れておきます.なお,ROMの出力のうち他の回路(Bレジスタ)と競合するのは下位4ビットだけなので,トライ・ステート・バッファは下位4ビットだけに入れることにします.

図2 ROMの内部ブロック図

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

4 to 16デコーダの回路図

図3に,「4 to 16デコーダ」をゲート・レベルで構成した回路図を示します.入力されたアドレス信号($A_0$から$A_3$)を上位2ビットと下位2ビットに分けて,それぞれの値が“00”,“01”,“10”,“11”のときにレベルが“1”になる信号を用意しています(“Lower 00”から“Upper 11”まで).なお,使用するトランジスタ数を減らすために,「ANDゲート」を「負論理入力のNORゲート」によって構築しています(図4).

図3 ROMの「4 to 16デコーダ」の回路図
図4 ANDゲートを負論理入力のNORゲートで作る

スイッチ・マトリクスの回路図

図5に,ROMのスイッチ・マトリクス部分の回路図を示します.8ビットのDIPスイッチを16個並べて構成しています.スイッチの出力部分にはLEDを取り付けているので,該当するアドレスのデータが読み出されるときにデータが“1”のビットが光ります.

各スイッチに接続されている“Encoder xxxx”という信号線は,「4 to 16デコーダ」の出力です.また,“SW_data x”という信号線は次段の「出力バッファ」に接続します.

図5 ROMの「スイッチ・マトリクス」部分の回路図

スイッチ・マトリクスで使っているダイオードの役割

スイッチの出力部分に取り付けているダイオードの役割について,図6を使って説明します.図6はROMの回路を単純化したもので,「0番地」と「1番地」の2つの番地だけを持ちます.また,各番地のデータ幅は2ビットです.ここで,「0番地」のデータは“$D_0 = 0$”,“$D_1 = 1$”となっています.また「1番地」のデータは“$D_0 = 1$”,“$D_0 = 1$”となっています.

図6(a)に示すように,ダイオードが無い状態で「0番地」のデータを読み出したとします.このとき「1番地」のスイッチは0ビット目($D_0$)と1ビット目($D_1$)の両方がONになっているので,「0番地」の1ビット目から流れ出した電流が「1番地」のスイッチを経由して0ビット目に現れてしまいます.結果としてROMの出力は“$D_0 = 1$”となり,誤ったデータが読み出されてしまいます.

この問題を解決するために,図6(b)のようにダイオードを挿入します.これによって,隣のスイッチを経由して電流が回り込むことを防げます.

なお,通常のダイオードの順方向電圧は“$V_\mathrm{F} \fallingdotseq 0.6 \mathrm{V}$”程度なので,電源電圧が小さい場合は十分に“1”のレベルを確保できなくなる可能性があります.そこで,今回は順方向電圧が小さいダイオードである「ショットキー・バリア・ダイオード」(Schottky barrier diode)を使うことにしました.今回使っているのは“BAT43XV2”という型番のもので,順方向電圧は“$V_\mathrm{F} \fallingdotseq 0.3 \mathrm{V}$”程度です.順方向電圧が小さいものであれば,これ以外のダイオードを使っても構いません.

図6 スイッチ・マトリクスにダイオードを挿入して逆流を防ぐ

出力部分の回路図

ROMの出力部分の回路図を図7に示します.スイッチの出力部分にはダイオードが入っているので,出力が“0”レベルのときに「吸い込み電流」を流すことができません.そこで,$10\ \mathrm{k\Omega}$のプル・ダウン抵抗を取り付けることで安定して“0”レベルを出力できるようにしています.また,下位4ビットには例によってトライ・ステート・バッファを挿入しています.

図7 ROMの「バッファ」部分の回路図

「ディジタル回路ブロック」によるROM回路の構成例

ROMの回路を図3図5図7の回路図にもとづいて作ったものを写真1に示します.これがCPUキット“CPU1738”の「ROM基板」となります.この回路には32個の論理ゲートが使われており,トランジスタの数は合計136個です.

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

ROMをFPGA上に実装する

設計の方針

FPGA上に自作のCPUを実装することを想定して,ROMの回路をVerilogで記述してみます.MAX10シリーズのFPGAにはフラッシュ・メモリが搭載されていますが,今回は使用しません.その代わりに,「入力されたアドレス信号に対して決まったデータを出力する回路」を定義してROMとして使うことにします.ROMと言いつつも,これは一種の「デコーダ」であると見なせます.

なお,FPGAの内部ではトライ・ステート・バッファの使用を避けるのが一般的です.これは,FPGAの内部信号がハイ・インピーダンス状態になって動作が不安定になることを防ぐためです.今回は上位階層(CPUのトップ・モジュール)でマルチプレクサを用意して,信号線の接続を切り替えることにします.

Verilogソース・コード

リスト1に,ROMの機能を実現するモジュールの設計例を示します.

入力信号は4ビットの“address”です.また,出力信号は“data_h”(上位4ビット)および“data_l”(下位4ビット)があり,合わせて8ビットとなっています.入力されたアドレスに対して値を返す“rom_data”というfunctionを定義して,この中に8ビットのデータを直接書き込むことで「プログラム」を作ります.今回は簡単な例として,「1から5までの足し算を実行するプログラム」を書いてみました.このプログラムは,後でCPU全体のデバッグをするときに使います.

`default_nettype none

module ROM
(
	input wire [3:0] address,

	output wire [3:0] data_h,
	output wire [3:0] data_l
);

wire [7:0] data;
assign data = rom_data(address);
assign data_h = data[7:4];
assign data_l = data[3:0];

function [7:0] rom_data;
	input [3:0] address;
	begin
		case(address)
			4'b0000: rom_data = 8'b00000001; //LD A, 0001
			4'b0001: rom_data = 8'b10000000; //OUT A
			4'b0010: rom_data = 8'b01100010;	//ADD A,	0010
			4'b0011: rom_data = 8'b10000000; //OUT A
			
			4'b0100: rom_data = 8'b01100011; //ADD A, 0011
			4'b0101: rom_data = 8'b10000000; //OUT A
			4'b0110: rom_data = 8'b01100100; //ADD A, 0100
			4'b0111: rom_data = 8'b10000000; //OUT A
		
			4'b1000: rom_data = 8'b01100101; //ADD A, 0101
			4'b1001: rom_data = 8'b10000000; //OUT A
			4'b1010: rom_data = 8'b00000000; //LD A, 0000 (NOP)
			4'b1011: rom_data = 8'b00000000; //LD A, 0000 (NOP) 
			
			4'b1100: rom_data = 8'b00000000;  //LD A, 0000 (NOP)
			4'b1101: rom_data = 8'b00000000;  //LD A, 0000 (NOP)
			4'b1110: rom_data = 8'b00000000;  //LD A, 0000 (NOP)
			4'b1111: rom_data = 8'b10100000;  //OUT 0000
			default: rom_data = 8'b00000000;
			endcase
	end	
endfunction

endmodule

`default_nettype wire

リスト1 ROM のVerilog ソース・コード“ROM.v”

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

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

“rom”という名前でROMモジュールのインスタンスを作成しています.“address”端子に0から15までの値を順番に入力していき,functionの“rom_data”で定義したとおりのデータが出力されることを確認します.

ModelSim用のスクリプト・ファイルはリスト3に示すものを使います.シミュレーション時間を管理するためのクロックの状態と,クロック・サイクルを数えるカウンタ“cycle_cnt”の値,ROMモジュールの入出力信号の波形を全て表示するように設定しています.

このテスト・ベンチを利用してシミュレーションを実行すると,図8に示す結果が得られます.“address”端子に対する入力の値に応じて,“ROM”モジュールで定義したとおりのデータが“data_h”および“data_l”端子から出力されていることが確認できます.

`timescale 1us/10ns

module tb_rom();

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

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

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

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

//ROM instance
ROM rom
(
	.address(cycle_cnt[3:0]),
	.data_h(data_h),
	.data_l(data_l)
);

endmodule

リスト2 ROM のテスト・ベンチ“tb_rom.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+../../ \
	../../ROM.v \
	./tb_rom.v

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

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

add wave -divider ROM
add wave -unsigned sim:/tb_rom/rom/address
add wave -bin sim:/tb_rom/rom/data_h
add wave -bin sim:/tb_rom/rom/data_l

#save all signal
log -r *

#run simulation
run -all

#wave window zoom setting
wave zoom full

リスト3 ROM のシミュレーションのためのスクリプト・ファイル“tb_rom.do”
図8 ROMのシミュレーション結果

プログラム・カウンタ(PC)の設計

PCの入出力仕様

プログラム・カウンタ(PC)は,ROMに対してアドレス信号を出力する回路です.通常の命令を実行するときは,PCはアドレスの値を“$+1$”するカウンタとして動作します.また,「ジャンプ命令」を実行するときは外部から入力された値を読み込んで,出力データを上書きする動作を行います.このような機能を実現するために,PCの入出力端子を図9のように定めます.“$CK$”はクロック入力,“$\overline{RST}$”は非同期リセット入力で,“$D_0$”から“$D_3$”は値を上書きするためのデータ入力です.“$Q_0$”から“$Q_3$”はROMに対するアドレス信号を出力する端子です.“$LD$”および“$UP$”は制御入力で,これらの値によってPCの動作が変化します.

図9 プログラム・カウンタ(PC)の入出力端子の仕様
表1 PCの特性表

PCの特性表

表1にPCの特性表を示します.“$LD=0$”かつ“$UP=0$”のときは直前の値を保持し,“$LD=0$”かつ“$UP=1$”のときはカウント・アップの動作を行います.また,“$LD=1$”のときは“$UP$”の値にかかわらず“$D_0$”から“$D_3$”に入力されたデータを読み込みます.

PCの内部ブロック図

図10にPCの内部ブロック図を示します.データを記憶するためのD-FFを中心として,“$+1$”の演算を行う加算器と,D-FFの入力データを選択するマルチプレクサ(MUX)によって構成されています.

直前のデータを保持する場合は,MUXでD-FFの出力“$Q$”を選択します.また,出力データを“$+1$”するときは加算器の出力“$Q+1$”を選択します.外部からのデータ入力“$D$”を選択すれば,任意の値をD-FFに記憶させることができます.

図10 PCの内部ブロック図

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

図10のブロック図をもとにして論理ゲート・レベルでPCの回路を作ると,図11のようになります.6個のNANDゲートで構成されるD-FFを4ビット分並べたものが基本になっていて,そこに3入力のMUXや“$+1$”の演算をするために最適化された加算器が接続されています.

図11 PCの論理ゲート・レベルの回路図

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

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

PCをFPGA上に実装する

Verilogソース・コード

リスト4に,PCの機能を実現するためのVerilogの記述例を示します.入出力端子は図9と同じ仕様で,「4ビットのレジスタ」を中心に回路を組み立てています.レジスタ“q”に代入する値を,制御入力“ld”および“up”の値に応じて切り替えています.

`default_nettype none

module PC
(
	input wire clk,
	input wire n_rst,
	input wire ld,
	input wire up,
	input wire [3:0] d,
	
	output reg [3:0] q
);

always @ (posedge clk, negedge n_rst)
begin
	if (~n_rst)
		q <= 4'd0;
	else if (ld)
		q <= d;
	else if (~ld & up)
		q <= q + 4'd1;
	else
		q <= q;
end

endmodule

`default_nettype wire

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

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

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

“pc”という名前でPCモジュールのインスタンスを作成しています.最初のリセット信号を印加した直後に“$ld=0$”かつ“$up=1$”として,アップ・カウンタとして動作させています.その後十分に時間が経過したところで“$ld=1$”として,“$d$”端子の入力データを読み込んでいます.“$d$”端子の入力は常に“$d = 4'\mathrm{d}7$”としているので,PCの出力は“$q=4'\mathrm{d}7$”となるはずです.その後“$ld=0$”かつ“$up=1$”として,再びアップ・カウンタとして動作させます.

リスト6のスクリプト・ファイルを利用してModelSimでシミュレーションを実行すると,図12に示す結果が得られます.設計どおり,最初に“0”から“15”(16進数表記だと“f”)までカウント・アップの動作をしていることが確認できます.また“$ld=1$”としたところで“$4'\mathrm{d}7$”が読み込まれ,そこからカウント・アップの動作が再開されていることがわかります.

`timescale 1us/10ns

module tb_pc();

//wire, register declaration
reg clk;
reg n_rst;
reg ld;
reg up;
reg [3:0] d;
wire [3:0] q;
reg [7:0] cycle_cnt;

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

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

//load signal
initial begin
	ld = 1'b0;
	#20;
	ld = 1'b1;
	#4;
	ld = 1'b0;
end

//count up signal
initial begin
	up = 1'b0;
	#2;
	up = 1'b1;
	#20;
	up= 1'b0;
	#2;
	up = 1'b1;
end


//data signal
initial begin
	d = 4'd7;
end

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

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

//PC instance
PC pc
(
	.clk(clk),
	.n_rst(n_rst),
	.ld(ld),
	.up(up),
	.d(d),
	.q(q)
);

endmodule

リスト5 PC のテスト・ベンチ“tb_pc.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+../../ \
	../../PC.v \
	./tb_pc.v

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

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

add wave -divider PC
add wave -bin sim:/tb_pc/pc/ld
add wave -bin sim:/tb_pc/pc/up
add wave -unsigned sim:/tb_pc/pc/d
add wave -unsigned sim:/tb_pc/pc/q

#save all signal
log -r *

#run simulation
run -all

#wave window zoom setting
wave zoom full

リスト6 PC のシミュレーションのためのスクリプト・ファイル“tb_pc.do”
図12 PCのシミュレーション結果

ALUの設計

ALUの入出力仕様

ALUはCPUにおける演算処理を担当する回路です.今回は加算と減算の2種類の算術演算を実行できるALUを作ります.図13にALUの入出力端子の仕様を示します.

演算データの入出力端子

「$A$入力」($A_0$から$A_3$)および「$B$入力」($B_0$から$B_3$)には計算処理の対象となる数値データが入力されます.「$Q$出力」($Q_0$から$Q_3$)には演算結果が出力されます.

演算結果によって変化するフラグ出力

演算結果が“$0$”の場合は「ゼロ・フラグ」が立ち,$Z = 1$”が出力されます.また,計算の途中で繰り上がりが発生した場合は「キャリー・フラグ」が立ち,“$C = 1$”が出力されます.

加算モードと減算モードの切り替え

“$AS$”は加算(Addition)モードと減算(Subtraction)モードを切り替える端子です.“$AS=0$”を入力すると加算($A+B$)が実行され,$AS=1$のときは減算($A-B$)が実行されます.

ALUの出力にはトライ・ステート・バッファを入れる

今回のCPUのアーキテクチャでは,ALUの出力端子は4ビットのバスに接続されます.このバスには「Aレジスタ」や「Bレジスタ」などの入力端子がつながっているので,適当な制御をすればALUの計算結果をこれらのレジスタに記憶させることができます.ただし,このバスには「入力ポート」(IN PORT)の出力も接続されているので,ALUとIN PORTの出力が衝突する可能性があります.これを避けるために,ALUの出力にトライ・ステート・バッファを入れて電気的に切り離せるようにしておきます.“$OE=0$”のときは出力端子がハイ・インピーダンス状態になり,“$OE=1$”のときは計算結果が出力されます.

データ入力部分のマルチプレクサ

今回のCPUのアーキテクチャでは,Aレジスタ,Bレジスタ,出力ポート,PCに対する入力データはすべてALUを経由する形になっています.そのため「ALUが$A$入力をそのまま出力する」あるいは「ALUが$B$入力をそのまま出力する」という仕組みを用意しておくと,後でCPU全体の制御回路を設計するときに見通しが良くなります.この機能を実現するために,ALUの入力$A$および$B$にはマルチプレクサ(MUX)を入れて「入力されたデータをそのまま通すモード」と「入力を“0000”(2進)にするモード」を切り替えられるようにしておきます.詳しい動作は次節の「ALUの演算部の真理値表」のところで解説します.

図13 ALUの入出力仕様
「ライト・エディション」を選択

ALUの動作仕様

ALUの演算部の真理値表

表2に,ALUの演算出力“$Q$”の真理値表を示します.出力イネーブル端子“$OE$”および演算モード選択端子“$AS$”の役割は先に説明した通りです.

“$MUX\_A=0$”とすると,ALU内部の演算回路に対する$A$入力は“0”になります.また,“$MUX\_A=1$”とすると外部から与えられた$A$入力がそのまま内部の演算回路に印加されます.“$MUX\_B$”端子は$B$入力に対して同様のはたらきをもちます.

 “$AS=0$”(加算モード)の状態で“'$MUX\_A=1$”かつ“$MUX\_B=0$”とすると,$A$入力をそのままALUの出力として取り出すことができます.また“$AS=0$”かつ“$MUX\_A=0$”かつ“$MUX\_B=1$”とすれば,$B$入力をALUの出力から取り出せます.後で命令デコーダ(ID)を設計するときは,これらの機能を積極的に使います.

表2 ALUの演算部の真理値表
「ライト・エディション」を選択

ALUのフラグの特性表

表3に,ALUのフラグ出力の特性表を示します.

ALUの$C$フラグおよび$Z$フラグは記憶回路(フリップフロップ)によって保持される値で,“$LD=1$”のときだけ更新されるものとします.先に説明したとおり,計算結果に繰り上がりがある場合は“$C=1$”となり,計算結果が“0”の場合は“$Z=1$”となります.

表3 ALUのフラグの特性表(リセット信号は省略)
「ライト・エディション」を選択

ALUの内部ブロック図

図14にALUの内部ブロック図を示します.4ビットの加減算器を中心として,入力部分には信号を切り替えるためのMUX,出力部分にはトライ・ステート・バッファが接続されています.また,$Z$フラグを管理するフリップフロップと,$C$フラグを管理するフリップフロップも備えています.

図14 ALUの内部ブロック図
「ライト・エディション」を選択

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

図14のブロック図をもとにして論理ゲート・レベルでALUの回路を作ると,図15のようになります.4ビットの全加算器の入力部分に「“$AS$”入力の値に応じて信号を反転させる回路」(EXORゲート)を挿入することで,4ビットの加減算器を構成しています.また,入力部分のマルチプレクサの役割は「信号を通すか“0000”(2進)にするか」を切り替えることなので,単純に加減算器の入力にANDゲートを入れることで実装しています.

$C$フラグおよび$Z$フラグを管理するフリップフロップは,例によって「非同期リセット付きのポジティブ・エッジ・トリガ型D-FF」を利用しています.

図15 ALUの論理ゲート・レベルの回路図
写真3は,図15に示したALUの回路を「ディジタル回路ブロック」で作ったものです.これがCPUキット“CPU1738”の「ALU基板」となります.論理ゲート数は58個,トランジスタの数は376個です.
写真3 CPUキット“CPU1738”の「ALU基板」

ALUをFPGA上に実装する

入出力端子の仕様

リスト7に,ALUのVerilog記述例を示します.FPGAに実装するALUでは出力部分にトライ・ステート・バッファを使わず,上位階層(CPUのトップ・モジュール)でマルチプレクサを使ってバスの信号経路を切り替えることにします.これに伴い,FPGA上に実装するALUは“$OE$”端子を持ちません.それ以外の入出力端子の仕様は図13と同じです.

演算処理の記述

リスト7の30行目からは演算処理を行う“calc”というfunctionを定義しています.今回扱うデータは4ビット幅ですが,「繰り上がり」を扱うために5ビット幅で演算を行っています.

加算を行う場合は5ビット幅のデータでそのまま足し算を行い,“result”というwireに結果を出力します.また,減算を行う場合は「引く数」(入力$b$)を2の補数で表して(ビット反転して“1”を加える)から,入力$a$と加算しています.

フラグ部分の記述

リスト7の43行目からは,フラグ用のレジスタ“c”および“z”の挙動を定義しています.

キャリー・フラグ(レジスタ“c”)は,単純に“result”の5ビット目の値をそのまま記憶しています.ゼロ・フラグ(レジスタ“z”)は“result”の0ビット目から3ビット目までの4本の信号線に対してNOR演算を行い,その値を記憶しています.ここでは記述を短くするために,“~|result[3:0]”という具合に「リダクション演算子」を使って表記しました.

`default_nettype none

module ALU
(
	input wire clk,
	input wire n_rst,
	
	input wire [3:0] a,
	input wire [3:0] b,
	input wire as,
	input wire ld,
	input wire mux_a,
	input wire mux_b,
	
	output wire [3:0] q,
	output reg c,
	output reg z	
);

//wire, register declaration
wire [3:0] wire_a;
wire [3:0] wire_b;
assign wire_a = mux_a ? a : 4'd0;
assign wire_b = mux_b ? b : 4'd0;

wire [4:0] result;
assign result = calc(wire_a, wire_b, as);
assign q = result[3:0];

//calculation
function [4:0] calc;
	input [3:0] a;
	input [3:0] b;
	input as;	
	begin 
		if(as == 1'b0)
			calc = {1'b0, a} + {1'b0, b};
		else
			calc = {1'b0, a} + {1'b0, ~b} + 5'd1;
	end
endfunction

// update C flag and Z flag
always@(posedge clk, negedge n_rst)
begin 
	if(~n_rst)
		begin 
			c <= 1'b0;
			z <= 1'b0;
		end
		
	else if (ld)
		begin 
			c <= result[4];
			z <= ~|result[3:0];
		end

	else
		begin 
			c <= c;
			z <= z;
		end
end

endmodule

`default_nettype wire

リスト7 ALU のVerilog ソース・コード“ALU.v”

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

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

“alu”という名前でALUモジュールのインスタンスを作っています.今回はすべての演算パターンを検証せずに,おおよその動作が正しく行われることを確認するにとどめています.前半は“$as=0$”として加算モードの動作確認,後半は“$as=1$”として減算モードの動作確認をしています.また,最後に“$ld=0$”の場合はフラグが更新されないことを確認しています.

リスト9のスクリプト・ファイルを利用してModelSimでシミュレーションを実行すると,図16に示す結果が得られます.加算および減算の演算処理が正常に行われていることがわかります.また,$C$フラグおよび$Z$フラグの挙動も問題ないことが確認できます.

`timescale 1us/10ns

module tb_alu();

//wire, register declaration
reg clk;
reg n_rst;
reg [3:0] data_a;
reg [3:0] data_b;
reg as;
reg ld;
reg mux_a;
reg mux_b;
wire [3:0] q;
wire c;
wire z;
reg [7:0] cycle_cnt;

//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

//data
initial begin
	//add
	data_a = 4'd3;
	data_b = 4'd4;
	#4;
	data_a = 4'd7;
	data_b = 4'd7;
	#1;
	data_a = 4'd8;
	data_b = 4'd7;
	#1;
	data_a = 4'd8;
	data_b = 4'd8;
	#1;
	data_a = 4'd9;
	data_b = 4'd8;
	#3;

	//sub
	data_a = 4'd3;
	data_b = 4'd4;
	#4;
	data_a = 4'd6;
	data_b = 4'd4;
	#1;
	data_a = 4'd5;
	data_b = 4'd4;
	#1;
	data_a = 4'd5;
	data_b = 4'd5;
	#1;
	data_a = 4'd5;
	data_b = 4'd6;
	#3;
	data_a = 4'd5;
	data_b = 4'd5;
end

//add/sub selection
initial begin
	as = 1'b0;
	#10;
	as = 1'b1;
end

//load signal
initial begin
	ld = 1'b1;
	#18;
	ld = 1'b0;
end

//multiplexer
initial begin
	mux_a = 0;
	mux_b = 0;
	#1;
	mux_a = 1;
	mux_b = 0;
	#1;
	mux_a = 0;
	mux_b = 1;
	#1;
	mux_a = 1;
	mux_b = 1;
	#7;
	mux_a = 0;
	mux_b = 0;
	#1;
	mux_a = 1;
	mux_b = 0;
	#1;
	mux_a = 0;
	mux_b = 1;
	#1;
	mux_a = 1;
	mux_b = 1;
end

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

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

//ALU instance
ALU alu
(
	.clk(clk),
	.n_rst(n_rst),
	.a(data_a),
	.b(data_b),
	.as(as),
	.ld(ld),
	.mux_a(mux_a),
	.mux_b(mux_b),
	.q(q),
	.c(c),
	.z(z)
);

endmodule

リスト8 ALU のテスト・ベンチ“tb_alu.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+../../ \
	../../ALU.v \
	./tb_alu.v

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

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

add wave -divider ALU_INPUT
add wave -bin sim:/tb_alu/alu/ld
add wave -bin sim:/tb_alu/alu/as
add wave -bin sim:/tb_alu/alu/mux_a
add wave -bin sim:/tb_alu/alu/mux_b
add wave -hex sim:/tb_alu/alu/a
add wave -hex sim:/tb_alu/alu/b

add wave -divider ALU_OUTPUT
add wave -hex sim:/tb_alu/alu/q
add wave -bin sim:/tb_alu/alu/c
add wave -bin sim:/tb_alu/alu/z

#save all signal
log -r *

#run simulation
run -all

#wave window zoom setting
wave zoom full

リスト9 ALU のシミュレーションのためのスクリプト・ファイル“tb_alu.do”
図16 ALUのシミュレーション結果
「ライト・エディション」を選択

関連資料


(c)2020 Nobuyasu Beppu