シンプルな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章 で説明します.