Diseño,  Jerarquía,  Verificación,  Verificación física

Verificación hardware de un microprocesador simple

Con este ejemplo queremos mostrar algunas características que puedan ser útiles en el diseño de sistemas más complejos; pero la arquitectura de este microprocesador (Multi-Cycle Processor) tiene poco que ver con las solicitadas en la tarea 3 fase 2 (single cycle processor) y 3 (pipeline processor) :

  • Realización de un interfaz para modelar un bus triestado de interconexión de los diferentes elementos de un microprocesador.
    • El bus triestado incomoda bastante a una FPGA y solo las células I/O de las FPGA tienen capacidad para implementarlo.
    • El código es sintetizable en nuestra FPGA Cyclone V
  • Determinación del funcionamiento del microprocesador mediante una especificación ASM (con 11 estados)
  • Es un Multi-Cycle Processor ( se requieren 6 ciclos para completar cada instrucción) sin posibilidad de comenzar una nueva instrucción hasta que ha finalizado la anterior, en contraposición a los denominados «pipeline processors» como el que vais a diseñar en la fase 3
  • Realización de un control cableado (FSM) en lugar de un control microprogramado
  • Implementación de una memoria RAM en la que se almacena el programa y los datos.

La estructura del microprocesador que vamos a diseñar es la de la siguiente figura

Veamos como sería el código del interfaz y del top de la jerarquía.

Interfaz

El interfaz lo voy a utilizar tanto para la distribución de los datos (mediante un bus triestado denomindo sysbus) como para la distribución de las señales de control que irán desde el control path a cada unos de los elementos del data-path (PC,IR, ALU y RAM) y para la recepción de las señales de estado desde el control-path procedentes del data-path.

import cpu_defs::*;

interface CPU_bus (input logic clock, n_reset,
                   inout wire [WORD_W-1:0] sysbus);

//senyales de control
logic ACC_bus, load_ACC, PC_bus, load_PC, load_IR,
      load_MAR, MDR_bus, load_MDR, ALU_ACC, ALU_add,
      ALU_sub, INC_PC, Addr_bus, CS, R_NW;
//senyales de estado
logic  z_flag;
logic [OP_W-1:0] op;

modport IR_port(input clock, n_reset, 
                input Addr_bus, load_IR,
                inout sysbus,
                output op);

modport RAM_port (input clock, n_reset, 
                  input MDR_bus, load_MDR, load_MAR, CS, R_NW,
                  inout sysbus);

modport ALU_port (input clock, n_reset, 
                  input ACC_bus, load_ACC, ALU_ACC, ALU_add, ALU_sub,
                  inout sysbus,
                  output z_flag);
modport PC_port (input clock, n_reset,
                 input PC_bus, load_PC, INC_PC,
                 inout sysbus);

modport seq_port (input clock, n_reset, 
                  input op, z_flag, 
                  output ACC_bus, load_ACC, PC_bus,
                         load_PC, load_IR, load_MAR,
                         MDR_bus, load_MDR, ALU_ACC,
                         ALU_add, ALU_sub, INC_PC,
                         Addr_bus, CS, R_NW);

endinterface

siendo el «package» importado el siguiente

package cpu_defs;

parameter WORD_W = 8;
parameter OP_W = 3;
enum logic[2:0] {LOAD=3'b101,
                 STORE=3'b110,
                 ADD=3'b010,
                 SUB=3'b011,
                 BNE=3'b111} opcodes;
endpackage

Como puede observarse nuestro microprocesador es de 8 bits y tenemos tan solo 3 bits para codificar instrucciones. Vamos a definir únicamente 5 instrucciones.

Top de la jerarquía

Ahora la descripción del top de la jerarquía se ve altamente simplificada. Es sin duda una de las cosas que se pretendía con el uso de la construcción «interface»

import cpu_defs::*;

module CPU (input logic CLOCK_50,
           input logic [0:0]  KEY,
           inout wire [WORD_W-1:0] sysbus);
				
logic clock;
assign clock=CLOCK_50;
logic n_reset;
assign n_reset=KEY[0];

CPU_bus bus (.*);

sequencer s1 (.*);

IR i1 (.*);

PC p1 (.*);

ALU a1 (.*);

RAM r1 (.*);

endmodule

Elementos del data-path

Veamos los elementos del data path

PC

import cpu_defs::*;

module PC (CPU_bus.PC_port bus);

logic [WORD_W-OP_W-1:0] count;
assign bus.sysbus = bus.PC_bus ?{{OP_W{1'b0}},count} : 'z;

always_ff @(posedge bus.clock, negedge bus.n_reset)
  begin
	if (!bus.n_reset)
		count <= 0;
	else
		if (bus.load_PC)
			if (bus.INC_PC)
				count <= count + 1;
			else
				count <= bus.sysbus;
  end
endmodule

ALU

import cpu_defs::*;

module ALU (CPU_bus.ALU_port bus);

logic [WORD_W-1:0] acc;

assign bus.sysbus = bus.ACC_bus ? acc : 'Z;
assign bus.z_flag = acc == 0 ? '1 : '0;

always_ff @(posedge bus.clock, negedge bus.n_reset)
  begin
  if (!bus.n_reset)
    acc <= 0;
  else
    if (bus.load_ACC)
        if (bus.ALU_ACC)
        begin
	    if (bus.ALU_add)
               acc <= acc + bus.sysbus;
            else if (bus.ALU_sub)
               acc <= acc - bus.sysbus;
        end
        else
            acc <= bus.sysbus;
  end
endmodule

Este modulo implementa más bien un registro A(o ACC) y un sumador/restador conectados en modo acumulación, de forma que se puedan hacer operaciones de sumas y restas sin problemas.

IR

import cpu_defs::*;

module IR (CPU_bus.IR_port bus);

logic  [WORD_W-1:0] instr_reg;

assign bus.sysbus = bus.Addr_bus ?{{OP_W{1'b0}}, instr_reg[WORD_W-OP_W-1:0]}:'z;

always_comb
  bus.op = instr_reg[WORD_W-1:WORD_W-OP_W];

always_ff @(posedge bus.clock, negedge bus.n_reset)
  begin
  if (!bus.n_reset)
    instr_reg <= 0;
  else
    if (bus.load_IR)
      instr_reg <= bus.sysbus;
  end

endmodule

RAM

import cpu_defs::*;

module RAM (CPU_bus.RAM_port bus);

logic [WORD_W-1:0] mdr;
logic [WORD_W-1:0] salida_mem;
logic [WORD_W-OP_W-1:0] mar;

assign bus.sysbus = bus.MDR_bus ? salida_mem : 'z;

always_ff @(posedge bus.clock, negedge bus.n_reset)
begin
	if (!bus.n_reset)
            begin
             mar <='0;
             mdr<='0;
            end
	else
		if (bus.load_MAR)
			mar <= bus.sysbus[WORD_W-OP_W-1:0];
		else if (bus.load_MDR)
			mdr <= bus.sysbus;
end

logic [WORD_W-1:0] mem [0:(1<<(WORD_W-OP_W))-1];
initial
   $readmemb("PGM_best.TXT",mem,0);

always_ff @(posedge bus.clock)
begin
 if (bus.CS)
	if (!bus.R_NW)
		mem[mar] <= mdr;
	else
		salida_mem= mem[mar];
end

endmodule

Este último componente si que tiene algunas características que conviene resaltar:

  • Tenemos una memoria «single port» de lectura síncrona
  • Tenemos dos registros de apoyo, uno (mdr) que registra el dato de entrada y otro (mar) que registra la dirección.

Control-path

El control path podemos observarlo en el siguiente fichero

import cpu_defs::*;

module sequencer (CPU_bus.seq_port bus);

enum {s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10}  state;

always_ff @(posedge bus.clock, negedge bus.n_reset)
  begin: seq
    if (!bus.n_reset)
      state <= s0;
    else
       case (state)
          s0: state <= s1;
          s1: state <= s2;
          s2: state <= s3;
          s3: if (bus.op == STORE)
                 state <= s4;
               else
                 state <= s6;
          s4: state <= s5;
          s5: state <= s0;
          s6: if (bus.op == LOAD)
                 state <= s7;
               else if (bus.op == BNE)
                  if (~bus.z_flag)
                   state <= s9;
                  else
                   state <= s10;
               else
                   state <= s8;
          s7: state <= s0;
          s8: state <= s0;
          s9: state <= s0;
          s10: state <= s0;
    endcase
  end
always_comb
begin: com
  // reset all the control signals to default
  bus.ACC_bus = '0;
  bus.load_ACC = '0;
  bus.PC_bus = '0;
  bus.load_PC = '0;
  bus.load_IR = '0;
  bus.load_MAR = '0;
  bus.MDR_bus = '0;
  bus.load_MDR = '0;
  bus.ALU_ACC = '0;
  bus.ALU_add = '0;
  bus.ALU_sub = '0;
  bus.INC_PC = '0;
  bus.Addr_bus = '0;
  bus.CS = '0;
  bus.R_NW = '0;
  case (state)
    s0: begin      bus.PC_bus = '1;
      bus.load_MAR = '1;
      bus.INC_PC = '1;
      bus.load_PC = '1;
      end
    s1: begin
      bus.CS = '1;
      bus.R_NW = '1;
      end
    s2: begin
      bus.MDR_bus = '1;
      bus.load_IR = '1;
      end
    s3: begin
      bus.Addr_bus = '1;
      bus.load_MAR = '1;
      end
    s4: begin
      bus.ACC_bus = '1;
      bus.load_MDR = '1;
      end
    s5: begin
      bus.CS = '1;
      end
    s6: begin
      bus.CS = '1;
      bus.R_NW = '1;
      end
    s7: begin
      bus.MDR_bus = '1;
      bus.load_ACC = '1;
      end
    s8: begin
      bus.MDR_bus = '1;
      bus.ALU_ACC = '1;
      bus.load_ACC = '1;
      if (bus.op == ADD)
        bus.ALU_add = '1;
      else if (bus.op == SUB)
        bus.ALU_sub = '1;
      end
    s9: begin
      bus.MDR_bus = '1;
      bus.load_PC = '1;
      end
    s10: ;
endcase
end
endmodule

Como puede observarse es una máquina Mealy de 11 estados

Procedimiento de verificación

Supongamos que tenemos un programa preparado para comprobar todas las instrucciones desarrolladas. En nuestro caso es un programa para generar números fibonacci.

10110010   //load A,[18];   Carga en el acumulador el número i-ésimo.
01010011   //add A,[19];    Súmale el (i+1)-ésimo.
11010100	//store [20],A;	 Guardalo en la siguiente posición.
10100000   //load A,[0];    Coje la primera instrucción.
01010010   //add A,[18];    Súmale 1, (Fib(0)=1). De este modo aumenta en uno la dirección.
11000000   //store [0],A;   Guarda la nueva instrucción.
10100001	//load A,[1];    idem...
01010010	//add A,[18];
11000001	//store [1],A; 
10100010	//load A,[2];    idem...
01010010	//add A,[18];
11000010	//store [2],A;  
10101111	//load A,[15];    idem...
01110010	//sub A,[18];
11001111	//store [15],A;
00001011        //cantidad de numeros fibonacci
11110001	//jump 0;		 Ejecuta las nuevas instrucciones.
00000000 
00000001   //Fib(0)
00000001   //Fib(1)

Vamos a explicar con el siguiente vídeo cómo verificar el funcionamiento del microprocesador desarrollado bajo una plataforma FPGA remota.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *