Diseño e implementación de un procesador RISC desde cero (II) 
En esta segunda entrega de la serie se profundiza en el diseño de la unidad de control, en la implementación en VHDL de los diferentes elementos y en la realización de una prueba de concepto sobre un simulador.

>>> Enlace a la primera entrega de la serie.

Lógica combinatoria: los multiplexores

Un multiplexor es un circuito combinacional con varias entradas y una salida que permite, mediante una entrada adicional de selección, decidir qué entrada se enruta a la salida.

El código VHDL para un multiplexor de, por ejemplo, 3 entradas sería el siguiente:

library ieee;
use ieee.std_logic_1164.all;

entity Mux3Inputs is
    generic (
        NBits : integer := 16
    );
    port (
        Sel     : in std_logic_vector(1 downto 0);
        DataIn0 : in std_logic_vector((NBits - 1) downto 0);
        DataIn1 : in std_logic_vector((NBits - 1) downto 0);
        DataIn2 : in std_logic_vector((NBits - 1) downto 0);
        DataOut : out std_logic_vector((NBits - 1) downto 0)
    );
end entity;

architecture Architecture1 of Mux3Inputs is
begin
    DataOut <= DataIn0 when (Sel = "00") else
               DataIn1 when (Sel = "01") else
               DataIn2;
end architecture;


Lógica combinatoria: el expansor del signo

El expansor del signo (EXP) es un bloque combinacional que expande el signo de un valor de M bits a N bits siendo M < N.

En nuestro caso, el expansor del signo incluye una entrada de selección de un bit para elegir entre M=12 (instrucciones de salto relativo) y M=15 (sólo para la instrucción LOADI).

library ieee;
use ieee.std_logic_1164.all;

entity SignExp is
    generic (
        NBitsToExpand0 : integer := 15;
        NBitsToExpand1 : integer := 12;
        NBits : integer := 16
    );
    port (
        SelIn   : in std_logic;
        DataIn  : in std_logic_vector((NBits - 1) downto 0);
        DataOut : out std_logic_vector((NBits - 1) downto 0)
    );
end SignExp;

architecture Architecture1 of SignExp is
signal Expansion0 : std_logic_vector((NBits - NBitsToExpand0 - 1) downto 0);
signal Expansion1 : std_logic_vector((NBits - NBitsToExpand1 - 1) downto 0);
begin
    Expansion0 <= (others => DataIn(NBitsToExpand0 - 1));
    Expansion1 <= (others => DataIn(NBitsToExpand1 - 1));
    DataOut <= Expansion0 & DataIn((NBitsToExpand0 - 1) downto 0) when (SelIn = '0') else
               Expansion1 & DataIn((NBitsToExpand1 - 1) downto 0);
end Architecture1;


Lógica combinatoria: ALU

La ALU es en este caso incluye dentro dos multiplexores, un sumador y un negador.

Partiendo de este diseño, la implementación en VHDL es directa.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity Alu is
    generic (
        NBits : integer := 16
    );
    port (
        DIn   : in std_logic_vector((NBits - 1) downto 0);
        SIn   : in std_logic_vector((NBits - 1) downto 0);
        RAIn  : in std_logic_vector((NBits - 1) downto 0);
        DOut  : out std_logic_vector((NBits - 1) downto 0);
        SelIn : in std_logic_vector(3 downto 0)
    );
end entity;

architecture Architecture1 of Alu is
    component Adder is
        generic (
            NBits : integer := 16
        );
        port (
            A : in std_logic_vector((NBits - 1) downto 0);
            B : in std_logic_vector((NBits - 1) downto 0);
            Y : out std_logic_vector((NBits - 1) downto 0)
        );
    end component;
    component Neg is
        generic (
            NBits : integer := 16
        );
        port(
            DataIn  : in std_logic_vector((NBits - 1) downto 0);
            DataOut : out std_logic_vector((NBits - 1) downto 0)
        );
    end component;
    signal AddOut : std_logic_vector((NBits - 1) downto 0);
    signal SInNeg : std_logic_vector((NBits - 1) downto 0);
    signal FirstOperand : std_logic_vector((NBits - 1) downto 0);
    signal SMuxOut : std_logic_vector((NBits - 1) downto 0);
begin
    -- mux for the first operand of the adder
    FirstOperand <= std_logic_vector(to_signed(1, NBits)) when (SelIn = "0111") else                              -- inc
                    std_logic_vector(to_signed(-1, NBits)) when (SelIn = "1000") else                             -- dec
                    DIn when ((SelIn = "0001") or (SelIn = "0010") or (SelIn = "1100") or (SelIn = "1101")) else  -- add, sub, jz, jn
                    (others => '0');                                                                              -- assign
    -- neg Y
    Negate : Neg generic map (
        NBits => NBits
    )
    port map (
        DataIn => SIn,
        DataOut => SInNeg
    );
    -- src mux
    SMuxOut <= SInNeg when (SelIn = "0010") else  -- sub
               (others => '0') when ((SelIn = "1100") and (RAIn /= std_logic_vector(to_unsigned(0, NBits)))) or ((SelIn = "1101") and (RaIn(NBits - 1) = '0')) else  -- jz, jn
               SIn;
    -- adder
    Add : Adder generic map (
        NBits => NBits
    )
    port map (
        A => FirstOperand,
        B => SMuxOut,
        Y => AddOut
    );
    -- final mux
    DOut <= AddOut when ((SelIn = "0000") or (SelIn = "0001") or (SelIn = "0010") or (SelIn = "0111") or (SelIn = "1000") or (SelIn = "1100") or (SelIn = "1101")) else
            (DIn and SIn) when (SelIn = "0011") else
            (DIn or SIn) when (SelIn = "0100") else
            (DIn xor SIn) when (SelIn = "0101") else
            (not(SIn)) when (SelIn = "0110") else
            ('0' & SIn(15 downto 1)) when (SelIn = "1001") else         -- slr
            (SIn(15) & SIn(15 downto 1)) when (SelIn = "1010") else     -- sar
            (SIn(14 downto 0) & '0') when (SelIn = "1011") else         -- sll
            (others => '0');
end architecture;

Nótese que el componente “Neg” es el negador y calcula el complemento a dos (también se trata, a su vez, de un circuito combinacional)

Lógica secuencial: los registros

Un registro no es más que una colección de biestables D en paralelo, uno por cada bit.

La forma más portable de implementar una entrada “enable” es poniendo un multiplexor en la entrada D que seleccione entre la entrada exterior y realimentar la propia Q. De esta forma simulamos un “enable” con lógica “estándar”.

La implementación en VHDL de biestables D es directa:

library ieee;
use ieee.std_logic_1164.all;

entity Reg is
    generic (
        NBits : integer := 16
    );
    port (
        Enable  : in std_logic;
        Clk     : in std_logic;
        DataIn  : in std_logic_vector((NBits - 1) downto 0);
        DataOut : out std_logic_vector((NBits - 1) downto 0)
    );
end Reg;

architecture Architecture1 of Reg is
signal QBus : std_logic_vector((NBits - 1) downto 0);
signal DBus : std_logic_vector((NBits - 1) downto 0);
signal PreDBus : std_logic_vector((NBits - 1) downto 0);
begin
    process (Clk)
    begin
        if (Clk'event and (Clk = '1')) then
            QBus <= DBus;
        end if;
    end process;

    DBus <= PreDBus when (Enable = '1') else QBus;
    PreDBus <= DataIn;
    DataOut <= QBus;
end Architecture1;


Lógica secuencial: la memoria

La unidad de memoria viene con una RAM y una ROM. Una memoria ROM no requiere secuencialidad y puede ser implementada como una LUT de forma combinatoria:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity Rom is
    port (
        AddressIn : in std_logic_vector(12 downto 0);
        DataOut   : out std_logic_vector(15 downto 0)
    );
end entity;

architecture Architecture1 of Rom is
type RomType is array (0 to 8191) of std_logic_vector(15 downto 0);
constant Data : RomType := (
    "0001000000000000",      --     load
    "0010000000001000",      --     op ra, ra, dec
    "0011000000000000",      --     store
    others => "0000000000000000"
);
begin
    DataOut <= Data(to_integer(unsigned(AddressIn)));
end architecture;

La memoria RAM sí requiere de la señal de reloj ya que es un circuito secuencial. La implementación VHDL usada es la recomendada por la mayoría de los fabricantes (usando un array de std_logic_vector):

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity Ram is
    generic (
        GPOutAddress : integer := 4096
    );
    port (
        AddressIn   : in std_logic_vector(12 downto 0);
        DataOut     : out std_logic_vector(15 downto 0);
        DataIn      : in std_logic_vector(15 downto 0);
        WriteEnable : in std_logic;
        GPOut       : out std_logic_vector(15 downto 0);
        Clk         : in std_logic
    );
end entity;

architecture Architecture1 of Ram is
type RamType is array(0 to 8191) of std_logic_vector(15 downto 0);
signal Data : RamType;
begin
    process (Clk)
    begin
        if ((Clk'event) and (Clk = '1')) then
            if (WriteEnable = '1') then
                Data(to_integer(unsigned(AddressIn))) <= DataIn;
            end if;
        end if;
    end process;

    DataOut <= Data(to_integer(unsigned(AddressIn)));
    GPOut <= Data(GPOutAddress);
end architecture;


La RAM incluye un puerto “GPOut” que mapea la dirección de memoria 4096 de la RAM en un puerto de salida de 16 bits. Este añadido se usará más adelante, en la prueba de concepto, para facilitar la depuración del procesador.

La unidad de control

Como se vio en la primera entrega, la unidad de control es realmente una FSM (máquina de estados finita) cuyas salidas gobiernan las entradas “enable” de los registros, las entradas de selección de los multiplexores y el resto de la lógica del procesador.

La FSM de la unidad de control va avanzando el contador de programa, carga las instrucciones en el IR y ejecuta el microcódigo de cada instrucción. Los estados de la FSM comunes a cualquier instrucción que se ejecute son los siguientes (extraído del anterior post):

0. MUX6 := "0", Habilitar PC

1. MUX1 = PC, Habilitar ADDR

2. Habilitar DATAin

3. Habilitar IR

4. MUX5 := FSM, ALU := inc, MUX4 := PC, Habilitar PC

5. EJECUTAR EL MICROCÓDIGO DE LA INSTRUCCIÓN ALMACENADA EN IR

6. Ir al estado 1

A continuación puede verse el grafo completo de la FSM.

El estado 0 es al estado que se va en el reset. A continuación pueden verse también las señales que unen la unidad de control con el resto de la lógica del procesador:

Se ha optado por implementar la FSM como una máquina de tipo Moore (la salida depende sólo del estado actual y el estado siguiente depende de las entradas y del estado actual)

library ieee;
use ieee.std_logic_1164.all;

entity FSM is
    port (
        DataOutEnable  : out std_logic;
        DataInEnable   : out std_logic;
        RAEnable       : out std_logic;
        RBEnable       : out std_logic;
        RCEnable       : out std_logic;
        SPEnable       : out std_logic;
        PCEnable       : out std_logic;
        AddrEnable     : out std_logic;
        IREnable       : out std_logic;
        Mux1Sel        : out std_logic_vector(1 downto 0);
        Mux2Sel        : out std_logic_vector(1 downto 0);
        Mux3Sel        : out std_logic_vector(2 downto 0);
        Mux4Sel        : out std_logic_vector(2 downto 0);
        Mux5Sel        : out std_logic;
        Mux6Sel        : out std_logic;
        AluOpSel       : out std_logic_vector(3 downto 0);
        MemWriteEnable : out std_logic;
        SignExpSel     : out std_logic;
        IRValue        : in std_logic_vector(15 downto 0);
        Clk            : in std_logic;
        Reset          : in std_logic
    );
end entity;

architecture Architecture1 of FSM is
    signal QBus : std_logic_vector(4 downto 0);
    signal DBus : std_logic_vector(4 downto 0);
begin

    process (Clk, Reset)
    begin
        if (Clk'event and (Clk = '1')) then
            if (Reset = '1') then
                QBus <= (others => '0');
            else
                QBus <= DBus;
            end if;
        end if;
    end process;

    -- next state logic
    -- for state "00000"   MUX6 := "0", Enable PC
    DBus <= "00001" when ((QBus = "00000") or (QBus = "00101") or (QBus = "01000") or (QBus = "01001") or (QBus = "01011") or (QBus = "01110") or (QBus = "10010") or (QBus = "10011") or (QBus = "10100") or (QBus = "10101")) else                -- MUX1 := PC, Enable ADDR
            "00010" when (QBus = "00001") else        -- Enable DATAIN
            "00011" when (QBus = "00010") else        -- Enable IR
            "00100" when (QBus = "00011") else        -- MUX5 := FSM, ALU := inc, MUX4 := PC, Enable PC
            -- LOADI states
            "00101" when ((QBus = "00100") and (IRValue(15) = '1')) else                  -- MUX2 := EXP, SignSel := 15 bits, Enable RA
            -- LOAD states
            "00110" when ((QBus = "00100") and (IRValue(15 downto 12) = "0001")) else     -- MUX1 := RB, Enable ADDR
            "00111" when (QBus = "00110") else                                            -- Enable DATAIN
            "01000" when (QBus = "00111") else                                            -- MUX2 := DATAIN, Enable RA
            -- OP states
            "01001" when ((QBus = "00100") and (IRValue(15 downto 12) = "0010")) else     -- MUX2 := ALU, MUX3 := dst, MUX4 := src, MUX5 := IR(3..0), Enable dst
            -- STORE states
            "01010" when ((QBus = "00100") and (IRValue(15 downto 12) = "0011")) else     -- MUX1 := RB, Enable ADDR, Enable DATAOUT
            "01011" when (QBus = "01010") else                                            -- WE := 1
            -- PUSH states
            "01100" when ((QBus = "00100") and (IRValue(15 downto 12) = "0100")) else     -- MUX4 := SP, MUX5 := FSM, ALU := inc, Enable SP
            "01101" when (QBus = "01100") else                                            -- MUX1 := SP, Enable ADDR, Enable DATAOUT
            "01110" when (QBus = "01101") else                                            -- WE := 1
            -- POP states
            "01111" when ((QBus = "00100") and (IRValue(15 downto 12) = "0101")) else     -- MUX1 := SP, Enable ADDR
            "10000" when (QBus = "01111") else                                            -- Enable DATAIN
            "10001" when (QBus = "10000") else                                            -- Enable RA, MUX2 := DATAIN
            "10010" when (QBus = "10001") else                                            -- MUX4 := SP, MUX5 := FSM, ALU := dec, Enable SP
            -- J states
            "10011" when ((QBus = "00100") and (IRValue(15 downto 12) = "0110")) else     -- MUX4 := EXP, SignSel := 12 bits, MUX3 := PC, MUX5 := FSM, ALU := add, Enable PC
            -- JZ states
            "10100" when ((QBus = "00100") and (IRValue(15 downto 12) = "0111")) else     -- MUX4 := EXP, SignSel := 12 bits, MUX3 := PC, MUX5 := FSM, ALU := add if RA=0, Enable PC
            -- JN states
            "10101" when ((QBus = "00100") and (IRValue(15 downto 12) = "0000")) else     -- MUX4 := EXP, SignSel := 12 bits, MUX3 := PC, MUX5 := FSM, ALU := add if RA<0, Enable PC
            "00000";

    -- output logic
    DataOutEnable <= '1' when (QBus = "01010") or (QBus = "01101") else
                     '0';
    DataInEnable <= '1' when (QBus = "00010") or (QBus = "00111") or (QBus = "10000") else
                    '0';
    RAEnable <= '1' when (QBus = "00101") or (QBus = "01000") or (QBus = "10001") or ((QBus = "01001") and (IRValue(10 downto 8) = "000")) else
                '0';
    RBEnable <= '1' when (QBus = "01001") and (IRValue(10 downto 8) = "001") else
                '0';
    RCEnable <= '1' when (QBus = "01001") and (IRValue(10 downto 8) = "010") else
                '0';
    SPEnable <= '1' when (QBus = "01100") or (QBus = "10010") or ((QBus = "01001") and (IRValue(10 downto 8) = "011")) else
                '0';
    PCEnable <= '1' when (QBus = "00000") or (QBus = "00100") or (QBus = "10011") or (QBus = "10100") or (QBus = "10101") or ((QBus = "01001") and (IRValue(10 downto 8) = "100")) else
                '0';
    AddrEnable <= '1' when (QBus = "00001") or (QBus = "00110") or (QBus = "01010") or (QBus = "01101") or (QBus = "01111") else
                  '0';
    IREnable <= '1' when (QBus = "00011") else
                '0';
    Mux1Sel <= "00" when (QBus = "00110") or (QBus = "01010") else
               "01" when (QBus = "01101") or (QBus = "01111") else
               "10";
    Mux2Sel <= "00" when (QBus = "01000") or (QBus = "10001") else
               "01" when (QBus = "01001") else
               "10";
    Mux3Sel <= "100" when (QBus = "10011") or (QBus = "10100") or (QBus = "10101") else
               IRValue(10 downto 8);
    Mux4Sel <= "011" when (QBus = "01100") or (QBus = "10010") else
               "100" when (QBus = "00100") else
               "101" when (QBus = "10011") or (QBus = "10100") or (QBus = "10101") else
               IRValue(6 downto 4);
    Mux5Sel <= '0' when (QBus = "01001") else
               '1';
    Mux6Sel <= '0' when (QBus = "00000") else
               '1';
    AluOpSel <= "0111" when (QBus = "00100") or (QBus = "01100") else
                "1000" when (QBus = "10010") else
                "0001" when (QBus = "10011") else
                "1100" when (QBus = "10100") else
                "1101" when (QBus = "10101") else
                "0000";
    MemWriteEnable <= '1' when (QBus = "01011") or (QBus = "01110") else
                      '0';
    SignExpSel <= '0' when (QBus = "00101") else
                  '1';
end architecture;

Al igual que en otras ocasiones, una vez tenemos el grafo de la FSM, su implementación en VHDL es totalmente mecánica.

Prueba de concepto

Como primera aproximación se ha creado un fichero Rom.vhd que contiene, escrito a mano, el código máquina del siguiente código ensamblador:

    # GPOut := 10
    loadi 12288
    op rb, ra, assign
    loadi 10
    store
loop:
    # if (GPOut == 0) then goto loopEnd
    loadi 12288
    op rb, ra, assign
    load
    jz loopEnd
    # decrementar GPOut
    loadi 12288
    op rb, ra, assign
    load
    op ra, ra, dec
    store
    # bucle
    j loop
loopEnd:
    j loopEnd


Para este programa el código VHDL de la ROM quedaría como sigue:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity Rom is
    port (
        AddressIn : in std_logic_vector(12 downto 0);
        DataOut   : out std_logic_vector(15 downto 0)
    );
end entity;

architecture Architecture1 of Rom is
type RomType is array (0 to 8191) of std_logic_vector(15 downto 0);
constant Data : RomType := (
    -- simple counter
    "1011000000000000",      --     loadi 12288
    "0010000100000000",      --     op rb, ra, assign
    "1000000000001010",      --     loadi 10
    "0011000000000000",      --     store
                             -- loop:
    "1011000000000000",      --     loadi 12288
    "0010000100000000",      --     op rb, ra, assign
    "0001000000000000",      --     load
    "0111000000000110",      --     jz loopEnd          (+6)
    "1011000000000000",      --     loadi 12288
    "0010000100000000",      --     op rb, ra, assign
    "0001000000000000",      --     load
    "0010000000001000",      --     op ra, ra, dec
    "0011000000000000",      --     store
    "0110111111110110",      --     j loop              (-10)
                             -- loopEnd:
    "0110111111111111",      --     j loopEnd           (-1)
    others => "0000000000000000"
);
begin
    DataOut <= Data(to_integer(unsigned(AddressIn)));
end architecture;

El puerto de salida está en la dirección 4096 de nuestra RAM pero como la RAM está situada después de la ROM, la dirección de memoria de este puerto de salida será realmente 8192 + 4096 = 12288.

Ejecutando la simulación

El paquete de software usado para realizar la simulación es el GHDL, un compilador y simulador VHDL open source que genera ficheros VCD de eventos. Estos ficheros VCD contienen las señales digitales de todo el circuito simulado y son visualizables con herramientas como el GtkWave.

El testbench utilizado se encarga simplemente de generar el tren de pulsos del reloj y de realizar un reset al principio.

Reset <= '0' after 3 ns;
Finished <= '1' after 2 us;
Clk <= not Clk after 1 ns when Finished /= '1' else
       '0';


A continuación pueden verse las señales de nuestra CPU al ejecutar una instrucción LOADI justo después del reset:

Si observamos el puerto de salida GPOut y alejamos el zoom se puede ver cómo el procesador ha ejecutado el programa correctamente (cuenta descendente desde 10 hasta 0 y se detiene).



Ya hemos conseguido que nuestro provesador V1 funcione en un simulador, ahora sólo nos queda implementarlo en una FPGA, pero eso será en la próxima entrega :-).

En la sección soft puede descargarse todo el código VHDL del proyecto.

>>> Enlace a la tercera entrega de la serie.

Comentarios 
Lo sentimos. No se permiten nuevos comentarios después de 90 días.