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

 −より実用的なCPUを目指して−

井澤 裕司 


1.はじめに

第2章 と 第3章 では,極めて シンプル な構成の CPU設計 について解説しました.
この設計では,タイミング系が単純になる方針を優先させたため, メモリ(ROMとRAM) を中心に
実用的ではない構成(回路記述)が含まれています.

例えば,フェッチブロックの " fetch.vhd " では,プログラムの ROM(PROM) のデータをVHDLの中で記述しています.
しかし,回路系と ROM のデータは完全に分離し,外部ファイルから ROMデータ を組み込むのが一般的な方法です.

VHDLの 開発ツール は,FPGAを製品化しているいくつかのメーカーから 無償 で提供されています.
それらのシステムには,” メガファンクション ”等の名称で, ROM や RAM をはじめとする高度な機能をもつ
ライブラリ が数多く用意されています.
この ライブラリ を使用することにより,外部ファイルからROMデータを組込むことが可能となります.

一方の RAM については,前回の設計ではレジスタと同様に複数の D型フリップフロップ を用いました.
すなわち,ライトバックブロックの出力をデコードブロックで選択する構成となっています.
しかし, メモリサイズ が増えると, " signal " 文で指定する 内部信号 の数が指数関数的に増えてしまい,
実用的ではありません.
この RAM には,VHDLの 配列(ARRAY) 表現を用いることにします.

このような内容を中心に,本章では より実用的 なCPUの設計法について解説します.


2.新しいCPUの構成


下の図に,新たに設計するCPUのブロック構成と各部の信号名を示します.

PROM に 「 メガファンクション 」 , RAM に 配列 を用いている点が,これまでの設計と異なります.

「 メガファンクション 」 は,ALTERA社が提供するVHDL開発ツール 「 Quatus�U 」 が提供するライブラリであり,
PROM のデータを mif形式 の外部ファイルからロードすることが可能です.
PROMのブロックでは,2種類のクロック ( CLK_WB_DLY , CLK_FT ) を用いている点に注意して下さい.
その理由については,本章の3.3で詳しく説明します.

また,RAM部はVHDLの配列(ARRAY)を用いて記述しています.
これにより,従来2つのコンポーネントにより記述していたRAMの回路は,1つのコンポーネント (ram) に統合されます.









3.メガファンクションを用いたROMの構成


本節では, メガファンクション を用いた ROM の記述法について解説します.

このメガファンクションは,VHDLの開発ツールに依存する内容を多く含んでおり,
今回は,ALTERA社が無償で提供している ”Quartus�U”(Web Edition) を用いた例を示します.
そのソースコードは,メガファンクション・ウィザードを起動することにより,自動的に生成されます.
下の例は,ALTERA社の FLEX10KE というデバイスを用いた場合のもので, 他のデバイスでは
動作しません ので,注意が必要です.

なお,他の開発ツールを用いた場合,別の記述法が必要になりますので,注意して下さい.


3.1 ROMの記述例

ALTERA社の開発ツール”Quartus�U”(Web Edition)の メガファンクション を用いて,
ROMの回路とデータを記述することができます.

メガファンクション・ウィザードを立ち上げ,ROMの仕様(アドレスやデータサイズ等)を設定することにより,
以下のようなソースコードが自動的に出力されます.
その具体的な方法は,上記システムのHELPを参照して下さい.

prom.vhd

-- magafunction wizard: %LPM_ROM%
-- GENERATION: STANDARD
-- VERSION: WM1.0
-- MODULE: lpm_rom

--    (以下略)

library ieee;
use ieee.std_logic_1164.all ;

library lpm;
use lpm.lpm_components.all ;

entity prom is
port (
address : in std_logic_vector(3 downto 0);
inclock : in std_logic;
outclock : in std_logic;
q : out std_logic_vector(14 downto 0)
);
end prom;

architecture SYN of prom is
signal sub_wire0 : std_logic_vector(14 downto 0);
component lpm_rom
GENERIC (
intended_device_family : string;
    (略)
lpm_type : string;
) ;
port (
outclock : in std_logic ;
address : in std_logic_vector(3 downto 0);
inclock : in std_logic ;
q : out std_logic_vector(14 downto 0)
);
end component ;
begin
q <= sub_wire0(14 downto 0);
lpm_rom_component : lpm_rom
GENERIC MAP (
intended_device_family => "FLEX10KE" ,
lpm_width => 15,
lpm_widthad => 4,
lpm_address_control => "REGISTERED" ,
lpm_outdata => "REGISTERED" ,
lpm_file => "prom.mif" ,
lpm_type => "LPM_ROM"
)
port map (
outclock => outclock,
address => address,
inclock => inclock,
q => sub_wire0
);
end SYN;

-- CNX file retrival info
--       (以下略)


3.2 ROMデータの記述例

ROMのデータは,以下に示すファイル "prom.mif" を用いて記述します.


-- prom.mif
-- 15bit RISC processor
-- cpu15h.vhd
-- Y.Izawa
-- H18.4.10

depth = 16;
width = 15;
address_radix = HEX;
data_radix = Bin;

content
begin
[00..0F]     : 000000000000000;
  00 : 100000000000000; -- ldl Reg0 0
01 : 100000100000001; -- ldl Reg1 1
02 : 100001000000000; -- ldl Reg2 0
03 : 100001100001010; -- ldl Reg3 10
  04 : 000101000100000; -- add Reg2 Reg1
05 : 000100001000000; -- add Reg0 Reg2
06 : 101001001100000; -- cmp Reg2 Reg3
07 : 101100000001001; -- je 9
08 : 110000000000100; -- jmp 4
09 : 111000001000000; -- st Reg0 64
0A : 111100000000000; -- hlt
0B : 000000000000000; -- nop
0C : 000000000000000; -- nop
0D : 000000000000000; -- nop
0E : 000000000000000; -- nop
0F : 000000000000000; -- nop
end;



3.3 メガファンクション”ROM”のクロック生成

メガファンクションを用いる際に注意すべき事項があります.

”Quartus�U”(Web Edition)のメガファンクションROMの場合,アドレスの書き込みクロックと,
データの読み込みクロックの2種類が必要になります.
これらを同じクロックで実行させると,アドレス入力後さらに1クロック遅延してROMのデータが出力されてしまい,
CPUは正常に動作しません.

フェッチのフェーズで,アドレス入力とデータの読み出しを一括して行うためには,
CLK_FT の立ち上りより,クロックの半周期前で立ち上る,もう1つのクロックを生成する必要があります.
これを実現するため,位相が1つ前にあるクロック CLK_WB を遅延させます.
以下に示す ”clk_dly.vhd” は,入力 Din を基本クロックの半周期だけ遅延して QOUT として出力する回路です.
半周期遅延させるため,基本クロック CLK の立ち下りエッジを用いている点に注意して下さい.


clk_dly.vhd

-- clk_dly.vhd
-- Y.Izawa
-- H18.4.10


library IEEE;
use IEEE.std_logic_1164.all;

entity clk_dly is
port (
CLK : in std_logic;
Din : in std_logic;
QOUT : out std_logic
);
end clk_dly;


architecture RTL of clk_dly is

begin
process (CLK)
begin
if (CLK'event and CLK = '0') then
    QOUT <= Din ;
end if ;
end process ;
end RTL;



4. RAMの配列(ARRAY)による表現

先に述べたように,RAMは配列を用いて,効率的に記述します.

配列を用いた場合,そのインデックスがRAMのアドレスに対応します.
また,フェッチブロックのアドレス出力を直接RAMのアドレスに入力し,RAMへの書き込みは,
ライトバックのフェーズで行います.

これにより,コンポーネント間の信号の受け渡しが単純化され,大容量化にも対応できます.

配列を用いた一般的なRAMの構成による記述例を以下に示します.
なお,書き込み時以外は常に読み出しモードとなり,アドレスを与えればRAMのアクセス時間の後に
データが出力されることに注意して下さい.


ram.vhd


-- ram.vhd
-- Y.Izawa
-- H18.4.3

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

entity ram is
port (
CLK_WB : in std_logic ;
RAM_WEN : in std_logic;
ADDR : in std_logic_vector (6 downto 0);
DATA_in : in std_logic_vector (15 downto 0);
DATA_OUT : out std_logic_vector (15 downto 0);
IO65_in : in std_logic_vector (15 downto 0);
IO64_OUT : out std_logic_vector (15 downto 0)
);
end ram;

architecture BEHAVIOR of ram is

subtype RAMWORD is std_logic_vector(15 downto 0);
type RAMARRAY is array (0 to 7) of RAMWORD;

signal RAMDATA : RAMARRAY;
signal ADR_IN  : integer range 0 to 65;

begin
ADR_IN <= conv_integer(ADDR);
process (CLK_WB)
begin
if (CLK_WB'event and CLK_WB = '1') then
if (RAM_WEN = '1') then
if (ADDR(6) = '0') then
RAMDATA(ADR_IN) <= DATA_in;
elsif (ADR_IN = 64) then
IO64_OUT <= DATA_in;
end if ;
end if ;
end if ;
end process ;

process (RAM_WEN)
begin
if ( RAM_WEN = '0') then
if (ADDR(6) = '0') then
DATA_OUT <= RAMDATA(ADR_IN);
elsif (ADR_IN = 65) then
DATA_OUT <= IO65_in;
else
DATA_OUT <= ( others => '0');
end if;
end if;
end process ;
end BEHAVIOR;




5. 新構成CPUの記述例


前節のROMやRAMを用いると,以下に示すソースコードのように記述することができます.

前章までの記述に対し,どのように変更されているか,注意して眺めて下さい.


新構成CPUのソースコード( cpu15h.vhd )

-- cpu15h.vhd
-- Y.Izawa
-- H18.4.10

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

entity cpu15h 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 cpu15h;


architecture RTL of cpu15h is

component   clk_gen
(略)
end component;

component clk_dly
port
(
CLK : in std_logic;
Din : in std_logic;
QOUT : out std_logic
);
end component;

component   prom
port
(
address : in std_logic_vector(3 downto 0);
inclock : in std_logic;
outclock : in std_logic;
q : out std_logic _vector(14 downto 0)
);
end component;


component   decode
(略)
end component;


component   reg_dc
(略)
end component;


component   exec
(略)
end component;


component   reg_wb
(略)
end component


component   ram
port
(
CLK_WB : in std_logic;
RAM_WEN : in std_logic ;
ADDR : in std_logic_vector (6 downto 0);
DATA_in : in std_logic _vector (15 downto 0);
DATA_OUT : out std_logic_vector (15 downto 0);
IO65_in : in 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 CLK_WB_DLY : 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 ;

begin
C1 : clk_gen port map (CLK, CLK_FT, CLK_DC, CLK_EX, CLK_WB);
C2 : clk_dly port map (CLK, CLK_WB, CLK_WB_DLY);
C3 : prom port map (P_COUNT(3 downto 0), CLK_WB_DLY, CLK_FT,
PROM_OUT);
C4 : decode port map (CLK_DC, PROM_OUT, OP_CODE, OP_DATA );
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(10 downto 8), N_REG_A, REG_A);
C6 : 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);
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 port map (CLK_WB, RAM_WEN, OP_DATA(6 downto 0),
     RAM_in, RAM_OUT, IO65_in, IO64_OUT);
end RTL;



6. まとめ

本章では, より実用的な CPUを実現するため, メモリ すなわち ROMとRAM の設計法について解説してきました.
設計したVHDLをFPGA上で動作させるため, 今回は メモリサイズの上限 を ROM,RAMともに 256 ワード としました.

しかし, 実用的な プログラムを動作させるためには,この メモリサイズでは不足 するでしょう.
その場合は, 命令コードの見直し が必要です.

例えば, ROM のメモリサイズについては,簡単に 8倍の 2048ワード に拡張することが可能です.
命令コードの表より明らかなように, Jump 関連の命令では,15bit のうち 11〜8 の 3bit を使用していません.
このため, プログラムカウンタ(PC)で表される アドレス を 3bit 追加し, 11bit に拡張 することができます.

一方, RAM のメモリサイズについては,一工夫必要です.
メモリとレジスタ間の命令は,ロード ( LD ) とストア ( ST ) で, 8個のレジスタ を
オペランドの 11〜8 の 3bit で指定します.
この部分で,ロード とストア命令のレジスタを,例えば Reg0 に固定 してしまえば, ROM と同じように
アドレスは 11bit となり, 8倍の 2048 ワードに拡張 することが可能です.

11bit でも足りない という場合は,15bit という 命令コード長の見直し が必要となりますが,
RAM の場合は,アドレスの上位bit を 別命令 もしくは, 特定のレジスタ で指定する方法もあります.
また,インテルの x86 シリーズのように, セグメントレジスタ を新たに設け, オペランドとの和 でアドレスを決定する
方法も考えられます.

また,性能が足りない用途では, 高速化手法 を導入することも可能です.
例えば,各ブロックのタイミングを互いにオーバラップさせる「 パイプライン処理 」を実現することも可能です.
すなわち, フェッチ , デコード , 実行 , ライトバック の4つの処理を,ベルトコンベアによる 流れ作業 により
高速化する手法で,理想的には見かけ上 1クロックで1つの命令 を実行することが可能です.
この「 パイプライン処理 」については,次の 第5章 で紹介します.

また, アセンブリ言語 によるプログラムを開発する ツール も用意する必要があるでしょう.
これらについては, 第6章 で説明します.


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