- Examino la documentación del Z80 para comprender qué debe hacer el opcode.
- Escribo la prueba unitaria correspondiente.
- Compruebo que la prueba falla.
- Implemento el opcode.
- 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:
- Instanciar una CPU.
- Establecer el estado de entrada E.
- Cargar en la memoria de la CPU el código máquina a probar y ejecutarlo.
- 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.
No hay comentarios:
Publicar un comentario