シンプルな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開発ツール QuatusU が提供するライブラリであり,
PROM のデータを mif形式 の外部ファイルからロードすることが可能です.
PROMのブロックでは,2種類のクロック ( CLK_WB_DLY CLK_FT ) を用いている点に注意して下さい.
その理由については,本章の3.3で詳しく説明します.

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









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


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

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

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


3.1 ROMの記述例

ALTERA社の開発ツール”QuartusU”(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”のクロック生成

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

”QuartusU”(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を作ってみよう」 トップページに戻る.