シンプルなCPUを作ってみよう (その2)

         −VHDLを用いた全体設計−

信州大学工学部  井澤裕司


1.はじめに

第1章(その1) では,オリジナルの 15bit RISC型プロセッサの命令セットを決定し,
C言語 を用いて その 「CPUシミュレータ」 を制作しました.

本章から, ハードウェア記述言語(Hardware Description Language) を用いて,実際の
プロセッサを設計します.
設計した回路のデータは, FPGA(Field Programmable Gate Array) という半導体デバイスに
ダウンロードして,動作させます.
ハードウェア記述言語 には,国際的に広く普及している VHDL を使用します.

このコンテンツの中で, VHDLの記述法 を詳細に説明することはできませんので,参考書や
インターネット等で勉強し,使いこなせるよう習熟して下さい.

なお,本章では命令セットの設計と同様,表記の分かりやすさを優先させているため,
メモリ関連 ハードウェアは大幅に 簡略化 しています.
例えば, プログラムROM(PROM) の内容を, VHDL の中で記述しています.
また,今回 RAMの容量 も小さく設定しているので, D-FF を用いて記述しています.

このような方法は必ずしも現実的な手法ではありませんが, VHDLのマクロファンクション を用いて,
外部ファイル からPROMのデータを読み込む手法や, 配列 を用いてRAMを記述する方法については,
第4章 で説明します.

本章では, CPUの基本的な動作 について説明した後, 全体の設計 を行います.

下の階層に位置する 個別の回路(コンポーネント) については,次の 第3章 で解説しますので,
機能を大きく捉え信号の受け渡しを中心に理解するようにして下さい.

それでは, CPUの構成 から説明しましょう.


2. CPUの構成

2.1 基本的なCPUの動作(ステージ)

はじめに,基本的なCPUの動作について復習します.

下の図は,CPUの動作を模式的に表したものです.
1命令の動作は4つのステージ,すなわち

(1) フェッチ 命令コードをPROMから読み込む
(2) デコード 読み込んだ命令コードを解析し、実行の準備を行う
(3) 実行 CPU内部の演算ユニット(ALU)で命令を実行する
(4) ライトバック 実行結果をレジスタや RAMに書き込む

から構成されています。

各ステージが1クロックで実行されるとき,1つの命令が実行されるのに 4クロック を要します.




2.2 CPUの基本構成

今回設計するCPUも,
 @ フェッチ
 A デコード
 B 実行
 C ライトバック
4ステージ で構成されます.

第1章 で示した ブロック構成図 を,もう一度示します.
図の左が @フェッチ ,右が Cライトバック のステージに対応します.
ステージの推移に伴い,左から右に向かって,必要なデータに変換され受け渡されてゆく構成となっている
ことを理解して下さい.


2.3 基本的なCPUのステージ

CPUの詳細なブロック構成と,各部の具体的な信号名を以下に示します.




2.4 CPUのタイムチャート

上で示した信号のタイムチャートを,以下の図に示します.

4つのステージ( @フェッチ Aデコード B実行 Cライトバック )で,必要な信号が生成され,次のステージに
受け渡される構成となっています.

例えば,デコードのステージでは,前段のフェッチの出力に対して論理演算(組合せ回路)が行われ,
その結果をラッチした信号が出力されます.
その信号は,次の実行ステージの入力となります.






3. CPUの全体設計

3.1 ブロック分割と各コンポーネントの設計


本節から,VHDLを用いたCPUの記述例について解説します.

このCPUは,以下に示す5つのブロックと,9個のコンポーネント回路により構成されています.
以下,簡単にその内容を説明します.

(1) クロック発生用ブロック

このブロックでは,基本クロックを基に,CPUの各ブロックに供給する4相のクロックを生成します.

入 力 基本クロック  CLK
出 力 @フェッチ,Aデコード,B実行,Cライトバックの各ブロックに供給する4つのクロック信号
CLK_FT ,CLK_DC, CLK_EX ,CLK_WB
クロック 基本クロック  CLK

これらのクロックはタイムチャートの図に示すように,互いに重なることがない
4相構造になっています.
なお,VHDLのコンポーネント名は  clk_gen.vhd  です.


(2) フェッチ(Fetch)ブロック

フェッチブロックの入出力は,以下のようになっています.

入 力 プログラムカウンタ(PC)の出力 P_COUNT (8bit)
出 力 カウンタをアドレスとするProgram ROMのデータ PROM_OUT (15bit)
クロック 第1相のフェッチクロック  CLK_FT

すなわち,プログラムカウンタ(PC)を入力とするPROM(Program ROM)のデータ(15bit)を
フェッチのタイミング(CLK_FT)でラッチし,出力します.
VHDLのコンポーネント名は  fetch.vhd  です.

一般に,プログラム(PROM)のデータは,CPUの回路から独立させ,別ファイルで記述します.
しかしながら,この例は分かり易さを優先させるため,VHDLの中でそれらのデータを記述しています.
第4章では,外部ファイルの形式でデータを読み込む方法について紹介します.


(3) デコード(Decode)ブロック

デコードブロックは, @ decode.vhd,A reg_dc.vhd,B ram_dc.vhd の3つのコンポーネントに分けて
記述しています

@ decode.vhd の入出力は,以下のようになっています.

入 力 ラッチされたProgram ROMのデータ出力 PROM_OUT (15bit)
出 力 オペコード OP_CODE (4bit)およびデータ(アドレス)のオペランド OP_DATA (8bit)
クロック 第2相のデコードクロック CLK_DC

A reg_dc.vhd の入出力は,以下の通りです.

入 力 Program ROMが示すレジスタA,Bの番号 N_REG_IN (3bit)
8個のレジスタの出力 REG_0〜7 (16bit)
出 力 レジスタA,Bの番号 N_REG_OUT (3bit)
レジスタA,Bの番号に対応するレジスタのデータ REG_OUT (16bit)
クロック 第2相のデコードクロック CLK_DC

B ram_dc.vhd の入出力を,以下に示します.

入 力 Program ROMが示すRAMのアドレス RAM_ADDR (8bit)
8個のRAM出力 RAM_0〜7 (16bit)
メモリマップドI/Oの入力データ IO65_IN (16bit)
出 力 アドレスで指定された RAMのデータ RAM_OUT (16bit)
クロック 第2相のデコードクロック CLK_DC



(4) 実行(Execute)ブロック

実行ブロックの VHDLコンポーネントは  exec.vhd  であり,
その入出力は以下のようになっています.

入 力 システムリセット入力 RESET
プログラムカウンタ(PC) PC_IN (8bit)
オペコード OP_CODE (4bit)およびデータ(アドレス)のオペランド OP_DATA (8bit)
レジスタ指定オペランド(3bit)に対応するレジスタのデータ REG_A (16bit), REG_B (16bit)
アドレス オペランド(8bit)に対応するRAMのデータ RAM_OUT (16bit)
出 力 プログラムカウンタ(PC) PC_OUT (8bit)
レジスタの演算結果 REG_IN (16bit)
レジスタへの書き込み許可信号 REG_WEN
アドレス オペランド(8bit)に対応するRAMのデータ RAM_IN (16bit)
RAMへの書き込み許可信号 RAM_WEN
クロック 第3相の実行クロック CLK_EX



(5) ライトバック(WriteBack)ブロック

ライトバックブロックは,2つのVHDLコンポーネント( @ reg_wb.vhd A ram_wb.vhd )に分けて記述されています.

@ reg_wb.vhd の入出力を,以下に示します.

入 力 レジスタオペランド(3bit)で指定したレジスタの番号 N_REG (3bit)
レジスタの演算結果 REG_IN (16bit)
レジスタへの書き込み許可信号 REG_WEN
出 力 8個のレジスタのデータ REG_0〜7 (16bit)
クロック 第4相のライトバッククロック CLK_WB

A ram_wb.vhd の入出力は,以下のようになっています.
なお,この例ではRAMのデータ RAM_0〜7 を個別に記述しています.
RAMを配列で表し,そのアドレスを配列のインデックスに対応させる方法については,第4章で紹介します.

入 力 アドレスオペランド(8bit)で指定したRAMアドレス RAM_ADDR (8bit)
選択されたレジスタのデータ RAM_IN (16bit)
RAMへの書き込み許可信号 RAM_WEN
出 力 RAMのデータ RAM_0〜7 (16bit) (8種類)
メモリマップドI/Oの出力データ IO64_OUT (16bit)
クロック 第4相のライトバッククロック CLK_WB



3.2 各コンポーネントのタイミング設計

ソフトウェア技術者にとって,ハードウェアの設計で最も難しいと感じるのが,タイミングの問題です.

ここで設計するすべてのコンポーネントはすべて,下の図ような単純な構造になっています.
すなわち,入力信号を組合せ回路の入力とし,その出力をクロックでラッチして出力します.
ラッチによる遅延は,常に1クロックとなる点に注意して下さい.


タイムチャートで示したように,各ステージは,その最後に立ち上がるクロック
でラッチされ出力されます.

ここで1つ質問があります.
本来,EXECブロックのプログラムカウンタ(PC)出力は,ライトバックのタイミングで,ラッチされる
ことになっていますが,信号名のブロック図では,その回路が省略されています.

このような省略により,問題が発生することはないのでしょうか? 考えて下さい.
(ヒント: 4相のクロックを用いている点に注意)


4. CPUのVHDLによる記述例

本節では,このCPUをVHDLにより記述した例(cpu15e.vhd)を示します.

各コンポーネントの詳細については,次章(その3)で説明しますので,
ここではそれらの機能を中心に把握して下さい.


4.1 VHDLを用いたCPUのソースコード(cpu15e.vhd)


-- exec.vhd
-- Y.Izawa
-- H18.3.27

library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.std_logic_unsigned.all;

entity cpu15e is

port
(
CLK : in std_logic;
RESET : in std_logic;
IO65_IN : in std_logic_vector (15 downto 0);
IO64_OUT : out std_logic_vector (15 downto 0)
);

end cpu15e;


architecture RTL of cpu15e is

component   clk_gen
port
(
CLK : in std_logic;
CLK_FT : out std_logic;
CLK_DC : out std_logic ;
CLK_EX : out std_logic;
CLK_WB : out std_logic
);
end component;


component   fetch
port
(
CLK_FT : in std_logic;
P_COUNT : in std_logic_vector (7 downto 0);
PROM_OUT : out std_logic_vector (14 downto 0)
);
end component;

component   decode
port
(
CLK_DC : in std_logic;
PROM_OUT : in std_logic_vector (14 downto 0);
OP_CODE : out std_logic_vector (3 downto 0);
OP_DATA : out std_logic_vector (7 downto 0)
);
end component;


component   reg_dc
port
(
CLK_DC : in std_logic;
REG_0 : in std_logic_vector (15 downto 0);
REG_1 : in std_logic_vector (15 downto 0);
REG_2 : in std_logic_vector (15 downto 0);
REG_3 : in std_logic_vector (15 downto 0);
REG_4 : in std_logic_vector (15 downto 0);
REG_5 : in std_logic_vector (15 downto 0);
REG_6 : in std_logic_vector (15 downto 0);
REG_7 : in std_logic_vector (15 downto 0);
N_REG_IN : in std_logic_vector (2 downto 0);
N_REG_OUT : out std_logic_vector (2 downto 0);
REG_OUT : out std_logic_vector (15 downto 0)
);
end component;


component   ram_dc
port
(
CLK_DC : in std_logic;
RAM_0 : in std_logic_vector (15 downto 0);
RAM_1 : in std_logic_vector (15 downto 0);
RAM_2 : in std_logic_vector (15 downto 0);
RAM_3 : in std_logic_vector (15 downto 0);
RAM_4 : in std_logic_vector (15 downto 0);
RAM_5 : in std_logic_vector (15 downto 0);
RAM_6 : in std_logic_vector (15 downto 0);
RAM_7 : in std_logic_vector (15 downto 0);
IO65_IN : in std_logic_vector (15 downto 0);
RAM_ADDR : in std_logic_vector (7 downto 0);
RAM_OUT : out std_logic_vector (15 downto 0)
);
end component;


component   exec
port
(
CLK_EX : in std_logic;
RESET : in std_logic ;
OP_CODE : in std_logic_vector (3 downto 0);
PC_IN : in std_logic_vector (7 downto 0);
REG_A : in std_logic_vector (15 downto 0);
REG_B : in std_logic_vector (15 downto 0);
OP_DATA : in std_logic_vector (7 downto 0);
RAM_OUT : in std_logic_vector (15 downto 0);
PC_OUT : out std_logic_vector (7 downto 0);
REG_IN : out std_logic_vector (15 downto 0);
RAM_IN : out std_logic_vector (15 downto 0);
REG_WEN : out std_logic ;
RAM_WEN : out std_logic
);
end component;


component   reg_wb
port
(
CLK_WB : in std_logic ;
N_REG : in std_logic_vector (2 downto 0);
REG_IN : in std_logic_vector (15 downto 0);
REG_WEN : in std_logic ;
REG_0WB : out std_logic_vector (15 downto 0);
REG_1WB : out std_logic_vector (15 downto 0);
REG_2WB : out std_logic_vector (15 downto 0);
REG_3WB : out std_logic_vector (15 downto 0);
REG_4WB : out std_logic_vector (15 downto 0);
REG_5WB : out std_logic_vector (15 downto 0);
REG_6WB : out std_logic_vector (15 downto 0);
REG_7WB : out std_logic_vector (15 downto 0)
);
end component


component   ram_wb
port
(
CLK_WB : in std_logic;
RAM_ADDR : in std_logic_vector (7 downto 0);
RAM_IN : in std_logic_vector (15 downto 0);
RAM_WEN : in std_logic;
RAM_0 : out std_logic_vector (15 downto 0);
RAM_1 : out std_logic_vector (15 downto 0);
RAM_2 : out std_logic_vector (15 downto 0);
RAM_3 : out std_logic_vector (15 downto 0);
RAM_4 : out std_logic_vector (15 downto 0);
RAM_5 : out std_logic_vector (15 downto 0);
RAM_6 : out std_logic_vector (15 downto 0);
RAM_7 : out std_logic_vector (15 downto 0);
IO64_OUT : out std_logic_vector (15 downto 0)
);
end component;

signal CLK_FT : std_logic;
signal CLK_DC : std_logic;
signal CLK_EX : std_logic;
signal CLK_WB : std_logic;
signal P_COUNT : std_logic_vector (7 downto 0);
signal PROM_OUT : std_logic_vector (14 downto 0);
signal OP_CODE : std_logic_vector (3 downto 0);
signal OP_DATA : std_logic_vector (7 downto 0);
signal N_REG_A : std_logic_vector (2 downto 0);
signal N_REG_B : std_logic_vector (2 downto 0);
signal REG_IN : std_logic_vector (15 downto 0);
signal REG_A : std_logic_vector (15 downto 0);
signal REG_B : std_logic_vector (15 downto 0);
signal REG_WEN : std_logic;
signal REG_0 : std_logic_vector (15 downto 0);
signal REG_1 : std_logic_vector (15 downto 0);
signal REG_2 : std_logic_vector (15 downto 0);
signal REG_3 : std_logic_vector (15 downto 0);
signal REG_4 : std_logic_vector (15 downto 0);
signal REG_5 : std_logic_vector (15 downto 0);
signal REG_6 : std_logic_vector (15 downto 0);
signal REG_7 : std_logic_vector (15 downto 0);
signal RAM_IN : std_logic_vector (15 downto 0);
signal RAM_OUT : std_logic_vector (15 downto 0);
signal RAM_WEN : std_logic ;
signal RAM_0 : std_logic_vector (15 downto 0);
signal RAM_1 : std_logic_vector (15 downto 0);
signal RAM_2 : std_logic_vector (15 downto 0);
signal RAM_3 : std_logic_vector (15 downto 0);
signal RAM_4 : std_logic_vector (15 downto 0);
signal RAM_5 : std_logic_vector (15 downto 0);
signal RAM_6 : std_logic_vector (15 downto 0);
signal RAM_7 : std_logic_vector (15 downto 0);

begin
C1 : clk_gen port map (CLK, CLK_FT, CLK_DC, CLK_EX, CLK_WB);
C2 : fetch port map (CLK_FT, P_COUNT, PROM_OUT);
C3 : decode port map (CLK_DC, PROM_OUT, OP_CODE, OP_DATA );
C4 : reg_dc port map (CLK_DC, REG_0, REG_1, REG_2, REG_3,
     REG_4, REG_5, REG_6, REG_7,
      PROM_OUT(10 downto 8), N_REG_A, REG_A);
C5 : reg_dc port map (CLK_DC, REG_0, REG_1, REG_2, REG_3,
     REG_4, REG_5, REG_6, REG_7,
      PROM_OUT(7 downto 5), N_REG_B, REG_B);
C6 : ram_dc port map (CLK_DC, RAM_0, RAM_1, RAM_2, RAM_3,
     RAM_4, RAM_5, RAM_6, RAM_7,
     IO65_IN, PROM_OUT(7 downto 0), RAM_OUT);
C7 : exec port map (CLK_EX, RESET, OP_CODE, P_COUNT,
     REG_A, REG_B, OP_DATA, RAM_OUT, P_COUNT,
     REG_IN, RAM_IN, REG_WEN, RAM_WEN);
C8 : reg_wb port map (CLK_WB, N_REG_A, REG_IN, REG_WEN,
     REG_0, REG_1, REG_2, REG_3,
     REG_4, REG_5, REG_6, REG_7);
C9 : ram_wb port map (CLK_WB, OP_DATA, RAM_IN, RAM_WEN,
     RAM_0, RAM_1, RAM_2, RAM_3,
     RAM_4, RAM_5, RAM_6, RAM_7, IO64_OUT);
end RTL;




以下, ソースコード の内容について簡単に補足します.

最初の entity は,このCPUの 入出力信号 を表しています.
基本となるクロック( CLK )と,リセット信号( RESET ),I/Oの入力と出力の信号(16bit)です.
例題のプログラムでは,I/Oの64番地に,演算結果が出力されます.

今回の設計では, メモリマップドI/O 方式を採用しています.
この方式は, メモリアドレス(RAM) の一部を使って,外部からの 入力ポート ,もしくは
外部への 出力ポート として使用するものです.
メモリとI/Oを分離する方式もよく使われていますが,今回は回路系の簡略化のため,
この方式を採用しました.

architecture RTL 文の初めに,利用するコンポーネント(8種類)を定義しています.
これらのファイル(例えば, clk_gen.vhd 等)は,本体( cpu15e.vhd )と同じディレクトリに置く
必要があります.
コンポーネントの機能等については, 3.1 を参照してください.

次に,コンポーネント間の信号を接続するための信号を, signal 文で定義します.

最後に,このCPUで使用する9個のコンポーネント (C1〜C9) を記述し, signal 文で定義した
信号を用いて,コンポーネント間の信号を接続します.

なお,2つのオペランドに対応するレジスタ( REG_A REG_B )の信号を生成する回路は,
同じコンポーネント reg_dc )を使用します.

以上の説明で,理解していただけましたでしょうか?

不明な点が残っている場合は,ソースコードをもう一度注意深く読み返して下さい.



5. 実行例


VHDLの開発ツールには, 波形表示 される シミュレータ の機能があります.

このシミュレータを用いて,ここで設計した CPUの動作 を確認してみましょう.

(1) 起動部分

シミュレータ を起動すると,各部の信号は下の図のように表示されます.
RESET 後,プログラムカウンタ PC の更新により,OPコード(図の OP_OUT )が読み出され,4つのレジスタ
R0_OUT R1_OUT R2_OUT R3_OUT )が所定の動作をしていることが分かります.




(2) 計算終了部

計算終了直前の 各部の信号 は下の図のようになります.
I/Oの 64番地 に,最終的な演算結果の 55 に対応する2進数( 0000000000110111 )が
出力されていることが分かります.


6. まとめ

本章では,CPUの 最上位階層 の回路を, VHDL を用いて記述しました.

下の階層に属する回路は, コンポーネント により記述していますが,その詳細については,
次の 第3章 の中で解説します.



=> 「シンプルなCPUを作ってみよう」 トップページに戻る.