cursos:ensamblador:avanzadas1

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:avanzadas1 [08-01-2024 09:14] sromerocursos:ensamblador:avanzadas1 [21-01-2024 10:31] (actual) – [Obtener el valor de PC (Contador de Programa)] sromero
Línea 14: Línea 14:
 En todos los programas que hemos visto, hemos recomendado ubicar nuestros programas en lugares como **35000**, es decir, utilizar el área desde 35000 hasta 65535 (si no usamos los UDGs o Gráficos Definidos por el Usuario) o hasta 65338 si los usamos (porque los últimos 168 bytes se suelen reservar para los UDGs en BASIC). En todos los programas que hemos visto, hemos recomendado ubicar nuestros programas en lugares como **35000**, es decir, utilizar el área desde 35000 hasta 65535 (si no usamos los UDGs o Gráficos Definidos por el Usuario) o hasta 65338 si los usamos (porque los últimos 168 bytes se suelen reservar para los UDGs en BASIC).
  
-Eso nos proporciona (con un ORG 35000) hasta unos 30KB de RAM en un Spectrum de 48KB para nuestro programa incluyendo código, textos, gráficos y sonidos. Podremos alojar datos y código, y leer y escribir en la memoria, en todo el espacio entre 35000 y 65535, es decir, en 30535 bytes (29.8KB, es decir, casi 30KB).+Eso nos proporciona (con un ''ORG 35000'') hasta unos 30KB de RAM en un Spectrum de 48KB para nuestro programa incluyendo código, textos, gráficos y sonidos. Podremos alojar datos y código, y leer y escribir en la memoria, en todo el espacio entre 35000 y 65535, es decir, en 30535 bytes (29.8KB, es decir, casi 30KB).
  
-Si bajamos ORG hasta 30000, ganamos unos 5KB extras para nuestro programa. Bajándolo hasta 28000, ganamos 7KB. Y en 26000 tenemos aproximadante 9 valiosísimos Kilobytes adicionales.+Si bajamos ''ORG'' hasta 30000, ganamos unos 5KB extras para nuestro programa. Bajándolo hasta 28000, ganamos 7KB. Y en 26000 tenemos aproximadante 9 valiosísimos Kilobytes adicionales.
  
-**__Sin embargo, nuestra recomendación es utilizar **ORG 33500** como valor mínimo para cargar nuestro programa.__**+**__Sin embargo, nuestra recomendación es utilizar ''ORG 33500'' como valor mínimo para cargar nuestro programa.__**
  
 Reducir la dirección de origen por debajo de 32768 tiene dos problemas que vamos a ver a continuación. Reducir la dirección de origen por debajo de 32768 tiene dos problemas que vamos a ver a continuación.
Línea 26: Línea 26:
 **1.- BASIC y la pila del Z80 están por debajo de ORG** **1.- BASIC y la pila del Z80 están por debajo de ORG**
  
-Por un lado, por debajo de nuestro programa están las variables del sistema, está BASIC, está el programa BASIC que estamos ejecutando y después del programa BASIC (decreciendo desde RAMTOP) tenemos la pila (stack) del Z80.+Por un lado, por debajo de nuestro programa están las variables del sistema, está BASIC, está el programa BASIC que estamos ejecutando y después del programa BASIC (decreciendo desde ''RAMTOP'') tenemos la pila (stack) del Z80.
  
-Es decir, que cuando usamos un **ORG dirección**, debajo de esa dirección debe caber tanto la pila como el programa cargador que vamos a usar para lanzar nuestro código máquina.+Es decir, que cuando usamos un ''ORG dirección'', debajo de esa dirección debe caber tanto la pila como el programa cargador que vamos a usar para lanzar nuestro código máquina.
  
 Hagamos un ejercicio de cálculo y supongamos que no queremos volver a BASIC al acabar de ejecutarse nuestro programa (algo que era normal en juegos y programas, donde simplemente reseteábamos el ordenador)... ¿hasta qué valor podríamos bajar el ORG y cargar los datos en él, sin afectar al cargador BASIC? Hagamos un ejercicio de cálculo y supongamos que no queremos volver a BASIC al acabar de ejecutarse nuestro programa (algo que era normal en juegos y programas, donde simplemente reseteábamos el ordenador)... ¿hasta qué valor podríamos bajar el ORG y cargar los datos en él, sin afectar al cargador BASIC?
  
-Como dirección mínima para un ORG, para que haya espacio para un cargador BASIC mínimo (un **CLEAR**, un **LOAD "" CODE** y un **RANDOMIZE USR dirección_de_inicio**), el valor mínimo que recomendamos usar es $6000 (24576). Estamos hablando con este valor de dejar muy poco espacio para el programa en BASIC, es decir, dejar hueco para un cargador mínimo, simple y básico. Y además tiene que caber la pila del Z80. No es un valor recomendado a menos que tengamos necesidades extremas de memoria y que además vayamos a reubicar la pila a otro lugar.+Como dirección mínima para un ORG, para que haya espacio para un cargador BASIC mínimo (un ''CLEAR X'', un ''LOAD "" CODE'' y un ''RANDOMIZE USR dirección_de_inicio''), el valor mínimo que recomendamos usar es $6000 (24576). Estamos hablando con este valor de dejar muy poco espacio para el programa en BASIC, es decir, dejar hueco para un cargador mínimo, simple y básico. Y además tiene que caber la pila del Z80. No es un valor recomendado a menos que tengamos necesidades extremas de memoria y que además vayamos a reubicar la pila a otro lugar.
  
 En el Spectrum un programa Basic comienza normalmente a partir de 23755. Por poco código BASIC que introduzcamos en el cargador, prácticamente llegaremos hasta la dirección 24000. Por eso muchos cargadores Basic tenían una línea 0 en la que mediante un REM ya metían el cargador en código máquina (que además permitía cargar sin las cabeceras, ahorrando ese espacio). En el Spectrum un programa Basic comienza normalmente a partir de 23755. Por poco código BASIC que introduzcamos en el cargador, prácticamente llegaremos hasta la dirección 24000. Por eso muchos cargadores Basic tenían una línea 0 en la que mediante un REM ya metían el cargador en código máquina (que además permitía cargar sin las cabeceras, ahorrando ese espacio).
Línea 52: Línea 52:
 El código que está en la contended memory tiene que ser leído por el procesador en el ciclo de "lectura-decodificación-ejecución", y si la ULA está leyendo la pantalla cuando el procesador va a leer un opcode de nuestro programa en ese bloque de 16Kb, sufrirá un pequeño retraso en la lectura hasta que la ULA termine de leer el dato que está buscando en la videomemoria. Lo mismo ocurrirá al escribir y leer en celdillas de memoria de este bloque. El código que está en la contended memory tiene que ser leído por el procesador en el ciclo de "lectura-decodificación-ejecución", y si la ULA está leyendo la pantalla cuando el procesador va a leer un opcode de nuestro programa en ese bloque de 16Kb, sufrirá un pequeño retraso en la lectura hasta que la ULA termine de leer el dato que está buscando en la videomemoria. Lo mismo ocurrirá al escribir y leer en celdillas de memoria de este bloque.
  
-Entonces ¿por qué no ubicar nuestro programa en **ORG 32768**? ¿Por qué se ha recomendado un valor mínimo de **ORG 33500**?+Entonces ¿por qué no ubicar nuestro programa en ''ORG 32768''? ¿Por qué se ha recomendado un valor mínimo de ''ORG 33500''?
  
-Con "**ORG 32768**" hay un problema, y es la pila (stack) del Z80. La pila crece habia abajo (decrece), y si hacemos un ORG 32768, **BASIC al hacer el CLEAR situará la pila POR DEBAJO de 32768**, con lo cual **la pila estará en zona de contended memory**.+Con ''ORG 32768'' hay un problema, y es la pila (stack) del Z80. La pila crece habia abajo (decrece), y con ''ORG 32768'', **BASIC al hacer el CLEAR situará la pila Por deBAJO de 32768**, con lo cual **la pila estará en zona de contended memory**.
  
-Al igual que pasaba con el código de nuestro programa, en este caso cualquier operación PUSH, POP, CALL (que tiene que hacer PUSH de la dirección de retorno a la pila) y RET (que tiene que leerla para volver) podrá ser interrumpida y retrasada por la ULA. Y CALL, RET, PUSH y POP son operaciones que usaremos mucho en nuestros programas.+Al igual que pasaba con el código de nuestro programa, en este caso cualquier operación ''PUSH''''POP''''call'' (que tiene que hacer ''PUSH'' de la dirección de retorno a la pila) y ''RET'' (que tiene que leerla para volver) podrá ser interrumpida y retrasada por la ULA. Y 'estas son operaciones que usaremos mucho en nuestros programas.
  
 Para que la pila quede fuera de la contended memory, dado que BASIC la situa por debajo de ORG, hay que subir un poco ORG, para darle espacio dentro de estos primeros bytes del bloque 32K-48K. Si establecemos ORG en diferentes valores, y con el siguiente programa miramos el valor de SP al inicio de nuestro programa después de cargarlo, veremos lo siguiente: Para que la pila quede fuera de la contended memory, dado que BASIC la situa por debajo de ORG, hay que subir un poco ORG, para darle espacio dentro de estos primeros bytes del bloque 32K-48K. Si establecemos ORG en diferentes valores, y con el siguiente programa miramos el valor de SP al inicio de nuestro programa después de cargarlo, veremos lo siguiente:
Línea 62: Línea 62:
 <code z80> <code z80>
 ; Programa utilizado para ver el valor de SP con diferentes ORG ; Programa utilizado para ver el valor de SP con diferentes ORG
-ORG 33500+    ORG 33500
  
-    CALL ROM_CLS+    call ROM_CLS
  
-    LD HL, 0 +    ld hl, 0 
-    ADD HLSP +    add hlsp 
-    LD BH +    ld bh 
-    LD CL +    ld cl 
-    CALL ROM_STACK_BC +    call ROM_STACK_BC 
-    CALL ROM_PRINT_FP+    call ROM_PRINT_FP
  
-    RET+    ret
  
-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 33500+    END 33500
 </code> </code>
  
Línea 93: Línea 93:
 \\  \\ 
  
-"ORG 35000es el valor que hemos usado en muchos ejemplos en este curso: nos deja 30KB de memoria libre para código, y tenemos 2KB por debajo para la pila, lo cual es probablemente incluso demasiado.+''ORG 35000'' es el valor que hemos usado en muchos ejemplos en este curso: nos deja 30KB de memoria libre para código, y tenemos 2KB por debajo para la pila, lo cual es probablemente incluso demasiado.
  
-"ORG 34000es una opción mucho más óptima seguramente, ya que una pila de 1KB es más que suficiente (probablemente siga siendo demasiado) y nos proporciona 1KB más de memoria para nuestro programa (~31KB).+''ORG 34000'' es una opción mucho más óptima seguramente, ya que una pila de 1KB es más que suficiente (probablemente siga siendo demasiado) y nos proporciona 1KB más de memoria para nuestro programa (~31KB).
  
-Con "ORG 33500tenemos una pila de 708 bytes, que probablemente es más que suficiente para cualquier programa que podamos realizar, a menos que programemos algoritmos recursivos profundos, que hagan mucho uso de la pila. Ganamos otro medio KB para nuestro programa con este valor.+Con ''ORG 33500'' tenemos una pila de 708 bytes, que probablemente es más que suficiente para cualquier programa que podamos realizar, a menos que programemos algoritmos recursivos profundos, que hagan mucho uso de la pila. Ganamos otro medio KB para nuestro programa con este valor.
  
 Cabe destacar que **no estamos hablando de que sólo tengamos 708 bytes de pila, y que si hacemos PUSHes adicionales se vaya a colgar el programa**. No, simplemente si hacemos muchas apilaciones de datos, sin sacarlos de la pila, los nuevos valores después de algo más de 350 PUSH'es caerían dentro de la contended memory, algo que tampoco es tan grave, y que probablemente ni siquiera tuvieron en cuenta muchos juegos comerciales de la época del Spectrum que usaban alegremente esta zona de memoria. Cabe destacar que **no estamos hablando de que sólo tengamos 708 bytes de pila, y que si hacemos PUSHes adicionales se vaya a colgar el programa**. No, simplemente si hacemos muchas apilaciones de datos, sin sacarlos de la pila, los nuevos valores después de algo más de 350 PUSH'es caerían dentro de la contended memory, algo que tampoco es tan grave, y que probablemente ni siquiera tuvieron en cuenta muchos juegos comerciales de la época del Spectrum que usaban alegremente esta zona de memoria.
  
-Y recordemos que la pila crece hacia abajo con PUSH y CALL pero decrece y tiende a volver a las direcciones más altas con cada POP y RET, por lo que lo normal es que se mueva en la parte superior donde la hemos situado, salvo que usemos funciones con recursividad, las cuales realizan muchos CALLs anidados y no realizan RETs hasta que sus llamadas a ellas mismas terminan (pero que, si están bien programadas, acaban vaciando la pila igualmente).+Y recordemos que la pila crece hacia abajo con ''PUSH'' ''call'' pero decrece y tiende a volver a las direcciones más altas con cada ''POP'' ''RET'', por lo que lo normal es que se mueva en la parte superior donde la hemos situado, salvo que usemos funciones con recursividad, las cuales realizan muchos calls anidados y no realizan RETs hasta que sus llamadas a ellas mismas terminan (pero que, si están bien programadas, acaban vaciando la pila igualmente).
  
 Así pues, nuestra recomendación de valor de inicio **ORG** mínimo para cualquier programa sería de __**cualquiera de esos 4 valores**__, ya sea **35000** si nos gusta ver un número redondo en el ORG, **34000** si queremos arañar un KB extra, o **33500** si estamos seguros de que tenemos pila suficiente con 708 bytes fuera de la contended memory. Así pues, nuestra recomendación de valor de inicio **ORG** mínimo para cualquier programa sería de __**cualquiera de esos 4 valores**__, ya sea **35000** si nos gusta ver un número redondo en el ORG, **34000** si queremos arañar un KB extra, o **33500** si estamos seguros de que tenemos pila suficiente con 708 bytes fuera de la contended memory.
  
 Va a ser complicado que lleguemos a ocupar los 30-31KB de memoria que tenemos por delante, salvo que estemos realizando un juego de calidad comercial o con muchos datos. En ese caso, en este mismo capítulo veremos ideas para, entre otras cosas, darle un uso a esos 7KB de RAM "desperdiciados" que tenemos entre 25000 y 32768, pero que están en contended memory. Va a ser complicado que lleguemos a ocupar los 30-31KB de memoria que tenemos por delante, salvo que estemos realizando un juego de calidad comercial o con muchos datos. En ese caso, en este mismo capítulo veremos ideas para, entre otras cosas, darle un uso a esos 7KB de RAM "desperdiciados" que tenemos entre 25000 y 32768, pero que están en contended memory.
- 
- 
  
 \\  \\ 
Línea 137: Línea 135:
 \\  \\ 
  
-Para poner uno de los bloques disponibles en la "ventana" entre 49152 ($C000) y 65535 ($FFFF), tendremos que perder de vista el bloque que tenemos "mapeado" actualmente.+Para poner uno de los bloques disponibles en la "ventana" entre 49152 ($c000) y 65535 ($ffff), tendremos que perder de vista el bloque que tenemos "mapeado" actualmente.
  
 Eso quiere decir que tenemos que organizar y diseñar nuestro código alrededor de esto ya que: Eso quiere decir que tenemos que organizar y diseñar nuestro código alrededor de esto ya que:
  
-  * El código de nuestro programa deberá estar en la ventana que no se mapea (entre 32768 -$8000- y 49152 -$BFFF-). Si vamos a cambiar de banco, la ejecución del programa no puede estar en el banco que vamos a "perder de vista", de modo que el código ejecutable del programa tiene que estar en la memoria entre 25000 y 49152.+  * El código de nuestro programa deberá estar en la ventana que no se mapea (entre 32768 -$8000- y 49152 -$bfff-). Si vamos a cambiar de banco, la ejecución del programa no puede estar en el banco que vamos a "perder de vista", de modo que el código ejecutable del programa tiene que estar en la memoria entre 25000 y 49152.
  
-  * La pila tampoco puede estar situada en el bloque que paginamos, porque la perderíamos y el próximo POP/RET tendría resultados desastrosos. Por suerte, en nuestro caso estamos ubicándola por debajo del ORG así que no debemos de tener problemas.+  * La pila tampoco puede estar situada en el bloque que paginamos, porque la perderíamos y el próximo ''POP''/''RET'' tendría resultados desastrosos. Por suerte, en nuestro caso estamos ubicándola por debajo del ORG así que no debemos de tener problemas.
  
   * Las rutinas de ISR y las rutinas que reproduzcan sonidos deberían también estar en la parte de la memoria no paginable, a menos que precisamente queramos aprovechar uno de los bancos de memoria del Spectrum para alojar la música y el reproductor. En ese caso en la rutina que atiende a la interrupción lo que haríamos sería cambiar de banco, llamar al reproductor para que "alimente" al chip AY, y volver a dejar el banco original antes de salir de la ISR.   * Las rutinas de ISR y las rutinas que reproduzcan sonidos deberían también estar en la parte de la memoria no paginable, a menos que precisamente queramos aprovechar uno de los bancos de memoria del Spectrum para alojar la música y el reproductor. En ese caso en la rutina que atiende a la interrupción lo que haríamos sería cambiar de banco, llamar al reproductor para que "alimente" al chip AY, y volver a dejar el banco original antes de salir de la ISR.
Línea 149: Línea 147:
   * Si no tenemos problemas de memoria y queremos hacer una versión 48K del mismo juego 128K, podemos hacer que la única diferencia entre ambos sea la carga de la música y el reproductor en un banco 128K. El "cargador" del juego (o el mismo juego, al principio) cargará primero la versión 48K. En ese momento deberá detectar si es un modelo 48K o 128K. Si es 128K, cambiará a otro banco, y cargará la música y el reproductor en él desde cinta (último bloque de la cinta) con la rutina de carga de la ROM. Usando la variable que hemos guardado sobre si estamos en un modelo 128K, en la ISR reproduciremos o no la música (cambiando al banco de la música y el player y deshaciendo el cambio). En este punto, la única diferencia entre las 2 versiones será que cuando juguemos en un 128K, la carga durará un poco más (se carga el bloque adicional con las músicas) y tendremos música AY. En un apartado posterior explicaremos de forma más detallada este proceso.   * Si no tenemos problemas de memoria y queremos hacer una versión 48K del mismo juego 128K, podemos hacer que la única diferencia entre ambos sea la carga de la música y el reproductor en un banco 128K. El "cargador" del juego (o el mismo juego, al principio) cargará primero la versión 48K. En ese momento deberá detectar si es un modelo 48K o 128K. Si es 128K, cambiará a otro banco, y cargará la música y el reproductor en él desde cinta (último bloque de la cinta) con la rutina de carga de la ROM. Usando la variable que hemos guardado sobre si estamos en un modelo 128K, en la ISR reproduciremos o no la música (cambiando al banco de la música y el player y deshaciendo el cambio). En este punto, la única diferencia entre las 2 versiones será que cuando juguemos en un 128K, la carga durará un poco más (se carga el bloque adicional con las músicas) y tendremos música AY. En un apartado posterior explicaremos de forma más detallada este proceso.
  
-  * Durante la carga del juego podríamos ir cambiando de banco para cargar diferentes bloques de datos desde cinta en los diferentes bancos. Esto lo podemos hacer por ejemplo cargando primero en RAM una pequeña rutina de cambio de bancos llamable desde BASIC (con USR) para ir cambiando de banco y hacer los correspondientes "**LOAD "" CODE 49152**" para cada banco a rellenar. También podríamos cargar un programa cargador propio que use la rutina de carga de la ROM para hacerlo.+  * Durante la carga del juego podríamos ir cambiando de banco para cargar diferentes bloques de datos desde cinta en los diferentes bancos. Esto lo podemos hacer por ejemplo cargando primero en RAM una pequeña rutina de cambio de bancos llamable desde BASIC (con ''USR'') para ir cambiando de banco y hacer los correspondientes ''LOAD "" CODE 49152'' para cada banco a rellenar. También podríamos cargar un programa cargador propio que use la rutina de carga de la ROM para hacerlo.
  
   * Organizando bien los datos del juego, el mismo ejecutable podría ser versión 48K y 128K. Simplemente, en la versión 48K vamos cargando de cinta las fases en el último bloque de 16KB de RAM, mientras que la versión de 128K cargaría todas las fases en una única carga con cambios de banco.   * Organizando bien los datos del juego, el mismo ejecutable podría ser versión 48K y 128K. Simplemente, en la versión 48K vamos cargando de cinta las fases en el último bloque de 16KB de RAM, mientras que la versión de 128K cargaría todas las fases en una única carga con cambios de banco.
Línea 173: Línea 171:
 Para hacer esto, lo idea sería tener 2 ORG en nuestro programa en sjasmplus, o dos ficheros ASM en pasmo, para generar 2 ficheros BIN de código máquina separados. En el primero, tendríamos el punto de ejecución del inicio de nuestro programa. Para hacer esto, lo idea sería tener 2 ORG en nuestro programa en sjasmplus, o dos ficheros ASM en pasmo, para generar 2 ficheros BIN de código máquina separados. En el primero, tendríamos el punto de ejecución del inicio de nuestro programa.
  
-Lo primero que hacemos es cambiar la pila para sacarla de la contended memory, asignándola a la misma dirección donde empieza el código del juego en sí (33500). Debido a este cambio, ya no podremos volver al BASIC si finalizamos el menú con un RET. Lo normal es que no se vuelva más al BASIC, pero si quisíeramos hacerlo, podemos guardarnos el valor de SP en una variable en memoria (**pila_original DEFW 0**) y restaurar el valor de SP antes de hacer un RET de salida del menú.+Lo primero que hacemos es cambiar la pila para sacarla de la contended memory, asignándola a la misma dirección donde empieza el código del juego en sí (33500). Debido a este cambio, ya no podremos volver al BASIC si finalizamos el menú con un ret. Lo normal es que no se vuelva más al BASIC, pero si quisíeramos hacerlo, podemos guardarnos el valor de SP en una variable en memoria (''pila_original DEFW 0'') y restaurar el valor de SP antes de hacer un ''RET'' de salida del menú.
  
 Lo siguiente que hacemos es llamar a todas las rutinas de inicialización de uso único que tenga el programa, alojadas en este "BIN" cargado en 26000 y que sólo llamaremos una vez o sólo llamaremos durante el menú. Lo siguiente que hacemos es llamar a todas las rutinas de inicialización de uso único que tenga el programa, alojadas en este "BIN" cargado en 26000 y que sólo llamaremos una vez o sólo llamaremos durante el menú.
Línea 179: Línea 177:
 Finalmente ejecutamos todo el código del menú hasta el momento en que el jugador comience la partida que deberemos saltar al punto de entrada del juego. Finalmente ejecutamos todo el código del menú hasta el momento en que el jugador comience la partida que deberemos saltar al punto de entrada del juego.
  
-El segundo BINel del juego, se define en 33500 y contiene todo el código del juego, unos 32KB de código entero para el juego, del que hemos rescatado hasta 7KB al sacarlos al menú.+El BIN generado por el primer ASM se utiliza para generar el código que se cargará en 26000:
  
 <code z80> <code z80>
 ;;; Fichero menu.asm => genera menu.bin ;;; Fichero menu.asm => genera menu.bin
-ORG 26000+    ORG 26000
  
-    ; Ponemos la pila colgando del programa principal, +    ; Ponemos la pila colgando del programa principal, y fuera 
-    ; y fuera de la contended memory. +    ; de la contended memory. Si necesitaramos volver a BASIC 
-    LD SP, 33500+    ; habria que hacer una copia de SP en una variable para 
 +    ; recuperarla antes del RET final del programa. 
 +    ld sp, 33500
  
-    CALL detectar_modelo_128K     ; rutina en este ASM / bloque +    call Detectar_Modelo_128K     ; rutina en este ASM / bloque 
-    CALL inicializar_algo         ; rutina en este ASM / bloque+    call Inicializar_Algo         ; rutina en este ASM / bloque
  
-menu:+Menu:
     ; codigo del menu aqui.     ; codigo del menu aqui.
     ; ... en algun punto..     ; ... en algun punto..
-    CALL iniciar_partida +    call Iniciar_Partida 
-    JP menu+    jp Menu
  
 graficos_menu DEFB ..... graficos_menu DEFB .....
Línea 203: Línea 203:
 ; rutinas que queremos alojar en este bloque de datos ; rutinas que queremos alojar en este bloque de datos
  
-END 26000 +    END 26000 
-</code z80>+</code> 
 + 
 +El segundo BIN, el del juego, se define en 33500 y contiene todo el código del mismo, unos 32KB de código entero para nuestro programa, del que hemos "reducido" hasta 7KB al sacarlos al menú que está alojado a partir de 26000:
  
 <code z80> <code z80>
 ;;; Fichero juego.asm => genera juego.bin ;;; Fichero juego.asm => genera juego.bin
-ORG 33500+    ORG 33500
  
-iniciar_partida:+Iniciar_Partida:
  
     ; codigo del juego aqui     ; codigo del juego aqui
  
-    ; para volver al menu, un RET +    ; para volver al menu, un ret 
-    RET+    ret
  
-</code z80>+    END 33500 
 +</code>
  
 Al tener 2 BINs, nos tendremos que hacer un cargador BASIC personalizado que cargue cada bloque de cinta en su dirección correcta, y crear una cinta que contenga: Al tener 2 BINs, nos tendremos que hacer un cargador BASIC personalizado que cargue cada bloque de cinta en su dirección correcta, y crear una cinta que contenga:
Línea 225: Línea 228:
 </code> </code>
  
-Otra opción, en lugar de colocar el menú a partir de 26000, sería comprimir los niveles del mapeado de nuestro juego en ZX0, e incluirlos con un INCBIN en un fichero ASM con origen 26000 para cargarlos en esa zona.+Otra opción, en lugar de colocar el menú a partir de 26000, sería comprimir los niveles del mapeado de nuestro juego en ZX0, e incluirlos con un ''INCBIN'' en un fichero ASM con origen 26000 para cargarlos en esa zona.
  
 Después, en el código principal del juego antes de empezar cada pantalla, podemos "descomprimir" los datos de cada nivel en un buffer de trabajo en RAM y trabajar con esos datos durante todo ese nivel. El proceso de descompresión sólo se realiza una vez cuando empezamos una partida (nivel 1) o cambiamos de nivel. Después, en el código principal del juego antes de empezar cada pantalla, podemos "descomprimir" los datos de cada nivel en un buffer de trabajo en RAM y trabajar con esos datos durante todo ese nivel. El proceso de descompresión sólo se realiza una vez cuando empezamos una partida (nivel 1) o cambiamos de nivel.
Línea 263: Línea 266:
 El segundo mecanismo de ahorro de tiempo de carga si usamos gráficos con máscaras, es "autogenerar" las máscaras a partir de los gráficos del sprite, algo que se puede realizar vía código si son convexos. El segundo mecanismo de ahorro de tiempo de carga si usamos gráficos con máscaras, es "autogenerar" las máscaras a partir de los gráficos del sprite, algo que se puede realizar vía código si son convexos.
  
 +\\ 
 +==== Utilizar el Buffer de Impresión para la pila o alojar variables ====
 +
 +Existe una zona de memoria en el Spectrum de 48K que BASIC utiliza como "Buffer de Impresión". Es un buffer temporal usado por la impresora del Spectrum cuando está imprimiendo, pero que no se utiliza en caso contrario.
 +
 +Este área son 256 bytes, desde 23296 (5B00h) a 23551 (5BFFh).
 +
 +Hay gente que pone la pila en la posición de memoria del buffer de impresión, que puede ser suficiente si no vamos a hacer más de 128 anidaciones (cada ''PUSH'' son 2 bytes), pero que puede causar problemas en los modelos de 128K, por lo que no es recomendable cambiar la pila a esta ubicación.
 +
 +Aún así, podemos utilizar esa zona como "pila" o incluso como "zona de variables" de nuestro programa y así ahorrar hasta 256 bytes de RAM, teniendo en cuenta 2 cosas:
 +
 +1.- Está en la Contended Memory, aunque no es un problema grande para lo que es el acceso puntual a variables.
 +
 +2.- Los modelos de 128 tienen ahí rutinas de paginación de las ROM de sintaxis 128 y también partes del intérpretes de 48. Si pretendemos volver al BASIC después de ejecutar nuestro programa, o usar rutinas de la ROM en un 128K, no podemos tocar ese área.
 +
 +3.- Tampoco podremos usarlo en modelos 128K si tenemos activas las interrupciones en im1 (Modo de Interrupciones 1). Si usamos nuestra propia rutina ISR en im2 y desde ella no llamamos a la im1 (para que actualice FRAMES u otras variables del sistema), no debería haber problemas en utilizar ese área si vemos que nuestro programa crece y necesitamos arañar unos bytes para las variables del mismo.
 +
 +\\ 
  
 \\  \\ 
Línea 275: Línea 296:
 Una pantalla de carga de casi 7KB, comprimida, se puede quedar en menos de 3KB (a veces incluso 1.5 - 2 KB). Es una reducción de como mínimo el 50% del tiempo de carga, si no de un 66% en algunas ocasiones. Una pantalla de carga de casi 7KB, comprimida, se puede quedar en menos de 3KB (a veces incluso 1.5 - 2 KB). Es una reducción de como mínimo el 50% del tiempo de carga, si no de un 66% en algunas ocasiones.
  
-Para eso, guardamos la pantalla comprimida, y en lugar de cargar la pantalla con **LOAD "" SCREEN$**, lo hacemos con un cargador propio que carga los datos comprimidos en cualquier zona libre de la memoria, y después los descomprime directamente en la videomemoria (16384).+Para eso, guardamos la pantalla comprimida, y en lugar de cargar la pantalla con ''LOAD "" SCREEN$'', lo hacemos con un cargador propio que carga los datos comprimidos en cualquier zona libre de la memoria, y después los descomprime directamente en la videomemoria (16384).
  
 Una vez descomprimida la pantalla, podemos sobreescribir todo el buffer de datos comprimidos con nuestro código del programa. Una vez descomprimida la pantalla, podemos sobreescribir todo el buffer de datos comprimidos con nuestro código del programa.
Línea 296: Línea 317:
 <code z80> <code z80>
 ;;; Bloque codigo menu.asm ;;; Bloque codigo menu.asm
-ORG 33500+    ORG 33500
  
    ; código del menu aqui    ; código del menu aqui
Línea 304: Línea 325:
 </code> </code>
  
-El BIN generado al ensamblar este programa lo podremos cargar con nuestro cargador (asumiendo que no ocupe más de 5KB) entre 27000 y 32000, y desempaquetarlo en 40000, para que todo el código y datos del menú desempaquetados acaben en la posición de memoria esperada (donde estarían con un LOAD "" CODE si los hubiéramos cargado sin comprimir).+El BIN generado al ensamblar este programa lo podremos cargar con nuestro cargador (asumiendo que no ocupe más de 5KB) entre 27000 y 32000, y desempaquetarlo en 40000, para que todo el código y datos del menú desempaquetados acaben en la posición de memoria esperada (donde estarían con un ''LOAD "" CODE'' si los hubiéramos cargado sin comprimir).
  
 Así, nuestro programa "inicial", el "autoejecutable" que lanzaríamos desde BASIC, sería un programa en código máquina que contendría el descompresor de ZX0, y tal vez rutinas de detección de modelos 128K, y haría lo siguiente: Así, nuestro programa "inicial", el "autoejecutable" que lanzaríamos desde BASIC, sería un programa en código máquina que contendría el descompresor de ZX0, y tal vez rutinas de detección de modelos 128K, y haría lo siguiente:
Línea 330: Línea 351:
 ==== Dirección inicial de carga del código máquina ==== ==== Dirección inicial de carga del código máquina ====
  
-Como acabamos de ver, si nuestro programa principal está realizado en BASIC y lo que vamos a hacer es llamar a rutinas en código máquina desde él, estas rutinas deberían ser cargadas mucho más allá de la dirección 26000, ya que tiene que caber no sólo un simple cargador BASIC, sino todo nuestro programa. En este caso, lo normal sería cargar las rutinas en la parte alta de la memoria (por ejemplo, según cómo de extenso sea el programa en BASIC, en **50000**, con su **CLEAR 49999** previo), ya que la mayor ocupación de memoria en este caso será seguro por parte del programa BASIC y no de las rutinas en código máquina (para la cual hemos dejado unos 15KB empezando en 50000 y acabando donde empiezan los UDG, en 65368).+Como acabamos de ver, si nuestro programa principal está realizado en BASIC y lo que vamos a hacer es llamar a rutinas en código máquina desde él, estas rutinas deberían ser cargadas mucho más allá de la dirección 26000, ya que tiene que caber no sólo un simple cargador BASIC, sino todo nuestro programa. En este caso, lo normal sería cargar las rutinas en la parte alta de la memoria (por ejemplo, según cómo de extenso sea el programa en BASIC, en **50000**, con su ''CLEAR 49999'' previo), ya que la mayor ocupación de memoria en este caso será seguro por parte del programa BASIC y no de las rutinas en código máquina (para la cual hemos dejado unos 15KB empezando en 50000 y acabando donde empiezan los UDG, en 65368).
  
 \\  \\ 
Línea 347: Línea 368:
 **RANDOMIZE USR dirección** **RANDOMIZE USR dirección**
  
-En esta opción, se ejecuta el **USR dirección**, lo que produce un salto a la rutina en dicha dirección. Cuando esta acaba, el valor del registro BC se devuelve a BASIC y se asigna como parámetro a RANDOMIZE.+En esta opción, se ejecuta el ''USR dirección'', lo que produce un salto a la rutina en dicha dirección. Cuando esta acaba, el valor del registro BC se devuelve a BASIC y se asigna como parámetro a RANDOMIZE.
  
 Esto produce que el valor devuelto por USR se use como semilla para números aleatorios. Es una forma de ejecutar código máquina sin sacar nada por pantalla y con una disrupción mínima para BASIC (sólo cambiamos la semilla de los números aleatorios). Es la forma normal de llamar a código máquina desde cargadores BASIC cuando no pretendemos continuar la ejecución en BASIC ni volver a él. Esto produce que el valor devuelto por USR se use como semilla para números aleatorios. Es una forma de ejecutar código máquina sin sacar nada por pantalla y con una disrupción mínima para BASIC (sólo cambiamos la semilla de los números aleatorios). Es la forma normal de llamar a código máquina desde cargadores BASIC cuando no pretendemos continuar la ejecución en BASIC ni volver a él.
  
-Pero si estamos haciendo un programa en BASIC con llamadas a rutinas código máquina, y la parte BASIC usa llamadas a **RND** para obtener números aleatorios, no querremos que se estropee el valor de la semilla de números aleatorios (porque será establecida con el valor que dejemos en BC) en cada llamada. En ese caso no debemos utilizar RANDOMIZE USR, o, si lo hacemos, nos puede interesar finalizar nuestras rutinas en código máquina con:+Pero si estamos haciendo un programa en BASIC con llamadas a rutinas código máquina, y la parte BASIC usa llamadas a ''RND'' para obtener números aleatorios, no querremos que se estropee el valor de la semilla de números aleatorios (porque será establecida con el valor que dejemos en BC) en cada llamada. En ese caso no debemos utilizar ''RANDOMIZE USR'', o, si lo hacemos, nos puede interesar finalizar nuestras rutinas en código máquina con:
  
 <code z80> <code z80>
-    LD BC, (23670) +    ld bc, (23670) 
-    RET+    ret
 </code> </code>
  
-El valor de **23760** (**$5C76**) es el actual valor de **SEED**, la variable del sistema que contiene la semilla por defecto que tuvieramos antes de la llamada, con lo si lo asignamos a BC antes de retornar, el comando RANDOMIZE volvería a establecer dicho valor y así lo preservaríamos al volver a BASIC.+El valor de **23760** (**$5c76**) es el actual valor de ''SEED'', la variable del sistema que contiene la semilla por defecto que tuvieramos antes de la llamada, con lo si lo asignamos a BC antes de retornar, el comando ''RANDOMIZE'' volvería a establecer dicho valor y así lo preservaríamos al volver a BASIC.
  
 \\  \\ 
 **LET V=USR dirección** **LET V=USR dirección**
  
-Finalmente tenemos LET USR. Si no queremos que aparezca nada por pantalla (PRINT USR) ni tampoco alterar el valor de la semilla de números aleatorios, ni tampoco preservarla al final de cada rutina (RANDOMIZE USR), podemos optar por usar LET con una variable desechable para poder llamar a USR y desechar el valor devuelvo por la rutina en BC. Este valor se asignaría a la variable que hayamos escogido en BASIC (y que se recomienda que sea de una sóla letra ya que es más rápido en el BASIC del Spectrum) y simplemente podemos ignorar su valor.+Finalmente tenemos ''LET USR''. Si no queremos que aparezca nada por pantalla (''PRINT USR'') ni tampoco alterar el valor de la semilla de números aleatorios, ni tampoco preservarla al final de cada rutina (''RANDOMIZE USR''), podemos optar por usar LET con una variable desechable para poder llamar a ''USR'' y desechar el valor devuelvo por la rutina en BC. Este valor se asignaría a la variable que hayamos escogido en BASIC (y que se recomienda que sea de una sóla letra ya que es más rápido en el BASIC del Spectrum) y simplemente podemos ignorar su valor.
  
 \\  \\ 
Línea 370: Línea 391:
 Otra restricción importante sobre nuestro código máquina si lo vamos a llamar desde BASIC es que no debemos de utilizar los registros del Z80 llamados **IX** e **IY** en funciones que vayamos a llamar desde BASIC. Otra restricción importante sobre nuestro código máquina si lo vamos a llamar desde BASIC es que no debemos de utilizar los registros del Z80 llamados **IX** e **IY** en funciones que vayamos a llamar desde BASIC.
  
-De hecho, no deberíamos utilizar **IY** ni siquiera desde ensamblador puro si estamos usando el modo de interrupción IM1.+De hecho, no deberíamos utilizar **IY** ni siquiera desde ensamblador puro si estamos usando el modo de interrupción im1.
  
-Como ya comentamos, la rutina de la ROM **$0038**, llamada en IM1, utiliza el registro IY para actualizar el tercer byte de la variable FRAMES con un **INC (IY+$40)** por lo que si modificamos IY en nuestros programas y se produce una interrupción, modificaremos una zona de la memoria que no es la esperada. Si necesitamos usar IY, es mejor desactivar las interrupciones antes de hacerlo, y volver a activarlas después.+Como ya comentamos, la rutina de la ROM **$0038**, llamada en im1, utiliza el registro IY para actualizar el tercer byte de la variable ''FRAMES'' con un **INC (IY+$40)** por lo que si modificamos IY en nuestros programas y se produce una interrupción, modificaremos una zona de la memoria que no es la esperada. Si necesitamos usar IY, es mejor desactivar las interrupciones antes de hacerlo, y volver a activarlas después.
  
 \\  \\ 
 ==== Uso del registro I ==== ==== Uso del registro I ====
  
-Tampoco se debe cargar el registro "I" con valores entre 40h o 7Fh (aunque no se vaya a usar IM2). Sólo se deben redirigir las interrupciones IM2 entre 8000h y BFFFh. Más adelante hablaremos de lo que son los modos de interrupción.+Tampoco se debe cargar el registro "I" con valores entre 40h o 7Fh (aunque no se vaya a usar im2). Sólo se deben redirigir las interrupciones im2 entre 8000h y BFFFh. Más adelante hablaremos de lo que son los modos de interrupción.
  
  
Línea 386: Línea 407:
  
 \\  \\ 
-1.- En el cargador del juego, una vez cargado todo el código del mismo (lo que sería la versión completa 48K), con las optimizaciones de carga que se deseen (compresión, etc) se ejecuta el RANDOMIZE USR que lanza la versión 48K del mismo.+1.- En el cargador del juego, una vez cargado todo el código del mismo (lo que sería la versión completa 48K), con las optimizaciones de carga que se deseen (compresión, etc) se ejecuta el ''RANDOMIZE USR'' que lanza la versión 48K del mismo.
  
 2.- En el inicio de nuestro juego, utilizamos la rutina de detección del modelo 128K mediante comprobación de la paginación, y averiguamos si es un modelo 128K ó 48K. 2.- En el inicio de nuestro juego, utilizamos la rutina de detección del modelo 128K mediante comprobación de la paginación, y averiguamos si es un modelo 128K ó 48K.
Línea 402: Línea 423:
 1.- Antes de mostrar el menú del juego 48K, si estamos en modo 128K (consultando la variable que estableció la rutina detectora de modelo) podemos utilizar los gráficos extra cargados en otro banco para mostrar la introducción (que sólo se verá al arrancar el juego). Conmutamos al banco y pintamos las pantallas, textos, animaciones, etc. Al acabar la intro, volvemos al banco original. 1.- Antes de mostrar el menú del juego 48K, si estamos en modo 128K (consultando la variable que estableció la rutina detectora de modelo) podemos utilizar los gráficos extra cargados en otro banco para mostrar la introducción (que sólo se verá al arrancar el juego). Conmutamos al banco y pintamos las pantallas, textos, animaciones, etc. Al acabar la intro, volvemos al banco original.
  
-2.- En la rutina ISR IM2 del juego, si es un modo 128K, conmutamos al banco de la música y llamamos al player. A este player le pediremos lo que sea necesario (parar, arrancar música, cambiar de melodía, etc) según lo que indique alguna variable de control que tengamos en memoria para que el IM2 sepa qué tiene que hacer con la música en cada momento. Esta variable la controlaremos desde el juego en sí como veremos en el paso 5.+2.- En la rutina ISR im2 del juego, si es un modo 128K, conmutamos al banco de la música y llamamos al player. A este player le pediremos lo que sea necesario (parar, arrancar música, cambiar de melodía, etc) según lo que indique alguna variable de control que tengamos en memoria para que el im2 sepa qué tiene que hacer con la música en cada momento. Esta variable la controlaremos desde el juego en sí como veremos en el paso 5.
  
 3.- Una vez se ha llamado a la rutina del player y hemos vuelto de ella (todavía en la ISR), si estamos en modo 128K ya se puede volver a conmutar al banco de memoria original, para que la ISR siga ejecutándose como en los modelos de 48K. 3.- Una vez se ha llamado a la rutina del player y hemos vuelto de ella (todavía en la ISR), si estamos en modo 128K ya se puede volver a conmutar al banco de memoria original, para que la ISR siga ejecutándose como en los modelos de 48K.
Línea 411: Línea 432:
  
 Con esto el mismo juego 48K podrá tener música en modo 128K con unos pequeños añadidos muy sencillos, y ser compatible con ambos modelos. Con esto el mismo juego 48K podrá tener música en modo 128K con unos pequeños añadidos muy sencillos, y ser compatible con ambos modelos.
 +
 +
 +\\ 
 +\\ 
 +===== Código Automodificable =====
 +
 +Ya vimos en un capítulo anterior un ejemplo de **código automodificable**. Consiste en sobreescribir en memoria un opcode o un dato en función de una condición, para que cuando lleguemos a ese punto del programa, se ejecute el opcode modificado.
 +
 +En el ejemplo que ya vimos, utilizamos código automodificable para sobreescribir un instrucción de carga "futura" que nos permitirá recuperar el valor del registro BC más adelante, por ejemplo porque vamos a modificarlo con un bucle, y no tenemos la pila disponible:
 +
 +<code z80>
 +    ld (save_bc+1), bc    ; Escribimos BC en la parte NN NN del
 +                          ; opcode "ld bc, NN NN" en memoria
 +
 +    ... hacer cosas con BC, perdiendo su valor ...
 +
 +save_bc:
 +    ld bc, $0000          ; En el LD anterior cambiamos $000
 +                          ; por el valor de BC, así que cuando
 +                          ; el z80 llegue aquí no es ya ld bc, 0
 +                          ; sino ld bc, valor_que_tenia_BC
 +                          ; así que recuperaremos BC aquí.
 +</code>
 +
 +Cuando se ejecuta la instrucción ''ld (save_bc+1), bc'', estamos sobreescribiendo nuestro propio programa, cambiando el opcode que estaba ensamblado en memoria (''ld bc, $0000'', es decir "**$01 $00 $00**") por **$01 XX XX**, siendo **XX XX** el nuevo valor que queremos introducir en el opcode.
 +
 +Cuando se ejecuta el ''ld (save_bc+1), bc'', estamos escribiendo el valor de BC en ese momento encima de "XX XX" de forma que cuando la ejecución llegue a ese punto, el comando ''ld bc, XX'' se ejecutará y recuperará en BC el valor que tenía BC cuando se almacenó en esa posición de memoria.
 +
 +La escritura en memoria (''ld (nn), bc'' = 20 ciclos de reloj) y su posterior asignación (10 ciclos) cuesta un total de 30 ciclos de reloj, mientras que si hubiéramos usado PUSH/POP costaría sólo 21 ciclos en total (11 el ''PUSH'' y 10 el ''POP''). Sin embargo, si estamos en una rutina de impresión de Sprites donde usamos la pila para recuperar datos del Sprite, por ejemplo, es una forma de recuperar valores de registros en una situación donde no podemos hacer ''PUSH'' y ''POP'' para ello.
 +
 +Y no sólo podemos modificar datos, también podríamos modificar opcodes, cambiando ''JR Z''s por ''JR NZ''s, por ejemplo, para cambiar la condicionalidad de los saltos de alguna forma.
 +
 +\\ 
 +===== Obtener el valor de PC (Contador de Programa) =====
 +
 +Utilizando ''CALL'' y la pila del Z80, podemos obtener en un registro el valor del Contador de Programa o ''PC'':
 +
 +<code z80>
 +    call NextInstr       ; CALL mete PC en la pila
 +NextInstr:
 +    pop hl               ; HL contiene ahora PC
 +</code>
 +
 +Al ejecutarse ''call NextInstr'', el Z80 mete en la pila la dirección de retorno para el ''CALL'', en este caso, justo la dirección en la que está el contador de programa. Una vez hecho eso, no hacemos un ''RET'' para volver sino que hacemos ''POP HL'' para extraer PC de la pila sin volver al punto de llamada. El flujo de ejecución continuaría tras el ''pop hl'', con el valor de PC introducido en HL.
 +
 +¿Para qué podría valer algo así? Nuestro compañero **<nowiki>Xor_A [Fer]</nowiki>** nos proporciona un ejemplo con el cual podemos utilizar el valor de PC para imprimir cadenas sin tener que cargar en HL cada vez la posición de la cadena, intercalando el código y los datos:
 +
 +<code z80>
 +Imprimir_Mensajes:
 +    call MyPrintString
 +    DB "cadena1", $ff
 +    call MyPrintString
 +    DB "cadena2", $ff
 +    call MyPrintString
 +    DB "cadena3", $ff
 +    (...)
 +
 +MyPrintString:
 +    pop hl          ; obtenemos la direccion de la cadena (PC)
 +mpr_loop:
 +    ld a, (hl)
 +    cp $ff
 +    Jr z, mprexit
 +    rst $10
 +    inc hl
 +    jr mpr_loop
 +mpr_exit:
 +    push hl         ; introducimos direccion de retorno
 +    ret             ; PC continuará después de la cadena impresa
 +</code>
 +
 +La llamada a ''MyPrintString'' mete en la pila la dirección de PC, que es la dirección del primer carácter de la cadena.
 +
 +La rutina extrae ese valor de la pila y lo usa como "puntero a la cadena" en HL. Incrementa HL tras imprimir cada carácter, y al acabar de imprimir la cadena, mete en la pila el valor de HL para que el ''RET'' vuelva al flujo principal del programa, pero justo después de los datos de la cadena, es decir, al siguiente ''call MyPrintString''.
 +
 +Esto nos evitaría lo siguiente, que suponen 3 bytes por cada instrucción ''ld hl, NNNN'':
 +
 +<code z80>
 +    ld hl, cadena1
 +    call MyPrintString
 +    ld hl, cadena2
 +    call MyPrintString
 +    ld hl, cadena3
 +    call MyPrintString
 +</code>
  
 \\  \\ 
-**[ [[.:indice|⬉]] | [[.:compresion_rle|⬅]] | [[.:avanzadas2|➡]] ]**+**[ [[.:indice|⬉]] | [[.:compresion_rle|⬅]] | [[.:avanzadas2|➡]] ]**
  • cursos/ensamblador/avanzadas1.1704705240.txt.gz
  • Última modificación: 08-01-2024 09:14
  • por sromero