06 junio, 2015

Sobre las pruebas unitarias del Z80

El emulador Z80 tiene pruebas unitarias desde el primer momento. La intención de las pruebas unitarias de mi emulador es verificar que la implementación de los opcodes es correcta y libre de bugs. Para ello el emulador se está desarrollando mediante metodología TDD. Cuando quiero implementar un opcode sigo el siguiente proceso:

  1. Examino la documentación del Z80 para comprender qué debe hacer el opcode.
  2. Escribo la prueba unitaria correspondiente.
  3. Compruebo que la prueba falla.
  4. Implemento el opcode.
  5. Compruebo que la prueba se pasa.
Sin embargo, me dejo en el tintero lo más importante: ¿cómo haces pruebas unitarias de un emulador? ¿Pruebas la función de cada opcode? Claramente no, en tanto que los opcodes los estoy implementando como funciones static.

El secreto se encuentra en considerar a la CPU como una máquina que cambia estados. Una CPU con un estado de entrada E ejecuta un opcode y genera un estado de salida S. Por ejemplo, si ejecutamos el opcode INC B, con un estado de entrada en el que B = 0x04, el estado de salida deberá cumplir que B = 0x05. Similarmente, otros opcodes manipulan registros, memoria y flags, pero siempre de una forma predecible.

Ese es precisamente el fundamento que siguen estas pruebas unitarias. Como los opcodes documentados son predecibles, todo lo que hace una prueba unitaria del Z80 es:
  1. Instanciar una CPU.
  2. Establecer el estado de entrada E.
  3. Cargar en la memoria de la CPU el código máquina a probar y ejecutarlo.
  4. Verificar el estado de salida S.
El estado de salida S se verifica del siguiente modo:
  • Un registro que no sea modificado por el opcode, deberá tener el mismo valor en E y en S.
  • Un registro que sea modificado por el opcode, deberá tener un valor predecible en S siempre que conozcamos E. Es el ejemplo que he puesto antes: si ejecuto el opcode INC B y en mi estado de entrada E el registro B vale 0x04, en S ese registro deberá valer 0x05, porque ha tenido que ser incrementado.
Pongo un ejemplo sacado del código fuente. Cada sección la he coloreado de un color.

START_TEST(inc_hl_test)
{    
    // Instancio la CPU.
    struct cpu_t* cpu = setup_cpu();

    // Cargo el código máquina del opcode a probar.
    cpu->mem[0] = 0x23;
    
    // Establezco el estado inicial.
    cpu->tstates = 1000;
    REG_HL(*cpu) = 0x1234;

    // Ejecuto el código máquina.
    execute_opcode(cpu);
    
    // Verifico el estado final.
    ck_assert(REG_HL(*cpu) == 0x1235);
    ck_assert(cpu->tstates == 1006);
}
END_TEST

Las partes más importantes son la verde y la fucsia.
  • En verde establezco el estado de entrada E. Los valores son arbitrarios.
  • En morado verifico que el estado de salida S se ha modificado de acuerdo a como debe ser. Por ejemplo, si estoy probando el opcode INC HL, debe verificarse que si HL_E = 0x1234, entonces HL_S = 0x1235. En cualquier otro caso, el opcode no se ha ejecutado correctamente. Además, lo mismo con los T-states. Si inicialmente el contador era 1000 y este opcode consume 6 T-states, necesariamente al final el contador debe valer 1000 + 6 = 1006. Cualquier otro valor es un error.

Ya sé que no está completa la prueba, porque no verifico que los opcodes que no deban cambiar no cambien. Esto es algo que me ha quedado por verificar.

11 marzo, 2015

El parpadeo

Ayer, ejecutando mi emulador, además de encontrar unos cuantos bugs tontos que debería haber visto antes de hacer el lanzamiento de la 0.1.0 en vez de lanzar a ciegas (los cuales por cierto ya están corregidos y en la rama de la 0.1, la 0.1.1 probablemente saldrá en menos de 24 horas), observé que la pantalla parpadeaba muy a menudo, especialmente en el PONG.

Al principio pensé que sería un bug, y de hecho creé un issue marcador en GitHub. Sin embargo, al rato lo tuve que cancelar ya que un compañero que también había probado el emulador y también había visto el parpadeo me recordó el motivo por el que parpadea, el cual es lógico.

Simplemente: del mismo modo que la ROM enciende los píxeles también los debe apagar. Por ejemplo, presumiblemente el PONG (no he mirado el código máquina) está usando una instrucción de dibujado para apagar los píxeles y acto seguido una instrucción de dibujado para encender los píxeles, en vez de usar una instrucción que apague y encienda de golpe los píxeles necesarios para que parezca que se está moviendo.

Probablemente eventualmente encuentre una mejor forma de renderizar la pantalla que provoque menos parpadeos, pero por el momento he marcado el bug como sin solución ya que técnicamente no es un problema mío.

09 marzo, 2015

CHIP-8 en Windows

En la sesión de live coding de ayer quedé un poco en ridículo al ver que nada de lo que tenía planeado funcionaba. La pantalla se veía en negro, y no entendía realmente por qué. Durante el directo eché la culpa a un montón de factores: era el Windows, luego era el VirtualBox, luego era el OpenGL, luego era el Windows otra vez.

Esto está compilado hace 5 minutos:




¿Cuál era el problema? Ni SDL, ni OpenGL, ni siquiera VirtualBox.

La respuesta: la ROM. En Windows no se estaba cargando. No ha sido fácil encontrar este bug y ha requerido una intensa sesión de depuración hasta ver que la memoria de la máquina estaba completamente vacía. Finalmente, después de buscar en Google, he encontrado una explicación coherente a por qué en Windows no se está cargando la ROM: fread/ftell apparently broken under Windows, works fine under Linux. La respuesta de esta pregunta de Stack Overflow dice que debería abrir el archivo en modo "rb", no en modo "r".

Según el manual de fopen(3), en sistemas compatibles con POSIX, el "b" es ignorado ya que el fopen funciona tal cual. Por eso en Linux y en Mac la llamada a fopen funciona correctamente. Sin embargo, en otros sistemas operativos (clara indirecta a Windows), esto puede ser distinto y puede que sí que sea necesario. Y, efectivamente, parece que sí que lo era.

Así que... nada. Voy a compilar una versión inestable y la voy a subir al repositorio. Y, supongo que en el próximo directo intentaré repetir lo que quería hacer ayer. Aún no he comprobado si VirtualBox será capaz de mostrarlo en cualquier caso. EDIT: He sido capaz de usarlo en Linux sobre VirtualBox, así que asumo que en Windows sobre VirtualBox funcionará igual.

06 marzo, 2015

Blitting

En la próxima sesión de live coding voy a ocuparme de construir la pantalla del emulador. Para ello va a ser necesario manipular pixels en SDL. En SDL2, la forma preferente de gestionar una ventana es con aceleración de hardware. Esto implica que las texturas se ubiquen en GPU y sea la GPU quien dibuje, no la CPU. Esto es un bache a la hora de manipular los píxeles, que es exactamente lo que necesita nuestro emulador, ya que a las texturas no les gusta que las manipulen directamente como podríamos hacer con una Surface en SDL1.

En SDL 2 tenemos texturas de tipo streaming. Estas texturas están pensadas para ser usadas en situaciones donde pueda cambiar la textura frecuentemente, incluso cada fotograma. Por medio de la función SDL_LockTexture luego puedo bloquear la textura. Todo lo que hago es pedirle a SDL_LockTexture que me pase la textura a una SDL_Surface y luego manipulo los pixels de esa textura. Cuando he terminado, invoco a SDL_UnlockTexture para desbloquearla. Este proceso se repite 60 veces por segundo. El resultado es este:


Estoy limitando la ejecución a 60 segundos por medio de SDL_WaitEventTimeout. Podría usar SDL_PollEvent pero he comprobado que así el consumo de CPU se dispara al 100% ya que PollEvent no es bloqueante, como sí es SDL_WaitEventTimeout, lo que reduce mucho el consumo de CPU, algo muy importante en ordenadores portátiles y móviles.

El código, un poco sucio, se puede ver aquí, https://gist.github.com/danirod/4008bf7aab2f8b2466d5.

02 marzo, 2015

Sobre máscaras de bit y operadores binarios

Me parecía oportuno hacer un inciso técnico sobre algo que llevo varios días mostrando en los streamings de live coding que estoy haciendo estos días: las máscaras de bit y los desplazamientos.

Una de las construcciones que más abundan el código fuente de mi emulador del CHIP-8, el cual estoy alojando en GitHub bajo una licencia GPL, es el aplicar desplazamiento y una máscara de bits sobre un resultado, por ejemplo:

mem.pc = (mem.pc + 2) & 0xFFF;
uint8_t n = opcode & 0xF;
uint8_t x = (opcode >> 8) & 0xF;

Asumiré que quien esté leyendo esto conoce más o menos cómo trabajar con bits. En cualquier caso como referencia quedan los dos episodios (el primero y el segundo) que dediqué a bits en mi videotutorial de C que grabé hace un par de años.

En el caso de las dos primeras operaciones las máscaras de bit las uso para limitar el número de bits que me quedo de la variable. Por ejemplo, en el primer caso, mem.pc es una variable de 16 bits. Pero puesto que la CHIP-8 sólo puede direccionar a 12 bits, debo asegurarme que no se guarda un valor de más de 12 bits. Puesto que 0xFFF son 12 unos, sólo los 12 bits menos significativos del contador de programa se guardarán. Los cuatro bits más significativos se perderán porque al hacer el AND, en la máscara están en 0.

Por ejemplo, supongamos que incremento un contador de programa que inicialmente vale 0x0FFE. Al sumarle 2, mi contador de programa pasa a valer 0x1000. Al hacer 0x1000 & 0x0FFF, puesto que & es un operador que hace el AND bit a bit, el resultado que obtengo es 0x000. ¿Otro ejemplo? Veamos: (0x0FFF + 2) & 0x0FFF = 0x1001 & 0x0FFF = 0x0001.

En el segundo ejemplo estoy haciendo lo mismo. La única diferencia es que la máscara de bit es 0xF, por lo que me quedo con los últimos 4 bits menos significativos del opcode. Por lo que supongamos que el opcode que he leído es 0xABCD. Al hacerle el AND, 0xABCD & 0x000F = 0x000D.

El tercer caso utiliza además desplazamientos. Lo hago porque en ese caso quiero quedarme con un valor que no está en los bits menos significativos. Lo que hago es desplazarlo a la derecha, para que sí esté en los bits menos significativos, y ya entonces le aplico la máscara. Por ejemplo, supongamos que el opcode es 0xABCD. Pues (0xABCD >> 8) & 0x000F = 0x00AB & 0x000F = 0x000B.

Una forma alternativa de hacer esta última operación aritmética hubiese sido al revés: en vez de desplazar y luego aplicar AND, primero aplicar el AND y luego desplazar a la derecha. En ese caso lo que tendría que hacer es aplicarle una máscara binaria que sólo deje activos los bits que me interesa. Por ejemplo, la tercera operación es reescribible como:

uint8_t x = (opcode & 0xF00) >> 8

En este caso la máscara de bit dejará activos los bits 8 a 11 del opcode. Luego al desplazar a la derecha esos valores pasarán a estar en las posiciones menos significativas. Por ejemplo, si el opcode es 0xABCD, lo que se hará es (0xABCD & 0x0F00) >> 8 = 0x0B00 >> 8 = 0x000B.

Y esta es más o menos la esencia que hay detrás de mis operadores de bit. ¿Por qué hacerlo así y no de otro modo? Por ejemplo, en vez de hacer esa máscara de bit al contador de programa podría haber hecho simplemente:

mem.pc = mem.pc + 2;
if (mem.pc > 0xFFF) {

  mem.pc = mem.pc - 0xFFF;
}

Es claramente más simple y más fácil de comprender por alguien que no tenga mucha experiencia con máscaras de bit. Pero por otro lado es mucho más lento. Las operaciones de bit las puede hacer el procesador en una única instrucción. Las sumas, las restas y los condicionales suelen necesitar más instrucciones máquina en nuestro ordenador. Para un emulador muy simple como el de un CHIP-8 igual no hace falta ser tan exquisito con las optimizaciones porque la máquina emulada es lenta y nuestro ordenador físico tiene varios núcleos, pero cuando emulamos arquitecturas más grandes y complejas, puede significar bastante.

27 febrero, 2015

He vuelto (¡chan!)

Cosas interesantes han pasado esta semana. Después de abandonar el proyecto el año pasado de forma silenciosa, me he decidido a retomarlo. Hoy, de forma improvisada. Sin avisar a nadie.

Para ello, esta vez no sólo desarrollaré un emulador, sino que mostraré cómo lo desarrollo. Y es que mi intención es programarlo en directo. Me voy a unir al concepto del live-coding que últimamente está tan de moda de enseñar en una emisión en directo (o en diferido) lo que se está programando. Es una experiencia curiosa cuanto menos.

Me he asociado con uno de mis canales de YouTube, Makigas, donde haré los directos. Realmente es un proyecto personal, pero he optado por subirlos a este canal en vez de a mi canal real porque tiene mucha más audiencia a la que le podría llegar información del proyecto.

La carga de los vídeos se va a centrar en temas de código. Obviamente en este primer vídeo he hablado de cómo funciona la Arquitectura de Von Neumann y cómo funciona un ordenador, qué es un registro y demás. Pero la idea es no hacer vídeos muy técnicos, dejando todas las explicaciones teóricas para este blog, porque así las puedo hacer despacio, sin las prisas del directo. Por lo tanto, para el siguiente vídeo, que va a ser de programación más pura, mostraré cosas más cercanas a C.

En estos momentos voy a trabajar en construir un pequeño emulador para el procesador CHIP-8, para coger fuerzas y también para practicar en esto del live-coding. Cuando esté desarrollado, comenzaré a trabajar en la implementación del Z80 y luego en extenderlo para que el emulador haga más cosas: bancos de memoria, para poder trabajar con más de 64 kB de memoria RAM, dispositivos de entrada salida, adaptador de vídeo, etc.


De momento aquí dejo el primer episodio, e idealmente mañana o pasado mañana haré la segunda emisión en directo, avisando previamente a través de mi Twitter o mi Google+.