cursos:ensamblador:paginacion_128k

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:paginacion_128k [04-01-2024 18:24] – [Paginación de memoria desde Z88DK (C)] sromerocursos:ensamblador:paginacion_128k [19-01-2024 12:23] (actual) – [Ejemplo sencillo: alternando Bancos 0 y 1] sromero
Línea 10: Línea 10:
  Los modelos de Spectrum 128K, +2, +2A, +2B y +3 disponen de 128KB de memoria, aunque no toda está disponible simultáneamente. Al igual que en el modelo de 48KB, el microprocesador Z80 sólo puede direccionar 64K-direcciones de memoria, por lo que para acceder a esta memoria "extra", se divide en bloques de 16KB y se "mapea" (pagina) sobre la dirección de memoria $c000.  Los modelos de Spectrum 128K, +2, +2A, +2B y +3 disponen de 128KB de memoria, aunque no toda está disponible simultáneamente. Al igual que en el modelo de 48KB, el microprocesador Z80 sólo puede direccionar 64K-direcciones de memoria, por lo que para acceder a esta memoria "extra", se divide en bloques de 16KB y se "mapea" (pagina) sobre la dirección de memoria $c000.
  
- ¿Qué quiere decir esto? Que nuestro procesador, con un bus de direcciones de 16 bits, sólo puede acceder a la memoria para leer las "celdillas" entre $0000 y $FFFF (0-65535). Para leer casillas de memoria superiores a 65535, harían falta más de 16 bits de direcciones, ya que 65535 es el mayor entero que se puede formar con 16 bits (1111111111111111b).+ ¿Qué quiere decir esto? Que nuestro procesador, con un bus de direcciones de 16 bits, sólo puede acceder a la memoria para leer las "celdillas" entre $0000 y $ffff (0-65535). Para leer casillas de memoria superiores a 65535, harían falta más de 16 bits de direcciones, ya que 65535 es el mayor entero que se puede formar con 16 bits (1111111111111111b).
  
  Así que ... ¿cómo hacemos para utilizar más de 64KB de memoria si nuestro procesador sólo puede leer datos de la celdilla 0, 1, 2, 3 ... 65534 y 65535? La respuesta es: **mediante la paginación**.  Así que ... ¿cómo hacemos para utilizar más de 64KB de memoria si nuestro procesador sólo puede leer datos de la celdilla 0, 1, 2, 3 ... 65534 y 65535? La respuesta es: **mediante la paginación**.
  
- Los 64KB de memoria del Spectrum, se dividen en 4 bloques de 16KB. El primer bloque ($0000 - $4000) está mapeado sobre la ROM del Spectrum. Accediendo a los bytes desde $0000 a $3FFF de la memoria estamos accediendo a los bytes $0000 a $3FFF del "chip (físico)" de 16K de memoria  que almacena la ROM del Spectrum. Se dice, pues, que la ROM está mapeada (o paginada) sobre $0000 en el mapa de memoria.+ Los 64KB de memoria del Spectrum, se dividen en 4 bloques de 16KB. El primer bloque ($0000 - $4000) está mapeado sobre la ROM del Spectrum. Accediendo a los bytes desde $0000 a $3fff de la memoria estamos accediendo a los bytes $0000 a $3fff del "chip (físico)" de 16K de memoria  que almacena la ROM del Spectrum. Se dice, pues, que la ROM está mapeada (o paginada) sobre $0000 en el mapa de memoria.
  
- Dejando la ROM de lado (16KB), los 48KB de memoria restantes están formados por 3 bloques de 16KB. El segundo bloque de 16K de memoria (primero de los 3 que estamos comentando), en el rango $4000 a $7FFF está mapeado sobre el "chip de memoria" que almacena el área de pantalla del Spectrum. El tercero ($8000 a $BFFFF) es memoria de propósito general (normalmente cargaremos nuestros programas aquí).+ Dejando la ROM de lado (16KB), los 48KB de memoria restantes están formados por 3 bloques de 16KB. El segundo bloque de 16K de memoria (primero de los 3 que estamos comentando), en el rango $4000 a $7fff está mapeado sobre el "chip de memoria" que almacena el área de pantalla del Spectrum. El tercero ($8000 a $bfffF) es memoria de propósito general (normalmente cargaremos nuestros programas aquí).
  
- El bloque que nos interesa a nosotros es el cuarto. La zona final del área de memoria, desde $C000 a $FFFF, es nuestra "ventana" hacia el resto de memoria del 128K. Dividiendo los 128KB de memoria en bloques de 16KB, tendremos 8 bloques que podremos "montar" (o paginar) sobre el área $C000 a $FFFF.+ El bloque que nos interesa a nosotros es el cuarto. La zona final del área de memoria, desde $c000 a $ffff, es nuestra "ventana" hacia el resto de memoria del 128K. Dividiendo los 128KB de memoria en bloques de 16KB, tendremos 8 bloques que podremos "montar" (o paginar) sobre el área $c000 a $ffff.
  
  Veamos una figura donde se muestra el estado del **mapa de memoria** del Spectrum:  Veamos una figura donde se muestra el estado del **mapa de memoria** del Spectrum:
Línea 26: Línea 26:
 \\  \\ 
  
- Cualquiera de los 8 bloques de 16KB (128 KB / 16 KB = 8 bloques) puede ser "mapeado" en los 16Kb que van desde $c000 a $ffff, y podremos cambiar este mapeo de un bloque a otro mediante instrucciones I/O concretas. + Cualquiera de los 8 bloques de 16KB (128 KB / 16 KB = 8 bloques) puede ser "mapeado" en los 16Kb que van desde $c000 a $ffff, y podremos cambiar este mapeo de un bloque a otro mediante instrucciones I/O concretas.
  
- La última porción de 16KB de la memoria es, pues, una "ventana" que podemos deslizar para que nos dé acceso a cualquiera de los 8 bloques disponibles. Esto nos permite "paginar" el bloque 0 y escribir o leer sobre él, por ejemplo. El byte 0 del bloque 0 se accede a través de la posición de memoria $C000 una vez paginado, el byte 1 desde $C0001, y así hasta el byte 16383, al que accedemos mediante $FFFF.+ La última porción de 16KB de la memoria es, pues, una "ventana" que podemos deslizar para que nos dé acceso a cualquiera de los 8 bloques disponibles. Esto nos permite "paginar" el bloque 0 y escribir o leer sobre él, por ejemplo. El byte 0 del bloque 0 se accede a través de la posición de memoria $c000 una vez paginado, el byte 1 desde $c0001, y así hasta el byte 16383, al que accedemos mediante $ffff.
  
- Si paginamos el bloque 1 en nuestra ventana $C000-$FFFF, cuando accedamos a este rango de memoria, ya no accederíamos a los mismos datos que guardamos en el banco 0, sino a la zona de memoria "Banco 1". Es posible incluso mapear la zona de pantalla (Banco 5), de forma que las direcciones $4000 y $C000 serían equivalentes: los 8 primeros píxeles de la pantalla.+ Si paginamos el bloque 1 en nuestra ventana $c000-$ffff, cuando accedamos a este rango de memoria, ya no accederíamos a los mismos datos que guardamos en el banco 0, sino a la zona de memoria "Banco 1". Es posible incluso mapear la zona de pantalla (Banco 5), de forma que las direcciones $4000 y $c000 serían equivalentes: los 8 primeros píxeles de la pantalla.
  
- El mapa de memoria del Spectrum con los bloques mapeables/paginables sobre $C000 es el siguiente:+ El mapa de memoria del Spectrum con los bloques mapeables/paginables sobre $c000 es el siguiente:
  
 \\  \\ 
 {{ cursos:ensamblador:pag_normal.png | Paginación de los modelos 128K/+2 }} {{ cursos:ensamblador:pag_normal.png | Paginación de los modelos 128K/+2 }}
 +\\ 
  
 \\  \\ 
 ===== Cambiando de banco ===== ===== Cambiando de banco =====
  
- El puerto que controla la paginación en los modelos 128K es el $7FFD. En realidad, nuestro Spectrum sólo decodifica los bits 1 y 15, por lo que cualquier combinación con los bits 1 y 15 a cero accederá a la gestión de paginación. No obstante, se recomiente utilizar $7FFD por compatibilidad con otros sistemas.+ El puerto que controla la paginación en los modelos 128K es el $7ffd. En realidad, nuestro Spectrum sólo decodifica los bits 1 y 15, por lo que cualquier combinación con los bits 1 y 15 a cero accederá a la gestión de paginación. No obstante, se recomiente utilizar $7ffd por compatibilidad con otros sistemas.
  
- La lectura del puerto $7FFD no devolverá ningún valor útil, pero sí podemos escribir en él un valor con cierto formato:+ La lectura del puerto $7ffd no devolverá ningún valor útil, pero sí podemos escribir en él un valor con cierto formato:
  
 |< 60% >| |< 60% >|
Línea 48: Línea 49:
 | 0-2 | Página de la RAM (0-7) a mapear en el bloque $c000 - $ffff. | | 0-2 | Página de la RAM (0-7) a mapear en el bloque $c000 - $ffff. |
 | 3 | Visualizar la pantalla gráfica "normal" (0) o shadow (1).\\ La pantalla normal está en el banco 5, y la shadow en el 7.\\ Aunque cambiemos a la visualización de la pantalla shadow,\\ la pantalla "normal" RAM5 seguirá mapeada entre $4000 y $7fff.\\ No es necesario tener mapeada la pantalla shadow\\ para que pueda ser visualizado su contenido. | | 3 | Visualizar la pantalla gráfica "normal" (0) o shadow (1).\\ La pantalla normal está en el banco 5, y la shadow en el 7.\\ Aunque cambiemos a la visualización de la pantalla shadow,\\ la pantalla "normal" RAM5 seguirá mapeada entre $4000 y $7fff.\\ No es necesario tener mapeada la pantalla shadow\\ para que pueda ser visualizado su contenido. |
-| 4 | Selección de la ROM, entre (0) ROM BASIC 128K (Menú y editor), y (1) BASIC 48K. |  +| 4 | Selección de la ROM, entre (0) ROM BASIC 128K (Menú y editor), y (1) BASIC 48K. | 
-| 5 | Si lo activamos, se desactivará el paginado de memoria hasta que se resetee el\\ Spectrum. El hardware ignorará toda escritura al puerto $7FFD. | +| 5 | Si lo activamos, se desactivará el paginado de memoria hasta que se resetee el\\ Spectrum. El hardware ignorará toda escritura al puerto $7ffd. | 
- +
  A la hora de cambiar de página de memoria hay que tener en cuenta lo siguiente:  A la hora de cambiar de página de memoria hay que tener en cuenta lo siguiente:
  
 \\  \\ 
-  * La pila (stack) debe de estar ubicada en una zona de memoria que no vaya a ser paginada (no puede estar dentro de la zona que va a cambiar). 
- 
   * Las interrupciones deben de estar deshabilitadas para realizar el cambio de banco.   * Las interrupciones deben de estar deshabilitadas para realizar el cambio de banco.
  
-  * Si se va a ejecutar código con interrupciones (y no pueden estar deshabilitadas), entonces debemos actualizar la variable del sistema $5B5C (23388d) con el último valor enviado al puerto $7FFD.+  * La pila (stack) debe de estar ubicada en una zona de memoria que no vaya a ser paginada (no puede estar dentro de la zona que va a cambiar), ya que su contenido se perdería después del cambio de banco. 
 + 
 +  * Si estamos utilizando o vamos a utilizar el modo de interrupciones 2 (''im 2''), tanto la tabla de vectores de salto como la propia ISR de gestión de la interrupción debe de estar en el área de memoria por debajo de $c000. Si no es así, la interrupción produciría un salto a una dirección incorrecta y se ejecutaría "código aleatorio" que no tendría nada que ver con la Rutina de Servicio de Interrupción de nuestro programa. Como es habitual tener la tabla de vectores de interrupción a partir de $fe00, podemos copiar la tabla en todos los bancos que vamos a usar (como se explicó en el capítulo dedicado a las interrupciones). Se podría hacer lo mismo con la ISR pero lo normal es ubicarla en la zona de memoria entre los 32 y los 48KB. 
 + 
 +  * Si se va a ejecutar código con interrupciones (y no pueden estar deshabilitadas), entonces debemos actualizar la variable del sistema $5b5c (23388d) con el último valor enviado al puerto $7ffd.
 \\  \\ 
  
Línea 64: Línea 67:
  
 <code z80> <code z80>
-     LD      A, ($5b5c)      ; Valor previo del puerto (variable del sistema) +    ld a, ($5b5c)        ; Valor previo del puerto (variable del sistema) 
-     AND     $f8             ; Cambia sólo los bits que debas cambiar +    and %11111000        ; Cambia sólo los bits que debas cambiar 
-     OR      4               ; Seleccionar banco 4 +    or %00000100         ; Seleccionar banco 4 
-     LD      BC, $7ffd       ; Colocamos en BC el puerto a +    ld bc, $7ffd         ; Colocamos en BC el puerto a 
-     DI                      ; Deshabilitamos las interrupciones +    di                   ; Deshabilitamos las interrupciones 
-     LD      ($5b5c), A      ; Actualizamos la variable del sistema +    ld ($5b5c), a        ; Actualizamos la variable del sistema 
-     OUT     (C), A          ; Realizamos el paginado +    out (c), a           ; Realizamos el paginado 
-     EI+    ei
 </code> </code>
  
Línea 83: Línea 86:
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
 SetRAMBank: SetRAMBank:
-   LD A, ($5b5c)      ; Valor previo del puerto (variable del sistema) +    ld a, ($5b5c)      ; Valor previo del puerto (variable del sistema) 
-   AND $f8             ; Cambia sólo los bits que debas cambiar +    and %11111000      ; Cambia sólo los bits que debas cambiar 
-   OR B               ; Seleccionar banco "B" +    or b               ; Seleccionar banco "B" 
-   LD BC, $7ffd       ; Colocamos en BC el puerto a +    ld bc, $7ffd       ; Colocamos en BC el puerto a 
-   DI                      ; Deshabilitamos las interrupciones +    di                 ; Deshabilitamos las interrupciones 
-   LD ($5b5c),      ; Actualizamos la variable del sistema +    ld ($5b5c),      ; Actualizamos la variable del sistema 
-   OUT (C), A          ; Realizamos el paginado +    out (c), a         ; Realizamos el paginado 
-   EI +    ei 
-   RET+    ret
 </code> </code>
  
- + Un detalle apuntado por la documentación de World Of Spectrum es que los bancos 1, 3, 5 y 7 son "contended memory", lo que quiere decir que se reduce ligeramente la velocidad de acceso a estos bancos con respecto a los otros bancos. Para terminar de complicarlo, en el caso del +2A y +3, los bancos de contended-memory ya no son el 1, 3, 5 y 7, sino los bloques 4, 5, 6 y 7.
- Un detalle apuntado por la documentación de World Of Spectrum es que los bancos 1, 3, 5 y 7 son "contended memory", lo que quiere decir que se reduce ligeramente la velocidad de acceso a estos bancos con respecto a los otros bancos. Un apunte muy importante es que en el caso del +2A y +3, los bancos de contended-memory ya no son el 1, 3, 5 y 7, sino los bloques 4, 5, 6 y 7. Al final de este capítulo veremos con más detalle qué es la contended-memory y en qué puede afectar a nuestros programas.+
  
 \\  \\ 
Línea 105: Línea 107:
   * Los bancos de contended-memory son los bloques 4, 5, 6 y 7 (no el 1, 3, 5 y 7).   * Los bancos de contended-memory son los bloques 4, 5, 6 y 7 (no el 1, 3, 5 y 7).
  
-  * +2A y +3 tienen 4 ROMS en lugar de 2, por lo que el bit 4 del puerto $7FFD se convierte ahora en el bit bajo de la ROM a seleccionar, mientras que el bit alto se toma del bit 2.+  * +2A y +3 tienen 4 ROMS en lugar de 2, por lo que el bit 4 del puerto $7ffd se convierte ahora en el bit bajo de la ROM a seleccionar, mientras que el bit alto se toma del bit 2.
  
-  * +2A y +3 tienen funcionalidades extra de paginado, que se controlan con el puerto $1FFD.+  * +2A y +3 tienen funcionalidades extra de paginado, que se controlan con el puerto $1ffd.
 \\  \\ 
  
- Este puerto (el $1FFD) tiene el siguiente significado a la hora de escribir en él:+ Este puerto (el $1ffd) tiene el siguiente significado a la hora de escribir en él:
  
 |< 60% >| |< 60% >|
Línea 119: Línea 121:
 | 3-4 | 3=Motor del disco (1/0, ON/OFF), 4=Impresora | | 3-4 | 3=Motor del disco (1/0, ON/OFF), 4=Impresora |
  
- Cuando se activa el modo especial, el mapa de memoria cambia a una de estas configuraciones, según los valores de los bits 1 y 2 del puerto $1FFD:+ Cuando se activa el modo especial, el mapa de memoria cambia a una de estas configuraciones, según los valores de los bits 1 y 2 del puerto $1ffd:
  
 \\ \\
Línea 130: Línea 132:
 ^ ROM ^ Contenido ^ ^ ROM ^ Contenido ^
 | 0 | Editor 128K, Menú y programa de testeo | | 0 | Editor 128K, Menú y programa de testeo |
-| 1 | Chequeador de sintaxis 128K BASIC | +| 1 | Chequeador de sintaxis 128K BASIC |
 | 2 | +3DOS | | 2 | +3DOS |
 | 3 | BASIC 48K | | 3 | BASIC 48K |
  
- De nuevo, al igual que en el caso del puerto genérico sobre paginación, cada vez que alteremos el "+3 Memory Paging Control" ($1FFD) es recomendable actualizar la variable del sistema que almacena el "valor actual" de este puerto, en $5B67 (23399).+ De nuevo, al igual que en el caso del puerto genérico sobre paginación, cada vez que alteremos el "+3 Memory Paging Control" ($1ffd) es recomendable actualizar la variable del sistema que almacena el "valor actual" de este puerto, en $5b67 (23399).
  
 \\  \\ 
Línea 142: Línea 144:
  
 \\  \\ 
-  * Paginamos el bloque/banco 0 sobre el área $C000-$FFFF.+  * Paginamos el bloque/banco 0 sobre el área $c000-$ffff.
  
-  * Escribimos en memoria, en la posición $C000, el valor $AA.+  * Escribimos en memoria, en la posición $c000, el valor $aa.
  
-  * Paginamos el bloque/banco 1 sobre el área $C000-$FFFF.+  * Paginamos el bloque/banco 1 sobre el área $c000-$ffff.
  
-  * Escribimos en memoria, en la posición $C000, el valor $01.+  * Escribimos en memoria, en la posición $c000, el valor $01.
  
   * Volvemos a paginar el banco 0 sobre el área de paginación.   * Volvemos a paginar el banco 0 sobre el área de paginación.
  
-  * Leemos el valor de la posición de memoria $C000 y rellenamos toda la pantalla con dicho valor.+  * Leemos el valor de la posición de memoria $c000 y rellenamos toda la pantalla con dicho valor.
  
   * Volvemos a paginar el banco 1 sobre el área de paginación.   * Volvemos a paginar el banco 1 sobre el área de paginación.
  
-  * Leemos el valor de la posición de memoria $C000 y rellenamos toda la pantalla con dicho valor.+  * Leemos el valor de la posición de memoria $c000 y rellenamos toda la pantalla con dicho valor.
 \\  \\ 
  
- Haciendo esto, guardamos 2 valores diferentes en 2 bancos diferentes, y posteriormente recuperamos dichos bancos para verificar que, efectivamente, los valores siguen en las posiciones (0000) de los bancos y que la paginación de una banco a otro funciona adecuadamente. Se han elegido los valores $AA y $01 porque se muestra en pantalla como 2 tramas de pixeles bastante diferenciadas, siendo la primera un entramado de barras verticales separadas por 1 pixel, y la segunda separados por 7 pixeles.+ Haciendo esto, guardamos 2 valores diferentes en 2 bancos diferentes, y posteriormente recuperamos dichos bancos para verificar que, efectivamente, los valores siguen en las posiciones (0000) de los bancos y que la paginación de una banco a otro funciona adecuadamente. Se han elegido los valores $aa y $01 porque se muestra en pantalla como 2 tramas de pixeles bastante diferenciadas, siendo la primera un entramado de barras verticales separadas por 1 pixel, y la segunda separados por 7 pixeles.
  
  Para terminar de comprender el ejemplo, lo mejor es compilarlo y ejecutarlo:  Para terminar de comprender el ejemplo, lo mejor es compilarlo y ejecutarlo:
Línea 166: Línea 168:
 ;---------------------------------------------------------------------- ;----------------------------------------------------------------------
 ; Bancos.asm ; Bancos.asm
-; 
 ; Demostracion del uso de bancos / paginación en modo 128K ; Demostracion del uso de bancos / paginación en modo 128K
 ;---------------------------------------------------------------------- ;----------------------------------------------------------------------
 +    ORG 35000
  
-ORG 35000+    call CLS
  
-  LD HL, 0 +    ld hl, 0 
-  ADD HLSP                      ; Guardamos el valor actual de SP +    add hlsp                      ; Guardamos el valor actual de SP 
-  EX DEHL                       ; lo almacenamos en DE+    ex dehl                       ; lo almacenamos en DE
  
-  LD SP24000                    ; Pila fuera de $c000-$ffff+    ld sp26000                    ; Pila fuera de $c000-$ffff
  
-  CALL Wait_For_Keys_Released +    call Wait_For_No_Key 
-  LD HL, $c000                   ; Nuestro puntero+    ld hl, $c000                    ; Nuestro puntero
  
-  ; Ahora paginamos el banco 0 sobre $c000 y guardamos un valor +    ; Ahora paginamos el banco 0 sobre $c000 y guardamos un valor 
-  ; en el primer byte de sus 16K (en la direccion $c000): +    ; en el primer byte de sus 16K (en la direccion $c000): 
-  LD B, 0 +    ld b, 0 
-  CALL SetRAMBank                 ; Banco 0+    call SetRAMBank                 ; Banco 0
  
-  LD A, $AA +    ld a, $AA 
-  LD (HL),                      ; ($c000) = $AA+    ld (hl),                      ; ($c000) = $AA
  
-  ; Ahora paginamos el banco 1 sobre $c000 y guardamos un valor +    ; Ahora paginamos el banco 1 sobre $c000 y guardamos un valor 
-  ; en el primer byte de sus 16K (en la direccion $c000): +    ; en el primer byte de sus 16K (en la direccion $c000): 
-  LD B, 1 +    ld b, 1 
-  CALL SetRAMBank                 ; Banco 1+    call SetRAMBank                 ; Banco 1
  
-  LD A, $01 +    ld a, $01 
-  LD (HL),                      ; ($C000) = $01+    ld (hl),                      ; ($C000) = $01
  
-  ; Esperamos una pulsación de teclas antes de empezar: +    ; Esperamos una pulsación de teclas antes de empezar: 
-  CALL Wait_For_Keys_Pressed +    call Wait_For_Key
-  CALL Wait_For_Keys_Released+
  
-  ; Ahora vamos a cambiar de nuevo al banco 0, leemos el valor que +    ; Ahora vamos a cambiar de nuevo al banco 0, leemos el valor que 
-  ; hay en $c000 y lo representamos en pantalla. Recordemos que +    ; hay en $c000 y lo representamos en pantalla. Recordemos que 
-  ; acabamos de escribir $01 (00000001) antes de cambiar de banco, +    ; acabamos de escribir $01 (00000001) antes de cambiar de banco, 
-  ; y que en su momento pusimos $AA (unos y ceros alternados): +    ; y que en su momento pusimos $AA (unos y ceros alternados): 
-  LD B, 0 +    ld b, 0 
-  CALL SetRAMBank                 ; Banco 0 +    call SetRAMBank                 ; Banco 0 
-  LD A, (HL)                      ; Leemos ($c000) +    ld a, (hl)                      ; Leemos ($c000) 
-  CALL ClearScreen                ; Lo pintamos en pantalla+    call ClearScreen                ; Lo pintamos en pantalla
  
-  ; Esperamos una pulsación de teclas: +    ; Esperamos una pulsación de teclas: 
-  CALL Wait_For_Keys_Pressed +    call Wait_For_Key
-  CALL Wait_For_Keys_Released+
  
-  ; Ahora vamos a cambiar de nuevo al banco 1, leemos el valor que +    ; Ahora vamos a cambiar de nuevo al banco 1, leemos el valor que 
-  ; hay en $c000 y lo representamos en pantalla. Recordemos que +    ; hay en $c000 y lo representamos en pantalla. Recordemos que 
-  ; acabamos de leer $A antes de cambiar de banco, y que en su +    ; acabamos de leer A antes de cambiar de banco, y que en su 
-  ; momento pusimos $01: +    ; momento pusimos $01: 
-  LD B, 1 +    ld b, 1 
-  CALL SetRAMBank                 ; Banco 0 +    call SetRAMBank                 ; Banco 0 
-  LD A, (HL)                      ; Leemos ($c000) +    ld a, (hl)                      ; Leemos ($c000) 
-  CALL ClearScreen                ; Lo pintamos en pantalla+    call ClearScreen                ; Lo pintamos en pantalla
  
-  ; Esperamos una pulsación de teclas: +    ; Esperamos una pulsación de teclas: 
-  CALL Wait_For_Keys_Pressed +    call Wait_For_Key
-  CALL Wait_For_Keys_Released +
- +
-  EX DE, HL                       ; Recuperamos SP para poder volver +
-  LD SP, HL                       ; a BASIC sin errores +
-  RET +
  
 +    ex de, hl                       ; Recuperamos SP para poder volver
 +    ld sp, hl                       ; a BASIC sin errores
 +    ret
  
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
Línea 237: Línea 235:
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
 SetRAMBank: SetRAMBank:
-   LD A,($5b5c)                  ; Valor anterior del puerto +    ld a, ($5b5c)                  ; Valor anterior del puerto 
-   AND $f8                       ; Sólo cambiamos los bits necesarios +    and %11111000                  ; Sólo cambiamos los bits necesarios 
-   OR B                           ; Elegir banco "B" +    or b                           ; Elegir banco "B" 
-   LD BC,$7ffd +    ld bc, $7ffd 
-   DI +    di 
-   LD ($5b5c),A +    ld ($5b5c), a 
-   OUT (C),A +    out (c), a 
-   EI +    ei 
-   RET +    ret
  
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
Línea 253: Línea 250:
 ;----------------------------------------------------------------------- ;-----------------------------------------------------------------------
 ClearScreen: ClearScreen:
-   PUSH HL +    push hl 
-   PUSH DE +    push de 
-   LD HL, 16384 +    ld hl, 16384 
-   LD (HL), A +    ld (hl), a 
-   LD DE, 16385 +    ld de, 16385 
-   LD BC, 6143 +    ld bc, 6143 
-   LDIR +    ldir 
-   POP DE +    pop de 
-   POP HL +    pop hl 
-   RET +    ret
- +
- +
-;----------------------------------------------------------------------- +
-; Rutinas para esperar la pulsación y liberación de todas las teclas: +
-;----------------------------------------------------------------------- +
-Wait_For_Keys_Pressed: +
-   XOR A                        ; A = 0 +
-   IN A, (254) +
-   OR 224 +
-   INC A +
-   JR Z, Wait_For_Keys_Pressed +
-   RET +
- +
-Wait_For_Keys_Released: +
-   XOR A +
-   IN A, (254) +
-   OR 224 +
-   INC A +
-   JR NZ, Wait_For_Keys_Released +
-   RET+
  
-END 35000+    ;--- Libreria utils.asm --- 
 +    INCLUDE "utils.asm"
  
 +    END 35000
 </code> </code>
  
Línea 296: Línea 275:
  Podemos utilizar las funciones de paginación para determinar si estamos ante un modelo de 128KB o uno de 48K.  Podemos utilizar las funciones de paginación para determinar si estamos ante un modelo de 128KB o uno de 48K.
  
- A continuación podemos ver una rutina, que nos permitirá detectar si el modelo de Spectrum que estamos ejecutando es 48K o 128K. Para saber si estamos en un modelo 48K o en un modelo 128K, simplemente tratamos de paginar escribiendo y recuperando valores en la dirección $C000, antes y después de paginar dicho banco, para ver si el valor cambia o no. Según el resultado de esta operación, establecemos a 1 o no la variable de memoria **es_128k** para que podamos usarla después en nuestro programa:+ A continuación podemos ver una rutina, que nos permitirá detectar si el modelo de Spectrum que estamos ejecutando es 48K o 128K. Para saber si estamos en un modelo 48K o en un modelo 128K, simplemente tratamos de paginar escribiendo y recuperando valores en la dirección $c000, antes y después de paginar dicho banco, para ver si el valor cambia o no. Según el resultado de esta operación, establecemos a 1 o no la variable de memoria ''es_128k'' para que podamos usarla después en nuestro programa:
  
 <code z80> <code z80>
-ORG 35000+    ORG 35000
  
-    CALL ROM_CLS        ; Limpiamos la pantalla+    call ROM_CLS        ; Limpiamos la pantalla
  
-    CALL CHECK_128      ; Comprobamos si es modelo 128K+    call CHECK_128      ; Comprobamos si es modelo 128K
  
-    LD B, 0 +    ld b, 0 
-    LD A, (es_128k)     ; Obtenemos el 0 (48K) o 1 (128K). +    ld a, (es_128k)     ; Obtenemos el 0 (48K) o 1 (128K). 
-    LD CA +    ld ca 
-    CALL ROM_STACK_BC +    call ROM_STACK_BC 
-    CALL ROM_PRINT_FP   ; Lo imprimimos por pantalla +    call ROM_PRINT_FP   ; Lo imprimimos por pantalla 
-    RET+    ret
  
 ;----------------------------------------------------- ;-----------------------------------------------------
Línea 319: Línea 298:
 ;----------------------------------------------------- ;-----------------------------------------------------
 CHECK_128: CHECK_128:
-    DI                ; Desactivar interrupciones +    di                ; Desactivar interrupciones 
-    LD BC, $7FFD +    ld bc, $7ffd 
-    LD DE, $1007      ; D=ROM basic,bank 0 ; E=ROM DOS,bank 7 +    ld de, $1007      ; D=ROM basic,bank 0 ; E=ROM DOS,bank 7 
-    LD HL, $C000      ; Direccion de memoria a escribir/leer +    ld hl, $c000      ; Direccion de memoria a escribir/leer 
-    LD A, (HL)        ; ponemos en A el contenido actual, asume bank 0 +    ld a, (hl)        ; ponemos en A el contenido actual, asume bank 0 
-    OUT (C),        ; cambiamos al segundo banco +    out (c),        ; cambiamos al segundo banco 
-    CPL               ; invertimos A +    cpl               ; invertimos A 
-    LD (HL),        ; cargamos A invertido en el segundo banco +    ld (hl),        ; cargamos A invertido en el segundo banco 
-    CPL               ; lo invertimos otra vez para restaurar el valor original +    cpl               ; lo invertimos otra vez para restaurar el valor original 
-    OUT (C),        ; cambiamos al banco original +    out (c),        ; cambiamos al banco original 
-    CP (HL)           ; comparamos A con el contenido de HL+    cp (hl)           ; comparamos A con el contenido de HL
                       ; en un 128k mantendra el valor original => Z                       ; en un 128k mantendra el valor original => Z
                       ; en un 48k el contenido de HL estara invertido => NZ                       ; en un 48k el contenido de HL estara invertido => NZ
-    LD (HL),        ; IMPORTANTE: restauramos el valor original en HL+    ld (hl),        ; IMPORTANTE: restauramos el valor original en HL
  
     ; Si NZ => 48K => salimos sin tocar el "0" de (es_128k)     ; Si NZ => 48K => salimos sin tocar el "0" de (es_128k)
-    JR NZ, fin_check_128+    jr nz, fin_check_128
  
     ; tiene paginacion de memoria es un 128k, +2, +2AB/E o +3/E     ; tiene paginacion de memoria es un 128k, +2, +2AB/E o +3/E
-    LD A, 1 +    ld a, 1 
-    LD (es_128k),   ; activamos la variable es_128k+    ld (es_128k),   ; activamos la variable es_128k
  
 fin_check_128: fin_check_128:
-    EI                ; Reactivar interrupciones +    ei                ; Reactivar interrupciones 
-    RET+    ret
  
 es_128k     DEFB 0 es_128k     DEFB 0
 ;------------------------------------------------ ;------------------------------------------------
  
-ROM_CLS       EQU  $0DAF +ROM_CLS       EQU  $0daf 
-ROM_STACK_BC  EQU  $2D2B +ROM_STACK_BC  EQU  $2d2b 
-ROM_PRINT_FP  EQU  $2DE3+ROM_PRINT_FP  EQU  $2de3
  
-END 35000+    END 35000
 </code> </code>
  
  Si ejecutamos este programa, veremos aparecer en pantalla un "1" si lo hacemos en un modelo 128K, y un "0" si es un modelo sin paginación.  Si ejecutamos este programa, veremos aparecer en pantalla un "1" si lo hacemos en un modelo 128K, y un "0" si es un modelo sin paginación.
  
- Esta rutina, ejecutada al principio de nuestro programa, establecerá pues la variable **(es_128k)** a 0 ó 1 de forma que nos permitirá saber durante el resto de la ejecución de nuestro programa si tenemos disponible la paginación (con su memoria adicional) o no. Es importante destacar que no preserva los valores de los registros con PUSH/POP por lo que se recomienda ejecutarla nada más lanzar nuestro programa, y que deja en el primer byte del banco 7 el valor inverso (CPL) del primer byte del banco 0, por lo que tenemos que tenerlo en cuenta si vamos a precargar datos en ese banco en el cargador BASIC del programa (para alterar el código de forma que preserve dicho valor).+ Esta rutina, ejecutada al principio de nuestro programa, establecerá pues la variable ''(es_128k)'' a 0 ó 1 de forma que nos permitirá saber durante el resto de la ejecución de nuestro programa si tenemos disponible la paginación (con su memoria adicional) o no. Es importante destacar que no preserva los valores de los registros con PUSH/POP por lo que se recomienda ejecutarla nada más lanzar nuestro programa, y que deja en el primer byte del banco 7 el valor inverso (cpl) del primer byte del banco 0, por lo que tenemos que tenerlo en cuenta si vamos a precargar datos en ese banco en el cargador BASIC del programa (para alterar el código de forma que preserve dicho valor).
  
  
Línea 385: Línea 364:
  El problema es que mientras el haz de electrones del monitor avanza redibujando la imagen, el Spectrum no puede interrumpirlo (de hecho, no puede controlarlo, sólo se sincroniza con él) y tiene que servirle todos los datos necesarios para el retrazado de la imagen conforme los vaya necesitando.  El problema es que mientras el haz de electrones del monitor avanza redibujando la imagen, el Spectrum no puede interrumpirlo (de hecho, no puede controlarlo, sólo se sincroniza con él) y tiene que servirle todos los datos necesarios para el retrazado de la imagen conforme los vaya necesitando.
  
- Esto implica que cuando la ULA está "redibujando" la pantalla (y recordemos que lo hace 50 veces por segundo) y por tanto leyendo de las sucesivas posiciones de memoria comenzando en $4000, el procesador no puede acceder a las celdillas exactas de memoria a las que accede la ULA hasta que ésta deja de hacer uso de ellas. En otras palabras, a la hora de leer celdillas de memoria entre $4000 y $7FFF, la ULA tiene prioridad sobre el procesador.+ Esto implica que cuando la ULA está "redibujando" la pantalla (y recordemos que lo hace 50 veces por segundo) y por tanto leyendo de las sucesivas posiciones de memoria comenzando en $4000, el procesador no puede acceder a las celdillas exactas de memoria a las que accede la ULA hasta que ésta deja de hacer uso de ellas. En otras palabras, a la hora de leer celdillas de memoria entre $4000 y $7fff, la ULA tiene prioridad sobre el procesador.
  
- Por eso, los programas que corren en el bloque de 16KB entre $4000 (16384) y $7FFF (32767), o pretenden acceder a la misma justo cuando la ULA quiere leer algún dato gráfico de la VRAM, pueden resultar más lentos en ejecución cuando la ULA está leyendo la pantalla.+ Por eso, los programas que corren en el bloque de 16KB entre $4000 (16384) y $7fff (32767), o pretenden acceder a la misma justo cuando la ULA quiere leer algún dato gráfico de la VRAM, pueden resultar más lentos en ejecución cuando la ULA está leyendo la pantalla.
  
  Como se detalla en la FAQ de comp.sys.sinclair alojada en World Of Spectrum, este efecto sólo se da cuando se está dibujando la pantalla propiamente dicha, ya que para el trazado del borde la ULA proporciona al haz de electrones el color a dibujar y no se accede a memoria, por lo que no se produce este retraso o delay.  Como se detalla en la FAQ de comp.sys.sinclair alojada en World Of Spectrum, este efecto sólo se da cuando se está dibujando la pantalla propiamente dicha, ya que para el trazado del borde la ULA proporciona al haz de electrones el color a dibujar y no se accede a memoria, por lo que no se produce este retraso o delay.
  
- Controlar exactamente los retrasos que se producen y cómo afectarán a la ejecución de nuestros programas es un ejercicio bastante complejo que requiere conocimiento de los tiempos de ejecución de las diferentes instrucciones, número de ciclo desde que comenzó el retrazado de la pantalla, etc. Por ejemplo, una misma instrucción, NOP, que requiere 4 ciclos de reloj para ejecutarse en condiciones normales, puede ver aumentado su tiempo de ejecución a 10 ciclos (4 de ejecución y 6 de delay) si el contador de programa (PC) está dentro de una zona de "memoria en contienda", y dicho delay afectaría sólo al primer ciclo (lectura de la instrucción por parte del procesador). Por contra, si en lugar de una instrucción NOP tenemos una instrucción LD que acceda a memoria (también contended), el delay puede ser mayor.+ Controlar exactamente los retrasos que se producen y cómo afectarán a la ejecución de nuestros programas es un ejercicio bastante complejo que requiere conocimiento de los tiempos de ejecución de las diferentes instrucciones, número de ciclo desde que comenzó el retrazado de la pantalla, etc. Por ejemplo, una misma instrucción, ''NOP'', que requiere 4 ciclos de reloj para ejecutarse en condiciones normales, puede ver aumentado su tiempo de ejecución a 10 ciclos (4 de ejecución y 6 de delay) si el contador de programa (''PC'') está dentro de una zona de "memoria en contienda", y dicho delay afectaría sólo al primer ciclo (lectura de la instrucción por parte del procesador). Por contra, si en lugar de una instrucción ''NOP'' tenemos una instrucción LD que acceda a memoria (también contended), el delay puede ser mayor.
  
  Como podéis imaginar, este es uno de los mayores quebraderos de cabeza para los programadores de emuladores, y es la principal causa (junto con la **Contended I/O**, su equivalente en cuanto a acceso de puertos, también producido por la ULA), de que en los primeros tiempos de la emulación, no todos los juegos fueran correctamente emulados con respecto a su funcionamiento en un Spectrum. También muchas demos con complejas sincronizaciones y timings dejaban de funcionar en emuladores de Spectrum que no implementaban estos "retrasos" y que, en su emulación "perfecta del micro", ejecutaban siempre todas las instrucciones a su velocidad "teórica".  Como podéis imaginar, este es uno de los mayores quebraderos de cabeza para los programadores de emuladores, y es la principal causa (junto con la **Contended I/O**, su equivalente en cuanto a acceso de puertos, también producido por la ULA), de que en los primeros tiempos de la emulación, no todos los juegos fueran correctamente emulados con respecto a su funcionamiento en un Spectrum. También muchas demos con complejas sincronizaciones y timings dejaban de funcionar en emuladores de Spectrum que no implementaban estos "retrasos" y que, en su emulación "perfecta del micro", ejecutaban siempre todas las instrucciones a su velocidad "teórica".
-  + 
- En nuestro caso, como programadores, la mejor manera de evitar problemas en la ejecución de nuestros programas es la tratar de no situar código, siempre que sea posible, entre $4000 y $7FFF, situando nuestro código por ejemplo a partir de la dirección 32768 ($8000). Si nuestro programa tiene necesidades extra de memoria, podemos colocar el inicio de nuestro programa a partir de 26000, por ejemplo, ganaremos 6768 bytes de memoria adicionales.+ En nuestro caso, como programadores, la mejor manera de evitar problemas en la ejecución de nuestros programas es la tratar de no situar código, siempre que sea posible, entre $4000 y $7fff, situando nuestro código por ejemplo a partir de la dirección 32768 ($8000). Si nuestro programa tiene necesidades extra de memoria, podemos colocar el inicio de nuestro programa a partir de 26000, por ejemplo, ganaremos 6768 bytes de memoria adicionales.
  
 Para que los retrasos de la ULA no afecten a nuestro programa, podemos poner al principio del mismo (en esos 6768 bytes iniciales) todas las rutinas que utilicemos al principio del mismo o que su ejecución no sea vital en cuanto a tiempos: funciones para el menú del juego, detección de 128K/48K, rutinas de redefinición del teclado, textos de los menúes y del fin del juego, etc. Para que los retrasos de la ULA no afecten a nuestro programa, podemos poner al principio del mismo (en esos 6768 bytes iniciales) todas las rutinas que utilicemos al principio del mismo o que su ejecución no sea vital en cuanto a tiempos: funciones para el menú del juego, detección de 128K/48K, rutinas de redefinición del teclado, textos de los menúes y del fin del juego, etc.
Línea 404: Línea 383:
 ===== Contended Memory + Paginación  ===== ===== Contended Memory + Paginación  =====
  
- ¿Cómo afecta la contended-memory al sistema de paginación de los modelos 128K? Al igual que en el 48K existe una "página" ($4000-$7FFF) a la que la ULA accede y por tanto afectada por sus lecturas, en el caso de los 128K existen bancos de memoria completos (páginas) de memoria en contienda. Como ya hemos visto, estos bancos son:+ ¿Cómo afecta la contended-memory al sistema de paginación de los modelos 128K? Al igual que en el 48K existe una "página" ($4000-$7fff) a la que la ULA accede y por tanto afectada por sus lecturas, en el caso de los 128K existen bancos de memoria completos (páginas) de memoria en contienda. Como ya hemos visto, estos bancos son:
  
    * Modelos +2/128K : Bancos 1, 3, 5 y 7.    * Modelos +2/128K : Bancos 1, 3, 5 y 7.
Línea 412: Línea 391:
  
 <code> <code>
-The RAM banks are of two types: RAM pages 4 to 7 which are contended +The RAM banks are of two types: RAM pages 4 to 7 which are contended
 (meaning that they share time with the video circuitry), and RAM pages (meaning that they share time with the video circuitry), and RAM pages
 0 to 3 which are uncontended (where the processor has exclusive use). 0 to 3 which are uncontended (where the processor has exclusive use).
Línea 421: Línea 400:
 For example, executing NOPs in contended RAM will give an effective For example, executing NOPs in contended RAM will give an effective
 clock frequency of 2.66Mhz as opposed to the normal 3.55MHz in clock frequency of 2.66Mhz as opposed to the normal 3.55MHz in
-uncontended RAM. +uncontended RAM.
  
 This is a reduction in speed of about 25%. This is a reduction in speed of about 25%.
Línea 433: Línea 412:
  
 \\  \\ 
-===== Paginación de memoria desde Z88DK (C)  ===== 
  
- Podemos paginar memoria también desde C usando Z88DK mediante un código como el siguiente:+===== En resumen =====
  
-<code c> + Comprendiendo el sistema de paginación de los modelos de 128K aprendiendo a utilizarlo conseguimos una gran cantidad de memoria adicionalEsta memoria adicional viene con una desventaja, y es que sólo podemos accederla a través de una ventana de 16KB visible en el bloque de memoria $c000 a $ffffPaginandoes decir"cambiando otra página"dejamos de "ver" el bloque de 16KB actual en ese rango de memoria y desde ese momento veremos el contenido de otro de los bloques.
-//--- SetRAMBank ------------------------------------------------------ +
-// +
-// Se mapea el banco (0-7) indicado sobre $C000. +
-// +
-// Ojo: aqui no se deshabilitan las interrupciones ademas en lugar +
-// de usar el registro B, se usa un parametro tomado desde la pila. +
-// En caso de ser importante la velocidadse puede usar "B" no pasar +
-// el parametro en la pila, llamando SetRAMBank con un CALL. +
-// +
-void SetRAMBank(char banco) +
-+
-  #asm +
-   .SetRAMBank +
-     ld hl+
-     add hlsp +
-     ld a, (hl)+
  
-     ld b, a + Esto es así porque el Z80 tiene un bus de direcciones de 16 bitspor lo que no es capaz de direccionar más de 65535 bytes. Por esopara acceder a más de 65535 bytes necesitamos "montar" los bloques extra de 16KB en algún lugar de la memoria a la que sí podemos accederen este caso en el bloque final de 16KB del Spectrum.
-     ld  A($5B5C) +
-     and F8h +
-     or +
-     ld  BC$7FFD +
-     ld  ($5B5C), A +
-     out (C)+
-   #endasm +
-+
-</code>+
  
-Con el anterior código podemos mapear uno de los bancos de memoria de 16KB sobre la página que va desde $C000 a $FFFF, pero debido al uso de memoriavariables y estructuras internas que hace Z88DK, debemos seguir una serie de consideraciones.+ Es nuestra responsabilidad paginar el bloque adecuado en cada momento para tener acceso a los datos que necesitamos, por lo que hay que tener cuidado con que la pila, la tabla de vectores de interrupción o la ISR que atiende las interrupciones no desaparezcan al paginarubicándolas por debajo de $c000.
  
-  * Todo el código en ejecución debe estar por debajo de $C000, para lo cual es recomendable definir los gráficos al final del "binario"+ Una vez tenidas en cuenta estas consideracionespodemos alternar el bloque de 16KB entre diferentes bloques y superar la barrera de los 64KB (48KB de RAM y 16KB de ROMaccediendo a memoria adicional.
-  * Es importantísimo colocar la pila en la memoria bajamediante la siguiente instrucción (o similar, según la dirección en que queremos colocarlaal principio de nuestro programa:+
  
-\\  + Así, por ejemplo, podemos almacenar los datos de diferentes niveles en diferentes bloques, y cambiar de uno a otro mediante paginación en el momento adecuado. Esto permite realizar cargas de datos desde cinta almacenando la totalidad de los datos del juego o programa en bancos libres de memoria y convertir nuestro juego multicarga (con una carga por fase) en un juego de carga única (con todos los elementos del juego almacenados en memoria), evitando el tedioso sistema de rebobinar y volver a cargar la primera fase cuando el jugador muere.
-<code> +
-/* Allocate space for the stack */ +
-#pragma output STACKPTR=24500 +
-</code> +
-\\ +
  
- La regla general es asegurarse de que no haya nada importante (para la ejecución de nuestro programa) en el bloque $C000 a $FFFF cuando se haga el cambio: ni la pila, ni código al que debamos acceder. Tan sólo datos que puedan ser intercambiandos de un banco a otro sin riesgo para la ejecución del mismo (por ejemplo, los datos de un nivel de juego en el que ya no estamos). + En modo 128K bastará con que nuestro programa, una vez cargado en memoria y en ejecución, pagine un determinado bloque, cargue 16K-datos sobre él, pagine otro bloque diferente, y realice otra carga de datos desde cinta, y así sucesivamente con todos los bloques de datos del juego. Estas cargas de datos podemos hacerlas bien desde nuestro programa "principal" una vez cargado y en memoria, o bien desde un mini-programa lanzado por el cargador BASIC y previo a cargar el programa definitivo.
- +
- +
-\\  +
-===== En resumen ===== +
- +
- Comprendiendo el sistema de paginación de los modelos de 128K y aprendiendo a utilizarlo conseguimos una gran cantidad de memoria adicional que ir paginando sobre el bloque $C000-$FFFF. +
- +
- Así, podemos almacenar los datos de diferentes niveles en diferentes bloques, y cambiar de uno a otro mediante paginación en el momento adecuado. Esto permite realizar cargas de datos desde cinta almacenando la totalidad de los datos del juego o programa en bancos libres de memoria y convertir nuestro juego multicarga (con una carga por fase) en un juego de carga única (con todos los elementos del juego almacenados en memoria), evitando el tedioso sistema de rebobinar y volver a cargar la primera fase cuando el jugador muere.  +
- +
- Ahora bastará con que nuestro programa, una vez cargado en memoria y en ejecución, pagine un determinado bloque, cargue 16K-datos sobre él, pagine otro bloque diferente, y realice otra carga de datos desde cinta, y así sucesivamente con todos los bloques de datos del juego. Estas cargas de datos podemos hacerlas bien desde nuestro programa "principal" una vez cargado y en memoria, o bien desde un mini-programa lanzado por el cargador BASIC y previo a cargar el programa definitivo. +
  
  El resultado: 128KB de memoria a nuestro alcance, tanto para cargar múltiples datos gráficos o de mapeado sobre ellos como para llenarlos internamente desde nuestro programa.  El resultado: 128KB de memoria a nuestro alcance, tanto para cargar múltiples datos gráficos o de mapeado sobre ellos como para llenarlos internamente desde nuestro programa.
Línea 510: Línea 447:
   * [[http://www.worldofspectrum.org/faq/reference/z80reference.htm|Z80 Reference de WOS]]   * [[http://www.worldofspectrum.org/faq/reference/z80reference.htm|Z80 Reference de WOS]]
  
 +\\ 
 +**[ [[.:indice|⬉]] | [[.:interrupciones|⬅]] | [[.:gfx1_vram|➡]] ]**
  • cursos/ensamblador/paginacion_128k.1704392694.txt.gz
  • Última modificación: 04-01-2024 18:24
  • por sromero