オール・トランジスタ4ビットCPUの製作とFPGA開発
[Vol.4 CPUのROM,PC,ALUの設計]
ALU,レジスタ,I/Oなどをトランジスタ・レベルで手作りし,さらにFPGAにも実装
- 著者・講師:別府 伸耕/Nobuyasu Beppu (リニア・テック)
- 企画編集・主催: ZEPエンジニアリング株式会社
- 関連製品:[VOD/KIT]実習キットでできる!ラズパイPicoでマイコン入門
- 関連製品:[VOD/KIT]実習キットでできる!ラズパイPico×Wi-FiモジュールでIoT超入門
- 関連製品:[VOD/KIT]一緒に動かそう!Lチカから始めるFPGA開発【基礎編】
- 関連製品:[VOD/KIT]STM32マイコン&Wi-Fiモジュールで学ぶ C/C++プログラミング入門
- 関連製品:[VOD/KIT]実習キットで一緒に作る!オープンソースCPU RISC-V入門
- 関連製品:[VOD/KIT]Python×実習キット×スマホでできる!ESP32マイコン活用術
【Index】
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)に接続されます.
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ビットだけに入れることにします.
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).
スイッチ・マトリクスの回路図
図5に,ROMのスイッチ・マトリクス部分の回路図を示します.8ビットのDIPスイッチを16個並べて構成しています.スイッチの出力部分にはLEDを取り付けているので,該当するアドレスのデータが読み出されるときにデータが“1”のビットが光ります.
各スイッチに接続されている“Encoder xxxx”という信号線は,「4 to 16デコーダ」の出力です.また,“SW_data x”という信号線は次段の「出力バッファ」に接続します.
スイッチ・マトリクスで使っているダイオードの役割
スイッチの出力部分に取り付けているダイオードの役割について,図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}$”程度です.順方向電圧が小さいものであれば,これ以外のダイオードを使っても構いません.
出力部分の回路図
ROMの出力部分の回路図を図7に示します.スイッチの出力部分にはダイオードが入っているので,出力が“0”レベルのときに「吸い込み電流」を流すことができません.そこで,$10\ \mathrm{k\Omega}$のプル・ダウン抵抗を取り付けることで安定して“0”レベルを出力できるようにしています.また,下位4ビットには例によってトライ・ステート・バッファを挿入しています.
「ディジタル回路ブロック」によるROM回路の構成例
ROMの回路を図3,図5,図7の回路図にもとづいて作ったものを写真1に示します.これがCPUキット“CPU1738”の「ROM基板」となります.この回路には32個の論理ゲートが使われており,トランジスタの数は合計136個です.
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全体のデバッグをするときに使います.
|
---|
リスト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”端子から出力されていることが確認できます.
|
---|
リスト3 ROM のシミュレーションのためのスクリプト・ファイル“tb_rom.do” |
プログラム・カウンタ(PC)の設計
PCの入出力仕様
プログラム・カウンタ(PC)は,ROMに対してアドレス信号を出力する回路です.通常の命令を実行するときは,PCはアドレスの値を“$+1$”するカウンタとして動作します.また,「ジャンプ命令」を実行するときは外部から入力された値を読み込んで,出力データを上書きする動作を行います.このような機能を実現するために,PCの入出力端子を図9のように定めます.“$CK$”はクロック入力,“$\overline{RST}$”は非同期リセット入力で,“$D_0$”から“$D_3$”は値を上書きするためのデータ入力です.“$Q_0$”から“$Q_3$”はROMに対するアドレス信号を出力する端子です.“$LD$”および“$UP$”は制御入力で,これらの値によって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に記憶させることができます.
PCを論理ゲート・レベルで作る
図10のブロック図をもとにして論理ゲート・レベルでPCの回路を作ると,図11のようになります.6個のNANDゲートで構成されるD-FFを4ビット分並べたものが基本になっていて,そこに3入力のMUXや“$+1$”の演算をするために最適化された加算器が接続されています.
写真2は,図11に示したPCの回路を「ディジタル回路ブロック」で作ったものです.これがCPUキット“CPU1738”の「PC基板」となります.論理ゲート数は49個,使用しているトランジスタの数は264個です.
PCをFPGA上に実装する
Verilogソース・コード
リスト4に,PCの機能を実現するためのVerilogの記述例を示します.入出力端子は図9と同じ仕様で,「4ビットのレジスタ」を中心に回路を組み立てています.レジスタ“q”に代入する値を,制御入力“ld”および“up”の値に応じて切り替えています.
|
---|
リスト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$”が読み込まれ,そこからカウント・アップの動作が再開されていることがわかります.
|
---|
リスト5 PC のテスト・ベンチ“tb_pc.v” |
|
---|
リスト6 PC のシミュレーションのためのスクリプト・ファイル“tb_pc.do” |
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の演算部の真理値表」のところで解説します.
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)を設計するときは,これらの機能を積極的に使います.
ALUのフラグの特性表
表3に,ALUのフラグ出力の特性表を示します.
ALUの$C$フラグおよび$Z$フラグは記憶回路(フリップフロップ)によって保持される値で,“$LD=1$”のときだけ更新されるものとします.先に説明したとおり,計算結果に繰り上がりがある場合は“$C=1$”となり,計算結果が“0”の場合は“$Z=1$”となります.
ALUの内部ブロック図
図14にALUの内部ブロック図を示します.4ビットの加減算器を中心として,入力部分には信号を切り替えるためのMUX,出力部分にはトライ・ステート・バッファが接続されています.また,$Z$フラグを管理するフリップフロップと,$C$フラグを管理するフリップフロップも備えています.
ALUを論理ゲート・レベルで作る
図14のブロック図をもとにして論理ゲート・レベルでALUの回路を作ると,図15のようになります.4ビットの全加算器の入力部分に「“$AS$”入力の値に応じて信号を反転させる回路」(EXORゲート)を挿入することで,4ビットの加減算器を構成しています.また,入力部分のマルチプレクサの役割は「信号を通すか“0000”(2進)にするか」を切り替えることなので,単純に加減算器の入力にANDゲートを入れることで実装しています.
$C$フラグおよび$Z$フラグを管理するフリップフロップは,例によって「非同期リセット付きのポジティブ・エッジ・トリガ型D-FF」を利用しています.
写真3は,図15に示したALUの回路を「ディジタル回路ブロック」で作ったものです.これがCPUキット“CPU1738”の「ALU基板」となります.論理ゲート数は58個,トランジスタの数は376個です.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]”という具合に「リダクション演算子」を使って表記しました.
|
---|
リスト7 ALU のVerilog ソース・コード“ALU.v” |
ALUの動作を論理シミュレーションで確認する
リスト7で定義したモジュール“ALU”の動作を論理シミュレーションで確認してみます.リスト8にテスト・ベンチの記述例を示します.
“alu”という名前でALUモジュールのインスタンスを作っています.今回はすべての演算パターンを検証せずに,おおよその動作が正しく行われることを確認するにとどめています.前半は“$as=0$”として加算モードの動作確認,後半は“$as=1$”として減算モードの動作確認をしています.また,最後に“$ld=0$”の場合はフラグが更新されないことを確認しています.
リスト9のスクリプト・ファイルを利用してModelSimでシミュレーションを実行すると,図16に示す結果が得られます.加算および減算の演算処理が正常に行われていることがわかります.また,$C$フラグおよび$Z$フラグの挙動も問題ないことが確認できます.
|
---|
リスト8 ALU のテスト・ベンチ“tb_alu.v” |
|
---|
リスト9 ALU のシミュレーションのためのスクリプト・ファイル“tb_alu.do” |
関連資料
-
CPU組み立てキット(ロボット用パーツ付き) CPU1738の取扱説明書
こちらからダウンロードしてください