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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
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
1 2 3 4 5 6 7 8 9 10 |
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»
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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.