Las siguientes imagenes son esquemas simplificados del diseño completo del pipeline MIPS, sus etapas con todos sus componentes dentro.
Este modulo, llamado I_FETCH.v consiste en una memoria de instrucciones (MEM_INSTRUCCIONES.v), el program counter (PC.v), un sumador (PC_ADDER.v), un multiplexor para elegir el proximo pc (PC_MUX.v) y otro (MUX_INSTR_NOP.v) para seleccionar la instruccion obtenida de la memoria, intercalar una NOP en caso de un branch o una HALT en caso de esa instruccion.
Sus inputs:
- i_address: es la direccion que ingresa a la memoria, en caso de estar cargandola viene desde la debug unit y se guarda la instruccion en dicha posicion, y en caso de estar funcionando el pipeline se direcciona mediante el pc y se obtiene la instruccion en esa posicion.
- i_instruccion: es la instruccion a guardar en la memoria en caso de estar cargandola.
- i_loading: si está en uno significa que estamos cargando instrucciones y si está en cero se van a obtener instrucciones guardadas.
- i_pcburbuja: viene desde el detector de riesgos, si está en uno se mantiene el pc anterior para hacer un stall.
- i_start: indica que el procesador esta en funcionamiento si esta en alto.
- i_step: si esta en alto quiere decir que se esta ejecutando un step mediante debug.
- i_pc_branch: viene desde la unidad de branch e ingresa a pc_mux. Es el pc calculado de una instruccion branch.
- i_pc_jump: viene desde la unidad de jump e ingresa a pc_mux. Es el pc calculado de una instruccion jump.
- i_select: selecciona entre i_pc_jump, i_pc_branch e i_pc_incr (PC + 1) para la entrada del Program Counter.
Está conectada al latch IF_ID mediante sus outputs:
- o_instruccion: es la instruccion obtenida de la memoria, que seguira su camino en el pipeline.
- o_pc_incr: es PC + 1 de la instruccion obtenida.
Además, tiene la salida o_pc_debug que está conectada a la debug unit.
Consiste en un banco de registros (REG_BANK.v), la unidad de branch (U_BRANCH.v), la de jump (U_JUMP.v), un extensor de signo (EXTENSOR.v), la unidad de control principal (CONTROL_PRINCIPAL.v), un sumador que obtiene el pc de una instruccion branch, un mux (MUX_CONTROL_PRINCIPAL.v) que elige entre los bits de control de c/ instruccion o bits "nulos" si se trata de un stall y otro (MUX_RD_31.v) que elige entre el registro rd, normalmente, o en caso de se una instruccion JAL elige el reg 31.
Inputs:
- i_debug: ingresa al reg bank, es un pulso que va aumentando un contador para ir pasando uno a uno el contendido de los registros hacia la unidad de debug.
- i_rt_rd: viene de WB, es el registro que se debe escribir en caso de ser necesario.
- i_regwrite: si es uno, se escribe un registro.
- i_writedata: el dato a escribir.
- i_instruccion: es la instruccion actual.
- i_burbuja: entra al mux que en caso de ser un uno, intercala bits de control nulos por ser un stall.
- i_currentpc: ingresa al sumador para obtener el pc nuevo en caso de un branch. Tambien a la unidad de jump para calcular el pc en dichas instrucciones.
Esta etapa esta conectada con sus salidas a ID_EX:
- o_regA: es el contenido del registro indicado por la entrada rs.
- o_regB: es el contenido del registro indicado por la entrada rt.
- o_mem: son 3 bits de control para la etapa MEM obtenidas segun c/ instruccion en la unidad de control principal.
- o_ex: son 4 bits de control para la etapa EXECUTE obtenidas segun c/ instruccion en la unidad de control principal.
- o_wb: son 2 bits de control para la etapa WB obtenidas segun c/ instruccion en la unidad de control principal.
- o_signedmem: es un bit de control para las instrucciones de tipo load.
- o_sizemem: 2 bits de control para instrucciones load y store.
- o_halt: esta en alto si la instruccion actual es HALT, este bit se va pasando por todas las etapas para en el final parar el procesador, dejando terminar las instrucciones anteriores.
- o_branch: en caso de una instruccion branch, se pone en uno.
- o_pcbranch: el proximo pc en caso de branch.
- o_jump: en caso de una instruccion jump, se pone en uno.
- o_pcjump: el proximo pc en caso de jump.
- o_return: se pone en alto con las instrucciones JAL y JALR para guardar la direccion de retorno.
- o_return_address: la direccion de retorno en caso de JAL y JALR. Con la primera se escribe en el reg 31 y con la segunda en el indicado en la instruccion.
- o_opcode: son los 6 bits mas altos de la instruccion.
- o_rs: los bits 25:21 de la instruccion.
- o_rt: los bits 20:16 de la instruccion.
- o_rd: los bits 15:11 de la instruccion.
- o_extendido: bits 15:0 de la instruccion.
Está conectada a la debug unit con o_reg_debug para ir pasando el contenido de los registros a la misma.
Esta etapa consta de una ALU (ALU.v), donde se realizan todos los calculos, un modulo de control de la ALU (ALU_CONTROL.v), 1 mux (MUX_EX_REGA_RESULT_MEM.v) que define el dato a y 2 (MUX_EX_REGA_RESULT_MEM.v y MUX_2_1_EX.v) que definen el dato b que ingresan a la ALU. Además hay otro mux (MUX_RT_RD.v) que define si el registro a escribir en la instruccion correspondiente es rt o rd segun regdst.
Inputs:
- i_cortocircuito A y B: son 2 bits que vienen de la unidad de cortocircuito que determinan de donde se obtienen los datos A y B de la ALU.
- i_reg A y B: son los valores que vienen del banco de registros (rs y rt).
- i_aluresult: es el valor de la ultima operacion realizada por la ALU, para utilizar si hay que hacer cortocircuito.
- i_reg_mem: es el valor leido en memoria, para utilizar si hay que hacer cortocircuito de un load.
- i_ex: son los bits de control de esta etapa. Alusrc es el bit mas alto y determina que valor se seleccionara para el dato B de la ALU. Aluop son los 2 bits mas bajos y se usan para el control de la ALU.
- i_extendido: son los 16 bits mas bajos de la instruccion, pueden ser usados como dato B.
- i_rd, i_rt: son los registros de la instruccion y entran a MUX_RT_RD.v para determinar cual será escrito.
- i_opcode: se utiliza para control de la ALU.
Mediante sus outputs está conectado al latch EX_MEM:
- o_regB: en el caso de un store es el valor que se escribira en memoria.
- o_aluresult: es el resultado de la ultima operacion.
- o_rd_rt: es el registro que será escrito en la etapa WB.
Consiste en una memoria de datos (MEM_DATOS.v). Inputs:
- i_address: direccion de la memoria donde se escribira o leera la memoria.
- i_datawrite: dato a escribir en la memoria.
- i_debug: ingresa a la memoria, es un pulso que va aumentando un contador para ir pasando uno a uno el contendido de la memoria hacia la unidad de debug.
- i_mem: son 2 bits de control de la etapa. Memread si es uno es un load y memwrite es uno si es un store.
- i_signedmem: indica si la instruccion de load es signada o no.
- i_sizemem: indica el tamaño de la operacion de memoria (byte, half word o word).
Con sus outputs esta conectada al latch MEM_WB:
- o_dataread: es el dato leido.
Con la salida o_mem_debug está conectada a la unidad de debug por la cual le enviara los datos leidos de la memoria.
Consiste en 2 mux (MUX_DATAREG.v y MUX_WB.v). El primero en el caso de ser un JAL o JALR elige como salida i_return_address y en las demas i_address (el resultado de la ALU). El segundo MUX elige entre el dato leido en la memoria, en loads, y si es otra operacion elige la salida del mux anterior.
Inputs:
- i_dataread: dato leido en MEM.
- i_address: resultado de la ALU.
- i_return: si es 1 se trata de un JAL o JALR.
- i_return_address: dato a escribir en un registro en caso de JAL o JALR.
- i_memtoreg: bit de control de la etapa, este indica si se debe escribir un registro.
Su salida o_mem_or_reg es el dato a escribir en el reg bank o el dato leido en memoria que sirve para la unidad de cortocircuito.
Estos son quienes reciben y pasan informacion de una etapa a otra, los bits de control, etc. Estan manejados por el clock y tienen las entradas i_start que se pone en 1 cuando la unidad de debug lanza el procesador, e i_step que en caso de modo continuo es siempre 1 y en modo debug se activa con un comando por UART. Estas entradas permiten el avance de los datos, y por consiguiente del procesador, o no.
Esta unidad recibe los registros rs y rt de la instruccion actual y los compara con los registros rt o rd que contienen los resultados de la ALU o de memoria de las 2 instrucciones anteriores y decide si debe hacer cortocircuito o no.
Esta unidad detecta si hay riesgo de load, que es cuando hay que hacer cortocircuito pero al ser un load hay que esperar un ciclo para tener el dato. En caso de ser necesario, se hace un stall que para el PC, intercala bits de control nulos entre la operacion actual y la siguiente.
Esta unidad es una FSM de 6 estados, con codificación One-Hot/One-Cold. Es una interfaz entre el procesador y el usuario y por esto cuenta con un modulo UART.v. Se encarga de proporcionarle las instrucciones y el modo de funcionamiento al MIPS, y los datos del PC, registros y memoria de datos al usuario. Estados:
- STATE_RECEIVING_INSTRUCTION: es el estado inicial, recibe de a 8 bits mediante UART y forma las instrucciones de 32 bits, una vez que c/u está completa se las envía a la memoria de instrucciones del procesador. Hace esto hasta recibir una instrucción completa de '0s' que significa cambio al estado STATE_RECEIVE_MODE.
- STATE_RECEIVE_MODE: En este estado espera recibir 8 bits que son un código para decidir el modo de funcionamiento del procesador. Todos '1s' significa modo debug, por lo que luego pasa a STATE_DEBUG, y todos '0s' es modo continuo entonces pasa a STATE_CONTINUE.
- STATE_DEBUG: Se envía la señal de start al procesador y está a la espera de recibir el código 8'b10101010 que envía un pulso de step al procesador y pasa a STATE_DEBUG_SEND. Si recibe i_finish significa que el procesador ejecutó todo el programa y entonces pasa a STATE_FINISH.
- STATE_DEBUG_SEND: obtiene cada vez el estado del PC, los registros y la memoria de datos del procesador y los va enviando mediante UART uno por uno de a 8 bits a la vez. Cuando termina vuelve a STATE_DEBUG.
- STATE_CONTINUE: espera a que el procesador termine y envie i_finish entonces pasa a STATE_FINISH.
- STATE_FINISH: obtiene el estado final del PC, los registros y la memoria de datos del procesador y los va enviando mediante UART uno por uno de a 8 bits a la vez. Cuando termina pasa a STATE_RECEIVING_INSTRUCTION.
- Limite superior 70 MHz: Al setear el clock interno a este valor, no se cumplen los requerimientos de timing debido al setup time, ya que el clock es mucho más rápido que la lógica del sistema y no se llega a obtener el dato estable antes del flanco de clock en el tiempo necesario.
- Limite inferior 19 MHz: Con el clock en esta frecuencia la lógica del sistema es más rápida que el clock y entonces el dato cambia antes de cumplir con el hold time necesario, lo que causa falla de requerimientos de timing.
Para el sistema utilizamos un clock del clock wizard de IP-Core, que tiene una frecuencia de input de 100 MHz y alimenta al sistema con una frecuencia de salida de 69 MHz (periodo 14.49275 ns). Escogimos esta frecuencia de clock ya que según lo investigado (https://support.xilinx.com/s/article/57304?language=en_US) la forma de encontrar la maxima frecuencia posible es buscando un clock objetivo que en el reporte de timing de un WNS (worst negative slack) menor a 0. Una vez obtenido, en nuestro caso fue de 70 MHz, se realiza el siguiente calculo:
A lo largo de todo el trabajo fuimos realizando distintos tests para ir probando las distintas partes y funcionalidades que ibamos agregando al diseño. Cómo la mayoría ya no sirve los fuimos eliminando del proyecto una vez que comprobabamos que lo que estaba bajo prueba funcionaba correctamente. Los tests finales del sistema son los siguientes:
- branch_test.v: prueba el codigo assembler del archivo /converter/branch_test.txt en modo continuo.
- jump_test.v: prueba el codigo assembler del archivo /converter/jump_test.txt en modo continuo.
- load_store_test.v: prueba el codigo assembler del archivo /converter/load_store_test.txt en modo continuo.
- step_test.v: prueba el codigo assembler del archivo /converter/step_test.txt en modo debug.
Los cuatro instancian un modulo TOP_MIPS.v y un UART.v y mediante este ultimo envian las distintas instrucciones y el modo de funcionamiento, y luego reciben el estado del procesador.
Para facilitar el testeo de distintas instrucciones y evitar el tedioso trabajo de ir escribiendo bit por bit los distintos codigos realizamos un programa en C, assembler_to_machine_mips.c que traduce las instrucciones MIPS requeridas por el trabajo a su correspondiente código máquina.