Entradas etiquetadas con Z80 Core
Implementando interrupciones en el Z80
24 jul
Antes de nada, os sugiero leer esta entrada donde ya comenté el funcionamiento de los modos de interrupción, tipo de interrupciones y el hardware asociado a todo esto.
Bien, si no me equivoco, en la Master System el único dispositivo que provoca interrupciones es el VDP. En concreto, genera una interrupción en dos momentos diferentes: al acabar de pintar un fotograma y al acabar de pintar una línea. Ya escribiré una entrada al respecto más adelante, pero así os hacéis una idea.
Por otro lado, según el manual de referencia de la propia SEGA, la Master System solo atiende al modo de interrupción 1 (el más sencillito de ellos), así que a menos que los programadores se salten las recomendaciones en sus juegos y establezcan otro modo sería suficiente con implementar éste (aunque yo he implementado los 3 posibles).
Respecto a las NMI, su línea está directamente conectada con el botón de Pausa de la consola, así que ese es el único momento en que se genera.
Vale, dicho esto, el Z80 tendría que tener una variable (o una función que acceda al VDP) que le diga si está activa la línea de interrupciones. Antes de ejecutar cada instrucción, si se encuentra activa la línea y están habilitadas las interrupciones (biestable IFF1=1) el procesador tratará la interrupción del modo en que se encuentre.
Concentrándonos en el modo 1 de la SMS, el Z80 salvaguardará el PC en la pila (igual que una instrucción PUSH(PC)), inhibe las interrupciones (para evitar que se encadenen) poniendo IFF1=IFF2=0 y carga el PC con el valor 0×0038.
A partir de ahí ya se seguirá ejecutando como de costumbre en la dirección 0×0038.
En cuanto a las NMI, la cosa es más sencilla, pues siempre que esté activada su línea se ejecutará la rutina, es decir, se salvaguarda el PC como en las INT, se salva IFF1 en IFF2 y se pone IFF1 a 0 y se salta a la dirección 0×0066.
Una cosa que no hice bien en su momento y tuve que rehacer hace unos días fue el tema de la línea de interrupciones, pues es el propio dispositivo que interrumpe el que debe solicitar la línea y desactivarla cuando no la necesite.
Espero que haya quedado más claro.
Algunos bugs corregidos
16 jul
Llevo todo el día buscando y corrigiendo los bugs que hacían fallar los tests del ZEXALL, pero algunos se me siguen resistiendo…
El que me tiene ahora loco es el relativo a las instrucciones DAA, CPL, CCF y SCF, pues por más que miro el código no le veo fallos… He probado incluso a incluir el código de un par de emuladores para estas instrucciones, pero los resultados son los mismos, lo cual me está llevando a pensar que igual el fallo no sea de mi emulador y sí del test en sí o bien que se de por asumido algún error y luego no tenga efectos negativos a la hora de emular los juegos (he probado este mismo test en otro emulador y falla en un test que en el mio sí se verifica correctamente).
Bueno, visto lo visto, mañana creo que empezaré a darle caña al VDP (procesador gráfico), a ver si pongo por escrito la información que he ido leyendo al respecto y así os intento explicar un poco cómo funciona.
Os dejo el listado de pruebas a día de hoy:
SDSC Terminal de depuracion. ---------------------------- Z80 instruction exerciser ld hl,(nnnn).................OK ld sp,(nnnn).................OK ld (nnnn),hl.................OK ld (nnnn),sp.................OK ld <bc,de>,(nnnn)............OK ld <ix,iy>,(nnnn)............OK ld <ix,iy>,nnnn..............OK ld (<ix,iy>+1),nn............OK ld <ixh,ixl,iyh,iyl>,nn......OK ld a,(nnnn) / ld (nnnn),a....OK ldd<r> (1)...................OK ldd<r> (2)...................OK ldi<r> (1)...................OK ldi<r> (2)...................OK ld a,<(bc),(de)>.............OK ld (nnnn),<ix,iy>............OK ld <bc,de,hl,sp>,nnnn........OK ld <b,c,d,e,h,l,(hl),a>,nn...OK ld (nnnn),<bc,de>............OK ld (<bc,de>),a...............OK ld (<ix,iy>+1),a.............OK ld a,(<ix,iy>+1).............OK shf/rot (<ix,iy>+1)..........OK ld <h,l>,(<ix,iy>+1).........OK ld (<ix,iy>+1),<h,l>.........OK ld <b,c,d,e>,(<ix,iy>+1).....OK ld (<ix,iy>+1),<b,c,d,e>.....OK <inc,dec> c..................OK <inc,dec> de.................OK <inc,dec> hl.................OK <inc,dec> ix.................OK <inc,dec> iy.................OK <inc,dec> sp.................OK <set,res> n,(<ix,iy>+1)......OK bit n,(<ix,iy>+1)............OK <inc,dec> a..................OK <inc,dec> b..................OK <inc,dec> bc.................OK <inc,dec> d..................OK <inc,dec> e..................OK <inc,dec> h..................OK <inc,dec> l..................OK <inc,dec> (hl)...............OK <inc,dec> ixh................OK <inc,dec> ixl................OK <inc,dec> iyh................OK <inc,dec> iyl................OK ld <bcdehla>,<bcdehla>.......OK cpd<r>.......................OK cpi<r>.......................OK <inc,dec> (<ix,iy>+1)........OK <rlca,rrca,rla,rra>..........OK shf/rot <b,c,d,e,h,l,(hl),a>.OK ld <bcdexya>,<bcdexya>.......OK <rrd,rld>....................OK <set,res> n,<bcdehl(hl)a>....OK neg..........................OK add hl,<bc,de,hl,sp>.........OK add ix,<bc,de,ix,sp>.........OK add iy,<bc,de,iy,sp>.........OK aluop a,nn...................OK <adc,sbc> hl,<bc,de,hl,sp>...OK bit n,<b,c,d,e,h,l,(hl),a>...OK <daa,cpl,scf,ccf>............ CRC:681be242 expected:9b4ba675 aluop a,(<ix,iy>+1)..........OK aluop a,<ixh,ixl,iyh,iyl>....OK aluop a,<b,c,d,e,h,l,(hl),a>. CRC:b2fcb670 expected:5ddf949b
Primera demo funcionando
15 jul
Viene en dos versiones, una que va mostrando los gráficos por la pantalla de la consola y otra que saca los datos a través de unos puertos de entrada/salida determinados definidos en un estandar llamado SDSC Debug Console.
Como la implementación del VDP (el procesador gráfico de la Master System) me llevará un tiempo y estaba ansioso por probar esta demo me he puesto a implementar la consola de depuración antes mencionada.
Esta demo hace unas pruebas exhaustivas (algunas de las cuales tardan más de una hora en una Master System real), así que me viene de perlas para ver qué instrucciones tengo que revisar para corregir errores.
La primera pasada ha sido relativamente esperanzadora, la verdad es que pensé que no pasaría ninguna prueba.
Os dejo el resultado a día de hoy (todo lo que sale debajo de los guiones lo escribe el programa a través del puerto 0xFD):
SDSC Terminal de depuracion.----------------------------Z80 instruction exerciser ld hl,(nnnn).................OKld sp,(nnnn).................OKld (nnnn),hl.................OKld (nnnn),sp.................OKld <bc,de>,(nnnn)............OKld <ix,iy>,(nnnn)............OKld <ix,iy>,nnnn..............OKld (<ix,iy>+1),nn............OKld <ixh,ixl,iyh,iyl>,nn......OKld a,(nnnn) / ld (nnnn),a....OKldd<r> (1)...................OKldd<r> (2)...................OKldi<r> (1)...................OKldi<r> (2)...................OKld a,<(bc),(de)>.............OKld (nnnn),<ix,iy>............OKld <bc,de,hl,sp>,nnnn........OKld <b,c,d,e,h,l,(hl),a>,nn...OKld (nnnn),<bc,de>............OKld (<bc,de>),a...............OKld (<ix,iy>+1),a.............OKld a,(<ix,iy>+1).............OKshf/rot (<ix,iy>+1)..........OKld <h,l>,(<ix,iy>+1).........OKld (<ix,iy>+1),<h,l>.........OKld <b,c,d,e>,(<ix,iy>+1).....OKld (<ix,iy>+1),<b,c,d,e>.....OK<inc,dec> c.................. CRC:e70a003c expected:c284554c<inc,dec> de.................OK<inc,dec> hl.................OK<inc,dec> ix.................OK<inc,dec> iy.................OK<inc,dec> sp.................OK<set,res> n,(<ix,iy>+1)......OKbit n,(<ix,iy>+1)............ CRC:7e74acf0 expected:55c9ea76<inc,dec> a.................. CRC:336713ec expected:d18815a4<inc,dec> b.................. CRC:020c0249 expected:5f682264<inc,dec> bc.................OK<inc,dec> d.................. CRC:ec6470d5 expected:4523de10<inc,dec> e.................. CRC:568caa00 expected:e175afcc<inc,dec> h.................. CRC:60f7bc16 expected:1ced847d<inc,dec> l.................. CRC:157d25f2 expected:56cd06f3<inc,dec> (hl)............... CRC:e13645be expected:46761d6b<inc,dec> ixh................OK<inc,dec> ixl................OK<inc,dec> iyh................OK<inc,dec> iyl................OKld <bcdehla>,<bcdehla>.......OKcpd<r>.......................OKcpi<r>.......................OK<inc,dec> (<ix,iy>+1)........ CRC:d38fd00d expected:8897c715<rlca,rrca,rla,rra>..........OKshf/rot <b,c,d,e,h,l,(hl),a>.OKld <bcdexya>,<bcdexya>.......OK<rrd,rld>....................OK<set,res> n,<bcdehl(hl)a>....OKneg..........................OKadd hl,<bc,de,hl,sp>.........OKadd ix,<bc,de,ix,sp>.........OKadd iy,<bc,de,iy,sp>.........OKaluop a,nn................... CRC:35dddc1a expected:48799360<adc,sbc> hl,<bc,de,hl,sp>... CRC:c7790b9c expected:f39089a0bit n,<b,c,d,e,h,l,(hl),a>... CRC:71aea11b expected:4b37451d<daa,cpl,scf,ccf>............ CRC:c29d3dbb expected:9b4ba675aluop a,(<ix,iy>+1).......... CRC:14534fa8 expected:2bc2d52daluop a,<ixh,ixl,iyh,iyl>.... CRC:91ed0c17 expected:a4026d5aaluop a,<b,c,d,e,h,l,(hl),a>. CRC:42f631aa expected:5ddf949b
Z80 Terminado
15 jul
Ahora estoy haciendo algunas pruebas más exigentes para ir puliendo errores que se me hayan pasado en el proceso.
Por otro lado, he ido recopilando varias demos y programas de prueba homebrew para la Master System para ir testeando el emulador cuando tenga implementado el procesador de video.
Avances y ejemplos de instrucciones
11 jul
Estas instrucciones de control de la CPU las necesitaba para implementar el manejo de las interrupciones, así que he aprovechado para dejarlo también listo y probado.
Por otro lado, he corregido la plantilla de Blogger para mostrar el código fuente en condiciones, pues como vísteis se descuadraba todo (no programo tan chapuzas como salía sin identar).
Bueno, lo prometido es deuda, aquí van algunas macros de ejemplo de las que tengo implementadas:
/************************************************ * LD (dir), n - 8 bits ***********************************************/#define LD_D_I(dir) \{ \ WM(dir, RM(_PC++)); \}
/************************************************ * DJNZ e ***********************************************/#define DJNZ(e) \{ \ _PC += (--_B) ? ((INT8) e)+1 : 1; \}
/************************************************ * CALL nn ***********************************************/#define CALL_NN \{ \ UINT16 dir = RM(_PC++) | (RM(_PC++)<<8); \ WM(_SP-1,_PCh); \ WM(_SP-2,_PCl); \ _SP -= 2; \ _PC = dir; \}
Bueno, por hoy es suficiente, mañana pongo algunos ejemplos comentados más.
Rotación y desplazamiento listas
10 jul
Al final han sido otros 198 códigos de operación con lo que la suma se sitúa en 1026 de los 1268 (he añadido otros 6 de instrucciones de llamada no documentados que he encontrado mirando el lista de códigos que me faltan). Mañana empezaré con las instrucciones de entrada/salida que parece que modifican menos flags (¿se nota que hoy he acabado quemado con los flags?).
Había prometido poner unos ejemplos de código, pero casi mejor lo dejo para mañana que ahora estoy hecho polvo.
Un día productivo
9 jul
Esta mañana me he puesto con las instrucciones de llamada y retorno que eran poquitas y sencillitas (solo 28 opcodes), así que he aprovechado para emplear la tarde con las instrucciones de establecimiento, borrado y testeo de bits.
Estas últimas instrucciones son extremadamente sencillitas y muy parecidas entre sí, el problema es que hay montooones de ellas. En total he implementado otros 576 códigos de operación con estas instrucciones, pero debido a su número no creo que vaya a probarlos todos como he hecho uno a uno con los anteriores…
Bueno, el estado actual es de 822 de 1268 códigos de operación implementados, ya queda menos
Mañana a ver si empiezo con las instrucciones de rotación y desplazamiento y voy poniendo algunos ejemplos de código de cada grupo de instrucciones implementadas para que os hagáis una idea de cómo funcionan.
El ciclo de instrucción
8 jul
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í
Carga de 16 bits implementadas
8 jul
Por otro lado, se me olvidó mencionar el domingo (ahora ando mal de internet con el vecino, tendré que conectar desde el móvil) que implementé las instrucciones de salto.
Bueno, ahora a leer un poco sobre las interrupciones y entrada salida en el Z80 y mañana empezaré con las instrucciones de llamada y retorno de subrutina.
Implementadas instrucciones de carga de 8 bits
7 jul
En total han sido más de 200 códigos de operación los que he tenido que implementar y testear con el simulador creado para tal fin (aunque parezca que no, siempre se pasan “fallitos tontos” que si no se encuentran ahora te pueden volver loco a la hora de probarlo directamente con una ROM).
Por otro lado, también he aprovechado para implementar algunas funcionalidades al simulador del Z80 (como la actualización de la tabla mostrando la memoria cada vez que se efectúe alguna escritura en la misma) que había dejado sin hacer para cuando me hicieran falta.
Había dicho que explicaría el ciclo de instrucción y su implementación, pero casi que lo dejo para mañana (y de paso algo de código de las instrucciones de carga), pues ahora estoy bastante cansado y creo que voy a aprovechar para ver alguna película…


