オール・トランジスタ4ビットCPUの製作とFPGA開発
[Vol.6 CPUの全体統合とプログラムの実行]

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


【Index】

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

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

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

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

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

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

これまで設計した全ブロックを接続してCPUを完成させる

いままで設計してきた“A REG”,“B REG”,“OUT PORT”,“IN PORT”,“ROM”,“PC”,“ALU”,“STATE MACHINE”,“ID”を図1のように接続するとCPUが完成します.

写真1は,CPU自作キット“CPU1738”を実際に組み立てたものです.このキットの回路構造は,図1のとおりになっています.

図1 CPUの全体接続図
写真1 “CPU1738”の全ての基板を接続した完成図

FPGA内に作り込むCPUの完成

トップ・モジュールのVerilogソース・コード

これまでトランジスタ・レベルの回路設計と並行して,FPGA内にCPUを構築する作業も進めてきました.こちらに関しても,トップ・モジュールを用意して全体を完成させます.Verilogの記述例をリスト1に示します.

23行目からはじまる“Global Clock”の部分では,外部から入力されたクロック信号“clk”とHALT命令を実行したときに“0”になるレジスタ“halt_reg”とのANDをとっています.これにより,HALT命令が実行されたときはCPU全体へのクロックの供給が止まるようにしています.

41行目から174行目までは,これまでに作った各モジュールをインスタンス化しています.また,必要な配線を定義して入出力ポートに対して接続しています.

176行目から184行目では,CPUの外部に出力する“state”信号および“halt”信号のassignを行っています.

186行目から197行目では,各種のデータ・バスを定義しています.先にトランジスタだけを使って設計した回路では,この部分にトライ・ステート・バッファを利用していました.これに対して,FPGAの内部では条件によって配線の接続先が変わるように記述しています.これは,マルチプレクサを実装していることに相当します.

//*********************************************************
// CPU_4bit.v
// 2021/01/27  N. Beppu
//*********************************************************

`default_nettype none

module CPU_4bit
(
	input wire clk,
	input wire n_rst,
	
	output wire state,
	output wire halt,
	
	output wire [3:0] a_reg_q,
	output wire [3:0] b_reg_q,
	output wire [3:0] out_reg_q,
	
	input wire [3:0] in_d
);

//=====================================
//Global Clock
//=====================================
wire g_clk;
wire ld_en;
reg halt_reg;
assign g_clk = clk & halt_reg;

always@(posedge clk, negedge n_rst)
begin 
	if(!n_rst)
		halt_reg <= 1'b1;
	else if (n_halt == 1'b0 & ld_en == 1'b1)
		halt_reg <= 1'b0;
	else
		halt_reg <= halt_reg;
end

//=====================================
//A Register
//=====================================
wire [3:0] a_reg_d;
wire a_reg_ld;
REGISTER a_reg(
	.clk(g_clk),
	.n_rst(n_rst),
	.ld(a_reg_ld),
	.d(a_reg_d),
	.q(a_reg_q)
);

//=====================================
//B Register
//=====================================
wire [3:0] b_reg_d;
wire b_reg_ld;
REGISTER b_reg(
	.clk(g_clk),
	.n_rst(n_rst),
	.ld(b_reg_ld),
	.d(b_reg_d),
	.q(b_reg_q)
);

//=====================================
//OUT Register
//=====================================
wire [3:0] out_reg_d;
wire out_reg_ld;
REGISTER out_reg(
	.clk(g_clk),
	.n_rst(n_rst),
	.ld(out_reg_ld),
	.d(out_reg_d),
	.q(out_reg_q)
);

//=====================================
//Program Counter
//=====================================
wire [3:0] pc_d;
wire [3:0] pc_q;
wire pc_ld;
wire pc_up;
PC pc(
	.clk(g_clk),
	.n_rst(n_rst),
	.ld(pc_ld),
	.up(pc_up),
	.d(pc_d),
	.q(pc_q)
);

//=====================================
//ROM
//=====================================
wire [3:0] rom_data_h;
wire [3:0] rom_data_l;
ROM rom(
	.address(pc_q),
	.data_h(rom_data_h),
	.data_l(rom_data_l)
);

//=====================================
//ALU
//=====================================
wire [3:0] alu_b;
wire alu_as;
wire alu_ld;
wire alu_mux_a;
wire alu_mux_b;
wire [3:0] alu_q;
wire c;
wire z;
ALU alu(
	.clk(g_clk),
	.n_rst(n_rst),
	
	.a(a_reg_q),
	.b(alu_b),
	.as(alu_as),
	.ld(alu_ld),
	.mux_a(alu_mux_a),
	.mux_b(alu_mux_b),
	
	.q(alu_q),
	.c(c),
	.z(z)	
);

//=====================================
//State Machine
//=====================================
STATE_MACHINE state_machine(
	.clk(g_clk),
	.n_rst(n_rst),
	.pc_ld(pc_ld),
	.ld_en(ld_en),
	.pc_up(pc_up)
);

//=====================================
//Instruction Decoder
//=====================================
wire b_reg_oe;
wire alu_oe;
wire out_oe;
wire n_halt;
ID id(
	.d(rom_data_h),
	.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_reg_ld),
	
	.pc_ld(pc_ld),

	.n_halt(n_halt)
);

//=====================================
//State signal
//=====================================
assign state = pc_up;

//=====================================
//Halt signal
//=====================================
assign halt = ~halt_reg;

//=====================================
//Bus 1: ALU input B
//=====================================
assign alu_b = (b_reg_oe) ? b_reg_q : rom_data_l;

//=====================================
//Bus 2: ALU output
//=====================================
assign a_reg_d = (alu_oe) ? alu_q : in_d;
assign b_reg_d = (alu_oe) ? alu_q : in_d;
assign pc_d = (alu_oe) ? alu_q : in_d;
assign out_reg_d = (alu_oe) ? alu_q : in_d;

endmodule

`default_nettype wire

リスト1 CPUのトップ・モジュールのVerilogソース・コード“CPU_4bit.v”

FPGAのピン配置

トップ・モジュールを含めたすべてのVerilogソース・コードが準備できたら,次はQuartus Prime上でFPGAのピン配置を設定します.“Pin Planner”ツールを起動して,表1のように設定します.

表1 FPGAのピン・アサイン

“clk”入力,“n_rst”入力,“in_d0”から“in_d3”までの入力ポートの端子は「シュミット・トリガ」として設定しておきます.図2のように“Pin Planner”の“I/O Standard”の欄で“2.5V Schmitt Trigger”を選択すれば,シュミット・トリガ入力になります.

図2 FPGAの入力ピンをシュミット・トリガに設定する方法

FPGAのテスト・ボードを作る

FPGA内に作成したCPUの動作を確認するためのテスト・ボードの回路図を図3に示します.基本的には,入出力ポートをそのまま外部に引き出しただけの構成です.クロック回路やリセット回路で使っているNOTゲートやシュミット・トリガは,「ディジタル回路ブロック」を使うことを想定しています.この部分に一般の汎用ロジック(4000シリーズや74HCシリーズなど)を使っても構いません.実際の製作例を写真2に示します.

図3 FPGAの内部に作成したCPUで実験するためのテスト・ボードの回路図
写真2 FPGAのテスト・ボードの製作例

プログラム例(1):加算プログラム(ループ処理なし)

完成させたCPUを実際に動かしてみましょう.今回設計したCPUの命令一覧を表2に示します.

表2 “CPU1738”の命令一覧

CPU自作キット“CPU1738”を「プログラム」するには,ROM基板のDIPスイッチを使います.アドレスごとに各ビットの“1”あるいは“0”を切り替えて,機械語を手作業で書き込みます(ハンド・アセンブル).

また,FPGA内に作成したCPUを動かす場合は“ROM”モジュール(ソース・コードは“ROM.v”)の中にプログラムを記述します.これはCPUの機械語を“1”と“0”で記述する作業になるので,「テキスト・エディタ上でバイナリを直接編集する」といった感覚になります.

さっそく,簡単な加算命令を実行するプログラムを作ってみましょう.表3に示すのは,“$1+2+3+4+5=15$”という計算を実行するプログラムです.途中計算はAレジスタを利用して行い,最終結果をOUTポートに出力します.

このプログラムを実行している様子は,YouTube上の動画(https://www.youtube.com/watch?v=aWBeJm-EizI)で見ることができます.

表3 “$1+2+3+4+5=15$”を計算するプログラム(ループ処理なし)
動画1 CPU1738で,“$1+2+3+4+5=15$”の計算を実行している様子

プログラム例(2):加算プログラム(ループ処理あり)

表4に示すプログラムでは,条件分岐命令を使ってループ処理を実装しています.この繰り返し処理は,C言語におけるfor文に相当します.計算する内容は,先ほどと同じく“$1+2+3+4+5=15$”です.

このプログラムでは,最初にBレジスタに格納した値“$n$”を「加算の回数」として扱い,ループ処理によって“$1+2+\cdots + n$”を計算します.ただし,扱える値の範囲は4ビットなので,桁あふれを発生させずに実行できるのは“$n\leqq 5$”の範囲です.

プログラム中でループ処理を行うためには,加算(あるいは減算)の対象となる2つの値を格納するレジスタに加えて,途中の計算結果を格納するレジスタが必要となります.すなわち,合計3つのレジスタを用意する必要があります.“CPU1738”は「Aレジスタ」と「Bレジスタ」の2つしか汎用レジスタを持たないので,ここでは「OUTレジスタ」に計算の途中結果を保存するようにしています.ただし,OUTレジスタの出力はCPU内部から参照できないので,このプログラムを実行するときはOUTポートとINポートをケーブルで接続します.こうすることで,“IN A”命令によってOUTレジスタの内容をAレジスタに読み込むことができます(表4の9番地(1001番地)の処理).

表4 “$1+2+3+4+5=15$”を計算するプログラム(ループ処理あり)
このプログラムを実行するときはOUTポートとINポートをケーブルで接続する

プログラム例(3):乗算プログラム

表5に,乗算を行うプログラムを示します.

アドレス1番地でBレジスタに格納される値“$b$”と,アドレス2番地でAレジスタに加算される値“$a$”にもとづいて,“$a\times b$”を計算して結果をOUTレジスタに格納します.ただし,例によって扱える値の範囲は4ビットまでなので,計算結果が“$(15)_{10}$”以下の計算しか扱えません.今回は例として“$3\times 4=12$”という計算を行っています.

乗算の処理は,指定した回数だけ加算を繰り返すことで実現しています.ループ処理を使っているので,このプログラムを実行するときはOUTレジスタの出力端子とINポートの入力端子をケーブルで接続する必要があります.

表5 “$3\times4=12$”を計算するプログラム
このプログラムを実行するときはOUTポートとINポートをケーブルで接続する

プログラム例(4):除算プログラム

表6に,除算を行うプログラムを示します.

アドレス0番地でAレジスタに格納する値“$a$”をアドレス2番地で定義する値“$b$”で割り算し,“$a\div b$”の結果(商)をOUTレジスタに格納します.また,割り算のあまり(剰余)をAレジスタに格納します.今回は例として,“$9 \div 2 = 4$あまり$1$”という計算を行っています.

割り算の処理は,計算結果が負の値になるまで引き算を繰り返し,その回数を数えることで実現しています.このCPUのALUは,「符号なし2進数の減算」の結果が負のときに$C$フラグとして“$C=0$”を出力します.この性質を利用して,割り算が終了したか否かを判定しています(アドレス3番地の処理が該当する).

なお,このプログラムでもループ処理を使っているので,例によってOUTポートとINポートをケーブルで接続する必要があります.

表6 “$9\div 2=4$あまり$1$”を計算するプログラム
このプログラムを実行するときはOUTポートとINポートをケーブルで接続する

プログラム例(5):「まるごとCPUロボット」を作る

ロボットの仕様

今回作ったCPUを利用して,動作を自由にプログラミングできるロボットを作ってみましょう.

“CPU1738”には4ビットの出力ポートおよび入力ポートがあります.そこで,表7のように各ビットの役割を決めれば,2個のモータのON・OFFおよび正転・反転をコントロールすることができます.また,ロボットの正面にプッシュ・スイッチを取り付けてINポートに接続すれば,ロボットが障害物にぶつかったことを検知できます.今回はINポートの“$D_1$”から“$D_3$”は使いません.

表7 ロボットの制御に使うI/Oの割り当て

ここでは例として,図4のような動作をプログラミングすることにします.通常動作時は「前進」を続けます.ロボットの正面に取り付けられたスイッチによって障害物を検知したら,「後退」→「停止」→「回転」という動きによって方向転換をします.方向転換をした後は再び「前進」を続けます.

図4 これから作るロボットの動作

ロボットのシャーシ部分を作る

表8の部品を使って,簡単なシャーシを作ります.「ウォームギヤーボックス HE」のギア比は“336:1”にしてください.このギア比(ロボットが進むスピード)にあわせて,後で作るプログラムを調整します.完成したシャーシを写真3に示します.

表8 ロボットのシャーシ部分の部品表
写真3 ロボットのシャーシ部分

モータ・ドライバの回路を作る

モータを動かすための「モータ・ドライバ」の回路を作ります.本連載の趣旨は「CPUをすべてトランジスタで作る」というものなので,モータ・ドライバもICを使わずにトランジスタだけで作ることにします.モータ・ドライバの部品表を表9に示します.また,モータ・ドライバの回路図を図5に示します.

今回のモータ・ドライバの回路には「リレー」(relay)を使います.1個のモータを制御するために2個のリレーを使用します.1つ目のリレーはモータに流れる電流をON・OFFするために使い,2つ目のリレーはモータに流れる電流の向きを反転させるために使います.

なお,リレー内のコイル(インダクタ)に電流をON・OFFする瞬間は,「ファラデーの電磁誘導の法則」によってコイルの両端に大きな誘導起電力が発生します.特に,電流をOFFする瞬間はトランジスタのコレクタ・エミッタ間に電源電圧よりも大きい電圧が印加されてトランジスタが破損する可能性があります.これを避けるために,コイルと並列にダイオード(還流ダイオード)を接続して,誘導電流をダイオードに逃がすようにします.このようにすると,コレクタの電位は“$V_{\mathrm{DD}} + V_{\mathrm{F}}$”以下になるのでトランジスタを守ることができます.ダイオードを取り付けるときは,向きに注意してください.

モータ・ドライバの電源はモータの定格に合わせて3Vとします.ロボットの電源とCPUの電源は両方とも3Vとなりますが,今回は2つの電源を分けて用意することにします.ロボットの正面に取り付けるスイッチの電源は,CPU側の電源を利用します.

今回使っているリレーは$3 \mathrm{V}$品で,コイルの励磁電流は$50 \mathrm{mA}$程度です.また,接点側の許容電流は$2 \mathrm{A}$です.まったく同じリレーを使う必要はありませんが,コイル電圧と接点の電流容量は同程度の物を選ぶようにしてください.

今回使っているトランジスタは“$V_{\mathrm{ce}}=160 \mathrm{V}$,“$I_{\mathrm{c}}=600 \mathrm{mA}$”のNPN型トランジスタです.他のNPN型トランジスタを使う場合は“$V_{\mathrm{ce}}$”が$50 \mathrm{V}$以上,“$I_{\mathrm{c}}$”はリレーの励磁電流の2倍程度を流せるものを選ぶことをお勧めします.

表9 ロボットのモータ・ドライバ部分の部品表
図5 ロボット用モータ・ドライバの回路図

CPU本体とロボット用のシャーシを合体させる

CPU本体とロボットのシャーシを合体させると,写真4のようになります.シャーシの前方に取り付けたスイッチがCPU本体で隠れないように,スイッチを取り付ける位置を調整してください.

写真4 “CPU1738”とロボットのシャーシを合体させたようす

「自動方向転換ロボット」のプログラム例

自動方向転換ロボットのプログラムを表10に示します.

ロボットの正面に取り付けられたスイッチはプル・アップされており,それ以外のINポートのビットはプル・ダウンされています.よって,スイッチが押されていない状態ではINポートの値は“$(0001)_2$”となります.また,スイッチが押されたときのINポートの値は“$(0000)_2$”となります.そのため,INポートの値が$“(0000)_2$”になったら方向転換の動作を開始するようにプログラムを作っています.

表10のプログラムでは,「後退」や「右回転」をする時間を何もしないループ処理によって生み出しています.クロック周波数を大きくしすぎると,この時間稼ぎが難しくなります(時間稼ぎのループが一瞬で終わってしまう).逆に,クロック周波数が小さすぎるとスイッチをチェックするサイクルが長くなるので障害物を検知するのが遅れてしまいます.今回は適当な値として,“20 Hz”のクロックで動かすことにします.

ギア比を“336:1”にしてクロック周波数を“20 Hz”で動かすと,表10のプログラムでちょうどよく方向転換の動作をするはずです.

問題なく動作することが確認できたら,ループの時間やモータ・ドライバに対する出力値を変更して,ロボットの動きを自分の好きなように設定してみてください.

表10 自動方向転換ロボットのプログラム

このロボットが動作している様子は,YouTube上の動画(https://www.youtube.com/watch?v=yfelmQOKYK4)で見ることができます.

動画2 CPU1738ロボットが自走しているところ

(c)2021 Nobuyasu Beppu