cursos:ensamblador:lenguaje_4

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:lenguaje_4 [06-01-2024 16:00] – [Integracion de ASM en Z88DK] sromerocursos:ensamblador:lenguaje_4 [22-01-2024 07:54] (actual) – [PUSH y POP] sromero
Línea 1: Línea 1:
 ====== Lenguaje Ensamblador del Z80 (IV) ====== ====== Lenguaje Ensamblador del Z80 (IV) ======
  
-====== La pila y las llamadas a subrutinas ======+===== La pila y las llamadas a subrutinas =====
  
 \\  \\ 
Línea 8: Línea 8:
  Este capítulo se centra en una de las estructuras más importantes del microprocesador Z80: **la pila** (o **Stack** en inglés).  Este capítulo se centra en una de las estructuras más importantes del microprocesador Z80: **la pila** (o **Stack** en inglés).
  
- La pila //es una porción de memoria donde se pueden almacenar valores de 16 bits//, apilados uno a continuación del siguiente. + La pila //es una porción de memoria donde se pueden almacenar valores de 16 bits//, apilados uno a continuación del siguiente.
  
- Su nombre viene del hecho que los datos se almacenan unos "encima" de los otros, como, por ejemplo, en una pila de platos. + Su nombre viene del hecho que los datos se almacenan unos "encima" de los otros, como, por ejemplo, en una pila de platos.
  
  Cuando almacenamos un nuevo plato en una pila, lo dejamos en la parte superior de la misma, sobre el plato anterior. Cuando queremos coger un plato, cogemos el plato de arriba, el situado en la parte superior de la pila.  Cuando almacenamos un nuevo plato en una pila, lo dejamos en la parte superior de la misma, sobre el plato anterior. Cuando queremos coger un plato, cogemos el plato de arriba, el situado en la parte superior de la pila.
  
- Es lo que se conoce como una estructura de datos "//tipo LIFO//" ("//Last In, First Out//"): el último que entró es el primero que sale. En nuestro ejemplo de los platos, efectivamente cuando retiramos un plato extraemos el que está arriba del todo, por lo que el primero en salir (First Out) es el último que habíamos dejado (Last In). + Es lo que se conoce como una estructura de datos "//tipo LIFO//" ("//Last In, First Out//"): el último que entró es el primero que sale. En nuestro ejemplo de los platos, efectivamente cuando retiramos un plato extraemos el que está arriba del todo, por lo que el primero en salir (First Out) es el último que habíamos dejado (Last In).
  
  En una pila de ordenador (como en nuestra pila de datos) sólo podemos trabajar  En una pila de ordenador (como en nuestra pila de datos) sólo podemos trabajar
Línea 20: Línea 20:
 de la pila. de la pila.
  
- La pila del Spectrum no es de platos sino de valores numéricos de 16 bits. Introducimos valores y sacamos valores mediante 2 instrucciones concretas: **PUSH <valor>** **POP <valor>**, donde + La pila del Spectrum no es de platos sino de valores numéricos de 16 bits. Introducimos valores y sacamos valores mediante 2 instrucciones concretas: ''PUSH <valor>'' ''POP <valor>'', donde normalmente <valor> será un registro (metemos en la pila el valor que contiene un registro de 16 bits, o bien leemos de la pila un valor y lo asignamos a un registro de 16 bits).
-normalmente <valor> será un registro (metemos en la pila el valor que contiene un registro de 16 bits, o bien leemos de la pila un valor y lo asignamos a un registro de 16 bits).+
  
- Por ejemplo, podemos guardar el valor que contiene un registro + Por ejemplo, podemos guardar el valor que contiene un registro en la pila si tenemos que hacer operaciones con ese registro para así luego recuperarlo tras realizar una determinada tarea:
-en la pila si tenemos que hacer operaciones con ese registro +
-para así luego recuperarlo tras realizar una determinada tarea:+
  
 <code z80> <code z80>
- LD BC, 1000 +    ld bc, 1000 
- PUSH BC         ; Guardamos el contenido de BC en la pila+    push bc         ; Guardamos el contenido de BC en la pila
  
- LD BC, 2000 +    ld bc, 2000 
- (...)           ; Operamos con BC +    (...)           ; Operamos con BC
  
- LD HL, 0 +    ld hl, 0 
- ADD HLBC      ; y ya podemos guardar el resultado de la operación +    add hlbc      ; y ya podemos guardar el resultado de la operación 
-                 ; (recordemos que no existe "LD HLBC", de modo que +                    ; (recordemos que no existe "ld hlbc", de modo que 
-                 ; lo almacenamos como HL = 0+BC +                    ; lo almacenamos como HL = 0+BC 
-  + 
- POP BC          ; Hemos terminado de trabajar con BC, ahora +    pop bc          ; Hemos terminado de trabajar con BC, ahora 
-                 ; recuperamos el valor que tenia BC (1000).+                    ; recuperamos el valor que tenia BC (1000).
 </code> </code>
  
- La instrucción "PUSH BC" introduce en memoria, en lo alto de la pila, el valor contenido en BC (1000), que recuperamos posteriormente con el "POP BC".+ La instrucción ''push bc'' introduce en memoria, en lo alto de la pila, el valor contenido en BC (1000), que recuperamos posteriormente con el ''pop bc''.
  
  La realidad es que //el Spectrum no tiene una zona de memoria especial o aislada de la RAM dedicada a la pila. En su lugar se utiliza la misma RAM// del Spectrum (0-65535).  La realidad es que //el Spectrum no tiene una zona de memoria especial o aislada de la RAM dedicada a la pila. En su lugar se utiliza la misma RAM// del Spectrum (0-65535).
Línea 59: Línea 56:
  Supongamos que SP (puntero de pila) apunta a 65535 y que tenemos los siguientes valores en BC y DE:  Supongamos que SP (puntero de pila) apunta a 65535 y que tenemos los siguientes valores en BC y DE:
  
-<code z80>  +<code z80> 
- LD BC, $00FF +    ld bc, $00ff 
- LD DE, $AABB +    ld de, $aabb 
- LD SP, 65535     ; Puntero de pila al final de la memoria+    ld sp, 65535     ; Puntero de pila al final de la memoria
 </code> </code>
  
 +(Nota: como veremos más adelante, para poner la pila al final de la memoria en realidad hay que ponerla en $0000, pero por motivos didácticos y para simplificar la explicación, vamos a hacer este ejemplo con ese valor).
 + 
  Si ahora hacemos:  Si ahora hacemos:
  
 <code z80> <code z80>
- PUSH BC          ; Apilamos el registro BC+    push bc          ; Apilamos el registro BC
 </code> </code>
  
Línea 74: Línea 73:
  
 <code> <code>
- SP = SP - 2 = 65533 +SP = SP - 2 = 65533 
- (SP) = BC = $00FF+(SP) = BC = $00ff
 </code> </code>
  
Línea 83: Línea 82:
       Celdilla    Contenido       Celdilla    Contenido
      -----------------------      -----------------------
-       65534         $FF+       65534         $ff
 SP ->  65533         $00 SP ->  65533         $00
 </code> </code>
Línea 90: Línea 89:
  
 <code z80> <code z80>
- PUSH DE          ; Apilamos el registro DE+    push de          ; Apilamos el registro DE
 </code> </code>
  
Línea 96: Línea 95:
  
 <code> <code>
- SP = SP - 2 = 65531 +SP = SP - 2 = 65531 
- (SP) = DE = $AABB+(SP) = DE = $aabb
 </code> </code>
  
Línea 105: Línea 104:
       Celdilla    Contenido       Celdilla    Contenido
      -----------------------      -----------------------
-       65534         $FF+       65534         $ff
        65533         $00        65533         $00
-       65532         $AA +       65532         $aa 
-SP ->  65531         $BB+SP ->  65531         $bb
 </code> </code>
  
- Si ahora hacemos un POP:+ Si ahora hacemos un ''POP'':
  
 <code z80> <code z80>
- POP DE+    pop de
 </code> </code>
  
Línea 120: Línea 119:
  
 <code> <code>
- DE = (SP) = $AABB +DE = (SP) = $aabb 
- SP = SP + 2 = 65533+SP = SP + 2 = 65533
 </code> </code>
  
Línea 129: Línea 128:
       Celdilla    Contenido       Celdilla    Contenido
      -----------------------      -----------------------
-       65534         $FF+       65534         $ff
 SP ->  65533         $00 SP ->  65533         $00
 </code> </code>
  
- Como podemos ver, //PUSH apila valores//, haciendo decrecer el valor de SP, mientras que //POP recupera valores//, haciendo crecer (en 2 bytes, 16 bits) el valor de SP.+ Como podemos ver, **PUSH apila valores**, haciendo decrecer el valor de SP, mientras que **pop recupera valores**, haciendo crecer (en 2 bytes, 16 bits) el valor de SP.
  
-Con el objetivo de que el ejemplo fuera más comprensible, hemos establecido SP a 65535, pero si te fijas, después del primer PUSH se han guardado los valores en 65534 y 65533 (no hemos escrito nada en 65535). En realidad, lo que deberíamos hacer para aprovechar la memoria es poner SP a 0, ya que la primera operación que se hace es el decremento y después la escritura.+Con el objetivo de que el ejemplo fuera más comprensible, hemos establecido SP a 65535, pero si te fijas, después del primer ''PUSH'' se han guardado los valores en 65534 y 65533 (no hemos escrito nada en 65535). En realidad, lo que deberíamos hacer para aprovechar la memoria es poner SP a 0, ya que la primera operación que se hace es el decremento y después la escritura.
  
-Por eso, poniendo SP = 0, el decremento en 2 unidades al hacer el PUSH produciría que los 2 valores se guardasen en 65534 y 65535, aprovechando así ese último byte de la memoria.+Por eso, poniendo SP = 0, el decremento en 2 unidades al hacer el ''PUSH'' produciría que los 2 valores se guardasen en 65534 y 65535, aprovechando así ese último byte de la memoria.
  
 \\  \\ 
Línea 143: Línea 142:
  
  Así pues, podemos hacer **PUSH** y **POP** de los siguientes registros:  Así pues, podemos hacer **PUSH** y **POP** de los siguientes registros:
-  + 
-  * PUSH:  AF, BC, DE, HL, IX, IY +  * ''PUSH'':  AF, BC, DE, HL, IX, IY 
-  * POP :  AF, BC, DE, HL, IX, IY+  * ''POP'' :  AF, BC, DE, HL, IX, IY
  
  Lo que hacen PUSH y POP, tal y como funciona la pila, es:  Lo que hacen PUSH y POP, tal y como funciona la pila, es:
  
 <code> <code>
- PUSH xx : +push xx : 
-   SP   = SP-2 +     SP  = SP-2 
-   (SP) = xx +    (SP) = xx 
-    + 
- POP xx : +pop xx : 
-   xx   = (SP) +    xx   = (SP) 
-   SP   = SP+2+    SP   = SP+2
 </code> </code>
  
- Nótese cómo la pila se decrementa ANTES de poner los datos en ella, + Visto en "pseucódigo BASIC", ambos comandos serían: 
-y se incrementa DESPUES de sacar datos de la misma. Esto mantiene + 
-siempre SP apuntando al TOS (//Top Of Stack//).+<code z80> 
 +push hl  =   LET SP = SP-2 
 +             POKE (SP+1), H 
 +             POKE SP, L 
 + 
 +pop hl     LET L = PEEK SP 
 +             LET H = PEEK (SP+1) 
 +             LET SP = SP+2 
 +</code> 
 + 
 + Nótese cómo la pila se decrementa ANTES de poner los datos en ella, y se incrementa DESPUES de sacar datos de la misma. Esto mantiene siempre SP apuntando al TOS (//Top Of Stack//).
  
 <code> <code>
-                        Flags +                        Flags
    Instrucción       |S Z H P N C|    Instrucción       |S Z H P N C|
  ----------------------------------  ----------------------------------
- POP xx              |- - - - - -| + pop xx              |- - - - - -| 
- PUSH xx             |- - - - - -|+ push xx             |- - - - - -|
 </code> </code>
  
- Nótese que también podemos apilar y desapilar AF. De hecho, es una forma + Nótese que también podemos apilar y desapilar AF. De hecho, es una forma de manipular los bits del registro F (hacer push bc con un valor determinado, por ejemplo, y hacer un pop af).
-de manipular los bits del registro F (hacer PUSH BC con un valor determinado, +
-por ejemplo, y hacer un POP AF). +
- +
  
 \\  \\ 
 ===== Utilidad de la pila del Spectrum ===== ===== Utilidad de la pila del Spectrum =====
  
- + La pila resulta muy útil para gran cantidad de tareas en programas en ensamblador. Veamos algunos ejemplos:
- La pila resulta muy útil para gran cantidad de tareas en programas en +
-ensamblador. Veamos algunos ejemplos:+
  
 \\  \\ 
-  * Intercambiar valores de registros mediante PUSH y POP. Por ejemplo, para intercambiar el valor de BC y de DE: +  * Uno de sus usos más evidentes es el de preservar valores de registros mientras ejecutamos porciones de códigoSupongamos que tenemos un registro cuyo valor queremos mantener, pero que tenemos que ejecutar una porción de código que lo modifica. Gracias a la pila podemos hacer lo siguiente:
- +
-<code z80> +
- PUSH BC       ; Apilamos BC +
- PUSH DE       ; Apilamos DE +
- POP BC        ; Desapilamos BC  +
-               ; ahora BC=(valor apilado en PUSH DE) +
- POP DE        ; Desapilamos DE +
-               ; ahora DE=(valor apilado en PUSH BC) +
-</code> +
- +
-\\  +
-  * Para manipular el registro F: La instrucción **POP AF** es la principal forma de manipular el registro F directamente (haciendo PUSH de otro registro y POP de AF). +
- +
-  * Almacenaje de datos mientras ejecutamos porciones de códigoSupongamos que tenemos un registro cuyo valor queremos mantener, pero que tenemos que ejecutar una porción de código que lo modifica. Gracias a la pila podemos hacer lo siguiente:+
 \\  \\ 
  
 <code z80> <code z80>
- PUSH BC       ; Guardamos el valor de BC+    push bc          ; Guardamos el valor de BC
  
- (código)      ; Hacemos operaciones+    (código)         ; Hacemos operaciones que necesitan usar BC
  
- POP BC        ; Recuperamos el valor que teníamos en BC+    pop bc           ; Recuperamos el valor que teníamos en BC
 </code> </code>
  
  Esto incluye, por ejemplo, el almacenaje del valor de BC en los bucles cuando necesitamos operador con B, C o BC:  Esto incluye, por ejemplo, el almacenaje del valor de BC en los bucles cuando necesitamos operador con B, C o BC:
  
-<code z80>  +<code z80> 
-    LD A, 0 +    ld a, 0 
-    LD B, 100+    ld b, 100 
 bucle: bucle:
-    PUSH BC         ; Guardamos BC +    push bc          ; Guardamos BC 
-    LD B, 1 +    ld b, 1 
-    ADD AB +    add ab 
-    POP BC          ; Recuperamos BC +    pop bc           ; Recuperamos BC 
-    DJNZ bucle  +    djnz bucle
 </code> </code>
  
- En este sentido, también podremos anidar 2 o más bucles que usen el registro B o BC con PUSH y POPs entre ellos. Supongamos un bucle BASIC del tipo:+ En este sentido, también podremos anidar 2 o más bucles que usen el registro B o BC con PUSHes y POPs entre ellos. Supongamos un bucle BASIC del tipo:
  
 <code basic> <code basic>
-  FOR I=0 TO 20:+For i=0 TO 20:
     FOR J=0 TO 100:     FOR J=0 TO 100:
-       CODIGO+        CODIGO
     NEXT J     NEXT J
-  NEXT I+NEXT I
 </code> </code>
  
Línea 236: Línea 226:
  
 <code z80> <code z80>
-    LD B, 20                ; repetimos bucle externo 20 veces+    ld b, 20                 ; repetimos bucle externo 20 veces
  
 bucle_externo: bucle_externo:
-    PUSH BC                 ; Nos guardamos el valor de BC +    push bc                  ; Nos guardamos el valor de BC 
-    LD B, 100               ; Iteraciones del bucle interno+    ld b, 100                ; Iteraciones del bucle interno 
 bucle_interno: bucle_interno:
     (... código ...)     (... código ...)
-    DJNZ bucle_interno      ; FOR J=0 TO 100 +    djnz bucle_interno       ; FOR J=0 TO 100 
-    POP BC                  ; Recuperamos el valor de B+    pop bc                   ; Recuperamos el valor de B
  
-    DJNZ bucle_externo      FOR I=0 TO 20+    djnz bucle_externo       For i=0 TO 20
 </code> </code>
  
- Hay que tener en cuenta que PUSH y POP implican escribir en memoria (en la dirección apuntada por SP), por que siempre serán más lentas que guardarse el valor actual de B en otro registro:+ Hay que tener en cuenta que ''PUSH'' ''POP'' implican escribir en memoria (en la dirección apuntada por SP), por que siempre serán más lentas que guardarse el valor actual de B en otro registro:
  
 <code z80> <code z80>
-    LD B, 20                ; repetimos bucle externo 20 veces+    ld b, 20                 ; repetimos bucle externo 20 veces
  
 bucle_externo: bucle_externo:
-    LD DB                 ; Nos guardamos el valor de B+    ld db                  ; Nos guardamos el valor de B 
 + 
 +    ld b, 100                ; Iteraciones del bucle interno
  
-    LD B, 100               ; Iteraciones del bucle interno 
 bucle_interno: bucle_interno:
-    (... código ...)        ; En este codigo no podemos usar D +    (... código ...)         ; En este codigo no podemos usar D 
-    DJNZ bucle_interno      ; FOR J=0 TO 100+    djnz bucle_interno       ; FOR J=0 TO 100
  
-    LD BD                 ; Recuperamos el valor de B +    ld bd                  ; Recuperamos el valor de B 
-    DJNZ bucle_externo      FOR I=0 TO 20+    djnz bucle_externo       For i=0 TO 20
 </code> </code>
  
- No obstante, en múltiples casos nos quedaremos sin registros libres donde guardar datos, por lo que la pila es una gran opción. No hay que obsesionarse con no usar la pila porque implique escribir en memoria. A menos que estemos hablando de una rutina muy muy crítica, que se ejecute muchas veces por cada fotograma de nuestro juego, PUSH y POP serán las mejores opciones para preservar valores, con un coste de 11 t-estados para el PUSH y 10 t-estados para el POP de los registros de propósito general y de 15 y 14 t-estados cuando trabajamos con IX e IY.+ No obstante, en múltiples casos nos quedaremos sin registros libres donde guardar datos, por lo que la pila es una gran opción. No hay que obsesionarse con no usar la pila porque implique escribir en memoria. A menos que estemos hablando de una rutina muy muy crítica, que se ejecute muchas veces por cada fotograma de nuestro juego, ''PUSH'' ''POP'' serán las mejores opciones para preservar valores, con un coste de 11 t-estados para el ''PUSH'' y 10 t-estados para el ''POP'' de los registros de propósito general y de 15 y 14 t-estados cuando trabajamos con IX e IY.
  
 \\  \\ 
-  * Almacenaje de datos de entrada y salida en subrutinas: Podemos pasar parámetros a nuestras rutinas apilándolos en el stack, de forma que nada más entrar en la rutina leamos de la pila esos parámetros.+  * Como veremos en el próximo apartadola pila es la clave de las subrutinas (''CALL''/''RET''en el Spectrum (equivalente al ''GOSUB''/''RETURN'' de BASIC).
  
-  Extendiendo un poco más el punto anterior, cuando realicemos funciones en ensamblador embebidas dentro de otros lenguajes (por ejemplo, dentro de programas en C con Z88DK), podremos recoger dentro de nuestro bloque en ensamblador los parámetros pasados con llamadas de funciones C.+\\  
 +  Almacenaje de datos de entrada y salida en subrutinas: Podemos pasar parámetros a nuestras rutinas apilándolos en el stack, de forma que nada más entrar en la rutina leamos de la pila esos parámetros. Además, cuando realicemos funciones en ensamblador embebidas dentro de otros lenguajes (por ejemplo, dentro de programas en C con Z88DK), recibiremos así dentro de nuestro bloque en ensamblador los parámetros pasados con llamadas de funciones C.
  
-  Como veremos en el próximo apartado, la pila es la clave de las subrutinas (CALL/RET) en el Spectrum (equivalente al GOSUB/RETURN de BASIC).+\\  
 +  Para manipular el registro F: La instrucción ''pop af'' es la principal forma de manipular el registro F directamente (haciendo ''PUSH'' de otro registro y luego un ''pop af'')
 + 
 +<code z80> 
 +    push af 
 +    pop bc        ; Ahora tenemos en el contenido de los Flags 
 +</code>
  
 \\  \\ 
 +  * El puntero de pila se puede utilizar también (como veremos más adelante en código más avanzado) para escribir (''PUSH'') o leer (''POP'') dos bytes de golpe con una única instrucción, permitiendo escribir por ejemplo rutinas de impresión de gráficos mucho más eficientes.
  
- Recordad también que tenéis instrucciones de intercambio (EX) que permiten manipular el contenido de la pila. Hablamos de:+\\  
 +  * Intercambiar valores de registros mediante ''PUSH'' y ''POP''. Por ejemplo, para intercambiar el valor de BC y de DE:
  
 <code z80> <code z80>
- EX (SP)HL +    ; Simulando "EX DEBC" 
- EX (SP), IX +     
- EX (SP), IY+    push bc          ; Apilamos BC 
 +    push de          ; Apilamos DE 
 +    pop bc           ; Desapilamos BC 
 +                     ; ahora BC=(valor apilado en push de
 +    pop de           ; Desapilamos DE 
 +                     ; ahora DE=(valor apilado en push bc)
 </code> </code>
  
 +Hemos usado el código anterior para ilustrar la posibilidad de intercambiar valores de registros usando exclusivamente la pila, aunque el siguiente código (que también la usa) es bastante más eficiente ya que la pareja ''PUSH''/''POP'' requiere 11+10 = 21 ciclos de reloj mientras que los dos ''LD'' cuestan apenas 4 ciclos de reloj:
 +
 +<code z80>
 +    ; Simulando "ex de, bc"
 +    push hl
 +    ld l, c
 +    ld h, b
 +    pop bc
 +</code>
 +
 +No sólo podemos intercambiar valores usando 2 ''PUSH'' y 2 ''POP'' sino que también existen las siguientes instrucciones de intercambio (''EX'') que permiten manipular el contenido de la pila:
 +
 +<code z80>
 +ex (sp), hl
 +ex (sp), ix
 +ex (sp), iy
 +</code>
 +
 +Con estas instrucciones podemos **simular** instrucciones que no existen en el Z80, como **EX BC, HL**, **EX BC, IX**, **EX BC, IY**, y otras combinaciones con diferentes registros.
 +
 +Por ejemplo, supongamos que queremos intercambiar el valor del registro BC con el del registro IX, sin usar dos ''PUSH''/''POP'' ni instrucciones ''LD'' de 8 bits:
 +
 +<code z80>
 +    push bc
 +    ex (sp), ix    ; IX = el valor de BC
 +    pop bc         ; BC = el valor de IX
 +</code>
 +   
 +Las siguientes instrucciones podrían ser simuladas con este método:
 + 
 +|< 50% 50% 50% >|
 +^ Instrucción incorrecta ^ Alternativa ^
 +| ex bc, hl | push bc\\ ex (sp), hl\\ pop bc |
 +| ex bc, ix | push bc\\ ex (sp), ix\\ pop bc |
 +| ex bc, iy | push bc\\ ex (sp), iy\\ pop bc |
 +| ex af, hl | push af\\ ex (sp), hl\\ pop af |
 +| ex af, ix | push af\\ ex (sp), ix\\ pop af |
 +| ex af, iy | push af\\ ex (sp), iy\\ pop af |
 +| ex de, ix | push de\\ ex (sp), ix\\ pop de |
 +| ex de, iy | push de\\ ex (sp), iy\\ pop de |
  
 \\  \\ 
 ===== Los peligros de la pila ===== ===== Los peligros de la pila =====
  
- +Pero como todo arma, las pilas también tienen un doble filo. Mal utilizada puede dar lugar a enormes desastres en nuestros programas.
- Pero como todo arma, las pilas también tienen un doble filo. Mal utilizada puede dar lugar a enormes desastres en nuestros programas.+
  
 Veamos algunos de los más habituales: Veamos algunos de los más habituales:
Línea 297: Línea 342:
   * Dado que la pila decrece en memoria, tenemos que tener cuidado con el valor de SP y la posición más alta de memoria donde hayamos almacenado datos o rutinas. Si ponemos un gráfico o una rutina cerca del valor inicial de SP, y realizamos muchas operaciones de PUSH, podemos sobreescribir nuestros datos con los valores que estamos apilando.   * Dado que la pila decrece en memoria, tenemos que tener cuidado con el valor de SP y la posición más alta de memoria donde hayamos almacenado datos o rutinas. Si ponemos un gráfico o una rutina cerca del valor inicial de SP, y realizamos muchas operaciones de PUSH, podemos sobreescribir nuestros datos con los valores que estamos apilando.
  
-  * Hacer más PUSH que POP o más POP que PUSH. Recordemos que la pila tiene que ser consistente. Si hacemos un push, debemos recordar hacer el pop correspondiente (a menos que haya una razón para ello), y viceversa. Como veremos a continuación, la pila es utilizada tanto para pasar parámetros a funciones como para volver de ellas, si introducimos un valor en ella con PUSH dentro de una función y no lo sacamos antes de hacer el RET, nuestro programa continuará su ejecución en algún lugar de la memoria que no era al que debía volver. Es más, si nuestro programa debe volver a BASIC correctamente tras su ejecución, entonces es obligatorio que hagamos tantos PUSH como POP para que el punto final de retorno del programa al BASIC esté en la siguiente posición de la pila cuando nuestro programa acabe.+  * Hacer más ''PUSH'' que ''POP'' o más ''POP'' que ''PUSH''. Recordemos que la pila tiene que ser consistente. Si hacemos un PUSH, debemos recordar hacer el pop correspondiente (a menos que haya una razón para ello), y viceversa. Como veremos a continuación, la pila es utilizada tanto para pasar parámetros a funciones como para volver de ellas, si introducimos un valor en ella con PUSH dentro de una función y no lo sacamos antes de hacer el ret, nuestro programa continuará su ejecución en algún lugar de la memoria que no era al que debía volver. Es más, si nuestro programa debe volver a BASIC correctamente tras su ejecución, entonces es obligatorio que hagamos tantos PUSH como POP para que el punto final de retorno del programa al BASIC esté en la siguiente posición de la pila cuando nuestro programa acabe.
  
-  * Ampliando la regla anterior, hay que tener cuidado con los bucles a la hora de hacer PUSH y POP. +  * Ampliando la regla anterior, hay que tener cuidado con los bucles a la hora de hacer ''PUSH'' ''POP''.
  
-  * Finalmente, no hay que asumir que SP tiene un valor correcto para nosotros. Tal vez tenemos planeado usar una zona de la memoria para guardar datos o subrutinas y el uso de PUSH y POP pueda sobreescribir estos datos. Si sabemos dónde no puede hacer daño SP y sus escrituras en memoria, basta con inicializar la pila al principio de nuestro programa a una zona de memoria libre (por ejemplo, "LD SP, 49999", o cualquier otra dirección que sepamos que no vamos a usar). Esto no es obligatorio y muchas veces el valor por defecto de SP será válido, siempre que no usemos zonas de la memoria que creemos libres como "almacenes temporales". Si usamos "variables" creadas en tiempo de ensamblado (definidas como DB o DW en el ensamblador) no deberíamos tener problemas, al menos con programas pequeños.+  * Finalmente, no hay que asumir que SP tiene un valor correcto para nosotros. Tal vez tenemos planeado usar una zona de la memoria para guardar datos o subrutinas y el uso de ''PUSH'' ''POP'' pueda sobreescribir estos datos. Si sabemos dónde no puede hacer daño SP y sus escrituras en memoria, basta con inicializar la pila al principio de nuestro programa a una zona de memoria libre (por ejemplo, ''ld sp, 49999'', o cualquier otra dirección que sepamos que no vamos a usar). Esto no es obligatorio y muchas veces el valor por defecto de SP será válido, siempre que no usemos zonas de la memoria que creemos libres como "almacenes temporales". Si usamos "variables" creadas en tiempo de ensamblado (definidas como DB o DW en el ensamblador) no deberíamos tener problemas, al menos con programas pequeños.
  
- Veamos algunos ejemplos de "errores" con la pila. Empecemos con el típico PUSH del cual se nos olvida hacer POP:+ Veamos algunos ejemplos de "errores" con la pila. Empecemos con el típico ''PUSH'' del cual se nos olvida hacer ''POP'':
  
 <code z80> <code z80>
-  ; Este programa se colgará (probablemente, depende de BC) +    ; Este programa se colgará (probablemente, depende de BC) 
-  ; pero en cualquier caso, no seguirá su ejecución normal. +    ; pero en cualquier caso, no seguirá su ejecución normal. 
-  PUSH BC +    push bc 
-  PUSH DE +    push de
-   +
-  (código)+
  
-  POP DE +    (código) 
-  RET          ; En lugar de volver a la dirección de memoria + 
-               ; a la que teníamos que volver, volveremos a +    pop de 
-               ; la dirección apuntada por el valor de BC, que +    ret         ; En lugar de volver a la dirección de memoria 
-               ; no hemos recogido de la pila.+                ; a la que teníamos que volver, volveremos a 
 +                ; la dirección apuntada por el valor de BC, que 
 +                ; no hemos recogido de la pila.
 </code> </code>
- +
  También hay que tener cuidado con los bucles:  También hay que tener cuidado con los bucles:
  
 <code z80> <code z80>
 bucle: bucle:
-    PUSH BC         ; Nos queremos guardar BC+    push bc         ; Nos queremos guardar BC 
     (código que usa B)     (código que usa B)
  
-    JR flag, bucle +    jr flag, bucle 
-    POP BC+    pop bc
 </code> </code>
  
- En ese código hacemos múltiples PUSHes pero un sólo POP. Probablemente, + En ese código hacemos múltiples PUSHes pero un sólo POP. Probablemente, en realidad, queremos hacer lo siguiente:
-en realidad, queremos hacer lo siguiente:+
  
 <code z80> <code z80>
 bucle: bucle:
-    PUSH BC         ; Nos queremos guardar BC+    push bc         ; Nos queremos guardar BC
     (código)     (código)
  
-    POP BC +    pop bc 
-    JR flag, bucle+    jr flag, bucle
 </code> </code>
  
Línea 346: Línea 391:
  
 <code z80> <code z80>
-    PUSH BC         ; Nos queremos guardar BC+    push bc         ; Nos queremos guardar BC 
 bucle: bucle:
     (código)     (código)
  
-    JR flag, bucle +    jr flag, bucle 
-    POP BC+    pop bc
 </code> </code>
  
- Y una curiosidad al respecto de la pila y la sentencia CLEAR de + Y una curiosidad al respecto de la pila y la sentencia ''CLEAR'' de BASIC: como ya vimos al principio del curso, lo que realiza la función CLEAR es cambiar el valor de la variable del sistema ''RAMTOP'', lo que implica cambiar el valor de SP. Así, con ''CLEAR XXXX'', //ponemos la pila colgando de la dirección de memoria XXXX-1//, asegurándonos de que BASIC no pueda hacer crecer la pila de forma que sobreescriba código máquina que hayamos cargado nosotros en memoria. Si, por ejemplo, vamos a cargar todo nuestro código a partir de la dirección 50000, en nuestro cargador BASIC haremos un ''CLEAR 49999'', de forma que BASIC no podrá tocar ninguna dirección de memoria por encima de este valor.
-BASIC: en el fondo, lo que realiza la función CLEAR es cambiar el valor +
-de la variable del sistema RAMTOP, lo que implica cambiar el valor de +
-SP. Así, con **CLEAR XXXX**, //ponemos la pila colgando de la dirección de +
-memoria XXXX//, asegurándonos de que BASIC no pueda hacer crecer la pila +
-de forma que sobreescriba código máquina que hayamos cargado nosotros en +
-memoria. Si, por ejemplo, vamos a cargar todo nuestro código a partir +
-de la dirección 50000, en nuestro cargador BASIC haremos un CLEAR 49999, de forma +
-que BASIC no podrá tocar ninguna dirección de memoria por encima de +
-este valor.+
  
 \\  \\ 
Línea 372: Línea 409:
 Si lo ponemos por encima, hay que estar alerta de que el stack, al crecer (por llamadas a subrutinas, y por múltiples PUSHs/POPs) no machaque el final de nuestro programa o datos, es decir, que no crezca de forma que pusheemos valores llegando a sobreescribir zonas de memoria con código o datos de nuestro programa. Si lo ponemos por encima, hay que estar alerta de que el stack, al crecer (por llamadas a subrutinas, y por múltiples PUSHs/POPs) no machaque el final de nuestro programa o datos, es decir, que no crezca de forma que pusheemos valores llegando a sobreescribir zonas de memoria con código o datos de nuestro programa.
  
-Si lo ponemos por debajo (con el ORG del programa por encima del inicio de la pila), ésta nunca pisará nuestro programa, pero podría (al "decrecer") sobreescribir zonas de memoria con las variables de BASIC. Esto puede no importarnos si no usamos para nada llamadas a la ROM (o usamos llamadas de la ROM que no usan las variables del sistema), y si no usamos nosotros las propias variables del sistema y no pretendemos volver al BASIC tras la ejecución de nuestro programa.+Si lo ponemos por debajo (con el ''ORG'' del programa por encima del inicio de la pila), ésta nunca pisará nuestro programa, pero podría (al "decrecer") sobreescribir zonas de memoria con las variables de BASIC. Esto puede no importarnos si no usamos para nada llamadas a la ROM (o usamos llamadas de la ROM que no usan las variables del sistema), y si no usamos nosotros las propias variables del sistema y no pretendemos volver al BASIC tras la ejecución de nuestro programa.
  
-Lo más sencillo es usar CLEAR x-1 (siendo x la dirección de inicio de nuestro programa) en el cargador BASIC y no tocar desde ASM el valor de SP, dejando que decrezca por debajo de nuestro programa. Nótese que si hacemos esto, y cargamos nuestro programa por ejemplo en 32768 ($6000), tendremos la pila en Contended Memory (ahora veremos qué es esto).+Lo más sencillo es usar ''CLEAR x-1'' (siendo x la dirección de inicio de nuestro programa) en el cargador BASIC y no tocar desde ASM el valor de SP, dejando que decrezca por debajo de nuestro programa. Nótese que si hacemos esto, y cargamos nuestro programa por ejemplo en 32768 ($6000), tendremos la pila en Contended Memory, algo que no es deseable como veremos a continuación.
  
-Otra opción es poner SP a 0, con lo que decrecerá desde el final de la RAMRecuerda que cuando hacemos PUSHprimero se decrementa SP y luego se guardan los valores en memoriapor lo que SP = 0 usará $FFFE para el byte menos significativo $FFFF para el más significativo (recordemos que el Z80 es Low-Endian).+Desde luego, la opción más segura es iniciar nuestro programa en una dirección de memoria tal que haya suficiente espacio para que la pila quepa bajo él (colgando de nuestra dirección de inicio), entre el inicio del programa y el fin de BASICComo ya vimospara eso se recomendaba un valor mínimo de ''ORG 33500'' o ''ORG 34000''. Esto nos dejará un tamaño de la pila suficientecon los primeros 708 bytes (ORG 33500) 1232 bytes (ORG 34000fuera de la Contended Memory. Todas estas consideraciones se explican en el capítulo dedicado a Consideraciones Avanzadas.
  
-Una opción más segura es acotar en nuestro programa un espacio con DB / DS donde alojar la pila, en cualquier punto del mismo (al principio, al final, o en medio, no importa, siempre que no nos salgamos con PUSHes del espacio que le hemos dejado).+Otra opción segura es acotar en nuestro programa un espacio con DB / DS donde alojar la pila, en cualquier punto del mismo (al principio, al final, o en medio, no importa, siempre que no nos salgamos con PUSHes del espacio que le hemos dejado). En ese caso el problema es que ese "buffer" ocupa espacio en el ejecutable y por tanto "tiempo de carga".
  
-Hay un caso en el que puede ser importante dónde ubiquemos la pila, y es si estás utilizándola para pintado rápido de pantallas porque estás usando estrategias de atributos de 8x2 u 8x1. En ese caso seguramente es necesario evitar que la pila esté en contended memory".+La última opción es poner SP a 0, con lo que decrecerá desde el final de la RAM. Recuerda que cuando hacemos PUSHprimero se decrementa SP luego se guardan los valores en memoria, por lo que SP = 0 usará $fffe para el byte menos significativo y $ffff para el más significativo (recordemos que el Z80 es Low-Endian). 
 + 
 +Pero si el target de nuestro programa es un modelo 128K y vamos a paginar, entonces el stack tiene que estar por debajo de $c000 ya que si no, al cambiar de banco lo perderíamos hasta volver al mismo (a menos que tengamos controlado que nuestro código no va a hacer ningún PUSH/pop hasta volver a poner el banco que tenía la pila, y además tengamos deshabilitadas las interrupciones). 
 + 
 +Por lo tanto, mantenemos la recomendación que hicimos en los primeros capítulos del curso de dejar la pila por debajo de nuestro programa, con un ''ORG 33500'' o ''34000'' para modelos 48K y 128K.
  
-Finalmente, si el target de nuestro programa es un modelo 128K y vamos a paginar, entonces el stack tiene que estar por debajo de $C000 ya que si no, al cambiar de banco lo perderíamos hasta volver al mismo (a menos que tengamos controlado que nuestro código no va a hacer ningún PUSH/POP hasta volver a poner el banco que tenía la pila, y además tengamos deshabilitadas las interrupciones). 
  
 \\  \\ 
Línea 399: Línea 439:
  Esto es lo que se conoce como "contented memory" o "memoria en contienda".  Esto es lo que se conoce como "contented memory" o "memoria en contienda".
  
- Esto implica que las lecturas y escrituras de nuestro programa (ejecutado por el Z80) en la página de memoria de 16KB que va desde 16384 a 32767 se ven interrumpidas de forma constante por la ULA (aunque de forma transparente para nuestro programa), por lo que ubicar la pila en esta zona puede suponer una ralentización con respecto a ubicarla más arriba de la dirección 32768. Recuerda que cada operación PUSH y POP es, físicamente, un acceso de escritura y lectura a memoria, y las rutinas de nuestro programa harán, seguro, gran uso de ellas, además de los CALLs RETs (PUSH PC + JP DIR / POP PC).+ Esto implica que las lecturas y escrituras de nuestro programa (ejecutado por el Z80) en la página de memoria de 16KB que va desde 16384 a 32767 se ven interrumpidas de forma constante por la ULA (aunque de forma transparente para nuestro programa), por lo que ubicar la pila en esta zona puede suponer una ralentización con respecto a ubicarla más arriba de la dirección 32768. Recuerda que cada operación ''PUSH'' ''POP'' es, físicamente, un acceso de escritura y lectura a memoria, y las rutinas de nuestro programa harán, seguro, gran uso de ellas, además de los ''CALL''''RET''(''PUSH PC'' ''JP DIR'' ''POP PC'').
  
  Por lo tanto, si establecemos la pila por debajo de nuestro programa, y tenemos nuestro programa en 32768, tendremos la pila en contended memory, lo cual implica que funcionará un poco más lenta en general que tener la pila por encima.  Por lo tanto, si establecemos la pila por debajo de nuestro programa, y tenemos nuestro programa en 32768, tendremos la pila en contended memory, lo cual implica que funcionará un poco más lenta en general que tener la pila por encima.
  
- Esto no tiene por qué ser un problema (de hecho, puede ser inapreciable) salvo que tengamos que hacer rutinas muy precisas o que estemos desarrollando un juego y necesitemos arañar hasta el último ciclo de reloj. Además, el problema no es que un PUSH/POP sea unos ciclos de reloj más lento en memoria contenida, sino que será más lento A VECES (de forma impredecible). Unas veces (cuando esté la ULA leyendo la VRAM) tardará unos ciclos más en hacer push/pop y otras veces (cuando no esté leyendo la VRAM) tardará menos, y no podemos saber cuándo estamos en un caso y cuando en otro, por lo que no es seguro hacer cosas de precisión / sincronización con la pila ahí.+ Esto no tiene por qué ser un problema (de hecho, puede ser inapreciable) salvo que tengamos que hacer rutinas muy precisas o que estemos desarrollando un juego y necesitemos arañar hasta el último ciclo de reloj. Además, el problema no es que un PUSH/POP sea unos ciclos de reloj más lento en "contended memory", sino que será más lento A VECES (de forma impredecible). Unas veces (cuando esté la ULA leyendo la VRAM) tardará unos ciclos más en hacer push/pop y otras veces (cuando no esté leyendo la VRAM) tardará menos, y no podemos saber cuándo estamos en un caso y cuando en otro, por lo que no es seguro hacer cosas de precisión / sincronización con la pila ahí.
  
- Otro punto que puede afectar a esto es si tenemos la pila en la contended memory y estamos haciendo un programa en C puro con Z88DK o en C con funciones en ensamblador, ya que el compilador de C pasa los parámetros de llamada de las funciones a través de la pila, por lo que se hace mucho uso de PUSH/POP. De hecho, el compilador Z88DK pone el inicio de la pila por defecto (creciendo habia abajo) en 65367 ($FF58) (reservando un total de 512 bytes para ella)+ Otro punto que puede afectar a esto es si tenemos la pila en la contended memory y estamos haciendo un programa en C puro con Z88DK o en C con funciones en ensamblador, ya que el compilador de C pasa los parámetros de llamada de las funciones a través de la pila, por lo que se hace mucho uso de PUSH/POP. De hecho, el compilador Z88DK pone el inicio de la pila por defecto (creciendo habia abajo) en 65367 ($ff58) (reservando un total de 512 bytes para ella).
- +
- No debemos obsesionarnos con el hecho de que la pila esté en la memoria contenida, pero está bien saberlo y tenerlo en cuenta.+
  
 + No debemos obsesionarnos con el hecho de que la pila esté en la memoria en contienda, pero está bien saberlo y tenerlo en cuenta.
  
 \\  \\ 
-===== 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.+===== Subrutinas: CALL y RET =====
  
-Este área son 256 bytesdesde 23296 (5B00h) a 23551 (5BFFh).+ Ya de por sí el lenguaje ensamblador es un lenguaje de listados "largos" y enrevesadosy donde teníamos 10 líneas en BASIC podemos tener 100 ó 1000 en ensamblador.
  
-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.+ Lo normal para hacer el programa más legible es //utilizar bloques de código que hagan unas funciones concretas// y a los cuales podamos llamar a lo largo de nuestro programa. Esos bloques de código son las **funciones** o **subrutinas**.
  
-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 RAMteniendo en cuenta 2 cosas:+ Las subrutinas son bloques de código máquina a las cuales saltamoshacen su tarea asignada, y devuelven el control al punto en que fueron llamadas. A veces, esperan recibir los registros con una serie de valores y devuelven registros con los valores resultantes.
  
-1.- Está en la Contented Memoryaunque no es un problema grande para lo que es el acceso puntual a variables.+ Para saltar a subrutinas utilizamos la instrucción ''CALL''y estas deben de terminar en un ''RET''.
  
-2.- Los modelos de 128 tienen ahí rutinas de paginación de las ROM de sintaxis 128 también partes del intérpretes de 48. Si pretendemos volver al BASIC después de ejecutar nuestro programa, no podemos tocar ese área.+ El lector podría preguntar, ¿por qué no utilizar las instrucciones de salto ''JP'' ''JR'' vistas hasta ahora? La respuesta es: debido a la necesidad de una dirección de retorno.
  
-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. + Veamos un ejemplo ilustrativo de la importancia de call/ret realizando una subrutina que se utilice ''JP'' para su llamada. Supongamos la siguiente "subrutina" sin ''RET'':
- +
-\\  +
-===== Subrutinas: CALL y RET ===== +
- +
- Ya de por sí el lenguaje ensamblador es un lenguaje de listados "largos" +
-y enrevesados, y donde teníamos 10 líneas en BASIC podemos tener 100 ó 1000 +
-en ensamblador. +
- +
- Lo normal para hacer el programa más legible es //utilizar bloques de código +
-que hagan unas funciones concretas// y a los cuales podamos llamar a lo largo +
-de nuestro programa. Esos bloques de código son las **funciones** o **subrutinas**. +
- +
- Las subrutinas son bloques de código máquina a las cuales saltamos, hacen +
-su tarea asignada, y devuelven el control al punto en que fueron llamadas. +
-A veces, esperan recibir los registros con una serie de valores y devuelven +
-registros con los valores resultantes. +
- +
- Para saltar a subrutinas utilizamos la instrucción **CALL**, y estas deben de terminar en un **RET**.  +
- +
- El lector podría preguntar, ¿por qué no utilizar las instrucciones de salto JP y JR vistas hasta ahora? La respuesta es: debido a la necesidad de una dirección de retorno. +
-  +
- Veamos un ejemplo ilustrativo de la importancia de CALL/RET realizando una subrutina que se utilice JP para su llamada. Supongamos la siguiente "subrutina" sin RET:+
  
 <code z80> <code z80>
Línea 457: Línea 473:
  
 SUMA_A_10: SUMA_A_10:
-   ADD A, 10         ; A = A + 10 +     add a, 10         ; A = A + 10 
-   LD B          ; B = A+     ld b          ; B = A
 </code> </code>
  
- Nuestra función/subrutina de ejemplo espera obtener en A un valor, y devuelve + Nuestra función/subrutina de ejemplo espera obtener en A un valor, y devuelve el resultado de su ejecución en B. Antes de llamar a esta rutina, nosotros deberemos poner en A el valor sobre el que actuar, y posteriormente interpretar el resultado (sabiendo que lo tenemos en B).
-el resultado de su ejecución en B. Antes de llamar a esta rutina, nosotros +
-deberemos poner en A el valor sobre el que actuar, y posteriormente interpretar +
-el resultado (sabiendo que lo tenemos en B).+
  
- Pero, ¿cómo llamamos a las subrutinas y volvemos de ellas? Comencemos probando con "JP":+ Pero, ¿cómo llamamos a las subrutinas y volvemos de ellas? Comencemos probando con ''JP'':
  
 <code z80> <code z80>
-    LD A, 35 +    ld a, 35 
-    JP SUMA_A_10+    jp SUMA_A_10
 volver1: volver1:
-   + 
-   (...)+    (...)
  
 ; SUMA_A_10 ; SUMA_A_10
Línea 479: Línea 492:
 ; Nota: Modifica el valor de A ; Nota: Modifica el valor de A
 SUMA_A_10: SUMA_A_10:
-   ADD A, 10         ; A = A + 10 +    add a, 10         ; A = A + 10 
-   LD B          ; B = A +    ld b          ; B = A 
-   JP volver1        ; Volvemos de la subrutina+    jp volver1        ; Volvemos de la subrutina
 </code> </code>
  
- En este caso, cargaríamos A con el valor 35, saltaríamos a la subrutina, + En este caso, cargaríamos A con el valor 35, saltaríamos a la subrutina, sumaríamos 10 a A (pasando a valer 45), haríamos B = 45, y volveríamos al lugar posterior al punto de llamada. 
-sumaríamos 10 a A (pasando a valer 45), haríamos B = 45, y volveríamos al + 
-lugar posterior al punto de llamada. + Pero ... ¿qué pasaría si quisieramos volver a llamar a la subrutina desde otro punto de nuestro programa? Que sería inviable, porque nuestra subrutina acaba con un ''jp volver1'' que no devolvería la ejecución al punto desde donde la hemos llamado, sino a "volver1".
-  +
- Pero ... ¿qué pasaría si quisieramos volver a llamar a la subrutina desde +
-otro punto de nuestro programa? Que sería inviable, porque nuestra subrutina +
-acaba con un "JP volver1que no devolvería la ejecución al punto desde donde +
-la hemos llamado, sino a "volver1".+
  
 <code z80> <code z80>
-    LD A, 35 +    ld a, 35 
-    JP SUMA_A_10+    jp SUMA_A_10
 volver1: volver1:
  
-    LD A, 50 +    ld a, 50 
-    JP SUMA_A_10+    jp SUMA_A_10
                      ; Nunca llegariamos a volver aqui                      ; Nunca llegariamos a volver aqui
-   (...)+    (...)
 SUMA_A_10: SUMA_A_10:
-   ADD A, 10         ; A = A + 10 +    add a, 10         ; A = A + 10 
-   LD B          ; B = A +    ld b          ; B = A 
-   JP volver1        ; Volvemos de la subrutina+    jp volver1        ; Volvemos de la subrutina
 </code> </code>
  
- Para evitar ese enorme problema es para lo que se usa **CALL** y **RET**.+ Para evitar ese enorme problema es para lo que se usa **call** y **ret**.
  
  
Línea 514: Línea 522:
 ===== Uso de CALL y RET ===== ===== Uso de CALL y RET =====
  
- **CALL** es, en esencia, similar a JP, salvo porque antes de realizar el salto, + ''CALL'' es, en esencia, similar a jp, salvo porque antes de realizar el salto, introduce en la pila (''PUSH'') el valor del registro **PC** (Program Counter, o contador de programa), el cual (una vez leída y decodificada la instrucción ''CALL'') apunta a la instrucción que sigue al ''CALL''.
-introduce en la pila (PUSH) el valor del registro PC (Program Counter, o  +
-contador de programa), el cual (una vez leída y decodificada la instrucción CALL) apunta +
-a la instrucción que sigue al CALL.+
  
- ¿Y para qué sirve eso? Para que lo aprovechemos dentro de nuestra subrutina + ¿Y para qué sirve eso? Para que lo aprovechemos dentro de nuestra subrutina con ''RET''ret lee de la pila la dirección que introdujo call y salta a ella. Así, cuando acaba nuestra función, el ret devuelve la ejecución a la instrucción siguiente al call que hizo la llamada.
-con **RET**RET lee de la pila la dirección que introdujo CALL y salta a ella. +
-Así, cuando acaba nuestra función, el RET devuelve la ejecución a la instrucción +
-siguiente al CALL que hizo la llamada.+
  
- Son, por tanto, el equivalente ensamblador de GO SUB y RETURN en BASIC (o + Son, por tanto, el equivalente ensamblador de ''GO SUB'' ''RETURN'' en BASIC (o más bien se debería decir que GO SUB y RETURN son la implantación en BASIC de estas instrucciones del microprocesador).
-más bien se debería decir que GO SUB y RETURN son la implantación en BASIC +
-de estas instrucciones del microprocesador).+
  
 <code> <code>
- CALL NN equivale a:+call NN equivale a:
     PUSH PC     PUSH PC
-    JP NN +    jp NN 
-    + 
- RET equivale a:+ret equivale a:
     POP PC     POP PC
 </code> </code>
  
- Veamos la aplicación de CALL y RET con nuestro ejemplo anterior:+ Veamos la aplicación de ''CALL'' ''RET'' con nuestro ejemplo anterior:
  
 <code z80> <code z80>
-    LD A, 35 +    ld a, 35 
-    CALL SUMA_A_10+    call SUMA_A_10
  
-    LD A, 50 +    ld a, 50 
-    CALL SUMA_A_10+    call SUMA_A_10
  
-    LD CB+    ld cb
  
-   (...)+    (...)
  
 SUMA_A_10: SUMA_A_10:
-   ADD A, 10         ; A = A + 10 +    add a, 10         ; A = A + 10 
-   LD B          ; B = A +    ld b          ; B = A 
-   RET               ; Volvemos de la subrutina+    ret               ; Volvemos de la subrutina
 </code> </code>
  
- En esta ocasión, cuando ejecutamos el primer CALL, se introduce en la pila + En esta ocasión, cuando ejecutamos el primer ''CALL'', se introduce en la pila el valor de PC, que se corresponde exáctamente con la dirección de memoria donde estaría ensamblada la siguiente instrucción (''ld a, 50''). El ''CALL'' cambia el valor de PC al de la dirección de ''SUMA_A_10'', y se continúa la ejecución dentro de la subrutina.
-el valor de PC, que se corresponde exáctamente con la dirección de memoria +
-donde estaría ensamblada la siguiente instrucción (LD A, 50). El CALL cambia +
-el valor de PC al de la dirección de "SUMA_A_10", y se continúa la ejecución +
-dentro de la subrutina.+
  
- Al acabar la subrutina encontramos el RET, quien extrae de la pila el valor + Al acabar la subrutina encontramos el ''RET'', quien extrae de la pila el valor de PC anteriormente introducido, con lo que en el siguiente ciclo de instrucción del microprocesador, el Z80 leerá, decodificará y ejecutará la instrucción ''ld a, 50'', siguiendo el flujo del programa linealmente desde ahí. Con la segunda llamada a ''CALL'' ocurriría lo mismo, pero esta vez lo que se introduce en la pila es la dirección de memoria en la que está ensamblada la instrucción ''ld cb''. Esto asegura el retorno de nuestra subrutina al punto adecuado.
-de PC anteriormente introducido, con lo que en el siguiente ciclo de +
-instrucción del microprocesador, el Z80 leerá, decodificará y ejecutará +
-la instrucción "LD A, 50", siguiendo el flujo del programa linealmente desde ahí. +
-Con la segunda llamada a CALL ocurriría lo mismo, pero esta vez lo que se +
-introduce en la pila es la dirección de memoria en la que está ensamblada +
-la instrucción "LD CB". Esto asegura el retorno de nuestra subrutina al +
-punto adecuado.+
  
- Al hablar de la pila os contamos lo importante que era mantener la misma + Al hablar de la pila os contamos lo importante que era mantener la misma cantidad de PUSH que de POPs en nuestro código. Ahora entenderéis por qué: si dentro de una subrutina hacéis un ''PUSH'' que no elimináis después con un ''POP'', cuando lleguéis al ''RET'' éste obtendrá de la pila un valor que no será el introducido por ''CALL'' (sino el introducido por el ''PUSH''), y saltará a esa dirección incorrecta. Por ejemplo:
-cantidad de PUSH que de POPs en nuestro código. Ahora entenderéis por qué: +
-si dentro de una subrutina hacéis un PUSH que no elimináis después con un +
-POP, cuando lleguéis al RET éste obtendrá de la pila un valor que no será +
-el introducido por CALL, y saltará allí. Por ejemplo:+
  
 <code z80> <code z80>
-  CALL SUMA_A_10 +    call SUMA_A_10 
-  LD C             ; Esta dirección se introduce en la pila con CALL+    ld c             ; Esta dirección se introduce en la pila con call
  
 SUMA_A_10: SUMA_A_10:
-  LD DE, $0000 +    ld de, $0000 
-  PUSH DE +    push de 
-  ADD A, 10 +    add a, 10 
-  LD B, a +    ld b, a 
-  RET                  ; RET no sacará de la pila lo introducido por CALL +    ret                  ; ret no sacará de la pila lo introducido por call 
-                       ; sino "0000", el valor que hemos pulsado nosotros.+                         ; sino "0000", el valor que hemos pulsado nosotros.
 </code> </code>
  
- Aquí RET sacará de la pila 0000h, en lugar de la dirección que introdujo + Aquí ''RET'' sacará de la pila 0000h, en lugar de la dirección que introdujo ''CALL'', y saltará al inicio del a ROM, produciendo un bonito reset.
-CALL, y saltará al inicio del a ROM, produciendo un bonito reset.+
  
- Ni CALL ni RET afectan a la tabla de flags del registro F.+ Ni ''CALL'' ni ''RET'' afectan a la tabla de flags del registro F.
  
 <code> <code>
-                        Flags +                        Flags
    Instrucción       |S Z H P N C|    Instrucción       |S Z H P N C|
  ----------------------------------  ----------------------------------
- CALL NN             |- - - - - -| + call NN             |- - - - - -| 
- RET                 |- - - - - -|+ ret                 |- - - - - -|
 </code> </code>
  
Línea 608: Línea 592:
 La instrucción **RST** es un resquicio de la compatibilidad que el Z80 tiene con el procesador 8080. La instrucción **RST** es un resquicio de la compatibilidad que el Z80 tiene con el procesador 8080.
  
-RST ejecuta un CALL como el que ya hemos visto, pero permitiendo un salto sólo a una serie de direcciones en el bloque desde la dirección 0 a la dirección 255. **RST $NN** es, literalmente, un **CALL $00NN**. +''RST'' ejecuta un ''CALL'' como el que ya hemos visto, pero permitiendo un salto sólo a una serie de direcciones en el bloque desde la dirección 0 a la dirección 255. **RST $NN** es, literalmente, un **call $00NN**.
  
 Las siguientes instrucciones son equivalentes en cuanto a resultado de la ejecución: Las siguientes instrucciones son equivalentes en cuanto a resultado de la ejecución:
Línea 614: Línea 598:
 |< 40% 50% 50% >| |< 40% 50% 50% >|
 ^ CALL ^ RST ^ ^ CALL ^ RST ^
-CALL $0000 | RST $00 | +call $0000 | rst $00 | 
-CALL $0008 | RST $08 | +call $0008 | rst $08 | 
-CALL $0010 | RST $10 | +call $0010 | rst $10 | 
-CALL $0018 | RST $18 | +call $0018 | rst $18 | 
-CALL $0020 | RST $20 | +call $0020 | rst $20 | 
-CALL $0028 | RST $28 | +call $0028 | rst $28 | 
-CALL $0030 | RST $30 | +call $0030 | rst $30 | 
-CALL $0038 | RST $38 |+call $0038 | rst $38 |
  
-La principal ventaja de RST es que ocupa un sólo byte (cada uno de los 8 RST tiene su propio opcode de 1 byte asociado).+La principal ventaja de ''RST'' es que ocupa un sólo byte (cada uno de los 8 RST tiene su propio opcode de 1 byte asociado).
  
 <code z80> <code z80>
-  RST 0      ; Opcode C7 (11 T-estados). +  rst 0      ; Opcode C7 (11 T-estados). 
-  RST 8      ; Opcode CF (11 T-estados). +  rst 8      ; Opcode CF (11 T-estados). 
-  RST 10h    ; Opcode D7 (11 T-estados). +  rst 10h    ; Opcode D7 (11 T-estados). 
-  RST 18h    ; Opcode DF (11 T-estados). +  rst 18h    ; Opcode DF (11 T-estados). 
-  RST 20h    ; Opcode E7 (11 T-estados). +  rst 20h    ; Opcode E7 (11 T-estados). 
-  RST 28h    ; Opcode EF (11 T-estados). +  rst 28h    ; Opcode EF (11 T-estados). 
-  RST 30h    ; Opcode F7 (11 T-estados). +  rst 30h    ; Opcode F7 (11 T-estados). 
-  RST 38h    ; Opcode FF (11 T-estados).+  rst 38h    ; Opcode FF (11 T-estados).
 </code> </code>
  
-Por contra, CALL ocupa 3 bytes en memoria ($CD NN NN).+Por contra, call ocupa 3 bytes en memoria ($cd NN NN).
  
 La ventaja de tener estas instrucciones de salto de 1 sólo byte es que un programador puede colocar en estas direcciones rutinas que sean muy comunes de usar ($0008, $0010, etc), ahorrando 2 bytes en cada llamada que después se hagan a ellas. La ventaja de tener estas instrucciones de salto de 1 sólo byte es que un programador puede colocar en estas direcciones rutinas que sean muy comunes de usar ($0008, $0010, etc), ahorrando 2 bytes en cada llamada que después se hagan a ellas.
  
-En el caso del Spectrum, estas direcciones de memoria caen en la ROM (no así en otros ordenadores que tienen la ROM al final, por ejemplo), por lo que no las podemos aprovechar en nuestros programas, aunque ya lo hicieron por nosotros los diseñadores de la ROM del Spectrum al colocar en esas direcciones de salto puntos de entrada a rutinas tan comunes como RST 16 (RST $10) que sirve, como ya hemos visto, para imprimir un carácter.+En el caso del Spectrum, estas direcciones de memoria caen en la ROM (no así en otros ordenadores que tienen la ROM al final, por ejemplo), por lo que no las podemos aprovechar en nuestros programas, aunque ya lo hicieron por nosotros los diseñadores de la ROM del Spectrum al colocar en esas direcciones de salto puntos de entrada a rutinas tan comunes como rst 16 (rst $10) que sirve, como ya hemos visto, para imprimir un carácter.
  
 \\  \\ 
 ===== Saltos y retornos condicionales ===== ===== Saltos y retornos condicionales =====
  
- Una de las peculiaridades de CALL RET es que tienen instrucciones condicionales + Una de las peculiaridades de call ret es que tienen instrucciones condicionales con respecto al estado de los flags, igual que ''jp cc'' ''jr cc'', de forma que podemos condicionar el SALTO (call) o el retorno (ret) al estado de un determinado flag.
-con respecto al estado de los flags, igual que "JP cc"JR cc", de forma que +
-podemos condicionar el SALTO (CALL) o el retorno (RET) al estado de un determinado +
-flag.+
  
  Para eso, utilizamos las siguientes instrucciones:  Para eso, utilizamos las siguientes instrucciones:
  
 \\   \\  
-  * **CALL flag, NN** :  Salta sólo si FLAG está activo. +  * **call flag, NN** :  Salta sólo si FLAG está activo. 
-  * **RET flag** : Vuelve sólo si FLAG está activo.+  * **ret flag** : Vuelve sólo si FLAG está activo.
 \\  \\ 
  
- Por ejemplo, supongamos que una de nuestras subrutinas tiene que comprobar + Por ejemplo, supongamos que una de nuestras subrutinas tiene que comprobar que uno de los parámetros que le pasamos, BC, no sea 0.
-que uno de los parámetros que le pasamos, BC, no sea 0.+
  
 <code z80> <code z80>
Línea 670: Línea 650:
 Copia_Pantalla: Copia_Pantalla:
  
-   ; lo primero, comprobamos que BC no sea cero: +    ; lo primero, comprobamos que BC no sea cero: 
-   LD AB +    ld ab 
-   OR C                           ; Hacemos un OR de B sobre C +    or c                           ; Hacemos un OR de B sobre C 
-                                  ; Si BC es cero, activará el flag Z +                                   ; Si BC es cero, activará el flag Z 
-   RET Z                          ; Si BC es cero, volvemos sin hacer nada+    ret z                          ; Si BC es cero, volvemos sin hacer nada
  
-   (más código) +    (más código) 
-   ; Aquí seguiremos si BC no es cero, el  +    ; Aquí seguiremos si BC no es cero, el ret no se habrá ejecutado.
-   ; RET no se habrá ejecutado.+
 </code> </code>
  
- Del mismo modo, el uso de CALL condicionado al estado de flags + Del mismo modo, el uso de ''CALL'' condicionado al estado de flags (''CALL Z''''CALL NZ''''CALL M''''CALL P'', etc) nos permitirá llamar o no a funciones según el estado de un flag.
-(CALL Z, CALL NZ, CALL M, CALL P, etc) nos permitirá llamar o no a +
- funciones según el estado de un flag.+
  
- Al igual que CALL y RET, sus versiones condicionales no afectan al estado + Al igual que ''CALL'' ''RET'', sus versiones condicionales no afectan al estado de los flags.
-de los flags.+
  
 <code> <code>
-                        Flags +                        Flags
    Instrucción       |S Z H P N C|         Pseudocodigo    Instrucción       |S Z H P N C|         Pseudocodigo
  -----------------------------------------------------------  -----------------------------------------------------------
- CALL cc, NN         |- - - - - -|        IF cc CALL NN + call cc, NN         |- - - - - -|        IF cc call NN 
- RET cc              |- - - - - -|        IF cc RET+ ret cc              |- - - - - -|        IF cc ret
 </code> </code>
  
Línea 700: Línea 676:
 ===== Pasando parametros a rutinas ===== ===== Pasando parametros a rutinas =====
  
- Ahora que ya sabemos crear rutinas y utilizarlas, vamos a ver los 3 + Ahora que ya sabemos crear rutinas y utilizarlas, vamos a ver los 3 métodos que hay para pasar y devolver parámetros a las funciones.
-métodos que hay para pasar y devolver parámetros a las funciones.+
  
 \\  \\ 
 ==== Método 1: Uso de registros ==== ==== Método 1: Uso de registros ====
  
- Este método consiste en modificar unos registros concretos antes + Este método consiste en modificar unos registros concretos antes de hacer el ''CALL'' a nuestra subrutina, sabiendo que dicha subrutina espera esos registros con los valores sobre los que actuar. Asímismo, nuestra rutina puede modificar alguno de los registros con el objetivo de devolvernos un valor. Si modifica algún registro en el transcurso de la rutina, lo normal es preservarlo con un PUSH 
-de hacer el CALL a nuestra subrutina, sabiendo que dicha subrutina + 
-espera esos registros con los valores sobre los que actuar. Asímismo, + Este método es el más habitual en los programas en ensamblador siempre y cuando no tengamos más parámetros de entrada a la rutina que registros existentes en el Z80.
-nuestra rutina puede modificar alguno de los registros con el objetivo +
-de devolvernos un valor.+
  
  Por ejemplo:  Por ejemplo:
Línea 716: Línea 689:
 <code z80> <code z80>
 ;-------------------------------------------------------------- ;--------------------------------------------------------------
-MULTIPLI: Multiplica DE*BC +Mult_HL_DE: Multiplica DE*BC 
-      Entrada:        DE: Multiplicando,   +; 
-                      BC: Multiplicador +Entrada:        DE: Multiplicando, 
-      Salida:         HL: Resultado.+                BC: Multiplicador 
 +; Salida:         HL: Resultado. 
 +; Modifica:       Ningun registro aparte de HL
 ;-------------------------------------------------------------- ;--------------------------------------------------------------
-MULTIPLICA+Mult_HL_DE
-        LD HL, 0 +    push af             ; Preservamos AF porque F se va a modificar 
-MULTI01+    push bc             ; Preservamos BC porque su valor se pierde 
-        ADD HLDE +    ld hl, 0 
-        DEC BC + 
-        LD AB +multiloop_01
-        OR C +    add hlde 
-        JR NZMULTI01 +    dec bc 
-        RET+    ld ab 
 +    or c 
 +    jr nzmultiloop_01 
 + 
 +    pop bc              ; Rescatamos el valor de BC 
 +    pop af              ; Rescatamos el valor de AF 
 +    ret
 </code> </code>
  
- Antes de hacer la llamada a MULTIPLICA, tendremos que cargar en DE y en BC + Antes de hacer la llamada a ''Mult_HL_DE'', tendremos que cargar en DE y en BC los valores que queremos multiplicar, de modo que si estos valores están en otros registros o en memoria, tendremos que moverlos a DE y BC.
-los valores que queremos multiplicar, de modo que si estos valores están +
-en otros registros o en memoria, tendremos que moverlos a DE y BC.+
  
- Además, sabemos que la salida nos será devuelta en HL, con lo que si dicho + Además, sabemos que la salida nos será devuelta en HL, con lo que si dicho registro contenía algún valor importante y que no debemos perder en el código que llama a la rutina, deberemos preservarlo previamente.
-registro contiene algún valor importante, deberemos preservarlo previamente.+
  
- Con este tipo de funciones resulta importantísimo realizarse cabeceras de + Con este tipo de funciones resulta importantísimo realizarse cabeceras de comentarios explicativos, que indiquen:
-comentarios explicativos, que indiquen:+
  
 a.- Qué función realiza la subrutina.\\  a.- Qué función realiza la subrutina.\\ 
Línea 747: Línea 724:
 d.- Qué registros modifica además de los de entrada y salida.\\  d.- Qué registros modifica además de los de entrada y salida.\\ 
  
- Con este tipo de paso de parámetros tenemos el mayor ahorro y la mayor + Con este tipo de paso de parámetros tenemos el mayor ahorro y la mayor velocidad: no se accede a la memoria, no se usa la pila (salvo para preservar parámetros), pero por contra tenemos que tener un gran control del "contexto" de la llamada. Tendremos que saber en cada momento qué parámetros de entrada y de salida utiliza (de ahí la importancia del comentario explicativo o de nuestra documentación del programa, al que acudiremos más de una vez cuando no recordemos en qué registros teníamos que pasarle los datos de entrada), y asegurarnos de que ninguno de los registros "extra" que modifica están en uso antes de llamar a la función, puesto que se verán alterados.
-velocidad: no se accede a la pila y no se accede a la memoria, pero por +
-contra tenemos que tenerlo todo controlado. Tendremos que saber en cada +
-momento qué parámetros de entrada y de salida utiliza (de ahí la importancia +
-del comentario explicativo, al que acudiremos más de una vez cuando no +
-recordemos en qué registros teníamos que pasarle los datos de entrada), y +
-asegurarnos de que ninguno de los registros "extra" que modifica están en +
-uso antes de llamar a la función, puesto que se verán alterados.+
  
- Si no queremos que la función modifique muchos registros además de los + Como hemos visto, si queremos preservar el valor de los registros modificados por la rutina, siempre podemos poner una serie de PUSH y POP en su inicio y final, al estilo:
-de entrada y salida, siempre podemos poner una serie de PUSH y POP en +
-su inicio y final, al estilo:+
  
 <code z80> <code z80>
 MiFuncion: MiFuncion:
-   PUSH BC +    push bc 
-   PUSH DE      ; Nos guardamos sus valores+    push de      ; Nos guardamos sus valores
  
-   (...)+    (...)
  
-   POP DE +    pop de 
-   POP BC       ; Recuperamos sus valores +    pop bc       ; Recuperamos sus valores 
-   RET+    ret
 </code> </code>
  
- En funciones que no sean críticas en velocidad, es una buena opción porque +En funciones que no sean críticas en velocidad, es una buena opción porque no tendremos que preocuparnos por el estado de nuestros registros durante la ejecución de la subrutina: al volver de ella tendrán sus valores originales (excepto aquellos de entrada y salida que consideremos necesarios).
-no tendremos que preocuparnos por el estado de nuestros registros durante +
-la ejecución de la subrutina: al volver de ella tendrán sus valores originales +
-(excepto aquellos de entrada y salida que consideremos necesarios).+
  
- No nos olvidemos de que en algunos casos podemos usar el juego de registros +Habrá casos en que no será necesario ponerlos. Si por ejemplo tenemos una función que ejecutamos al inicio del programa para, por ejemplo, precalcular algunos datos, no necesitaremos que preserve registros ya que sus valores al llamarlas no son importantes. También, en funciones muy críticas y que necesitan ser rápidas, en ocasiones no preservaremos los registros en ellas y lo que haremos será cerciorarnos en el código que hace la llamada que estas no modifican ningún registro que sea importante para nosotros en esa parte del código. 
-alternativos (EX AFAF', EXX) para evitar algún PUSH o POP.+ 
 + No nos olvidemos de que en algunos casos (muy pocos normalmente) podemos usar el juego de registros alternativos (''ex afaf<nowiki>'</nowiki>'', ''EXX'') para evitar algún ''PUSH'' ''POP''. En el caso del ejemplo anterior, podríamos haber reemplazado el **push af** y **pop af** (11 y 10 ciclos de reloj respectivamente, 21 ciclos en total) por dos ''ex af, af<nowiki>'</nowiki>'' (4 ciclos cada uno, 8 en total, mucho más eficiente).
  
 \\  \\ 
-==== Método 2: Uso de localidades de memoria ====+==== Método 2: Uso de variables en memoria ====
  
- Aunque no es una opción especialmente rápida, el uso de variables o posiciones de memoria para pasar y recoger parámetros de funciones es bastante efectivo y sencillo. Nos ahorra el uso de muchos registros, y hace que podamos usar dentro de las funciones prácticamente todos los registros. Se hace especialmente útil usando el juego de registros alternativos.+ Aunque no es una opción especialmente rápida, el uso de variables o posiciones de memoria para pasar y recoger parámetros de funciones es bastante efectivo y sencillo. Nos ahorra el uso de muchos registros, y hace que podamos usar dentro de las funciones prácticamente todos los registros. Se hace especialmente útil en este caso usando el juego de registros alternativos para compensar el uso de la memoria con el evitar PUSHs/POPs para preservar registros.
  
  Por ejemplo:  Por ejemplo:
  
 <code z80> <code z80>
-      LD A, 10 +    ld a, 10 
-      LD (x), A +    ld (x), a 
-      LD A, 20 +    ld a, 20 
-      LD (y), A +    ld (y), a 
-      LD BC, 40 +    ld bc, 40 
-      LD (size), BC      ; Parametros de entrada a la funcion +    ld (size), bc      ; Parametros de entrada a la funcion 
-      CALL MiFuncion +    call MiFuncion 
-      (...)+    (...)
  
 MiFuncion: MiFuncion:
-      EXX                ; Preservamos TODOS los registros+    exx                ; Preservamos TODOS los registros
  
-      LD A, (x) +    ld a, (x) 
-      LD BA +    ld ba 
-      LD A, (y) +    ld a, (y) 
-      LD BC, (size)      ; Leemos los parametros+    ld bc, (size)      ; Leemos los parametros
  
-      (Codigo)+    (... código ...)
  
-      LD (salida), a     ; Devolvemos un valor +    ld (salida), a     ; Devolvemos un valor 
-      EXX +    exx 
-      RET+    ret
  
 x      DB  0 x      DB  0
Línea 817: Línea 783:
 </code> </code>
  
- Este es un ejemplo exagerado donde todos los parámetros se pasan en variables, + Este es un ejemplo exagerado donde todos los parámetros se pasan en variables, pero lo normal es usar un método mixto entre este y el anterior, pasando cosas en registros excepto si nos quedamos sin ellos (por que una función requiere muchos parámetros, por ejemplo), de forma que algunas cosas las pasamos con variables de memoria.
-pero lo normal es usar un método mixto entre este y el anterior, pasando cosas +
-en registros excepto si nos quedamos sin ellos (por que una función requiere +
-muchos parámetros, por ejemplo), de forma que algunas cosas las pasamos con +
-variables de memoria.+
  
- La ventaja del paso de parámetros por memoria es que podemos utilizar las rutinas desde BASICPOKEando los parámetros en memoria y llamando a la rutina con RANDOMIZE USR DIRECCION.+ Este método se puede pues utilizar en conjunción con el anterior pasando algunos parámetros en registros y otros por memoria, ya sean variables "generales/globales" del programa, variables del sistema, o "variables locales" que hemos definido bajo la función como uno o varios DB/DW exclusivos para pasar parámetros a la rutina.
  
 + La ventaja del paso de parámetros por memoria es que podemos utilizar las rutinas desde BASIC, POKEando los parámetros en memoria y llamando a la rutina con ''RANDOMIZE USR DIRECCION''.
  
 \\  \\ 
 ==== Método 3: Uso de la pila (método C) ==== ==== Método 3: Uso de la pila (método C) ====
  
- El tercer método es el sistema que utilizan los lenguajes de alto nivel + El tercer método es el sistema que utilizan los lenguajes de alto nivel para pasar parámetros a las funciones: el apilamiento de los mismos. Este sistema no se suele utilizar en ensamblador, pero vamos a comentarlo de forma que os permita integrar funciones en ASM dentro de programas escritos en C, como los compilables con el ensamblador Z88DK.
-para pasar parámetros a las funciones: el apilamiento de los mismos. Este +
-sistema no se suele utilizar en ensamblador, pero vamos a comentarlo +
-de forma que os permita integrar funciones en ASM dentro de programas +
-escritos en C, como los compilables con el ensamblador Z88DK.+
  
- En C (y en otros lenguajes de programación) los parámetros se insertan + En C (y en otros lenguajes de programación) los parámetros se insertan en la pila en el orden en que son leídos. La subrutina después lee los valores sin desapilarlos, usando el valor de SP para acceder a ellosEn ensamblador no es normal utilizar este método a menos que tengamos muchos parámetros, no nos quepan en registros y que queramos ir rescatándolos de la pila en el punto de la función que nos interese (sea con el valor de SP o con POP). 
-en la pila en el orden en que son leídos. La subrutina debe utilizar el + 
-registro SP (una copia) para acceder a los valores apilados en orden +En ese caso, simplemente apilamos los parámetros con ''PUSH'' y dentro de la rutina los vamos recogiendo con ''POP'':
-inversoEstos valores son siempre de 16 bits aunque las variables +
-pasadas sean de 8 bits (en este caso ignoraremos el byte que no +
-contiene datos, el segundo).+
  
  Veamos unos ejemplos:  Veamos unos ejemplos:
  
-<code> +<code z80
-//----------------------------------------------------------------- +    push bc        ; coordenada X 
-// Sea parte de nuestro programa en C:+    push de        ; coordenada Y 
 +    push hl        ; direccion grafico 
 +    call Rutina 
 +    (...)
  
-  int jugador_x, jugador_y;+Rutina: 
 +    pop hl         HL = coordenada Y
  
-  jugador_x = 10; +    ; (trabajamos con HL)
-  jugador_y = 200; +
-  Funcion( jugador_x, jugador_y ); +
-  (...)+
  
 +                   ; Ahora recogemos el parametro 2,
 +                   ; en HL o en cualquier otro registro
 +    pop hl         ; HL = coordenada X
  
-//----------------------------------------------------------------- +    ; (hacemos calculos con HL)
-int Funcionint x, int y ) +
-{+
  
-#asm +    pop de         ; HL = direccion grafico
-   LD HL,2              +
-   ADD HL,SP           ; Ahora SP apunta al ultimo parametro metido +
-                       ; en la pila por el compilador (valor de Y)+
  
-   LD C, (HL) +    ret
-   INC HL +
-   LD B, (HL) +
-   INC HL              ; Ahora BC = y +
- +
-   LD E, (HL) +
-   INC HL +
-   LD D, (HL) +
-   INC HL              ; Ahora, DE = x +
- +
-   ;;; (ahora hacemos lo que queramos en asm) +
- +
-#endasm +
-}+
 </code> </code>
  
- No tenemos que preocuparnos por hacer PUSH POP de los registros para + Nótese que hacemos ''PUSH'' de los 3 parámetros con 3 registros concretos pero que luego no hacemos ''POP'' de esos mismos registros. Hacer eso sería lo mismo que pasarse los parámetros en esos registros sin usar la pila. Si estamos usando la pila, es porque tenemos más parámetros que registros, o porque necesitamos extraer cada parámetro en el punto del programa donde nos interese y en un registro concreto. Lo importante es que antes del ''RET'' hayamos sacado de la pila todo lo que se introdujo, para que lo siguiente que esté presente en la pila sea la dirección de retorno para ''RET''.
-preservar su valor dado que Z88DK lo hace automáticamente antes y después +
-de cada #asm y #endasm.+
  
- El problema es que conforme crece el número de parámetros apilados, es posible +También podemos usar acceso directo memoria mediante el valor de SP saltándonos los 2 bytes de la dirección de retorno introducida en la pila por el ''CALL''como hace el compilador de C Z88DKpero en ese caso necesitaremos antes de salir de la rutina hacer un POP de todos los parámetros introducidos:
-que tengamos que hacer malabarismos para almacenarlos, dado que no podemos +
-usar HL (es nuestro puntero a la pila en las lecturas). Veamos el siguiente +
-ejemplo con 3 parámetrosdonde tenemos que usar PUSH para guardar el valor +
-de DE y EX DEHL para acabar asociando el valor final a HL:+
  
-<code> +<code z80
-//----------------------------------------------------------------- +    push bc        coordenada X 
-int Funcion( int x, int y, int z ) +    push de        coordenada Y 
-+    call Rutina
- +
-#asm +
-   LD HL,2              +
-   ADD HL,SP           Ahora SP apunta al ultimo parametro metido +
-                       en la pila por el compilador (z) +
-   LD C, (HL) +
-   INC HL +
-   LD B, (HL) +
-   INC HL              ; Ahora BC = z +
- +
-   LD E, (HL) +
-   INC HL +
-   LD D, (HL) +
-   INC HL              ; Ahora, DE = y+
  
-   PUSH DE             Guardamos DE+Rutina: 
 +    ld hl, 2       2 bytes = direccion de retorno introducida por call 
 +    add hl, sp     ; Ahora SP apunta al ultimo parametro metido 
 +                   ; en la pila por el compilador (valor de Y)
  
-   LD E, (HL+    ld c, (hl
-   INC HL +    inc hl 
-   LD D, (HL+    ld b, (hl
-   INC HL              Usamos DE para leer el valor de x+    inc hl         Ahora BC = Y
  
-   EX DEHL           ; Ahora cambiamos x a HL +    ld e, (hl) 
-   POP DE              Y recuperamos el valor de y en DE +    inc hl 
 +    ld d(hl) 
 +    inc hl         Ahora, DE = X
  
-   ;;; (ahora hacemos lo que queramos en asm)+    ;;; (ahora hacemos lo que queramos en asm)
  
-#endasm +    pop de 
-}+    pop bc          ; Tambien podriamos simplemente restar N a SP
 </code> </code>
  
- La manera de leer bytes (variables de tipo char) pulsados en C es de la misma forma + Los parámetros de 8 bits en este caso se tienen que pasar como 16 bits con la parte alta a 0ya que la pila usa 2 bytes en PUSH POP. Si usamos POP para acceder a los datos en la pila, simplemente ignoramos la parte alta del registro recuperado. Si usamos el valor de SP para acceder a la pila, simplemente tenemos que avanzar HL para saltarnos la parte alta de cada valor 16 bits:
-que leemos una palabra de 16 bits, pero ignorando la parte alta. En +
-realidadcomo la pila es de 16 bits, el compilador convierte el +
-dato de 8 bits en uno de 16 (rellenando con ceros) mete en la pila este valor:+
  
-<code> +<code z80
-//----------------------------------------------------------------- +Rutina: 
-int Funcion( char xchar y ) +    ld hl2            ; 2 bytes = direccion de retorno introducida por call 
-{+    add hl, sp          ; Ahora SP apunta al ultimo parametro metido en pila
  
-#asm +    ld a(hl)          ; Aquí tenemos nuestro dato de 8 bits (Y) 
-   LD HL,2              +    ld ba 
-   ADD HL,SP           ; Ahora SP apunta al ultimo parametro metido +    inc hl 
-                       en la pila por el compilador (z)+    inc hl              La parte alta del byte no nos interesa
  
-   LD A, (HL)          ; Aquí tenemos nuestro dato de 8 bits (y+    ld a, (hl)          ; Aquí tenemos nuestro dato de 8 bits (X
-   LD BA +    ld ca 
-   INC HL +                        Aqui no necesitamos mas inc hl, no hay mas parametros
-   INC HL              La parte alta del byte no nos interesa+
  
-   LD A, (HL)          Aquí tenemos nuestro dato de 8 bits (x) +    Nuestra rutina empieza a trabajar aquí
-   LD C, A +
-   INC HL +
-   INC HL              ; La parte alta del byte no nos interesa +
- +
-   ;;; (ahora hacemos lo que queramos en asm) +
- +
-#endasm +
-}+
 </code> </code>
  
- En ocasiones, es posible que incluso tengamos que utilizar variables +Por contra, para devolver valores no se utiliza la pila (dado que no podemos tocarla debido al ''RET''), sino que se utilizarán registros o direcciones de memoria.
-auxiliares de memoria para guardar datos: +
- +
-<code> +
-//----------------------------------------------------------------- +
-int Funcion( int x, int y, char z ) +
-+
- +
-#asm +
-   LD HL,2              +
-   ADD HL,SP           ; Ahora SP apunta al ultimo parametro metido +
-                       ; en la pila por el compilador (z) +
- +
-   LD C, (HL) +
-   INC HL +
-   LD B, (HL) +
-   INC HL              ; Ahora BC = y +
-   LD (valor_y), BC    ; nos lo guardamos, BC libre de nuevo +
- +
-   LD C, (HL) +
-   INC HL +
-   LD B, (HL) +
-   INC HL  +
-   LD (valor_x), BC    ; Nos lo guardamos, BC libre de nuevo +
- +
-   LD A, (HL) +
-   LD (valor_z), A     ; Nos guardamos el byte +
-   INC HL +
-   INC HL              ; La parte alta del byte no nos interesa +
- +
-   ;;; (ahora hacemos lo que queramos en asm) +
- +
-   RET  +
- +
-valor_x   DW +
-valor_y   DW +
-valor_z   DB +
- +
-#endasm +
-+
-</code> +
- +
- Por contra, para devolver valores no se utiliza la pila (dado que +
-no podemos tocarla), sino que se utiliza un determinado registro. +
-En el caso de Z88DK, se utiliza el registro HLSi la función es +
-de tipo INT o CHAR en cuanto a devolución, el valor que dejemos +
-en HL será el que se asignará en una llamada de este tipo: +
- +
-<code c> +
- valor = MiFuncion_ASM( x, y, z); +
-</code>+
  
- Hemos considerado importante explicar este tipo de paso de parámetros +Como hemos comentado, en programas íntegramente escritos en ensamblador usaremos este método muy raras veces.
-y devolución de valores porque nos permite integrar nuestro código +
-ASM en programas en C.+
  
 \\  \\ 
-===== Integracion de ASM en Z88DK ===== 
  
- 
- Para aprovechar esta introducción de "uso de ASM en Z88DK", veamos 
-el código de alguna función en C que use ASM internamente y que 
-muestre, entre otras cosas, la lectura de parámetros de la pila, 
-el acceso a variables del código C, el uso de etiquetas, o la 
-devolución de valores. 
- 
-<code> 
-// 
-// Devuelve la direccion de memoria del atributo de un caracter 
-// de pantalla, de coordenadas (x,y). Usando la dirección que 
-// devuelve esta función (en HL, devuelto en la llamada), podemos 
-// leer o cambiar los atributos de dicho carácter. 
-// 
-// Llamada:   valor =  Get_LOWRES_Attrib_Address( 1, 3 ); 
-// 
-int Get_LOWRES_Attrib_Address( char x, char y ) 
-{ 
-#asm 
- 
-   LD HL, 2 
-   ADD HL, SP                 ; Leemos x e y de la pila 
-   LD  D, (HL)  ; d = y        
-   INC HL                     ; Primero "y" y luego "x". 
-   INC HL                     ; Como son "char", ignoramos parte alta. 
-   LD  E, (HL)  ; e = x        
-                                    
-   LD H, 0 
-   LD L, D 
-   ADD HL, HL                 ; HL = HL*2 
-   ADD HL, HL                 ; HL = HL*4 
-   ADD HL, HL                 ; HL = HL*8 
-   ADD HL, HL                 ; HL = HL*16 
-   ADD HL, HL                 ; HL = HL*32 
-   LD D, 0 
-   ADD HL, DE                 ; Ahora HL = (32*y)+x 
-   LD BC, 16384+6144          ; Ahora BC = offset attrib (0,0) 
-   ADD HL, BC                 ; Sumamos y devolvemos en HL 
- 
-#endasm 
-} 
- 
-// 
-// Set Border 
-// Ejemplo de modificación del borde, muestra cómo leer variables 
-// globales de C en ASM, añadiendo "_" delante. 
-// 
- 
-unsigned char bordeactual; 
- 
-void BORDER( unsigned char value ) 
-{ 
-#asm 
-   LD HL, 2 
-   ADD HL, SP 
-   LD A, (HL) 
-   LD C, 254 
-   OUT (C), A 
-   LD (_bordeactual), A 
- 
-   RLCA                   ; Adaptamos el borde para guardarlo 
-   RLCA                   ; en la variable del sistema BORDCR 
-   RLCA                   ; Color borde -> a zona de PAPER 
-   LD HL, 23624           ; lo almacenamos en BORDCR para que 
-   LD (HL), A             ; lo usen las rutinas de la ROM. 
-#endasm 
-} 
- 
- 
-// 
-// Realización de un fundido de la pantalla hacia negro 
-// Con esta función se muestra el uso de etiquetas. Nótese 
-// como en lugar de escribirse como ":", se escriben sin 
-// ellos y con un punto "." delante. 
-// 
-void FadeScreen( void ) 
-{ 
- 
-#asm 
-   LD B, 9                      ; Repetiremos el bucle 9 veces 
- 
-.fadescreen_loop1 
-   LD HL, 16384+6144            ; Apuntamos HL a la zona de atributos 
-   LD DE, 768                   ; Iteraciones bucle 
- 
-   HALT 
-   HALT                         ; Ralentizamos el efecto 
-    
-.fadescreen_loop2 
-   LD A, (HL)                   ; Cogemos el atributo 
-   AND 127                      ; Eliminamos el bit de flash 
-   LD C, A 
- 
-   AND 7                        ; Extraemos la tinta (AND 00000111b) 
-   JR Z, fadescreen_ink_zero    ; Si la tinta ya es cero, no hacemos nada 
- 
-   DEC A                        ; Si no es cero, decrementamos su valor 
- 
-.fadescreen_ink_zero 
-   
-   EX AF, AF                    ; Nos hacemos una copia de la tinta en A 
-   LD A, C                      ; Recuperamos el atributo 
-   SRA A 
-   SRA A                        ; Pasamos los bits de paper a 0-2 
-   SRA A                        ; con 3 instrucciones de desplazamiento >> 
- 
-   AND 7                        ; Eliminamos el resto de bits 
-   JR Z, fadescreen_paper_zero  ; Si ya es cero, no lo decrementamos 
- 
-   DEC A                        ; Lo decrementamos 
- 
-.fadescreen_paper_zero 
-   SLA A 
-   SLA A                        ; Volvemos a color paper en bits 3-5 
-   SLA A                        ; Con 3 instrucciones de desplazamiento << 
- 
-   LD C, A                      ; Guardamos el papel decrementado en A 
-   EX AF, AF                    ; Recuperamos A 
-   OR C                         ; A = A OR C  =  PAPEL OR TINTA 
- 
-   LD (HL), A                   ; Almacenamos el atributo modificado 
-   INC HL                       ; Avanzamos puntero de memoria 
- 
-   DEC DE 
-   LD A, D 
-   OR E 
-   JP NZ, fadescreen_loop2      ; Hasta que DE == 0 
- 
-   DJNZ fadescreen_loop1        ; Repeticion 9 veces 
- 
-#endasm 
-} 
-</code> 
- 
- Si tenéis curiosidad por ver el funcionamiento de esta rutina de Fade (fundido), 
-podéis verla integramente en ASM en el fichero fade.asm. Un detalle a tener en cuenta, 
-en Z88DK se soporta "EX AF, AF", mientras que pasmo requiere poner la 
-comilla del shadow-register: "EX AF, AF'". 
- 
-\\  
-{{ cursos:ensamblador:fade.png?640 |Captura durante el fade de la pantalla}} 
-\\  
- 
- En la anterior captura podéis ver el aspecto de uno de los pasos del fundido. 
- 
- 
-\\  
 ===== La importancia de usar subrutinas ===== ===== La importancia de usar subrutinas =====
  
- Usar subrutinas es mucho más importante de lo que parece a simple + Usar subrutinas es mucho más importante de lo que parece a simple vista:// nos permite organizar el programa en unidades o módulos funcionales// que cumplen una serie de funciones específicas, lo que hace mucha más sencilla su depuración y optimización.
-vista:// nos permite organizar el programa en unidades o módulos +
-funcionales// que cumplen una serie de funciones específicas, lo que +
-hace mucha más sencilla su depuración y optimización.+
  
- Si en el menú de nuestro juego estamos dibujando una serie de + Si en el menú de nuestro juego estamos dibujando una serie de sprites móviles, y también lo hacemos a lo largo del juego, resulta absurdo "construir" 2 bloques de código, uno para mover los sprites del menú y otro para los del juego. Haciendo esto, si encontramos un error en una de las 2 rutinas, o realizamos una mejora, deberemos corregirlo en ambas.
-sprites móviles, y también lo hacemos a lo largo del juego, resulta +
-absurdo "construir" 2 bloques de código, uno para mover los sprites +
-del menú y otro para los del juego. Haciendo esto, si encontramos +
-un error en una de las 2 rutinas, o realizamos una mejora, deberemos +
-corregirlo en ambas.+
  
- Por contra, si creamos una subrutina, digamos, DrawSprite, que podamos + Por contra, si creamos una subrutina, digamos, ''DrawSprite'', que podamos llamar con los parámetros adecuados en ambos puntos del programa, cualquier cambio, mejora o corrección que realicemos en ''DrawSprite'' afectará a todas las llamadas que le hagamos. También reducimos así el tamaño de nuestro programa (y con él el tiempo de carga del mismo), las posibilidades de fallo, y la longitud del listado (haciéndolo más legible y manejable).
-llamar con los parámetros adecuados en ambos puntos del programa, cualquier +
-cambio, mejora o corrección que realicemos en DrawSprite afectará a +
-todas las llamadas que le hagamos. También reducimos así el tamaño de +
-nuestro programa (y con él el tiempo de carga del mismo), las posibilidades +
-de fallo, y la longitud del listado (haciéndolo más legible y manejable).+
  
- Aunque no sea el objetivo de esta serie de artículos, antes de sentarse a + Aunque no sea el objetivo de esta serie de artículos, antes de sentarse a teclear, un buen programador debería coger un par de folios de papel y hacer un pequeño análisis de lo que pretende crear. Este proceso, la fase de diseño, define qué debe de hacer el programa y, sobre todo, una división lógica de cuáles son las principales partes del mismo. Un sencillo esquema en papel, un diagrama de flujo, identificar las diferentes partes del programa, etc.
-teclear, un buen programador debería coger un par de folios de papel y  +
-hacer un pequeño análisis de lo que pretende crear. Este proceso, la +
-fase de diseño, define qué debe de hacer el programa y, sobre todo, +
-una división lógica de cuáles son las principales partes del mismo. +
-Un sencillo esquema en papel, un diagrama de flujo, identificar las +
-diferentes partes del programa, etc.+
  
- El proceso empieza con un esbozo muy general del programa, que será + El proceso empieza con un esbozo muy general del programa, que será coincidente con la gran mayoría de los juegos: inicialización de variables, menú (que te puede llevar bien a las opciones o bien al juego en sí), y dentro del juego, lectura de teclado/joystick, trazado de la pantalla, lógica del juego, etc.
-coincidente con la gran mayoría de los juegos: inicialización de +
-variables, menú (que te puede llevar bien a las opciones o bien al +
-juego en sí), y dentro del juego, lectura de teclado/joystick, +
-trazado de la pantalla, lógica del juego, etc.+
  
- Después, se teclea un programa vacío que siga esos pasos, pero que no + Después, se teclea un programa vacío que siga esos pasos, pero que no haga nada; un bucle principal que tenga un aspecto parecido a:
-haga nada; un bucle principal que tenga un aspecto parecido a:+
  
 <code z80> <code z80>
 BuclePrincipal: BuclePrincipal:
-     CALL Leer_Teclado +    call LeerTeclado             ; o Leer_Teclado 
-     CALL Logica_Juego +    call LogicaJuego             ; o Logica_Juego 
-     CALL Comprobar_Estado +    call ComprobarEstado         ; o Comprobar_Estado 
-     jp Bucle_Principal+    jp BuclePrincipal            ; o Bucle_Principal
  
-Leer_Teclado+LeerTeclado
-     RET+    ret
  
-Logica_Juego+LogicaJuego
-     RET+    ret
  
-Comprobar_Estado+ComprobarEstado
-     RET+    ret
 </code> </code>
  
- Tras esto, ya tenemos el "esqueleto del programa". Y ahora hay que rellenar + Tras esto, ya tenemos el "esqueleto del programa". Y ahora hay que rellenar ese esqueleto, y la mejor forma de hacerlo es aprovechar esa "modularidad" que hemos obtenido con ese diseño en papel.
-ese esqueleto, y la mejor forma de hacerlo es aprovechar esa "modularidad" que +
-hemos obtenido con ese diseño en papel.+
  
- Por ejemplo, supongamos que nuestro juego tiene que poder dibujar + Por ejemplo, supongamos que nuestro juego tiene que poder dibujar sprites y pantallas hechas a bases de bloques que se repiten (tiles). Gracias a nuestro diseño, sabemos que necesitamos una rutina que imprima un sprite, una rutina que dibuje un tile y una rutina que dibuje una pantalla llena de tiles.
-sprites y pantallas hechas a bases de bloques que se repiten (tiles). +
-Gracias a nuestro diseño, sabemos que necesitamos una rutina que +
-imprima un sprite, una rutina que dibuje un tile y una rutina que +
-dibuje una pantalla llena de tiles.+
  
- Pues bien, creamos un programa en ASM nuevo, desde cero, y en él + Pues bien, creamos un programa en ASM nuevo, desde cero, y en él creamos una función DrawSprite que acepte como parámetros la dirección origen de los datos del Sprite, y las posiciones X e Y donde dibujarlo, y la realizamos. En este nuevo programa, pequeño, sencillo de leer, realizamos todo tipo de pruebas:
-creamos una función DrawSprite que acepte como parámetros la dirección +
-origen de los datos del Sprite, y las posiciones X e Y donde dibujarlo, +
-y la realizamos. En este nuevo programa, pequeño, sencillo de leer, +
-realizamos todo tipo de pruebas:+
  
 <code z80> <code z80>
-   ORG 50000+    ORG 50000 
 + 
 +    ; Probamos de diferentes formas nuestra rutina 
 +    ld b, 10 
 +    ld c, 15 
 +    ld hl, sprite 
 +    call DrawSprite 
 +    ret
  
-   ; Probamos de diferentes formas nuestra rutina 
-   LD B, 10 
-   LD C, 15 
-   LD HL, sprite 
-   CALL DrawSprite 
-   RET 
-  
 ; Rutina DrawSprite ; Rutina DrawSprite
 ; Acepta como parametros ... y devuelve ... ; Acepta como parametros ... y devuelve ...
 DrawSprite: DrawSprite:
-   (aquí el código) +    (aquí el código) 
-   RET+    ret
  
 sprite DB 0,0,255,123,121,123,34, (etc...) sprite DB 0,0,255,123,121,123,34, (etc...)
  
-   END 50000+    END 50000
 </code> </code>
  
- Gracias a esto, podremos probar nuestra nueva rutina y trabajar con + Gracias a esto, podremos probar nuestra nueva rutina y trabajar con ella limpiamente y en un fichero de programa pequeño. Cuando la tenemos lista, basta con copiarla a nuestro programa "principal" y ya sabemos que la tenemos disponible para su uso con call.
-ella limpiamente y en un fichero de programa pequeño. Cuando la tenemos +
-lista, basta con copiarla a nuestro programa "principal" y ya sabemos +
-que la tenemos disponible para su uso con CALL.+
  
- Así, vamos creando diferentes rutinas en un entorno controlado y + Así, vamos creando diferentes rutinas en un entorno controlado y testeable, y las vamos incorporando a nuestro programa. Si hay algún bug en una rutina y tenemos que reproducirlo, podemos hacerlo en nuestros pequeños programas de prueba, evitando el típico problema de tener que llegar a un determinado punto de nuestro programa para chequear una rutina, o modificar su bucle principal para hacerlo.
-testeable, y las vamos incorporando a nuestro programa. Si hay algún bug +
-en una rutina y tenemos que reproducirlo, podemos hacerlo en nuestros +
-pequeños programas de prueba, evitando el típico problema de tener que +
-llegar a un determinado punto de nuestro programa para chequear una +
-rutina, o modificar su bucle principal para hacerlo.+
  
- Además, el definir de antemano qué tipo de subrutinas necesitamos y + Además, el definir de antemano qué tipo de subrutinas necesitamos y qué parámetros deben aceptar o devolver permite trabajar en equipo. Si sabes que necesitarás una rutina que dibuje un sprite, o que lea el teclado y devuelva la tecla pulsada, puedes decir los registros de entrada y los valores de salida que necesitas, y que la realice una segunda persona y te envíe la rutina lista para usar.
-qué parámetros deben aceptar o devolver permite trabajar en equipo. Si +
-sabes que necesitarás una rutina que dibuje un sprite, o que lea el +
-teclado y devuelva la tecla pulsada, puedes decir los registros de +
-entrada y los valores de salida que necesitas, y que la realice una +
-segunda persona y te envíe la rutina lista para usar.+
  
- En ocasiones una excesiva desgranación del programa en módulos más + En ocasiones una excesiva desgranación del programa en módulos más pequeños puede dar lugar a una penalización en el rendimiento, aunque no siempre es así. Por ejemplo, supongamos que tenemos que dibujar un mapeado de 10x10 bloques de 8x8 pixeles cada uno. Si hacemos una función de que dibuja un bloque de 8x8, podemos llamarla en un bucle para dibujar nuestros 10x10 bloques.
-pequeños puede dar lugar a una penalización en el rendimiento, aunque +
-no siempre es así. Por ejemplo, supongamos que tenemos que dibujar +
-un mapeado de 10x10 bloques de 8x8 pixeles cada uno. Si hacemos una +
-función de que dibuja un bloque de 8x8, podemos llamarla en un bucle +
-para dibujar nuestros 10x10 bloques.+
  
- Hay gente que, en lugar de esto, preferirá realizar una función + Hay gente que, en lugar de esto, preferirá realizar una función específica que dibuje los 10x10 bloques dentro de una misma función. Esto es así porque de este modo te evitas 100 calls (10x10) y sus correspondientes RETs, lo cual puede ser importante en una rutina gráfica que se ejecute X veces por segundo. Por supuesto, en muchos casos tendrán razón, en ciertas ocasiones hay que hacer rutinas concretas para tareas concretas, aún cuando puedan repetir parte de otro código que hayamos escrito anteriormente, con el objetivo de evitar llamadas, des/apilamientos u operaciones innecesarias en una función crítica.
-específica que dibuje los 10x10 bloques dentro de una misma función. +
-Esto es así porque de este modo te evitas 100 CALLs (10x10) y sus +
-correspondientes RETs, lo cual puede ser importante en una rutina +
-gráfica que se ejecute X veces por segundo. Por supuesto, en muchos +
-casos tendrán razón, en ciertas ocasiones hay que hacer rutinas +
-concretas para tareas concretas, aún cuando puedan repetir parte +
-de otro código que hayamos escrito anteriormente, con el objetivo +
-de evitar llamadas, des/apilamientos u operaciones innecesarias en +
-una función crítica.+
  
- Pero si, por ejemplo, nosotros sólo dibujamos la pantalla una vez + Pero si, por ejemplo, nosotros sólo dibujamos la pantalla una vez cuando nuestro personaje sale por el borde, y no volvemos a dibujar otra hasta que sale por otro borde (típico caso de juegos sin scroll que muestran pantallas completas de una sóla vez), vale la pena el usar funciones modulares dado que unos milisegundos más de ejecución en el trazado de la pantalla no afectarán al desarrollo del juego.
-cuando nuestro personaje sale por el borde, y no volvemos a dibujar +
-otra hasta que sale por otro borde (típico caso de juegos sin scroll +
-que muestran pantallas completas de una sóla vez), vale la pena el +
-usar funciones modulares dado que unos milisegundos más de ejecución +
-en el trazado de la pantalla no afectarán al desarrollo del juego.+
  
- Al final hay que llegar a un compromiso entre modularidad y optimización, + Al final hay que llegar a un compromiso entre modularidad y optimización, en algunos casos nos interesará desgranar mucho el código, y en otros nos interesará hacer funciones específicas. Y esa decisión no deja de ser, al fin y al cabo, diseño del programa.
-en algunos casos nos interesará desgranar mucho el código, y en otros +
-nos interesará hacer funciones específicas. Y esa decisión no deja de +
-ser, al fin y al cabo, diseño del programa.+
  
- En cualquier caso, el diseño nos asegura que podremos implementar + En cualquier caso, el diseño nos asegura que podremos implementar nuestro programa en cualquier lenguaje y en cualquier momento. Podremos retomar nuestros "papeles de diseño" 3 meses después y, pese a no recordar en qué parte del programa estábamos, volver a su desarrollo sin excesivas dificultades.
-nuestro programa en cualquier lenguaje y en cualquier momento. Podremos +
-retomar nuestros "papeles de diseño" 3 meses después y, pese a no +
-recordar en qué parte del programa estábamos, volver a su desarrollo +
-sin excesivas dificultades.+
  
- Una de las cosas más complicadas de hacer un juego es el pensar por + Una de las cosas más complicadas de hacer un juego es el pensar por dónde empezar. Todo este proceso nos permite empezar el programa por la parte del mismo que realmente importa. Todos hemos empezado alguna vez a realizar nuestro juego por el menú, perdiendo muchas horas de trabajo para descubrir que teníamos un menú, pero no teníamos un juego, y que ya estábamos cansados del desarrollo sin apenas haber empezado.
-dónde empezar. Todo este proceso nos permite empezar el programa por la parte +
-del mismo que realmente importa. Todos hemos empezado alguna vez a  +
-realizar nuestro juego por el menú, perdiendo muchas horas de trabajo +
-para descubrir que teníamos un menú, pero no teníamos un juego, y que +
-ya estábamos cansados del desarrollo sin apenas haber empezado. +
  
- Veamos un ejemplo: suponiendo que realizamos, por ejemplo, un juego + Veamos un ejemplo: suponiendo que realizamos, por ejemplo, un juego de puzzles tipo Tetris, lo ideal sería empezar definiendo dónde se almacenan los datos del area de juego, hacer una función que convierta esos datos en imágenes en pantalla, y realizar un bucle que permita ver caer la pieza. Después, se agrega control por teclado para la pieza y se pone la lógica del juego (realización de líneas al tocar suelo, etc).
-de puzzles tipo Tetris, lo ideal sería empezar definiendo dónde se +
-almacenan los datos del area de juego, hacer una función que convierta +
-esos datos en imágenes en pantalla, y realizar un bucle que permita ver +
-caer la pieza. Después, se agrega control por teclado para la pieza y +
-se pone la lógica del juego (realización de líneas al tocar suelo, etc).+
  
- Tras esto, ya tenemos el esqueleto funcional del juego y podemos añadir + Tras esto, ya tenemos el esqueleto funcional del juego y podemos añadir opciones, menúes y demás. Tendremos algo tangible, funcional, donde podemos hacer cambios que implican un inmediato resultado en pantalla, y no habremos malgastado muchas horas con un simple menú.
-opciones, menúes y demás. Tendremos algo tangible, funcional, donde podemos +
-hacer cambios que implican un inmediato resultado en pantalla, y no habremos +
-malgastado muchas horas con un simple menú.+
  
- Por otra parte, el diseñar correctamente nuestro programa y desgranarlo + Por otra parte, el diseñar correctamente nuestro programa y desgranarlo en piezas reutilizables redundará en nuestro beneficio no sólo actual (con respecto al programa que estamos escribiendo) sino futuro, ya que podremos crearnos nuestras propias "bibliotecas" de funciones que reutilizar en futuros programas.
-en piezas reutilizables redundará en nuestro beneficio no sólo actual (con +
-respecto al programa que estamos escribiendo) sino futuro, ya que podremos +
-crearnos nuestras propias "bibliotecas" de funciones que reutilizar en +
-futuros programas.+
  
- Aquella rutina de dibujado de Sprites, de zoom de pantalla o de compresión + Aquella rutina de dibujado de Sprites, de zoom de pantalla o de compresión de datos que tanto nos costó programar, bien aislada en una subrutina y con sus parámetros de entrada y salida bien definidos puede ser utilizada directamente en nuestros próximos programas simplemente copiando y pegando el código correspondiente.
-de datos que tanto nos costó programar, bien aislada en una subrutina y +
-con sus parámetros de entrada y salida bien definidos puede ser utilizada +
-directamente en nuestros próximos programas simplemente copiando y pegando el código correspondiente.+
  
- Más aún, podemos organizar funciones con finalidades comunes en ficheros + Más aún, podemos organizar funciones con finalidades comunes en ficheros individuales. Tendremos así nuestro fichero / biblioteca con funciones gráficas, de sonido, de teclado/joystick, etc. El ensamblador Pasmo nos permite incluir un fichero en cualquier parte de nuestro código con la directiva ''INCLUDE''.
-individuales. Tendremos así nuestro fichero / biblioteca con funciones +
-gráficas, de sonido, de teclado/joystick, etc. El ensamblador PASMO nos +
-permite incluir un fichero en cualquier parte de nuestro código con la +
-directiva "INCLUDE".+
  
  Así, nuestro programa en ASM podría comenzar (o acabar) por algo como:  Así, nuestro programa en ASM podría comenzar (o acabar) por algo como:
Línea 1358: Línea 976:
  
  La organización del código en bibliotecas de funciones contribuye a reducir fallos en la codificación, hacer más corto el "listado general del programa", y, sobre todo, reduce el tiempo de desarrollo.  La organización del código en bibliotecas de funciones contribuye a reducir fallos en la codificación, hacer más corto el "listado general del programa", y, sobre todo, reduce el tiempo de desarrollo.
- 
  
 \\  \\ 
Línea 1365: Línea 982:
   * {{cursos:ensamblador:06_reset.asm|Ejemplo de reset por el mal uso de la pila}}   * {{cursos:ensamblador:06_reset.asm|Ejemplo de reset por el mal uso de la pila}}
   * {{cursos:ensamblador:06_reset.tap|Tap del ejemplo anterior}}   * {{cursos:ensamblador:06_reset.tap|Tap del ejemplo anterior}}
-  * {{cursos:ensamblador:06_call_nz.asm|Experimentando con CALL NZ}}+  * {{cursos:ensamblador:06_call_nz.asm|Experimentando con call NZ}}
   * {{cursos:ensamblador:06_call_nz.tap|Tap del ejemplo anterior}}   * {{cursos:ensamblador:06_call_nz.tap|Tap del ejemplo anterior}}
   * {{cursos:ensamblador:06_fade.asm|Sencillo fundido de pantalla}}   * {{cursos:ensamblador:06_fade.asm|Sencillo fundido de pantalla}}
Línea 1376: Línea 993:
   * [[http://www.z80.info|Web del Z80]]   * [[http://www.z80.info|Web del Z80]]
   * [[http://www.worldofspectrum.org/faq/reference/z80reference.htm|Z80 Reference de WOS]]   * [[http://www.worldofspectrum.org/faq/reference/z80reference.htm|Z80 Reference de WOS]]
-  * [[http://ti86.acz.org/z80_ref.htm|Z80 Reference de TI86]]   +  * [[http://ti86.acz.org/z80_ref.htm|Z80 Reference de TI86]] 
-  * [[http://icarus.ticalc.org/articles/z80_faq.html|FAQ de Icarus Productions]] +  * [[http://icarus.ticalc.org/articles/z80_faq.html|FAQ de Icarus Productions]]
   * [[http://www.speccy.org/trastero/cosas/Fichas/fichas.htm|Microfichas de CM de MicroHobby]]   * [[http://www.speccy.org/trastero/cosas/Fichas/fichas.htm|Microfichas de CM de MicroHobby]]
   * [[http://www.ticalc.org/pub/text/z80/|Tablas de ensamblado y t-estados]] (pulsar en z80.txt, z80_reference.txt, z80time.txt).   * [[http://www.ticalc.org/pub/text/z80/|Tablas de ensamblado y t-estados]] (pulsar en z80.txt, z80_reference.txt, z80time.txt).
  • cursos/ensamblador/lenguaje_4.txt
  • Última modificación: 22-01-2024 07:54
  • por sromero