11 noviembre, 2013

Emulación de una arquitectura interna de registros

Dentro del procesador, junto a la unidad de control, que es quien se ocupa de ejecutar cada una de las instrucciones, hay una pequeña cantidad de memoria de almacenamiento volátil usado por el procesador: se trata de los registros.

En el sentido más puro, un registro no es más que un conjunto de flip-flops. La diferencia entre un flip-flop y un registro es que mientras que el primero está diseñado para guardar un bit, un registro está diseñado para guardar varios bits. Sin embargo, el procesador por lo general trabajará de forma conjunta con todos los bits del registro mediante el mismo identificador.

Los procesadores tienen varios tipos de registros. Algunos registros son específicos para algunas tareas y son usados por el propio procesador, como el registro de instrucción, donde se guarda el código de operación que el procesador esté ejecutando en un momento; el contador de programa, que guarda la dirección de memoria de la instrucción que se está ejecutando, o una batería de registros que pueden ser usados por el programa por lo que se los denomina registros de propósito general.

Orientado a la línea de este blog, el procesador Z80 dispone de varios registros, siendo estos los más principales:
  • Dos bancos de registros de propósito general. Mientras que la mayoría de procesadores tienen un único banco, el Z80 tiene dos. El programa sólo tiene acceso al banco principal, pero por medio de algunas instrucciones, puede intercambiar los bancos para acceder al otro. El registro más importante es el A, que es el acumulador, y el F, que guarda los flags. Otros registros son el B, C, D, E, H y L. Los registros secundarios son A', F', B', C', D', E', H' y L'.
  • Unos registros de índice (IX, IY)
  • Un contador de programa (PC) y un puntero de pila (SP).
  • Un registro de interrupción (I) y un registro de refresco (R)
  • El registro de instrucción.
Cuando se construye un emulador, estos registros se implementan en memoria usando unas variables, ya que por lo general no tenemos acceso a los registros del procesador físico de la máquina anfitrión. Esto puede ser un problema al emular máquinas más potentes ya que acceder a la memoria es más costoso que acceder a un registro por lo que nuestro emulador se sobrecargará más. En mi caso, que estoy emulando un Z80, ni me va ni me viene, ya que se trata de una máquina que tiene apenas unos pocos megahercios.

Otra de las cuestiones es cómo implementar tantas variables en el programa. Es importante tener en mente que van a ser tocadas por todo el emulador, por lo que deben ser bien accesibles.
  • Una forma es juntando todas las variables en una o varias estructuras. Mínimo una estructura con todas las variables de registro, aunque puede haber una estructura con los registros de propósito general dentro de otra estructura mayor con más variables. El acceso a estas variables es por medio de registro base: la CPU anfitrión calculará la posición de la variable del registro a partir de la dirección de memoria de la estructura y un incremento en el registro. Esto es algo más costoso que acceder directamente a la posición de memoria, que podría hacerse en un paso.
  • Otra forma es manteniendo cada variable separada. El mantenimiento en este caso es más costoso. Aquí hay que tener en cuenta el ámbito, especialmente si vamos a usar funciones en el emulador, ya que hay que permitir a las funciones trabajar con unos registros u otros. Otra forma sería usar variables globales; en este caso el mantenimiento no se vuelve tan asqueroso como puede parecer puesto que en el fondo son variables con un objetivo claro y que son usadas por todo el programa así que podría merecer hasta la pena saltarse los principios de la programación por un rato.
Sobre las variables, otra pregunta es con qué tipo de datos se debe trabajar. Puesto que de momento el emulador lo estoy haciendo en Blitz3D, sólo tengo acceso a un único tipo de datos, Integer, que de acuerdo con la documentación tiene 32 bits. Sin embargo, al trabajar en C, hay que tener en cuenta que hay múltiples tipos de datos según su tamaño. Hay dos opciones, cada una tiene sus ventajas y sus desventajas:
  • Usar tipos int para todo.
    Pro: usas el tamaño de palabra nativo de la CPU anfitrión. Esto suele tener un mejor rendimiento por la forma en la que funcionan los procesadores.
    Contra: debes tener en cuenta que si los registros que emulas tienen menos bits que tu tipo de datos deberás hacer ajustes (usar máscaras AND o cualquier otra técnica) para que la variable no tenga más bits de los que se supone que debe tener.
  • Usar los tipos del mismo tamaño que los registros de la plataforma, preferentemente unsigned para que no tengas problemas con el signo ya que al fin y al cabo, del signo de los "registros" se encarga la CPU que estés emulando.
    Pro: no es necesario tragar con el problema de variables más grandes que los registros que se esté emulando.
    Contra: justo el inverso del caso anterior: es posible que algunas CPUs tengan menos rendimiento al trabajar con tipos de datos de menor tamaño que la arquitectura del procesador.

10 noviembre, 2013

El esquema básico de funcionamiento de un emulador

La función principal del emulador será simular la arquitectura hardware del ordenador que está emulando. Vamos a centrarnos de momento en el procesador. El funcionamiento básico del procesador es procesar una instrucción de código máquina, luego otra, luego otra... así que cabe pensar que la parte principal del emulador no va a ser otra cosa que un bucle.

Durante ese procesamiento de una instrucción de código máquina ocurren varias cosas:
  1. El procesador obtiene la siguiente instrucción de la memoria.
  2. Se incrementa el registro CP.
  3. En función de la instrucción se hace una cosa u otra.
  4. (Normalmente) se comprueba si hay que hacer otras cosas: comprobar si se ha disparado una interrupción, si se ha pulsado una tecla, si se ha movido el ratón o simplemente refrescar la pantalla.
Se muestran los pasos anteriores en forma de diagrama donde cada paso es un círculo que conecta con otro.

Es posible implementar el paso 3 usando un SWITCH gigante que dependa del opcode que se ha leído en el paso 1 y que en función del valor del opcode haga una cosa u otra. Por ejemplo, supongamos que tenemos un conjunto de instrucciones bastante crudo basado en tres opcodes: 0x00 = LD A, [inmediato], 0x01 = ADD A, inmediato, 0x02 STO [inmediato]. Los corchetes indican que lo que se lee no es el inmediato, sino el valor que haya en la posición de memoria indicada por el inmediato. Un esquema básico y no del todo correcto (ya contaré) del bucle principal sería algo como lo siguiente:

for(;;) {
    opcode = mem[pc++] // pasos 1 y 2
    switch(opcode) {
        case 0x00:
            a = mem[ mem[pc++] ];
            break;
        case 0x01:
            a += mem[pc++];
            break;
        case 0x02:
            mem[ mem[pc++] ] = a;
            break;
        default:
            printf("Opcode no reconocido.");
            // tratar error o detener emulador
            break;
    }
    
    // añadir paso 4 aquí
}

He dicho que no es del todo correcto por que no estamos teniendo en cuenta la suma de los ciclos de reloj o los T-States, algo que como mínimo es importante para poder ejecutar correctamente el paso 4. Sobre el funcionamiento del paso 4 me tengo que explicar bastante por lo que prefiero dejarlo para un futuro post separado.

A modo de cierre de este post, decir que en procesadores más complejos, el SWITCH puede complicarse bastante. Puede haber cientos de instrucciones distintas y esto puede hacer que haya dos formas distintas de actuar. Una es tirarse directamente a hacer un CASE para cada valor que pueda haber, y otra es pararse a estudiar un poco el conjunto de instrucciones con el que se está trabajando.

La segunda parte puede ofrecer mayores ventajas, especialmente en términos de complejidad del código. La arquitectura del Z80 tiene muchas instrucciones que hacen la misma operación pero con distintos operandos. Por ejemplo, hay una instrucción LD A, B, otra LD A, C, otra LD A, D... y así sucesivamente. En este caso al estudiar el conjunto de instrucciones podría verse que todas estas instrucciones tienen el mismo opcode, sólo que cambian dos o tres bits que son los que indican cuál es el segundo operando, mientras que el resto de bits siguen sin cambiar, y eso podría hacer que se simplificara el procesamiento al no repetir casos, comprobando únicamente si la instrucción es de tipo LD A, x viendo si tiene los bits comunes puestos en 1 y luego determinando cuál es el segundo operando viendo esos bits que sí cambian.

En este sentido, estas tablas que hay en la web Z80Info son bastante útiles ya que hacen esto mismo para todas las instrucciones del procesador Z80. El código se hace más complejo porque tienes varios niveles de profundidad en el código (se hace un SWITCH para unos bits, dentro otro switch para otros bits y dentro otro SWITCH para otros bits), pero se gana en que no hay que implementar opcode a opcode el conjunto de instrucciones, sino que se agrupan aquellas instrucciones comunes que cambian sólo en los datos de entrada.

09 noviembre, 2013

Qué pretendo hacer

Mi objetivo es programar, a modo de hobby y puramente académico, un emulador para PC que simule la arquitectura de un procesador, y si la cosa avanza, un ordenador completo que disponga también de memoria RAM y de dispositivos de entrada-salida, como una salida por pantalla, una entrada por teclado y la posibilidad de interactuar con discos duros virtuales.

El procesador que pretendo emular es un Z80 de Zilog. Estos procesadores de 8 bits comenzaron a fabricarse antes de los años 80, pero su versatilidad ha permitido que sigan existiendo en el mercado. La gran mayoría de ordenadores personales de finales de los 70 y de buena parte de los 80 e incluso de principios de los años 90 han usado un procesador Z80 o uno derivado de éste. Su buen precio, su facilidad de uso y la cantidad de información que hay acerca de él hace que siga teniendo buen uso.

Sobre la plataforma de desarrollo, aunque tengo la intención de portar el código a C para poder aplicar más optimizaciones, inicialmente voy a trabajar con un lenguaje de más alto nivel. He rescatado del olvido mi vieja copia de Blitz3D, el primer lenguaje de programación que aprendí (oh, 2007, se te ve tan lejano), y la he instalado sobre mi equipo con Windows 7 de 32 bits. Aparentemente funciona. Digo aparentemente porque Blitz3D fue diseñado para Windows 95/98 y usa DirectX 7, por lo que está obsoleto y se sabe que da problemas en Windows 8 y posiblemente en algunos ordenadores de 64 bits. Al no usar ni Windows 8 ni un ordenador de 64 bits puedo confirmar nada de esto. Blitz3D usa uno de esos lenguajes de programación derivados de BASIC pero llevados al mundo de la programación estructurada. Está especializado para desarrollar juegos y otras aplicaciones multimedia, por lo que cuando quiera enviar información a la pantalla, todas las funciones de procesamiento gráfico (para dibujar píxeles), ya las tendré a mano.

En este blog iré compartiendo técnicas, información y otras notas que considere interesantes así como un progreso de lo que voy desarrollando.