Archivo

Posts Tagged ‘emulador’

YASS ha encontrado un hogar

Los que hayáis seguido el tutorial que he publicado durante el verano acerca del desarrollo de un emulador de Sinclair ZX Spectrum, sabed que YASS (“Yet Another Spectrum Simulator”) ha encontrado un hogar adoptivo en Speccy.org – el mayor foro de usuarios de Spectrum en castellano.

YASSAtSpeccyPoco después de terminar el tutorial (imagino que ya me vale) descubrí Speccy.org, uno de los mayores (si no el mayor) foro de usuarios de Sinclair ZX Spectrum de habla hispana. Lleno a rabiar de gente que sabe muchísimo más que yo de tan adorable máquina, mi intención era simplemente dejarles saber que había un nuevo emulador y que, quizá, alguien podría encontrar mi trabajo interesante.

El recibimiento por parte de la gente de Speccy fue simplemente como si fuese uno más de la familia: mensajes de bienvenida, sugerencias para mejorar el emulador y ampliar sus capacidades, ideas a cual más interesante para continuar con el proyecto. Y para mi sorpresa, los administradores me invitaron cordialmente a hospedar YASS y los contenidos del tutorial en sus propios servidores, junto con otra inmensa cantidad de proyectos también relacionados con la emulación y preservación del ZX Spectrum.

Me pareció una excelente idea, para preservar YASS, para facilitar su acceso – tenerlo como artículos sueltos en un blog hace bastante complicado no perderse en la navegación. Así que rehíce los contenidos y podréis acceder a ellos de forma mucho más cómoda (y mejor organizada) en http://yass.speccy.org.

Desde aquí, agradecer a los administradores de speccy.org la posibilidad de sentirme uno más de una familia. Gracias!

Anuncios
Categorías:Desarrollo, Noticias Etiquetas: , , , , , ,

Hazte un Spectrum (8ª Parte y final)

Todos los componentes están realizados y en su sitio, el código ha ido tomando forma con una maravillosa fluidez y he aprendido lo indecible durante estas breves semanas. Aprovecho que mañana vuelvo a la dura vida cotidiana para cerrar esta serie de artículos – unos remates finales y completar el código fuente.

Hemos llegado al final del camino – unas semanas que me han servido para conocer un poco más uno de los mitos de la informática personal de los años ‘80 y que para mí, hasta hace unas semanas, era casi un completo desconocido. Hemos empezado por diseñar una máquina virtual, ir aprendiendo cómo funcionaban los diferentes componentes de la misma y emulando su comportamiento. Hoy concluye este fascinante viaje. Vamos a divertirnos un poco y darle incluso un aire más “retro” al emulador, aprendiendo de paso cómo aprovechar las capacidades de aceleración hardware de gráficos de WPF.

Vamos a empezar con una herramienta imprescindible para diseñar interfaces de usuario en aplicaciones modernas: Photoshop. Con ella vamos a realizar una capa gráfica (“overlay”) que podremos activar a voluntad para darle ese toque retro, imprescindible para mi: el “efecto TV” o, como me ha dado por llamarlo en el emulador, el “modo Telefunken” (y apuesto a que mi hermano estará de acuerdo conmigo a que el nombre le viene que ni pintado gracias a nuestras experiencias de chavales).

Una nueva herramienta de desarrollo: Photoshop

Comencemos (sorprendentemente) buscando en Google Images por un fondo apropiado. Nada más facil que navegar http://images.google.com y buscar por “TV frame”. Busco un “marco de televisor”, y he escogido el que me ha parecido más simpático, que incluso ya viene con el hueco transparente. Veamos los pasos siguientes, uno a uno:

El primer paso es abrir la imágen que más nos guste en Photoshop y ajustar su tamaño a 640×480. Invocando el menú Image/Image Size podremos ajustarlo. Si habéis cargado la imagen como “Background”, haced doble click sobre ella para convertirla en una capa normal (Layer). Ponedle como nombre “TV Frame” por ejemplo y borrad todo lo que pudiese verse dentro la imagen para que quede transparente: TVLayer-4
Vamos a crear una nueva imagen, con un tamaño algo peculiar: 1 pixel de ancho por 2 de alto. Esta imagen nos servirá como “relleno” para el familiar efecto de las líneas en pantalla TVLayer-1
Tras crear la imagen, seleccionamos la herramienta “lápiz” y rellenamos el pixel de arriba en negro, dejando el pixel de abajo transparente TVLayer-2
Seleccionad la imagen con Ctrl+A y luego Edit/Define Pattern. Yo le puse de nombre “ScanLine”. TVLayer-3
Volved ahora a la imagen principal con nuestro televisor. Añadid una nueva capa y aseguraos que queda debajo del marco del televisor. Ponedle de nombre por ejemplo “Scanlines”: TVLayer-5
Aseguraos que tenéis la capa Scanlines seleccionada y rellenadla con el patrón que diseñamos antes. Pulsad mayúsculas+BackSpace, seleccionad “Pattern” de la lista de opciones y el patrón Scanlines como “Custom Pattern”: TVLayer-6
Ya tenemos nuestro familiar efecto de líneas de televisor… simplemente ajustad la opacidad de la capa “al gusto” (40-60%) TVLayer-7
Agregad una nueva capa al diseño, y colocadla entre el marco del televisor y la capa de líneas. Yo la he llamado “Glare”: TVLayer-8
Seleccionad la capa y empleando la herramienta de selección, dibujad un rectángulo que ocupe más o menos el tercio superior de la pantalla. Luego redondead la selección con la herramienta Select/Modify/Feather y algo así como 25 píxeles: TVLayer-9
Comprobad que vuestros colores son el blanco para la tinta y el negro para el fondo, seleccionad la herramienta de gradientes y en la barra superior escoged la configuración “Foreground to transparent” (esto es, de blanco – nuestro color actual – a transparente). TVLayer-10
Ahora pulsad con el botón más o menos en la zona superior de la selección y arrastradla hasta un poco antes de la zona inferior. Luego ajustad un poco la opacidad de la capa, quizá entre 60-70%: TVLayer-11

Ya tenemos el overlay listo – simplemente guardadlo dentro del proyecto del emulador como PNG de 32 bits (con transparencia!!) y ya podemos importarlo en el proyecto. Para ello pulsad con el botón derecho del ratón sobre el proyecto en el que queráis añadir la imagen y seleccionad “Add/Existing Item”. Buscad la imagen PNG, aceptad el diálogo y aseguraos que en las propiedades de la imagen, la entrada Build Action aparece configurada como “Resource”: el compilador “incrustará” la imagen dentro del binario, permitiéndonos olvidarnos de distribuir los ficheros sueltos junto a la aplicación.

Ahora abrid el archivo MainWindow.xaml, y añadid una nueva imagen justo debajo del control zx:Spectrum:

    
    

La propiedad Source especifica la imagen que debe emplear el control, en este caso Source=”/YASS;component/Resources/TVEffect.png”. La primera parte (“/YASS;”) indica el nombre del ensamblado que lleva el archivo, en este caso “YASS.exe” que será el nombre del ejecutable. Lo que sigue es el camino (o “path”) hasta el recurso: en mi caso el archivo se llama “TVEffect.png” y está dentro de la carpeta “Resources” del proyecto. Si no queréis tener que averiguar el camino al archivo, insertad una imagen sin campo “Source” y luego simplemente usad las propiedades del control en Visual Studio. Navegad hasta la propiedad Source en la lista y seleccionad directamente la imagen con el menú desplegable – mucho más fácil que ponerlo a mano y sin errores!!! Fijaos que los dos elementos (el control zx:Spectrum y la imagen “tvOverlay” están situados en la misma celda del Grid que los contiene. El efecto es que la imagen cubrirá permanentemente al control, sin tener que hacer absolutamente nada en código – solo cambiar su visibilidad.

Aprovechad tambien para añadir al menú de la aplicación una entrada para activar o desactivar el “modo Telefunken”:

  ...
  
  
  
  ...

Finalmente, basta con agregar la función “OnTVMode()” al programa, que alternará la visibilidad de la capa:

  private void OnTVMode(object sender, RoutedEventArgs e)
  {
    // Invert visibility of tv overlay
    tvOverlay.Visibility = tvOverlay.Visibility == Visibility.Collapsed ? Visibility.Visible : Visibility.Collapsed;
    tvModeMenu.IsChecked = tvOverlay.Visibility == Visibility.Visible;
  }

Si ejecutamos el emulador y seleccionamos el menú “Telefunken Mode”, el efecto será un emulador incluso más retro:

YASS-OldieLook

El overlay está acelerado por hardware, así que la capa no afecta en lo más mínimo a la velocidad de ejecución del emulador, ni incrementa el consumo de la cpu – la tarjeta gráfica hace todo el trabajo.

Alguna cosita más?

Pues si, un par de cosas que he añadido al código final antes de publicarlo. Lo más significativo es que al seleccionar un archivo de cinta ya no es necesario escribir LOAD “” para iniciar la carga. La solución, rápida y simple: una función que emula la pulsación de las teclas para escribir el comando tras abrir el archivo TAP. Muy simple, pero cumple con su cometido – con la pega de que necesitamos que el Spectrum esté dispuesto a aceptar el comando para que funcione. También encontraréis opciones para parar, arrancar y resetear el emulador – funciones que he empleado para depurar algunas cosas y asegurarme de que no me dejaba nada “tirado” por ahí.

Bueno… creo que eso es todo

El emulador está completo y el viaje ha llegado a su fin. Me he divertido mucho, realmente mucho durante estas semanas y me ha encantado intentar documentar todo el proceso. El emulador que os dejo está lejos de ser completo o de ejecutar “absolutamente todos los programas del Spectrum” – no había tiempo para tanto, siendo más bien mi intención la de ofreceros un tutorial completo sobre cómo desarrollar un emulador “desde cero”, cómo combinar varios lenguajes de programación para obtener los mejores resultados y, sobre todo, mostraros cómo era la informática de los años 80. He intentado mantener el código fuente lo más limpio y estructurado posible y creo que es bastante facil de leer y, sobre todo, ampliar.

Como os decía al principio, hace unas semanas no conocía apenas nada del Sinclair ZX Spectrum, y seguro que algún error habré cometido al documentar o emular alguna funcionalidad – espero que sepáis disculparme y, sobre todo, dejadme saber dónde he metido la pata – quiero seguir aprendiendo!

Mañana me toca volver a los quehaceres diarios, pero esta “retromanía” me sigue afectando… cual podría ser el próximo proyecto?

Enlaces:

Emulador YASS para Windows
Código fuente del Emulador YASS – actualizado a VS2015
Enlaces actualizados el 26 de Febrero de 2018

– Hazte un Spectrum: 1ª Parte, 2ª Parte, 3ª Parte, 4ª Parte, 5ª Parte, 6ª Parte, 7ª Parte, 8ª Parte

Categorías:Desarrollo, Tutorial Etiquetas: , , , , ,

Hazte un Spectrum (7ª Parte)

Ya casi llegamos al final y hoy vamos a añadir dos componentes a nuestro emulador: la capacidad de hacer “ruido” y la posibilidad de emular un joystick Kempston.

Y para comenzar, vamos con lo facilito primero: el joystick. El hardware de un joystick Kempston es lo más simple del mundo: solamente un registro de I/O que se direcciona poniendo el bit de dirección 5 a cero – muy parecido al direccionamiento de un bit de la ULA del Spectrum. Al leer el registro (típicamente de 0x1F == 00011111b por ejemplo) se obtiene un byte donde los cinco bits menos significativos son el estado de las cinco posiciones del joystick (arriba, abajo, izquierda, derecha y “fuego”):

KempstonBits

Dado que hasta ahora estábamos conectado la ULA directamente al bus de I/O de la cpu Z80, ya no puede ser el caso, y necesitamos un decodificador intermedio. La clase hospeda los componentes (ULA y Kempston), decodifica la dirección y reenvía la petición al controlador apropiado:

class SpectrumIOBus : public BusComponent<0x00,0x10000>
{
public:
  SpectrumIOBus()
  {
    ULA = NULL;
    Kempston = NULL;
  }

  unsigned char Read(unsigned int address)
  {
    // Kempston interface is addressed by A5 == 0
    if (Kempston && ((address & 0x20) == 0))
      return(Kempston->Read(address));

    // ULA is addressed by A0 == 0
    if (ULA && ((address & 0x01) == 0))
    return(ULA->Read(address));

    // No device
    return(0xFF);
  }

  void Write(unsigned int address,unsigned char value)
  {
    // ULA is addressed by A0 == 0
    if (ULA && ((address & 0x01) == 0))
      return(ULA->Write(address,value));
  }

public:
  BusComponentBase *ULA;
  BusComponentBase *Kempston;
};

Si os fijáis, en el Write del bus ni siquiera nos molestamos en enviar las escrituras al controlador Kempston…

La implementación de la clase Kempston es igual de simple:

#define KEMPSTON_RIGHT        0x01
#define KEMPSTON_LEFT         0x02
#define KEMPSTON_DOWN         0x04
#define KEMPSTON_UP           0x08
#define KEMPSTON_FIRE         0x10

class Kempston : public BusComponent<0x1F,1>
{
public:
  Kempston()
  {
    kempstonData = 0;
  }

public:
  void SetKempstonData(unsigned char data)
  {
    kempstonData = data;
  }

public:
  unsigned char Read(unsigned int address)
  {
    return(kempstonData);
  }

  void Write(unsigned int address,unsigned char value)
  {
    // Do nothing
  }

  unsigned char kempstonData;
};

La variable miembro kempstonData (pública) es donde la aplicación host establece el estado del joystick, y la clase simplemente conserva el valor y lo devuelve a la cpu cada vez que nos lo pida. Finalmente, en la clase Spectrum vamos a modificar los buses de nuestro procesador para integrar el nuevo decodificador de I/O y agregar el interface Kempston:

  void Spectrum::emulatorMain()
  {
    // Components
    Bus16 bus;              // Main Z80 data bus
    SpectrumIOBus ioBus;    // Main Z80 I/O bus
    ROM<0,16384> rom;       // Main system ROM
    ULA ula;                // Spectrum's ULA chip + 16KB
    RAM<32768,32*1024> ram; // Remaining RAM (32KB)
    Kempston kempston;      // Kempston joystick
    Z80 cpu;                // and finally, the CPU core

    // Load ROM contents
    if (rom.Load("48.rom"))
      throw gcnew System::IO::FileNotFoundException("Unable to load rom '48.ROM'");

    // Configure the ULA with the native Windows bitmap used to emulate the screen
    ula.SetNativeBitmap((LPBYTE)screenData,bytesPerScreenLine);

    // Populate buses
    bus.AddBusComponent(&rom);
    bus.AddBusComponent((ULAMemory*)&ula);
    bus.AddBusComponent(&ram);

    ioBus.ULA = (ULAIO*)&ula;
    ioBus.Kempston = &kempston;

    // And attach busses to cpu core
    cpu.DataBus = &bus;
    cpu.IOBus = &ioBus;

    pCurrentUla = &ula;
    pCurrentKempston = &kempston;

Con una enumeración de .NET y un par de funciones, tenemos el API para poder usar el joystick desde una aplicación host:

namespace Emulation
{
  // Kempston joystick status values.
  [Flags]
  public enum class KempstonFlags
  {
    Right = KEMPSTON_RIGHT,
    Left = KEMPSTON_LEFT,
    Down = KEMPSTON_DOWN,
    Up = KEMPSTON_UP,
    Fire = KEMPSTON_FIRE
  };

  public ref class Spectrum : System::Windows::Controls::Border, IDisposable
  {
    ...
  public:
    void SetKempstonStatus(KempstonFlags status)
    {
      if (pCurrentKempston != nullptr)
        pCurrentKempston->SetKempstonData((unsigned char)status);
    }

  private:
    ULA *pCurrentUla;
    Kempston *pCurrentKempston;
  }

Sólo queda ampliar nuestra gestión del teclado en C# para mapear las teclas de cursor y “ctrl” (por ejemplo) al joystick Kempston (nuevamente, tanto en OnKeyDown() como en OnKeyUp()):

    Emulation.KempstonFlags kempston;

    protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)
    {
      base.OnKeyDown(e);
      switch (e.Key)
      {
        ...
        // Kempston
        case System.Windows.Input.Key.Left: kempston |= Emulation.KempstonFlags.Left; break;
        case System.Windows.Input.Key.Right: kempston |= Emulation.KempstonFlags.Right; break;
        case System.Windows.Input.Key.Up: kempston |= Emulation.KempstonFlags.Up; break;
        case System.Windows.Input.Key.Down: kempston |= Emulation.KempstonFlags.Down; break;
        case System.Windows.Input.Key.LeftCtrl: kempston |= Emulation.KempstonFlags.Fire; break;
      }
      emulator.SetKempstonStatus(kempston);
    }

    protected override void OnKeyUp(System.Windows.Input.KeyEventArgs e)
    {
      base.OnKeyUp(e);
      switch (e.Key)
      {
      ...
        // Kempston
        case System.Windows.Input.Key.Left: kempston &= ~Emulation.KempstonFlags.Left; break;
        case System.Windows.Input.Key.Right: kempston &= ~Emulation.KempstonFlags.Right; break;
        case System.Windows.Input.Key.Up: kempston &= ~Emulation.KempstonFlags.Up; break;
        case System.Windows.Input.Key.Down: kempston &= ~Emulation.KempstonFlags.Down; break;
        case System.Windows.Input.Key.LeftCtrl: kempston &= ~Emulation.KempstonFlags.Fire; break;
      }
      emulator.SetKempstonStatus(kempston);
    }

Vamos finalmente con la parte de audio que, lamentablemente, es algo más compleja que la implementación del interface Kempston. Veamos primero un poco de la teoría

Sonido de 1 bit?

Aunque siendo pragmáticos el sonido del Spectrum realmente son “2” bits: uno para manipular el altavoz y otro para la generación de audio para la unidad de cassette, la única diferencia entre ambos es una resistencia para limitar la amplitud de la segunda señal. El audio de “1 bit” sólo permite establecer dos niveles de audio, frente a los 65536 que se pueden definir con (por ejemplo) una moderna tarjeta de sonido de 16 bits. Usando Audacity (un estupendo editor de audio open source) podemos comparar audio de 1 bit con audio de 16 bits:

Audio16vs1

Con una limitación como ésta, parecería imposible que el Spectrum pudiese producir nada más que tristes pitidos, verdad? Pues no: utilizando pulsos escrupulosamente medidos (codificación PWM o “Pulse Width Modulation”) se puede obtener un audio que simule características muy avanzadas: voces múltiples, control de volumen, etc. El truco del audio PWM es que, ya que no puedo “colocar el altavoz en la posición que yo quiero”, lo que sí puedo hacer es dar “empujoncitos” para aproximarlo. El símil que se me ocurre es lo que hacemos en los juegos de conducción: aunque los controles “izquierda” o “derecha” sean botones y un volante real sea “analógico”, puedo dar toques, más o menos largos, a los controles de izquierda o derecha para aproximar la posición del volante a la deseada. Fijaos en el siguiente gráfico de una demo del Spectrum (“Garfield”):

PWMAudio

Audio PWM en estado puro!! El programa cuenta ciclos para subir o bajar la señal de audio y controlar el ancho de los pulsos… Luego nuestro cerebro hace el resto – y creedme que el resultado es sorprendente!!

Generación de audio

Ya conocemos un poco la teoría, vamos ahora a la práctica. En el ZX Spectrum, para generar audio se usan los bits 4 y 3 de la ULA, escribiendo (por ejemplo) en la posición 0xFE de I/O. Cuando se manipula el bit 4 el nivel de la señal es muy amplio (más “alta”) mientras que el bit 3 se usa para modular la señal que se dirige a la cinta de cassette. Parece simple entonces que, cada vez que nos llegue una escritura a la ULA podamos calcular el nivel de la señal. Pero no nos basta con una muestra: necesitamos generar una señal en el tiempo, o sea que necesitamos muchas muestras. Pero cuantas?

Recordemos que la emulación deja correr al procesador durante un cuadro de pantalla (1/50 de segundo, o lo que es lo mismo 20 milisegundos) para luego hacer una pausa y “sincronizar” la velocidad del Spectrum emulado en nuestro rápido PC. Podemos trabajar en bloques de audio de 1/50 de segundo aprovechando el contador que usamos en la ULA para saber nuestra posición en pantalla.

Ahora sabemos que necesitamos 20 milisegundos de audio por cada cuadro de pantalla. Cuantas muestras son esas? un par de cálculos nos lo dirán: vamos a generar audio a 48KHz, esto es, 48000 muestras por segundo. Eso son (48000/50) = 960 muestras cada 20 milisegundos. Pero qué valor le damos a esas muestras?

No podemos predecir cuándo van a escribir los programas en los controles de audio del chip ULA, pero sí sabemos la posición (en ciclos) con respecto a la pantalla. Aprovechando la función AddCycles() de la ULA, podemos actualizar la muestra que corresponda con la posición del tubo CRT:

#define   AUDIO_SAMPLE_RATE         (48000)
#define   SAMPLES_PER_FRAME         (AUDIO_SAMPLE_RATE / 50)

class ULA : public ULAMemory, ULAIO
{
  unsigned char ULAIOData;
  short FrameAudio[SAMPLES_PER_FRAME];

  ...
  void AddCycles(unsigned int cycles,bool& IRQ)
  {
    dwFrameTStates += cycles;
    dwScanLineTStates += cycles;

    // Update the analog audio output from ULA
    // First, compute audio output value for this cycle
    int signal = 0;
    signal = (ULAIOData & 0x10) ? +16384 : -16384;
    signal += (ULAIOData & 0x08) ? +8192 : -8192;

    // Now, add audio output over an 8 tap filter:
    // 1: Maintain 7/8ths of the original signal
    audioOutput -= (audioOutput / 8);
    // 2: ...and add 1/8th of the new one
    audioOutput += (signal / 8);

    // Update the audio sample corresponding to this screen tState
    unsigned int offset = (dwFrameTStates * SAMPLES_PER_FRAME) / (TSTATES_PER_SCANLINE * TVSCANLINES);
    // As clocks don't match and this is a quick approximation, limit offset output
    if (offset < SAMPLES_PER_FRAME)
      FrameAudio[offset] = audioOutput;

    if (dwScanLineTStates > TSTATES_PER_SCANLINE)
      ScanLine(IRQ);
  }

  void IOWrite(unsigned int address,unsigned char value)
  {
    ...
    // save a copy of ULA's state
    ULAIOData = value;
    ...
  }

La función AddCycles() se invoca después de cada instrucción ejecutada por el procesador, y recorrerá de forma más o menos uniforme los casi 69000 ciclos que forman la pantalla. Dado que 69000 ciclos entre 960 muestras dan para muchas repeticiones en cada muestra de audio, la función integra repetidamente la señal con un filtro de 8 taps, esto es, sólo 1/8 de la señal de agrega en cada interacción, algo así como un “condensador en software”. De esta forma, los rápidos cambios en los controles de audio de la ULA se convierten en señales un poco más “analógicas” que, posteriormente, la tarjeta de sonido terminará de digerir. Para la amplitud de la señal he usado +/-16384 para la señal del altavoz y +/- 8192 para la del cassette. Arbitrario? Totalmente.

Ahora que ya tenemos la muestra de 20 milisegundos de audio, es necesario enviarla a la tarjeta de sonido para que la reproduzca, y es aquí donde aparece el mayor problema (con diferencia) para la emulación: cómo sincronizar a la perfección el Spectrum con el audio para que no dé tirones ni cortes.

Para mi la solución consiste en emplear el cronómetro más exacto que se pueda conseguir en un PC.

El temporizador más preciso de un PC: la tarjeta de sonido

Hasta ahora, la solución para “ralentizar” el Spectrum a su velocidad real era ver el tiempo que habíamos necesitados para correr durante 20 milisegundos y complementarlo con un Sleep() del sistema operativo. Aunque funciona (y es válido si no hay sonido), hay que tener en cuenta que la mayoría de los sistemas operativos no son realmente “de tiempo real”: una instrucción Sleep(17) no dura realmente 17 milisegundos, sino cualquier cosa entre 1 (o 0!!) y 30 (o más), dependiendo de la resolución de los “timers” del hardware, de lo ocupada que está la cpu, etc. Cómo conseguimos sincronizar entonces nuestro audio?

Aunque parezca mentira, el mejor cronómetro de un PC moderno es su tarjeta de sonido. En la tarjeta de sonido, 20 milisegundos son realmente 20 milisegundos. De hecho, los reproductores multimedia sincronizan los streams de audio y vídeo empleando el audio como reloj maestro. Nosotros vamos a hacer lo mismo.

Dado que generamos bloques de audio de 20 milisegundos, en lugar de intentar cronometrar nosotros mismos el retardo vamos a emplear el evento de la tarjeta de sonido, enviándole bloques de 20 milisegundos y “congelando” la emulación hasta que nos avise que ya se han terminado de reproducir. Obviamente, tenemos el problema de que cuando “termine de reproducirlos” vamos a tardar un poco en generar otros 20 milisegundos de audio, así que para solucionarlo vamos a utilizar tres bufferes de 20 milisegundos que enviaremos al sistema de audio de golpe. A medida que se vayan consumiendo, tendremos 40 milisegundos de audio todavía pendiente de reproducir para seguir emulando instrucciones y alimentando al sistema de sonido. El resultado? Audio de bajísima latencia sin cortes ni chasquidos.

Cualquiera que haya programado el sistema de audio de Windows sabe las complicaciones que conlleva la reserva de los bloques de memoria, las cabeceras, abrir y cerrar los dispositivos de audio… Vamos a hacerlo encapsulando toda la funcionalidad en una sola clase (bueno, en un par) que nos evite perder el enfoque en nuestro principal problema, que es la emulación. Aquí va el código completo del sistema de audio:

#pragma comment(lib,"WinMM.lib")

template<unsigned int sampleRate> struct AudioBlock
{
  WAVEHDR Header;
  short AudioData[sampleRate / 50];
};

#define   AUDIOBLOCK_COUNT      (3)

template<unsigned int sampleRate> class WaveOutAudioManager
{
public:
  WaveOutAudioManager()
  {
    hWaveDev = NULL;
    hWaveEvent = INVALID_HANDLE_VALUE;
  }
  virtual ~WaveOutAudioManager()
  {
    Close();
  }

public:
  int Open()
  {
    if (hWaveDev != NULL)
      return(ERROR_INVALID_STATE);

    if (hWaveEvent != INVALID_HANDLE_VALUE)
      CloseHandle(hWaveEvent);
    hWaveEvent = CreateEvent(NULL,FALSE,FALSE,NULL);

    WAVEFORMATEX wf;
    ZeroMemory(&wf,sizeof(wf));
    wf.cbSize = sizeof(wf);
    wf.wFormatTag = WAVE_FORMAT_PCM;
    wf.nChannels = 1;   // mono audio
    wf.nSamplesPerSec = sampleRate;
    wf.nAvgBytesPerSec = sampleRate * sizeof(short);
    wf.nBlockAlign = sizeof(short);
    wf.wBitsPerSample = 8 * sizeof(short);
    int result = waveOutOpen(&hWaveDev,WAVE_MAPPER,&wf,(DWORD_PTR)hWaveEvent,NULL,CALLBACK_EVENT);
    if (result != 0)
    {
      Close();
      return(result);
    }

    // Prepare audio buffers. Each one will be 20ms long
    for (int dd=0;dd<AUDIOBLOCK_COUNT;dd++)
    {
      ZeroMemory(&audioBlocks[dd],sizeof(AudioBlock<sampleRate>));
      audioBlocks[dd].Header.lpData = (LPSTR)&audioBlocks[dd].AudioData[0];
      audioBlocks[dd].Header.dwBufferLength = sizeof(audioBlocks[dd].AudioData);
      result = waveOutPrepareHeader(hWaveDev,&audioBlocks[dd].Header,sizeof(audioBlocks[dd].Header));
      if (result != MMSYSERR_NOERROR)
      {
        Close();
        return(result);
      }
    }

    // Everything ready...
    return(0);
  }

  void Close()
  {
    if (hWaveDev != NULL)
    {
      waveOutReset(hWaveDev);
      waveOutClose(hWaveDev);
      hWaveDev = NULL;
    }

    if (hWaveEvent != INVALID_HANDLE_VALUE)
    {
      CloseHandle(hWaveEvent);
      hWaveEvent = INVALID_HANDLE_VALUE;
    }
  }

protected:
  AudioBlock<sampleRate> *FindAvailableBlock()
  {
    for (int dd=0;dd<AUDIOBLOCK_COUNT;dd++)
    {
      if ((audioBlocks[dd].Header.dwFlags & WHDR_INQUEUE) == 0)
        return(&audioBlocks[dd]);
    }
    return(NULL);
  }

public:
  bool QueueAudio(short* audioData)
  {
    // Check if any block is available right now
    AudioBlock<sampleRate> *block = FindAvailableBlock();
    // If no audio block is available, just wait for one (max 20ms)
    if (block == NULL)
    {
      WaitForSingleObject(hWaveEvent,1000);   // Wait for 1 second max
      block = FindAvailableBlock();
    }

    // This is weird... something really strange happened.
    if (block == NULL)
      return(false);

    // Fill block with audio data
    for (int dd=0;dd<sampleRate/50;dd++)
      block->AudioData[dd] = audioData[dd];
    // and send again to audio subsystem
    int result = waveOutWrite(hWaveDev,&block->Header,sizeof(block->Header));
    return(result == MMSYSERR_NOERROR ? true : false);
  }

protected:
  HANDLE hWaveEvent;
  HWAVEOUT hWaveDev;
  AudioBlock<sampleRate> audioBlocks[AUDIOBLOCK_COUNT];
};

La clase WaveOutAudioManager<> inicializa el sistema de sonido y tres bufferes (de 20 milisegundos de audio). La función que nos interesa es QueueAudio(), que recibe 20 milisegundos de audio desde el bucle principal de la emulación. Si encuentra bufferes disponibles (que no estén en ejecución) la función envía el bloque al sistema de audio para su reproducción y vuelve de inmediato. Si todos los bufferes están encolados, la función espera pacientemente a que el sistema de audio nos notifique por medio de un evento que uno de los bloques se ha completado, encolando un nuevo bloque.

Para los bufferes y cabeceras empleo una clase template (AudioBlock<>)que agrega las dos estructuras como una sola, simplificando enormemente el código y haciendo innecesario el uso de reserva dinámica de memoria.

Volviendo ahora a la función principal de emulación (emulatorMain), el código queda así:

  void Spectrum::emulatorMain()
  {
    ...
    // Native audio driver
    WaveOutAudioManager<AUDIO_SAMPLE_RATE> audMgr;

    ...

    audMgr.Open();

    // Ready to roll!!
    do
    {
      // Tape load trap
      if ((cpu.regs.PC == 0x056B) && (OnLoad))
      {
         .... 
      }

      // And emulate next instruction
      cpu.tStates = 0;
      cpu.EmulateOne();

      // After each instruction, report the ULA the number of cycles we've used
      bool irq = false;
      ula.AddCycles(cpu.tStates,irq);

      // As in the real Spectrum, the ULA will trigger an IRQ for every frame. This
      // implementation uses cpu clock cycles to know where the screen beam is.
      if (irq)		// Ula signals a frame interrupt
      {
        cpu.INT();		// Generate system interrupt

        // If screen contents have been modified, set a flag for the WPF rendering event.
        if (ula.IsDirty)
        {
          screenDataDirty = true;
          ula.IsDirty = false;
        }

        // Submit audio to driver. The "driver" will hold the loop until an audio buffer
        // is available - that is, 20 ms. This will match the speed of our emulated
        // Spectrum to the real one.
        audMgr.QueueAudio(ula.FrameAudio);
      }
    } while(quitEmulation == false);

    pCurrentUla = nullptr;
    pCurrentKempston = nullptr;
  }

SpectrumSaving

El resultado? Probad simplemente a arrancar el emulador, escribir un programa de un par de líneas y salvarlo con SAVE “test”… veréis las familiares líneas en el borde de la pantalla, junto con el igualmente familiar sonido de la grabación, ese sonido que nos acompañó durante largas tardes de juego. Pero si queréis ver realmente de lo que era capaz el Spectrum, os recomiendo como un ejemplo absolutamente encantador la música de Alter Ego y de lo que se puede hacer un audio “de un solo bit”.

AlterEgo

Pues creo que eso es todo…

Nuestro emulador está completo, y tenemos un flamante ZX Spectrum de 48KB, capaz de generar sonido (y recuerdos) y un estupendo joystick que podéis adaptar a todas vuestras necesidades. En el último capítulo de este tutorial daremos los últimos toques a la emulación… y podréis descargar el código fuente completo de la misma.

Enlaces:

Emulador YASS para Windows
Código fuente del Emulador YASS

– Hazte un Spectrum: 1ª Parte, 2ª Parte, 3ª Parte, 4ª Parte, 5ª Parte, 6ª Parte, 7ª Parte, 8ª Parte

Categorías:Desarrollo, Tutorial Etiquetas: , , , , ,

Hazte un Spectrum (6ª Parte)

Queda poco para completar un emulador de Sinclair ZX Spectrum, y hoy nos vamos a dedicar a emular una cinta de cassette para poder volver a disfrutar de esos videojuegos de los ‘80.

A ver, pregunta: cuántos de vosotros no recordabais o incluso sabíais que se usaban cintas de cassette como medio de almacenamiento masivo en los ordenadores personales de los ‘80? Yo me he dado cuenta de lo raro que suena hoy cuando al escribir “cassette” el corrector ortográfico se ha quejado porque no reconoce la palabra… Hoy vamos a darle a nuestro emulador soporte para leer el formato más habitual: los archivos .TAP.

Nuevamente, y gracias a la información encontrada en WorldOfSpectrum.org he obtenido todo lo necesario para implementarlo. Lo primero, un poco de teoría…

Usar cintas de cassette como almacenamiento

Cualquier ordenador que use cintas de audio para almacenar o recuperar programas lo que realmente hace es generar sonidos con ciertos patrones que luego puede reconocer al reproducir la señal. En el caso del ZX Spectrum éste formato es totalmente conocido y se genera (cómo no) con el chip ULA. El chip ULA puede generar sonidos por dos vías diferentes, pero con exactamente las mismas herramientas: un sólo bit (0 ó 1) se emplea para generar una señal de alto nivel (1) o bajo nivel (0) en una salida de audio. Hay dos bits en el registro de I/O de la ULA, uno genera una señal más amplia que la otra. La primera se emplea para activar el altavoz interno del ordenador, mientras que la otra se envía al conector MIC que se enchufa a la cinta de cassette. Realmente las dos señales están unidas, siendo la única diferencia que la salida de la unidad de cassette pasa por una resistencia para atenuar la señal.

Para grabar datos en la cinta basta con modificar la señal de audio a 1 y a 0 contando ciclos entre cada “flanco”. Por ejemplo, el tono de aviso previo a los datos de una cabecera consiste en 8063 pulsos (de 0 a 1 y vuelta a 0) con una duración de 2168 ciclos (tStates) entre flancos. Los bits se codifican igualmente con pulsos: un bit a “0” se codifica como dos pulsos de 855 ciclos entre flancos y un bit a “1” se codifica como dos pulsos de 1710 ciclos entre flancos. La recuperación es igual de simple: la señal de audio entra al chip ULA, que pone un bit a 0 o a 1 dependiendo del nivel de la señal. Los programas en la ROM del Spectrum simplemente leen el bit y calculan el tiempo que la señal está en “alto” o en “bajo”.

El formato .TAP reproduce exactamente el formato binario de los bloques que genera la ROM del Spectrum, sin las señales de sincronismo. Simplemente añade dos bytes antes de cada uno de ellos para poder recuperarlos de un archivo secuencial.

Las rutinas en ROM del Spectrum graban la información como dos bloques separados. El primer bloque contiene una cabecera y mide 17 bytes. El bloque que sigue contiene la información propiamente dicha. Aprovechando el ejemplo más documentado del mundillo (grabar en cinta los dos primeros bytes de la ROM de un Spectrum con SAVE “ROM” CODE 0,2), un ZX Spectrum grabaría los siguiente:

– 8063 pulsos de 2168 ciclos (tStates) cada uno para la señal de aviso de cabecera.
– Un pulso de sincronización de 667 ciclos.
– Un segundo pulso de sincronización de 735 ciclos.
– Un bloque de datos de 19 bytes con la cabecera: un byte con flags, más los datos de la cabecera que incluye tipo de bloque, longitud, ubicación en memoria, etc, y finalmente un byte de suma de control.

Tras una breve pausa, se grabarían los datos propiamente dichos:

– 3223 pulsos de 2168 ciclos para la señal de aviso de cabecera.
– Un pulso de sincronización de 667 ciclos.
– Un segundo pulso de sincronización de 735 ciclos.
– Un bloque de datos con un byte con flags, los dos bytes de datos y un byte con la suma de control del bloque.

Un fichero .TAP equivalente contendría lo siguiente:

– Dos bytes (0x13+0x00 == 0013h ó 19 decimal) con la longitud del bloque
– Los 19 bytes grabados físicamente en la cinta (flags+datos+suma de control)
– Dos bytes (0x03+0x00 == 0004h) con la longitud del bloque.
– Los 4 bytes grabados físicamente en la cinta (flags+datos+suma de control)

Parece obvio que los archivos .TAP contienen los datos grabados por la ROM del Spectrum “tal cual”… no sería posible leerlos de la misma forma?

Interceptando la ROM

Pues realmente sí. Tras leer bastante al respecto y gracias a la inestimable (e imprescindible) ayuda del libro “The Complete ZX Spectrum ROM Dissasembly” que encontraréis (cómo no) en WorldOfSpectrum.org/documentation.html, vamos a interceptar el curso normal de ejecución del código de la ROM y esperar tranquilamente hasta que ésta llegue al punto donde se leen bloques de cinta.

No hay que buscar mucho (nuevamente gracias a la ingente cantidad de información disponible). La función que queremos interceptar está a partir de la posición 056Bh de la ROM y su misión es leer un bloque (tanto de cabecera como de datos propiamente dicho) desde la cinta. Esta función (documentada como “LD-BREAK” en el desensamblado) es el primer (y frontal!!) paso en la lectura de un bloque.

Como la idea es hacer el emulador programable, en lugar de implementar los archivos .TAP directamente en el control lo vamos a implementar como un delegado normal y corriente de .NET. Cada vez que la ROM necesite un bloque de datos, la función que hayamos asignado a dicho delegado (escrita en C#) recibirá la petición y podrá devolver los contenidos. El cambio en la clase es mínimo:

namespace Emulation
{
  public delegate array<Byte>^ LoaderHandler(int bytesToLoad);

  public ref class Spectrum : System::Windows::Controls::Border, IDisposable
  {
  public:
    LoaderHandler^ OnLoad;

   ....

La firma del delegado es la de una función que recibe un número de bytes a cargar y que devuelve un array de bytes con los datos. Ahora vamos a utilizar al delegado desde la emulación.

Dado que cada bucle que ejecuta el emulador consiste en una instrucción de la cpu Z80, para saber si hemos llegado al punto crítico para interceptar la carga basta con comparar el valor del registro PC de la cpu con la dirección que nos interesa antes de emularla:

    // Ready to roll!!
    do
    {
      // Tape load trap
      if ((cpu.regs.PC == 0x056B) && (OnLoad))
      {
        try
        {
          LoadTrap(cpu);
          // set up registers for success
          cpu.regs.BC = 0xB001;
          cpu.regs.altAF = 0x0145;
          cpu.regs.CF = 1;		// Carry flag set: Success
        }
        catch(Exception^)
        {
          // set up registers for failure
          cpu.regs.CF = 0;		// Carry flag reset: Failure
        }
        cpu.regs.PC = 0x05e2;	// "Return" from the "tape block load" routine
      }

      // And emulate next instruction
      cpu.tStates = 0;
      cpu.EmulateOne();
      ...

Fácil, no? Justo antes de emular cada instrucción comprobamos si el registro PC (“Program Counter”) indica que hemos llegado a la dirección del programa de carga. Si es así y el delegado está inicializado en el emulador, invocamos una pequeña función que hará el trabajo sucio: pedir los datos y cargarlos en la memoria del Spectrum. El uso de C++ gestionado hace fácil hablar los dos “idiomas” (.NET y nativo) al mismo tiempo:

void Spectrum::LoadTrap(Z80& cpu)
{
  // Call the delegate to obtain the next tape block
  array<Byte>^ data = OnLoad(cpu.regs.DE);

  // First byte of data contains value for the A register on return. Last
  // byte is blocks checksum (not using it).
  int nBytes = data->Length-2;
  if (cpu.regs.DE < nBytes)
    nBytes = cpu.regs.DE;

  // We must place data read from tape at IX base address onwards
  // DE is the number of bytes to read, IX increments with each byte read.
  for (int dd=0;dd<nBytes;dd++)
  {
    // Write block using cpu's data bus and cpu's registers...
    cpu.DataBus->Write(cpu.regs.IX++,data[dd+1]);
    cpu.regs.DE--;
  }
}

La implementación es tremendamente simple, pero funciona. Si la función delegado devuelve un array de bytes (lo esperado), simplemente usamos los buses del procesador (y sus registros!!) para ir escribiendo los contenidos en la memoria del Spectrum. Cualquier excepción en la función será interpretada como un error que llegará al Spectrum emulador como un “error de cinta”. Una vez completado el trabajo, volvemos a colocar el registro PC de la cpu a la dirección de programa donde todo debería continuar normalmente de emplear una unidad de cinta “de verdad”.

Ficheros .TAP en C#

La parte pesada de todo ésto (la manipulación de los archivos y sus bloques propiamente dicha) la vamos a implementar en C#, porque precisamente para éstas cosas es el lenguaje ideal. La implementación es como sigue:

  class TapBlock
  {
    public TapBlock(BinaryReader stream)
    {
      // A TAP block consists of a two byte header plus data bytes.
      // Data is raw data as saved by Spectrum - and as expected
      // by ROM routines we are replacing.
      UInt16 size = stream.ReadUInt16();
      data = stream.ReadBytes(size);
    }

    public Byte[] Data
    {
      get
      {
        return (data);
      }
    }

    protected byte[] data = null;
  }

  class TapFile
  {
    public void Open(String fileName)
    {
      System.Collections.Generic.List<TapBlock> blockCol = new System.Collections.Generic.List<TapBlock>();

      try
      {
        using (var file = File.OpenRead(fileName))
        {
          var stream = new BinaryReader(file);
          while (stream.BaseStream.Position < stream.BaseStream.Length)
          {
            var newBlock = new TapBlock(stream);
            blockCol.Add(newBlock);
          }
        }
      }
      catch (Exception) { }

      blocks = blockCol.ToArray();
    }

    public TapBlock[] Blocks
    {
      get
      {
        return (blocks);
      }
    }

    protected TapBlock[] blocks = null;
  }

Realmente no es más que eso: un objeto TapFile abre el archivo en disco y lee todos los bloques, añadiéndolos a una colección de objetos TapBlock que obtenemos de su propiedad Blocks. Cada bloque simplemente contiene los datos en su miembro Data.

Cómo no, lo siguiente es que nuestro emulador pueda leer alguna cinta y empezar a hacer cosas verdaderamente divertidas con nuestro Spectrum. Para ello, vamos a añadir un menú a nuestro emulador con la opción “Load”. En el archivo MainWindow.xaml añadimos lo siguiente a nuestra ventana:

  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition/>
    </Grid.RowDefinitions>
    <Menu IsMainMenu="True" Grid.Row="0">
      <MenuItem Header="File">
        <MenuItem Header="Load tape..." Click="OnSelectTape"/>
      </MenuItem>
    </Menu>
    <zx:Spectrum x:Name="emulator" Grid.Row="1" />
  </Grid>

Hemos añadido un simple menú con la opción de leer cintas. Ahora en el archivo de código MainWindow.xaml.cs añadimos lo siguiente:

  public partial class MainWindow : Window
  {
    TapFile currentTape = null;
    int currentTapeBlock = 0;
    ...

    private void OnSelectTape(object sender, RoutedEventArgs e)
    {
      FileDialog dialog = new OpenFileDialog();
      dialog.AddExtension = true;
      dialog.CheckFileExists = true;
      dialog.CheckPathExists = true;
      dialog.Filter = "Tape files (*.tap)|*.tap";
      dialog.DefaultExt = "tap";
      bool? result = dialog.ShowDialog(this);
      if (result == null)
        return;
      if ((bool)result == false)
        return;

      try
      {
        var tape = new TapFile();
        tape.Open(dialog.FileName);
        currentTape = tape;
        currentTapeBlock = 0;
      }
      catch (Exception)
      {
        MessageBox.Show("Unable to load tape file");
      }
    }

En el delegado OnSelectTape() (invocado al selecciona la opción de menú) creamos una instancia de la clase TapFile que obtiene los contenidos del archivo y la asignamos a una variable que leeremos bloque a bloque a medida que el emulador nos lo vaya pidiendo. Sólo queda activar la intercepción de la carga de bloques:

  public partial class MainWindow : Window
  {
    public MainWindow()
    {
      InitializeComponent();

      // Set up load traps
      emulator.OnLoad = TapeLoad;

      emulator.Start();
    }

    protected Byte[] TapeLoad(int bytesToLoad)
    {
      TapBlock currentBlock = currentTape.Blocks[currentTapeBlock++];
      return (currentBlock.Data);
    }

En el constructor conectamos el delegado del emulador a nuestra implementación (la función TapeLoad()). La función intentará leer los bloque secuencialmente (empleando la variable currentTapeBlock como índice) y devuelve directamente el array de bytes al control.

Lo siguiente es una nueva visita (cómo no!) al inmenso archivo de programas de WorldOfSpectrum y descargar nuestros juegos favoritos. En dos clicks encontramos el archivo TAP de Head Over Heels. Vamos a probar el emulador!!

A jugar!

Una vez arrancado el emulador, abrimos el menú y seleccionamos la opción de cargar un archivo. Simplemente buscamos la ubicación del archivo TAP y lo abrimos.

LoadingFirstTape

El hecho de cargar una cinta en el programa no da ninguna indicación al emulador de que la lea – al igual que en un Spectrum real, tras insertar la cinta de cassette requiere que el usuario inicie la carga de la misma. Tecleamos LOAD “” en el emulador (tecla “J” para “LOAD” y CTRL+P para las comillas) y el emulador se pondrá a cargar la cinta que le hemos preparado:

FirstLoad

Tras pulsar retorno de carro, nuestro emulador pedirá bloque a bloque los contenidos de la cinta… Y el resultado es éste:FirstGame

Jugar con el emulador es divertido, pero he perdido (hace mucho!!) la costumbre de usar el teclado y más todavía empleando las combinaciones de teclas que se usan en el Spectrum. Como remate final a nuestro emulador, en el próximo capítulo vamos a añadir lo que creo que pueden ser los dos únicos componentes que quedan para completarlo: la emulación de un joystick Kempston y el sonido! Y no nos vamos a quedar en emular el “beep” tradicional del Spectrum, sino que vamos a soportar (y emular!) los programas que generaban audio multicanal usando un solo bit: el audio PWM.

Enlaces:

Emulador YASS para Windows
Código fuente del Emulador YASS

– Hazte un Spectrum: 1ª Parte, 2ª Parte, 3ª Parte, 4ª Parte, 5ª Parte, 6ª Parte, 7ª Parte, 8ª Parte

Categorías:Desarrollo, Tutorial Etiquetas: , , , , ,

Hazte un Spectrum (5ª Parte)

En el capítulo anterior conseguimos arrancar por primera vez nuestro nuevo emulador de Spectrum, pero sin ninguna forma de recibir entradas del mundo exterior su utilidad es francamente nula. Hoy implementaremos la simulación del teclado y las temporizaciones de la máquina.

La implementación del teclado tiene dos partes realmente: la primera, el interface hardware entre la cpu y las teclas, misión encargada (obviamente) al chip ULA. Por otra parte, el Spectrum necesita un reloj que periódicamente le haga “visitar” el teclado y leerlo. Todos los ordenadores necesitan este tipo de relojes y la mayoría incluyen relojes internos programables que facilitan la tarea. En los ‘80 éste no era el caso y la mayoría empleaban la temporización de la pantalla para sincronizar sus operaciones. Y cuando digo “pantalla” me refiero a “televisor” y su conocido refresco de 50 cuadros por segundo.

El chip ULA del Spectrum es el responsable (como ya hemos visto) de generar la imagen que aparece en la pantalla. Para hacerlo, el chip ULA cuenta meticulosamente ciclos de la máquina para determinar dónde está el cañón del televisor en cada momento. Durante unas cuantas líneas (el borde superior de la pantalla) el chip simplemente genera el color del borde y periódicamente la señal de sincronismo horizontal para que el cañón vuelva al flanco izquierdo de la pantalla. Cuando la ULA determina que ha llegado a la zona de pantalla propiamente dicha, comienza a leer la memoria de vídeo y convertir los contenidos en imágenes durante 256 píxeles, luego genera un poco más de borde, un sincronismo horizontal, un poco más de borde (en el lado izquierdo) y luego otra línea de pantalla, así hasta que completa las 192 líneas. FInalmente, vuelve a generar color de borde hasta que se completa el cuadro de pantalla (312 líneas) y genera la señal de sincronismo vertical, para que le haz de electrones del cañón vuelva a la esquina superior izquierda de la pantalla. Y esto, 50 veces por segundo. Es en éste momento cuando el chip ULA envía una señal a la cpu (la señal INT) que interrumpe el proceso de la cpu cada cuadro y que el Spectrum emplea para sincronizar sus procesos.

En nuestra emulación no existe un cañón de electrones de una televisión, ni nada que se le parezca, pero necesitamos simular esa interrupción 50 veces por segundo. Podríamos simularla con un simple temporizador, pero su precisión sería desastrosa. Os recuerdo que muchos juegos del Spectrum hacían simpáticos efectos con el borde de la pantalla (cambiando el color muchas más veces que 50 por segundo). También quiero simular ese efecto.

Afortunadamente, el chip ULA emplea los mismos relojes que la cpu Z80 y los usa para cronometrar sus operaciones. El chip ULA empieza a contar ciclos de reloj y considera que los 16384 primeros ciclos son el borde superior de la pantalla, justo hasta el borde izquierdo del contenido de pantalla de la primera línea de visualización. Luego cada línea son 224 ciclos exactos: 128 ciclos para presentar los 256 píxeles de pantalla (esto es, 2 píxeles por cada ciclo), mas 96 ciclos para el borde derecho, el sincronismo horizontal y el borde izquierdo de la siguiente línea. En total, cada fotograma de pantalla son 224 ciclos por 312 líneas, esto es, 69888 ciclos (o tStates) y si multiplicáis esta cifra por 50 fotogramas por segundo obtenemos 3494400 ciclos (los famosos 3.5 MHz a los que funciona el Spectrum). De hecho, el chip ULA realmente funciona a 3.5 MHz, lo cual resulta en una visualización en pantalla de 50.08 cuadros por segundo… afortunadamente, a ningún televisor le afecta esta diferencia.

Así que vamos a implementar la función de la ULA que cuenta los ciclos de reloj a medida que avanza la visualización, en la función AddCycles():

#define   TSTATES_PER_SCANLINE   (224)
#define   TVSCANLINES            (312)
#define   TSTATES_PER_FRAME      (TSTATES_PER_SCANLINE * TVSCANLINES)

void AddCycles(unsigned int cycles,bool& IRQ)
{
  dwFrameTStates += cycles;
  dwScanLineTStates += cycles;

  if (dwScanLineTStates > TSTATES_PER_SCANLINE)
    ScanLine(IRQ);
}

Empleamos dos contadores porque es más simple que hacer operaciones sobre uno solo, y mantenemos un contador de líneas y un contador de cuadros. Cuando detectamos que se ha completado una línea de visualización completa, invocamos a la función ScanLine():

void ScanLine(bool &IRQ)
{
  dwScanLineTStates %= TSTATES_PER_SCANLINE;
  dwScanLine++;
  if (dwScanLine >= TVSCANLINES)
  {
    // Frame complete - trigger IRQ
    IRQ = true;
    dwFrameTStates %= TSTATES_PER_FRAME;
    dwScanLine = 0;
    if (++dwFrameCount > 16)    // each 16 full frames...
    {
      dwFrameCount = 0;         // reset counter...
      blinkState = !blinkState; // ... invert blink ...
      UpdateBlink();            // ... and update screen contents
    }
  }

  // Check if current scanline (0-311) lies within the "visible portion"
  // of screen bitmap (lines 0-239), i.e., skip first and last 36 lines (front/back porches)
  if ((dwScanLine < 36) || (dwScanLine > (239+36))) return;

  // Now check if current background color for scanline is different
  // from the required one
  DWORD bitmapLine = dwScanLine-36;
  if (dwCurrentScanLineBackColor[bitmapLine] == dwBorderRGBColor)  // Already right color
    return;

  // Redraw current scanline with proper color
  LPDWORD scr = nativeBitmap;
  scr += (bitmapLine * 320);	// Get pointer to start of line

  // top/bottom borders?
  if ((bitmapLine < 24) || (bitmapLine > 24+191))
  {
    // Draw whole line
    unsigned int bCount = 320;
    do
    {
      *scr++ = dwBorderRGBColor;
    }while (--bCount);
    }
  else
  // screen contents area
  {
    // First 32 pixels
    unsigned int bCount = 32;
    do
    {
      *scr++ = dwBorderRGBColor;
    } while (--bCount);

    // Last 32 pixels
    scr += 256;
    bCount = 32;
    do
    {
      *scr++ = dwBorderRGBColor;
    } while (--bCount);
  }

  // and remember this scanline's colour
  dwCurrentScanLineBackColor[bitmapLine] = dwBorderRGBColor;
  IsDirty = true;
}

La función realiza tres operaciones: cuenta los ciclos de reloj tanto para la línea en curso como para la pantalla completa, y si determina que ésta se ha dibujado totalmente indica que es necesario invocar la interrupción del procesador poniendo a verdadero la variable IRQ. También cuenta bloques de 16 fotogramas para hacer parpadear los caracteres de pantalla que tengan el atributo “blink”. Por último la función comprueba si ha cambiado el color del borde y repinta cada línea de la pantalla (si es que ha cambiado) con el nuevo color.

Para añadir la interrupción, basta un pequeño cambio en la función emulatorMain() que ya vimos en un capítulo anterior:

    DWORD dwFrameStartTime = GetTickCount();
    do
    {
      // Emulate next instruction
      cpu.tStates = 0;
      cpu.EmulateOne();

      // After each instruction, report the ULA the number of cycles we've used
      bool irq = false;
      ula.AddCycles(cpu.tStates,irq);

      // As in the real Spectrum, the ULA will trigger an IRQ for every frame. This
      // implementation uses cpu clock cycles to know where the screen beam is.
      if (irq)		// Ula signals a frame interrupt
      {
        cpu.INT();		// Generate system interrupt

        // If screen contents have been modified, set a flag for the WPF rendering event.
        if (ula.IsDirty)
        {
          screenDataDirty = true;
          ula.IsDirty = false;
        }

        // The PC executes code a lot faster than the original Z80.
        // As we now that "20ms" (1 frame) have ellapsed, pause execution to
        // match host PC and emulated computer
        DWORD dwNow = GetTickCount();
        DWORD dwEllapsed = dwNow - dwFrameStartTime;
        if (dwEllapsed < 20)    // Running above real time?
        {
          Sleep(20 - dwEllapsed);
          dwNow = GetTickCount();
        }
        dwFrameStartTime = dwNow;
      }
    } while(quitEmulation == false);

Utilizando el contador de ciclos de la clase Z80 vamos actualizando la ULA para que mantenga la posición de pantalla. Si AddCycles() activa la variable local que le pasamos como referencia (irq), simplemente invocamos el método INT() del Z80 para que dispare una interrupción. Fijaos que también podemos aprovecharnos de esta característica para hacer que nuestro emulador corra más o menos a la misma velocidad que un Spectrum: dado que sabemos que la máquina ha estado corriendo durante un cuadro de pantalla completo (1/50 de segundo = 20 milisegundos), y que nosotros sabemos cuanto tiempo hemos empleado en hacerlo (dwFrameStartTime – dwNow), basta con hacer un Sleep() del tiempo restante hasta cumplir los 20 milisegundos. No es exacto, pero el efecto es muy, muy aproximado.

Finalmente, añadimos una variable (screenDataDirty) para saber si los contenidos de pantalla han cambiado o no y optimizar la función de refresco de WPF (y no repintar cada fotograma completo como hasta ahora):

void Spectrum::OnRenderTick(Object^ sender,EventArgs ^e)
{
  if (screenDataDirty)
  {
    emulatorScreen->Lock();
    emulatorScreen->AddDirtyRect(Int32Rect(0,0,320,240));
    screenDataDirty = false;
    emulatorScreen->Unlock();
  }
}

Ya tenemos interrupciones! Ahora viene la parte en la que emulamos el hardware de teclado de la ULA.

8 ó 16 bits?

Al que se haya fijado, comprobará como el bus de I/O que hemos definido para nuestro procesador Z80 es de 16 bits. En cambio, y según la documentación de Zilog, el bus de direcciones de I/O del procesador real es de sólo 8 bits. Y esto? Bueno, no tengo documentación de la época y afortunadamente el datasheet actualizado lo describe, pero es que realmente cuando el procesador hace una operación de I/O coloca direcciones en los 16 bits de direcciones, aprovechando el contenido de otros registros del procesador.

Cuando leemos un puerto con una instrucción como IN A,(C), el procesador realmente coloca en el bus de direcciones el contenido completo (16 bits) del registro BC, y no sólo la parte baja del mismo (C) como cabría esperar. De igual forma, cuando hacemos un IN A,#puerto, el procesador tiene la necesidad de colocar “algo” en los 8 bits más significativos del bus de direcciones, y coloca los contenidos del acumulador (registro A) porque es lo que tiene más a mano. Por ejemplo, las instrucciones LD A,#14; IN A,$FE realmente leen el puerto de I/O $14FE. Y lo más curioso es que el Spectrum hace uso de esta característica para leer el teclado.

El chip ULA sólo utiliza una línea de direcciones para saber si están hablando con él: el bit de dirección 0 del bus. Esto es, cualquier dirección par de I/O accede a la ULA. Por convención, siempre se usa la dirección 0xFE, pero cualquier dirección par obtiene el mismo resultado. Y para leer el teclado aprovecha los 8 bits superiores de la dirección de I/O para saber qué sección del teclado deseamos leer.

El teclado del Spectrum

Ahora veamos cómo se conecta el teclado al resto del sistema. Las 40 teclas del Spectrum (4 filas de 10 teclas) están divididas en 8 bloques de cinco teclas cada uno:SpectrumKeyboard

Para leer una fila del teclado, hay que leer el puerto de I/O de la ULA (0xFE) colocando en el byte más significativo la fila en la que estamos interesados. La fila se codifica poniendo a “cero” el bit que represente su posición. Por ejemplo, para leer la fila “0” (teclas Shift, Z, X, C y V) se coloca “11111110” (0xFE), mientras que para leer la fila “5” (con las teclas Y, U, I, O y P) se coloca “11011111” (o lo que es lo mismo: 0xDF) en la parte alta de la dirección del puerto. Tras cada lectura, el chip ULA devuelve el estado de las cinco teclas correspondientes a esa fila (los cinco bits menos significativos) estando a cero el bit que represente una tecla pulsada. Fijaos también que la ordenación de los bits de las semilíneas 0 a 3 es la inversa que para las semifilas 4 a 7.

Para la implementación del teclado del emulador, voy a seguir el mismo patrón: un array de 8 bytes que representa las ocho “semifilas” de teclas y que podremos manipular desde el exterior para simular las pulsaciones:

protected:
  unsigned char keyMatrix[8];

public:
  void PressKey(unsigned int keyRow,unsigned int keyCol,bool down)
  {
    if (keyCol > 9)
      return;
    if (keyRow > 3)
      return;

    // Spectrum keyboard layout
    //       D0 D1 D2 D3 D4    D4 D3 D2 D1 D0
    // A11      ROW3              ROW4          A12
    // A10      ROW2              ROW5          A13
    //  A9      ROW1              ROW6          A14
    //  A8      ROW0              ROW7          A15

    int rowNdx;
    int bitMask;
    if (keyCol < 5)   // Left bank
    {
      rowNdx = 3 - keyRow;
      bitMask = 0x01 << keyCol;
    }
    else              // Right bank
    {
      rowNdx = 4 + keyRow;
      bitMask = 0x01 << (9 - keyCol);
    }

    if (down)
      keyMatrix[rowNdx] |= bitMask;
    else
      keyMatrix[rowNdx] &= ~bitMask;
  }

La función recibe las coordenadas de una tecla en el formato físico del Spectrum como “fila/columna”, y modifica el bit correspondiente a la configuración física por medio de unas simples operaciones, dependiendo de si la tecla ha sido pulsada (down == TRUE) o liberada (down == FALSE). El array keyMatrix queda totalmente configurado para poder leerlo muy rápidamente (y con la misma facilidad que en el caso real) cuando el código del Spectrum lo necesite.

Las peticiones de lectura del teclado llegarán a la ULA desde el procesador por medio de la función IORead(), que hasta ahora no hacía nada salvo devolver 0xFF (valor habitual en buses vacíos):

  unsigned char IORead(unsigned int address)
  {
    // ULA is selected by A0 being low
    if ((address & 0x01) == 0)
    {
      // Get the scancodes
      unsigned char kData = 0xFF;   // Pull ups
      unsigned char row = (address >> 8) ^ 0xFF;
      for (int dd=0;dd<8;dd++)
      {
        if (row & (1 << dd))        // Select scanline?
          kData &= ~keyMatrix[dd];  // pull down bits representing "pressed" lines
      }
      return(kData);
    }
    return(0xFF);
  }

La función decodifica la dirección de I/O de la misma forma que el chip ULA real: viendo si el bit menos significativo de la dirección es “0”. En tal caso, recorre el array de filas, comprobando si la posición coincide con la máscara requerida. Si es así, limpia los bits correspondientes a las teclas pulsadas en la fila. La función recorre las ocho filas de teclas: si se solicita un identificador de fila con varios bits a cero, simplemente combina las teclas pulsadas en todas las filas seleccionadas.

Publicando el API del teclado

Dado que nuestro emulador de Spectrum es un control, necesitamos publicar funciones que nos permitan interactuar con él desde nuestra aplicación host. Dado que la instancia de la clase ULA es interna a la función emulatorMain() (y que hace todo el trabajo), vamos a publicar un puntero en nuestra clase que nos permita acceder a la instancia desde el exterior. Nada más simple: en la declaración de la clase Spectrum añadimos lo siguiente:

private:
  ULA *pCurrentUla;

y en la función emulatorMain()

void Spectrum::emulatorMain()
{
  // Components
  ...  
  ULA ula;     // Spectrum's ULA chip + 16KB
  ...

  pCurrentUla = &ula;
  ...

Cuando la emulación termina la función limpia la variable. Ahora, nada más fácil ahora que implementar dos funciones públicas (PressKey() y ReleaseKey()) que nos permitan simular las pulsaciones de teclas desde la aplicación host:

public:
  void Spectrum::PressKey(int keyRow,int keyCol)
  {
    if (pCurrentUla != nullptr)
      pCurrentUla->PressKey(keyRow,keyCol,true);
  }

  void Spectrum::ReleaseKey(int keyRow,int keyCol)
  {
    if (pCurrentUla != nullptr)
      pCurrentUla->PressKey(keyRow,keyCol,false);
  }

Terminemos añadiendo la emulación de teclado a nuestra aplicación host en C#. Sobrecargando las funciones OnKeyDown() y OnKeyUp() de la clase base, podemos pulsar o liberar las teclas del Spectrum. Aquí sigue un fragmento de la función OnKeyDown() – como podréis imaginar la función OnKeyUp() es básicamente la misma, solo que invocando a la función ReleaseKey() del control:

protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)
{
   base.OnKeyDown(e);
   switch (e.Key)
   {
     .....
     case System.Windows.Input.Key.Q: emulator.PressKey(1, 0); break;
     case System.Windows.Input.Key.W: emulator.PressKey(1, 1); break;
     case System.Windows.Input.Key.E: emulator.PressKey(1, 2); break;
     case System.Windows.Input.Key.R: emulator.PressKey(1, 3); break;
     case System.Windows.Input.Key.T: emulator.PressKey(1, 4); break;
     case System.Windows.Input.Key.Y: emulator.PressKey(1, 5); break;
     case System.Windows.Input.Key.U: emulator.PressKey(1, 6); break;
     case System.Windows.Input.Key.I: emulator.PressKey(1, 7); break;
     case System.Windows.Input.Key.O: emulator.PressKey(1, 8); break;
     case System.Windows.Input.Key.P: emulator.PressKey(1, 9); break;
     .....

Podemos aprovechar también para fabricarnos “atajos” en el teclado del Spectrum. Por ejemplo, para borrar un carácter (tecla “Delete” del Spectrum) es necesario pulsar la combinación “mayúsculas” y “0”. Podemos mapear la tecla del PC a una combinación de ambas ( y recordad hacer lo mismo en la función OnKeyUp()!!)

     // Keyboard shortcuts:
     case System.Windows.Input.Key.Back:
       emulator.PressKey(3, 0);      // Shift
       emulator.PressKey(0, 9);      // '0'
       break;

Ya podemos escribir!

SpectrumWKeyboard

Gracias a todo lo visto hoy nuestro control Spectrum está casi listo – ya podemos iniciar de nuevo nuestro emulador y disfrutar escribiendo código como se hacía en los ‘80… Quien no ha escrito algo así? Es lo primero que hicimos cuando, de chavales, descubríamos el maravilloso mundo de la informática, los ordenadores, la programación… y los videojuegos!

En la próxima entrega de este tutorial implementaremos la simulación de una unidad de cinta de cassette… porque todos queremos jugar a Head Over Heels o Skooldaze, no?

Enlaces:

Emulador YASS para Windows
Código fuente del Emulador YASS

– Hazte un Spectrum: 1ª Parte, 2ª Parte, 3ª Parte, 4ª Parte, 5ª Parte, 6ª Parte, 7ª Parte, 8ª Parte

Categorías:Desarrollo, Tutorial Etiquetas: , , , , ,

Hazte un Spectrum (4ª Parte)

En la anterior parte de este largo tutorial, la ULA del Spectrum estaba empezando a tomar cuerpo. A partir de ahora empieza la integración práctica de todos los componentes gracias a la inestimable ayuda de .NET y WPF.

Ya empieza a ver muchos componentes sueltos, y toca empezar a desarrollar el armazón que sujetará todo. Ya que toda la base está desarrollada en C++, bien podríamos seguir en éste lenguaje y completar un emulador totalmente nativo – solo que da como pereza, sobre todo teniendo en cuenta que, para completarlo acabaríamos interactuando con el GDI de Windows “de toda la vida”, y que está más que obsoleto. En cambio pensé en usar .NET y WPF que ofrecen una infinita mayor versatilidad y, sobre todo, una impresionante facilidad de migración hacia el futuro – léase WinRT. Pero no adelantemos acontecimientos.

Para un componente de bajo nivel como nuestro emulador de Spectrum, C++ es sin duda el lenguaje ideal. Pero para completarlo necesitamos un “front-end”, una “aplicación emulador” y pensé cómo convertir todo el código creado hasta ahora como un componente reutilizable. Simplemente, convirtamos el emulador en un control WPF, que podremos emplear desde una sencillísima aplicación .NET y C#.

C++ Gestionado

Parece que existe la convicción de que “todo” se puede (e incluso, “debe”) hacerse en C# para ser molón, para ser “del siglo XXI”, pero para mí no es el caso. C# es una maravilla de lenguaje, encuentro su estructura absolutamente preciosa, pero no me da todo lo que necesito. Lo que la mayoría de la gente pasa por alto es que hay “otro C++” el C++ Gestionado (ó “Managed C++”) que es la combinación perfecta de los dos entornos: la flexibilidad, la potencia de C++, unido a poder emplear un runtime moderno como .NET. En C++ Gestionado se puede mezclar el código nativo, con acceso a bajo nivel a la máquina junto con clases de altísimo nivel de abstracción, todo en un sólo lenguaje, todo en una sola librería. El truco? Las clases “ref”.

Las clases en C++ se declaran como “class NombreDeLaClase”. Esa clase será nativa, con acceso total a la máquina y al API. Pero, y si una clase en C++ desea acceder (y ser accedida) desde .NET? Basta con declarar la clase como “ref class NombreDeLaClase”. Una “ref class” es una clase .NET a todos los efectos, solo que escrita en C++. Eso es justo lo que queremos.

La clase “Emulation.Spectrum”

Vamos a escribir una clase que, vista desde .NET es un control WPF más, pero que internamente implenta un Sinclair ZX Spectrum. El primer paso es crear un nuevo projecto, una “class library” en C++ Gestionado. Y si, apuesto a que muchos de vosotros ni siquiera os habíais fijado en esa parte de los proyectos que se pueden crear con Visual Studio:

ManagedCPPDLL

En qué se diferencia programar en C++ en un entorno gestionado? Absolutamente en nada. Simplemente se abre la puerta a la posibilidad de crear clases “visibles” desde .NET y que soporten toda su inmensa librería de componentes – lo cual, frente a tener que hacerlo usando al API clásico de Windows, es toda una ventaja. El cambio llega al crear clases gestionadas, donde la sintaxis clásica de C++ se extiende. Si, hay una curva de aprendizaje, pero considero que vale la pena. Y no es tan duro como podría parecer en un principio.

Una vez creado el proyecto, simplemente copié dentro todos los fuentes C++ que ya había diseñado. Y compila, aunque claro, no genera nada visible desde .NET. Así que ahora toca implementar el control. Para ello, escogí el control Border (nombre completo, “System.Windows.Controls.Border”) como clase base de la que derivarme porque necesito lo más básico posible. De hecho, me vale cualquier control WPF que tenga un “Background” o un “Fill”… Así que la clase es más o menos como sigue (y nótese que es una “ref class”):

using namespace System;
using namespace System::Threading;
using namespace System::Windows::Media::Imaging;

namespace Emulation
{
  public ref class Spectrum : System::Windows::Controls::Border, IDisposable
  {
    public:
      Spectrum();
      ~Spectrum()
      {
        Stop();
        GC::SuppressFinalize(this);
      };
      !Spectrum()
      {
        Stop();
        emulatorScreen = nullptr;
      }

    public:
      void Start();
      void Stop() { };

    protected:
      void emulatorMain();
      void OnRenderTick(Object^ sender,EventArgs^ e);

    private:
      Thread^ emulatorThread;
      WriteableBitmap^ emulatorScreen;
      LPDWORD screenData;
      bool screenDataDirty;
      int bytesPerScreenLine;
      int quitEmulation;
  };
}

Lo primero que llama la atención a un programador C++ (e incluso a uno de C#) es el uso de “^” para representar un puntero. Realmente no es un puntero, es un “handle” a un objeto .NET. Pronto descubriremos como podemos mezclar, en la misma clase, handles a otros objetos .NET y punteros a instancias de clases nativas (como el “LPDWORD” al buffer nativo del bitmap). Todo al mismo tiempo.

La implementación es muy simple: la emulación va a correr en un thread independiente (para simplificar incluso más la programación desde un entorno .NET). La emulación de la pantalla voy a resolverla usando un componente WriteableBitmap, que no es más que una “superficie pintable” (si es que eso significa algo) que voy a emplear como “Background” de mi control.

El método Start de la clase es simple: instanciamos el bitmap (WriteableBitmap) para tener dónde “pintar” la pantalla del Spectrum y arrancamos el thread de emulación:

  Spectrum::Spectrum()
  {
    emulatorScreen = nullptr;
    quitEmulation = false;
    screenData = nullptr;
    screenDataDirty = false;
    m_RenderDelegate = gcnew EventHandler(this,&Spectrum::OnRenderTick);
  }

  void Spectrum::Start()
  {
    if (emulatorThread != nullptr)
      return;		// Already running

    quitEmulation = false;

    // Attach to WPF screen refresh to update
    CompositionTarget::Rendering += m_RenderDelegate;

    // Build the emulated screen and WPF render surface.
    if (emulatorScreen == nullptr)
    {
      // Emulator will draw on this bitmap
      emulatorScreen = gcnew WriteableBitmap(320,240,72,72,PixelFormats::Bgra32,nullptr);
      bytesPerScreenLine = emulatorScreen->BackBufferStride;

      emulatorScreen->Lock();
      screenData = (LPDWORD)emulatorScreen->BackBuffer.ToPointer();

      for (int dd=0;dd<320*240;dd++)
        screenData[dd] = 0xFFC0C0C0;
      emulatorScreen->Unlock();

      // ... and the bitmap is attached as the contents of the control's background.
      ImageBrush^ bkgnd = gcnew ImageBrush();
      bkgnd->ImageSource = emulatorScreen;
      Background = bkgnd;
    }

    emulatorThread = gcnew Thread(gcnew ThreadStart(this,&Spectrum::emulatorMain));
    emulatorThread->Start();
  }

Fijaos en otra diferencia al programar en C++ gestionado: las clases nativas se instancian con “new”, mientras que las clases gestionadas se instancian con “gcnew”. Nunca he entendido el por qué de emplear dos comandos para hacer algo que el compilador podría distinguir él solito…

Lo único reseñable de éste fragmento de código es cómo usamos la clase WriteableBitmap como elemento de unión entre WPF y C++: por un lado obtenemos un puntero “clásico de toda la vida” a los contenidos físicos del bitmap, mientras que a la vez lo usamos para construir una textura que usamos como “fondo” (“Background”) del control. El único trabajo extra es que necesitamos avisar a WPF cada vez que el bitmap cambie, y para eso nos “enganchamos” al delegado de repintado (CompositionTarget.Rendering) del sistema.

Por cierto, sí que necesitamos una cosa más: un módulo que contenga la ROM del Spectrum: nuevamente, empleando nuestros buses la implementación es sencilla:

template<unsigned int B,unsigned int S> class ROM : public BusComponent<B,S>
{
public:
  int Load(char *romName)
  {
    FILE *file = NULL;
    int err = fopen_s(&file,romName,"rb");
    if ((err == 0) && (file != NULL))
    {
      int got = fread(data,1,S,file);
      fclose(file);
    }
    return(err);
  };

  public:
  void Write(unsigned int address,unsigned char value)
  {
    // Do nothing... this is ROM!!!
  }
  unsigned char Read(unsigned int address)
  {
    return(data[address-B]);
  }

protected:
  unsigned char data[S];
};

Para la implementación de la función “Load()” he usado el código más “ANSI” posible – la portabilidad primero (y por eso no hago más que usar “unsigned short”, “unsigned int”, etc, en lugar de los términos habituales de WIN32 como USHORT o DWORD).

Ahora, echemos un vistazo a la función “emulatorMain()”, que es donde se realiza el trabajo de verdad. Observad como para ser una clase .NET, maneja tanto objetos nativos como gestionados con la misma soltura:

  void Spectrum::emulatorMain()
  {
    // Components
    Bus16 bus;              // Main Z80 data bus
    ROM<0,16384> rom;       // Main system ROM
    ULA ula;                // Spectrum's ULA chip + 16KB
    RAM<32768,32*1024> ram; // Remaining RAM (32KB)
    Z80 cpu;                // and finally, the CPU core

    // Load ROM contents
    if (rom.Load("48.rom"))
      throw gcnew System::IO::FileNotFoundException("Unable to load rom '48.ROM'");

    // Configure the ULA with the native Windows bitmap used to emulate the screen
    ula.SetNativeBitmap((LPBYTE)screenData,bytesPerScreenLine);

    // Populate busses
    bus.AddBusComponent(&rom);
    bus.AddBusComponent((ULAMemory*)&ula);
    bus.AddBusComponent(&ram);

    // And attach busses to cpu core
    cpu.DataBus = &bus;
    cpu.IOBus = (ULAIO*)&ula;

    cpu.regs.PC = 0;

    // Ready to roll!!
    DWORD dwFrameStartTime = GetTickCount();
    do
    {
      // Emulate next instruction
      cpu.EmulateOne();
    } while(quitEmulation == false);
  }

Dos cosas: la primera es que si os fijáis, he conectado la parte de I/O del chip ULA “a capón” al procesador (otra ventaja más de nuestra estructura de buses). Para que ésto funcione, sólo tengo que crear las dos funciones IORead() e IOWrite() de la ULA:

  unsigned char IORead(unsigned int address)
  {
    return(0xFF);  // pull ups on bus
  }

  void IOWrite(unsigned int address,unsigned char value)
  {
    // ULA is selected when reading an even address
    if (address & 0x01)
      return;

    // Update border color member variable.
    dwBorderRGBColor = dwColorTable[value & 0x07];
  }

La arquitectura de la ULA – y de los buses de I/O del Spectrum en general están simplificados al máximo. Si bien la dirección de I/O “estándar, documentada” de la ULA es 0xFE, cualquier dirección con el bit 0 de direcciones de “0” (o sea, cualquier dirección de I/O par) también sirve. De momento aprovecho para quedarme con el color para el borde de la pantalla, que se configura en los tres bits menos significativos del único puerto de I/O de la ULA. Pero de eso toca hablar un poco más adelante.

En segundo lugar, y si recordáis el capítulo anterior, nuestra implementación de ULA estaba a la espera de que nos diesen un bitmap sobre el que dibujar. Como ya lo tenemos – gracias al WriteableBitmap e instanciado en el método Start() de la clase, ha llegado el momento de hacérselo saber a la ULA para que lo use, por medio de la función “SetNativeBitmap()”. La implementación es trivial:

  void SetNativeBitmap(LPBYTE pBitmap,int bytesPerScanLine)
  {
    nativeBitmap = (LPDWORD)pBitmap;
    dwordsPerRow = bytesPerScanLine / 4;
  }

Sólo queda una cosa más por hacer: WPF nos llamará 50 veces por segundo para saber si tenemos algo que pintar. De momento, vamos a lo bestia:

void Spectrum::OnRenderTick(Object^ sender,EventArgs ^e)
{
  emulatorScreen->Lock();
  emulatorScreen->AddDirtyRect(Int32Rect(0,0,320,240));
  emulatorScreen->Unlock();
}

Si, poco optimizado (invalida los contenidos del WriteableBitmap 50 veces por segundo), pero por lo menos nos sirve para ver hasta dónde llegamos.

Compilamos todo y obtenemos una bonita DLL en C++ gestionado. Ahora, habrá que usarla en alguna parte, verdad?

Vuelta a C#

Ya tocaba? Bien, esto será muchísimo más familiar para la mayoría. Vamos a hacer una aplicación WPF en C#, aunque VB sería exactamente igual de válido. Cread un nuevo proyecto de “aplicación WPF” (yo le he puesto “Emulator” de nombre – original que es uno) y agregad la referencia a nuestro flamante control y abrid “MainWindow.xaml”.

Lo primero, es necesario definir un “namespace” XML para referirnos a nuestro control. Simplemente agregadlo en la cabecera del XAML, indicando la DLL que contenga la implementación (“Spectrum.DLL” en mi caso). Para el namespace, escogí (como no podía ser de otro modo) “zx”:

<Window x:Class="Emulator.MainWindow"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Title="YASS - Yet another Spectrum Simulator"
   Height="350"
   Width="525"
   xmlns:zx="clr-namespace:Emulation;assembly=Spectrum">

Quitamos todo lo que sobra que VisualStudio tiene a bien colocar de forma predeterminada en el cuerpo de la ventana y lo reemplazamos por ésto:

  <Grid>
    <zx:Spectrum x:Name="emulator" />
  </Grid>

Sólo queda arrancar el emulador al inicio del programa. En el constructor de la clase basta con invocar el método Start() del control:

  public MainWindow()
  {
    InitializeComponent();
    emulator.Start();
  }

Nos aseguramos que el proyecto “Emulator” está configurado como predeterminado (VisualStudio tiene tendencia a dejar como predeterminado el primer proyecto que se agrega a una solución), pulsamos F5 (o sea: “Debug/Start”), cruzamos los dedos y nuestro trabajo se verá recompensado con ésto:

FirstStart

Es el Spectrum más “sordo” del universo (no tiene teclado, no tiene sonido, no tiene nada de nada), pero la secuencia de arranque la completó a la perfección y nuestra implementación de ULA es capaz de actualizar un bitmap de WPF recibiendo las escrituras de la cpu a la memoria de vídeo emulada. Vamos bien…

El siguiente paso será darle a nuestro Spectrum un estupendo teclado, para poder comunicarnos con él. También descubriremos cómo necesitaremos recrear las temporizaciones de un televisor para sincronizarlo todo… Tranquilos, es más fácil de lo que parece.

Enlaces:

Emulador YASS para Windows
Código fuente del Emulador YASS

– Hazte un Spectrum: 1ª Parte, 2ª Parte, 3ª Parte, 4ª Parte, 5ª Parte, 6ª Parte, 7ª Parte, 8ª Parte

Categorías:Desarrollo, Tutorial Etiquetas: , , , , ,

Hazte un Spectrum (3ª Parte)

En las dos primeras partes de éste tutorial ha quedado definida una arquitectura de un procesador Z80 simulado y de los componentes que permiten la conexión de nuestro procesador a diversos componentes a través de buses. Hoy empezamos a construir el componente hardware que convierte nuestra plataforma virtual en un Spectrum: el chip ULA.

ulaEl chip ULA del Spectrum es lo que da la “personalidad” de la máquina: define cómo se gestionan los gráficos, cómo se genera la señal de vídeo que vemos en el monitor (perdón… “televisor”), cómo se genera el sonido, cómo leer el teclado… Esta es, con diferencia, la parte que más me atraía de éste apasionante rato de aprendizaje.

Para empezar, lo primero es hacer un poco de memoria y recordar cómo estaba construido un Spectrum. En la época era impensable diseñar algo ni remotamente parecido a lo que hoy se considera una estructura de ordenador “moderna”: una cpu que habla con un chip “northbridge” que se encarga de redistribuir las señales a los distintos componentes del hardware, ajustando sus diferentes velocidades, anchos de bus y, a ser posible, sin atascar al procesador esperando a que dispositivos lentos terminen su trabajo.

En la época del Spectrum tuvieron una idea económica, sencilla, y facilísima de implementar: en lugar de emplear un costosísimo (y complejísimo para la época) chip que se encargase de éstas funciones, lo que hicieron fue unir directamente y sin escalas el procesador principal y el chip ULA del Spectrum al mismo bus. El problema es que el chip ULA tiene que compartir la memoria con el procesador principal, para leer sus contenidos y generar la imagen que vemos en el televisor. Cómo podríamos hacer que dos componentes hardware activos, que necesitan leer la memoria en el mismo momento, puedan convivir usando los mismos buses?

ZXHardware

En el diagrama (escandalosamente simplificado) se puede ver cómo el chip ULA está conectado directamente al primer banco de memoria de 16KB (y único en el caso del Spectrum 16KB). El chip ULA necesita leer la memoria para generar la imagen de vídeo, pero al mismo tiempo la cpu debe acceder a la memoria también para seguir ejecutando programas. La solución es simple: la cpu Z80 tiene una señal hardware (“WAIT”) que normalmente se emplea por dispositivos hardware “lentos” para permitirles terminar su trabajo cuando son accedidos por el procesador. En éste caso, la señal WAIT está conectada al chip ULA que, cuando necesita acceder a la memoria RAM (o cuando la cpu accede a sus registros) simplemente mantiene activa la señal, haciendo que la cpu tenga que esperar. La conexión del bus de datos del bloque de RAM de 16KB y el procesador está separado por simples resistencias, así que si la cpu lee memoria fuera del rango de la ULA las señales de los chips de memoria llegarán directamente a la CPU (las resistencias bloquean el paso de las señales al bus de la ULA). Cuando la cpu lee los 16KB inferiores y dado que ningún chip de memoria en el bus principal forzará señal alguna en el bus, llegará al bloque de memoria a través de las resistencias. Francamente muy ingenioso (y económico de implementar, por cierto!!).

EmuHardwarePara nuestra implementación vamos a intentar hacer algo parecido, pero en nuestro caso, y dado que el software nos permite emular el hardware “grátis”, sí vamos a emplear el concepto de “northbridge” de una cpu moderna: vamos a implementar una ULA que contiene su propio bloque de 16KB y a los que la cpu accede a través de la misma, independientemente de otros componentes (32KB extra, por ejemplo) que pueda haber conectados. Esta arquitectura nos facilita otra emulación que vendrá en un futuro: emular los retardos que introduce la ULA en la ejecución de programas – y os sorprendería la cantidad de software que “cuenta ciclos” para realizar curiosos efectos gráficos.

ULA – implementación básica

Según podemos ver en el gráfico, el chip ULA se conecta al mismo tiempo tanto al bus de datos como al bus de I/O del procesador. Hasta ahora nuestros componentes (como por ejemplo la RAM) podían conectarse a uno u a otro, pero no a los dos a la vez. Tipo de bus nuevo? Nuevas clases de dispositivos? O cómo hacemos para que un componente herede de dos clases bases iguales a la vez? Nuevamente, C++ viene al rescate: herencia múltiple.

Necesitamos que nuestro componente ULA distinga entre los accesos al bus de datos y accesos al bus de direcciones, pero la cpu usa la misma función en ambos casos: Read() o Write() de las clases base BusComponentBase.  Podríamos pensar en que la hemos pifiado con el diseño original, pero no es el caso: implementar la ULA derivando de dos buses a la vez es tan “simple” como ésto:

class ULAMemory : public RAM<16384,16*1024>
{
protected:
  void Write(unsigned int address,unsigned char value)
  {
    // First, store data
    __super::Write(address,value);
    // and forward to ULA
    MemoryWrite(address,value);
  }
  virtual void MemoryWrite(unsigned int address,
                           unsigned char value) = 0;
};

class ULAIO : public BusComponent<0xFE,1>
{
protected:
  unsigned char Read(unsigned int address)
  {
    return(IORead(address));
  }

  void Write(unsigned int address,unsigned char value)
  {
    IOWrite(address,value);
  }

  virtual unsigned char IORead(unsigned int address) = 0;
  virtual void IOWrite(unsigned int address,
                       unsigned char value) = 0;
};

class ULA : public ULAMemory, ULAIO
{
   ....
   void MemoryWrite(unsigned int address,unsigned char value);
   void IOWrite(unsigned int address,unsigned char value);
   unsigned char IORead(unsigned int address);
   ....
};

Definimos dos clases base (virtuales) que cuando reciben las llamadas desde la cpu Read() y Write() invocan a una correspondiente función MemoryWrite(), IORead() e IOWrite() de una futura clase derivada, implementada por la clase ULA, que deriva de ambas. En éste caso no implementamos MemoryRead(), porque la implementación base (de la clase RAM) hace precisamente lo que necesitamos: devolver el contenido de la posición de memoria emulada a la cpu. En cambio la función MemoryWrite() la emplearemos para actualizar la pantalla simulada. Para más efectividad, la clase ULAMemory deriva de RAM<16384,16384>, con lo que el almacenamiento de la ULA ya está implementado. No ha sido tan difícil, verdad?

Pero cómo conectamos cada uno de los dos buses de la ULA a los buses correspondientes del procesador? Fácil, casteando la instancia de la ULA al bus correspondiente:

   dataBus.AddBusComponent((ULAMemory*)&ula);
   ....
   ioBus.AddBusComponent((ULAIO*)&ula);

La pantalla del Spectrum

Ya que estamos rehaciendo la ULA, el primer paso será analizar la pantalla del Spectrum y analizar su emulación.

El Spectrum tenía una pantalla de 256×192 píxeles (increíble que se viese algo, verdad?) y emplea dos zonas de memoria para la generación de la imagen. La primera zona de memoria de 6 kilobytes, comprendida entre las posiciones de memoria 16384 (0x4000) a 22527 (0x57FF) contiene el bitmap en sí, donde cada byte define una secuencia de 8 píxeles horizontales. Una segunda sección inmediatamente después de 768 bytes, desde 22528 (0x5800) a 23295 (0x5AFF) contiene un byte de atributo por cada bloque de 8×8 píxeles del bitmap. Cada byte de atributos, que se aplica a cada bloque de 8×8 pixeles en pantalla, tiene el formato siguiente:AttrFormat

El bit marcado como ‘F’ indica que el carácter parpadea, esto es, intercambia cada cierto tiempo los colores de fondo y principal. Los bits ‘P’ indican el color de fondo (o “PAPER” usando nomenclatura del Spectrum), Los bits ‘I’ a su vez indican el color principal (“INK”) del carácter, y el bit ‘B’ indica que el color es brillante.

Parece fácil, no? Bueno, casi.

El “problema” es que, internamente, la líneas que componen el bitmap de pantalla del Spectrum no están ordenados. De hecho, el truco consiste en que “cruzaron” líneas de direcciones entre los bloques superior e inferior de direcciones, de tal forma que podían usar la parte alta de los registros de 16 bits (H en el registro HL, por ejemplo) para ir “bajando” líneas a medida que dibujaban caracteres. Para que ésto funcione, cada línea tiene que estar 256 bytes más adelante en memoria, en lugar de los 32 (32 bytes x 8 pixeles son los 192 pixeles horizontales de pantalla) que serían necesarios de usar un bitmap correlativo. Se ahorran un montón de cálculos, porque para avanzar líneas basta con hacer “INC H” en lugar de otras operaciones aritméticas.

ScreenVidConversionDado que vamos a emular la pantalla en un bitmap de 320×240 (tamaño francamente estándar y que además nos da margen para emular el borde de la pantalla), era necesario “linealizar” la memoria de vídeo del Spectrum. Resulta que es tan fácil como intercambiar dos bloques de tres bits en la dirección para obtener otra dirección, lineal en un bitmap de 256×192.

Como tenía que definir el formato en que iba a general el bitmap, y para que fuese lo más portable posible, opté por emplear BGRA en 32 bits. Lo mejor del caso es que, dado que los colores que puede generar la ULA del Spectrum son limitados a 8 (con otros 8 iguales pero más brillantes) y lo mejor es hacerlo por tabla, adaptar de un formato a otro es trivial en la mayoría de los casos. Así que manos a la obra e incluímos el siguiente código en la función MemoryWrite() de la clase ULA:

  void MemoryWrite(unsigned int address,unsigned char value)
  {
    if (nativeBitmap == NULL)
      return;

    address &= 0x3FFF;
    if (address > 0x1AFF)   // Above bitmap+attrs?
      return;

    // Video memory: 0x0000->0x17FF bitmap graphics,
    //               0x1800->0x1AFF attributes
    if (address < 0x1800)   // Bitmap graphics
    {
      // Convert from Spectrum memory video to plain buffer
      unsigned int rowbase = address >> 5;
      rowbase = ((rowbase & 0xC0) |
      ((rowbase & 0x38) >> 3) |
      ((rowbase & 0x07) << 3));

      LPDWORD scr = nativeBitmap;
      // Center pixel video (256x192) in the whole
      //bitmap (320x240) == offset 32 horz, 24 vert
      scr += (320 * 24);
      scr += 32;
      scr += (rowbase * dwordsPerRow);
      scr += ((address & 0x001F) * 8);

      // Fetch attribute
      unsigned char attr = data[0x1800 + ((rowbase / 8) * 32) + (address & 0x001F)];
      DWORD dwInk;
      DWORD dwPaper;
      if ((attr & 0x80) && blinkState)
      {
        dwPaper = dwColorTable[((attr & 0x40) >> 3) | (attr & 0x07)];
        dwInk = dwColorTable[(attr & 0x78) >> 3];
      }
      else
      {
        dwInk = dwColorTable[((attr & 0x40) >> 3) | (attr & 0x07)];
        dwPaper = dwColorTable[(attr & 0x78) >> 3];
      }

      for (int dd=7;dd >= 0;dd--)
      {
        *scr++ = (value & (1 << dd)) ? dwInk : dwPaper;
      }
      IsDirty = true;
    }
    else    // Attribute memory
    {
      // ToDo: Redraw whole affected "character block"
    }
  }

La variable miembro memoryBitmap se inicializará (por otros medios) al buffer real que hay que manipular y que nos será provisto desde una instancia superior. En la clase está definido como un DWORD*, para poder acceder a cada pixel BGRA (32 bits) como un solo elemento del array. La función accede al miembro data de la clase base ULAMemory para determinar el atributo (los colores) a emplear para “pintar” la pantalla. Y finalmente, la variable blinkState determina en qué fase del “parpadeo” se encuentra el carácter y que será actualizada más adelante (a “0” ó “1” alternativamente).

La cosa se complica un poco cuando lo que se escribe en la memoria de la ULA es un atributo de color: hay que actualizar un carácter completo de 8×8 píxeles, así que vamos a escribir una función especializada en ello. La función simplemente recibe el offset del carácter a redibujar, de 0 a 767:

void UpdateChar(unsigned int nChar)
{
  int memOffset = ((nChar & 0x300) << 3) | (nChar & 0xFF);
  unsigned char value = data[nChar + 0x1800];

  LPDWORD scr = nativeBitmap;
  // Center pixel video (256x192) in the whole
  // bitmap (320x240) == offset 32 horz, 24 vert
  scr += (320 * 24);
  scr += 32;
  scr += (((nChar / 32) * 8) * dwordsPerRow);
  scr += ((nChar & 0x001F) * 8);

  DWORD dwInk;
  DWORD dwPaper;
  if ((value & 0x80) && blinkState)
  {
    dwPaper = dwColorTable[((value & 0x40) >> 3) | (value & 0x07)];
    dwInk = dwColorTable[(value & 0x78) >> 3];
  }
  else
  {
    dwInk = dwColorTable[((value & 0x40) >> 3) | (value & 0x07)];
    dwPaper = dwColorTable[(value & 0x78) >> 3];
  }

  // Redraw 8x8 pixels
  for (int yy=0;yy<8;yy++)
  {
    unsigned char scanData = data[memOffset];
    LPDWORD pixel = scr;
    for (int dd=7;dd >= 0;dd--)
      *pixel++ = (scanData & (1 << dd)) ? dwInk : dwPaper;
    memOffset += 256;   // Next scanline on Spectrums memory
    scr += dwordsPerRow; // Next scanline on native bitmap
  }
}

Ahora podemos completar la función MemoryWrite(), para que actualice el bloque de 8×8 píxeles cuando se cambia un atributo:

    else    // Attribute memory
    {
      // Redraw whole affected "character block"
      UpdateChar(address - 0x1800);
    }

Finalmente, sólo nos queda definir la paleta de colores (dwColorTable) del Spectrum:

static const unsigned int dwColorTable[] =
{
  0xFF000000,
  0xFF0000CD,
  0xFFCD0000,
  0xFFCD00CD,
  0xFF00CD00,
  0xFF00CDCD,
  0xFFCDCD00,
  0xFFCDCDCD,

  0xFF000000,
  0xFF0000FF,
  0xFFFF0000,
  0xFFFF00FF,
  0xFF00FF00,
  0xFF00FFFF,
  0xFFFFFF00,
  0xFFFFFFFF
};

El siguiente paso

Qué sigue ahora? Ahora toca hacer un banco de pruebas para todos estos componentes, y verificar que podemos traer un Spectrum a la vida. Para hacernos la vida más facil vamos a usar un aliado que nos hará casi, casi trivial la implementación de una aplicación que una todas las piezas que hemos desarrollado: WPF. Y para facilitar la unión de un entorno de tan alto nivel y nuestros componentes en C++, nada mejor que seguir desarrollando en C++… pero gestionado y soportado por .NET.

Enlaces:

Emulador YASS para Windows
Código fuente del Emulador YASS

– Hazte un Spectrum: 1ª Parte, 2ª Parte, 3ª Parte, 4ª Parte, 5ª Parte, 6ª Parte, 7ª Parte, 8ª Parte

Categorías:Desarrollo, Tutorial Etiquetas: , , , , ,