Bueno, como prometí, voy a intentar explicar el ciclo de instrucción.

Los procesadores son máquinas secuenciales, es decir, su funcionamiento se basa en ejecutar una tras otra las instrucciones contenidas en memoria.

Estas instrucciones, como ya comenté, se encuentran contenidas en memoria como una ristra de 0 y 1 (para que resulte más fácil su manejo los veremos como dígitos hexadecimales).

El ciclo de instrucción podemos dividirlo en tres partes:

  • 1. Tomar la siguiente instrucción (fetch).
  • 2. Decodificar la instrucción.
  • 3. Ejecutar la instrucción.

1. Tomar la siguiente instrucción (fetch).
El procesador siempre tiene su registro PC (Contador de Programa) apuntando a la dirección de memoria de la siguiente instrucción a ejecutar. Durante esta fase el procesador extrae la instrucción de la memoria apuntada por el PC volcando el contenido de este registro al bus de direcciones y pidiendo una operación de lectura a la memoria. Simultáneamente a esta petición, el PC se incrementará en una unidad para apuntar a la siguiente instrucción o bien al primer operando de la instrucción extraída si esta lo tuviera.

Cuando la memoria está preparada para atender la petición, vuelca el contenido de la dirección pedida al bus de datos de donde el procesador recoge el código de operación y lo coloca en un registro especial llamado Registro de Instrucción (IR).

2. Decodificar la instrucción.
Una vez tiene el código de operación alojado en el IR, el procesador decodifica éste para saber de qué instrucción se trata y obtiene los parámetros de la memoria (si los tuviera), incrementando el PC en una unidad por cada parámetro extraído.

3. Ejecutar la instrucción
Llegado a este paso, el procesador ya sabe de qué instrucción se trata y los parámetros que necesita, luego simplemente la ejecuta de la forma apropiada.

Al finalizar este paso finaliza una iteración del ciclo de instrucción, volveremos al fetch de la siguiente instrucción y así sucesivamente.

IMPLEMENTACIÓN
Hay múltiples formas de implementar esto, una por cada programador, así que voy a comentar cómo lo he hecho yo, con una aproximación entre lo didáctico y lo eficiente, pero desde luego sin esperar que sea la mejor solución.

Como ya comenté, mi proyecto se basa en una implementación orientada a objetos, en este caso tengo un objeto de la clase Z80 y otro objeto de la clase Memoria relacionados entre sí de forma que desde el Z80 haya visibilidad hacia la Memoria, pero no al revés.

Por otro lado, tengo un programa principal que tiene instancias de estas dos clases y será el encargado de controlar la emulación (aunque de momento estoy usándolas desde el simulador del Z80 implementado para el testing).

Bien, desde este programa principal se pedirá al Z80 que ejecute x ciclos de reloj, llevando a cabo las instrucciones que den tiempo en esos ciclos (cada instrucción consume unos determinados ciclos).

Esto lo he implementado de forma similar a:


UINT32 Z80::ejecutaZ80(UINT32 ciclos)
{
z80.ciclosRestantes += ciclos;
do
{
ejecutaInst(mem->readMem(_PC++));
} while (z80.ciclosRestantes > 0);
return z80.ciclosEjecutados;
}

Como vemos, se le pasa la cantidad de ciclos a ejecutar y nos devolverá los que realmente se han ejecutado.

Las instrucciones se ejecutan íntegramante (no se puede ejecutar media instrucción o tres cuartos), así que es muy posible que se ejecuten más ciclos de los que se han pedido. Por ejemplo, si pedimos ejecutar 5 ciclos y en el programa tenemos una instrucción de 3 y otra de 6 ciclos realmente ejecutará 9. Por este motivo se guarda en ciclosRestantes los ciclos a ejecutar sumados a los que quedaban de la anterior ejecución (estos serán un número negativo) de modo que si en la anterior ejecución nos pasamos en 4 ciclos y en esta le pedimos ejecutar 7 pues realmente intente ejecutar solo la diferencia, es decir, 3 ciclos.

Como vemos, la fase de fetch se resuelve en

mem->readMem(_PC++)

, pues tomamos la instrucción de la dirección de memoria apuntada por el PC e incrementamos éste.

Pararemos de ejecutar instrucciones cuando ciclosRestantes sea igual o menor que 0 (como veremos esta variable se decrementa en cada ejecución de instrucción).

La rutina ejecutaInst es la que se encarga de decodificar y ejecutar la instrucción. Quizá el modo más eficiente de hacerlo fuera creando una función para cada código de operación y luego una tabla con punteros a estas funciones, pero mi implementación ha sido otra.

Al hacerlo en C++, si no me equivoco (soy bastante novato en C++), cada función de la clase debe estar declarada en la parte pública o privada de la misma. Meter ahí 1268 funciones (una por cada código de operación) me parece aberrante, por otro lado, aun agrupándolas en funcionalidades similares me saldrían más de 70, lo cual me sigue pareciendo excesivo. Debido a esto he tomado la determinación de hacer un switch gigante con todos los códigos de operación (esperando que el compilador sea lo suficientemente inteligente para codificarlo como una tabla y no como miles de ifs) e introducir en cada uno de estos el código de las instrucciones.

Escribir “a pelo” todo el código aparte de tedioso es muy poco elegante, así que ahí entran en juego las macros del preprocesador de C.

La función ejecutaInst quedaría algo así:


void Z80::ejecutaInst(UINT8 co)
{
z80.ciclosRestantes -= cc[co];
switch (co)
{
case 0x00: NOP; break;
case 0x01: LD16_R_I(_BC); break;
case 0x02: LD_D_R(_BC,_A); break;
.
.
.
.
}
}

Como vemos, resta una cantidad de ciclos específica de cada instrucción (contenido su valor en la tabla cc a la que se accede por el código de operación) y, dependiendo del código de operación, ejecutará una macro. Como vemos, en esta rutina se realizan las fases de decodificación y ejecución.

Aquí van un par de estas macros de ejemplo:


/************************************************
* LD (dir), r - 8 bits
***********************************************/
#define LD_D_R(dir,origen) \
{ \
WM(dir, origen); \
}

/************************************************
* LD dd, nn - 16 bits
***********************************************/
#define LD16_R_I(destino) \
{ \
destino = RM(_PC++) | (RM(_PC++)<<8); \
}

Cabe mencionar que en esta entrada no he tenido en cuenta el tema de las interrupciones (tampoco las tengo implementadas todavía), así que no os extrañéis si véis que no se chequean al ejecutar instrucciones.

Espero que haya quedado claro, sino ya sabéis, usad los comentarios que, salvo excepciones, parece que escribo solo para mí :P