>>> 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.
Lo sentimos. No se permiten nuevos comentarios después de 90 días.