Diferencias
Muestra las diferencias entre dos versiones de la página.
Ambos lados, revisión anterior Revisión previa Próxima revisión | Revisión previaÚltima revisiónAmbos lados, revisión siguiente | ||
cursos:ensamblador:interrupciones [14-01-2024 07:21] – [Control de ticks, segundos y minutos] sromero | cursos:ensamblador:interrupciones [21-01-2024 11:20] – [Utilizar un JP como ISR] sromero | ||
---|---|---|---|
Línea 11: | Línea 11: | ||
| | ||
- | El Z80A (el corazón del ZX Spectrum) dispone de 2 tipos de señales de interrupción: | + | El Z80A (el corazón del ZX Spectrum) dispone de 2 tipos de señales de interrupción: |
\\ | \\ | ||
Línea 35: | Línea 35: | ||
| | ||
- | * **Modo 0**: En este modo de interrupción, | + | * **Modo 0**: En este modo de interrupción, |
- | * **Modo 1**: Cuando se recibe una señal INT y el procesador está en IM 1, el Z80 ejecuta un DI (Disable Interrupts), | + | * **Modo 1**: Cuando se recibe una señal INT y el procesador está en im 1, el Z80 ejecuta un di (Disable Interrupts), |
* **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, | * **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, | ||
Línea 47: | Línea 47: | ||
| | ||
- | 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: |
< | < | ||
Línea 53: | Línea 53: | ||
</ | </ | ||
- | 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 " | + | * Combina el valor de I ($fe en nuestro ejemplo) con el " |
- | * 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). |
</ | </ | ||
- | 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 ('' | + | 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 ('' |
- | 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, | + | 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, |
\\ | \\ | ||
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 |
</ | </ | ||
Línea 108: | Línea 108: | ||
<code z80> | <code z80> | ||
- | HALT ; Halt computer and wait for INT (4 T-Estados). | + | halt ; Halt computer and wait for INT (4 T-Estados). |
</ | </ | ||
Línea 119: | Línea 119: | ||
==== 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 '' | + | Una instrucción de uso infrecuente con una peculiar utilidad es '' |
- | 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 '' | + | Así, una vez ejecuado un '' |
- | Como curiosidad, la instrucción '' | + | Como curiosidad, la instrucción '' |
\\ | \\ | ||
Línea 134: | Línea 134: | ||
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), | + | Esto quiere decir que cada 1/50 (o 1/60) segundos, la ULA produce una señal INT (interrupción enmascarable), |
- | En el modo '' | + | En el modo '' |
- | En cuanto al modo '' | + | En cuanto al modo '' |
- | Como ya hemos visto en la definición del modo '' | + | Como ya hemos visto en la definición del modo '' |
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 157: | ||
Así pues, por convención, | Así pues, por convenció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 166: | ||
==== 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 '' | + | La ULA provee al Spectrum en modo '' |
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 174: | ||
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, | 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, | ||
- | 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 | + | 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 |
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 " | 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 " | ||
Línea 191: | Línea 191: | ||
{{ cursos: | {{ cursos: | ||
- | | + | |
- | 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 264: | ||
bucle: | bucle: | ||
; hacer algo | ; hacer algo | ||
- | | + | |
</ | </ | ||
Línea 271: | Línea 271: | ||
<code z80> | <code z80> | ||
bucle: | bucle: | ||
- | | + | |
; hacer algo | ; hacer algo | ||
- | | + | |
</ | </ | ||
- | 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 '' | + | 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 '' |
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 288: | ||
===== 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 294: | ||
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 '' | 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 '' | ||
- | 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 '' | + | 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 '' |
Así pues, de las rutinas ISR llamadas en las interrupciones se debe de volver con una instrucción '' | Así pues, de las rutinas ISR llamadas en las interrupciones se debe de volver con una instrucción '' | ||
<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). |
</ | </ | ||
Línea 317: | Línea 317: | ||
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 326: | ||
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, | + | (reti according to the official documentation, |
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. | + | You can use ret in stead of reti too, it depends on hardware setup. |
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 | + | I called all the undocumented EDxx ret instructions |
- | 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.) | ||
</ | </ | ||
- | Es decir, para aquellos sistemas basados en Z80 con hardware PIO que soporte múltiples dispositivos I/O encadenando sus interrupciones, | + | Es decir, para aquellos sistemas basados en Z80 con hardware PIO que soporte múltiples dispositivos I/O encadenando sus interrupciones, |
En el caso del Spectrum con la ULA como (habitualmente) único dispositivo que interrumpe, se utiliza normalmente '' | En el caso del Spectrum con la ULA como (habitualmente) único dispositivo que interrumpe, se utiliza normalmente '' | ||
- | Como ya hemos comentado antes, | + | Como ya hemos comentado antes, |
<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 | ||
- | | + | |
(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 | ||
- | | + | |
- | | + | |
- | | + | |
</ | </ | ||
- | Si '' | + | Si '' |
- | | + | |
- | | + | |
\\ | \\ | ||
===== 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 ('' | + | A modo de curiosidad, vamos a ver el código de la ISR que se ejecuta en modo 1 ('' |
<code z80> | <code z80> | ||
Línea 387: | Línea 387: | ||
; 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 | + | ; BYTE 3 FRAMES |
; | ; | ||
0038 MASK-INT: | 0038 MASK-INT: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
0048 KEY-INT: | 0048 KEY-INT: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
</ | </ | ||
Línea 416: | Línea 416: | ||
| | ||
- | La rutina actualiza el valor de la variable del sistema '' | + | La rutina actualiza el valor de la variable del sistema '' |
- | | + | |
\\ | \\ | ||
===== 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 '' | + | 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 '' |
- | Como hemos explicado anteriormente, | + | Como hemos explicado anteriormente, |
- | 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 " | + | 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 " |
- | Si sólo vamos a recibir un $FF en el bus de datos cada vez que se ejecute una interrupción, | + | Si sólo vamos a recibir un $ff en el bus de datos cada vez que se ejecute una interrupción, |
- | 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 444: | ||
* Desactivar las interrupciones con '' | * Desactivar las interrupciones con '' | ||
- | * 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 '' | + | * Asignar a I el valor $fe y saltar a '' |
* Activar las interrupciones con '' | * Activar las interrupciones con '' | ||
Línea 459: | Línea 459: | ||
; Instalamos la ISR: | ; Instalamos la ISR: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
(resto programa) | (resto programa) | ||
Línea 471: | Línea 471: | ||
;--- Rutina de ISR. --------------------------------------------- | ;--- Rutina de ISR. --------------------------------------------- | ||
ISR_ASM_ROUTINE: | ISR_ASM_ROUTINE: | ||
- | | + | |
- | | + | |
- | | + | |
(código de la ISR) | (código de la ISR) | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
</ | </ | ||
- | De esta forma, saltamos a '' | + | De esta forma, saltamos a '' |
\\ | \\ | ||
* 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 '' | + | * 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 '' |
* 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 '' | + | Finalmente destacar que (aunque no es lo habitual) desde dentro de nuestra rutina ISR en im2 podemos llamar a '' |
<code z80> | <code z80> | ||
Rutina_ISR: | Rutina_ISR: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | ; IY no lo podemos tocar si usamos | + | ; IY no lo podemos tocar si usamos |
; Aqui hacemos nuestras cosas, como actualizar contadores | ; Aqui hacemos nuestras cosas, como actualizar contadores | ||
Línea 520: | Línea 520: | ||
(...) | (...) | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
</ | </ | ||
- | 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 '' | + | 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 '' |
- | 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> | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | INC | + | INC |
frames_no_inc: | frames_no_inc: | ||
</ | </ | ||
Línea 549: | Línea 549: | ||
<code z80> | <code z80> | ||
- | | + | |
- | | + | |
</ | </ | ||
- | 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> | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
frames_no_inc: | frames_no_inc: | ||
</ | </ | ||
Línea 574: | Línea 574: | ||
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 " | + | 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 " |
- | En la ISR incrementamos el valor de la variable '' | + | En la ISR incrementamos el valor de la variable '' |
Ese código de ISR está corriendo de forma automática 50 veces por segundo, por lo que '' | Ese código de ISR está corriendo de forma automática 50 veces por segundo, por lo que '' | ||
Línea 584: | Línea 584: | ||
ORG 50000 | ORG 50000 | ||
- | | + | |
; Instalamos la ISR: | ; Instalamos la ISR: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
Bucle: | Bucle: | ||
- | | + | |
ImprimirNumero: | ImprimirNumero: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
; | ; | ||
Línea 615: | Línea 615: | ||
CLOCK_ISR_ASM_ROUTINE: | CLOCK_ISR_ASM_ROUTINE: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
; por si alguien incremento su valor externamente) | ; por si alguien incremento su valor externamente) | ||
- | | + | |
- | | + | |
- | FRAMES_menor_que_50: | + | frames_menor_que_50: |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
ticks DB 0 | ticks DB 0 | ||
Línea 654: | Línea 654: | ||
===== 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 '' | + | Como ya hemos dicho, la ULA no está preparada para funcionar en '' |
- | | + | |
- | 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 664: | ||
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. |
< | < | ||
Línea 673: | Línea 673: | ||
Posición | Posición | ||
-------------------- | -------------------- | ||
- | ($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 |
</ | </ | ||
- | La desventaja de este sistema es que convertimos al versátil modo '' | + | La desventaja de este sistema es que convertimos al versátil modo '' |
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. | ||
- | | + | |
- | * 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 " | + | * 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 " |
- | * 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 " | * La Tabla de Vectores de Interrupción se debe mantener en memoria (son 257 bytes de memoria " | ||
Línea 708: | Línea 708: | ||
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 |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
; 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: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
; --------------------------------------------------------------- | ; --------------------------------------------------------------- | ||
Línea 728: | Línea 728: | ||
; --------------------------------------------------------------- | ; --------------------------------------------------------------- | ||
- | ; 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 743: | ||
; | ; | ||
CLOCK_ISR_ASM_ROUTINE: | CLOCK_ISR_ASM_ROUTINE: | ||
- | | + | |
- | | + | |
; (... aqui insertamos el resto de la ISR...) | ; (... aqui insertamos el resto de la ISR...) | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
; 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 764: | ||
</ | </ | ||
- | El '' | + | El '' |
<code z80> | <code z80> | ||
Línea 778: | Línea 778: | ||
; | ; | ||
- | ; 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 |
| | ||
Línea 789: | Línea 789: | ||
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 799: | ||
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 '' | + | * La rutina de ISR se ensambla en $f1f1 mediante una directiva del ensamblador '' |
- | * 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), | + | Otros autores de libros sobre programación (y a su vez programadores), |
\\ | \\ | ||
Línea 826: | Línea 826: | ||
\\ | \\ | ||
- | 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, | + | 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, |
Tras esto, nos mantenemos en un bucle de programa infinito que detecta cuándo la variable '' | Tras esto, nos mantenemos en un bucle de programa infinito que detecta cuándo la variable '' | ||
Línea 836: | Línea 836: | ||
ORG 40000 | ORG 40000 | ||
- | | + | |
- | ; Generamos una tabla de 257 valores "$A1" desde $FE00 a $FF00 | + | ; Generamos una tabla de 257 valores "$a1" desde $fe00 a $ff00 |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | ; Activamos | + | ; Activamos |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
Bucle: | Bucle: | ||
- | | + | |
- | | + | |
- | | + | |
; 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 | ||
- | | + | |
- | | + | |
ImprimirNumero: | ImprimirNumero: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
clock_changed DB 0 | clock_changed DB 0 | ||
Línea 891: | Línea 891: | ||
; | ; | ||
- | ; 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 908: | ||
; | ; | ||
CLOCK_ISR_ASM_ROUTINE: | CLOCK_ISR_ASM_ROUTINE: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
; si ticks >= 50, cambiar seg:min | ; si ticks >= 50, cambiar seg:min | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
clock_isr_fin: | clock_isr_fin: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
; 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 " | ; Con el " | ||
Línea 986: | Línea 986: | ||
\\ | \\ | ||
- | * '' | + | * '' |
* '' | * '' | ||
- | * En el programa tenemos 3 bloques diferentes de código: '' | + | * En el programa tenemos 3 bloques diferentes de código: '' |
\\ | \\ | ||
Línea 1005: | Línea 1005: | ||
* Actualizar regularmente el buffer de " | * Actualizar regularmente el buffer de " | ||
- | * 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 " | + | * 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 " |
\\ | \\ | ||
Línea 1014: | Línea 1014: | ||
; Instalamos la ISR: | ; Instalamos la ISR: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
Bucle_entrada: | Bucle_entrada: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
ticks DB 0 | ticks DB 0 | ||
Línea 1048: | Línea 1048: | ||
; | ; | ||
WaitNTicks: | WaitNTicks: | ||
- | | + | |
- | Waitnticks_loop: ; bucle de espera, la ISR lo ira decrementando | + | waitnticks_loop: ; bucle de espera, la ISR lo ira decrementando |
- | | + | |
- | | + | |
- | | + | |
; decir que ha pasado el tiempo a esperar. | ; decir que ha pasado el tiempo a esperar. | ||
- | | + | |
- | | + | |
; | ; | ||
Línea 1062: | Línea 1062: | ||
; | ; | ||
CLOCK_ISR_ASM_ROUTINE: | CLOCK_ISR_ASM_ROUTINE: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
clock_isr_fin: | clock_isr_fin: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
END 50000 | END 50000 | ||
Línea 1100: | Línea 1100: | ||
; | ; | ||
WaitKeyOrTime: | WaitKeyOrTime: | ||
- | | + | |
- | Waitkeyticks_loop: ; bucle de espera, la ISR lo ira decrementando | + | waitkeyticks_loop: ; bucle de espera, la ISR lo ira decrementando |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
+ | ; Si A!=0 => ZF = 0 => hay alguna tecla => salimos | ||
- | | + | |
- | | + | |
- | | + | |
; decir que ha pasado el tiempo a esperar. | ; decir que ha pasado el tiempo a esperar. | ||
- | | + | |
- | | + | |
</ | </ | ||
+ | |||
+ | |||
+ | \\ | ||
+ | ===== Utilizar un JP como ISR ===== | ||
+ | |||
+ | En muchos juegos comerciales y homebrew, la rutina de ISR no es una rutina en sí misma sino un '' | ||
+ | |||
+ | |||
+ | <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 " | ||
+ | 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 | ||
+ | </ | ||
+ | |||
+ | De esta forma, la tabla de vectores de interrupción y la rutina ISR (un simple '' | ||
Línea 1121: | Línea 1172: | ||
===== 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 " | + | Al respecto de la ubicación de la tabla de salto o " |
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 " | 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 " | ||
- | 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 1190: | ||
</ | </ | ||
- | 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> | ||
- | | + | |
- | | + | |
</ | </ | ||
- | 2. Antes de cambiar a '' | + | 2. Antes de cambiar a '' |
- | Para eso, creamos una rutina que sea " | + | Para eso, creamos una rutina que sea " |
* 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 1215: | ||
===== 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 " | + | Lo mismo ocurre con la dirección de nuestra ISR con el " |
+ | |||
+ | 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, | ||
+ | |||
+ | \\ | ||
+ | |< 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 '' | ||
+ | | Where Time Stood Still | $8400-$8500 | $bebe | Rutina ISR | - | | ||
+ | | Demo 7th Reality | $6300-$6400 | $6464 | ISR es '' | ||
+ | | La Abadia Del Crimen | $be00-$bf00 | $bfbf | Rutina ISR | - | | ||
+ | | Chase HQ 2 | $9b00-$9c00 | $fdfd | ISR es '' | ||
+ | | Grand Prix Circuit | $8200-$8300 | $6363 | ISR es '' | ||
+ | | Robocop 2 | $7700-$7800 | $5b5b | ISR es '' | ||
+ | | Robocop 3 | $7700-$7800 | $7676 | ISR es '' | ||
+ | | Spacegun | $be00-$bf00 | $bfbf | ISR es '' | ||
+ | | Desafio Total (Total Recall) | $9100-$9200 | $5d5d | ISR es '' | ||
+ | | Carrier Command | $8300-$8400 | $8585 | Rutina ISR | - | | ||
+ | | El Gran Halcón (Hudson Hawk) | $9000-$8100 | $8181 | Rutina ISR | - | | ||
+ | | NARC | $be00-$bf00 | $bfbf | ISR es '' | ||
+ | | Navy Seals | $9100-$9200 | $5d5d | ISR es '' | ||
+ | | Pang | $8000-$8101 | $8181 | ISR es '' | ||
+ | \\ | ||
+ | |||
+ | Varias curiosidades: | ||
+ | |||
+ | * Como se puede ver, no hay un estándar sobre dónde ubicar la tabla de vectores de interrupción, | ||
+ | |||
+ | * 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, | ||
+ | |||
+ | * Sólo 4 juegos de la lista implementan la rutina directamente en la dirección apuntada por el vector de interrupciones, | ||
- | 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, | + | 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, |
- | | + | |
- | | + | |
| | ||
Línea 1190: | Línea 1272: | ||
{{ cursos: | {{ cursos: | ||
- | // | + | // |
<code z80> | <code z80> | ||
- | | + | |
- | | + | |
... resto del programa... | ... resto del programa... | ||
Línea 1201: | Línea 1283: | ||
</ | </ | ||
- | // | + | // |
- | //En un emulador que cree snapshosts esto no es un problema, pero en un Spectrum real sí. Los " | + | //En un emulador que cree snapshosts esto no es un problema, pero en un Spectrum real sí. Los " |
- | //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 | + | //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 |
Línea 1223: | Línea 1305: | ||
==== El bug de la ISR de NMI en la ROM ==== | ==== El bug de la ISR de NMI en la ROM ==== | ||
- | | + | |
- | 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 '' | + | 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 '' |
- | La instrucción '' | + | La instrucción '' |
<code z80> | <code z80> | ||
Línea 1235: | Línea 1317: | ||
; 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: | + | 0066 RESET: |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | 0070 NO-RESET | + | 0070 NO-RESET |
- | | + | |
- | | + | |
</ | </ | ||
Línea 1268: | Línea 1350: | ||
* [[http:// | * [[http:// | ||
* [[http:// | * [[http:// | ||
- | * [[http:// | + | * [[http:// |
* [[http:// | * [[http:// | ||
* The Complete Spectrum ROM Disassembly [[ftp:// | * The Complete Spectrum ROM Disassembly [[ftp:// |