0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

VHDLの練習として4ビットCPU「CPU1738」(別府伸耕, トランジスタ技術2020年5月号)をMAX10 FPGAで作る

Last updated at Posted at 2021-09-16

1. 概要

テキスト(別府伸耕, 『トランジスタ技術2020年5月号』)はすべて基本ゲートを組み合わせて作り上げているが、とてもそんな真似はできないので、おおむね図34「CPUの全体接続図」(p.126)のモジュールごとにVHDLで記述して組み合わせる。またテキストはバスの選択にトライステートバッファーを使っているが、ここではマルチプレクサに置き換える。

クロックジェネレーターおよびROM (プログラムメモリー)はCPUの創りかたのときと同じく、メモリーブロックとして独立させる。使用したMAX10の評価ボードもCPUの創りかたのときと同じである。

2. モジュールごとに作る

2.1 CPU部を作る

2.1.1 ALUを作る

加減算のほかに、0出力、Aスルー、Bスルー、-Bスルーの各機能がある。

ソースコードを見る/隠す
alu.vhdl
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity alu is
	generic(
		NUM_BITS: natural := 4
	);
	port(
		a: in std_logic_vector(NUM_BITS-1 downto 0);
		b: in std_logic_vector(NUM_BITS-1 downto 0);
		
		mux_a: in std_logic;
		mux_b: in std_logic;

		as        : in std_logic; -- when 0, add; when 1, sub;
		ld        : in std_logic;
		clk_inside: in std_logic;
		aclr_n    : in std_logic;
	
		add_sub: out std_logic_vector(NUM_BITS-1 downto 0);
		zero   : out std_logic;
		cout   : out std_logic
	);
end entity;

architecture rtl of alu is
	signal a_temp   : unsigned(NUM_BITS-1 downto 0);
	signal b_temp   : unsigned(NUM_BITS-1 downto 0);
	signal calc_temp: unsigned(NUM_BITS   downto 0);
begin

	a_temp <=
		(others => '0') when mux_a = '0' else -- mux_aが0ならオペランドaを強制的に0にし、
		unsigned(a);

	b_temp <=
		(others => '0') when mux_b = '0' else -- mux_bが0ならオペランドbを強制的に0にし、
		0-unsigned(b)   when as    = '1' else -- 引き算のときはオペランドbを2の補数にし、
		unsigned(b);
	
	calc_temp <= ('0' & a_temp) + b_temp; -- 1ビット拡張して和(差)を計算して、

	process(clk_inside)
	begin
		
		if aclr_n = '0' then
			-- 非同期クリアして、
			add_sub <= (others => '0');
		else
			-- 和(差)を非同期更新して、
			add_sub <= std_logic_vector(calc_temp)(NUM_BITS-1 downto 0);
		
			if rising_edge(clk_inside) then
				if ld then
					-- キャリーアウトを同期更新して、
					cout <= calc_temp(NUM_BITS);

					-- ゼロフラグを同期更新する。
					if calc_temp(NUM_BITS-1 downto 0) = 0 then
						zero <= '1';
					else
						zero <= '0';
					end if;

				end if;
			end if;

		end if;
	end process;
end architecture;

2.1.2 A、B、OUTの各レジスタおよびPCを作る

CPUの創りかたのときと同じく、まず共通のカウンター兼レジスタを作っておいて、それを4つまとめて実体化する。

2.1.2.1 共通のカウンター兼レジスタを作る

ソースコードを見る/隠す
counter_register.vhdl
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity counter_register is
	generic(
		NUM_BITS: natural := 4
	);
	port(
		clk   : in std_logic;
		aclr_n: in std_logic;
		sload : in std_logic;
		d     : in std_logic_vector(NUM_BITS-1 downto 0);
		
		ena_count: in std_logic;
		
		q: out std_logic_vector(NUM_BITS-1 downto 0)
	);
end entity;

architecture rtl of counter_register is
	signal count: natural range 0 to 2**NUM_BITS-1; -- この信号をカウンターとして使う。
	constant COUNT_MAX: natural := 2**NUM_BITS-1;   -- カウンターのトップ値。
begin
	-- カウント値をそのまま出力する。
	q <= std_logic_vector(to_unsigned(count, NUM_BITS));
	
	process(all)
	begin

		-- !非同期リセット
		if aclr_n = '0' then
			count <= 0;

		elsif rising_edge(clk) then

			-- 同期ロード
			if sload then
				count <= to_integer(unsigned(d));

			-- 同期カウント
			elsif ena_count then
				if count >= COUNT_MAX then
					count <= 0;
				else
					count <= count + 1;
				end if;
			end if;
		end if;
	end process;
	
end architecture;

2.1.2.2 それを4つ実体化して組み合わせる

ソースコードを見る/隠す
registers.vhdl
library ieee;
use ieee.std_logic_1164.all;

entity registers is
	generic(
		NUM_BITS: natural := 4
	);
	port(
		inp: in std_logic_vector(NUM_BITS-1 downto 0);

		a_reg_ld: in std_logic;
		b_reg_ld: in std_logic;
		out_ld  : in std_logic;
		pc_ld   : in std_logic;
		
		clk_inside: in std_logic;
		aclr_n    : in std_logic;
		
		pc_up: in std_logic;

		a_reg  : out std_logic_vector(NUM_BITS-1 downto 0);
		b_reg  : out std_logic_vector(NUM_BITS-1 downto 0);
		out_reg: out std_logic_vector(NUM_BITS-1 downto 0);
		pc     : out std_logic_vector(NUM_BITS-1 downto 0)
	);
end entity;

architecture rtl of registers is
begin
	-- A_REGを実体化
	a_register: entity work.counter_register
		generic map(
			NUM_BITS => NUM_BITS
		)
		port map(
			clk       => clk_inside,
			aclr_n    => aclr_n,
			sload     => a_reg_ld,
			d         => inp,
			ena_count => '0', -- カウンターを無効化
			q         => a_reg
		);

	-- B_REGを実体化
	b_register: entity work.counter_register
		generic map(
			NUM_BITS => NUM_BITS
		)
		port map(
			clk       => clk_inside,
			aclr_n    => aclr_n,
			sload     => b_reg_ld,
			d         => inp,
			ena_count => '0',  -- カウンターを無効化
			q         => b_reg
		);

	-- OUT_REGを実体化
	out_register: entity work.counter_register
		generic map(
			NUM_BITS => NUM_BITS
		)
		port map(
			clk       => clk_inside,
			aclr_n    => aclr_n,
			sload     => out_ld,
			d         => inp,
			ena_count => '0',  -- カウンターを無効化
			q         => out_reg
		);

	-- PCを実体化
	program_counter: entity work.counter_register
		generic map(
			NUM_BITS => NUM_BITS
		)
		port map(
			clk       => clk_inside,
			aclr_n    => aclr_n,
			sload     => pc_ld,
			d         => inp,
			ena_count => pc_up,  -- カウンターを有効化
			q         => pc
		);

end architecture;

image.png

2.1.3 命令デコーダを作る

表10「命令デコーダの真理値表(PC_LD以外)」(p.124)、表11「"PC_LD=1"を出力するときの入力の組み合わせ」(p.124)の各表をそのまま書き写した。

ソースコードを見る/隠す
instruction_decoder.vhdl
library ieee;
use ieee.std_logic_1164.all;

entity instruction_decoder is
	port(
		data: in std_logic_vector(7 downto 4);

		zero: in std_logic;
		cout: in std_logic;
		
		ld_en: in std_logic;
		
		a_reg_ld : out std_logic;
		b_reg_ld : out std_logic;
		b_reg_oe : out std_logic;
		rom_oe   : out std_logic;
		alu_as   : out std_logic;
		alu_oe   : out std_logic;
		alu_ld   : out std_logic;
		alu_mux_a: out std_logic;
		alu_mux_b: out std_logic;
		in_oe    : out std_logic;
		out_ld   : out std_logic;
		halt_n   : out std_logic;
		
		pc_ld: out std_logic
	);
end entity;

architecture rtl of instruction_decoder is
	subtype ascending_vect_t is std_logic_vector(0 to 11);
	signal truth_tbl: ascending_vect_t;
begin

	with data select
	truth_tbl <=
		ascending_vect_t'("100101001001") when 4d"0",  -- LD A, [Data]
		ascending_vect_t'("010101001001") when 4d"1",  -- LD B, [Data]
		ascending_vect_t'("101001001001") when 4d"2",  -- LD A, B
		ascending_vect_t'("01-001010001") when 4d"3",  -- LD B, A
		ascending_vect_t'("101001111001") when 4d"4",  -- ADD A, B
		ascending_vect_t'("101011111001") when 4d"5",  -- SUB A, B
		ascending_vect_t'("100101111001") when 4d"6",  -- ADD A, [Data]
		ascending_vect_t'("100111111001") when 4d"7",  -- SUB A, [Data]
		ascending_vect_t'("00-001010011") when 4d"8",  -- OUT A
		ascending_vect_t'("001001001011") when 4d"9",  -- OUT B
		ascending_vect_t'("000101001011") when 4d"10", -- OUT [Data]
		ascending_vect_t'("10-00000-101") when 4d"11", -- IN A
		ascending_vect_t'("000101001001") when 4d"12", -- JUMP [Address]
		ascending_vect_t'("000101001001") when 4d"13", -- JNC [Address]
		ascending_vect_t'("000101001001") when 4d"14", -- JNZ [Address]
		ascending_vect_t'("000-0-00-000") when 4d"15", -- HALT
		ascending_vect_t'(12b"0")         when others;

	a_reg_ld  <= truth_tbl(0) and ld_en;
	b_reg_ld  <= truth_tbl(1) and ld_en;
	b_reg_oe  <= truth_tbl(2);
	rom_oe    <= truth_tbl(3);
	alu_as    <= truth_tbl(4);
	alu_oe    <= truth_tbl(5);
	alu_ld    <= truth_tbl(6) and ld_en;
	alu_mux_a <= truth_tbl(7);
	alu_mux_b <= truth_tbl(8);
	in_oe     <= truth_tbl(9);
	out_ld    <= truth_tbl(10) and ld_en;
	halt_n    <= truth_tbl(11);

	pc_ld <=
		'1' when data = 4d"12"                else -- JUMP [Address]
		'1' when data = 4d"13" and cout = '0' else -- JNC [Address]
		'1' when data = 4d"14" and zero = '0' else -- JNZ [Address]
		'0';

end architecture;

image.png

2.1.4 ステートマシンを作る

図29「ステート・マシンの内部ブロック図」(p.122)の接続図をそのままVHDLで表現した。

ソースコードを見る/隠す
state_machine.vhdl
library ieee;
use ieee.std_logic_1164.all;

entity state_machine is
	port(
		halt_n    : in std_logic;
		pc_ld     : in std_logic;
		clk_inside: in std_logic;
		aclr_n    : in std_logic;

		clk_en: out std_logic;
		halt  : out std_logic;
		pc_up : out std_logic;
		ld_en : out std_logic
	);
end entity;

architecture rtl of state_machine is
	signal halt_temp : std_logic;
	signal pc_up_temp: std_logic;
begin
	process(all)
	begin

		halt_temp  <= halt_n nor pc_up; 
		pc_up_temp <= pc_ld  nor pc_up;

		if aclr_n = '0' then
			halt   <= '0';
			clk_en <= '1';
			pc_up  <= '0';
			ld_en  <= '1';

		elsif rising_edge(clk_inside) then
			halt   <=     halt_temp;
			clk_en <= not halt_temp;
			pc_up  <=     pc_up_temp;
			ld_en  <= not pc_up_temp;
		end if;

	end process;
end architecture;

image.png

2.1.5 CPUにまとめる

ここまで作ったモジュール同士を配線してCPUを作る。

ソースコードを見る/隠す
cpu.vhdl
library ieee;
use ieee.std_logic_1164.all;

entity cpu is
	port(
		aclr_n : in std_logic;
		inp    : in std_logic_vector(3 downto 0);
		data   : in std_logic_vector(7 downto 0);
		clk    : in std_logic;

		halt   : out std_logic;
		a_reg  : out std_logic_vector(3 downto 0);
		b_reg  : out std_logic_vector(3 downto 0);
		address: out std_logic_vector(3 downto 0);
		outp   : out std_logic_vector(3 downto 0)
	);
end entity;

architecture rtl of cpu is

	signal alu_b     : std_logic_vector(3 downto 0);
	signal alu_mux_a : std_logic;
	signal alu_mux_b : std_logic;
	signal alu_as    : std_logic;
	signal alu_ld    : std_logic;
	signal clk_inside: std_logic;
	signal add_sub   : std_logic_vector(3 downto 0);
	signal zero      : std_logic;
	signal cout      : std_logic;
	
	signal ld_en    : std_logic;
	signal a_reg_ld : std_logic;
	signal b_reg_ld : std_logic;
	signal b_reg_oe : std_logic;
	signal rom_oe   : std_logic;
	signal alu_oe   : std_logic;
	signal in_oe    : std_logic;
	signal out_ld   : std_logic;
	signal halt_n   : std_logic;
	signal pc_ld    : std_logic;
	
	signal clk_en: std_logic;
	signal pc_up : std_logic;
	
	signal registers_in: std_logic_vector(3 downto 0);
	
	signal a_reg_inside: std_logic_vector(3 downto 0);

begin

	module_alu: entity work.alu
	port map(
		a          => a_reg_inside,
		b          => alu_b,
		mux_a      => alu_mux_a,
		mux_b      => alu_mux_b,
		as         => alu_as,  -- when 0, add; when 1, sub;
		ld         => alu_ld,
		clk_inside => clk_inside,
		aclr_n     => aclr_n, -- 外部へ接続
		add_sub    => add_sub,
		zero       => zero,
		cout       => cout
	);
	
	module_id:entity work.instruction_decoder
	port map(
		data      => data(7 downto 4), -- 外部へ配線
		zero      => zero,
		cout      => cout,
		ld_en     => ld_en ,
		a_reg_ld  => a_reg_ld,
		b_reg_ld  => b_reg_ld,
		b_reg_oe  => b_reg_oe,
		rom_oe    => rom_oe,
		alu_as    => alu_as,
		alu_oe    => alu_oe,
		alu_ld    => alu_ld,
		alu_mux_a => alu_mux_a,
		alu_mux_b => alu_mux_b,
		in_oe     => in_oe,
		out_ld    => out_ld,
		halt_n    => halt_n,
		pc_ld     => pc_ld
	);
	
	module_state_machine:entity work.state_machine
	port map(
		halt_n     => halt_n,
		pc_ld      => pc_ld,
		clk_inside => clk_inside,
		aclr_n     => aclr_n, -- 外部へ配線
		clk_en     => clk_en,
		halt       => halt, -- 外部へ配線
		pc_up      => pc_up,
		ld_en      => ld_en
	);

	module_registers:entity work.registers
	port map(
		inp        => registers_in,
		a_reg_ld   => a_reg_ld,
		b_reg_ld   => b_reg_ld,
		out_ld     => out_ld,
		pc_ld      => pc_ld,
		clk_inside => clk_inside,
		aclr_n     => aclr_n, -- 外部へ配線
		pc_up      => pc_up,
		a_reg      => a_reg_inside,
		b_reg      => b_reg,
		out_reg    => outp, -- 外部へ配線
		pc         => address  -- 外部へ配線
	);
	
	clk_inside <= clk_en and clk;
	
	-- これがトライステートバッファーから代えたマルチプレクサ
	-- ALUの出力かインポートかいずれかをレジスタ群へ渡す。
	registers_in <=
		add_sub when alu_oe else
		inp     when in_oe  else
		(others => '0');

	-- これもトライステートバッファーから代えたマルチプレクサ
	-- Bレジスタかプログラムメモリーに含まれるオペランドかいずれかをALUへ渡す。
	alu_b <=
		b_reg            when b_reg_oe else
		data(3 downto 0) when rom_oe   else
		(others => '0');

	-- A、Bレジスタも確認のため外へ出す。
	a_reg <= a_reg_inside;
	b_reg <= b_reg;
end architecture;

image.png

2.2 クロックジェネレーターを作る

ここで作ったものとまったく同じ。

2.3 ROM (プログラムメモリー)を作る

ここで作ったものとまったく同じ。

2.4 全部組み合わせてCPU1738を完成させる

ソースコードを見る/隠す
cpu1738.vhdl
library ieee;
use ieee.std_logic_1164.all;

entity cpu1738 is
	port(
		source_clk   : in std_logic;
		manual_clk_in: in std_logic;
		sel          : in std_logic_vector(1 downto 0);
		aclr_n       : in std_logic;
		inp          : in std_logic_vector(3 downto 0);

		halt   : out std_logic;
		a_reg  : out std_logic_vector(3 downto 0);
		b_reg  : out std_logic_vector(3 downto 0);
		outp   : out std_logic_vector(3 downto 0);
		clk_out: out std_logic
	);
end entity;

architecture rtl of cpu1738 is
	signal address      : std_logic_vector(3 downto 0);
	signal data         : std_logic_vector(7 downto 0);
	signal clk_inside   : std_logic;
	signal delayed_clk_0: std_logic;
	signal delayed_clk_1: std_logic;
begin
	module_cpu: entity work.cpu
	port map(
		aclr_n  => aclr_n, -- 外部へ配線
		inp     => inp, -- 外部へ配線
		data    => data,
		clk     => delayed_clk_1,
		halt    => halt, -- 外部へ配線
		a_reg   => a_reg, -- 外部へ配線
		b_reg   => b_reg, -- 外部へ配線
		address => address,
		outp    => outp -- 外部へ配線
	);
	
	module_clk_gen: entity work.clk_gen
	port map(
		clk           => source_clk,
		manual_clk_in => manual_clk_in,
		sel           => sel,
		clk_out       => clk_inside
	);
	
	module_rom: entity work.rom
	port map(
		clock   => clk_inside,
		address => address,
		q       => data
	);

	-- 確認のためクロックジェネレーターの出力を外へ出す。
	clk_out <= clk_inside;

	--『CPUの創りかた』のときと同じく、CPUへ与えるクロックを少し遅らせる。
	process(clk_out)
	begin
		if rising_edge(source_clk) then
			delayed_clk_0 <= clk_inside;
			delayed_clk_1 <= delayed_clk_0;
		end if;
	end process;
end architecture;

image.png

3. ROM (プログラムメモリー)の初期化ファイルを作る

初期化ファイル(.mif)を生成するためのアセンブラのようなものはPythonで作った。

ソースコードを見る/隠す
example_div.py
def generate_mif(file_name, width, depth, rom):
    f = open(file_name, "w")
    f.write("WIDTH=%d;\n" % width)
    f.write("DEPTH=%d;\n" % depth)
    f.write("ADDRESS_RADIX=UNS;\n")
    f.write("DATA_RADIX=BIN;\n")
    f.write("CONTENT BEGIN\n")
    format_of_code = "0" + str(width) + "b"
    for i in range(depth):
        machine_code = format(rom[i], format_of_code)
        f.write("%10d   :   %s;\n" % (i, machine_code))
    f.write("END;\n")
    f.close()

def LD_A(Data)   : return ( 0 << 4)|(Data & 0xF)    # A ← [Data]
def LD_B(Data)   : return ( 1 << 4)|(Data & 0xF)    # B ← [Data]
def LD_AB()      : return ( 2 << 4)                 # A ← B
def LD_BA()      : return ( 3 << 4)                 # B ← A
def ADD_AB()     : return ( 4 << 4)                 # A ← A + B
def SUB_AB()     : return ( 5 << 4)                 # A ← A - B
def ADD_A(Data)  : return ( 6 << 4)|(Data & 0xF)    # A ← A + [Data]
def SUB_A(Data)  : return ( 7 << 4)|(Data & 0xF)    # A ← A - [Data]
def OUT_A()      : return ( 8 << 4)                 # OUT ← A
def OUT_B()      : return ( 9 << 4)                 # OUT ← B
def OUT(Data)    : return (10 << 4)|(Data & 0xF)    # OUT ← [Data]
def IN_A()       : return (11 << 4)                 # A ← IN
def JUMP(Address): return (12 << 4)|(Address & 0xF) # PC ← [Address]
def JNC(Address) : return (13 << 4)|(Address & 0xF) # PC ← [Address] if NOT Carry
def JNZ(Address) : return (14 << 4)|(Address & 0xF) # PC ← [Address] if NOT Zero
def HALT()       : return (15 << 4)                 # HALT

FILE_NAME = "rom_init.mif"
WIDTH =  8
DEPTH = 16
_ = [0] * DEPTH


# テキストのプログラム例(4)
# インポートとアウトポートとを直結しておく。
# 9/2=4(0100)余り1を計算する。
# 0111(9-2=7)、0101(9-2-2=5)、0011(9-2-2-2=3)、0001(9-2-2-2-2=1)の順に表示されたあと、
# 0100(9/2=4)と表示される。
_[0] = LD_A(9)  # 割られる数
_[1] = LD_B(0)  # 何回引き算ができたかの初期値
_[2] = SUB_A(2) # 割る数(引く数)
_[3] = JNC(10)  # 引いた結果が負なら、ループを抜けて10番地へ
_[4] = OUT_A()  # 今の引き算結果(すなわち余り)を出力して、
_[5] = LD_AB()  # 引き算できた回数をこの3行でインクリメントして、
_[6] = ADD_A(1)
_[7] = LD_BA()
_[8] = IN_A()   # 今の引き算結果(すなわち余り)(今OUTレジスタにある)を読み込んで、
_[9] = JUMP(2)  # 引き算を繰り返す。
_[10] = IN_A()  # (引いた結果が負なら3番地から)余り(今OUTレジスタにある)をAに格納し、
_[11] = OUT_B() # 商(何回引き算ができたか)を出力する。
_[12] = HALT()

generate_mif(FILE_NAME, WIDTH, DEPTH, _)

↓ これが、生成された.mifファイル。

ソースコードを見る/隠す
rom_int.mif
WIDTH=8;
DEPTH=16;
ADDRESS_RADIX=UNS;
DATA_RADIX=BIN;
CONTENT BEGIN
         0   :   00001001;
         1   :   00010000;
         2   :   01110010;
         3   :   11011010;
         4   :   10000000;
         5   :   00100000;
         6   :   01100001;
         7   :   00110000;
         8   :   10110000;
         9   :   11000010;
        10   :   10110000;
        11   :   10010000;
        12   :   11110000;
        13   :   00000000;
        14   :   00000000;
        15   :   00000000;
END;

4. 動かしてみる

9/2=4余り1を計算する。右から4ビットずつ、Bレジスタ、Aレジスタ、一番左がOUTレジスタ。OUTレジスタに7、5、3、1の順に表示されて、最後に商4が表示される。余り1がAレジスタに表示される。

5. ファイル一式

https://github.com/ti-nspire/VHDL_for_Quartus_Prime/tree/main/CPU1738

6. 参考

補遺A.

フルコンパイルせずに.mifファイルのデータだけを入れ換える方法

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?