Diferencias
Muestra las diferencias entre dos versiones de la página.
Ambos lados, revisión anterior Revisión previa Próxima revisión | Revisión previa | ||
cursos:ensamblador:lenguaje_4 [10-01-2024 08:21] – [Método 3: Uso de la pila (método C)] sromero | cursos: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 " | + | Su nombre viene del hecho que los datos se almacenan unos " |
| | ||
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: | + | 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: |
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: | 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: | ||
<code z80> | <code z80> | ||
- | | + | |
- | | + | |
- | | + | |
(...) ; Operamos con BC | (...) ; Operamos con BC | ||
- | | + | |
- | | + | |
- | ; (recordemos que no existe "LD HL, BC", de modo que | + | ; (recordemos que no existe "ld hl, bc", de modo que |
; lo almacenamos como HL = 0+BC | ; lo almacenamos como HL = 0+BC | ||
- | | + | |
; recuperamos el valor que tenia BC (1000). | ; recuperamos el valor que tenia BC (1000). | ||
</ | </ | ||
- | La instrucción | + | La instrucción |
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> | ||
- | | + | |
- | | + | |
- | | + | |
</ | </ | ||
+ | (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, | ||
+ | |||
Si ahora hacemos: | Si ahora hacemos: | ||
<code z80> | <code z80> | ||
- | | + | |
</ | </ | ||
Línea 72: | Línea 74: | ||
< | < | ||
SP = SP - 2 = 65533 | SP = SP - 2 = 65533 | ||
- | (SP) = BC = $00FF | + | (SP) = BC = $00ff |
</ | </ | ||
Línea 80: | Línea 82: | ||
Celdilla | Celdilla | ||
| | ||
- | | + | |
SP -> 65533 $00 | SP -> 65533 $00 | ||
</ | </ | ||
Línea 87: | Línea 89: | ||
<code z80> | <code z80> | ||
- | | + | |
</ | </ | ||
Línea 94: | Línea 96: | ||
< | < | ||
SP = SP - 2 = 65531 | SP = SP - 2 = 65531 | ||
- | (SP) = DE = $AABB | + | (SP) = DE = $aabb |
</ | </ | ||
Línea 102: | Línea 104: | ||
Celdilla | Celdilla | ||
| | ||
- | | + | |
| | ||
- | | + | |
- | SP -> 65531 $BB | + | SP -> 65531 $bb |
</ | </ | ||
- | Si ahora hacemos un POP: | + | Si ahora hacemos un '' |
<code z80> | <code z80> | ||
- | | + | |
</ | </ | ||
Línea 117: | Línea 119: | ||
< | < | ||
- | DE = (SP) = $AABB | + | DE = (SP) = $aabb |
SP = SP + 2 = 65533 | SP = SP + 2 = 65533 | ||
</ | </ | ||
Línea 126: | Línea 128: | ||
Celdilla | Celdilla | ||
| | ||
- | | + | |
SP -> 65533 $00 | SP -> 65533 $00 | ||
</ | </ | ||
- | 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, | + | Con el objetivo de que el ejemplo fuera más comprensible, |
- | 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 '' |
\\ | \\ | ||
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 | + | * '' |
- | * 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: | ||
< | < | ||
- | PUSH xx : | + | push xx : |
| | ||
(SP) = xx | (SP) = xx | ||
- | POP xx : | + | pop xx : |
xx = (SP) | xx = (SP) | ||
SP = SP+2 | SP = SP+2 | ||
+ | </ | ||
+ | |||
+ | Visto en " | ||
+ | |||
+ | <code z80> | ||
+ | push hl = LET SP = SP-2 | ||
+ | POKE (SP+1), H | ||
+ | POKE SP, L | ||
+ | |||
+ | pop hl | ||
+ | LET H = PEEK (SP+1) | ||
+ | LET SP = SP+2 | ||
</ | </ | ||
Línea 162: | Línea 176: | ||
| | ||
| | ||
- | POP xx |- - - - - -| | + | pop xx |- - - - - -| |
- | PUSH xx |- - - - - -| | + | push xx |- - - - - -| |
</ | </ | ||
- | | + | |
\\ | \\ | ||
Línea 174: | Línea 188: | ||
\\ | \\ | ||
- | * Intercambiar valores | + | * Uno de sus usos más evidentes |
- | + | ||
- | <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) | + | |
- | </ | + | |
- | + | ||
- | \\ | + | |
- | * Para manipular el registro F: La instrucción **POP AF** es la principal forma de manipular | + | |
- | + | ||
- | * Almacenaje de datos mientras ejecutamos porciones de código: Supongamos 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> | ||
- | | + | |
- | (código) | + | (código) |
- | | + | |
</ | </ | ||
Línea 202: | Línea 202: | ||
<code z80> | <code z80> | ||
- | | + | |
- | | + | |
bucle: | bucle: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
</ | </ | ||
- | 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 |
<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> | ||
- | | + | |
bucle_externo: | bucle_externo: | ||
- | | + | |
- | | + | |
bucle_interno: | bucle_interno: | ||
(... código ...) | (... código ...) | ||
- | | + | |
- | | + | |
- | | + | |
</ | </ | ||
- | 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 '' |
<code z80> | <code z80> | ||
- | | + | |
bucle_externo: | bucle_externo: | ||
- | | + | |
- | | + | |
bucle_interno: | bucle_interno: | ||
- | (... código ...) ; En este codigo no podemos usar D | + | (... código ...) |
- | | + | |
- | | + | |
- | | + | |
</ | </ | ||
- | 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, |
\\ | \\ | ||
- | * Almacenaje de datos de entrada y salida en subrutinas: Podemos pasar parámetros a nuestras rutinas apilándolos | + | * Como veremos |
- | | + | \\ |
+ | | ||
- | | + | \\ |
+ | | ||
+ | |||
+ | <code z80> | ||
+ | push af | ||
+ | pop bc ; Ahora tenemos | ||
+ | </code> | ||
\\ | \\ | ||
+ | * El puntero de pila se puede utilizar también (como veremos más adelante en código más avanzado) para escribir ('' | ||
- | Recordad también que tenéis instrucciones | + | \\ |
+ | * Intercambiar valores | ||
<code z80> | <code z80> | ||
- | EX (SP), HL | + | ; Simulando "EX DE, BC" |
- | 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) | ||
</ | </ | ||
+ | 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 '' | ||
+ | |||
+ | <code z80> | ||
+ | ; Simulando "ex de, bc" | ||
+ | push hl | ||
+ | ld l, c | ||
+ | ld h, b | ||
+ | pop bc | ||
+ | </ | ||
+ | |||
+ | No sólo podemos intercambiar valores usando 2 '' | ||
+ | |||
+ | <code z80> | ||
+ | ex (sp), hl | ||
+ | ex (sp), ix | ||
+ | ex (sp), iy | ||
+ | </ | ||
+ | |||
+ | 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 '' | ||
+ | |||
+ | <code z80> | ||
+ | push bc | ||
+ | ex (sp), ix ; IX = el valor de BC | ||
+ | pop bc ; BC = el valor de IX | ||
+ | </ | ||
+ | |||
+ | 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, | + | * Hacer más '' |
- | * 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 '' |
- | * 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, | + | * 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 '' |
- | | + | |
<code z80> | <code z80> | ||
; Este programa se colgará (probablemente, | ; Este programa se colgará (probablemente, | ||
; pero en cualquier caso, no seguirá su ejecución normal. | ; pero en cualquier caso, no seguirá su ejecución normal. | ||
- | | + | |
- | | + | |
(código) | (código) | ||
- | | + | |
- | | + | |
; 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: | ||
- | | + | |
(código que usa B) | (código que usa B) | ||
- | | + | |
- | | + | |
</ | </ | ||
Línea 327: | Línea 381: | ||
<code z80> | <code z80> | ||
bucle: | bucle: | ||
- | | + | |
(código) | (código) | ||
- | | + | |
- | | + | |
</ | </ | ||
Línea 337: | Línea 391: | ||
<code z80> | <code z80> | ||
- | | + | |
bucle: | bucle: | ||
(código) | (código) | ||
- | | + | |
- | | + | |
</ | </ | ||
- | 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. | + | Y una curiosidad al respecto de la pila y la sentencia |
\\ | \\ | ||
Línea 355: | 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 " | + | Si lo ponemos por debajo (con el '' |
- | 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. | + | Lo más sencillo es usar '' |
- | 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 BASIC. Como ya vimos, para eso se recomendaba un valor mínimo de **ORG 33500** o **ORG 34000**. Esto nos dejará un tamaño de la pila suficiente, con los primeros 708 bytes (ORG 33500) y 1232 bytes (ORG 34000) fuera de la Contended Memory. Todas estas consideraciones se explican en el capítulo dedicado a Consideraciones Avanzadas. | + | 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 BASIC. Como ya vimos, para eso se recomendaba un valor mínimo de '' |
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 " | 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 " | ||
- | 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 '' |
Línea 385: | Línea 439: | ||
Esto es lo que se conoce como " | Esto es lo que se conoce como " | ||
- | 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, | + | 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 |
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. | ||
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 " | 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 " | ||
- | 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, | + | 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, |
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. | ||
\\ | \\ | ||
- | ===== 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 " | ||
- | |||
- | Este área son 256 bytes, desde 23296 (5B00h) a 23551 (5BFFh). | ||
- | |||
- | Hay gente que pone la pila en la posición de memoria del buffer de impresión, que puede ser suficiente si no vamos a hacer más de 128 anidaciones (cada PUSH son 2 bytes), pero que puede causar problemas en los modelos de 128K, por lo que no es recomendable cambiar la pila a esta ubicación. | ||
- | |||
- | Aún así, podemos utilizar esa zona como " | ||
- | |||
- | 1.- Está en la Contented Memory, aunque no es un problema grande para lo que es el acceso puntual a variables. | ||
- | |||
- | 2.- Los modelos de 128 tienen ahí rutinas de paginación de las ROM de sintaxis 128 y también partes del intérpretes de 48. Si pretendemos volver al BASIC después de ejecutar nuestro programa, no podemos tocar ese área. | ||
- | |||
- | 3.- Tampoco podremos usarlo en modelos 128K si tenemos activas las interrupciones en IM1 (Modo de Interrupciones 1). Si usamos nuestra propia rutina ISR en IM2 y desde ella no llamamos a la IM1 (para que actualice FRAMES u otras variables del sistema), no debería haber problemas en utilizar ese área si vemos que nuestro programa crece y necesitamos arañar unos bytes para las variables del mismo. | ||
- | |||
- | \\ | ||
===== Subrutinas: CALL y RET ===== | ===== Subrutinas: CALL y RET ===== | ||
Línea 421: | Línea 459: | ||
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. | 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 | + | Para saltar a subrutinas utilizamos la instrucción |
- | 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 '' |
- | | + | |
<code z80> | <code z80> | ||
Línea 435: | Línea 473: | ||
SUMA_A_10: | SUMA_A_10: | ||
- | ADD A, 10 ; A = A + 10 | + | add a, 10 ; A = A + 10 |
- | LD B, A ; B = A | + | ld b, a ; B = A |
</ | </ | ||
| | ||
- | 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 '' |
<code z80> | <code z80> | ||
- | | + | |
- | | + | |
volver1: | volver1: | ||
Línea 454: | Línea 492: | ||
; Nota: Modifica el valor de A | ; Nota: Modifica el valor de A | ||
SUMA_A_10: | SUMA_A_10: | ||
- | | + | |
- | | + | |
- | | + | |
</ | </ | ||
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 " | + | 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 '' |
<code z80> | <code z80> | ||
- | | + | |
- | | + | |
volver1: | volver1: | ||
- | | + | |
- | | + | |
; Nunca llegariamos a volver aqui | ; Nunca llegariamos a volver aqui | ||
(...) | (...) | ||
SUMA_A_10: | SUMA_A_10: | ||
- | | + | |
- | | + | |
- | | + | |
</ | </ | ||
- | 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 484: | 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. | + | '' |
- | ¿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 | + | ¿Y para qué sirve eso? Para que lo aprovechemos dentro de nuestra subrutina con '' |
- | 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 '' |
< | < | ||
- | CALL NN equivale a: | + | call NN equivale a: |
PUSH PC | PUSH PC | ||
- | | + | |
- | RET equivale a: | + | ret equivale a: |
POP PC | POP PC | ||
</ | </ | ||
- | | + | |
<code z80> | <code z80> | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
(...) | (...) | ||
SUMA_A_10: | SUMA_A_10: | ||
- | | + | |
- | | + | |
- | | + | |
</ | </ | ||
- | 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 |
- | Al acabar la subrutina encontramos el RET, quien extrae de la pila el valor de PC anteriormente introducido, | + | Al acabar la subrutina encontramos el '' |
- | 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, y saltará | + | 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 '' |
<code z80> | <code z80> | ||
- | | + | |
- | | + | |
SUMA_A_10: | SUMA_A_10: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
; sino " | ; sino " | ||
</ | </ | ||
- | 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. | + | |
- | Ni CALL ni RET afectan a la tabla de flags del registro F. | + | |
< | < | ||
Línea 545: | Línea 583: | ||
| | ||
| | ||
- | CALL NN |- - - - - -| | + | call NN |- - - - - -| |
- | RET |- - - - - -| | + | ret |- - - - - -| |
</ | </ | ||
Línea 554: | 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, | + | '' |
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 560: | 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 '' |
<code z80> | <code z80> | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
</ | </ | ||
- | Por contra, | + | Por contra, |
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 y RET es que tienen instrucciones condicionales con respecto al estado de los flags, igual que " | + | Una de las peculiaridades de call y ret es que tienen instrucciones condicionales con respecto al estado de los flags, igual que '' |
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 613: | Línea 651: | ||
; lo primero, comprobamos que BC no sea cero: | ; lo primero, comprobamos que BC no sea cero: | ||
- | | + | |
- | | + | |
; Si BC es cero, activará el flag Z | ; Si BC es cero, activará el flag Z | ||
- | | + | |
(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. |
</ | </ | ||
- | 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. | + | Del mismo modo, el uso de '' |
- | Al igual que CALL y RET, sus versiones condicionales no afectan al estado de los flags. | + | Al igual que '' |
< | < | ||
Línea 630: | Línea 668: | ||
| | ||
| | ||
- | CALL cc, NN |- - - - - -| IF cc CALL NN | + | call cc, NN |- - - - - -| IF cc call NN |
- | RET cc |- - - - - -| IF cc RET | + | ret cc |- - - - - -| IF cc ret |
</ | </ | ||
Línea 643: | Línea 681: | ||
==== Método 1: Uso de registros ==== | ==== Método 1: Uso de registros ==== | ||
- | 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. | + | Este método consiste en modificar unos registros concretos antes de hacer el '' |
+ | |||
+ | 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. | ||
Por ejemplo: | Por ejemplo: | ||
Línea 649: | Línea 689: | ||
<code z80> | <code z80> | ||
; | ; | ||
- | ; MULTIPLI: Multiplica DE*BC | + | ; Mult_HL_DE: Multiplica DE*BC |
- | ; | + | ; |
- | ; | + | ; Entrada: |
- | ; | + | ; |
+ | ; Salida: | ||
+ | ; Modifica: | ||
; | ; | ||
- | MULTIPLICA: | + | Mult_HL_DE: |
- | | + | |
+ | push bc ; Preservamos BC porque su valor se pierde | ||
+ | ld hl, 0 | ||
+ | |||
+ | multiloop_01: | ||
+ | add hl, de | ||
+ | dec bc | ||
+ | ld a, b | ||
+ | or c | ||
+ | jr nz, multiloop_01 | ||
- | MULTI01: | + | pop bc ; Rescatamos el valor de BC |
- | ADD HL, DE | + | |
- | DEC BC | + | |
- | | + | |
- | OR C | + | |
- | JR NZ, MULTI01 | + | |
- | | + | |
</ | </ | ||
- | Antes de hacer la llamada a MULTIPLICA, tendremos que cargar en DE y en BC los valores que queremos multiplicar, | + | Antes de hacer la llamada a '' |
- | | + | |
Con este tipo de funciones resulta importantísimo realizarse cabeceras de comentarios explicativos, | Con este tipo de funciones resulta importantísimo realizarse cabeceras de comentarios explicativos, | ||
Línea 677: | 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 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, | + | Con este tipo de paso de parámetros tenemos el mayor ahorro y la mayor velocidad: no se accede a la memoria, |
- | Si no queremos | + | Como hemos visto, si queremos |
<code z80> | <code z80> | ||
MiFuncion: | MiFuncion: | ||
- | | + | |
- | | + | |
(...) | (...) | ||
- | | + | |
- | | + | |
- | | + | |
</ | </ | ||
- | 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). | + | 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). |
+ | |||
+ | 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 AF, AF', 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 ('' |
\\ | \\ | ||
- | ==== Método 2: Uso de localidades de memoria ==== | + | ==== Método 2: Uso de variables en memoria ==== |
| | ||
Línea 705: | Línea 754: | ||
<code z80> | <code z80> | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
(...) | (...) | ||
MiFuncion: | MiFuncion: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | (Codigo) | + | (... código ...) |
- | | + | |
- | | + | |
- | | + | |
x DB 0 | x DB 0 | ||
Línea 736: | Línea 785: | ||
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, | 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, | ||
- | La ventaja del paso de parámetros por memoria | + | Este método se puede pues utilizar en conjunción con el anterior pasando algunos |
+ | 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 '' | ||
\\ | \\ | ||
Línea 744: | Línea 794: | ||
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, | 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, | ||
- | 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, | + | 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, |
- | En ensamblador no es normal utilizar este método a menos que tengamos muchos parámetros, | + | En ese caso, simplemente apilamos los parámetros con '' |
- | + | ||
- | En ese caso, simplemente apilamos los parámetros con PUSH y dentro de la rutina los vamos recogiendo con POP: | + | |
| | ||
<code z80> | <code z80> | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
(...) | (...) | ||
Rutina: | Rutina: | ||
- | | + | |
- | (trabajamos con HL) | + | |
- | + | | |
- | | + | |
- | (hacemos calculos con HL) | + | ; Ahora recogemos el parametro 2, |
- | + | ; en HL o en cualquier otro registro | |
- | | + | pop hl ; HL = coordenada X |
- | + | ||
- | | + | |
+ | |||
+ | | ||
+ | |||
+ | | ||
</ | </ | ||
- | | + | Nótese que hacemos '' |
+ | |||
+ | También podemos usar acceso directo a memoria mediante el valor de SP saltándonos los 2 bytes de la dirección de retorno introducida en la pila por el '' | ||
<code z80> | <code z80> | ||
- | | + | |
- | | + | |
- | | + | |
- | + | ||
Rutina: | Rutina: | ||
- | | + | |
- | | + | |
; en la pila por el compilador (valor de Y) | ; en la pila por el compilador (valor de Y) | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
;;; (ahora hacemos lo que queramos en asm) | ;;; (ahora hacemos lo que queramos en asm) | ||
+ | |||
+ | pop de | ||
+ | pop bc ; Tambien podriamos simplemente restar N a SP | ||
</ | </ | ||
- | Los parámetros de 8 bits en este caso se tienen que pasar como 16 bites con la parte alta a 0, ya que la pila usa 2 bytes en PUSH y POP. Si usamos el valor de SP para acceder a la pila, simplemente tenemos que ignorar | + | Los parámetros de 8 bits en este caso se tienen que pasar como 16 bits con la parte alta a 0, ya que la pila usa 2 bytes en PUSH y 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 |
<code z80> | <code z80> | ||
Rutina: | Rutina: | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | INC HL | + | ; Aqui no necesitamos mas inc hl, no hay mas parametros |
- | INC HL | + | |
; Nuestra rutina empieza a trabajar aquí | ; Nuestra rutina empieza a trabajar aquí | ||
</ | </ | ||
- | Por contra, para devolver valores no se utiliza la pila (dado que no podemos tocarla), sino que se utilizarán registros o direcciones de memoria. | + | Por contra, para devolver valores no se utiliza la pila (dado que no podemos tocarla |
+ | Como hemos comentado, en programas íntegramente escritos en ensamblador usaremos este método muy raras veces. | ||
\\ | \\ | ||
Línea 827: | Línea 884: | ||
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 " | 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 " | ||
- | 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). | + | Por contra, si creamos una subrutina, digamos, |
| | ||
Línea 837: | Línea 894: | ||
<code z80> | <code z80> | ||
BuclePrincipal: | BuclePrincipal: | ||
- | | + | |
- | | + | |
- | | + | |
- | jp Bucle_Principal | + | jp BuclePrincipal |
- | Leer_Teclado: | + | LeerTeclado: |
- | | + | |
- | Logica_Juego: | + | LogicaJuego: |
- | | + | |
- | Comprobar_Estado: | + | ComprobarEstado: |
- | | + | |
</ | </ | ||
Línea 862: | Línea 919: | ||
; Probamos de diferentes formas nuestra rutina | ; Probamos de diferentes formas nuestra rutina | ||
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
; Rutina DrawSprite | ; Rutina DrawSprite | ||
Línea 872: | Línea 929: | ||
DrawSprite: | DrawSprite: | ||
(aquí el código) | (aquí el código) | ||
- | | + | |
sprite DB 0, | sprite DB 0, | ||
Línea 879: | Línea 936: | ||
</ | </ | ||
- | | + | |
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, | 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, | ||
Línea 887: | 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, | 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, | ||
- | 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, | + | 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, |
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 905: | Línea 962: | ||
| | ||
- | 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/ | + | 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/ |
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 925: | Línea 982: | ||
* {{cursos: | * {{cursos: | ||
* {{cursos: | * {{cursos: | ||
- | * {{cursos: | + | * {{cursos: |
* {{cursos: | * {{cursos: | ||
* {{cursos: | * {{cursos: |