>>> 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.
[ añadir comentario ] ( 1899 visualizaciones ) | [ 0 trackbacks ] | enlace permanente | ( 3 / 1895 )