Diseño e implementación de un procesador RISC desde cero (III) 
En la anterior entrega de la serie se llegó hasta la fase de simulación y se comprobó, usando el software GHDL, el funcionamiento del procesador V1. En esta tercera entrega se ha implementado y probado el diseño en una FPGA real: una Spartan-3E de Xilinx.

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

Cuando se va a implementar un diseño VHDL sobre un dispositivo real siempre nos podemos topar con problemas o limitaciones inherentes al hardware: por eso el paso final debería ser siempre el sintetizado del código sobre una FPGA. En este caso se optó por el sintetizado sobre una FPGA Spartan-3E de Xilinx, en concreto la que viene incluida en la placa de desarrollo Papilio One.

El problema de la memoria

Todas las FPGAs que hay en el mercado incluyen zonas de su sustrato especialmente diseñadas para implementar memorias RAM y/o ROM. Lo ideal, por lo tanto es que el código VHDL que implemente la ROM y la RAM sea fácilmente detectable o “inferible”, para que el entorno de desarrollo del fabricante utilice estos recursos de forma eficiente y que lo que queremos que sea una RAM se implemente realmente como una RAM y que lo que queremos que sea una ROM se implemente realmente como una ROM.

En el caso del código original que se hizo en la segunda entrega de la serie, a la hora de sintetizar para la FPGA de Xilinx, el entorno de desarrollo ISE detecta y sintetiza la ROM como una ROM pero no es capaz de inferir el código VHDL de la RAM como una RAM: de hecho para la RAM de 8192 palabras de 16 bits sintetiza ¡131072 biestables!

Si nos fijamos en el código VHDL de la RAM de la anterior entrega se puede observar cómo se integró la salida de proposito general (un puerto de salida que se denominó “GPOut”) como salida adicional en la RAM. Para facilitar que el entorno de Xilinx detectase este módulo como una RAM real se pasó la lógica de este puerto GPOut de la RAM al módulo de mas alto nivel “Memory.vhd”. Así mismo se incluyó una entrada “Enable” en el módulo RAM acorde con los estándares de diseño recomendados por los fabricantes. Esta entrada se deja permanentemente a 1 pero facilita al entorno de desarrollo la detección del codigo y su posterior sintetizado como una RAM real.

Ahora “Ram.vhd” es una implementacion más estándar de lo que es una RAM (sin puertos de salida y con una entrada "Enable"):

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

entity Ram is
    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;
        Enable      : in std_logic;
        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 (Enable = '1') then
                if (WriteEnable = '1') then
                    Data(to_integer(unsigned(AddressIn))) <= DataIn;
                end if;
                DataOut <= Data(to_integer(unsigned(AddressIn)));
            end if;
        end if;
    end process;
end architecture;

Y la lógica relacionada con el puerto de salida GPOut se ha trasladado a “Memory.vhd”:

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

entity Memory is
    generic (
        GPOutAddress : integer := 12288
    );
    port (
        AddressIn   : in std_logic_vector(15 downto 0);
        DataIn      : in std_logic_vector(15 downto 0);
        DataOut     : out 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 Memory is
    component Rom is
        port (
            AddressIn : in std_logic_vector(12 downto 0);
            DataOut   : out std_logic_vector(15 downto 0)
        );
    end component;
    component Ram is
        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;
            Enable      : in std_logic;
            Clk         : in std_logic
        );
    end component;
    signal RomOut : std_logic_vector(15 downto 0);
    signal RamOut : std_logic_vector(15 downto 0);
    signal GPOutD : std_logic_vector(15 downto 0);
    signal GPOutQ : std_logic_vector(15 downto 0);
begin
    -- GPOut register
    process (Clk)
    begin
        if ((Clk'event) and (Clk = '1')) then
            GPOutQ <= GPOutD;
        end if;
    end process;
    GPOutD <= DataIn when (to_integer(unsigned(AddressIn)) = GPOutAddress) else
              GPOutQ;
    GPOut <= GPOutQ;

    -- ROM
    Rom1 : Rom port map (    -- 8192 words (16 bit)
        AddressIn => AddressIn(12 downto 0),
        DataOut => RomOut
    );

    -- RAM
    Ram1 : Ram port map (    -- 8192 words (16 bit)
        AddressIn => AddressIn(12 downto 0),
        DataOut => RamOut,
        WriteEnable => WriteEnable,
        Enable => '1',
        DataIn => DataIn,
        Clk => Clk
    );

    -- RAM vs ROM multiplexer
    DataOut <= RomOut when (AddressIn(13) = '0') else     -- bit 13 = 0 --> ROM,  bit 13 = 1 --> RAM
               RamOut;
end architecture;

Con estas pequeñas modificaciones el entorno de desarrollo ISE de Xilinx sí infiere correctamente la RAM a partir del código VHDL y la implementa como una RAM real dentro de la FPGA.

Estados de espera para la memoria

Introducir este modelo de memoria RAM síncrono tanto para la lectura como para la escritura de datos obliga a introducir un estado de espera antes de cada estado que habilite el registro DATAin en la unidad de control. En concreto es necesario introducir el estado de espera en tres sitios de la máquina de estados: en la secuencia le lectura de instrucciones, en el microcódigo de la instrucción POP y en el microcódigo de la instrucción LOAD.

Para la lectura de instrucciones la secuencia de microcódigo quedaría como sigue:

0. MUX6 := "0", Habilitar PC (El vector de reset es el 0)

1. MUX1 = PC, Habilitar ADDR (Se carga IR con la instrucción apuntada por PC)

2. Estado de espera para la lectura de la memoria

3. Habilitar DATAin

4. Habilitar IR

5. MUX5 := FSM, ALU := inc, MUX4 := PC, Habilitar PC (Se hace PC := PC + 1)

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

7. Ir al estado 1

El microcódigo de la instrucción POP quedaría así:

POP
MUX1 := SP, Habilitar ADDR
Estado de espera
Habilitar DATAin
Habilitar RA, MUX2 := DATAin
MUX4 := SP, MUX5 := FSM, ALU := dec, Habilitar SP

Mientras que el microcódigo de la instrucción LOAD sería el siguiente:

LOAD
MUX1 := RB, Habilitar ADDR
Estado de espera
Habilitar DATAin
MUX2 := DATAin, Habilitar RA

Estos tres nuevos estados de espera se introducen como nuevos estados en la FSM de la unidad de control.

Blinker

Para probar el procesador se inicializa la ROM (el fichero “Rom.vhd”) con el código máquina asociado al siguiente código ensamblador V1. El programa simplemente cambia de forma alternativa los bits del puerto GPOut (0x0000, 0xFFFF, 0x0000, 0xFFFF, 0x0000, etc.).

    # [12288] <-- 0
    loadi 12288
    op rb, ra, assign
    loadi 0
    store
loop:
    # RA <-- [12288]
    loadi 12288
    op rb, ra, assign
    load
    # RA <-- NOT RA
    op ra, ra, not
    # [12288] <-- RA
    store
    # wait loop
    loadi 100
    op rb, ra, assign
waitLoop1:
    op ra, rb, assign
    jz waitLoop1End
    loadi 8192
waitLoop2:
    jz waitLoop2End
    op ra, ra, dec
    j waitLoop2
waitLoop2End:
    op rb, rb, dec
    j waitLoop1
waitLoop1End:
    # end of wait loop
    j loop

Se trata de un bucle infinito en el que en cada iteración se cambia de estado el puerto GPOut (alternativamente 0x0000 y 0xFFFF) y que, entre iteración e iteración, incluye dos bucles anidados que provocan un retardo significativo y visible entre cada cambio de estado.

El V1 tiene dos entradas de un bit cada una (Clk y Reset) y una salida de 16 bits (GPOut). La entrada de reloj (Clk) se encuentra conectada internamente en la placa Papilio One a un oscilador de cristal de 32MHz, la entrada Reset se mapea al pin A0 de la placa y la salida GPOut se mapea a los pines B0 a B15 (16 bits). Para comprobar que el blinker funciona basta con conectar un led a cualquiera de los pines B0 a B15.

Resultado

Como se puede ver en el siguiente vídeo, nuestro procesador V1 implementado en la FPGA ejecuta perfectamente el código de la ROM.



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

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