オール・トランジスタ4ビットCPUの製作とFPGA開発
[Vol.5 ステート・マシンと命令デコーダの設計]
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】
ステート・マシンの設計
ステート・マシンの役割
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ステップとなります.
これから作るCPUの命令サイクル
今回のCPUでは,回路を簡単にするためにROMが出力するデータをレジスタなどに保存せず,そのまま命令デコーダに接続する設計にしています.そのため,「命令フェッチ」と「命令デコード」が同時に行われます.また,命令デコードと同時にALUに対する制御信号入力やデータ入力が行われるので,ALUによる「命令実行」も同時に行われます.その結果として,図2に示すように命令をROMから読み出したタイミングで「フェッチ」,「デコード」,「実行」が一度に行われることになります.これに続くクロックでレジスタに演算結果を書き込む「ライト・バック」が行われ,1連の命令サイクルが終了します.
これ以降は,CPUが「フェッチ」,「デコード」,「実行」を行っている状態を「状態0」と呼び,「ライト・バック」を行っている状態を「状態1」と呼ぶことにします.
ステート・マシンの動作:ジャンプなし命令
ステート・マシンの仕様を具体的に検討します.
これから作る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は以上の動作を繰り返して,次々と命令を処理していきます.
ステート・マシンの動作:ジャンプ命令
続いて,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が連続することになります.
ステート・マシンの状態遷移図
ここまでの内容をまとめると,ステート・マシンの挙動を図5のように表すことができます.
このような図を「状態遷移図」(state transition diagram)といいます.
実行する命令がジャンプ命令か否かで,IDが出力する“$PC\_LD$”の値が変化します.「ジャンプなし命令」のときは“$PC\_LD = 0$”となります.ステート・マシンはこれを受けて,「状態0」の次に「状態1」となる動作をします.「状態1」になった後は必ず「状態0」に戻ります.
これに対して,「ジャンプ命令」のときは“$PC\_LD = 1$”となります.この場合,ステート・マシンは「状態0」の後に再び「状態0」になります.
これから,図5の状態遷移図を実現するようにステート・マシンの回路を設計します.
ステート・マシンの入出力仕様
図6に,ステート・マシンの入出力端子を示します.
“$PC\_LD$”は先に説明したとおりステート・マシンの動作を制御する入力端子です.また“$\overline{HALT}$”は“HALT”(ホールト)命令を実行するときに“0”を入力する端子で,このときCPUは動作を停止します.
なお,今回の設計ではステート・マシンのブロックの中に「クロック回路」と「リセット回路」も実装することにします.これにともない,図6にはクロック信号の出力端子“$CK$”およびリセット信号の出力端子“$\overline{RST}$”を追加しています.
ステート・マシンの内部ブロック図
図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のブロック図をもとにして論理ゲート・レベルでステート・マシンの回路を作ると,図8のようになります.FF1およびFF2は,例によって「非同期リセット付きポジティブ・エッジ・トリガ型D-FF」で構成しています.
クロック信号を発生させる“CLOCK”のブロックは,NOTゲートによる発振回路で構成しました.発振回路の周波数を決めるキャパシタの容量は,スイッチで切り替えられるようにしてあります.キャパシタのスイッチをすべてONにするとおよそ2 Hz,すべてOFFにするとおよそ200 kHzのクロックが得られます.
また,クロック信号は手動のスイッチで入力することもできます.スイッチの出力はシュミット・トリガで整形してから利用するようにしています.
リセット信号を発生させる“RESET”のブロックでも,リセット用のスイッチが発生させる波形をシュミット・トリガで整形してCPU全体に供給しています.
写真1は,図8に示したステート・マシンの回路を「ディジタル回路ブロック」で作ったものです.これがCPUキット“CPU1738”の「STATE MACHINE基板」となります.論理ゲート数は20個,使用しているトランジスタ数は100個です.
ステート・マシンをFPGA上に実装する
FPGA内に実装するステート・マシンの仕様検討
ここまで考えてきた「トランジスタ・レベル」の回路では,CPU全体に対して供給するクロック信号およびリセット信号を「ステート・マシン」のブロックに含めていました.これに対して,FPGAを使う場合は外部からクロック信号やリセット信号を与えるのが一般的です.そこで,外部からこれらの信号を受け取るように,ステート・マシンの仕様を変更することにします.
図9に,これからFPGA内に実装するステート・マシンの入出力端子を示します.入力端子として,クロック端子“$CK$”およびリセット端子“$\overline{RST}$”を用意しています.また,“HALT”命令を実行する回路は上位階層(トップ・モジュール)に実装するので,ここでは“$\overline{HALT}$”端子を用意していません.
Verilogソース・コード
図9の仕様にしたがってステート・マシンを実装するためのVerilog記述例をリスト1に示します.“HALT”命令に関係するフリップフロップは上位階層に置くので,ここではCPUのステート(状態0か状態1)を管理するフリップフロップ“state”だけを用意しています.出力“$PC\_UP$”および“$LD\_EN$”は,いずれもこのフリップフロップの出力を引き出したものになっています.
|
---|
リスト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に示す結果が得られます.問題なく動作していることが確認できます.
|
---|
リスト2 ステート・マシンのテスト・ベンチ“tb\_state\_machine.v” |
|
---|
リスト3 ステート・マシンのシミュレーションのためのスクリプト・ファイル“tb\_state\_machine.do” |
命令デコーダ(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における信号名は“[回路ブロック名]\_[その回路ブロックにおける端子名]”という形式にしています.
各命令を実行するときのCPUの動作
図12および図13に,各命令を実行するときのデータの流れを示します.IDは,これらの図における太い矢印が通過する経路の回路ブロックをアクティブにします.なお,“※”印がついている経路は動作とは関係ありませんが,IDの回路を簡略化するために有効にしています.
IDの真理値表
図12および図13の動作を実現するIDの真理値表を作ると,表1のようになります.なお,“$PC\_LD = 1$”となる条件は少しややこしいので,別途考えることにします.
表1において“※”印がついている信号線は,ステート・マシンが出力する“$LD\_EN$”信号とのANDをとって出力します.これはCPUのステートが「状態1」のときだけレジスタに対する書き込みを許可するためです.また,表1における“(1)”という表記はIDの回路を簡略化するために“1”にしていることを意味しています.
“$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のようになります.
IDを論理ゲート・レベルで作る
以上の内容を実現するIDの回路を論理ゲート・レベルで作ると,図1のようになります. これはあくまで一例で,何通りもの実装方法が考えられます. もっと最適化して,トランジスタ数を減らすことも可能です.
ここでは回路図の読みやすさとトランジスタ数のバランスを考えて図14の形にまとめました.
写真2は,図14に示したIDの回路を「ディジタル回路ブロック」で作ったものです.これがCPUキット“CPU1738”の「ID基板」となります.論理ゲート数は50個,使用トランジスタ数は216個です.
IDをFPGA上に実装する
Verilogソース・コード
リスト4に,IDの機能を実現するためのVerilogの記述例を示します.表1および表2に示す真理値表のとおりに動作するように,functionや条件演算子を利用して記述しています.
真理値表から図14の回路図を起こして,さらに論理ゲートで実装するのはかなり骨が折れる作業でした.これに対して,FPGAを使えば簡潔なVerilog記述によってスマートに論理回路を実装することができます(FPGAは便利ですね).
|
---|
リスト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に示す結果が得られます.問題なく動作していることが確認できます.
|
---|
リスト5 IDのテスト・ベンチ“tb\_id.v” |
|
---|
リスト6 IDのシミュレーションのためのスクリプト・ファイル“tb\_id.do” |