-- Copyright (c) 2007 Frank Buss (fb@frank-buss.de)
-- See license.txt for license

library IEEE;
use IEEE.STD_LOGIC_1164.ALL; 
use IEEE.NUMERIC_STD.ALL; 
use work.ALL;

entity forth_core is
    generic(
        cell_size   : natural := 16
    );
    port(
        clock       : in std_logic;
        reset       : in std_logic;
        address     : out unsigned(cell_size - 1 downto 0);
        wren        : out std_logic;
        data_out    : out unsigned(cell_size - 1 downto 0);
        q           : in unsigned(cell_size - 1 downto 0)
    );
end entity forth_core;

architecture rtl of forth_core is
    -- initial values for pointers
    constant pc_start : natural := 256;
    constant rp_start : natural := 255;
    constant sp_start : natural := 63;

    -- number of bits for a command. Max: cell_size-11
    constant command_size : natural := 4;

    -- microcode size
    constant microcode_size : natural := command_size + 3;

    subtype cell_type is unsigned(cell_size - 1 downto 0);
    subtype command_type is unsigned(command_size - 1 downto 0);
    subtype microcode_type is unsigned(microcode_size - 1 downto 0);

    -- commands
    constant cmd_q2a        : command_type := to_unsigned(0, command_size);
    constant cmd_a2data     : command_type := to_unsigned(1, command_size);
    constant cmd_a2address  : command_type := to_unsigned(2, command_size);
    constant cmd_pc2address : command_type := to_unsigned(3, command_size);
    constant cmd_pc2data    : command_type := to_unsigned(4, command_size);
    constant cmd_a2pc       : command_type := to_unsigned(5, command_size);
    constant cmd_branch     : command_type := to_unsigned(6, command_size);
    constant cmd_q2pc       : command_type := to_unsigned(7, command_size);
    constant cmd_and        : command_type := to_unsigned(8, command_size);
    constant cmd_or         : command_type := to_unsigned(9, command_size);
    constant cmd_xor        : command_type := to_unsigned(10, command_size);
    constant cmd_plus       : command_type := to_unsigned(11, command_size);
    constant cmd_lshift     : command_type := to_unsigned(12, command_size);
    constant cmd_rshift     : command_type := to_unsigned(13, command_size);
    constant cmd_pop        : command_type := to_unsigned(14, command_size);
    constant cmd_popr       : command_type := to_unsigned(15, command_size);

    -- delays main process for simpler memory access
    signal delay : boolean := false;

    -- every opcode contains two microcodes, this is the second microcode
    signal next_microcode : microcode_type;

    -- program counter
    signal pc : cell_type := to_unsigned(pc_start, cell_size);

    -- stack pointer
    signal sp : cell_type := to_unsigned(sp_start, cell_size);

    -- return stack pointer
    signal rp : cell_type := to_unsigned(rp_start, cell_size);

    -- additional address register
    signal a : cell_type := to_unsigned(0, cell_size);

    -- temporary register and input value for RAM write access
    signal data : cell_type;

    -- CPU statemachine steps
    type state_type is (
        read_opcode,
        step1,
        step2
    );
    signal state : state_type := read_opcode;

begin

    -- the CPU process
    process(clock, reset)
        variable microcode : microcode_type;
        variable command : command_type;
    begin
        if reset = '1' then
            state <= read_opcode;
            delay <= false;
            pc <= to_unsigned(pc_start, cell_size);
            rp <= to_unsigned(rp_start, cell_size);
            sp <= to_unsigned(sp_start, cell_size);
            wren <= '0';
        elsif rising_edge(clock) then
            delay <= not delay;
            if delay then
                microcode := (others => '0');
                wren <= '0';
                data <= q;
                case state is
                    when read_opcode =>
                        address <= pc;
                        pc <= pc + 1;
                        state <= step1;
                    when step1 =>
                        if q(15) = '1' then
                            pc <= '0' & q(14 downto 0);
                            address <= rp;
                            rp <= rp - 1;
                            wren <= '1';
                            data <= pc;
                            state <= read_opcode;
                        else
                            microcode := q(microcode_size - 1 downto 0);
                            next_microcode <= q(2 * microcode_size - 1 downto microcode_size);
                            state <= step2;
                        end if;
                    when step2 =>
                        microcode := next_microcode;
                        state <= read_opcode;
                end case;

                if microcode(1 downto 0) /= "00" then
          command := microcode(microcode_size - 1 downto 3);
          
          -- VHDL IEEE standard forbids case/end case on variables, using ifs is allowed
          if command = cmd_q2a then a <= q; end if;
                    if command = cmd_a2data then data <= a; end if;
                    if command = cmd_a2address then address <= a; end if;
                    if command = cmd_pc2address then address <= pc; pc <= pc + 1; end if;
                    if command = cmd_pc2data then data <= pc; end if;
                    if command = cmd_a2pc then pc <= a; end if;
                    if command = cmd_branch then if q /= x"0000" then pc <= a; end if; end if;
                    if command = cmd_q2pc then pc <= q; end if;
                    if command = cmd_and then data <= q and a; end if;
                    if command = cmd_or then data <= q or a; end if;
                    if command = cmd_xor then data <= q xor a; end if;
                    if command = cmd_plus then data <= q + a; end if;
                    if command = cmd_lshift then data <= q(cell_size - 2 downto 0) & '0'; end if;
                    if command = cmd_rshift then data <= '0' & q(cell_size - 1 downto 1); end if;
                    if command = cmd_pop then address <= sp + 1; sp <= sp + 1; end if;
                    if command = cmd_popr then address <= rp + 1; rp <= rp + 1; end if;
                end if;

                if microcode(2) = '1' then
                    wren <= '1';
                end if;
            
                case microcode(1 downto 0) is
                    when "10" => 
                        -- push
                        address <= sp;
                        sp <= sp - 1;
                    when "11" =>
                        -- pushr
                        address <= rp;
                        rp <= rp - 1;
                    when others => null;
                end case;
            end if;
        end if;
    end process;

    -- connect internal data buffer to outgoing signal
    data_out <= data;
    
end architecture rtl;
