cursos:ensamblador:interrupciones

Diferencias

Muestra las diferencias entre dos versiones de la página.

Enlace a la vista de comparación

Ambos lados, revisión anterior Revisión previa
Próxima revisión
Revisión previa
cursos:ensamblador:interrupciones [14-01-2024 07:21] – [Control de ticks, segundos y minutos] sromerocursos:ensamblador:interrupciones [21-01-2024 11:23] (actual) – [Instrucción HALT] sromero
Línea 11: Línea 11:
  Cuando se finaliza la ejecución de la rutina ISR, el procesador continúa la ejecución desde donde se detuvo al llegarle la señal de interrupción. Para nuestro programa la ejecución de la ISR es "transparente". No obstante, es importante que estas rutinas ISR sean lo más reducidas y rápidas posibles para no afectar a la velocidad de ejecución del programa principal.  Cuando se finaliza la ejecución de la rutina ISR, el procesador continúa la ejecución desde donde se detuvo al llegarle la señal de interrupción. Para nuestro programa la ejecución de la ISR es "transparente". No obstante, es importante que estas rutinas ISR sean lo más reducidas y rápidas posibles para no afectar a la velocidad de ejecución del programa principal.
  
- El Z80A (el corazón del ZX Spectrum) dispone de 2 tipos de señales de interrupción: una señal de alta prioridad (NMI, Non-mascarable-Interrupt), y otra señal enmascarable de menor prioridad (MI). El procesador, como hemos dicho, lee el estado de las señales /NMI e /INT al acabar la ejecución de cada instrucción (salvo en el caso de instrucciones repetitivas como LDDR, por ejemplo, que lo realiza al acabar cada subinstrucción como LDD).+ El Z80A (el corazón del ZX Spectrum) dispone de 2 tipos de señales de interrupción: una señal de alta prioridad (NMI, Non-mascarable-Interrupt), y otra señal enmascarable de menor prioridad (MI). El procesador, como hemos dicho, lee el estado de las señales /NMI e /INT al acabar la ejecución de cada instrucción (salvo en el caso de instrucciones repetitivas como lddr, por ejemplo, que lo realiza al acabar cada subinstrucción como ldd).
  
 \\ \\
Línea 35: Línea 35:
  Cuando el procesador recibe una de estas interrupciones actúa de 3 formas diferentes según el modo actual de interrupción en que esté. El Z80 puede estar en 3 modos de interrupción o IM (Interrupt Mode):  Cuando el procesador recibe una de estas interrupciones actúa de 3 formas diferentes según el modo actual de interrupción en que esté. El Z80 puede estar en 3 modos de interrupción o IM (Interrupt Mode):
  
-   * **Modo 0**: En este modo de interrupción, el dispositivo que desea interrumpir al procesador activa la pantilla ''/INT''del mismo y durante el ciclo de reconocimiento de interrupción del procesador coloca el opcode de una instrucción en el bus de datos del Z80. Normalmente será una instrucción de 1 sólo byte (normalmente un RST XX) ya que esto sólo hace necesario escribir y mantener un opcode en el bus de datos (aunque puede ser, en periféricos más complejos y con una correcta temporización, un JP CALL seguido de la dirección de salto). Este modo de interrupción existe principalmente por compatibilidad con el procesador 8080.+   * **Modo 0**: En este modo de interrupción, el dispositivo que desea interrumpir al procesador activa la pantilla ''/INT''del mismo y durante el ciclo de reconocimiento de interrupción del procesador coloca el opcode de una instrucción en el bus de datos del Z80. Normalmente será una instrucción de 1 sólo byte (normalmente un RST XX) ya que esto sólo hace necesario escribir y mantener un opcode en el bus de datos (aunque puede ser, en periféricos más complejos y con una correcta temporización, un jp call seguido de la dirección de salto). Este modo de interrupción existe principalmente por compatibilidad con el procesador 8080.
  
-   * **Modo 1**: Cuando se recibe una señal INT y el procesador está en IM 1, el Z80 ejecuta un DI (Disable Interrupts), se salva en la pila el valor actual de PC, y se realiza un salto a la ISR ubicada en la dirección $0038 (en la ROM). Es el modo de interrupción por defecto del Spectrum (por ejemplo, en el intérprete BASIC), y en este modo el Spectrum no sabe qué dispositivo ha causado la interrupción y es la rutina ISR la encargada de determinar qué dispositivo externo (o proceso interno) es el que requiere la atención del procesador.+   * **Modo 1**: Cuando se recibe una señal INT y el procesador está en im 1, el Z80 ejecuta un di (Disable Interrupts), se salva en la pila el valor actual de PC, y se realiza un salto a la ISR ubicada en la dirección $0038 (en la ROM). Es el modo de interrupción por defecto del Spectrum (por ejemplo, en el intérprete BASIC), y en este modo el Spectrum no sabe qué dispositivo ha causado la interrupción y es la rutina ISR la encargada de determinar qué dispositivo externo (o proceso interno) es el que requiere la atención del procesador.
  
    * **Modo 2**: Es el modo más utilizado en los programas comerciales e implica el uso del registro I y el bus de datos para generar un vector de salto. Los 8 bits superiores de la dirección de salto del ISR se cargan en el registro I. El dispositivo que desea interrumpir al procesador colocará los 8 bits bajos de la dirección (por convención, un número par, es decir, con el bit 0 a 0) en el bus de datos.    * **Modo 2**: Es el modo más utilizado en los programas comerciales e implica el uso del registro I y el bus de datos para generar un vector de salto. Los 8 bits superiores de la dirección de salto del ISR se cargan en el registro I. El dispositivo que desea interrumpir al procesador colocará los 8 bits bajos de la dirección (por convención, un número par, es decir, con el bit 0 a 0) en el bus de datos.
Línea 47: Línea 47:
  Concretamente I*256 apunta a una tabla de direcciones de ISR (Tabla de Vectores de Interrupción) que será indexada por el byte bajo de la dirección (lo que nos da un total de 128 ISR posibles de 2 bytes de dirección de inicio absoluta cada una). Es por esto que la dirección del byte bajo es, por convención, un número par, de forma que siempre accedamos a las direcciones de 16 bits correctas en la tabla (I*256+N y I*256+N+1 siendo N par) y no a media dirección de una ISR y media de otra (como veremos más adelante).  Concretamente I*256 apunta a una tabla de direcciones de ISR (Tabla de Vectores de Interrupción) que será indexada por el byte bajo de la dirección (lo que nos da un total de 128 ISR posibles de 2 bytes de dirección de inicio absoluta cada una). Es por esto que la dirección del byte bajo es, por convención, un número par, de forma que siempre accedamos a las direcciones de 16 bits correctas en la tabla (I*256+N y I*256+N+1 siendo N par) y no a media dirección de una ISR y media de otra (como veremos más adelante).
  
- Así pues, se puede definir la dirección de salto de la interrupción en modo IM2 como:+ Así pues, se puede definir la dirección de salto de la interrupción en modo im2 como:
  
 <code> <code>
Línea 53: Línea 53:
 </code> </code>
  
- Así, si hemos puesto en I el valor $FE, quiere decir que hemos creado una tabla desde $FE00 a $FEFF+1, la cual contiene 128 direcciones de salto de 2 bytes. Cada dirección de esta tabla, debería apuntar a una rutina en memoria que gestione las interrupciones de ese dispositivo. Por ejemplo, si se produce una interrupción por parte de la ULA que pone en el bus de datos el valor $FF, ocurre lo siguiente:+ Así, si hemos puesto en I el valor $fe, quiere decir que hemos creado una tabla desde $fe00 a $feFF+1, la cual contiene 128 direcciones de salto de 2 bytes. Cada dirección de esta tabla, debería apuntar a una rutina en memoria que gestione las interrupciones de ese dispositivo. Por ejemplo, si se produce una interrupción por parte de la ULA que pone en el bus de datos el valor $ff, ocurre lo siguiente:
  
     * El Z80 deja de ejecutar el programa que estuviera ejecutando en PC para atender la interrupción.     * El Z80 deja de ejecutar el programa que estuviera ejecutando en PC para atender la interrupción.
-    * El Z80 lee el bus de datos y encuentra el valor que ha puesto la ULA ($FF). +    * El Z80 lee el bus de datos y encuentra el valor que ha puesto la ULA ($ff). 
-    * Combina el valor de I ($FE en nuestro ejemplo) con el "identificador de la interrupción" y genera el valor $FEFF+    * Combina el valor de I ($fe en nuestro ejemplo) con el "identificador de la interrupción" y genera el valor $feFF
-    * Va a la memoria, a la dirección $FEFF, y lee de ella 2 bytes. Esos 2 bytes son la rutina ISR.+    * Va a la memoria, a la dirección $feff, y lee de ella 2 bytes. Esos 2 bytes son la rutina ISR.
     * El Z80 deshabilita las interrupciones y hace un salto a esa dirección.     * El Z80 deshabilita las interrupciones y hace un salto a esa dirección.
-    * Nosotros, antes de activar el IM2, ya pusimos en $FEFF la dirección de la rutina de ISR que tenemos en nuestro código. +    * Nosotros, antes de activar el im2, ya pusimos en $feff la dirección de la rutina de ISR que tenemos en nuestro código. 
-    * Nuestra ISR se ejecuta, y al final de ella, hace un EI (para volver a activar las interrupciones) y un RETI. Además no debe de modificar ningún registro ni flag (debe de preservarlos todos con PUSH/POP), ni siquiera los registros alternativos (y debe de dejar como set de registros activo, el que había al entrar).+    * Nuestra ISR se ejecuta, y al final de ella, hace un ei (para volver a activar las interrupciones) y un reti. Además no debe de modificar ningún registro ni flag (debe de preservarlos todos con PUSH/POP), ni siquiera los registros alternativos (y debe de dejar como set de registros activo, el que había al entrar).
     * El Z80 continúa ejecutando el código por donde se quedó PC, hasta que llegue la siguiente interrupción.     * El Z80 continúa ejecutando el código por donde se quedó PC, hasta que llegue la siguiente interrupción.
  
Línea 67: Línea 67:
  
   * NMI: 11 t-estados   * NMI: 11 t-estados
-  * INT IM 0: 13 t-estados (si la instrucción del bus es un RST) +  * INT im 0: 13 t-estados (si la instrucción del bus es un RST) 
-  * INT IM 1: 13 t-estados +  * INT im 1: 13 t-estados 
-  * INT IM 2: 19 t-estados+  * INT im 2: 19 t-estados
  
 \\ \\
Línea 80: Línea 80:
  
 <code z80> <code z80>
-IM 0       ; Cambiar a modo IM 0 (8 T-Estados). +im 0       ; Cambiar a modo im 0 (8 T-Estados). 
-IM 1       ; Cambiar a modo IM 1 (8 T-Estados). +im 1       ; Cambiar a modo im 1 (8 T-Estados). 
-IM 2       ; Cambiar a modo IM 2 (8 T-Estados).+im 2       ; Cambiar a modo im 2 (8 T-Estados).
 </code> </code>
  
- Como ya hemos dicho, el Spectrum opera normalmente en IM 1, donde se llama regularmente a una ISR que lee el estado del teclado y actualiza ciertas variables del sistema (''LAST_K'', ''FRAMES'', etc.) para la conveniencia del intérprete de BASIC (y, en algunos casos, de nuestros propios programas). Esta ISR (la ''RST $38'', en **$0038**) pretende hacer uso exclusivo del registro IY por lo que si nuestro programa necesita hacer uso de este registro es importante hacerlo entre un DI y un EI para evitar que pueda ocurrir una interrupción con su valor modificado por nosotros y provocar un reset en el Spectrum. También tenemos que tener en cuenta esto si estando en modo ''IM 2'' llamamos manualmente a la ''RST $38'' para actualizar variables del sistema (aunque no es habitual que necesitemos ejecutar la ISR que usa el intérprete de BASIC).+ Como ya hemos dicho, el Spectrum opera normalmente en im 1, donde se llama regularmente a una ISR que lee el estado del teclado y actualiza ciertas variables del sistema (''LAST_K'', ''FRAMES'', etc.) para la conveniencia del intérprete de BASIC (y, en algunos casos, de nuestros propios programas). Esta ISR (la ''rst $38'', en **$0038**) pretende hacer uso exclusivo del registro IY por lo que si nuestro programa necesita hacer uso de este registro es importante hacerlo entre un di y un ei para evitar que pueda ocurrir una interrupción con su valor modificado por nosotros y provocar un reset en el Spectrum. También tenemos que tener en cuenta esto si estando en modo ''im 2'' llamamos manualmente a la ''rst $38'' para actualizar variables del sistema (aunque no es habitual que necesitemos ejecutar la ISR que usa el intérprete de BASIC).
  
- En el caso de aplicaciones y juegos, lo normal es cambiar a IM 2 con una rutina propia de ISR que realice las tareas que nosotros necesitemos, especialmente temporización, actualización del buffer del chip AY de audio para reproducir melodías, etc.+ En el caso de aplicaciones y juegos, lo normal es cambiar a im 2 con una rutina propia de ISR que realice las tareas que nosotros necesitemos, especialmente temporización, actualización del buffer del chip AY de audio para reproducir melodías, etc.
  
 \\ \\
Línea 95: Línea 95:
  
 <code z80> <code z80>
-DI         ; Disable Interrupts (4 T-estados) -> IFF=0 +di         ; Disable Interrupts (4 T-estados) -> IFF=0 
-EI         ; Enable Interrupts (4 T-estados). -> IFF=1+ei         ; Enable Interrupts (4 T-estados). -> IFF=1
 </code> </code>
  
Línea 105: Línea 105:
 ==== Instrucción HALT ==== ==== Instrucción HALT ====
  
- La instrucción ''HALT'' es una instrucción muy útil que detiene el proceso de ejecución de la CPU. Al llamarla, la CPU comienza a ejecutar continuamente instrucciones ''NOP'' de 4 t-estados (sin incrementar el contador de programa), hasta que se vea interrumpido por una NMI o una MI (INT), en cuyo momento se incrementa PC y se procesa la interrupción. Al volver de la ISR, el procesador continúa la ejecución del programa en la instrucción siguiente al ''HALT''.+ La instrucción ''HALT'' es una instrucción muy útil que detiene el proceso de ejecución de la CPU. Al llamarla, la CPU realiza de forma continuada el mismo procedimiento que cuando se ejecutan instrucciones ''NOP'' de 4 t-estados (pero sin incrementar el contador de programa, e incrementando el registro **R**), hasta que se vea interrumpido por una NMI o una MI (INT), en cuyo momento se incrementa PC y se procesa la interrupción. Al volver de la ISR, el procesador continúa la ejecución del programa en la instrucción siguiente al ''HALT''.
  
 <code z80> <code z80>
-HALT       ; Halt computer and wait for INT (4 T-Estados).+halt       ; Halt computer and wait for INT (4 T-Estados).
 </code> </code>
  
- Como veremos más adelante, la instrucción ''HALT'' nos será especialmente útil en determinadas ocasiones al trabajar con la manipulación del área de datos de la videomemoria.+ Básicamente, la ejecución de nuestro programa se detiene en el ''HALT'' hasta que ocurra una interrupción.
  
- Como veremos con detalle a continuación, la ULA genera una interrupción 50 veces por segundo (cada vez que se sincroniza con el haz de electrones de la pantalla para su redibujado, en la parte superior izquierda de la pantalla, antes de empezar a dibujar el actual BORDER). Al colocar un ''HALT'' en nuestro programa, la ejecución del mismo se detendrá en ese punto y dejaremos en espera nuestro programa hasta que se produzca dicha interrupción. Es una manera de limitar la velocidad del programa y de sincronizarse con el haz de electrones para hacer efecto concretos con temporizaciones controladas.+ Como veremos con detalle a continuación, la ULA genera una interrupción 50 veces por segundo (cada vez que se sincroniza con el haz de electrones de la pantalla para su redibujado, en la parte superior izquierda de la pantalla, antes de empezar a dibujar el actual BORDER). Al colocar un ''HALT'' en nuestro programa, la ejecución del mismo se detendrá en ese punto y lo dejaremos en espera hasta que se produzca el siguiente retrazo vertical y la ULA genere la interrupción correspondiente. Es una manera de limitar la velocidad del programa y de sincronizarse con el haz de electrones para hacer efectos concretos con temporizaciones controladas
 + 
 + Como veremos más adelante, la instrucción ''HALT'' nos será especialmente útil en determinadas ocasiones al trabajar con la manipulación del área de datos de la videomemoria.
  
 \\ \\
Línea 119: Línea 121:
 ==== Instrucciones "LD A, R" y "LD A, I" ==== ==== Instrucciones "LD A, R" y "LD A, I" ====
  
- Una instrucción de uso infrecuente con una peculiar utilidad es ''LD AR''. Con esta instrucción cargamos el valor del registro interno del procesador R (utilizado para el refresco de la DRAM) en el acumulador. Comunmente se utiliza para obtener algún tipo de valor "variable" como semilla o parte del proceso de generación de números aleatorios.+ Una instrucción de uso infrecuente con una peculiar utilidad es ''ld ar''. Con esta instrucción cargamos el valor del registro interno del procesador R (utilizado para el refresco de la DRAM) en el acumulador. Comunmente se utiliza para obtener algún tipo de valor "variable" como semilla o parte del proceso de generación de números aleatorios.
  
- No obstante, esta instrucción de 2 bytes ($ED $5F) y 9 t-estados de ejecución tiene la particular utilidad de copiar en el flag P/V el contenido del flip-flop IFF2, por lo que podemos utilizarla para conocer el estado de las interrupciones enmascarables.+ No obstante, esta instrucción de 2 bytes ($ed $5f) y 9 t-estados de ejecución tiene la particular utilidad de copiar en el flag P/V el contenido del flip-flop IFF2, por lo que podemos utilizarla para conocer el estado de las interrupciones enmascarables.
  
- Así, una vez ejecuado un ''LD AR'', sabemos que si la bandera está a 1 es que las interrupciones están habilitadas, mientras que si están a cero, es porque han sido deshabilitadas.+ Así, una vez ejecuado un ''ld ar'', sabemos que si la bandera está a 1 es que las interrupciones están habilitadas, mientras que si están a cero, es porque han sido deshabilitadas.
  
- Como curiosidad, la instrucción ''LD AI'' produce la misma afectación de P/V que ''LD AR''. Otros flags afectados por ambas instrucciones son ''S'', ''C'' (reseteado) y ''Z''.+ Como curiosidad, la instrucción ''ld ai'' produce la misma afectación de P/V que ''ld ar''. Otros flags afectados por ambas instrucciones son ''S'', ''C'' (reseteado) y ''Z''.
  
 \\ \\
Línea 134: Línea 136:
  La ULA, como encargada de gestionar la I/O, el teclado, y de refrescar el contenido de la pantalla usando los datos almacenados en el área de videoram del Spectrum, interrumpe al procesador de forma constante, a razón de 50 veces por segundo en sistemas de televisión PAL (Europa y Australia) y 60 veces por segundo en sistemas NTSC (USA).  La ULA, como encargada de gestionar la I/O, el teclado, y de refrescar el contenido de la pantalla usando los datos almacenados en el área de videoram del Spectrum, interrumpe al procesador de forma constante, a razón de 50 veces por segundo en sistemas de televisión PAL (Europa y Australia) y 60 veces por segundo en sistemas NTSC (USA).
  
- Esto quiere decir que cada 1/50 (o 1/60) segundos, la ULA produce una señal INT (interrupción enmascarable), que provoca la ejecución de la ISR de turno (''RST $38'' en modo ''IM 1'' ó la ISR que hayamos definido en modo ''IM 2'').+ Esto quiere decir que cada 1/50 (o 1/60) segundos, la ULA produce una señal INT (interrupción enmascarable), que provoca la ejecución de la ISR de turno (''rst $38'' en modo ''im 1'' ó la ISR que hayamos definido en modo ''im 2'').
  
- En el modo ''IM 1'' (el modo en que arranca el Spectrum), el salto a ''RST $38'' provocado por las interrupciones generadas por la ULA produce la ejecución regular y continua cada 1/50 segundos de las rutinas de lectura del teclado, actualización de variables del sistema de BASIC y del reloj del sistema (''FRAMES'') requeridas por el intérprete de BASIC para funcionar.+ En el modo ''im 1'' (el modo en que arranca el Spectrum), el salto a ''rst $38'' provocado por las interrupciones generadas por la ULA produce la ejecución regular y continua cada 1/50 segundos de las rutinas de lectura del teclado, actualización de variables del sistema de BASIC y del reloj del sistema (''FRAMES'') requeridas por el intérprete de BASIC para funcionar.
  
- En cuanto al modo ''IM 2'', el que nos interesa principalmente para la realización de programas y juegos, la dirección de salto del ISR se compone como un valor de 16 bits que se utiliza como "vector de salto". Como parte alta del mismo se utiliza el valor del registro I, y como parte baja el identificador de dispositivo presente en el bus de datos.+ En cuanto al modo ''im 2'', el que nos interesa principalmente para la realización de programas y juegos, la dirección de salto del ISR se compone como un valor de 16 bits que se utiliza como "vector de salto". Como parte alta del mismo se utiliza el valor del registro I, y como parte baja el identificador de dispositivo presente en el bus de datos.
  
- Como ya hemos visto en la definición del modo ''IM 2'', la dirección resultante ((I*256)+ID_DE_DISPOSITIVO_EN_BUS_DATOS) se utiliza para consultar una tabla de vectores de interrupción para saltar. A partir de la dirección I*256, debe de haber una tabla de 256 bytes con 128 direcciones de salto absolutas de 2 bytes cada una.+ Como ya hemos visto en la definición del modo ''im 2'', la dirección resultante ((I*256)+ID_DE_DISPOSITIVO_EN_BUS_DATOS) se utiliza para consultar una tabla de vectores de interrupción para saltar. A partir de la dirección I*256, debe de haber una tabla de 256 bytes con 128 direcciones de salto absolutas de 2 bytes cada una.
  
  De esta forma, cada dispositivo de hasta un total de 128 puede colocar su ID en el bus de datos y tener su propia ISR en la tabla:  De esta forma, cada dispositivo de hasta un total de 128 puede colocar su ID en el bus de datos y tener su propia ISR en la tabla:
Línea 157: Línea 159:
  Así pues, por convención, todos los dispositivos que se conectan a un Z80 tienen que colocar como ID de dispositivo en el bus de datos un identificador único par, que asegure que los vectores de salto de 2 dispositivos nunca puedan solaparse.  Así pues, por convención, todos los dispositivos que se conectan a un Z80 tienen que colocar como ID de dispositivo en el bus de datos un identificador único par, que asegure que los vectores de salto de 2 dispositivos nunca puedan solaparse.
  
- Existe una excepción notable a esta regla, y no es otra que la propia ULA. La ULA no está diseñada para funcionar en modo IM 2, ya que no coloca ningún identificador de dispositivo en el bus de datos cuando genera interrupciones. Está diseñada para funcionar en modo 1, donde no se espera este identificador y siempre se produce el salto a ''$0038'', sea cual sea el dispositivo que solicita la interrupción.+ Existe una excepción notable a esta regla, y no es otra que la propia ULA. La ULA no está diseñada para funcionar en modo im 2, ya que no coloca ningún identificador de dispositivo en el bus de datos cuando genera interrupciones. Está diseñada para funcionar en modo 1, donde no se espera este identificador y siempre se produce el salto a ''$0038'', sea cual sea el dispositivo que solicita la interrupción.
  
  Por suerte, cuando no se coloca ningún valor en el bus de datos del Spectrum, éste adquiere el valor de 8 señales uno (11111111b, 255d o FFh), debido a las resistencias de pull-up al que están conectadas las líneas de dicho bus. Por lo tanto, nuestro procesador Z80A obtendrá como device-id del dispositivo que interrumpe un valor FFh (con cierta particularidad que veremos en la sección sobre Compatibilidad).  Por suerte, cuando no se coloca ningún valor en el bus de datos del Spectrum, éste adquiere el valor de 8 señales uno (11111111b, 255d o FFh), debido a las resistencias de pull-up al que están conectadas las líneas de dicho bus. Por lo tanto, nuestro procesador Z80A obtendrá como device-id del dispositivo que interrumpe un valor FFh (con cierta particularidad que veremos en la sección sobre Compatibilidad).
Línea 166: Línea 168:
 ==== La interrupción de la ULA y el VSync de vídeo ==== ==== La interrupción de la ULA y el VSync de vídeo ====
  
- La ULA provee al Spectrum en modo ''IM 1'' de un mecanismo para, regularmente, escanear el teclado y evitar así que sean los propios programas quienes tengan que realizar esa tarea por software.+ La ULA provee al Spectrum en modo ''im 1'' de un mecanismo para, regularmente, escanear el teclado y evitar así que sean los propios programas quienes tengan que realizar esa tarea por software.
  
  La interrupción generada por la ULA debe de ser de una regularidad tal que se ejecute suficientes veces por segundo para que el escaneo del teclado no pierda posibles pulsaciones de teclas del usuario, pero no tan frecuente como para que requiera gran cantidad de tiempo de procesador ejecutando una y otra vez la ISR asociada.  La interrupción generada por la ULA debe de ser de una regularidad tal que se ejecute suficientes veces por segundo para que el escaneo del teclado no pierda posibles pulsaciones de teclas del usuario, pero no tan frecuente como para que requiera gran cantidad de tiempo de procesador ejecutando una y otra vez la ISR asociada.
Línea 174: Línea 176:
  Este es el precisamente el motivo por el cual la interrupción generada por la ULA se genera 50 (ó 60 veces por segundo): un aprovechamiento de la señal de VSYNC para los televisores, con el consiguiente ahorro de electrónica adicional que supondría generar otra señal adicional para INTrq.  Este es el precisamente el motivo por el cual la interrupción generada por la ULA se genera 50 (ó 60 veces por segundo): un aprovechamiento de la señal de VSYNC para los televisores, con el consiguiente ahorro de electrónica adicional que supondría generar otra señal adicional para INTrq.
  
- Por otra parte, para los programadores es una enorme ventaja el saber que la interrupción del procesador por parte de la ULA coincide con el VSYNC, ya que nos permite el uso de la instrucción HALT en nuestro programa para forzar al mismo a esperar a dicha interrupción y poder ejecutar código después del HALT que trabaje sobre la pantalla sabiendo que el haz de electrones no la está redibujando.+ Por otra parte, para los programadores es una enorme ventaja el saber que la interrupción del procesador por parte de la ULA coincide con el VSYNC, ya que nos permite el uso de la instrucción halt en nuestro programa para forzar al mismo a esperar a dicha interrupción y poder ejecutar código después del halt que trabaje sobre la pantalla sabiendo que el haz de electrones no la está redibujando.
  
  Como veremos en el capítulo dedicado a la memoria de vídeo, la ULA lee regularmente un área de aprox. 7 KB que empieza en la dirección de memoria $4000 y con los datos que hay en ese área alimenta al haz de electrones del monitor para que forme la imagen que aparece en pantalla. Como parte del proceso de generación de la imagen, 50 veces por segundo (una vez por cada "cuadro de imagen") la ULA debe de generar un pulso de VSYNC durante 256 microsegundos para el monitor que asegure que el inicio de la generación de la imagen está sincronizado con las líneas de vídeo que se le envían. Durante ese período, la ULA no está generando señal de vídeo sobre el televisor y podemos alterar el contenido de la videoram con seguridad.  Como veremos en el capítulo dedicado a la memoria de vídeo, la ULA lee regularmente un área de aprox. 7 KB que empieza en la dirección de memoria $4000 y con los datos que hay en ese área alimenta al haz de electrones del monitor para que forme la imagen que aparece en pantalla. Como parte del proceso de generación de la imagen, 50 veces por segundo (una vez por cada "cuadro de imagen") la ULA debe de generar un pulso de VSYNC durante 256 microsegundos para el monitor que asegure que el inicio de la generación de la imagen está sincronizado con las líneas de vídeo que se le envían. Durante ese período, la ULA no está generando señal de vídeo sobre el televisor y podemos alterar el contenido de la videoram con seguridad.
Línea 191: Línea 193:
 {{ cursos:ensamblador:vsync_int.png | Recorrido del haz de electrones del monitor/TV }} {{ cursos:ensamblador:vsync_int.png | Recorrido del haz de electrones del monitor/TV }}
  
- Sabemos que la interrupción generada por la ULA llega al procesador cuando se realiza el VSYNC con el monitor (cuando el haz de electrones está en el punto superior de su retroceso a la esquina superior izquierda de la pantalla), así que podemos utilizar HALT en nuestro programa para forzar al mismo a esperar una interrupción, es decir, a que se finalice el trazado del cuadro actual, asegurándonos que no se está escribiendo en pantalla. Esto nos deja un valioso pero limitado tiempo para realizar actualizaciones de la misma antes de que el haz de electrones comience el retrazado o incluso alcance el punto que queremos modificar.+ Sabemos que la interrupción generada por la ULA llega al procesador cuando se realiza el VSYNC con el monitor (cuando el haz de electrones está en el punto superior de su retroceso a la esquina superior izquierda de la pantalla), así que podemos utilizar halt en nuestro programa para forzar al mismo a esperar una interrupción, es decir, a que se finalice el trazado del cuadro actual, asegurándonos que no se está escribiendo en pantalla. Esto nos deja un valioso pero limitado tiempo para realizar actualizaciones de la misma antes de que el haz de electrones comience el retrazado o incluso alcance el punto que queremos modificar.
  
- En nuestro ejemplo anterior de la pantalla azul y verde, un HALT antes de llamar a la rutina que pinta la pantalla de verde aseguraría que la pantalla se mostrara completamente en verde (y no parcialmente) al haber realizado el cambio de los atributos tras el HALT (durante el VSYNC) y no durante el retrazado de la pantalla en sí misma.+ En nuestro ejemplo anterior de la pantalla azul y verde, un halt antes de llamar a la rutina que pinta la pantalla de verde aseguraría que la pantalla se mostrara completamente en verde (y no parcialmente) al haber realizado el cambio de los atributos tras el halt (durante el VSYNC) y no durante el retrazado de la pantalla en sí misma.
  
  Hay que tener en cuenta un detalle para temporizaciones precisas: aunque el microprocesador siempre recibe la señal de interrupción en el mismo instante de retorno del haz, la señal sólo es leída por el microprocesador al acabar la ejecución de la instrucción en curso, por lo que dependiendo del estado actual de ejecución y del tipo de instrucción (su tamaño y tiempo de ejecución) puede haber una variación de hasta 23 t-estados en el tiempo de procesado de la INT.  Hay que tener en cuenta un detalle para temporizaciones precisas: aunque el microprocesador siempre recibe la señal de interrupción en el mismo instante de retorno del haz, la señal sólo es leída por el microprocesador al acabar la ejecución de la instrucción en curso, por lo que dependiendo del estado actual de ejecución y del tipo de instrucción (su tamaño y tiempo de ejecución) puede haber una variación de hasta 23 t-estados en el tiempo de procesado de la INT.
Línea 264: Línea 266:
 bucle: bucle:
     ; hacer algo     ; hacer algo
-    JR bucle+    jr bucle
 </code> </code>
  
Línea 271: Línea 273:
 <code z80> <code z80>
 bucle: bucle:
-    HALT+    halt
  
     ; hacer algo     ; hacer algo
  
-    JR bucle+    jr bucle
 </code> </code>
  
-Sabemos que, como máximo, se ejecutará 50 veces por segundo (en Europa, 60 veces en máquinas Timex Sinclair) ya que antes de cada iteración del bucle pararemos el procesador con ''HALT'' hasta que se produzca una interrupción. Por tanto, ese bucle como máximo se ejecutará 50 veces por segundo (dependiendo del tiempo que cueste ejecutar el código que ubiquemos entre el HALT y el salto).+Sabemos que, como máximo, se ejecutará 50 veces por segundo (en Europa, 60 veces en máquinas Timex Sinclair) ya que antes de cada iteración del bucle pararemos el procesador con ''HALT'' hasta que se produzca una interrupción. Por tanto, ese bucle como máximo se ejecutará 50 veces por segundo (dependiendo del tiempo que cueste ejecutar el código que ubiquemos entre el halt y el salto).
  
 Esto funciona sin importar si estamos en un INVES o en un modelo normal, lo que varía es el tiempo del que disponemos para dibujar los sprites de nuestro juego antes de que el haz de electrones alcance el pixel (0,0). Esto funciona sin importar si estamos en un INVES o en un modelo normal, lo que varía es el tiempo del que disponemos para dibujar los sprites de nuestro juego antes de que el haz de electrones alcance el pixel (0,0).
Línea 288: Línea 290:
 ===== Las rutinas de ISR ===== ===== Las rutinas de ISR =====
  
- Hemos hablado ya de las rutinas de ISR y de cómo son llamadas 50 (o 60) veces por segundo. Lo normal en el desarrollo de un juego o programa medianamente complejo es que utilicemos el modo IM 2 y desarrollemos nuestra propia rutina ISR para que cumpla nuestras necesidades.+ Hemos hablado ya de las rutinas de ISR y de cómo son llamadas 50 (o 60) veces por segundo. Lo normal en el desarrollo de un juego o programa medianamente complejo es que utilicemos el modo im 2 y desarrollemos nuestra propia rutina ISR para que cumpla nuestras necesidades.
  
  Las ISR deben optimizarse lo máximo posible, tratando de que sean lo más rápidas y óptimas posibles, ya que nuestro programa se ha visto interrumpido y no se continuará su ejecución hasta la salida de la ISR. Si tenemos en cuenta que normalmente nuestras ISR se ejecutarán 50 veces por segundo, es importante no ralentizar la ejecución del programa principal llenando de código innecesario la ISR.  Las ISR deben optimizarse lo máximo posible, tratando de que sean lo más rápidas y óptimas posibles, ya que nuestro programa se ha visto interrumpido y no se continuará su ejecución hasta la salida de la ISR. Si tenemos en cuenta que normalmente nuestras ISR se ejecutarán 50 veces por segundo, es importante no ralentizar la ejecución del programa principal llenando de código innecesario la ISR.
Línea 294: Línea 296:
  Es crítico también que en la salida de la ISR no hayamos modificado los valores de los registros con respecto a su entrada. Para eso, podemos utilizar la pila y hacer ''PUSH'' + ''POP'' de los registros utilizados o incluso utilizar los Shadow Registers si sabemos a ciencia cierta que nuestro programa no los utiliza (con un ''EXX'' y un ''EX AF, AF<nowiki>'</nowiki>'' al principio y al final de nuestra ISR).  Es crítico también que en la salida de la ISR no hayamos modificado los valores de los registros con respecto a su entrada. Para eso, podemos utilizar la pila y hacer ''PUSH'' + ''POP'' de los registros utilizados o incluso utilizar los Shadow Registers si sabemos a ciencia cierta que nuestro programa no los utiliza (con un ''EXX'' y un ''EX AF, AF<nowiki>'</nowiki>'' al principio y al final de nuestra ISR).
  
- Al principio de nuestra ISR no es necesario desactivar las interrupciones con DI, ya que el Z80 las deshabilita al aceptar la interrupción. Debido a este ''DI'' automático realizado por el procesador, las rutinas de ISR deben incluir un ''EI'' antes del ''RET''/''RETI''.+ Al principio de nuestra ISR no es necesario desactivar las interrupciones con di, ya que el Z80 las deshabilita al aceptar la interrupción. Debido a este ''DI'' automático realizado por el procesador, las rutinas de ISR deben incluir un ''EI'' antes del ''RET''/''RETI''.
  
  Así pues, de las rutinas ISR llamadas en las interrupciones se debe de volver con una instrucción ''RETN'' en las interrupciones no enmascarables y un ''EI'' + ''RETI'' en las enmascarables (aunque en algunos casos, según el periférico que provoca la interrupción, también se puede utilizar ''EI'' + ''RET'', que es ligeramente más rápido y que tiene el mismo efecto en sistemas como el Spectrum).  Así pues, de las rutinas ISR llamadas en las interrupciones se debe de volver con una instrucción ''RETN'' en las interrupciones no enmascarables y un ''EI'' + ''RETI'' en las enmascarables (aunque en algunos casos, según el periférico que provoca la interrupción, también se puede utilizar ''EI'' + ''RET'', que es ligeramente más rápido y que tiene el mismo efecto en sistemas como el Spectrum).
  
 <code z80> <code z80>
-RETI        ; Return from interrupt (14 T-Estados). +reti        ; Return from interrupt (14 T-Estados). 
-RETN        ; Return from non-maskable interrupt (14 T-Estados).+retn        ; Return from non-maskable interrupt (14 T-Estados).
 </code> </code>
  
Línea 317: Línea 319:
 to the /change/ of the line from +5 to 0. When this happens, a call is done to the /change/ of the line from +5 to 0. When this happens, a call is done
 to address 0066h and IFF1 is reset so the routine isn't bothered by maskable to address 0066h and IFF1 is reset so the routine isn't bothered by maskable
-interrupts. The routine should end with an RETN (RETurn from Nmi) which is +interrupts. The routine should end with an retn (RETurn from Nmi) which is 
-just a usual RET, but also copies IFF2 to IFF1, so the IFFs are the same as+just a usual ret, but also copies IFF2 to IFF1, so the IFFs are the same as
 before the interrupt. before the interrupt.
  
Línea 326: Línea 328:
 At the end of a maskable interrupt, the interrupts should be enabled again. At the end of a maskable interrupt, the interrupts should be enabled again.
 You can assume that was the state of the IFFs because otherwise the interrupt You can assume that was the state of the IFFs because otherwise the interrupt
-wasn't accepted. So, an INT routine always ends with an EI and a RET +wasn't accepted. So, an INT routine always ends with an ei and a ret 
-(RETI according to the official documentation, more about that later):+(reti according to the official documentation, more about that later):
  
 INT: . INT: .
  .  .
  .  .
- EI + ei 
- RETI (or RET)+ reti (or ret)
  
-Note a fact about EI: a maskable interrupt isn't accepted directly after it, +Note a fact about ei: a maskable interrupt isn't accepted directly after it, 
-so the next opportunity for an INT is after the RETI. This is very useful;+so the next opportunity for an INT is after the reti. This is very useful;
 if the INT is still low, an INT is generated again. If this happens a lot and if the INT is still low, an INT is generated again. If this happens a lot and
-the interrupt is generated before the RETI, the stack could overflow (since +the interrupt is generated before the reti, the stack could overflow (since 
-the routine is called again and again). But this property of EI prevents this.+the routine is called again and again). But this property of ei prevents this.
  
-You can use RET in stead of RETI too, it depends on hardware setup. RETI+You can use ret in stead of reti too, it depends on hardware setup. reti
 is only useful if you have something like a Z80 PIO to support daisy-chaining: is only useful if you have something like a Z80 PIO to support daisy-chaining:
 queueing interrupts. The PIO can detect that the routine has ended by the queueing interrupts. The PIO can detect that the routine has ended by the
-opcode of RETI, and let another device generate an interrupt. That is why +opcode of reti, and let another device generate an interrupt. That is why 
-I called all the undocumented EDxx RET instructions RETN: All of them +I called all the undocumented EDxx ret instructions retn: All of them 
-operate like RETN, the only difference to RETI is its specific opcode.+operate like retn, the only difference to reti is its specific opcode.
 (Which the Z80 PIO recognises.) (Which the Z80 PIO recognises.)
 </code> </code>
  
- Es decir, para aquellos sistemas basados en Z80 con hardware PIO que soporte múltiples dispositivos I/O encadenando sus interrupciones, se define un opcode especial RETI distinto de RET de forma que el PIO pueda detectar el fin de la ISR y pueda permitir a otros dispositivos generar una interrupción.+ Es decir, para aquellos sistemas basados en Z80 con hardware PIO que soporte múltiples dispositivos I/O encadenando sus interrupciones, se define un opcode especial reti distinto de ret de forma que el PIO pueda detectar el fin de la ISR y pueda permitir a otros dispositivos generar una interrupción.
  
  En el caso del Spectrum con la ULA como (habitualmente) único dispositivo que interrumpe, se utiliza normalmente ''RET'' en lugar de ''RETI'' por ser ligeramente más rápida en ejecución. No obstante, en nuestros ejemplos hemos utilizado ''RETI'' para acomodarlos a la teoría mostrada.  En el caso del Spectrum con la ULA como (habitualmente) único dispositivo que interrumpe, se utiliza normalmente ''RET'' en lugar de ''RETI'' por ser ligeramente más rápida en ejecución. No obstante, en nuestros ejemplos hemos utilizado ''RETI'' para acomodarlos a la teoría mostrada.
  
- Como ya hemos comentado antes, EI no activa las interrupciones al acabar su ejecución, sino al acabar la ejecución de la siguiente instrucción. El motivo de esto es evitar que se pueda recibir una interrupción estando dentro de una ISR entre el ''EI'' y el ''RET'':+ Como ya hemos comentado antes, ei no activa las interrupciones al acabar su ejecución, sino al acabar la ejecución de la siguiente instrucción. El motivo de esto es evitar que se pueda recibir una interrupción estando dentro de una ISR entre el ''EI'' y el ''RET'':
  
 <code z80> <code z80>
 Nuestra_ISR: Nuestra_ISR:
     ; Hay que preservar todos los registros que se modifiquen, incluido AF     ; Hay que preservar todos los registros que se modifiquen, incluido AF
-    PUSH XX+    push XX
  
     (código de la ISR)     (código de la ISR)
  
     ; Y recuperar su valor antes de finalizar la ISR     ; Y recuperar su valor antes de finalizar la ISR
-    POP XX +    pop XX 
-    EI +    ei 
-    RETI+    reti
 </code> </code>
  
- Si ''EI'' habilitara las interrupciones de forma instantánea y se recibiera una interrupción entre la instrucción ''EI'' y el ''RETI'', se volvería a entrar en la ISR, y por lo tanto se volvería a realizar el ''PUSH'' de ''PC'' y el ''PUSH'' de HL, rompiendo el flujo correcto del programa. Por contra, tal y como funciona EI sólo se habilitarán de nuevo las interrupciones tras la ejecución de ''RETI'' y la recuperación de ''PC'' de la pila, permitiendo así la ejecución de una nueva interrupción sin corromper el contenido del stack.+ Si ''EI'' habilitara las interrupciones de forma instantánea y se recibiera una interrupción entre la instrucción ''EI'' y el ''RETI'', se volvería a entrar en la ISR, y por lo tanto se volvería a realizar el ''PUSH'' de ''PC'' y el ''PUSH'' de HL, rompiendo el flujo correcto del programa. Por contra, tal y como funciona ei sólo se habilitarán de nuevo las interrupciones tras la ejecución de ''RETI'' y la recuperación de ''PC'' de la pila, permitiendo así la ejecución de una nueva interrupción sin corromper el contenido del stack.
  
- Finalmente, es importantísimo en los modelos de más de 16K (48K y 128K paginados) utilizar una tabla de vector de interrupciones ubicada en la página superior de la RAM (memoria por encima de los 32K), ya que la utilización del bloque inferior de memoria (dejando de lado la ROM, el bloque desde 16K a 32K) provocaría un efecto nieve en la pantalla. La elección estándar de la dirección de la tabla de vector de interrupciones recae en direcciones a partir de $FE00, por motivos que veremos al hablar sobre la ULA. Por otra parte, en la sección de //Consideraciones y Curiosidades// veremos cómo solucionar este problema en sistemas de 16K de memoria RAM.+ Finalmente, es importantísimo en los modelos de más de 16K (48K y 128K paginados) utilizar una tabla de vector de interrupciones ubicada en la página superior de la RAM (memoria por encima de los 32K), ya que la utilización del bloque inferior de memoria (dejando de lado la ROM, el bloque desde 16K a 32K) provocaría un efecto nieve en la pantalla. La elección estándar de la dirección de la tabla de vector de interrupciones recae en direcciones a partir de $fe00, por motivos que veremos al hablar sobre la ULA. Por otra parte, en la sección de //Consideraciones y Curiosidades// veremos cómo solucionar este problema en sistemas de 16K de memoria RAM.
  
- Ninguna de las instrucciones que hemos visto (''RST XX'', ''EI'', ''DI'', ''RETI'', ''RETN'', ''HALT'' o ''IM XX'') produce afectación alguna en los flags, salvo ''LD AR'', que altera el flag P/V con la utilidad que ya hemos visto.+ Ninguna de las instrucciones que hemos visto (''RST XX'', ''EI'', ''DI'', ''RETI'', ''RETN'', ''HALT'' o ''IM XX'') produce afectación alguna en los flags, salvo ''ld ar'', que altera el flag P/V con la utilidad que ya hemos visto.
  
 \\ \\
 ===== La ISR de IM 1 ===== ===== La ISR de IM 1 =====
  
- A modo de curiosidad, vamos a ver el código de la ISR que se ejecuta en modo 1 (''RST $38''), tomado del documento **The Complete Spectrum ROM Disassembly** con alguna modificación en los comentarios:+ A modo de curiosidad, vamos a ver el código de la ISR que se ejecuta en modo 1 (''rst $38''), tomado del documento **The Complete Spectrum ROM Disassembly** con alguna modificación en los comentarios:
  
 <code z80> <code z80>
Línea 387: Línea 389:
 ; FRAMES = 3 bytes variable. ; FRAMES = 3 bytes variable.
  
-; BYTES 1 & 2 FRAMES -> $5C78 and $5C79 +; BYTES 1 & 2 FRAMES -> $5c78 and $5c79 
-; BYTE 3 FRAMES      -> IY+40h = $5C7A+; BYTE 3 FRAMES      -> IY+40h = $5c7a
 ; ;
 0038 MASK-INT: 0038 MASK-INT:
-                  PUSH  AF                  ; Save the current values held in +                  push  af                  ; Save the current values held in 
-                  PUSH  HL                  ; these registers. +                  push  hl                  ; these registers. 
-                  LD    HL,($5C78)          ; The lower two bytes of the+                  ld    hl,($5c78)          ; The lower two bytes of the
  
-                  INC   HL                  ; frame counter are incremented (FRAMES) +                  inc   hl                  ; frame counter are incremented (FRAMES) 
-                  LD    ($5C78),HL          ; every 20 ms. (UK) -> INC BYTE_1_2(FRAMES) +                  ld    ($5c78),hl          ; every 20 ms. (UK) -> inc bYTE_1_2(FRAMES) 
-                  LD    A,                ; The highest byte of the frame counter is +                  ld    a,                ; The highest byte of the frame counter is 
-                  OR                      ; only incremented when the value +                  or                      ; only incremented when the value 
-                  JR    NZ,KEY-INT          ; of the lower two bytes is zero +                  jr    nz,KEY-INT          ; of the lower two bytes is zero 
-                  INC   (IY+40h)            ; INC BYTE_3(FRAMES) ($5C7A)+                  inc   (iy+$40)            ; inc bYTE_3(FRAMES) ($5c7a)
 0048 KEY-INT: 0048 KEY-INT:
-                  PUSH  BC                  ; Save the current values held +                  push  bc                  ; Save the current values held 
-                  PUSH  DE                  ; in these registers. +                  push  de                  ; in these registers. 
-                  CALL  KEYBOARD            ; Now scan the keyboard. (CALL $02BF+                  call  KEYBOARD            ; Now scan the keyboard. (call $02bf
-                  POP   DE                  ; Restore the values. +                  pop   de                  ; Restore the values. 
-                  POP   BC +                  pop   bc 
-                  POP   HL +                  pop   hl 
-                  POP   AF +                  pop   af 
-                  EI                        ; The maskable interrupt is en- +                  ei                        ; The maskable interrupt is en- 
-                  RET                       ; abled before returning.+                  ret                       ; abled before returning.
  
 </code> </code>
Línea 416: Línea 418:
  Nótese cómo la ISR del modo 1 se ajusta a lo visto hasta ahora: se preserva cualquier registro que pueda utilizarse dentro de la misma, se reduce el tamaño y tiempo de ejecución de la ISR en la medida de lo posible, y se vuelve con un ''EI''+''RET''.  Nótese cómo la ISR del modo 1 se ajusta a lo visto hasta ahora: se preserva cualquier registro que pueda utilizarse dentro de la misma, se reduce el tamaño y tiempo de ejecución de la ISR en la medida de lo posible, y se vuelve con un ''EI''+''RET''.
  
- La rutina actualiza el valor de la variable del sistema ''FRAMES'' (que viene a ser el equivalente de la variable "abs_ticks" del ejemplo que veremos en el siguiente apartado) y llama a la rutina de la ROM ''KEYBOARD'' ($02BF) que es la encargada de chequear el estado del teclado y actualizar ciertas variables del sistema para que el intérprete BASIC (o nuestros programas si corren en IM 1) pueda gestionar las pulsaciones de teclado realizadas por el usuario. Si bien la rutina ''KEYBOARD'' a la que se llama desde la ISR no es todo lo "pequeña" que se podría esperar de algo que se va a ejecutar en una ISR, sí que es cierto que es una de las partes primordiales del intérprete BASIC y que es más óptimo y rápido obtener el estado del teclado en la ISR (aunque la rutina sea larga e interrumpa 50 veces por segundo a nuestro programa con su ejecución) que tener que realizar la lectura del teclado dentro del propio intérprete de forma continuada.+ La rutina actualiza el valor de la variable del sistema ''FRAMES'' (que viene a ser el equivalente de la variable "abs_ticks" del ejemplo que veremos en el siguiente apartado) y llama a la rutina de la ROM ''KEYBOARD'' ($02bf) que es la encargada de chequear el estado del teclado y actualizar ciertas variables del sistema para que el intérprete BASIC (o nuestros programas si corren en im 1) pueda gestionar las pulsaciones de teclado realizadas por el usuario. Si bien la rutina ''KEYBOARD'' a la que se llama desde la ISR no es todo lo "pequeña" que se podría esperar de algo que se va a ejecutar en una ISR, sí que es cierto que es una de las partes primordiales del intérprete BASIC y que es más óptimo y rápido obtener el estado del teclado en la ISR (aunque la rutina sea larga e interrumpa 50 veces por segundo a nuestro programa con su ejecución) que tener que realizar la lectura del teclado dentro del propio intérprete de forma continuada.
  
- Nótese, como nos apunta //metalbrain// en los foros de Speccy.org, que ''FRAMES'' es una variable de 3 bytes y que esta rutina ISR utiliza IY para acceder al tercer byte de esta variable cuando el incremento de los 2 bytes más bajos requieren el incremente del tercer byte. Lo hace a través de IY+40h y esto explica porqué desde BASIC, bajo IM1, no debemos utilizar código ASM que haga uso de IY, bajo riesgo de que este ''INC'' pueda realizarse sobre un valor de IY que no sea el esperado y por tanto "corromper" un byte de código de nuestro programa o de datos, pantalla, etc.+ Nótese, como nos apunta //metalbrain// en los foros de Speccy.org, que ''FRAMES'' es una variable de 3 bytes y que esta rutina ISR utiliza IY para acceder al tercer byte de esta variable cuando el incremento de los 2 bytes más bajos requieren el incremente del tercer byte. Lo hace a través de IY+40h y esto explica porqué desde BASIC, bajo im1, no debemos utilizar código ASM que haga uso de IY, bajo riesgo de que este ''INC'' pueda realizarse sobre un valor de IY que no sea el esperado y por tanto "corromper" un byte de código de nuestro programa o de datos, pantalla, etc.
  
 \\ \\
 ===== ISR de atención a la ULA en IM 2 ===== ===== ISR de atención a la ULA en IM 2 =====
  
- La clave de este capítulo, y la principal utilidad del uso de interrupciones en nuestros programas es la de aprovechar las interrupciones que la ULA genera 50 veces por segundo (60 en América) en el modo ''IM 2''.+ La clave de este capítulo, y la principal utilidad del uso de interrupciones en nuestros programas es la de aprovechar las interrupciones que la ULA genera 50 veces por segundo (60 en América) en el modo ''im 2''.
  
- Como hemos explicado anteriormente, antes de pasar al modo ''IM 2'', nosotros somos los responsables de generar la tabla de vectores de interrupción con los valores a los que el procesador debe de saltar en caso de recibir una interrupción de un dispositivo externo. Para eso, generamos en memoria una tabla de 257 bytes y en ella introducimos las direcciones de salto de las ISR.+ Como hemos explicado anteriormente, antes de pasar al modo ''im 2'', nosotros somos los responsables de generar la tabla de vectores de interrupción con los valores a los que el procesador debe de saltar en caso de recibir una interrupción de un dispositivo externo. Para eso, generamos en memoria una tabla de 257 bytes y en ella introducimos las direcciones de salto de las ISR.
  
- En un Spectrum estándar sin dispositivos conectados al bus de expansión sólo recibiremos interrupciones generadas por la ULA, **con un device_id teórico de valor $FF** (este device-id es también el causante de que la tabla sea de 257 bytes y no de 256).+ En un Spectrum estándar sin dispositivos conectados al bus de expansión sólo recibiremos interrupciones generadas por la ULA, **con un device_id teórico de valor $ff** (este device-id es también el causante de que la tabla sea de 257 bytes y no de 256).
  
- En realidad, técnicamente hablando, la ULA no pone ningún valor en el bus de datos como sí haría un dispositivo hardware conectado para "identificarse". Lo que ocurre es que las resistencias de "pull-up" en la circuitería del Spectrum hacen que por defecto ese valor sea $FF.+ En realidad, técnicamente hablando, la ULA no pone ningún valor en el bus de datos como sí haría un dispositivo hardware conectado para "identificarse". Lo que ocurre es que las resistencias de "pull-up" en la circuitería del Spectrum hacen que por defecto ese valor sea $ff.
  
- Si sólo vamos a recibir un $FF en el bus de datos cada vez que se ejecute una interrupción, el Z80 irá a buscar la dirección de la ISR a (I*256) + $FF, por lo que si queremos nuestra tabla de vectores de salto en $FE (I=$FE), en realidad no necesitamos una tabla, sino que sólo necesitamos poner la dirección de nuestra ISR en $FEFF y $FEFF+1.+ Si sólo vamos a recibir un $ff en el bus de datos cada vez que se ejecute una interrupción, el Z80 irá a buscar la dirección de la ISR a (I*256) + $ff, por lo que si queremos nuestra tabla de vectores de salto en $fe (I=$fe), en realidad no necesitamos una tabla, sino que sólo necesitamos poner la dirección de nuestra ISR en $feFF y $feFF+1.
  
- Como veremos luego, esto es en realidad un error y funciona en prácticamente la totalidad de los Spectrums europeos, pero no en modelos Timex Sinclair, en algunos clones o en Spectrums con algún dispositivo conectado que modifique este valor $FF por parte de la ULA. Lo normal es que con este método todo funcione correctamente y vamos a verlo, porque se ha utilizado tal cual en muchísimo software de Spectrum.+ Como veremos luego, esto es en realidad un error y funciona en prácticamente la totalidad de los Spectrums europeos, pero no en modelos Timex Sinclair, en algunos clones o en Spectrums con algún dispositivo conectado que modifique este valor $ff por parte de la ULA. Lo normal es que con este método todo funcione correctamente y vamos a verlo, porque se ha utilizado tal cual en muchísimo software de Spectrum.
  
-Veamos pues cómo instalar una rutina ISR que se ejecute cada vez que se recibe una interrupción de la ULA asumiendo que pone en el bus de datos el valor $FF. En teoría deberíamos generar nuestra tabla de vectores de interrupción a partir de una posición de memoria como, por ejemplo, $FE00. Pero sabiendo que la segunda parte de la dirección va a ser $FF, sólo necesitamos modificar $FE00 y $FEFF+1 ($FF00).+Veamos pues cómo instalar una rutina ISR que se ejecute cada vez que se recibe una interrupción de la ULA asumiendo que pone en el bus de datos el valor $ff. En teoría deberíamos generar nuestra tabla de vectores de interrupción a partir de una posición de memoria como, por ejemplo, $fe00. Pero sabiendo que la segunda parte de la dirección va a ser $ff, sólo necesitamos modificar $fe00 y $feff+1 ($ff00).
  
- Para atender a las interrupciones generadas por la ULA (device_id $FF), tendremos que realizar los siguientes pasos:+ Para atender a las interrupciones generadas por la ULA (device_id $ff), tendremos que realizar los siguientes pasos:
  
 \\ \\
Línea 444: Línea 446:
   * Desactivar las interrupciones con ''DI''.   * Desactivar las interrupciones con ''DI''.
  
-  * Colocar la dirección de nuestra ISR en las posiciones de memoria $FEFF y $FEFF+1 (que es de donde leerá el Z80 la dirección de salto cuando reciba la interrupción con ID $FF).+  * Colocar la dirección de nuestra ISR en las posiciones de memoria $feff y $feff+1 (que es de donde leerá el Z80 la dirección de salto cuando reciba la interrupción con ID $ff).
  
-  * Asignar a I el valor $FE y saltar a ''IM 2'' (de esta forma, le decimos al Z80 que la tabla de vectores de interrupción empieza en $FE00).+  * Asignar a I el valor $fe y saltar a ''im 2'' (de esta forma, le decimos al Z80 que la tabla de vectores de interrupción empieza en $fe00).
  
   * Activar las interrupciones con ''EI''.   * Activar las interrupciones con ''EI''.
Línea 459: Línea 461:
  
     ; Instalamos la ISR:     ; Instalamos la ISR:
-    LD HL, ISR_ASM_ROUTINE        ; HL = direccion de la rutina de ISR +    ld hl, ISR_ASM_ROUTINE        ; HL = direccion de la rutina de ISR 
-    DI                            ; Deshabilitamos las interrupciones +    di                            ; Deshabilitamos las interrupciones 
-    LD ($FEFF), HL                ; Guardamos en (65279 = $FEFF) la direccion +    ld ($feff), hl                ; Guardamos en (65279 = $feff) la direccion 
-    LD A, $FE                     ; de la rutina ISR_ASM_ROUTINE +    ld a, $fe                     ; de la rutina ISR_ASM_ROUTINE 
-    LD I                      ; Colocamos en I el valor $FE +    ld i                      ; Colocamos en I el valor $fe 
-    IM 2                          ; Saltamos al modo de interrupciones 2 +    im 2                          ; Saltamos al modo de interrupciones 2 
-    EI+    ei
  
     (resto programa)     (resto programa)
Línea 471: Línea 473:
 ;--- Rutina de ISR. --------------------------------------------- ;--- Rutina de ISR. ---------------------------------------------
 ISR_ASM_ROUTINE: ISR_ASM_ROUTINE:
-    PUSH AF +    push af 
-    PUSH HL +    push hl 
-    PUSH ..+    push ...
  
     (código de la ISR)     (código de la ISR)
  
-    POP .. +    pop ... 
-    POP HL +    pop hl 
-    POP AF+    pop af
  
-    EI +    ei 
-    RETI+    reti
 </code> </code>
  
- De esta forma, saltamos a ''IM 2'' y el procesador se encargará de ejecutar la rutina ISR_ASM_ROUTINE 50 veces por segundo, por el siguiente proceso:+ De esta forma, saltamos a ''im 2'' y el procesador se encargará de ejecutar la rutina ISR_ASM_ROUTINE 50 veces por segundo, por el siguiente proceso:
  
 \\ \\
   * La ULA provoca una señal de interrupción enmascarable INT.   * La ULA provoca una señal de interrupción enmascarable INT.
  
-  * La ULA no coloca ningún device ID en el bus de datos; debido a las resistencias de pull-up, el bus de datos toma el valor $FF.+  * La ULA no coloca ningún device ID en el bus de datos; debido a las resistencias de pull-up, el bus de datos toma el valor $ff.
  
   * El procesador termina de ejecutar la instrucción en curso y, si las interrupciones están actualmente habilitadas procesa la interrupción.   * El procesador termina de ejecutar la instrucción en curso y, si las interrupciones están actualmente habilitadas procesa la interrupción.
  
-  * El procesador lee del bus de datos el valor $FE y, al estar en modo 2, compone junto al registro una dirección "$FEFF" (campo $FF dentro de la tabla de vectores de interrupción que empieza en $FE*256 = $FE00).+  * El procesador lee del bus de datos el valor $fe y, al estar en modo 2, compone junto al registro una dirección "$feFF" (campo $ff dentro de la tabla de vectores de interrupción que empieza en $fe*256 = $fe00).
  
-  * El procesador lee la dirección de 16 bits que se compone con el contenido de las celdillas de memoria $FEFF y $FEFF+1 ($FF00). Esta dirección de 16 bits es la que hemos cargado nosotros con ''LD ($FEFF), HL'' y que apunta a nuestra rutina ISR.+  * El procesador lee la dirección de 16 bits que se compone con el contenido de las celdillas de memoria $feff y $feff+1 ($ff00). Esta dirección de 16 bits es la que hemos cargado nosotros con ''ld ($feff), hl'' y que apunta a nuestra rutina ISR.
  
   * El procesador salta a la dirección de 16 bits compuesta, que es la dirección de nuestra ISR.   * El procesador salta a la dirección de 16 bits compuesta, que es la dirección de nuestra ISR.
  
-  * Se ejecuta la ISR, de la cual salimos con EI+RETI, provocando la continuación de la ejecución del programa original hasta la siguiente interrupción.+  * Se ejecuta la ISR, de la cual salimos con ei+reti, provocando la continuación de la ejecución del programa original hasta la siguiente interrupción.
 \\ \\
  
- Como hemos dicho en el apartado sobre las ISR, es crítico que la tabla de vectores de interrupción se ubique en una página alta de la RAM, es decir; que no esté dentro del área comprendida entre los 16K y los 32K que el procesador y la ULA comparten regularmente para que la ULA pueda actualizar la pantalla. En todos los ejemplos que hemos visto y veremos, la dirección de la tabla de vectores de interrupción comienza a partir de $FE00. Ubicarla a partir de $FF00 (que es la única página más alta que $FE00) no sería una elección apropiada puesto que al necesitar una tabla de 257 bytes para la ULA (device ID=$FF), parte de la dirección de salto se compondría con "$FFFF +1 = $0000" (la ROM).+ Como hemos dicho en el apartado sobre las ISR, es crítico que la tabla de vectores de interrupción se ubique en una página alta de la RAM, es decir; que no esté dentro del área comprendida entre los 16K y los 32K que el procesador y la ULA comparten regularmente para que la ULA pueda actualizar la pantalla. En todos los ejemplos que hemos visto y veremos, la dirección de la tabla de vectores de interrupción comienza a partir de $fe00. Ubicarla a partir de $ff00 (que es la única página más alta que $fe00) no sería una elección apropiada puesto que al necesitar una tabla de 257 bytes para la ULA (device ID=$ff), parte de la dirección de salto se compondría con "$ffFF +1 = $0000" (la ROM).
  
-Finalmente destacar que (aunque no es lo habitual) desde dentro de nuestra rutina ISR en IM2 podemos llamar a ''RST $38'' (la rutina que sería ejecutada por IM1) si queremos que ésta actualice determinadas variables del sistema, siempre y cuando no usemos el registro IY en nuestro programa. En ese caso, recordemos que hay que preservar todos los registros:+Finalmente destacar que (aunque no es lo habitual) desde dentro de nuestra rutina ISR en im2 podemos llamar a ''rst $38'' (la rutina que sería ejecutada por im1) si queremos que ésta actualice determinadas variables del sistema, siempre y cuando no usemos el registro IY en nuestro programa. En ese caso, recordemos que hay que preservar todos los registros:
  
 <code z80> <code z80>
 Rutina_ISR: Rutina_ISR:
-    PUSH AF             ; Preservamos todos los registros +    push af             ; Preservamos todos los registros 
-    PUSH BC +    push bc 
-    PUSH HL +    push hl 
-    PUSH DE +    push de 
-    PUSH IX             ; IX tambien. +    push ix             ; IX tambien. 
-                        ; IY no lo podemos tocar si usamos RST $38.+                        ; IY no lo podemos tocar si usamos rst $38.
  
     ; Aqui hacemos nuestras cosas, como actualizar contadores     ; Aqui hacemos nuestras cosas, como actualizar contadores
Línea 520: Línea 522:
     (...)     (...)
  
-    RST $38             ; Lectura del teclado, actualización de FRAMES+    rst $38             ; Lectura del teclado, actualización de FRAMES
  
-    POP IX +    pop ix 
-    POP DE +    pop de 
-    POP HL +    pop hl 
-    POP BC +    pop bc 
-    POP AF              ; restauramos todos los registros +    pop af              ; restauramos todos los registros 
-    EI +    ei 
-    RETI+    reti
 </code> </code>
  
-Es posible que no queramos llamar a IM1 para evitar tareas superflúas para nosotros como la lectura del teclado por parte de la ROM, pero sí que queramos que se siga actualizando la variable ''FRAMES''.+Es posible que no queramos llamar a im1 para evitar tareas superflúas para nosotros como la lectura del teclado por parte de la ROM, pero sí que queramos que se siga actualizando la variable ''FRAMES''.
  
-Ese ese caso, podemos actualizarla nosotros copiando parte del código de la IM1 dentro de nuestra ISR. Así es como lo incrementa la rutina real de la ROM ($0038):+Ese ese caso, podemos actualizarla nosotros copiando parte del código de la im1 dentro de nuestra ISR. Así es como lo incrementa la rutina real de la ROM ($0038):
  
 <code z80> <code z80>
-    LD    HL,($5C78)          ; The lower two bytes of the +    ld    hl,($5c78)          ; The lower two bytes of the 
-    INC   HL                  ; frame counter are incremented (FRAMES) +    inc   hl                  ; frame counter are incremented (FRAMES) 
-    LD    ($5C78),HL          ; every 20 ms. (UK) -> INC BYTE_1_2(FRAMES) +    ld    ($5c78),hl          ; every 20 ms. (UK) -> inc bYTE_1_2(FRAMES) 
-    LD    A,                ; The highest byte of the frame counter is +    ld    a,                ; The highest byte of the frame counter is 
-    OR                      ; only incremented when the value +    or                      ; only incremented when the value 
-    JR    NZ,frames_no_inc    ; of the lower two bytes is zero +    jr    nz,frames_no_inc    ; of the lower two bytes is zero 
-    INC   (IY+40h)            ; INC BYTE_3(FRAMES) ($5C7A)+    INC   (IY+40h)            ; inc bYTE_3(FRAMES) ($5c7a)
 frames_no_inc: frames_no_inc:
 </code> </code>
Línea 549: Línea 551:
  
 <code z80> <code z80>
-    LD HL, $5C78              ; The lower two bytes of the +    ld hl, $5c78              ; The lower two bytes of the 
-    INC (HL)                  ; frame counter are incremented (FRAMES)+    inc (hl)                  ; frame counter are incremented (FRAMES)
 </code> </code>
  
- O bien no usamos IY y simulamos el mismo código pero incrementando el valor de $5C7A con HL:+ O bien no usamos IY y simulamos el mismo código pero incrementando el valor de $5c7a con HL:
  
 <code z80> <code z80>
-    LD HL, ($5C78+    ld hl, ($5c78
-    INC HL +    inc hl 
-    LD ($5C78), HL +    ld ($5c78), hl 
-    LD AH +    ld ah 
-    OR L +    or l 
-    JR NZ, frames_no_inc +    jr nz, frames_no_inc 
-    LD HL, $5C7A           ; O "INC HL" + "INC HL" (+2 t-states -1 byte) +    ld hl, $5c7a           ; O "inc hl" + "inc hl" (+2 t-states -1 byte) 
-    INC (HL)               ; INC BYTE_3(FRAMES) ($5C7A)+    inc (hl)               ; inc bYTE_3(FRAMES) ($5c7a)
 frames_no_inc: frames_no_inc:
 </code> </code>
Línea 574: Línea 576:
 A continuación se muestra un ejemplo completo de ISR que implementa un contador de segundos. A continuación se muestra un ejemplo completo de ISR que implementa un contador de segundos.
  
-Al principio del programa, se escribe en $FEFF la dirección de nuestra ISR y se cambia a IM2 con A en $FE para que se lea (en un Spectrum estándar) el valor $FEFF como "vector único de ISR".+Al principio del programa, se escribe en $feff la dirección de nuestra ISR y se cambia a im2 con A en $fe para que se lea (en un Spectrum estándar) el valor $feff como "vector único de ISR".
  
-En la ISR incrementamos el valor de la variable ''ticks'' y cuando esta llega a 50, incrementamos el número de segundos y a ticks lo hacemos de nuevo 0 (en realidad, le restamos 50, por si hubiese sido incrementado externamente, pero podríamos simplemente haber hecho un ''XOR A'').+En la ISR incrementamos el valor de la variable ''ticks'' y cuando esta llega a 50, incrementamos el número de segundos y a ticks lo hacemos de nuevo 0 (en realidad, le restamos 50, por si hubiese sido incrementado externamente, pero podríamos simplemente haber hecho un ''xor a'').
  
 Ese código de ISR está corriendo de forma automática 50 veces por segundo, por lo que ''ticks'' y ''seconds'' se están modificando por la ISR. El código principal de nuestro programa simplemente, en un bucle, cambia el cursor a 0,0 y muestra el valor de ''seconds''. Ese código de ISR está corriendo de forma automática 50 veces por segundo, por lo que ''ticks'' y ''seconds'' se están modificando por la ISR. El código principal de nuestro programa simplemente, en un bucle, cambia el cursor a 0,0 y muestra el valor de ''seconds''.
Línea 584: Línea 586:
     ORG 50000     ORG 50000
  
-    CALL CLS+    call CLS
  
     ; Instalamos la ISR:     ; Instalamos la ISR:
-    LD HL, CLOCK_ISR_ASM_ROUTINE +    ld hl, CLOCK_ISR_ASM_ROUTINE 
-    DI                            ; Desactivar interrupciones +    di                            ; Desactivar interrupciones 
-    LD ($FEFF), HL                ; Guardamos en ($FEFF) la direccion +    ld ($feff), hl                ; Guardamos en ($feff) la direccion 
-    LD A, $FE                     ; de la rutina CLOCK_ISR_ASM_ROUTINE +    ld a, $fe                     ; de la rutina CLOCK_ISR_ASM_ROUTINE 
-    LD I                      ; I = $FE => direccion de ISR en $FEFF +    ld i                      ; I = $fe => direccion de ISR en $feFF 
-    IM 2                          ; Cambiar a IM +    im 2                          ; Cambiar a im 
-    EI                            ; Activar interrupciones+    ei                            ; Activar interrupciones
  
-    JR ImprimirNumero             ; Hacemos la 1a impresion: "00"+    jr ImprimirNumero             ; Hacemos la 1a impresion: "00"
  
 Bucle: Bucle:
-    HALT+    halt
  
 ImprimirNumero: ImprimirNumero:
-    LD DE, 0 +    ld de, 0 
-    CALL CursorAt                 ; Cursor en (0,0)+    call CursorAt                 ; Cursor en (0,0)
  
-    LD A, (seconds) +    ld a, (seconds) 
-    CALL PrintNum2digits+    call PrintNum2digits
  
-    JR Bucle                      ; Repetir indefinidamente+    jr Bucle                      ; Repetir indefinidamente
  
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
Línea 615: Línea 617:
  
 CLOCK_ISR_ASM_ROUTINE: CLOCK_ISR_ASM_ROUTINE:
-    PUSH AF +    push af 
-    PUSH HL +    push hl 
-    LD A, (ticks) +    ld a, (ticks) 
-    INC A +    inc a 
-    LD (ticks), A +    ld (ticks), a 
-    CP INTS_PER_SECOND +    cp INTS_PER_SECOND 
-    JR CFRAMES_menor_que_50+    jr cframes_menor_que_50
  
-    SUB INTS_PER_SECOND       ; Restamos 50 a A (no lo hago 0 directamente+    sub INTS_PER_SECOND       ; Restamos 50 a A (no lo hago 0 directamente
                               ; por si alguien incremento su valor externamente)                               ; por si alguien incremento su valor externamente)
  
-    LD HL, seconds +    ld hl, seconds 
-    INC (HL)                  ; Incrementamos segundos+    inc (hl)                  ; Incrementamos segundos
  
-FRAMES_menor_que_50+frames_menor_que_50
-    LD (ticks), A +    ld (ticks), a 
-    POP HL +    pop hl 
-    POP AF +    pop af 
-    EI +    ei 
-    RETI+    reti
  
 ticks              DB    0 ticks              DB    0
Línea 654: Línea 656:
 ===== IM2 implementado con Tabla de Vectores de Interrupción ===== ===== IM2 implementado con Tabla de Vectores de Interrupción =====
  
-Como ya hemos dicho, la ULA no está preparada para funcionar en ''IM 2'' y por lo tanto no coloca ningún valor en el bus de datos antes de generar una señal de interrupción. Debido a las resistencias pull-up, el valor "por defecto" de este bus es $FFh (11111111b), que es el valor que hemos utilizado en nuestros anteriores ejemplos para diseñar una ISR que se ejecute las 50 (ó 60) veces por segundo que interrumpe la ULA al Z80A. Es por eso que en los ejemplos anteriores estamos escribiendo la dirección de nuestra ISR en ($FEFF y $FEFF+1).+Como ya hemos dicho, la ULA no está preparada para funcionar en ''im 2'' y por lo tanto no coloca ningún valor en el bus de datos antes de generar una señal de interrupción. Debido a las resistencias pull-up, el valor "por defecto" de este bus es $ffh (11111111b), que es el valor que hemos utilizado en nuestros anteriores ejemplos para diseñar una ISR que se ejecute las 50 (ó 60) veces por segundo que interrumpe la ULA al Z80A. Es por eso que en los ejemplos anteriores estamos escribiendo la dirección de nuestra ISR en ($feff y $feff+1).
  
- Nuestro pequeño truco de escribir la dirección de nuestra ISR en $FEFF es sencillo y funciona, pero no es la manera correcta de hacer las cosas. Algunos clones de Spectrum, con diferente circuitería electrónica, no tienen por qué dejar como "valor por defecto" $FF en el bus de datos. Es más, un Spectrum estándar, europeo, con algún dispositivo hardware conectado al Slot de expansión trasero, podría modificar ese valor.+ Nuestro pequeño truco de escribir la dirección de nuestra ISR en $feff es sencillo y funciona, pero no es la manera correcta de hacer las cosas. Algunos clones de Spectrum, con diferente circuitería electrónica, no tienen por qué dejar como "valor por defecto" $ff en el bus de datos. Es más, un Spectrum estándar, europeo, con algún dispositivo hardware conectado al Slot de expansión trasero, podría modificar ese valor.
  
-El mismo problema sucede en ciertos modelos de Timex Sinclair, como por ejemplo el TS2068, el cual no tiene las resistencias de pull-up conectadas a las líneas del bus de datos por lo que el valor que aparezca en dicho bus puede ser totalmente arbitrario o aleatorio. Los programas anteriores de ejemplo, que ubicaba la ISR en $FEFF (asumiendo el device-id de $FF), no tendría asegurada la compatibilidad con este modelo.+El mismo problema sucede en ciertos modelos de Timex Sinclair, como por ejemplo el TS2068, el cual no tiene las resistencias de pull-up conectadas a las líneas del bus de datos por lo que el valor que aparezca en dicho bus puede ser totalmente arbitrario o aleatorio. Los programas anteriores de ejemplo, que ubicaba la ISR en $feff (asumiendo el device-id de $ff), no tendría asegurada la compatibilidad con este modelo.
  
 Con un valor aleatorio en el bus de datos, el procesador podría saltar a cualquiera de las direcciones de la tabla de vectores de interrupción. Con un valor aleatorio en el bus de datos, el procesador podría saltar a cualquiera de las direcciones de la tabla de vectores de interrupción.
Línea 664: Línea 666:
  Una primera aproximación a solucionar este problema podría ser la de introducir la misma dirección de salto (la de nuestra rutina ISR) en las 128 direcciones de salto de la tabla de vectores. De esta forma, fuera cual fuera el valor en el bus de datos, el procesador siempre saltaría a nuestra ISR.  Una primera aproximación a solucionar este problema podría ser la de introducir la misma dirección de salto (la de nuestra rutina ISR) en las 128 direcciones de salto de la tabla de vectores. De esta forma, fuera cual fuera el valor en el bus de datos, el procesador siempre saltaría a nuestra ISR.
  
- El problema es que tampoco podemos determinar si este valor aleatorio en el bus es par o impar, de forma que si fuera par saltaría a la dirección correcta de uno de los vectores ($FE00), mientras que si fuera impar saltaría a una dirección incorrecta compuesta por parte de la dirección de un device-id, y parte de la dirección del otro ($00FE), como ya vimos en un apartado anterior.+ El problema es que tampoco podemos determinar si este valor aleatorio en el bus es par o impar, de forma que si fuera par saltaría a la dirección correcta de uno de los vectores ($fe00), mientras que si fuera impar saltaría a una dirección incorrecta compuesta por parte de la dirección de un device-id, y parte de la dirección del otro ($00fe), como ya vimos en un apartado anterior.
  
- La forma de solucionar esta problemática es bastante curiosa y original: basta con ubicar nuestra ISR en una dirección donde coincidan la parte alta y la parte baja de la misma, y rellenar la tabla de vectores de interrupción con este valor. Por ejemplo, en el compilador de C z88dk y la librería SPlib se utiliza para su ISR la dirección $F1F1. De esta forma, la tabla de vectores de interrupción se llena con 257 valores "$F1". Así, sea cual sea el valor que tome el bus de datos cuando se recibe la interrupción (y sea par o impar), el procesador siempre saltará a $F1F1, donde estará nuestra ISR.+ La forma de solucionar esta problemática es bastante curiosa y original: basta con ubicar nuestra ISR en una dirección donde coincidan la parte alta y la parte baja de la misma, y rellenar la tabla de vectores de interrupción con este valor. Por ejemplo, en el compilador de C z88dk y la librería SPlib se utiliza para su ISR la dirección $f1f1. De esta forma, la tabla de vectores de interrupción se llena con 257 valores "$f1". Así, sea cual sea el valor que tome el bus de datos cuando se recibe la interrupción (y sea par o impar), el procesador siempre saltará a $f1f1, donde estará nuestra ISR.
  
 <code> <code>
Línea 673: Línea 675:
 Posición    Valor Posición    Valor
 -------------------- --------------------
-($FE00)    -   $F1 +($fe00)    -   $f1 
-($FE01)    -   $F1 +($fe01)    -   $f1 
-($FE02)    -   $F1 +($fe02)    -   $f1 
-($FE03)    -   $F1 +($fe03)    -   $f1 
-(...)      -   $F1 +(...)      -   $f1 
-($FEFF)    -   $F1 +($feff)    -   $f1 
-($FF00)    -   $F1+($ff00)    -   $f1
 </code> </code>
  
- La desventaja de este sistema es que convertimos al versátil modo ''IM 2'' con posibilidad de ejecutar hasta 128 ISR diferentes que atiendan cada una a su periférico correspondiente en una evolución del ''IM 1'' (donde siempre se saltaba a $0038), pero en la cual la dirección de salto está fuera de la ROM y es personalizable. Es decir, perdemos la capacidad del IM2 de que cada dispositivo pueda disponer de código específico para atender sus interrupciones. Este "IM 1 mejorado" lo que nos permite es funcionar como en IM1 pero con nuestra propia ISR única. De hecho, esta es la forma más habitual de utilizar ''IM 2''.+ La desventaja de este sistema es que convertimos al versátil modo ''im 2'' con posibilidad de ejecutar hasta 128 ISR diferentes que atiendan cada una a su periférico correspondiente en una evolución del ''im 1'' (donde siempre se saltaba a $0038), pero en la cual la dirección de salto está fuera de la ROM y es personalizable. Es decir, perdemos la capacidad del im2 de que cada dispositivo pueda disponer de código específico para atender sus interrupciones. Este "im 1 mejorado" lo que nos permite es funcionar como en im1 pero con nuestra propia ISR única. De hecho, esta es la forma más habitual de utilizar ''im 2''.
  
  La única desventaja es que en esta ISR deberemos gestionar todas las interrupciones de cualquier periférico basado en interrupciones que queramos que interactúe con nuestro programa, aunque en el 99% de los programas o juegos (a excepción del uso del AMX mouse o similares) no se suele interactuar con los periféricos mediante este sistema.  La única desventaja es que en esta ISR deberemos gestionar todas las interrupciones de cualquier periférico basado en interrupciones que queramos que interactúe con nuestro programa, aunque en el 99% de los programas o juegos (a excepción del uso del AMX mouse o similares) no se suele interactuar con los periféricos mediante este sistema.
  
- Resumamos lo que acabamos de ver y comprender de forma esquemática las ventajas de una tabla de 257 bytes con el valor $F1 en cada elemento de la misma:+ Resumamos lo que acabamos de ver y comprender de forma esquemática las ventajas de una tabla de 257 bytes con el valor $f1 en cada elemento de la misma:
  
-   * Por lo que hemos visto hasta ahora, en un Spectrum estándar sin dispositivos en el bus de expansión la ULA se identifica al interrumpir el procesador como $FF, aunque no intencionadamente pues es el resultado del valor por defecto que hay en el bus de datos cuando no se coloca ningún dato debido a las resistencias de pull-up.+   * Por lo que hemos visto hasta ahora, en un Spectrum estándar sin dispositivos en el bus de expansión la ULA se identifica al interrumpir el procesador como $ff, aunque no intencionadamente pues es el resultado del valor por defecto que hay en el bus de datos cuando no se coloca ningún dato debido a las resistencias de pull-up.
  
-   * En nuestras anteriores rutinas guardábamos en FEFFh la dirección de la rutina de ISR que queríamos que se ejecutara cuando la interrupcion se identificaba con ID $FF (la ULA).+   * En nuestras anteriores rutinas guardábamos en FEFFh la dirección de la rutina de ISR que queríamos que se ejecutara cuando la interrupcion se identificaba con ID $ff (la ULA).
  
-   * Cargábamos I con 256 (FE) antes de saltar a IM2, de esta forma cuando la ULA producía una interrupción con id $FFh se saltaba a la direccion que había en ($FEFF), que era la de nuestra ISR.+   * Cargábamos I con 256 (FE) antes de saltar a im2, de esta forma cuando la ULA producía una interrupción con id $ffh se saltaba a la direccion que había en ($feff), que era la de nuestra ISR.
  
-   * Por desgracia, en ciertos modelos Timex Sinclair o si tenemos dispositivos conectados al bus de expansion puede que no encontremos $FF en el bus de datos, sino un valor arbitrario. Esto puede producir que la interrupcion no llegue como "ID=FF" y que, por lo tanto, no se produzca el salto a ($FEFF) sino a otro de los elementos de la tabla de vectores de interrupcion.+   * Por desgracia, en ciertos modelos Timex Sinclair o si tenemos dispositivos conectados al bus de expansion puede que no encontremos $ff en el bus de datos, sino un valor arbitrario. Esto puede producir que la interrupcion no llegue como "ID=FF" y que, por lo tanto, no se produzca el salto a ($feff) sino a otro de los elementos de la tabla de vectores de interrupcion.
  
-   * Para evitar que esto ocurra, podemos generar una tabla de 257 bytes y llenarla con el valor "$F1". De esta forma, sea cual sea el valor leído en el bus de datos, se saltará a $F1F1 (ya sea par o impar el valor del bus, las 2 partes de la dirección de salto en la tabla siempre sería $F1 + $F1).+   * Para evitar que esto ocurra, podemos generar una tabla de 257 bytes y llenarla con el valor "$f1". De esta forma, sea cual sea el valor leído en el bus de datos, se saltará a $f1F1 (ya sea par o impar el valor del bus, las 2 partes de la dirección de salto en la tabla siempre sería $f1 + $f1).
  
-   * Nuestra ISR estará pues en $F1F1 (en este ejemplo) y ser la responsable de gestionar cualquier periferico que pueda haber generado la interrupción.+   * Nuestra ISR estará pues en $f1f1 (en este ejemplo) y ser la responsable de gestionar cualquier periferico que pueda haber generado la interrupción.
  
    * La Tabla de Vectores de Interrupción se debe mantener en memoria (son 257 bytes de memoria "perdidos" para nuestro código o datos) ya que será usada en cada Interrupción del Z80 para buscar la ISR a la cual saltar.    * La Tabla de Vectores de Interrupción se debe mantener en memoria (son 257 bytes de memoria "perdidos" para nuestro código o datos) ya que será usada en cada Interrupción del Z80 para buscar la ISR a la cual saltar.
Línea 708: Línea 710:
     ORG 50000     ORG 50000
  
-    ; Generamos una tabla de 257 valores "$F1" desde $FE00 a $FF00 +    ; Generamos una tabla de 257 valores "$f1" desde $fe00 a $ff00 
-    LD HL, $FE00 +    ld hl, $fe00 
-    LD A, $F1 +    ld a, $f1 
-    LD (HL),                    ; Cargamos $F1 en $FE00 +    ld (hl),                    ; Cargamos $f1 en $fe00 
-    LD DE, $FE01                  ; Apuntamos DE a $FE01 +    ld de, $fe01                  ; Apuntamos DE a $fe01 
-    LD BC, 256                    ; Realizamos 256 LDI para copiar $F1 +    ld bc, 256                    ; Realizamos 256 ldi para copiar $f1 
-    LDIR                          ; en toda la tabla de vectores de int.+    ldir                          ; en toda la tabla de vectores de int.
  
     ; Arrancamos las interrupciones con todas las direcciones     ; Arrancamos las interrupciones con todas las direcciones
     ; del vector apuntando a nuestra ISR:     ; del vector apuntando a nuestra ISR:
-    DI +    di 
-    LD A, $FE                     ; Tenemos la tabla a partir de $FE00+    ld a, $fe                     ; Tenemos la tabla a partir de $fe00
-    LD IA +    ld ia 
-    IM 2                          ; Saltamos a IM2 +    im 2                          ; Saltamos a im2 
-    EI+    ei
  
 ; --------------------------------------------------------------- ; ---------------------------------------------------------------
Línea 728: Línea 730:
 ; --------------------------------------------------------------- ; ---------------------------------------------------------------
  
-; A continuación la rutina ISR, ensamblada en $F1F1:+; A continuación la rutina ISR, ensamblada en $f1f1:
 ; ;
-; Con el ORG $F1F1 nos aseguramos de que la ISR sera ensamblada+; Con el ORG $f1f1 nos aseguramos de que la ISR sera ensamblada
 ; por el ensamblador a partir de esta direccion, que es donde queremos ; por el ensamblador a partir de esta direccion, que es donde queremos
 ; que este ubicada para que el salto del procesador sea a la ISR. ; que este ubicada para que el salto del procesador sea a la ISR.
 ; Asi pues, todo lo que siga a este ORG se ensamblara para cargarse ; Asi pues, todo lo que siga a este ORG se ensamblara para cargarse
-; a partir de la direccion $F1F1 de la RAM.+; a partir de la direccion $f1f1 de la RAM.
  
-    ORG $F1F1+    ORG $f1f1
  
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
Línea 743: Línea 745:
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
 CLOCK_ISR_ASM_ROUTINE: CLOCK_ISR_ASM_ROUTINE:
-    PUSH AF +    push af 
-    PUSH HL+    push hl
  
    ; (... aqui insertamos el resto de la ISR...)    ; (... aqui insertamos el resto de la ISR...)
  
-    POP HL +    pop hl 
-    POP AF+    pop af
  
-    EI +    ei 
-    RETI+    reti
  
 ; Si vamos a colocar mas codigo en el fichero ASM detrás de la ISR; este ; Si vamos a colocar mas codigo en el fichero ASM detrás de la ISR; este
 ; sera ensamblado en direcciones a partir del final de la ISR en memoria ; sera ensamblado en direcciones a partir del final de la ISR en memoria
 ; (siguiendo a la misma). Como seguramente no queremos esto, es mejor ubicar ; (siguiendo a la misma). Como seguramente no queremos esto, es mejor ubicar
-; la ISR con su ORG $F1F1 al final del listado, o bien enlazarla como+; la ISR con su ORG $f1f1 al final del listado, o bien enlazarla como
 ; un binario aparte junto al resto del programa, o bien colocar otro ORG ; un binario aparte junto al resto del programa, o bien colocar otro ORG
 ; tras las ISR y antes de la siguiente rutina a ensamblar. ; tras las ISR y antes de la siguiente rutina a ensamblar.
Línea 764: Línea 766:
 </code> </code>
  
- El ''ORG $F1F1'' indica al ensamblador que debe ensamblar todo lo que va detrás de esta directiva a partir de la dirección de memoria indicada. En el ejemplo anterior hemos ubicado la rutina de ISR al final del programa, puesto que si seguimos añadiendo código tras la ISR, esté será ensamblado en ($F1F1 + TAMAÑO_ISR). Si no queremos que la ISR esté al final del listado fuente, podemos utilizar las siguientes directivas de pasmo para continuar el ensamblado de más rutinas a partir de la dirección inmediatamente anterior al ORG:+ El ''ORG $f1f1'' indica al ensamblador que debe ensamblar todo lo que va detrás de esta directiva a partir de la dirección de memoria indicada. En el ejemplo anterior hemos ubicado la rutina de ISR al final del programa, puesto que si seguimos añadiendo código tras la ISR, esté será ensamblado en ($f1f1 + TAMAÑO_ISR). Si no queremos que la ISR esté al final del listado fuente, podemos utilizar las siguientes directivas de pasmo para continuar el ensamblado de más rutinas a partir de la dirección inmediatamente anterior al ORG:
  
 <code z80> <code z80>
Línea 778: Línea 780:
  
     ;----------------------------------------------------     ;----------------------------------------------------
-    ; Nuestra rutina de ISR ensamblada en $F1F1 debido +    ; Nuestra rutina de ISR ensamblada en $f1f1 debido 
-    ; a la directiva ORG $F1F1+    ; a la directiva ORG $f1f1
     ;----------------------------------------------------     ;----------------------------------------------------
-    ORG $F1F1+    ORG $f1f1
  
  Rutina_ISR:  Rutina_ISR:
Línea 789: Línea 791:
     ORG PUNTO_ENSAMBLADO     ORG PUNTO_ENSAMBLADO
     ;----------------------------------------------------     ;----------------------------------------------------
-    ; El codigo continua pero no ensamblado tras $F1F1+    ; El codigo continua pero no ensamblado tras $f1f1
     ; sino en la direccion anterior al ORG     ; sino en la direccion anterior al ORG
     ;----------------------------------------------------     ;----------------------------------------------------
Línea 799: Línea 801:
  El listado completo del ejemplo está disponible para su descarga al final del capítulo, pero es esencialmente igual al primer ejemplo de reloj interno basado en ISR con ciertas excepciones:  El listado completo del ejemplo está disponible para su descarga al final del capítulo, pero es esencialmente igual al primer ejemplo de reloj interno basado en ISR con ciertas excepciones:
  
-   * La rutina de ISR se ensambla en $F1F1 mediante una directiva del ensamblador ''ORG $F1F1'' antes de la misma, lo cual hace que en el programa resultante, dicho código se ubique a partir de $F1F1.+   * La rutina de ISR se ensambla en $f1f1 mediante una directiva del ensamblador ''ORG $f1f1'' antes de la misma, lo cual hace que en el programa resultante, dicho código se ubique a partir de $f1f1.
  
-   * Se genera en $FE00 una tabla de 257 bytes conteniendo el valor $F1, para que la dirección de salto de una interrupción sea siempre $F1F1 independientemente de cuál sea el valor del device_id en el bus de datos (sea también par o impar).+   * Se genera en $fe00 una tabla de 257 bytes conteniendo el valor $f1, para que la dirección de salto de una interrupción sea siempre $f1F1 independientemente de cuál sea el valor del device_id en el bus de datos (sea también par o impar).
  
  De esta forma nuestra ISR será compatible con los diferentes modelos de Sinclair Spectrum con o sin periféricos conectados al bus de expansión.  De esta forma nuestra ISR será compatible con los diferentes modelos de Sinclair Spectrum con o sin periféricos conectados al bus de expansión.
  
- Otros autores de libros sobre programación (y a su vez programadores), como David Webb, proponen la utilización de $FDFD como vector de salto, y colocar en esta dirección un ''JP'' a la dirección de la rutina real. Nótese que $FDFD está 3 bytes en memoria antes que $FE00, por lo que de esta forma se puede tener el salto a la ISR junto a la tabla de vectores de interrupción, consecutivos en memoria. No obstante, esto añade 10 t-estados adicionales a la ejecución de la ISR, los relativos al salto, y no nos es de especial utilidad dada la posibilidad de los ensambladores cruzados de ubicar nuestra ISR en $F1F1 mediante la directiva ''ORG''.+ Otros autores de libros sobre programación (y a su vez programadores), como David Webb, proponen la utilización de $fdfd como vector de salto, y colocar en esta dirección un ''jp'' a la dirección de la rutina real. Nótese que $fdfd está 3 bytes en memoria antes que $fe00, por lo que de esta forma se puede tener el salto a la ISR junto a la tabla de vectores de interrupción, consecutivos en memoria. No obstante, esto añade 10 t-estados adicionales a la ejecución de la ISR, los relativos al salto, y no nos es de especial utilidad dada la posibilidad de los ensambladores cruzados de ubicar nuestra ISR en $f1f1 mediante la directiva ''ORG''.
  
 \\ \\
Línea 826: Línea 828:
 \\ \\
  
- Para ello generamos una ISR y la enlazamos con el modo 2 de interrupciones. En este caso, en vez de ubicarla en $F1F1, la hemos ubicado en $A1A1, para que sea compatible con utilizar paginación, como explicaremos en el próximo apartado.+ Para ello generamos una ISR y la enlazamos con el modo 2 de interrupciones. En este caso, en vez de ubicarla en $f1f1, la hemos ubicado en $a1a1, para que sea compatible con utilizar paginación, como explicaremos en el próximo apartado.
  
  Tras esto, nos mantenemos en un bucle de programa infinito que detecta cuándo la variable ''clock_changed'' cambia de 0 a 1 y que actualiza el valor en pantalla del reloj, volviendo a setear dicha variable a 0 hasta que la ISR modifique de nuevo el reloj. Cuando clock_changed vale 0, el programa se mantiene en un simple bucle que no realiza acciones salvo comprobar el estado de clock_changed continuamente. La ISR se ejecuta, por tanto, "en paralelo" a nuestro programa cuando las interrupciones solicitan la atención del procesador, 50 veces por segundo.  Tras esto, nos mantenemos en un bucle de programa infinito que detecta cuándo la variable ''clock_changed'' cambia de 0 a 1 y que actualiza el valor en pantalla del reloj, volviendo a setear dicha variable a 0 hasta que la ISR modifique de nuevo el reloj. Cuando clock_changed vale 0, el programa se mantiene en un simple bucle que no realiza acciones salvo comprobar el estado de clock_changed continuamente. La ISR se ejecuta, por tanto, "en paralelo" a nuestro programa cuando las interrupciones solicitan la atención del procesador, 50 veces por segundo.
Línea 836: Línea 838:
     ORG 40000     ORG 40000
  
-    CALL CLS+    call CLS
  
-    ; Generamos una tabla de 257 valores "$A1" desde $FE00 a $FF00 +    ; Generamos una tabla de 257 valores "$a1" desde $fe00 a $ff00 
-    LD HL, $FE00 +    ld hl, $fe00 
-    LD A, $A1                     ; A = $A1 +    ld a, $a1                     ; A = $a1 
-    LD (HL),                    ; Cargamos $A1 en $FE00 +    ld (hl),                    ; Cargamos $a1 en $fe00 
-    LD DE, $FE01                  ; Apuntamos DE a $FE01 +    ld de, $fe01                  ; Apuntamos DE a $fe01 
-    LD BC, 256                    ; Realizamos 256 LDI para copiar $A1 +    ld bc, 256                    ; Realizamos 256 ldi para copiar $a1 
-    LDIR                          ; en toda la tabla de vectores de int.+    ldir                          ; en toda la tabla de vectores de int.
  
-    ; Activamos IM2 con nuestra ISR +    ; Activamos im2 con nuestra ISR 
-    DI +    di 
-    LD A, $FE                     ; Definimos la tabla a partir de $FE00+    ld a, $fe                     ; Definimos la tabla a partir de $fe00
-    LD IA +    ld ia 
-    IM 2                          ; Saltamos a IM2 +    im 2                          ; Saltamos a im2 
-    EI+    ei
  
-    JR ImprimirNumero             ; Hacemos la 1a impresion: "00:00"+    jr ImprimirNumero             ; Hacemos la 1a impresion: "00:00"
  
 Bucle: Bucle:
-    LD A, (clock_changed) +    ld a, (clock_changed) 
-    AND A +    and a 
-    JR Z, Bucle                   ; Si clock_changed no vale 1, no hay+    jr z, Bucle                   ; Si clock_changed no vale 1, no hay
                                   ; que imprimir el mensaje                                   ; que imprimir el mensaje
  
     ; Si estamos aqui es que clock_changed = 1... lo reseteamos     ; Si estamos aqui es que clock_changed = 1... lo reseteamos
     ; e imprimimos por pantalla la información como MM:SS     ; e imprimimos por pantalla la información como MM:SS
-    XOR A +    xor a 
-    LD (clock_changed),         ; clock_changed = 0+    ld (clock_changed),         ; clock_changed = 0
  
 ImprimirNumero: ImprimirNumero:
-    LD DE, 0 +    ld de, 0 
-    CALL CursorAt+    call CursorAt
  
-    LD A, (minutes)               ; Imprimimos minutos + ":" + segundos +    ld a, (minutes)               ; Imprimimos minutos + ":" + segundos 
-    CALL PrintNum2digits +    call PrintNum2digits 
-    LD A, ":" +    ld a, ":" 
-    RST 16 +    rst 16 
-    LD A, (seconds) +    ld a, (seconds) 
-    CALL PrintNum2digits+    call PrintNum2digits
  
-    JR Bucle                      ; Repetir indefinidamente+    jr Bucle                      ; Repetir indefinidamente
  
 clock_changed DB 0 clock_changed DB 0
Línea 891: Línea 893:
  
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
-; Con este ORG $A1A1 nos aseguramos de que la ISR sera ensamblada+; Con este ORG $a1a1 nos aseguramos de que la ISR sera ensamblada
 ; por el ensamblador a partir de esta direccion, que es donde queremos ; por el ensamblador a partir de esta direccion, que es donde queremos
 ; que este ubicada para que el salto del procesador sea a la ISR. ; que este ubicada para que el salto del procesador sea a la ISR.
 ; Asi pues, todo lo que siga a este ORG se ensamblara para cargarse ; Asi pues, todo lo que siga a este ORG se ensamblara para cargarse
-; a partir de la direccion $A1A1 de la RAM.+; a partir de la direccion $a1a1 de la RAM.
 ; ;
-; Se ha elegido $A1A1 para que esté en el bloque de 16K que empieza+; Se ha elegido $a1a1 para que esté en el bloque de 16K que empieza
 ; en 32768, es decir, fuera de la contented memory y fuera del bloque ; en 32768, es decir, fuera de la contented memory y fuera del bloque
 ; de paginación 128K. ; de paginación 128K.
  
-    ORG $A1A1+    ORG $a1a1
  
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
Línea 908: Línea 910:
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
 CLOCK_ISR_ASM_ROUTINE: CLOCK_ISR_ASM_ROUTINE:
-    PUSH AF +    push af 
-    PUSH HL+    push hl
  
-    LD A, (pause) +    ld a, (pause) 
-    OR A +    or a 
-    JR NZ, clock_isr_fin          ; Si pause==1, no continuamos la ISR+    jr nz, clock_isr_fin          ; Si pause==1, no continuamos la ISR
  
-    LD HL, (abs_ticks) +    ld hl, (abs_ticks) 
-    INC HL +    inc hl 
-    LD (abs_ticks), HL            ; Incrementamos abs_ticks (absolutos)+    ld (abs_ticks), hl            ; Incrementamos abs_ticks (absolutos)
  
-    LD HL, (timer) +    ld hl, (timer) 
-    DEC HL +    dec hl 
-    LD (timer), HL                ; Decrementamos timer (ticks absolutos)+    ld (timer), hl                ; Decrementamos timer (ticks absolutos)
  
-    LD A, (ticks) +    ld a, (ticks) 
-    INC A +    inc a 
-    LD (ticks),                 ; Incrementamos ticks (50 veces/seg)+    ld (ticks),                 ; Incrementamos ticks (50 veces/seg)
  
-    CP 50 +    cp 50 
-    JR C, clock_isr_fin           ; if ticks < 50,  fin de la ISR+    jr c, clock_isr_fin           ; if ticks < 50,  fin de la ISR
                                   ; si ticks >= 50, cambiar seg:min                                   ; si ticks >= 50, cambiar seg:min
-    XOR A +    xor a 
-    LD (ticks),                 ; ticks = 0+    ld (ticks),                 ; ticks = 0
  
-    LD A, 1 +    ld a, 1 
-    LD (clock_changed),         ; ha cambiado el numero de segundos+    ld (clock_changed),         ; ha cambiado el numero de segundos
  
-    LD A, (seconds) +    ld a, (seconds) 
-    INC A +    inc a 
-    LD (seconds),               ; seconds = segundos +1+    ld (seconds),               ; seconds = segundos +1
  
-    CP 60 +    cp 60 
-    JR C, clock_isr_fin           ; si segundos < 60 -> salir de la ISR+    jr c, clock_isr_fin           ; si segundos < 60 -> salir de la ISR
  
-    XOR A                         ; si segundos == 60 -> inc minutos +    xor a                         ; si segundos == 60 -> inc minutos 
-    LD (seconds),               ; seconds = 0+    ld (seconds),               ; seconds = 0
  
-    LD A, (minutes) +    ld a, (minutes) 
-    INC A +    inc a 
-    LD (minutes),               ; minutes = minutos + 1+    ld (minutes),               ; minutes = minutos + 1
  
-    CP 60 +    cp 60 
-    JR C, clock_isr_fin           ; si minutos >= 60 -> resetear minutos +    jr c, clock_isr_fin           ; si minutos >= 60 -> resetear minutos 
-    XOR A +    xor a 
-    LD (minutes),               ; minutes = 0+    ld (minutes),               ; minutes = 0
  
 clock_isr_fin: clock_isr_fin:
-    POP HL +    pop hl 
-    POP AF+    pop af
  
-    EI +    ei 
-    RETI+    reti
  
 ; Si vamos a colocar mas codigo en el fichero ASM detra de la ISR; este sera ; Si vamos a colocar mas codigo en el fichero ASM detra de la ISR; este sera
-; ensamblado en direcciones a partir de $A1A1 a partir del final de la ISR en+; ensamblado en direcciones a partir de $a1a1 a partir del final de la ISR en
 ; memoria. Como seguramente no queremos esto, es mejor ubicar la ISR con su ; memoria. Como seguramente no queremos esto, es mejor ubicar la ISR con su
-; ORG $A1A1 al final del listado o bien colocar otro ORG antes de la siguiente+; ORG $a1a1 al final del listado o bien colocar otro ORG antes de la siguiente
 ; rutina a ensamblar, usando un EQU $ previo para poder continuar ; rutina a ensamblar, usando un EQU $ previo para poder continuar
 ; Con el "POSICION_DETRAS_DEL_MAIN  EQU $" de antes de la ISR, nos guardamos ; Con el "POSICION_DETRAS_DEL_MAIN  EQU $" de antes de la ISR, nos guardamos
Línea 986: Línea 988:
  
 \\ \\
-   * ''CALL CLS'' y ''RST 16'' (''CLS'' y ''PRINT-A''): Podemos aprovechar las rutinas de la ROM dentro de nuestros programas, evitando escribir más código del necesario cuando la ROM ya provee de alguna rutina para ello. Por contra, esto hace nuestros programas no portables, ya que las rutinas de la ROM del Spectrum no están presentes (al menos no en las mismas direcciones y con los mismos parámetros de entrada y salida) en otros sistemas basados en Z80 como el Amstrad o el MSX, y menos eficientes que si usamos nuestras propias rutinas. Y tampoco podemos usar el registro IY pese a que ahora estamos en IM2.+   * ''call CLS'' y ''rst 16'' (''CLS'' y ''PRINT-A''): Podemos aprovechar las rutinas de la ROM dentro de nuestros programas, evitando escribir más código del necesario cuando la ROM ya provee de alguna rutina para ello. Por contra, esto hace nuestros programas no portables, ya que las rutinas de la ROM del Spectrum no están presentes (al menos no en las mismas direcciones y con los mismos parámetros de entrada y salida) en otros sistemas basados en Z80 como el Amstrad o el MSX, y menos eficientes que si usamos nuestras propias rutinas. Y tampoco podemos usar el registro IY pese a que ahora estamos en im2.
  
    * ''EQU $'' : Mediante ''$'', podemos acceder al valor de la posición exacta del punto de ensamblado (por tanto, una dirección de memoria absoluta calculable por el programa ensamblador al conocer el ORG del programa) en el momento en que lo usamos. Si lo asociamos a una constante, podemos referenciar ese punto del programa en cualquier momento, por ejemplo para un ''ORG'' posterior. Podemos asignar múltiples veces $ a diferentes constantes, ya que su valor variará según el punto en que la definamos.    * ''EQU $'' : Mediante ''$'', podemos acceder al valor de la posición exacta del punto de ensamblado (por tanto, una dirección de memoria absoluta calculable por el programa ensamblador al conocer el ORG del programa) en el momento en que lo usamos. Si lo asociamos a una constante, podemos referenciar ese punto del programa en cualquier momento, por ejemplo para un ''ORG'' posterior. Podemos asignar múltiples veces $ a diferentes constantes, ya que su valor variará según el punto en que la definamos.
  
-   * En el programa tenemos 3 bloques diferentes de código: ''ORG 40000'' (el inicio del programa), ''ORG POSICION_DETRAS_DEL_MAIN'' (que va inmediatamente después del código ensamblado en 40000) y ''ORG $A1A1'' (41377). Lo normal es que el programa ensamblador, ante diferentes directivas ORG, generase varios bloques de código máquina (dos en este caso: uno en 40000 y otro en 41377 con los 77 bytes que ocupa el código de la ISR) y hacer un cargador BASIC que cargue ambos, pero **Pasmo** lo que hace es generar un único bloque que empieza en 40000 y acaba en 41454. Todo el hueco entre el final del código principal y la ISR, lo rellena con ceros y ocupa espacio en el BIN (y en el TAP).\\ \\ De hecho, si bajamos el ''ORG'' de 40000 a 35000, veremos que el TAP resultante ocupa 5KB más (5KB más de "blancos"). Otros ensambladores como **sjasmplus** saben resolver esto con múltiples bloques, y también lo podemos resolver nosotros ensamblando las 2 cosas por separado, y generando un cargador TAP a mano, y no automáticamente con <nowiki>pasmo --tapbas</nowiki>.+   * En el programa tenemos 3 bloques diferentes de código: ''ORG 40000'' (el inicio del programa), ''ORG POSICION_DETRAS_DEL_MAIN'' (que va inmediatamente después del código ensamblado en 40000) y ''ORG $a1a1'' (41377). Lo normal es que el programa ensamblador, ante diferentes directivas ORG, generase varios bloques de código máquina (dos en este caso: uno en 40000 y otro en 41377 con los 77 bytes que ocupa el código de la ISR) y hacer un cargador BASIC que cargue ambos, pero **Pasmo** lo que hace es generar un único bloque que empieza en 40000 y acaba en 41454. Todo el hueco entre el final del código principal y la ISR, lo rellena con ceros y ocupa espacio en el BIN (y en el TAP).\\ \\ De hecho, si bajamos el ''ORG'' de 40000 a 35000, veremos que el TAP resultante ocupa 5KB más (5KB más de "blancos"). Otros ensambladores como **sjasmplus** saben resolver esto con múltiples bloques, y también lo podemos resolver nosotros ensamblando las 2 cosas por separado, y generando un cargador TAP a mano, y no automáticamente con <nowiki>pasmo --tapbas</nowiki>.
 \\ \\
  
Línea 1005: Línea 1007:
   * Actualizar regularmente el buffer de "notas" del chip AY de los modelos de 128K para reproducir melodías AY en paralelo a la ejecución de nuestro programa.   * Actualizar regularmente el buffer de "notas" del chip AY de los modelos de 128K para reproducir melodías AY en paralelo a la ejecución de nuestro programa.
  
-  * Llevar un control exacto de ticks para procesos de retardos con valores precisos. Es decir, si necesitamos hacer una espera de N ticks, o de N segundos (sabiendo que 50 ticks son 1 segundo), podemos utilizar la variable de 16 bits "timer" que se decrementa en cada ejecución de la ISR. Podemos así generar una rutina ''WaitNTicks'' en la que establecer "timer" a un valor concreto y esperar a que valga 0 (ya que será decrementado por la ISR).\\ \\  En este ejemplo en lugar de esperar a que timer valga 0, esperamos a que su byte alto valga FFh, en previsión de utilizarla en otros bloques de código más largos en el que se nos pueda pasar el ciclo exacto en que timer sea igual a cero. Comprobando que el byte alto de timer sea FFh, tenemos una rutina que nos permite tiempos de espera desde 1 tick hasta 49151/50/60=16 minutos. Veamos el siguiente ejemplo (el cual, por simplificar el código, no usa el sistema de tabla de vectores de interrupción sino simplemente $FEFF para apuntar a la ISR):+  * Llevar un control exacto de ticks para procesos de retardos con valores precisos. Es decir, si necesitamos hacer una espera de N ticks, o de N segundos (sabiendo que 50 ticks son 1 segundo), podemos utilizar la variable de 16 bits "timer" que se decrementa en cada ejecución de la ISR. Podemos así generar una rutina ''WaitNTicks'' en la que establecer "timer" a un valor concreto y esperar a que valga 0 (ya que será decrementado por la ISR).\\ \\  En este ejemplo en lugar de esperar a que timer valga 0, esperamos a que su byte alto valga FFh, en previsión de utilizarla en otros bloques de código más largos en el que se nos pueda pasar el ciclo exacto en que timer sea igual a cero. Comprobando que el byte alto de timer sea FFh, tenemos una rutina que nos permite tiempos de espera desde 1 tick hasta 49151/50/60=16 minutos. Veamos el siguiente ejemplo (el cual, por simplificar el código, no usa el sistema de tabla de vectores de interrupción sino simplemente $feff para apuntar a la ISR):
 \\ \\
  
Línea 1014: Línea 1016:
  
     ; Instalamos la ISR:     ; Instalamos la ISR:
-    LD HL, CLOCK_ISR_ASM_ROUTINE +    ld hl, CLOCK_ISR_ASM_ROUTINE 
-    DI +    di 
-    LD ($FEFF), HL                ; Guardamos en ($FEFF) la direccion +    ld ($feff), hl                ; Guardamos en ($feff) la direccion 
-    LD A, 254                     ; de la rutina CLOCK_ISR_ASM_ROUTINE +    ld a, 254                     ; de la rutina CLOCK_ISR_ASM_ROUTINE 
-    LD IA +    ld ia 
-    IM +    im 
-    EI+    ei
  
 Bucle_entrada: Bucle_entrada:
-    LD A, "." +    ld a, "." 
-    RST 16 +    rst 16 
-    LD A, "5" +    ld a, "5" 
-    RST 16 +    rst 16 
-    LD A, " " +    ld a, " " 
-    RST 16                        ; Imprimimos por pantalla ".5 " +    rst 16                        ; Imprimimos por pantalla ".5 " 
-    LD HL, 25                     ; Esperamos 25 ticks (0.5 segundos) +    ld hl, 25                     ; Esperamos 25 ticks (0.5 segundos) 
-    CALL WaitNTicks+    call WaitNTicks
  
-    LD A, "3" +    ld a, "3" 
-    RST 16 +    rst 16 
-    LD A, " " +    ld a, " " 
-    RST 16                        ; Imprimimos por pantalla "3 " +    rst 16                        ; Imprimimos por pantalla "3 " 
-    LD HL, 3*50                   ; Esperamos 150 ticks (3 segundos) +    ld hl, 3*50                   ; Esperamos 150 ticks (3 segundos) 
-    CALL WaitNTicks+    call WaitNTicks
  
-    JP Bucle_entrada+    jp Bucle_entrada
  
 ticks         DB 0 ticks         DB 0
Línea 1048: Línea 1050:
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
 WaitNTicks: WaitNTicks:
-    LD (timer), HL          ; seteamos "timer" con el tiempo de espera+    ld (timer), hl          ; seteamos "timer" con el tiempo de espera
  
-Waitnticks_loop:            ; bucle de espera, la ISR lo ira decrementando +waitnticks_loop:            ; bucle de espera, la ISR lo ira decrementando 
-    LD HL, (timer) +    ld hl, (timer) 
-    LD A                ; cuando (timer) valga 0 y lo decrementen, su +    ld a                ; cuando (timer) valga 0 y lo decrementen, su 
-    CP $FF                  ; byte alto pasara a valer FFh, lo que quiere+    cp $ff                  ; byte alto pasara a valer FFh, lo que quiere
                             ; decir que ha pasado el tiempo a esperar.                             ; decir que ha pasado el tiempo a esperar.
-    JR NZWaitnticks_loop  ; si no, al bucle de nuevo. +    jr nzwaitnticks_loop  ; si no, al bucle de nuevo. 
-    RET+    ret
  
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
Línea 1062: Línea 1064:
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
 CLOCK_ISR_ASM_ROUTINE: CLOCK_ISR_ASM_ROUTINE:
-    PUSH AF +    push af 
-    PUSH HL+    push hl
  
-    LD HL, (timer) +    ld hl, (timer) 
-    DEC HL +    dec hl 
-    LD (timer), HL                ; Decrementamos timer (absolutos)+    ld (timer), hl                ; Decrementamos timer (absolutos)
  
-    LD A, (ticks) +    ld a, (ticks) 
-    INC A +    inc a 
-    LD (ticks),                 ; Incrementamos ticks (50 veces/seg)+    ld (ticks),                 ; Incrementamos ticks (50 veces/seg)
  
-    CP 50 +    cp 50 
-    JR C, clock_isr_fin           ; if ticks < 50,  fin de la ISR +    jr c, clock_isr_fin           ; if ticks < 50,  fin de la ISR 
-    XOR A                         ; si ticks >= 50, cambiar seg:min +    xor a                         ; si ticks >= 50, cambiar seg:min 
-    LD (ticks),                 ; y ticks = 0+    ld (ticks),                 ; y ticks = 0
  
 clock_isr_fin: clock_isr_fin:
-    POP HL +    pop hl 
-    POP AF +    pop af 
-    EI +    ei 
-    RETI+    reti
  
     END 50000     END 50000
Línea 1100: Línea 1102:
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
 WaitKeyOrTime: WaitKeyOrTime:
-    LD (timer), HL            ; seteamos "timer" con el tiempo de espera+    ld (timer), hl            ; seteamos "timer" con el tiempo de espera
  
-Waitkeyticks_loop:            ; bucle de espera, la ISR lo ira decrementando +waitkeyticks_loop:            ; bucle de espera, la ISR lo ira decrementando 
-    XOR +    xor a                     ; = 0 => leer todas las semifilas del teclado 
-    IN A,(254+    in a, ($fe              ; Leer teclado 
-    OR 224 +    or %11100000              ; Poner a 1 los 3 bits más altos 
-    INC A                     ; Comprobamos el estado del teclado +    inc a                     ; A=A+1. Comprobamos el estado del teclado 
-    RET NZ                    ; Si hay tecla pulsadasalimos+    ret nz                    ; Si A=0  => ZF = 1 => no hay tecla pulsada 
 +                              ; Si A!=0 => ZF = 0 => hay alguna tecla => salimos
  
-    LD HL, (timer) +    ld hl, (timer) 
-    LD A                  ; cuando (timer) valga 0 y lo decrementen, su +    ld a                  ; cuando (timer) valga 0 y lo decrementen, su 
-    CP $FF                    ; byte alto pasara a valer FFh, lo que quiere+    cp $ff                    ; byte alto pasara a valer FFh, lo que quiere
                               ; decir que ha pasado el tiempo a esperar.                               ; decir que ha pasado el tiempo a esperar.
-    JR NZWaitkeyticks_loop  ; si no, al bucle de nuevo. +    jr nzwaitkeyticks_loop  ; si no, al bucle de nuevo. 
-    RET+    ret
 </code> </code>
 +
 +
 +\\
 +===== Utilizar un JP como ISR =====
 +
 + En muchos juegos comerciales y homebrew, la rutina de ISR no es una rutina en sí misma sino un ''jp'' a la rutina real, de modo que podamos tenerla junto al resto de nuestro código, y no en una ubicación específica de memoria:
 +
 +
 +<code z80>
 +; Rutina de ISR ubicada junto a nuestro programa, y no en dirección tipo $XYXY
 +    ORG 40000
 +
 +    ; Generamos una tabla de 257 valores "$a2" desde $a000 a $a101
 +    ld hl, $a000
 +    ld a, $a2                     ; A = $a2
 +    ld (hl), a                    ; Cargamos $a2 en $fe00
 +    ld de, $fe01                  ; Apuntamos DE a $fe01
 +    ld bc, 256                    ; Realizamos 256 ldi para copiar $a2
 +    ldir                          ; en toda la tabla de vectores de int.
 +
 +    ; Activamos im2 con nuestra ISR
 +    di
 +    ld a, $fe                     ; Definimos la tabla a partir de $fe00.
 +    ld i, a
 +    im 2                          ; Saltamos a im2
 +    ei
 +
 +    ; Nuestro programa
 +    ; (...)
 +
 +Rutina_ISR:
 +    ; La rutina ISR
 +    (...)
 +    ei
 +    reti
 +
 +    ; Guardamos en una variable de preprocesador la posicion
 +    ; de este punto en el proceso de ensamblado ($)
 +    PUNTO_ENSAMBLADO EQU $
 +
 +    ;-------------------------------------------------------------
 +    ; Nuestra rutina de ISR ensamblada en $A2A2: JP a rutina real
 +    ;-------------------------------------------------------------
 +    ORG $A2A2
 +
 +PUNTO_ENTRADA_ISR:
 +    jp Rutina_ISR
 +</code>
 +
 +De esta forma, la tabla de vectores de interrupción y la rutina ISR (un simple ''jp'' a la rutina real) están prácticamente juntas en memoria y sabemos que acaba 3 bytes después de **$a2a2**, por lo que podemos poner código a continuación sin tener que calcular cuánto ocupa la rutina de ISR. De esta forma también podemos tener la rutina de ISR junto al resto del código, con la única penalización de 3 bytes y 10 ciclos de reloj del ''jp NN''.
  
  
Línea 1121: Línea 1174:
 ===== Paginación 128K y Tabla de Vectores de Interrupción ===== ===== Paginación 128K y Tabla de Vectores de Interrupción =====
  
-Al respecto de la ubicación de la tabla de salto o "vectores de interrupción" es necesaria una consideración importante: La dirección que hemos tomado para el ejemplo ($FEFF para el ejemplo de la ULA, o $FE00-$FEFF+1 para el ejemplo de la tabla de vectores): es válida tanto para modelos 48K como para 128K, siempre que en estos últimos no usemos paginación de memoria.+Al respecto de la ubicación de la tabla de salto o "vectores de interrupción" es necesaria una consideración importante: La dirección que hemos tomado para el ejemplo ($feff para el ejemplo de la ULA, o $fe00-$feff+1 para el ejemplo de la tabla de vectores): es válida tanto para modelos 48K como para 128K, siempre que en estos últimos no usemos paginación de memoria.
  
 Como veremos en el siguiente capítulo, la paginación del Spectrum consiste en que tenemos acceso a bloques adicionales de 16KB de RAM del Spectrum usando la "ventana" de 16KB final de la memoria (el bloque entre 49152 y 65535). Como veremos en el siguiente capítulo, la paginación del Spectrum consiste en que tenemos acceso a bloques adicionales de 16KB de RAM del Spectrum usando la "ventana" de 16KB final de la memoria (el bloque entre 49152 y 65535).
  
-Si ponemos la tabla de vectores de interrupción en $FEXX (al final de la memoria) o directamente en $FEFF (asumiendo ULA = $FF), todo funcionará correctamente hasta que hagamos un cambio de página. En el momento en que hagamos un cambio de página, la siguiente interrupción se irá al final de la memoria a buscar la dirección de salto de la ISR (en $FEFF) y no encontrará en ella la dirección de nuestra rutina sino el valor que haya en $FEFF en ese banco de memoria. Saltará a ella, y lo más probable será un cuelgue del Spectrum.+Si ponemos la tabla de vectores de interrupción en $fexx (al final de la memoria) o directamente en $feff (asumiendo ULA = $ff), todo funcionará correctamente hasta que hagamos un cambio de página. En el momento en que hagamos un cambio de página, la siguiente interrupción se irá al final de la memoria a buscar la dirección de salto de la ISR (en $feff) y no encontrará en ella la dirección de nuestra rutina sino el valor que haya en $feff en ese banco de memoria. Saltará a ella, y lo más probable será un cuelgue del Spectrum.
  
 Para solucionar esto hay 2 opciones: Para solucionar esto hay 2 opciones:
  
-1. No usamos $FE00 sino una dirección de memoria más baja para construir la tabla. Por ejemplo, en nuestro programa podemos hacer un:+1. No usamos $fe00 sino una dirección de memoria más baja para construir la tabla. Por ejemplo, en nuestro programa podemos hacer un:
  
 <code z80> <code z80>
-; Reservamos en $BE00 257 bytes para usarlos de tabla. +; Reservamos en $be00 257 bytes para usarlos de tabla. 
-    ORG $BE00+    ORG $be00
  
 tabla_vectores_isr: tabla_vectores_isr:
Línea 1139: Línea 1192:
 </code> </code>
  
- Y después usar I = $BE, para que la tabla de vectores de interrupción vaya desde $BE00 hasta $BEFF+1.+ Y después usar I = $be, para que la tabla de vectores de interrupción vaya desde $be00 hasta $beFF+1.
  
- En este caso, ubicaremos la dirección de nuestra ISR en $BEFF y $BEFF+1.+ En este caso, ubicaremos la dirección de nuestra ISR en $beff y $beff+1.
  
 <code z80> <code z80>
-    LD HLRutina_ISR +    ld hlrutina_ISR 
-    LD ($BEFF), HL+    ld ($beff), hl
 </code> </code>
  
-2. Antes de cambiar a ''IM 2'', creamos la tabla de vectores de interrupción (o escribimos el valor único para $FF) en todos los bancos que vayamos a utilizar durante el programa.+2. Antes de cambiar a ''im 2'', creamos la tabla de vectores de interrupción (o escribimos el valor único para $ff) en todos los bancos que vayamos a utilizar durante el programa.
  
-Para eso, creamos una rutina que sea "Generar_Tabla_Vectores", que genere la tabla en $FENN o escriba el valor en $FEFF, y hacemos lo siguiente al principio del programa, antes de cambiar a ''IM 2'':+Para eso, creamos una rutina que sea "Generar_Tabla_Vectores", que genere la tabla en $feNN o escriba el valor en $feFF, y hacemos lo siguiente al principio del programa, antes de cambiar a ''im 2'':
  
    * Llamar a Generar_Tabla_Vectores (generará la tabla en la memoria "por defecto")    * Llamar a Generar_Tabla_Vectores (generará la tabla en la memoria "por defecto")
Línea 1164: Línea 1217:
 ===== Paginación 128KB y ubicación de nuestra ISR ===== ===== Paginación 128KB y ubicación de nuestra ISR =====
  
-Lo mismo ocurre con la dirección de nuestra ISR con el "truco" de poner el mismo byte en todas las direcciones de la tabla de vectores. El valor $F1, es decir, la dirección $F1F1, está en la parte alta de la memoria y por tanto no podemos utilizarla para ubicar la ISR si vamos a utilizar paginación 128KB.+Lo mismo ocurre con la dirección de nuestra ISR con el "truco" de poner el mismo byte en todas las direcciones de la tabla de vectores. El valor $f1, es decir, la dirección $f1F1, está en la parte alta de la memoria y por tanto no podemos utilizarla para ubicar la ISR si vamos a utilizar paginación 128KB
 + 
 +Necesitaremos buscar un hueco en nuestro programa, para colocar un ORG adecuado delante de la ISR, como por ejemplo $a1a1 (41377), o cualquier otra dirección donde el byte alto y el bajo coincidan, y que nos venga bien según el mapa de memoria de nuestro juego. 
 + 
 +Por citar ejemplos de direcciones, podemos ver las obtenidas en el artículo "//Disassemble Interrupt Mode on some popular ZX Spectrum 128k Games//", de Andy Dansby, quien estudió la ubicación de la rutina de ISR en diferentes juegos de Spectrum comerciales: 
 + 
 +\\  
 +|< 90% >| 
 +^ Juego ^ Ubicación tabla vectores ^ Ubicación ISR ^ Tipo de ISR (rutina o\\ salto a rutina real) ^ Notas ^ 
 +| La Familia Addams (The Addams Family) | $b900-$ba00 | $5b5b | ISR es ''jp $ba6e'' | - | 
 +| Where Time Stood Still | $8400-$8500 | $bebe | Rutina ISR | - | 
 +| Demo 7th Reality | $6300-$6400 | $6464 | ISR es ''jp $624d'' | - | 
 +| La Abadia Del Crimen | $be00-$bf00 | $bfbf | Rutina ISR | - | 
 +| Chase HQ 2 | $9b00-$9c00 | $fdfd | ISR es ''jp $ba6e'' | ISR en bloque paginable |  
 +| Grand Prix Circuit | $8200-$8300 | $6363 | ISR es ''jp $a07a'' | - | 
 +| Robocop 2 | $7700-$7800 | $5b5b | ISR es ''jp $9cb4'' | - | 
 +| Robocop 3 | $7700-$7800 | $7676 | ISR es ''jp $8225'' | - | 
 +| Spacegun | $be00-$bf00 | $bfbf | ISR es ''jp $a07a'' | - | 
 +| Desafio Total (Total Recall) | $9100-$9200 | $5d5d | ISR es ''jp $71ff'' | - | 
 +| Carrier Command | $8300-$8400 | $8585 | Rutina ISR | - | 
 +| El Gran Halcón (Hudson Hawk) | $9000-$8100 | $8181 | Rutina ISR | - | 
 +| NARC | $be00-$bf00 | $bfbf | ISR es ''jp $de38'' | - | 
 +| Navy Seals | $9100-$9200 | $5d5d | ISR es ''jp $6dfd'' | - | 
 +| Pang | $8000-$8101 | $8181 | ISR es ''jp $6286'' | - | 
 +\\  
 + 
 +Varias curiosidades: 
 + 
 +  * Como se puede ver, no hay un estándar sobre dónde ubicar la tabla de vectores de interrupción, ni la ISR, es una cuestión de elegir las direcciones deseadas según diferentes criterios (como el mapa de memoria de nuestro programa). 
 + 
 +  * Sólo 2 juegos de la lista (**Grand Prix Circuit** y **Pang**) tienen una tabla de 257 bytes, teniendo el resto de juegos una tabla de 1 página (256 bytes), con lo que técnicamente, había una posibilidad "baja" de que el juego se colgase al arrancar si IM2 cogía $FF y $FF+1. Estos dos juegos implementan IM2 correctamente técnicamente hablando. 
 + 
 +  * Sólo 4 juegos de la lista implementan la rutina directamente en la dirección apuntada por el vector de interrupciones, mientras que el resto simplemente tienen un ''JP'' a la rutina de ISR real.
  
-Necesitaremos buscar un hueco en nuestro programa, para colocar un ORG adecuado delante de la ISR, como por ejemplo $A1A1 (41377), o cualquier otra dirección donde el byte alto y el bajo coincidan, y que nos venga bien según el mapa de memoria de nuestro juego. 
  
 \\ \\
 ==== Modo IM 2 en Spectrum 16K ==== ==== Modo IM 2 en Spectrum 16K ====
  
- Como ya hemos comentado, la ULA y el procesador compiten en uso por la zona de memoria comprendida entre los 16K y los 32K, por lo que es crítico ubicar el vector de interrupciones en un banco de memoria por encima de los 32K (típicamente, en $FE00).+ Como ya hemos comentado, la ULA y el procesador compiten en uso por la zona de memoria comprendida entre los 16K y los 32K, por lo que es crítico ubicar el vector de interrupciones en un banco de memoria por encima de los 32K (típicamente, en $fe00).
  
- Lamentablemente, en los modelos de 16KB de memoria sólo tenemos disponible la famosa página de 16KB entre $4000 y $7FFF que se ve afectada por las lecturas de memoria de la ULA.+ Lamentablemente, en los modelos de 16KB de memoria sólo tenemos disponible la famosa página de 16KB entre $4000 y $7fff que se ve afectada por las lecturas de memoria de la ULA.
  
- Aunque no es habitual diseñar programas para los modelos de Spectrum de 16KB, Miguel A. Rodríguez Jódar nos aporta una solución basada en apuntar el registro I a la ROM de tal forma que (I*256)+$FF proporcione un valor de la ROM cuyo contenido sea una dirección de memoria física en RAM disponible para ubicar nuestra ISR. Para poder realizar este pequeño truco es importante saber que el valor del bus de datos durante la interrupción será $FF, es decir, saber que no hay dispositivos mal diseñados conectados al bus de expansión que puedan alterar el valor del bus de datos:+ Aunque no es habitual diseñar programas para los modelos de Spectrum de 16KB, Miguel A. Rodríguez Jódar nos aporta una solución basada en apuntar el registro I a la ROM de tal forma que (I*256)+$ff proporcione un valor de la ROM cuyo contenido sea una dirección de memoria física en RAM disponible para ubicar nuestra ISR. Para poder realizar este pequeño truco es importante saber que el valor del bus de datos durante la interrupción será $ff, es decir, saber que no hay dispositivos mal diseñados conectados al bus de expansión que puedan alterar el valor del bus de datos:
  
  Citando a Miguel A.:  Citando a Miguel A.:
Línea 1190: Línea 1274:
 {{ cursos:ensamblador:result_busqueda_i.png?640 | Búsqueda de direcciones en la ROM }} {{ cursos:ensamblador:result_busqueda_i.png?640 | Búsqueda de direcciones en la ROM }}
  
- //Así, por ejemplo, se puede escribir una rutina IM 2 para un programa de 16K, así://+ //Así, por ejemplo, se puede escribir una rutina im 2 para un programa de 16K, así://
  
 <code z80> <code z80>
-    LD I, 40 +    ld i, 40 
-    IM 2+    im 2
     ... resto del programa...     ... resto del programa...
  
Línea 1201: Línea 1285:
 </code> </code>
  
- //Aunque se use este sistema, hay aún un "peligro" oculto, aunque considero que es de menor relevancia, al menos hoy día. Es el hecho de que los periféricos "copiones" tipo Transtape y similares, usados en un Spectrum real para crear un snapshot, no pueden saber si el micro está en modo IM1 IM2. Pueden saber si las interrupciones estaban habilitadas o no consultando el valor del flip-flop IFF2 al que se accede mediante el bit de paridad del registro F tras ejecutar una instrucción LD A,ó LD A,, pero no se puede saber directamente si el micro está en modo IM 0, IM1 ó IM 2.//+ //Aunque se use este sistema, hay aún un "peligro" oculto, aunque considero que es de menor relevancia, al menos hoy día. Es el hecho de que los periféricos "copiones" tipo Transtape y similares, usados en un Spectrum real para crear un snapshot, no pueden saber si el micro está en modo im1 im2. Pueden saber si las interrupciones estaban habilitadas o no consultando el valor del flip-flop IFF2 al que se accede mediante el bit de paridad del registro F tras ejecutar una instrucción ld a,ó ld a,, pero no se puede saber directamente si el micro está en modo im 0, im1 ó im 2.//
  
-//En un emulador que cree snapshosts esto no es un problema, pero en un Spectrum real sí. Los "copiones" usan un método puramente heurístico que consiste en ver el valor de I: si dicho valor es mayor que 63, entonces asumen que el programa lo ha modificado con la intención de usar su propia ISR, por lo que graban el snapshot indicando que el micro estaba en modo IM 2. En cualquier otro caso, asumen IM 1.//+//En un emulador que cree snapshosts esto no es un problema, pero en un Spectrum real sí. Los "copiones" usan un método puramente heurístico que consiste en ver el valor de I: si dicho valor es mayor que 63, entonces asumen que el programa lo ha modificado con la intención de usar su propia ISR, por lo que graban el snapshot indicando que el micro estaba en modo im 2. En cualquier otro caso, asumen im 1.//
  
-//Habría que ver el código de la ROM de los copiones más populares para ver qué condición chequean, ya que la otra opción es que asuman IM 1 sólamente cuando I valga 63 (el valor que la ROM pone por defecto) y asuman IM 2 en cualquier otro caso. Si es así como lo hacen, un snapshot de 16K con interrupciones se generará con la información correcta.//+//Habría que ver el código de la ROM de los copiones más populares para ver qué condición chequean, ya que la otra opción es que asuman im 1 sólamente cuando I valga 63 (el valor que la ROM pone por defecto) y asuman im 2 en cualquier otro caso. Si es así como lo hacen, un snapshot de 16K con interrupciones se generará con la información correcta.//
  
  
Línea 1223: Línea 1307:
 ==== El bug de la ISR de NMI en la ROM ==== ==== El bug de la ISR de NMI en la ROM ====
  
- Resulta especialmente curioso el motivo por el cual las interrupciones NMI son generalmente de nula utilidad en el Spectrum salvo para provocar un reset. Como ya hemos dicho, al recibirse una NMI se realiza un salto a $0066 donde hay un bloque de código "ISR" especial de la ROM el cual, en teoría, estaba preparado para permitir la ejecución de una subrutina propia cuya dirección ubicaríamos en la dirección de memoria **$5CB0** (''NMIADD''), a menos que el contenido de $5CB0 fuera 0, que provocaría un retorno de la NMI.+ Resulta especialmente curioso el motivo por el cual las interrupciones NMI son generalmente de nula utilidad en el Spectrum salvo para provocar un reset. Como ya hemos dicho, al recibirse una NMI se realiza un salto a $0066 donde hay un bloque de código "ISR" especial de la ROM el cual, en teoría, estaba preparado para permitir la ejecución de una subrutina propia cuya dirección ubicaríamos en la dirección de memoria **$5cb0** (''NMIADD''), a menos que el contenido de $5cb0 fuera 0, que provocaría un retorno de la NMI.
  
-Por desgracia, un bug en esta subrutina acabó dejándola inservible salvo en el caso de ubicar un cero en esta variable del sistema, con RESET como única consecuencia. Como puede verse, en lugar de ''JR Z'' para saltar si $5CB0 valía 0, el programador escribió ''JR NZ'' lo cual produce que si NMIADD es distinta de 0, se salte a NO-RESET (y no se haga nada), y si NMIADD es 0, se salte con ''JP (HL)'' a su contenido, que al ser 0 produce un reset.+Por desgracia, un bug en esta subrutina acabó dejándola inservible salvo en el caso de ubicar un cero en esta variable del sistema, con RESET como única consecuencia. Como puede verse, en lugar de ''JR Z'' para saltar si $5cb0 valía 0, el programador escribió ''JR NZ'' lo cual produce que si NMIADD es distinta de 0, se salte a NO-RESET (y no se haga nada), y si NMIADD es 0, se salte con ''jp (hl)'' a su contenido, que al ser 0 produce un reset.
  
-La instrucción ''JR NZ, $0070'' debería haber sido un ''JR Z, $0070''" para permitir un ''JP ($5CB0)'' al recibir una NMI.+La instrucción ''jr nz, $0070'' debería haber sido un ''jr z, $0070''" para permitir un ''jp ($5cb0)'' al recibir una NMI.
  
 <code z80> <code z80>
Línea 1235: Línea 1319:
 ; NMI line. The system variable at 5CB0, named here NMIADD, has ; NMI line. The system variable at 5CB0, named here NMIADD, has
 ; to have the value zero for the reset to occur. ; to have the value zero for the reset to occur.
-0066 RESET:       PUSH  AF                  ; Save the current values held +0066 RESET:       push  af                  ; Save the current values held 
-                  PUSH  HL                  ; in these registers. +                  push  hl                  ; in these registers. 
-                  LD    HL,($5CB0)          ; The two bytes of NMIADD +                  ld    hl,($5cb0)          ; The two bytes of NMIADD 
-                  LD    A,                ; must both be zero for the reset +                  ld    a,                ; must both be zero for the reset 
-                  OR                      ; to occur. +                  or                      ; to occur. 
-                  JR    NZ,$0070            ; Note: This should have been JR Z! +                  jr    nz,$0070            ; Note: This should have been jr Z! 
-                  JP    (HL)                ; Jump to START. +                  jp    (hl)                ; Jump to START. 
-0070 NO-RESET     POP   HL                  ; Restore the current values to +0070 NO-RESET     pop   hl                  ; Restore the current values to 
-                  POP   AF                  ; these registers and return. +                  pop   af                  ; these registers and return. 
-                  RETN+                  retn
 </code> </code>
  
Línea 1268: Línea 1352:
   * [[http://www.z80.info|Web del Z80]].   * [[http://www.z80.info|Web del Z80]].
   * [[http://www.z80.info/z80undoc3.txt|z80undoc3.txt en z80.info]].   * [[http://www.z80.info/z80undoc3.txt|z80undoc3.txt en z80.info]].
-  * [[http://www.z80.info/interrup2.htm|El bit 0 en IM2]].+  * [[http://www.z80.info/interrup2.htm|El bit 0 en im2]].
   * [[http://www.z88dk.org/wiki/doku.php?id=library:interrupts|Interrupciones en Z88DK]].   * [[http://www.z88dk.org/wiki/doku.php?id=library:interrupts|Interrupciones en Z88DK]].
   * The Complete Spectrum ROM Disassembly [[ftp://ftp.worldofspectrum.org/pub/sinclair/books/CompleteSpectrumROMDisassemblyThe.txt|TXT]] y [[ftp://ftp.worldofspectrum.org/pub/sinclair/books/CompleteSpectrumROMDisassemblyThe.pdf|PDF]].   * The Complete Spectrum ROM Disassembly [[ftp://ftp.worldofspectrum.org/pub/sinclair/books/CompleteSpectrumROMDisassemblyThe.txt|TXT]] y [[ftp://ftp.worldofspectrum.org/pub/sinclair/books/CompleteSpectrumROMDisassemblyThe.pdf|PDF]].
  • cursos/ensamblador/interrupciones.1705216860.txt.gz
  • Última modificación: 14-01-2024 07:21
  • por sromero