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 [14-01-2024 07:34] – [Utilizar el Buffer de Impresión para la pila o alojar variables] 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 25: Línea 25:
  
 <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 57: Línea 57:
  
 <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 72: Línea 74:
 <code> <code>
 SP = SP - 2 = 65533 SP = SP - 2 = 65533
-(SP) = BC = $00FF+(SP) = BC = $00ff
 </code> </code>
  
Línea 80: Línea 82:
       Celdilla    Contenido       Celdilla    Contenido
      -----------------------      -----------------------
-       65534         $FF+       65534         $ff
 SP ->  65533         $00 SP ->  65533         $00
 </code> </code>
Línea 87: Línea 89:
  
 <code z80> <code z80>
-    PUSH DE          ; Apilamos el registro DE+    push de          ; Apilamos el registro DE
 </code> </code>
  
Línea 94: Línea 96:
 <code> <code>
 SP = SP - 2 = 65531 SP = SP - 2 = 65531
-(SP) = DE = $AABB+(SP) = DE = $aabb
 </code> </code>
  
Línea 102: 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>
  
Línea 111: Línea 113:
  
 <code z80> <code z80>
-    POP DE+    pop de
 </code> </code>
  
Línea 117: Línea 119:
  
 <code> <code>
-DE = (SP) = $AABB+DE = (SP) = $aabb
 SP = SP + 2 = 65533 SP = SP + 2 = 65533
 </code> </code>
Línea 126: 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.
Línea 141: Línea 143:
  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>
 +
 + Visto en "pseucódigo BASIC", ambos comandos serían:
 +
 +<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> </code>
  
Línea 162: Línea 176:
    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 de manipular los bits del registro F (hacer PUSH BC con un valor determinado, por ejemplo, y hacer un POP AF).+ 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).
  
 \\  \\ 
Línea 174: Línea 188:
  
 \\  \\ 
-  * 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 luego un ''POP 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 que necesitan usar BC+    (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>
  
Línea 202: Línea 202:
  
 <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>
  
Línea 216: Línea 216:
  
 <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
Línea 226: 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>
  
Línea 243: Línea 243:
  
 <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>
  
Línea 261: Línea 261:
  
 \\  \\ 
-  * 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 |
  
 \\  \\ 
Línea 288: 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'' y ''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'' 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.
  
  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'':
Línea 299: Línea 353:
     ; 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)     (código)
  
-    POP DE +    pop de 
-    RET         ; En lugar de volver a la dirección de memoria+    ret         ; En lugar de volver a la dirección de memoria
                 ; a la que teníamos que volver, volveremos a                 ; a la que teníamos que volver, volveremos a
                 ; la dirección apuntada por el valor de BC, que                 ; la dirección apuntada por el valor de BC, que
Línea 315: Línea 369:
 <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>
  
Línea 327: Línea 381:
 <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 337: 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>
  
Línea 363: Línea 417:
 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". 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".
  
-La última opción es poner SP a 0, con lo que decrecerá desde el final de la RAM. Recuerda que cuando hacemos PUSH, primero se decrementa SP y 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).+La última opción es poner SP a 0, con lo que decrecerá desde el final de la RAM. Recuerda que cuando hacemos PUSH, primero se decrementa SP y 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).+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. 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.
Línea 391: Línea 445:
  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í.  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 en contienda, 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.
Línea 409: Línea 463:
  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.  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'':+ 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 419: 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>
  
Línea 428: Línea 482:
  
 <code z80> <code z80>
-    LD A, 35 +    ld a, 35 
-    JP SUMA_A_10+    jp SUMA_A_10
 volver1: volver1:
  
Línea 438: 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, sumaríamos 10 a A (pasando a valer 45), haríamos B = 45, y volveríamos al lugar posterior al punto de llamada.  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.
  
- 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 volver1'' que 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 468: 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, 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''.+ ''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''.
  
- ¿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.+ ¿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.
  
  Son, por tanto, el equivalente ensamblador de ''GO SUB'' y ''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).  Son, por tanto, el equivalente ensamblador de ''GO SUB'' y ''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).
  
 <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>
Línea 486: Línea 540:
  
 <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 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.+ 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.
  
- 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.+ 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.
  
  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:  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:
  
 <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>
Línea 529: Línea 583:
    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 538: 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 544: 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 con respecto al estado de los flags, igual que ''JP cc'' o ''JR cc'', de forma que podemos condicionar el SALTO (CALL) o el retorno (RET) al estado de un determinado flag.+ Una de las peculiaridades de call ret es que tienen instrucciones condicionales con respecto al estado de los flags, igual que ''jp cc'' o ''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.
 \\  \\ 
  
Línea 597: Línea 651:
  
     ; 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 RET no se habrá ejecutado.+    ; Aquí seguiremos si BC no es cero, el ret no se habrá ejecutado.
 </code> </code>
  
Línea 614: Línea 668:
    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 635: 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 
-      Modifica:       Ningun registro aparte de HL+; Salida:         HL: Resultado. 
 +; Modifica:       Ningun registro aparte de HL
 ;-------------------------------------------------------------- ;--------------------------------------------------------------
-MULTIPLICA+Mult_HL_DE
-    PUSH AF             ; Preservamos AF porque F se va a modificar +    push af             ; Preservamos AF porque F se va a modificar 
-    PUSH BC             ; Preservamos BC porque su valor se pierde +    push bc             ; Preservamos BC porque su valor se pierde 
-    LD HL, 0+    ld hl, 0
  
-MULTI01+multiloop_01
-    ADD HLDE +    add hlde 
-    DEC BC +    dec bc 
-    LD AB +    ld ab 
-    OR C +    or c 
-    JR NZMULTI01+    jr nzmultiloop_01
  
-    POP BC              ; Rescatamos el valor de BC +    pop bc              ; Rescatamos el valor de BC 
-    POP AF              ; Rescatamos el valor de AF +    pop af              ; Rescatamos el valor de AF 
-    RET+    ret
 </code> </code>
  
- Antes de hacer la llamada a MULTIPLICA, 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.+ 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.
  
  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.  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.
Línea 675: Línea 730:
 <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>
  
Línea 689: Línea 744:
 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. 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.
  
- 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'' o ''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 AFAF<nowiki>'</nowiki>'' (4 ciclos cada uno, 8 en total, mucho más eficiente).+ 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'' o ''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 afaf<nowiki>'</nowiki>'' (4 ciclos cada uno, 8 en total, mucho más eficiente).
  
 \\  \\ 
Línea 699: Línea 754:
  
 <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 741: Línea 796:
  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 ellos. En 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 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 ellos. En 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 ese caso, simplemente apilamos los parámetros con PUSH y dentro de la rutina los vamos recogiendo con POP:+En ese caso, simplemente apilamos los parámetros con ''PUSH'' y dentro de la rutina los vamos recogiendo con ''POP'':
  
  Veamos unos ejemplos:  Veamos unos ejemplos:
  
 <code z80> <code z80>
-    PUSH BC        ; coordenada X +    push bc        ; coordenada X 
-    PUSH DE        ; coordenada Y +    push de        ; coordenada Y 
-    PUSH HL        ; direccion grafico +    push hl        ; direccion grafico 
-    CALL Rutina+    call Rutina
     (...)     (...)
  
 Rutina: Rutina:
-    POP HL         ; HL = coordenada Y+    pop hl         ; HL = coordenada Y
  
     ; (trabajamos con HL)     ; (trabajamos con HL)
Línea 759: Línea 814:
                    ; Ahora recogemos el parametro 2,                    ; Ahora recogemos el parametro 2,
                    ; en HL o en cualquier otro registro                    ; en HL o en cualquier otro registro
-    POP HL         ; HL = coordenada X+    pop hl         ; HL = coordenada X
  
     ; (hacemos calculos con HL)     ; (hacemos calculos con HL)
  
-    POP DE         ; HL = direccion grafico+    pop de         ; HL = direccion grafico
  
-    RET+    ret
 </code> </code>
  
Línea 773: Línea 828:
  
 <code z80> <code z80>
-    PUSH BC        ; coordenada X +    push bc        ; coordenada X 
-    PUSH DE        ; coordenada Y +    push de        ; coordenada Y 
-    CALL Rutina+    call Rutina
  
 Rutina: Rutina:
-    LD HL, 2       ; 2 bytes = direccion de retorno introducida por CALL +    ld hl, 2       ; 2 bytes = direccion de retorno introducida por call 
-    ADD HLSP     ; Ahora SP apunta al ultimo parametro metido+    add hlsp     ; Ahora SP apunta al ultimo parametro metido
                    ; en la pila por el compilador (valor de Y)                    ; en la pila por el compilador (valor de Y)
  
-    LD C, (HL+    ld c, (hl
-    INC HL +    inc hl 
-    LD B, (HL+    ld b, (hl
-    INC HL         ; Ahora BC = Y+    inc hl         ; Ahora BC = Y
  
-    LD E, (HL+    ld e, (hl
-    INC HL +    inc hl 
-    LD D, (HL+    ld d, (hl
-    INC HL         ; Ahora, DE = X+    inc hl         ; Ahora, DE = X
  
     ;;; (ahora hacemos lo que queramos en asm)     ;;; (ahora hacemos lo que queramos en asm)
  
-    POP DE +    pop de 
-    POP BC          ; Tambien podriamos simplemente restar N a SP+    pop bc          ; Tambien podriamos simplemente restar N a SP
 </code> </code>
  
Línea 802: Línea 857:
 <code z80> <code z80>
 Rutina: Rutina:
-    LD HL, 2            ; 2 bytes = direccion de retorno introducida por CALL +    ld hl, 2            ; 2 bytes = direccion de retorno introducida por call 
-    ADD HLSP          ; Ahora SP apunta al ultimo parametro metido en pila+    add hlsp          ; Ahora SP apunta al ultimo parametro metido en pila
  
-    LD A, (HL)          ; Aquí tenemos nuestro dato de 8 bits (Y) +    ld a, (hl)          ; Aquí tenemos nuestro dato de 8 bits (Y) 
-    LD BA +    ld ba 
-    INC HL +    inc hl 
-    INC HL              ; La parte alta del byte no nos interesa+    inc hl              ; La parte alta del byte no nos interesa
  
-    LD A, (HL)          ; Aquí tenemos nuestro dato de 8 bits (X) +    ld a, (hl)          ; Aquí tenemos nuestro dato de 8 bits (X) 
-    LD CA +    ld ca 
-                        ; Aqui no necesitamos mas INC HL, no hay mas parametros+                        ; Aqui no necesitamos mas inc hl, no hay mas parametros
  
     ; Nuestra rutina empieza a trabajar aquí     ; Nuestra rutina empieza a trabajar aquí
Línea 839: Línea 894:
 <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>
  
Línea 864: Línea 919:
  
     ; Probamos de diferentes formas nuestra rutina     ; Probamos de diferentes formas nuestra rutina
-    LD B, 10 +    ld b, 10 
-    LD C, 15 +    ld c, 15 
-    LD HL, sprite +    ld hl, sprite 
-    CALL DrawSprite +    call DrawSprite 
-    RET+    ret
  
 ; Rutina DrawSprite ; Rutina DrawSprite
Línea 874: Línea 929:
 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...)
Línea 881: Línea 936:
 </code> </code>
  
- 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.+ 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.
  
  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.  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.
Línea 889: Línea 944:
  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.  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.
  
- 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.+ 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.
  
  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.  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.
Línea 927: 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}}
  • cursos/ensamblador/lenguaje_4.1705217670.txt.gz
  • Última modificación: 14-01-2024 07:34
  • por sromero