Lo que aprendí sobre la máquina arcade Bomb Jack en el proceso de creación de su emulador
Recientemente escribí un pequeño emulador para una máquina Bomb Jack, principalmente para descubrir cómo estas primeras máquinas recreativas de 8 bits diferían en diseño de las computadoras domésticas de 8 bits.
Como supe mucho más tarde, una reunión en una feria de verano en mi ciudad natal con máquinas recreativas como Bomb Jack fue uno de esos momentos que cambió mi destino. En un día normal de verano, después de haber gastado todo mi suministro de monedas en máquinas recreativas, regresé a casa y mi cabeza estaba llena de flores y efectos de sonido. Traté de entender cómo funcionaban estos juegos. Y luego, hasta el final del año, pasé todo el tiempo después de la escuela creando copias bastante desvaídas de estos juegos de arcade en la computadora de mi casa. Era como un fanático del culto a la carga de las islas del Océano Pacífico, que quería crear una estación de radio militar estadounidense a partir de palos.
Al principio pensé en la idea de crear un emulador
Pengo , porque mi cerebro adolescente estaba mucho más impresionado con este juego que Bomb Jack (por cierto, aquí está mi
versión de culto de Pengo ). Pero el equipo arcade de Pengo requeriría la creación de nuevos emuladores de chips para audio y video, y para Bomb Jack había suficientes partes que ya tenía (Z80 como CPU y AY-3-8910 para sonido), así que fui el primero en enfrentarme a Bomb Jack.
Además, Bomb Jack fue una gran oportunidad para finalmente agregar soporte NMI (interrupción no enmascarable) a mi emulador Z80. Ninguna de las máquinas basadas en Z80 que emulé anteriormente usaba NMI y, por lo tanto, no tenía mucho sentido recrear esta función. Todavía no podía verificar su funcionamiento.
Si no sabes qué es Bomb Jack, entonces este juego se veía así (no estoy seguro si elegí la relación de aspecto correcta):
La versión del emulador en WebAssembly se puede encontrar aquí:
https://floooh.imtqy.com/tiny8bit/bombjack.htmlDespués de que se complete el procedimiento de carga y aparezca la tabla de puntaje alto, presione
1 para soltar una moneda y luego
Enter (o cualquier otra tecla excepto las flechas y la barra espaciadora) para comenzar el juego.
Dentro del juego, usa las
teclas de flecha para cambiar de dirección y la
barra espaciadora para saltar. Mientras está en el aire, presione la
barra espaciadora para frenar la caída.
El código fuente está aquí:
https://github.com/floooh/chips-test/blob/master/examples/sokol/bombjack.cUtiliza
encabezados de chip para proporcionar la emulación de Z80 y AY-3-8910, así como
encabezados de sokol como envoltorio multiplataforma (para ingresar a la aplicación, renderizado, entrada y sonido).
Paso 1: investigación
"Investigación" es una palabra demasiado grande: acabo de cumplir con las "especificaciones de hardware de la sala de juegos Bombjack" de Google.
En comparación con las populares computadoras hogareñas de los 80 (o incluso las misteriosas computadoras de Europa del Este, que a menudo todavía tienen comunidades activas), hay muy poca información sobre Bomb Jack en Internet.
Encontré dos datos muy importantes: el
diagrama de circuito de la máquina y, por supuesto, el
código fuente del emulador MAME .
También hay un proyecto que implementa
Bomb Jack en FGPA , a partir de las fuentes VHDL de las cuales logré encontrar detalles que no están en el diagrama del circuito.
Comprender el código fuente MAME sería complicado, porque los emuladores de máquinas recreativas generalmente son solo un montón de macros que describen cómo interactúan varios equipos, pero no
hay mucho
código fuente .
Sin embargo, las descripciones macro del equipo, y especialmente los comentarios, aún resultaron ser muy útiles para comprender el funcionamiento del hardware, y cuando se volvieron demasiado crípticos (por ejemplo, la
parte de decodificación de video ), la prueba y el error fueron suficientes, así como Estudio detallado del concepto.
Descripción del hardware
Lo más interesante del hardware de Bomb Jack es que en realidad son
dos computadoras conectadas entre sí por una cinta eléctrica: hay una
placa principal con CPU Z80 y equipo de decodificación de video y una
tarjeta de sonido separada con su propia CPU Z80 y tres (sí, tres!) chips de sonido AY-3-8910.
El equipo de decodificación de video no se implementa como un circuito integrado: son solo muchos pequeños chips de propósito general (su circuito toma 6 de 10 páginas del diagrama del circuito del dispositivo). Al crear un emulador, decidí tomar un camino corto: en lugar de emular ciertas partes del equipo de decodificación de video, emulé solo su comportamiento, creando la salida correspondiente a partir de los datos de entrada y sin preocuparme realmente por cómo funciona el equipo en el medio.
Tal solución simplificada es bastante adecuada para una máquina arcade separada, que está diseñada para ejecutar solo un programa. Si el juego comienza y funciona correctamente, entonces la emulación puede considerarse "suficientemente buena".
Además, este enfoque simplificado es una diferencia importante de la emulación de la mayoría de las computadoras domésticas: algunos juegos requieren una emulación más precisa que otros, por ejemplo, máquinas como C64 o Amstrad CPC necesitan una emulación muy precisa hasta ciclos de reloj, de modo que los sistemas de video de algunos juegos y gráficos demos funcionó correctamente.
También significa que mis emuladores de chip de sonido y CPU ya hechos son en realidad un trabajo superfluo para Bomb Jack, por ejemplo, trabajar con CPU Z80 con la implementación de la fraccionalidad del ciclo de la máquina es excesivo, sería suficiente una fragmentación más simple y más rápida en el nivel de instrucción.
Tablero principal
Por lo general, lo primero que trato de descubrir al escribir un nuevo emulador es el esquema de asignación de memoria (donde están las áreas de ROM y RAM, memoria de video y direcciones especiales o puertos de entrada / salida).
Solo hay un chip "interesante" en la placa principal de Bomb Jack: la CPU Z80 que funciona a 4 MHz. Todo el espacio restante en la placa principal está ocupado por un equipo de decodificación de video (con la excepción de un par de chips RAM y ROM).
El espacio de direcciones de 16 bits es el siguiente:
- 0000..7FFF : 32 KB ROM
- 8000..8FFF : 4 KB de RAM de uso general
- 9000..93FF : 1 KB de memoria de video
- 9400..97FF : 1 KB de RAM de color
- 9820..987F : 96 bytes de sprite RAM
- 9C00..9CFF : 256 bytes de paleta de colores RAM
- 9E00, B000..B005, B800 : puertos de entrada-salida
- C000..DFFF : ROM de 8 KB
El área del puerto de E / S es la siguiente. Algunos puertos son de solo escritura, algunos son de solo lectura y algunos tienen diferentes funciones al leer y escribir en ellos:
- 9E00 : escribir: número de imagen de fondo actual, leer: -
- B000 : lectura: estado del joystick del jugador 1, escritura: activar / desactivar máscara NMI
- B001 : leer: estado del joystick del jugador 2, escribir: -
- B002 : leer: monedas y botones de Inicio, escribir: -
- B003 : leer: CPU watchdog, escribir: ???
- B004 : lectura: interruptores DIP 1, escritura: pantalla de interruptores
- B005 : lectura: interruptores DIP 2, escritura: -
- B800 : escribir: tarjeta de sonido de comando, leer: -
Vale la pena mencionar lo siguiente:
- El dispositivo tiene MUCHA ROM (40 Kbytes) y muy poca RAM (aproximadamente 7 Kbytes, y solo 4 Kbytes de ellos son "RAM de uso general")
- Solo se asignan 2 Kbytes para la RAM de la pantalla, divididos en dos fragmentos de 1 Kbyte, lo que parece muy pequeño para una pantalla a todo color de 256x256, en la que, al parecer, los colores se configuran píxel por píxel
- ¡Este es un sistema de E / S en un esquema de asignación de memoria!
La E / S en el esquema de asignación de memoria es un poco inusual para una máquina Z80, porque una de las características del Z80 es su espacio separado de direcciones de 16 bits para E / S. Esto se hace para ahorrar valioso espacio de direcciones de memoria. La E / S en un esquema de asignación de memoria generalmente se encuentra en computadoras con un procesador 6502.
Una mirada al diagrama de circuito confirma esto: el pin IORQ no se detecta en la CPU de la placa principal, solo el pin MREQ está conectado (que se usa para inicializar la lectura o escritura en la memoria):
Esto significa que no tenemos que preocuparnos por las solicitudes de E / S para la función del temporizador de la CPU de la placa principal en el emulador, sino que solo nos ocupamos de las solicitudes de memoria.
Después de estudiar el diagrama del circuito, encontré otro detalle interesante sobre la CPU de la placa principal:
Solo el pin NMI está conectado, mientras que el pin INT siempre mantiene un alto nivel de señal de reloj / permanece inactivo (esto significa que las interrupciones enmascaradas "habituales" no se ejecutan y solo se producen interrupciones sin enmascarar):
Esto también es bastante inusual para un automóvil con el Z80. En todas las computadoras hogareñas basadas en Z80 con las que solía lidiar, sucedía lo contrario: solo usaban interrupciones enmascarables y nunca usaban no enmascarables. La interrupción enmascarada Z80 es una mejora muy flexible y seria en comparación con el sistema de interrupción primitivo de su "padre ilegítimo" - Intel 8080, o su competidor - MOS 6502. Pero esta mayor flexibilidad también es más difícil de implementar en el equipo (a menos que sea una fuente de interrupciones se utilizan otros chips de la familia Z80, en los que ya hay un protocolo de interrupción complejo incorporado cuando se conecta por bus).
Bueno, suficientes detalles sobre el equipo, ¡pasemos al emulador!
Procedimiento de arranque
El siguiente paso después de determinar la configuración de la memoria es conectar la CPU emulada al esquema de asignación de memoria emulada, registrar algún tipo de visualización del contenido de la memoria de video e iniciar los ciclos de la CPU.
Sorprendentemente, un enfoque tan tosco a menudo es suficiente para pasar por el procedimiento de carga y mostrar
algo en la pantalla. Al diseñar el emulador Bomb Jack, tomé el contenido de la memoria de video de 1 KB en el rango de 0x9000 a 0x93FF como una matriz de bytes de 32x32. Cuando el byte era 0, rendericé un bloque de píxeles negros de 8x8 y, de lo contrario, un bloque de píxeles blancos.
Luego simplemente ejecuté la CPU emulada y esperé lo mejor. He aquí! Apareció una especie de imagen legible:
La imagen superior se ve como una pantalla de prueba de hardware en el arranque, y la inferior se ve como una pantalla de registro de puntaje que aparece después de completar el procedimiento de arranque:
... pero rotó 90 grados (lo cual es lógico, porque la pantalla de las máquinas recreativas a menudo tenía una orientación vertical "vertical").
¡Genial, el comienzo es prometedor!
El siguiente paso es descubrir cómo convertir estos bloques blancos en píxeles de color ... (y este es un gran paso, los detalles se describen a continuación en la sección sobre decodificación de video).
Al principio, todo fue bastante rápido, en la pantalla de prueba, se mostraron píxeles y colores durante la carga (luego noté que la decodificación del color era completamente incorrecta, y sin embargo ...)
Pero cuando apareció la pantalla de grabación, obtuve una pantalla negra. Al piratear el color de fondo para que "no sea negro", descubrí que los píxeles se representan, pero toda la paleta de colores es negra. Hmm ...
Después de mirar esta pantalla durante un par de minutos, recordé que algunos de los colores en la pantalla de puntuación más alta están animados, y cuando hay animación, debería haber algún tipo de temporizador. La fuente lógica de tiempo en la configuración de este equipo es la señal de visualización VSYNC, y VSYNC está conectado al pin NMI de la CPU (o más bien, no VSYNC, sino VBLANK, que es el breve momento entre la señal VSYNC y el haz de rayos catódicos que se mueve hacia la esquina superior izquierda).
Y todavía no he implementado todo esto ...
La noche siguiente, cuando agregué la primera versión del procesamiento NMI a la emulación Z80 y la conecté al primer contador vsync / vblank en la función de temporizador de la CPU de la placa principal, ¡de repente comenzaron a suceder muchas cosas!
En primer lugar, aparecieron colores en la pantalla de registros, y algunos de ellos estaban animados:
¡Después de unos segundos, comenzó algo aún más emocionante! La puntuación más alta desapareció y se mostró una extraña visualización del primer mapa. Estaba claro que este es un modo de demostración de una máquina arcade para llamar la atención: vi varias bombas con animaciones en color que desaparecieron cuando una bomba Jack imaginaria saltó sobre un mapa recogiendo estas bombas:
Los colores todavía estaban completamente equivocados, ¡y sin embargo es PROGRESO!
Es el momento adecuado para hacer el resto de la decodificación de video:
Plancha de video
A primera vista, el equipo de procesamiento de video en Bomb Jack parecía muy potente para una máquina de 8 bits de 1984: a pesar de la resolución de solo 256x256 píxeles, podía mostrar simultáneamente 128 (de 4096) colores y renderizar hasta 24 sprites de hardware (tamaño 16x16) o 32x32) con píxel por píxel de color.
Las computadoras domésticas de 8 bits de la época tenían aproximadamente la misma resolución de pantalla, pero tenían muchas restricciones de color. Estas restricciones son claramente visibles cuando se comparan las versiones de Bomb Jack para ZX Spectrum y Amstrad CPC con la versión para la máquina arcade:
La
versión para el ZX Spectrum tenía una resolución de píxeles bastante buena (256x192), pero muy pocos colores, y sufría el típico efecto de "conflicto de color" de Spectrum (aunque los desarrolladores hicieron todo lo posible para que esto no fuera tan notable):
La versión para Amstrad CPC es más a todo color, pero para obtener más colores, los desarrolladores tuvieron que cambiar al modo de visualización de baja resolución (160x200). Como resultado de esto, Jack y los monstruos se convirtieron en un montón ilegible de píxeles:
Compare esto con la versión para la máquina arcade, que tenía la misma resolución de píxeles que el ZX Spectrum, pero con muchos más colores
y mayor resolución de color de píxel a píxel:
Lo interesante aquí es que la versión arcade tiene mejores gráficos, no porque funcione en hardware más potente (tiene más ROM para almacenar más datos gráficos, pero la "potencia informática" es casi la misma), sino porque los desarrolladores del dispositivo podrían enfocarse en la fabricación de una máquina especializada para un tipo específico de juego y no necesitaban crear una computadora doméstica universal para uso general.
Así es como funciona el hardware de la pantalla (al menos en mi interpretación de alto nivel):
Tres capas de pantalla
La señal de video final de Bomb Jack se combina a partir de tres capas: una capa de fondo, una capa frontal y una capa de sprite.
Tal sistema de capas tiene dos ventajas principales:
- Implementa una compresión de imagen de hardware bastante complicada para generar una imagen a todo color de "alta resolución" a partir de una cantidad muy pequeña de datos.
- Reduce significativamente la cantidad de trabajo de la CPU necesaria para actualizar los elementos dinámicos de la pantalla (incluso a una frecuencia de 4 MHz, una CPU de 8 bits no tiene suficiente potencia para mover tantos objetos en una pantalla de 256x256 con una frecuencia de 60 Hz)
La plancha de video es bastante diferente de lo que vi en las computadoras hogareñas de 8 bits, pero en MAME se implementan clases auxiliares generalizadas para este tipo de equipo, por lo que puedo suponer que es bastante común en las máquinas recreativas.
Capa de fondo
La capa de fondo puede representar 1 de 5 imágenes de fondo incrustadas en ROM. La imagen de fondo se selecciona escribiendo un valor del 1 al 5 en la dirección 0x9E00 (parece que el valor 0 es especial y muestra un fondo completamente negro).
De hecho, parece que el equipo es capaz de reproducir 7 imágenes diferentes, pero solo se usan 5 en el juego. Secretamente, esperaba encontrar datos de imágenes previamente no detectados en la ROM. Pero, por desgracia, no están allí (sí, probablemente no soy el primero en buscarlos allí).
Así es como se ve la capa de fondo del primer mapa sin las otras dos capas:
La capa de fondo se ensambla a partir de mosaicos de
16x16 píxeles.
La ventaja de crear imágenes de fondo a partir de mosaicos es que los mismos mosaicos se pueden usar varias veces, por lo que se pueden almacenar menos datos en la ROM. Tenga en cuenta que el cielo azul, partes de la pirámide y la arena debajo de la pirámide usan los mismos mosaicos:
Para ahorrar memoria, el equipo de la capa de fondo implementa otro truco: los mosaicos se pueden girar horizontalmente. Casi me perdí esto en mi implementación porque supuse que el software no usa esta función de hardware, pero noté un pequeño error en el fondo de la tercera tarjeta:
Utilicé el mismo truco en el quinto mapa, pero aquí es un poco más difícil de notar si no sabes qué buscar:
Capa frontal:
Sobre la capa de fondo se encuentra la "capa frontal", que representa todas las partes fijas de la pantalla, que sin embargo deben ser actualizadas por la CPU (principalmente texto, plataformas y bombas). El diseño se lee desde la RAM (a partir de fragmentos de RAM de 1 KB y RAM de color de 1 KB).
Así es como se ve la capa frontal aislada del primer mapa:
La capa frontal también consta de mosaicos (así como el fondo), pero utiliza mosaicos más pequeños de 8x8:
La principal ventaja de dividir el fondo y el frente en capas separadas es que la CPU no necesita preocuparse por almacenar y restaurar píxeles de fondo al crear o eliminar elementos frontales.
Capa de sprite
Finalmente, los sprites de hardware se representan sobre la capa frontal. Todo lo que se mueve por la pantalla se implementa en sprites. El equipo de Bomb Jack puede generar hasta 24 sprites, y cada sprite puede tener un tamaño de 16x16 o 32x32 píxeles. En este caso, los sprites se pueden colocar con precisión de píxel a píxel:
Decodificador de azulejos 8x8
En el corazón del equipo de decodificación de video hay una paleta de colores con 128 elementos y un decodificador de mosaico de 8x8 píxeles. La tarea del decodificador de mosaico es generar un índice de paleta de colores de 7 bits para cada uno de los 64 píxeles del mosaico.
8x8 — 16x16, 8x8 16x16 32x32.
- 8x8 (, ):
- :
- « » ( 32x32 ) ( 32x32). , , . 8x8 .
- . ( ). , , ( ).
- 8 , 8 ( ). , , 8x8 24 (3 ).
- 64 7- . 3 , 4 — . , , 16 «», 8 . 8 .
- 7- , , 12- RGB- (4 ). ( , , ; , ).
, , :
- 512 8x8. 9- , 8 . «» ( 4 , 4 ). 3 8x8 , , «» .
- 16x16, 16x16=256 256 (512 ). , 16x16 8x8, . , ; «» : 7 , .
- 16x16 32x32 , 4 16 8x8 . , 16x16 96 , 32x32 — 384 . , 3 , .
, ,
C , PNG (3 8 ).
. , , ( ), Bomb Jack, , (, 90 , ):
. , 16x16 8x8. 16x16 8x8. 2, 3 4.
, , . 16x16 , 32x32 — .
Bomb Jack , . , , :
Bomb Jack , :
- 24 . , .
- 16x16 32x32
- 16 8
- .
- 128 .
8x8, .
0x9820 0x987F — 96 , 4 . , ; , .
4 :
- 0 :
- 7 : , 32x32, 16x16
- 6..0 : 7 , .
- 1 :
- 2 : X
- Byte 3 : Y
No está claro qué hacen los bits 4 y 5 del byte 1, el comentario en MAME dice esto:
e ? (, )
f ? (, (B)?)
Puertos de E / S de memoria
Algunas notas sobre los puertos de entrada / salida de la placa principal. Como se indicó anteriormente, los puertos de E / S se ven así:
- 9E00 : escribir: número de imagen de fondo actual, leer: -
- B000 : lectura: estado del joystick del jugador 1, escritura: activar / desactivar máscara NMI
- B001 : leer: estado del joystick del jugador 2, escribir: -
- B002 : leer: monedas y botones de Inicio, escribir: -
- B003 : leer: CPU watchdog, escribir: ???
- B004 : lectura: interruptores DIP 1, escritura: pantalla de interruptores
- B005 : lectura: interruptores DIP 2, escritura: -
- B800 : escribir: tarjeta de sonido de comando, leer: -
La dirección 0x9E00 (selección de la imagen de fondo) que ya hemos considerado anteriormente, y la dirección 0xB800 (tarjeta de sonido de comando) que consideraremos en la siguiente sección. Sigue siendo las direcciones de 0xB000 a 0xB005:
La lectura de las direcciones 0xB000 y 0xB001 devuelve el estado actual de los dos joysticks. Los bytes establecidos indican interruptores de joystick cerrados:
- bit 0 : dirección correcta
- bit 1 : dirección izquierda
- bit 2 : hacia arriba
- bit 3 : dirección hacia abajo
- bit 4 : botón de salto presionado
Los 3 bits restantes se ignoran.
La lectura de 0xB002 devuelve el estado del aceptador de monedas y los botones de Inicio:
- bit 0 : se lanza la moneda del jugador 1
- bit 1 : se lanza la moneda del jugador 2
- bit 2 : botón de inicio del jugador 1
- bit 3 : botón de inicio del jugador 2
La lectura de las direcciones 0xB004 y 0xB005 devuelve el estado de los interruptores DIP que se utilizan para configurar el comportamiento de la máquina arcade:
- B004 :
- bits 0,1 : cuántos "juegos" se dan para una moneda (1, 2, 3 o 5)
- bits 2,3 : lo mismo para el jugador 2
- bits 4,5 : cuántas vidas por juego (3, 4, 5 o 2)
- bit 6 : la ubicación de la máquina arcade: "mesa de cóctel" o "vertical".
- bit 7 : si reproducir sonido en modo de espera
- B005 :
- bits 3.4 : dificultad 1 (velocidad del pájaro)
- bits 5,6 : dificultad 2 (número y velocidad de enemigos)
- bit 7 : frecuencia de ocurrencia de una moneda en particular
Finalmente, la lectura de la dirección
B003 implementa un perro guardián de software. La CPU a menudo debe leer desde esta dirección, de lo contrario, la máquina arcade realizará un reinicio de hardware. Si por alguna razón el juego falla, el equipo se reiniciará automáticamente.
Puede escribir en algunas direcciones de puerto de E / S:
- B000 : si generar NMI durante vblank; parece estar desactivado solo durante el procedimiento de arranque
- B004 : voltea toda la pantalla; Nunca conocí el uso de esta función, pero tengo una teoría al respecto (ver más abajo)
La funcionalidad de cambio de pantalla es un poco confusa, porque cuando jugaba, nunca vi su uso. Sin embargo, tengo un presentimiento sobre lo que está haciendo, pero para confirmarlo, debes escribir el código. Cuando la máquina arcade está en la configuración de "mesa de cóctel", dos jugadores se sientan uno frente al otro. Por lo tanto, sugerí que cuando un juego cambia del jugador 1 al jugador 2, esta función voltea la pantalla. Sin embargo, todavía no he implementado el modo de dos jugadores en el emulador.
Tablero de sonido
La tarjeta de sonido en sí es una computadora con todas las funciones con una CPU Z80 (que funciona a una frecuencia de 3 MHz), tres chips de sonido (AY-38910 que funcionan a una frecuencia de 1.5 MHz), así como RAM y ROM. El esquema de asignación de memoria de la tarjeta de sonido parece bastante simple:
- 0000..2000 : 8 Kbytes de ROM
- 4000..4400 : 1 KB de RAM
- 6000 : comando de sonido desde la placa principal
Como no hay nada interesante en el esquema de asignación de memoria por encima de la dirección 0x8000, el contacto de dirección más alto de la CPU ni siquiera está conectado:
La dirección especial 0x6000 es el puerto de E / S (bloqueo de 8 bits) ubicado en la memoria, que no corresponde a la RAM real. Este es el mismo puerto que se encuentra en la placa principal en 0xB800. Es un canal de comunicación entre la tarjeta principal y la tarjeta de sonido.
Los tres chips de sonido están controlados por estas instrucciones de salida Z80, no a través de los puertos de memoria. AY-3-8910 tiene solo dos puertos de E / S abiertos, el primero se usa para almacenar el número de registro y el segundo se usa para escribir o leer el contenido del registro especificado por el primer puerto.
El circuito de E / S es el siguiente:
- 0x00 : primer chip de sonido: selección de registro
- 0x01 : primer chip de sonido: acceso al registro seleccionado
- 0x10 : segundo chip de sonido: selección de registro
- 0x11 : segundo chip de sonido: acceso al registro seleccionado
- 0x80 : tercer chip de sonido: selección de registro
- 0x81 : tercer chip de sonido: acceso al registro seleccionado
Algunas palabras sobre el chip de sonido AY-3-8910:
Este es un dispositivo bastante estándar, muy popular en las computadoras domésticas de esa época (por ejemplo, en Amstrad CPC, ZX Spectrum 128, en computadoras MSX y muchas otras). AY-3-8910 generó muchas variaciones y clones (por ejemplo, Yamaha YM2149, que en sí mismo se convirtió en la base de toda una familia de chips de sonido más potentes).
AY-3-8910 tiene 3 canales de señales rectangulares, un generador de ruido que se puede mezclar con tres canales y un generador de envolvente. Como solo había un generador de envolvente para los tres canales, no era particularmente útil, y la mayoría de los juegos usaban una CPU para modular el tono y el volumen.
Esto significa que el chip AY-3-8910 requiere más intervención de la CPU para crear un sonido de alta calidad (a diferencia de los chips SID más independientes, por ejemplo, en una computadora C64).
Es sorprendente ver qué se puede hacer con tres chips de sonido bastante simples y la CPU que los controla. La música y los efectos de sonido de Bomb Jack son mucho más ricos de lo que he escuchado en la mayoría de los juegos de computadora en casa.
Lo único que es realmente interesante en esta tarjeta de sonido es la forma en que recibe sus comandos de la placa principal.
Comando de sonido pestillo
El "pestillo de sonido" es un almacenamiento de un solo byte (pestillo de 8 bits) común a las tarjetas de sonido y principal. El pestillo está vinculado a la dirección 0xB800 en la placa principal y a la dirección 0x6000 en la tarjeta de sonido.
Cuando se activa la interrupción NMI usando VSYNC, la tarjeta de sonido realiza una rutina de servicio de interrupción muy simple, que lee el pestillo del hardware, lo escribe en la dirección de memoria normal y establece el "bit de señal", que le dice al "bucle principal" que se ha recibido un nuevo comando de sonido:
ex af,af' ;0066 exx ;0067 ld hl,04390h ;0068 set 0,(hl) ;006b ld a,(06000h) ;006d ld (04391h),a ;0070 exx ;0073 ex af,af' ;0074 retn ;0075
El método de activación de contacto NMI es ligeramente diferente del método de la placa principal:
En la placa principal, el pin NMI se activa durante la ejecución de VBLANK.
Sin embargo, en la tarjeta de sonido, NMI se activa cuando se activa VSYNC, y permanece activo no durante VBLANK, sino hasta que el procedimiento de servicio de interrupción lea los datos del pestillo a 0x6000.
Cuando el equipo reconoce la lectura de la dirección 0x6000, realiza dos operaciones codificadas:
- el contenido del clip de sonido se restablece a 0
- El contacto de NMI se vuelve inactivo
De hecho, esta es una simple eliminación del rebote de contacto, que no permite que un comando de sonido se ejecute dos veces.
La única pregunta sigue siendo: con qué frecuencia la placa principal escribe un nuevo comando (porque la forma de implementar la emulación de dos placas depende de esto).
Después de la depuración con printf, descubrí que la placa principal registra como máximo un comando de sonido por cuadro de 60 Hz. Esto simplificó enormemente la estructura del "ciclo principal" del emulador.
El problema del trabajo conjunto de dos computadoras emuladas separadas que deben intercambiar datos entre sí es que la emulación de una computadora es efectiva solo si puede realizar muchos ciclos a la vez sin interferencia.
Por ejemplo, el peor de los casos sería:
- ejecutamos una instrucción en computadora 1
- ejecutamos una instrucción en la computadora 2
- repetir ...
Mi emulador Z80 no está optimizado para salir y entrar en la emulación para cada instrucción, porque en este caso debería descargarse en la memoria y cargar desde la memoria el estado de la CPU al principio y al final de cada instrucción. Si la CPU puede procesar muchas instrucciones sin interferencia, entonces puede almacenar (la mayoría de) el estado de la CPU en los registros y restablecer el estado a la memoria en la última instrucción.
Es decir, una situación ideal sería esta: realizamos un sistema emulado sin interferencia en todo el marco del sistema host (para una CPU con una frecuencia de 4 MHz y a 60 Hz, esto significa aproximadamente 67 mil ciclos por marco, o entre 3 mil y 16 mil instrucciones Z80).
Cuando trabajaba con Bomb Jack, necesitaba asegurarme de que la placa principal no grabe un nuevo comando antes de que la tarjeta de sonido pueda leer el último comando. Antes de descubrir que la placa principal no registra más de un comando por cuadro, consideré la necesidad de crear una cola compleja de comandos que interceptara las grabaciones en el pestillo de sonido de la placa principal y almacenara el número de ciclo y el byte de comando en la cola.
Luego, en el momento en que la tarjeta de sonido ejecutaba su cuadro, tomaría un nuevo comando de la cola de comandos cuando se alcanzara el número del ciclo de comando.
Tal sistema funcionaría y sería "correcto", pero aumentaría enormemente la complejidad del código.
Al final, decidí usar una solución mucho más simple sin colas. Como la placa principal registra solo un comando por cuadro, alterné la ejecución en dos computadoras para que cada una de ellas realizara dos segmentos de tiempo por cuadro:
- realizar la primera mitad del marco en el tablero principal
- realizar la primera mitad del cuadro en la tarjeta de sonido
- realizar la segunda mitad del marco en el tablero principal
- realizar la segunda mitad del cuadro en la tarjeta de sonido
Esto garantiza que la tarjeta de sonido vea correctamente todos los comandos grabados por la placa principal y, al mismo tiempo, pueda ejecutar cada emulación durante miles de ciclos.
Por supuesto, el hecho de que el sistema host funcione a una velocidad de cuadro de 60 Hz es una suposición muy audaz :)
Y el ultimo ...
El último hecho interesante sobre la versión del emulador en WebAssembly:
Tamaño comprimido de todos los archivos descargados cuando se ejecuta el emulador en WebAssembly
aproximadamente igual a 113 Kbytes:
- aproximadamente 2.5 Kbytes para HTML, CSS y JS "manuscrito"
- 26.8 kb por archivo JS de tiempo de ejecución emscripten
- 83,7 KB por archivo .wasm
El archivo WASM contiene las ROM integradas de la máquina arcade.
Sin comprimir, estas ROM ocupan 112 Kbytes.
Es decir,
todo el emulador comprimido con ROM integradas ocupa casi el mismo volumen que las ROM sin comprimir :)
Las ROM de 112 kilobytes se comprimen a aproximadamente 57 KB, es decir, el tamaño real del código comprimido en WASM sin datos de ROM es inferior a 30 KB (84-57).
Me parece bastante bueno para un emulador completo de un sistema de 8 bits;)