ETC: Diseño de un Microprocesador
El objetivo de este tema será ver como se construye un procesador capaz de realizar instrucciones de la ISA de MIPS, estas instrucciones se ensamblaran con las puertas lógias que vimos en los dos primeros temas. Para no tener que hacer trescientas instrucciones implementaremos unas cuantas de varios grupos aunque al final
Aritmetico-lógicas: add, sub, and, or y slt.
Lectura y escritura: lw, sw.
El ISA (Instruction Set Architecture) es, como ya sabemos, el tipo de repertorio de instrucciones que es capaz de realizar un microprocesador y que por tanto define las operaciones que hará el procesador. Pero aún hay mas, el ISA tambien determina cuantos registros habrán, de que tamaños serán y en general cuantos bits tienen las partes de una instruccion (codigo de operacion, registro a, registro b, func, shamt...).
En general nosotros usaremos el MIPS32 con algunas simplificaciones gracias al programa MARS.
El ISA del microprocesador a diseñar se divide en las siguientes fases:
Aritmetico-lógicas: add, sub, and, or y slt.
add rd, rs, rt -> rd := rs + rt
Lectura y escritura: lw, sw.
lw rd imm(rs) -> Rd posee el contenido en la Dir Memoria de [rs+imm]
sw rd imm(rs) -> Dir Memoria [rs+imm] contiene rd
beq rd, rs, etiq -> saltamos a etiq si se cumple la condicion con rd y rs
j etiq -> saltamos a etiq
rd, rt, rs son cualquiera de los 32 registros enteros.
imm es un numero en ca2 entre -(2^15) -- (2^15)-1, si fuera sin signo entonces sería un numero hast 2^16
etiq es una direccion de memoria “enmascarada” bajo un nombre cualquiera.
CODIFICACION DE LAS INSTRUCCIONES
Aunque queremos aprender a codificar las instrucciones en primer lugar hay que saber como el procesador lee las instrucciones y en realidad hacemos primero lo contrario, descodificamos la instruccion y luego vemos como implementarla y eso es lo que hacen todos los procesadores antes de realizar ninguna operacion, saber que hay que hacer es el primer paso.
En MIPS las instrucciones se descomponen de tres maneras posibles:
Las aritmetico lógicas seguiran siempre el formato R, mientras que las de acceso a memoria y salto condicional seguirán el formato I y las de salto incondicional siguen su propia estructura.
R: El codigo de operacion siempre es 0, los registros rs, rt, rd están invertidos a la hora de definir de la instruccion. El apartado Shamt es indiferente para nosotros y el apartado func es lo que diferencia una operacion add de otra sub por ejemplo:
add $t1, $t2, $t3 := 000000 | $t2 | $t3 | $t1 | ¿¿?? | 0x20
sub $t1, $t2, $t3 := 000000 | $t2 | $t3 | $t1 | ¿¿?? | 0x22
I: Siguen la misma pauta que las R salvo que el apartado del numero immediato Imm redirecciona en el caso del salto condicional x posiciones atrás o adelante
lw $t1 0($sp) := 0x23 | $t1 | $sp | 0
sw $t1 0($sp) := 0x2b | $t1 | $sp | 0
beq $t1, $t2, etiq := 0x4 | $t1 | $t2 | -5
J: Lo mismo que antes, salvo que en vez de un inmediato es una direccion de memoria.
j label := 0x2 | 0x100020
TIEMPO DE EJECUCIÓN DE LAS INSTRUCCIONES
Para modelar cuanto tiempo tarda un ISA en ejecutar cierto programa utilizaremos una simple fórmula que ayudará a comparar que programa es mas rapido en que ISA o cual es el tiempo medio.
T(ejec) = Nº(inst) * C.P.I. * T(ciclo)
T(ejec): Es el tiempo de ejecucion final del programa, su unidad depende de la unidad en el tiempo de ciclo que hubiesemos usado.
Nº(inst): Es el numero total de instrucciones del programa.
CPI: Es la media de todos los ciclos entre todas las instrucciones.
T(ciclo): Tiempo que necesita el procesador para dar un ciclo.
Dependiendo de nuestra implementación, esta operacion puede reducirse o incrementarse. Por ejemplo si mi T(ciclo) fuera 1 siempre, tendría ciclos constantes pero instrucciones que podrían estar perdiendo tiempo sin hacer nada.
Implementacion Monociclo: Todas las instrucciones se realizan en un ciclo, por lo tanto CPI es 1 pero T(ciclo) es mayor.
Implementacion Multiciclo: Algunas instrucciones requieren mas de un ciclo, CPI es mayor que 1 pero T(ciclo) baja. Esta será la mas general de todas.
Implementación Segmentada: Las instrucciones se dividen en pequeños pasos, cada paso conlleva un ciclo pero el inicio de la instruccion se ejecuta en cada ciclo (asi que no hay que esperar a instrucciones mas tardías o mas tempranas). CPI segmentado<CPI multiciclo, CPI segmentado es próximo a 1 y el Tciclo es menor.
Implementacion Multiescalar: Ademas de segmentar las instrucciones, se ejecutan varias instrucciones de golpe, lo que nos permite que el CPI sea menor que 1.
VISION GENERAL DE LA IMPLEMENTACIÓN
Usaremos los componentes vistos en los anteriores temas, puertas lógicas, registros, ALU, Bancos de Registros y Memoria.
Para sincronizar el acceso a cada una de las partes usaremos los ciclos de reloj por lo que todos los componentes estan conectados por la misma señal
Las entradas a los bloques logios combinacionales serán valores que se calcularon en los anteriores (no puedo sumar dos registros si no calcule antes cuales eran).
El valor de salida del bloque, se escribirá al final del ciclo actual (en el flanco activo)
Se deben guardar en elementos de estados todo los datos calculados en un ciclo que se necesiten en los ciclos posteriores. Bancos de registros, registro PC, Memoria y Registros Auxiliares.
Supondremos que la ALU, la memoria, y el banco de registros son las unidades funcionales principales. Asi que la mas lenta de todas es la que marcará el tiempo mínimo de ejecución.
La entrada de una unidad funcional no puede depender de los valores calculados por otra en el mismo ciclo, asi que no pueden conectarse en serie.
Los registros auxiliares se utilizan para comunicar la ALU, la memoria y el Banco de Registros. Los datos escritos en un registro están disponibles al ciclo siguiente, si el registro no tiene señal de habilitación de escritura entonces los datos se sobreescribirán durante todos los ciclos:
PC: Almacena la direccion de la siguiente instrucción que se realizará.
IR (Instruction Register): Almacena los 32 bits (en mips) de la instruccion que se ejecutara.
MDR (Memory-Data Register): Almacena un dato leido desde la memoria.
Registros A y B: Almacenan los datos leidos desde el banco de registros.
SalidaAlu (AluOut): Almacena el resultado de la ALU.
INSTRUCCIONES PASO A PASO
Tendremos que subdividir cada instrucción para que nuestro procesador pueda ejecutarlas en pequeños pasos.
Nos interesa balancear lo mejor posible el trabajo entre los pasos para aprovechar el máximo tiempo de cada ciclo.
En cada paso necesitaremos que se activen distintos componentes, y que se coneten entre sí de formas diferentes.
En base a esta información todas las instrucciones requieren la búsqueda de la instruccion y la decodificación. Dependiendo del tipo de instruccion requeriran 3 o mas pasos.
J: Búsqueda de instrucción, decodificación, salto.
Beq: Búsqueda de instruccion, decodificación, comparación y salto.
Add,sub,slt,and,or: Búsqueda de instruccion, decodificación, operacion, finalización de la operación.
Lw: Búsqueda de instrucción, decodificación, cálculo de la dirección, lectura de memoria, finalización de lectura.
Sw: Búsqueda de instrucción, decodificación, calculo de la dirección, escritura en memoria.
PASO 1: BÚSQUEDA DE LA INSTRUCCION
IR : Memoria[PC] //la instrucción está en la celda de memoria que señala PC.
PC = PC + 4 //Aumento PC+4 para encontrar la siguiente direccion de memoria de la instrucción.
Lo primero que hacemos es meter la direccion del pc en la memoria. El dato que se lee entra directamente al registro de instruccion donde se descompondrá en diversas partes. Simultaneamente el PC entra a la ALU donde se le sumará 4 y se llevará al PC de nuevo.
PASO 2: DECODIFICACIÓN Y LECTURA DE OPERANDOS
A = Reg[IR[25-21]] //El operando A tiene 5 bytes (los 6 anteriores eran el op) y entra al banco de registros para grabarse en el dato leido 1.
B = Reg[IR[20-16]] //El operando B tiene 5 bytes y entra al banco de registros para grabarse en el dato leido 2.
AluOUT = PC + extender_signo(15-0) <<2 //Si hubiera que calcular una direccion de salto se hace desde aqui.
Asi que en general guardamos en rs, A, en rt B y la direccion de salto aunque no tengamos porque utilizarlos despues. Los primeros seis bits de la instruccion son el codigo de operacion, que no me interesa. Los siguientes 5 se meten al registro de lectura 1 que pasa al registro A, los otros 5 pasan al registro de lectura 2 y salen por el registro B.
Suponiendo las operaciones aritmeticológicas (add, and, sub....)
AluOUT = A func B //dependiendo de la funcion habrá que meter uno u otro (func = 6 bits).
Dependiendo del codigo de operacion se realizara una funcion u otra que saldrá por AluOUT.
PASO 4a: FINALIZACIÓN DE LA INSTRUCCIÓN
Suponiendo las operaciones aritmeticológicas (add, and, sub...)
Reg[IR[15-11]] = AluOUT //Escribimos en el registro rd (15-11) lo que salió de la AluOUT
El registro destino se encuentra en la direccion 15-11 del registro de instrucciona si que metemos eso en la direccion de registro de escritura y metemos la salida de la ALU como entrada para escribirse en el banco de registros.
PASO 3b) CALCULO DE LAS DIRECCIONES DE MEMORIA
Suponiendo instrucciones lw, sw.
AluOUT = A + extender_signo(Reg[IR[15-0]])
La direccion de memoria ocupa 16 bits asi que para calcularlo cogemos los ultimos 16 bits del IR y lo extenderemos para sumarle el registro A en la ALU.
PASO 4a) LECTURA DE MEMORIA
Suponiendo la instrucción lw.
MDR = Memoria[AluOUT] //La direccion de memoria que quiero leer es la posicion que señala AluOUT y se guarda en el registro de memoria
PASO 5) FINALIZACIÓN DE LA LECTURA
Suponiendo la instruccion lw.
Reg[IR[20-16]] = MDR //El banco de Registros almacena el dato que contiene el registro de datos de memoria en el registro Rt.
PASO 4c) ESCRITURA EN MEMORIA
Suponiendo la instruccion sw.
Memoria[AluOUT] = B //Guardo en la posicion de memoria señalada por la saluda de la ALU el contenido del registro B.
PASO 3c) COMPARACION Y SALTO
Suponiendo la instrucción beq.
Si (A==B) entones PC = AluOut
Si la resta es igual a 0 entonces se habilita la escritura en el PC y se mete la AluOut (que ya contenía el PC + 4).
Suponiendo la instrucción j.
PC = PC [31-28] << 28 || IR [25-0]<<2
Se cogen los primeros cuatro bits del pc y del resto del IR tras extenderlo dos a la izquierda antes de pasarselo al PC.
COMBINANDO TODOS LOS PASOS
Necesitamos un camino de datos capaz de realizar todos los pasos que hemos visto anteriormente. Como cada paso requiere que utilicen unos bloques funcionales que no se usaran de forma constante necesitamos meter multiplexores en caso todos los sitios.
CONTROL DEL CAMINO DE DATOS
Para guiar todo el camino que han de recorrer los datos necesitamos un dispositivo que llamaremos unidad de control.
Esta unidad de control seleccionará las entradas adecuadas de los multiplexores.
Activar las señales de habilitacion de las entradas de escritura y/o de lectura en los bloques lógicos siempre y cuando sea necesario.
Indicar a la ALU que operación realizar.
EscPC: Controla la escritura en el PC.
1: Se actualiza con el valor que hay en la entrada (1,3c,3d).
LeerMem: Activa la lectura de la memoria.
1: Se lee la dirección de la memoria indicada (1, 4b).
EscMem: Activa la escritura de la memoria.
1: Se escribe el dato en la posicion de memoria indicada (4c).
EscIr: Activa la escritura en el registro de instrucciones.
0: Se conserva el registro de instrucciones.
1: Se escribe la instruccion el Registro de Instrucciones (1).
EscReg: Controla la escritura en el banco de registros.
0: Se conserva el contenido en el banco de registros.
1: Se escribe en el banco de registros el dato dado (4a,5).
IoD: Selecciona que dirección se utiliza para acceder a la memoria.
0: El PC suministra la direccion para acceder a la memoria (1).
1: AluOut proporciona la direccion de memoria para acceder a memoria (4b,4c).
RegDest: Selecciona que campo elige el registro a escribir (destino).
0: El registro destino es el registro rt [20-16].
1: El registro destino es el registro rd [15-11].
MemAReg: Selecciona qué se escribe en el banco de registros.
0: El valor proviene de la salida AluOUT (4a).
1: El valor proviene del registro MDR (5).
SelAluA: Selecciona el dato A de la Alu.
0: El dato es el PC (1,2)
1: El primer dato de la Alu es el registro A (3a,3b,3c)
SelAluB: Selecciona el dato B de la Alu.
00: El registro B (3a, 3c)
10: Los 16 bits de menos peso del IR extendidos de signo (3b)
11: Los 16 bits de menos peso del IR extendidos de signo y desplazados a la izquierda 2 bits (2).
FuentePC: Selecciona que se escribe en el registro PC.
00: La salida de la Alu es (1)
01: El contenido de la ALUOut (3c)
10: El campo j desplazado dos bits a la izquierda y concatedano con los cuatro bits de mayor peso del pc actual (3d).
AluOP: Selecciona que la funcion de operacion va a realizarse en el Alu.
00: La Alu realiza una suma
01: La Alu realiza una resta
10: Realiza un AND, OR, suma, resta, comparacion o desplazamiento dependiendo de lo que haya en func.
Para que funcione la instruccion beq hay que usar un par de puertas lógicas. En especial esa condicional para que debamos de habilitarla o no (esa puerta logica ademas se conecta con la señal “cero”..
INSERCION DE NUEVAS INSTRUCCIONES
Para insertar una nueva instruccion necesitaremos la metodología siguiente:
Especificacion precisa de la semántica del proceso de la instruccion (que es lo que hace la instruccion de forma verbal).
Identificacion del trabajo a realizar por cada uno de los bloques logicos (Banco de registros, ALU y Memoria).
Establecimiento del orden de precedencia de las funciones.
Definicion de cada uno de los ciclos de esa instruccion.
Division del trabajo entre ciclos.
Extension del camino de datos si fuera necesario.
Extension de las señales de control.
Ejemplo: Implementar una instruccion push que hace espacio en la pila para un registro X.
Banco de Registros: Leer el registro $sp, leer el registro $x, escribir $sp-4 en $sp.
Memoria: Acceder a la direccion de $sp actualizado, meter el valor del registro x en esa direccion.
Leer $sp -> Leer $x -> Calcular $sp - 4 -> Escribir $x en $sp -4 || Escribir en $sp, $sp - 4
Elijo Sp como Rs y como Rd porque tendré que guardarla en el mismo sitio, sin embargo, x no hace nada en esa operacion asi que lo meto en el “temporal”.
A = Reg[IR[26-21]] || A = Reg[rs]
B = Reg[IR[20-16]] || B = Reg[rt]
Extension del camino de datos: No hace falta implementar ningun tipo de cable al procesador, por tanto no hay que extender el camino de datos.
Extension del camino de control:
En el Ciclo 3 es necesario que SelAluA valga 1 (para seleccionar como operando el registro A), SelAluB será 01 (para tomar la constante 4) y AluOp tendrá el codigo de la suma 00.
En el ciclo 4 es necesario que se habilite la escritura en el banco de registros EscReg 1, MemAReg deberá de ser 0 (porque el dato que voy a meter sale de AluOut), EscMem será 1 para habilitar la escritura en la Memoria, y LeerMem será 1 para poder leer la posicion de memoria que señala AluOut pero antes IoD tiene que seleccionar AluOut como lectura para saber a que memoria acceder (1).
bltzal $1, $2: Realiza la suma de los dos registros y en dicha posicion de memoria almacena la direccion de la siguiente instruccion a ejecutar.
R: Op = bltzal, Rs = $1, Rt = $2, Rd = $1, shamt = ¿¿|??, func = ¿¿|??
A = Reg[IR[26-21]] = Reg[rs]
B = Reg[IR[20-16]] = Reg[rd]
Modificacion del camino de datos: Sí, necesario un cable para conectar el registro A con la memoria (esto puede ser NO necesario si consideramos que en A está $2, y en B $1 pero considerando el ejemplo sí. Habrá que poner un multiplexor (DatoEscMem) que permita variar entre 0 (tomamos el dato B) o 1 (tomamos el dato A)
SelAluA = 1 tomo el valor A
SelAluB = 00 tomo el valor B
AluOp = 00 realizo la suma
EscrMem = 1 habilito la escritura
IoD = 1 tomo aluout para la direccion
DatoEscMem = 1 (considerando que $1 está en A)
jal etiq : Se produce un salto hacia la etiqueta con un link.
I: op = jal, rs = $31 ($ra), rt = $31 , imm
PC = PC + 4 //por aluresult se calcula este proceso, justo antes de AluOut
AluOut = Reg[rt] //lo sacamos de AluOut
PC = PC+[Extender_signo(15-0)<<2 //la etiqueta está en el immediato.
Extension del camino de datos: Ninguno.
Señales de control:
EscReg = 1 habilitamos la capacidad de escribir en los registros.
RegDest = 0 seleccionamos el registro rs
MemAReg = 0 seleccionamos como dato de entrada al banco de registros AluOut
SelAluA = 0 seleccionamos PC
SelAluB = 11 seleccionamos extender_signo(15-0)<<2
EscrPc = 1 seleccionamos para habilitar la escritura
AluOp = 00 seleccionamos la suma
FuentePC = 01 seleccionamos AluOut