2011/03/17

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

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

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


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

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

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

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

3.1 ROMの記述例

ALTERA社の開発ツール”QuartusⅡ”(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Ⅱ”(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, ADR_IN)
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を用いると,以下に示すソースコードのように記述することができます.

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

5.1 新構成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章で説明します.

No comments:

Post a Comment