En este último número de la revista hacemos una promoción 2x1. Llévese los dos últimos
artículos del curso de ensamblador por el precio de uno.
Lenguaje Ensamblador del Z80 (IV)
La pila y las llamadas a subrutinas
Los 2 temas que vamos a tratar hoy, la pila por un lado y las llamadas a subrutinas por
otro, están íntimamente relacionados. Como veremos, la pila del Spectrum es aquello que
permite la utilización de rutinas a las que pasaremos parámetros, y que volverán al
punto donde fueron llamadas tras su ejecución.
La pila del Spectrum
Hoy vamos a tratar una de las cosas más importantes del microprocesador Z80: la
pila o Stack (en inglés).
La pila, teóricamente, es una porción de memoria donde se pueden almacenar valores de
16 bits.
Su nombre viene del hecho que los datos se almacenan unos encima de otros, como en una
pila de platos. Cuando almacenamos un nuevo plato en una pila, lo dejamos encima del
todo de la misma, sobre el plato anterior. Cuando queremos coger un plato, cogemos el
plato más alto, el que está en la parte más alta de la pila.
Las pilas son una estructura de datos conocida como estructura LIFO: Last In, First
Out: el último que entró es el primero que sale. En nuestro ejemplo de los platos,
efectivamente cuando retiramos un plato (el primero que sale) extraemos el que está
arriba del todo (el último que habíamos dejado). En una pila de ordenador (como en
nuestra pila de datos) sólo podemos trabajar con el dato (o plato) que está arriba del
todo de la pila: no podemos extraer uno de los platos intermedios. Sólo podemos apilar
un plato nuevo y desapilar el plato apilado arriba del todo de la pila.
La pila del Spectrum no es de platos sino de valores numéricos de 16 bits. Introducimos
valores y sacamos valores mediante 2 instrucciones concretas: PUSH
y POP
, donde normalmente
será un registro (metemos en la pila el valor que contiene un registro de 16
bits, o bien leemos de la pila un valor y lo asignamos a un registro de 16 bits).
Por ejemplo, podemos guardar el valor que contiene un registro en la pila si tenemos que
hacer operaciones con ese registro para así luego recuperarlo tras realizar la tarea que
tengamos que realizar:
LD BC, 1000
PUSH BC ; Guardamos el contenido de BC en la pila
LD BC, 2000
(...) ; Operamos con BC
LD HL, 0
ADD HL, BC ; y ya podemos guardar el resultado de la operación
; (recordemos que no existe "LD HL, BC", de modo que
; lo almacenamos como HL = 0+BC
POP BC ; Hemos terminado de trabajar con BC, ahora
; recuperamos el valor que tenia BC=1000.
|
La instrucción PUSH BC lo que ha hecho es introducir en memoria, en lo alto de la pila,
el valor 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).
El Z80 tiene un registro conocido como SP (Stack Pointer), o puntero de
pila, que es un registro de 16 bits que contiene una dirección de memoria. Esa
dirección de memoria es la cabeza de la pila: apunta al próximo lugar donde
almacenaremos un dato (al hueco donde guardaremos el próximo plato).
La peculiaridad de la pila del Spectrum es que crece hacia abajo, en lugar de
hacia arriba. Veamos un ejemplo práctico:
|
Como crece y decrece la pila del Z80 |
Supongamos que SP (puntero de pila) apunta a 65535 (la última posición de la memoria).
Hagamos una serie de PUSHes y POPs de registros de 16 bits. Sea el siguiente
programa:
LD BC, 00FFh
LD DE, AABBh
LD SP, 65535 ; Puntero de pila al final de la memoria
|
Si ahora hacemos:
PUSH BC ; Apilamos el registro BC
|
Lo que estaremos haciendo es:
SP = SP - 2 = 65533
(SP) = BC = 00FFh
|
Con lo que el contenido de la memoria sería:
|
Celdilla |
Contenido |
|
65534 |
FFh |
SP -> |
65533 |
00h |
Si a continuación hacemos otro PUSH:
PUSH DE ; Apilamos el registro DE
|
Lo que estaremos haciendo es:
SP = SP - 2 = 65531
(SP) = DE = AABBh
|
Con lo que el contenido de las celdillas de memoria sería:
|
Celdilla |
Contenido |
|
65534 |
FFh |
|
65533 |
00h |
|
65532 |
AAh |
SP -> |
65531 |
BBh |
Si ahora hacemos un POP:
Lo que hacemos es:
DE = (SP) = AABBh
SP = SP + 2 = 65533
|
Y la memoria queda, de nuevo, como:
|
Celdilla |
Contenido |
|
65534 |
FFh |
SP -> |
65533 |
00h |
Como podemos ver, PUSH va apilando valores, haciendo decrecer el valor de SP,
mientras que POP recupera valores, haciendo crecer (en 2 bytes, 16 bits)
el valor de SP.
PUSH y POP
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:
PUSH xx :
SP = SP-2
(SP) = xx
POP xx :
xx = (SP)
SP = SP+2
|
Nótese cómo la pila se decrementa ANTES de poner los datos en ella, y se incrementa
DESPUES de sacar datos de la misma. Esto mantiene siempre SP apuntando al TOS (Top
Of Stack).
Flags
Instrucción |S Z H P N C|
----------------------------------
POP xx |- - - - - -|
PUSH xx |- - - - - -|
|
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).
Utilidad de la pila del Spectrum
La pila resulta muy útil para gran cantidad de tareas en programas en ensamblador. Veamos
algunos ejemplos:
- Intercambiar valores de registros mediante PUSH y POP. Por ejemplo, para
intercambiar el valor de BC y de DE:
PUSH BC ; Apilamos BC
PUSH DE ; Apilamos DE
POP BC ; Desapilamos BC
; ahora BC=(valor apilado de DE)
POP DE ; Desapilamos DE
; ahora DE=(valor apilado de BC)
|
- POP AF es la principal forma de manipular el registro F directamente (haciendo PUSH
de otro registro y POP de AF).
- Almacenaje de datos mientras ejecutamos porciones de có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:
PUSH BC ; Guardamos el valor de BC
(código) ; Hacemos operaciones
POP BC ; Recuperamos el valor que teníamos en BC
|
Esto incluye, por ejemplo, el almacenaje del valor de BC en los bucles cuando necesitamos
operador con B, C o BC:
LD A, 0
LD B, 100
bucle:
PUSH BC ; Guardamos BC
LD B, 1
ADD A, B
POP BC ; Recuperamos BC
DJNZ 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:
FOR I=0 TO 20:
FOR J=0 TO 100:
CODIGO
NEXT J
NEXT I
|
En ensamblador podríamos hacer:
LD B, 20 ; repetimos bucle externo 20 veces
bucle_externo:
PUSH BC ; Nos guardamos el valor de BC
LD B, 100 ; Iteraciones del bucle interno
bucle_interno:
(... código ...)
DJNZ bucle_interno ; FOR J=0 TO 100
POP BC ; Recuperamos el valor de B
DJNZ bucle_externo ; FOR I=0 TO 20
|
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:
LD B, 20 ; repetimos bucle externo 20 veces
bucle_externo:
LD D, B ; Nos guardamos el valor de B
LD B, 100 ; Iteraciones del bucle interno
bucle_interno:
(... código ...) ; En este codigo no podemos usar D
DJNZ bucle_interno ; FOR J=0 TO 100
LD B, D ; Recuperamos el valor de B
DJNZ bucle_externo ; FOR I=0 TO 20
|
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.
- 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.
- 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.
- 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).
Recordad también que tenéis instrucciones de intercambio (EX) que permiten manipular la
pila. Hablamos de:
EX (SP), HL
EX (SP), IX
EX (SP), IY
|
Los peligros de la pila
Pero como todo arma, las pilas también tienen un doble filo. Mal utilizada puede dar
lugar a enormes desastres en nuestros programas. Veamos algunos de los más
habituales:
- 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 machacar nuestros datos con los valores de los
registros 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 el 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.
- 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 machacarlo. 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, 23999, 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:
; Este programa se colgará (probablemente, depende de BC)
; pero en cualquier caso, no seguirá su ejecución normal.
PUSH BC
PUSH DE
(código)
POP DE
RET ; En lugar de volver a la dirección de memoria
; a la que teníamos que volver, volveremos a
; la dirección apuntada por el valor de BC, que
; no hemos recogido de la pila.
|
También hay que tener cuidado con los bucles:
bucle:
PUSH BC ; Nos queremos guardar BC
(código que usa B)
JR flag, bucle
POP BC
|
En ese código hacemos múltiples PUSHes pero un sólo POP. Probablemente, en realidad,
queremos hacer lo siguiente:
PUSH BC ; Nos queremos guardar BC
bucle:
(código)
JR flag, bucle
POP BC
|
Y una curiosidad al respecto de la pila y la sentencia CLEAR de BASIC: en el
fondo, lo que realiza esta función es cambiar el valor de la variable del sistema
RAMTOP, lo que implica cambiar el valor de SP. Así, con CLEAR XXXX, ponemos
la pila colgando de la dirección de memoria XXXX, asegurándonos de que BASIC no
pueda hacer crecer la pila de forma que machaque código máquina que hayamos cargado
nosotros en memoria. Si, por ejemplo, vamos a cargar todo nuestro código a partir de
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.
Rutinas: CALL y RET
Ya de por sí el lenguaje ensamblador es un lenguaje de listados largos y enrevesados, y
donde teníamos 10 líneas en BASIC podemos tener 100 ó 1000 en ensamblador.
Lo normal para hacer el programa más legible es utilizar bloques de código que hagan
unas funciones concretas y a los cuales podamos llamar a lo largo de nuestro
programa. Esos bloques de código son las funciones o subrutinas.
Las subrutinas son bloques de código máquina a las cuales saltamos, hacen su tarea
asignada, y devuelven el control al punto en que fueron llamadas. A veces, esperan
recibir los registros con una serie de valores y devuelven registros con los valores
resultantes.
Por ejemplo:
; SUMA_A_10
;
; SUMA 10 a A y devuelve el resultado en B
;
; Nota: Modifica el valor de A
SUMA_A_10:
ADD A, 10 ; A = A + 10
LD B, A ; B = A
|
Nuestra función/subrutina de ejemplo espera obtener en A un valor, y devuelve el
resultado de su ejecución en B. Antes de llamar a esta rutina, nosotros deberemos poner
en A el valor sobre el que actuar, y posteriormente interpretar el resultado (sabiendo
que lo tenemos en B).
Pero, ¿cómo llamamos a las subrutinas y volvemos de ellas? Podéis pensar en nuestro
ejemplo anterior y la orden JP:
LD A, 35
JP SUMA_A_10
volver1:
(...)
SUMA_A_10:
ADD A, 10 ; A = A + 10
LD B, A ; B = A
JP volver1 ; Volvemos de la subrutina
|
En este caso, cargaríamos A con el valor 35, saltaríamos a la subrutina, sumaríamos 10 a
A (pasando a valer 45), haríamos B = 45, y volveríamos al lugar posterior al punto de
llamada. 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.
LD A, 35
JP SUMA_A_10
volver1:
LD A, 50
JP SUMA_A_10
; Nunca llegariamos a volver aqui
(...)
SUMA_A_10:
ADD A, 10 ; A = A + 10
LD B, A ; B = A
JP volver1 ; Volvemos de la subrutina
|
Para evitar ese enorme problema es para lo que se usa 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ído y decodificado el CALL apunta a la siguiente
instrucción tras el mismo.
¿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).
CALL NN equivale a:
PUSH PC
JP NN
RET equivale a:
POP PC
Veamos la aplicación de CALL y RET con nuestro ejemplo anterior:
LD A, 35
CALL SUMA_A_10
LD A, 50
CALL SUMA_A_10
LD C, B
(...)
SUMA_A_10:
ADD A, 10 ; A = A + 10
LD B, A ; B = A
RET ; Volvemos 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 C, B. 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, y saltará allí. Por
ejemplo:
CALL SUMA_A_10
LD C, B ; Esta dirección se introduce en la pila con CALL
SUMA_A_10:
LD DE, $0000
PUSH DE
ADD A, 10
LD B, a
RET ; RET no sacará de la pila lo introducido por CALL
; sino "0000", el valor que hemos pulsado nosotros.
|
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.
Flags
Instrucción |S Z H P N C|
----------------------------------
CALL NN |- - - - - -|
RET |- - - - - -|
|
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 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:
- CALL flag, NN : Salta sólo si FLAG está activo.
- RET flag : Vuelve sólo si FLAG está activo.
Por ejemplo, supongamos que una de nuestras subrutinas tiene que comprobar que uno de los
parámetros que le pasamos, BC, no sea 0.
; Copia_Pantalla:
;
; Entrada:
; HL = direccion origen
; DE = direccion destino
; BC = bytes a copiar
;
Copia_Pantalla:
; lo primero, comprobamos que BC no sea cero:
LD A, B
OR C ; Hacemos un OR de B sobre C
; Si BC es cero, activará el flag Z
RET Z ; Si BC es cero, volvemos sin hacer nada
(más código)
; 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.
Al igual que CALL y RET, sus versiones condicionales no afectan al estado de los
flags.
Flags
Instrucción |S Z H P N C| Pseudocodigo
-----------------------------------------------------------
CALL cc, NN |- - - - - -| IF cc CALL NN
RET cc |- - - - - -| IF cc RET
|
Pasando parametros a rutinas
Ahora que ya sabemos crear rutinas y utilizarlas, vamos a ver los 3 métodos que hay para
pasar y devolver parámetros a las funciones.
Mé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.
Por ejemplo:
; Función de multiplicación de 8 bits
;
; Entrada:
; H = valor 8 bits
; E = valor 8 bits
; Salida:
; HL = valor 16 bits H*E
; Modifica:
; B, D, L, FLAGS
;
MULTIPLICA:
LD L, 0
LD D, L ; L=0 AND D=0
LD B, 8
MULT_LOOP:
ADD HL, HL
JR NC, NOADD
ADD HL, DE
NOADD:
DJNZ MULT_LOOP
|
Antes de hacer la llamada a MULTIPLICA, tendremos que cargar en H y en E los valores que
queremos multiplicar, de modo que si estos valores están en otros registros o en
memoria, tendremos que moverlos a H y a E.
Además, sabemos que la salida nos será devuelta en HL, con lo que si dicho registro
(especialmente L en nuestro caso, ya que H es un parámetro de entrada) contiene algún
valor importante, deberemos preservarlo previamente.
Con este tipo de funciones resulta importantísimo realizarse cabeceras de comentarios
explicativos, que indiquen:
- a.- Qué función realiza la subrutina.
- b.- Qué registros espera como entrada.
- c.- Qué registros devuelve como 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, al que acudiremos más de una
vez cuando no recordemos en qué registros teníamos que pasarle los datos de entrada), y
asegurarnos de que ninguno de los registros extra que modifica están en uso antes de
llamar a la función, puesto que se verán alterados.
Si no queremos que la función modifique muchos registros además de los de entrada y
salida, siempre podemos poner una serie de PUSH y POP en su inicio y final, al
estilo:
MiFuncion:
PUSH BC
PUSH DE ; Nos guardamos sus valores
(...)
POP DE
POP BC ; Recuperamos sus valores
RET
|
En funciones que no sean críticas en velocidad, es una buena opción porque no tendremos
que preocuparnos por el estado de nuestros registros durante la ejecución de la
subrutina: al volver de ella tendrán sus valores originales (excepto aquellos de entrada
y salida que consideremos necesarios).
No nos olvidemos de que en algunos casos podemos usar el juego de registros alternativos
(EX AF, AF', EXX) para evitar algún PUSH o POP.
Método 2: Uso de localidades de memoria
Aunque no es una opción rápida, sí que es bastante efectivo y sencillo el uso de
variables o posiciones de memoria para pasar y recoger parámetros de funciones. Nos
ahorra el uso de muchos registros, y hace que podamos usar dentro de las funciones
prácticamente todos los registros. Se hace especialmente útil usando el juego de
registros alternativos.
Por ejemplo:
LD A, 10
LD (x), A
LD A, 20
LD (y), A
LD BC, 40
LD (size), BC ; Parametros de entrada a la funcion
CALL MiFuncion
(...)
MiFuncion:
EXX ; Preservamos TODOS los registros
LD A, (x)
LD B, A
LD A, (y)
LD BC, (size) ; Leemos los parametros
(Codigo)
LD (salida), a ; Devolvemos un valor
EXX
x DB 0
y DB 0
size DW 0
salida DB 0
|
Este es un ejemplo exagerado donde todos los parámetros se pasan en variables, pero lo
normal es usar un método mixto entre este y el anterior, pasando cosas en registros
excepto si nos quedamos sin ellos (por que una función requiere muchos parámetros, por
ejemplo), de forma que algunas cosas las pasamos con variables de memoria.
Método 3: Uso de la pila (método C)
El tercer método es el sistema que utilizan los lenguajes de alto nivel para pasar
parámetros a las funciones: el apilamiento de los mismos. Este sistema no se suele
utilizar en ensamblador, pero vamos a comentarlo de forma que os permita integrar
funciones en ASM dentro de programas escritos en C, como los compilables con el
ensamblador Z88DK.
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 debe utilizar el registro SP (una copia) para
acceder a los valores apilados en orden inverso. Estos valores son siempre de 16 bits
aunque las variables pasadas sean de 8 bits (en este caso ignoraremos el byte que no
contiene datos, el segundo).
Veamos unos ejemplos:
//-----------------------------------------------------------------
// Sea parte de nuestro programa en C:
int jugador_x, jugador_y;
jugador_x = 10;
jugador_y = 200;
Funcion( jugador_x, jugador_y );
(...)
//-----------------------------------------------------------------
int Funcion( int x, int y )
{
#asm
LD HL,2
ADD HL,SP ; Ahora SP apunta al ultimo parametro metido
; en la pila por el compilador (valor de Y)
LD C, (HL)
INC HL
LD B, (HL)
INC HL ; Ahora BC = y
LD E, (HL)
INC HL
LD D, (HL)
INC HL ; Ahora, DE = x
(ahora hacemos lo que queramos en asm)
#endasm
}
|
No tenemos que preocuparnos por hacer PUSH y POP de los registros para preservar su valor
dado que C lo hace automáticamente antes y después de cada #asm y #endasm.
El problema es que conforme crece el número de parámetros apilados, es posible que
tengamos que hacer malabarismos para almacenarlos, dado que no podemos usar HL (es
nuestro puntero a la pila en las lecturas). Veamos el siguiente ejemplo con 3
parámetros, donde tenemos que usar PUSH para guardar el valor de DE y EX DE, HL para
acabar asociando el valor final a HL:
//-----------------------------------------------------------------
int Funcion( int x, int y, int z )
{
#asm
LD HL,2
ADD HL,SP ; Ahora SP apunta al ultimo parametro metido
; en la pila por el compilador (tamanyo)
LD C, (HL)
INC HL
LD B, (HL)
INC HL ; Ahora BC = z
LD E, (HL)
INC HL
LD D, (HL)
INC HL ; Ahora, DE = y
PUSH DE ; Guardamos DE
LD E, (HL)
INC HL
LD D, (HL)
INC HL ; Usamos DE para leer el valor de x
EX DE, HL ; Ahora cambiamos x a HL
POP DE ; Y recuperamos el valor de y en DE
; (ahora hacemos lo que queramos en asm)
#endasm
}
|
La manera de leer bytes (char) pulsados en C es de la misma forma que leemos una palabra
de 16 bits, pero ignorando la parte alta. En realidad, como la pila es de 16 bits, el
compilador convierte el dato de 8 bits en uno de 16 (rellenando con ceros) y pulsa este
valor:
//-----------------------------------------------------------------
int Funcion( char x, y )
{
#asm
LD HL,2
ADD HL,SP ; Ahora SP apunta al ultimo parametro metido
; en la pila por el compilador (tamanyo)
LD A, (HL) ; Aquí tenemos nuestro dato de 8 bits (y)
LD B, A
INC HL
INC HL ; La parte alta del byte no nos interesa
LD A, (HL) ; Aquí tenemos nuestro dato de 8 bits (x)
LD C, A
INC HL
INC HL ; La parte alta del byte no nos interesa
(ahora hacemos lo que queramos en asm)
#endasm
}
|
En ocasiones, es posible que incluso tengamos que utilizar variables auxiliares de
memoria para guardar datos:
//-----------------------------------------------------------------
int Funcion( int x, int y, char z )
{
#asm
LD HL,2
ADD HL,SP ; Ahora SP apunta al ultimo parametro metido
; en la pila por el compilador (tamanyo)
LD C, (HL)
INC HL
LD B, (HL)
INC HL ; Ahora BC = y
LD (valor_y), BC ; nos lo guardamos, BC libre de nuevo
LD C, (HL)
INC HL
LD B, (HL)
INC HL
LD (valor_x), BC ; Nos lo guardamos, BC libre de nuevo
LD A, (HL)
LD (valor_z), A ; Nos guardamos el byte
INC HL
INC HL ; La parte alta del byte no nos interesa
(ahora hacemos lo que queramos en asm)
RET
valor_x defw 0
valor_y defw 0
valor_z defb 0
#endasm
}
|
Por contra, para devolver valores no se utiliza la pila (dado que no podemos tocarla),
sino que se utiliza un determinado registro. En el caso de Z88DK, se utiliza el registro
HL. Si la función es de tipo INT o CHAR en cuanto a devolución, el valor que dejemos en
HL será el que se asignará en una llamada de este tipo:
valor = MiFuncion_ASM( x, y, z);
|
Hemos considerado importante explicar este tipo de paso de parámetros y devolución de
valores porque nos permite integrar nuestro código ASM en programas en C.
Integracion de ASM en Z88DK
Para aprovechar esta introducción de uso de ASM en z88dk, veamos el código de alguna
función en C que use ASM internamente y que muestre, entre otras cosas, la lectura de
parámetros de la pila, el acceso a variables del código C, el uso de etiquetas, o la
devolución de valores.
//
// Devuelve la direccion de memoria del atributo de un caracter
// de pantalla, de coordenadas (x,y). Usando la dirección que
// devuelve esta función (en HL, devuelto en la llamada), podemos
// leer o cambiar los atributos de dicho carácter.
//
// Llamada: valor = Get_LOWRES_Attrib_Address( 1, 3 );
//
int Get_LOWRES_Attrib_Address( char x, char y )
{
#asm
LD HL, 2
ADD HL, SP ; Leemos x e y de la pila
LD D, (HL) ; d = y
INC HL ; Primero "y" y luego "x".
INC HL ; Como son "char", ignoramos parte alta.
LD E, (HL) ; e = x
LD H, 0
LD L, D
ADD HL, HL ; HL = HL*2
ADD HL, HL ; HL = HL*4
ADD HL, HL ; HL = HL*8
ADD HL, HL ; HL = HL*16
ADD HL, HL ; HL = HL*32
LD D, 0
ADD HL, DE ; Ahora HL = (32*y)+x
LD BC, 16384+6144 ; Ahora BC = offset attrib (0,0)
ADD HL, BC ; Sumamos y devolvemos en HL
#endasm
}
//
// Set Border
// Ejemplo de modificación del borde, muestra cómo leer variables
// globales de C en ASM, añadiendo "_" delante.
//
char my_tmp_border;
void BORDER( unsigned char value )
{
my_tmp_border = value<<3;
#asm
LD HL, 2
ADD HL, SP
LD A, (HL)
LD C, 254
OUT (C), A
LD HL, 23624
LD A, (_my_tmp_border)
LD (HL), A
#endasm
}
//
// Realización de un fundido de la pantalla hacia negro
// Con esta función se muestra el uso de etiquetas. Nótese
// como en lugar de escribirse como ":", se escriben sin
// ellos y con un punto "." delante.
//
void FadeScreen( void )
{
#asm
LD B, 16
.fadescreen_loop1
LD HL, 16384+6144
LD DE, 768
.fadescreen_loop2
LD A, (HL) ; get attr
LD C, A
AND 7 ; get last bits
JR Z, fadescreen_ink_zero ; if its zero, not dec it
DEC A
EX AF, AF
.fadescreen_ink_zero
LD A, C
AND 7
SRA A
SRA A
SRA A
JR Z, fadescreen_paper_zero ; if its zero, not dec it
DEC A
.fadescreen_paper_zero
SLA A
SLA A
SLA A
LD C, A
EX AF, AF
ADD A, C
LD (HL), A
INC HL
DEC DE ; internal loop
LD A, D
OR E
JP NZ, fadescreen_loop2
DJNZ fadescreen_loop1
#endasm
}
|
Si tenéis curiosidad por ver el funcionamiento de esta rutina de Fade (fundido), podéis
verla integramente en ASM en el fichero fade.asm. Un detalle a tener en cuenta, en Z88DK
se soporta EX AF, AF, mientras que pasmo requiere poner la comilla del
shadow-register: EX AF, AF'.
|
Captura durante el fade de la pantalla |
En la anterior captura podéis ver el aspecto de uno de los pasos del fundido.
La importancia de usar subrutinas
Usar subrutinas es mucho más importante de lo que parece a simple vista: nos permite
organizar el programa en unidades o módulos funcionales que cumplen una serie de
funciones específicas, lo que hace mucha más sencilla su depuración y optimización.
Si en el menú de nuestro juego estamos dibujando una serie de sprites móviles, y también
lo hacemos a lo largo del juego, resulta absurdo construir 2 bloques de código, uno
para mover los sprites del menú y otro para los del juego. Haciendo esto, si encontramos
un error en una de las 2 rutinas, o realizamos una mejora, deberemos corregirlo en
ambas.
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).
Aunque no sea el objetivo de esta serie de artículos, antes de sentarse a teclear, un
buen programador debería coger un par de folios de papel y hacer un pequeño análisis de
lo que pretende crear. Este proceso, la fase de diseño, define qué debe de hacer el
programa y, sobre todo, una división lógica de cuáles son las principales partes del
mismo. Un sencillo esquema en papel, un diagrama de flujo, identificar las diferentes
partes del programa, etc.
El proceso empieza con un esbozo muy general del programa, que será coincidente con la
gran mayoría de los juegos: inicialización de variables, menú (que te puede llevar bien
a las opciones o bien al juego en sí), y dentro del juego, lectura de teclado/joystick,
trazado de la pantalla, lógica del juego, etc.
Después, se teclea un programa vacío que siga esos pasos, pero que no haga nada; un bucle
principal que tenga un aspecto parecido a:
BuclePrincipal:
CALL Leer_Teclado
CALL Logica_Juego
CALL Comprobar_Estado
jp Bucle_Principal
Leer_Teclado:
RET
Logica_Juego:
RET
Comprobar_Estado:
RET
|
Tras esto, ya tenemos el esqueleto del programa. Y ahora hay que rellenar ese
esqueleto, y la mejor forma de hacerlo es aprovechar esa modularidad que hemos
obtenido con ese diseño en papel.
Por ejemplo, supongamos que nuestro juego tiene que poder dibujar sprites y pantallas
hechas a bases de bloques que se repiten (tiles). Gracias a nuestro diseño, sabemos que
necesitamos una rutina que imprima un sprite, una rutina que dibuje un tile y una rutina
que dibuje una pantalla llena de tiles.
Pues bien, creamos un programa en ASM nuevo, desde cero, y en él creamos una función
DrawSprite que acepte como parámetros la dirección origen de los datos del Sprite, y las
posiciones X e Y donde dibujarlo, y la realizamos. En este nuevo programa, pequeño,
sencillo de leer, realizamos todo tipo de pruebas:
ORG 50000
; Probamos de diferentes formas nuestra rutina
LD B, 10
LD C, 15
LD HL, sprite
CALL DrawSprite
RET
; Rutina DrawSprite
; Acepta como parametros ... y devuelve ...
DrawSprite:
(aquí el código)
RET
sprite DB 0,0,255,123,121,123,34, (etc...)
|
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.
Además, el definir de antemano qué tipo de subrutinas necesitamos y qué parámetros deben
aceptar o devolver permite trabajar en equipo. Si sabes que necesitarás una rutina que
dibuje un sprite, o que lea el teclado y devuelva la tecla pulsada, puedes decir los
registros de entrada y los valores de salida que necesitas, y que la realice una segunda
persona y te envíe la rutina lista para usar.
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 10×10 bloques de 8×8 pixeles cada uno.
Si hacemos una función de que dibuja un bloque de 8×8, podemos llamarla en un bucle para
dibujar nuestros 10×10 bloques.
Hay gente que, en lugar de esto, preferirá realizar una función específica que dibuje los
10×10 bloques dentro de una misma función. Esto es así porque de este modo te evitas 100
CALLs (10×10) 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.
Al final hay que llegar a un compromiso entre modularidad y optimización, en algunos
casos nos interesará desgranar mucho el código, y en otros nos interesará hacer
funciones específicas. Y esa decisión no deja de ser, al fin y al cabo, diseño del
programa.
En cualquier caso, el diseño nos asegura que podremos implementar nuestro programa en
cualquier lenguaje y en cualquier momento. Podremos retomar nuestros papeles de diseño
3 meses después y, pese a no recordar en qué parte del programa estábamos, volver a su
desarrollo sin excesivas dificultades.
Una de las cosas más complicadas de hacer un juego es el pensar por dónde empezar. Todo
este proceso nos permite empezar el programa por la parte del mismo que realmente
importa. Todos hemos empezado alguna vez a realizar nuestro juego por el menú, perdiendo
muchas horas de trabajo para descubrir que teníamos un menú, pero no teníamos un juego,
y que ya estábamos cansados del desarrollo sin apenas haber empezado.
Veamos un ejemplo: suponiendo que realizamos, por ejemplo, un juego de puzzles tipo
Tetris, lo ideal sería empezar definiendo dónde se almacenan los datos del area de
juego, hacer una función que convierta esos datos en imágenes en pantalla, y realizar un
bucle que permita ver caer la pieza. Después, se agrega control por teclado para la
pieza y se pone la lógica del juego (realización de líneas al tocar suelo, etc).
Tras esto, ya tenemos el esqueleto funcional del juego y podemos añadir opciones, menúes
y demás. Tendremos algo tangible, funcional, donde podemos hacer cambios que implican un
inmediato resultado en pantalla, y no habremos malgastado muchas horas con un simple
menú.
Por otra parte, el diseñar correctamente nuestro programa y desgranarlo en piezas
reutilizables redundará en nuestro beneficio no sólo actual (con respecto al programa
que estamos escribiendo) sino futuro, ya que podremos crearnos nuestras propias
bibliotecas de funciones que reutilizar en futuros programas. Aquella rutina de
dibujado de Sprites, de zoom de pantalla o de compresión de datos que tanto nos costó
programar, bien aislada en una subrutina y con sus parámetros de entrada y salida bien
definidos puede ser utilizada directamente en nuestros próximos programas simplemente
copiando y pegando el código correspondiente.
Más aún, podemos organizar funciones con finalidades comunes en ficheros individuales.
Tendremos así nuestro fichero / biblioteca con funciones gráficas, de sonido, de
teclado/joystick, etc. El ensamblador PASMO nos permite incluir un fichero en cualquier
parte de nuestro código con la directiva INCLUDE.
Así, nuestro programa en ASM podría comenzar (o acabar) por algo como:
INCLUDE "graficos.asm"
INCLUDE "sonido.asm"
INCLUDE "teclado.asm"
INCLUDE "datos.asm"
|
Esto contribuye a reducir fallos en la codificación, hacer más corto el listado general
del programa, y, sobre todo, reduce el tiempo de desarrollo.
Lenguaje Ensamblador del Z80 (V)
Puertos de E/S y Tabla de Opcodes
En este capítulo se introducirán las instrucciones IN y OUT para la
exploración
de los puertos del microprocesador, mostrando cómo el acceso a dichos puertos nos
permitirá la gestión de los diferentes dispositivos conectados al microprocesador
(teclado, altavoz, controladora de disco, etc...).
Finalmente, para acabar con la descripción del juego de instrucciones del Z80
veremos algunos ejemplos de opcodes no documentados, y una
tabla-resumen con
la mayoría de instrucciones, así como sus tiempos de ejecución y tamaños.
Los puertos E/S
Como ya vimos en su momento, el microprocesador Z80 se conecta
mediante los puertos de entrada/salida de la CPU a los periféricos
externos (teclado, cassette y altavoz de audio), pudiendo leer el
estado de los mismos (leer del teclado, leer del cassette) y escribir
en ellos (escribir en el altavoz para reproducir sonido, escribir en
el cassette) por medio de estas conexiones conocidas como "I/O Ports".
|
Esquema de hardware de un ZX Spectrum |
Aunque para nosotros el teclado o el altavoz puedan ser parte del
ordenador, para el Z80, el microprocesador en sí mismo, son tan
externos a él como el monitor o el joystick. Nuestro microprocesador
accede a todos estos elementos externos mediante una serie de patillas
(buses de datos y direcciones) que son conectadas eléctricamente a
todos los elementos externos con los que queremos que interactúe. La
memoria, el teclado, el altavoz, o los mismos pines del bus trasero
del Spectrum, se conectan al Z80 y éste nos permite su acceso a través
de dichas líneas, o de los puertos de entrada/salida (I/O).
IN y OUT
Ya conocemos la existencia y significado de los puertos y su conexión
con el microprocesador. Sólo resta saber: ¿cómo accedemos a un puerto
tanto para leer como para escribir desde nuestros programas en
ensamblador?
La respuesta la tienen los comandos IN y OUT del Z80.
Comenzaremos con IN, que nos permite leer el valor de un puerto ya
sea directamente, o cargado sobre el registro BC:
IN registro, (C)
Leemos el puerto "BC" y ponemos su contenido en el registro especificado.
En realidad, pese a que teóricamente el Spectrum sólo tiene acceso a puertos
E/S de 8 bits (0-255), para acceder a los puertos, IN r, (C) pone todo el valor
de BC en el bus de direcciones.
IN A, (puerto)
Leemos el puerto "A*256 + Puerto" y ponemos su contenido en A. En esta
ocasión, el Spectrum pone en el bus de direcciones el valor del registro
de 16 bits formado por A y (puerto) (en lugar de BC).
Por ejemplo, estas 2 lecturas de puerto (usando los 2 formatos de la
instrucción IN vistos anteriormente) son equivalentes:
; Forma 1
LD BC, FFFEh
IN A, (C) ; A = Lectura de puerto FFFEh
; Forma 2
LD A, FFh
IN A, (FEh) ; A = Lectura de puerto FFFEh
|
Aunque la instrucción de la "Forma 1" hable del puerto C, en realidad
el puerto es un valor de 16 bits y se carga en el registro BC.
De la misma forma, podemos escribir un valor en un puerto con sus equivalentes "OUT":
OUT (puerto), A
Escribimos en "puerto" (valor de 8 bits) el valor de A.
OUT (C), registro
Escribimos en el puerto "C" el valor contenido en "registro" (aunque se pone el
valor de BC en el bus de direcciones).
Curiosamente, como se explica en el excelente documento "//The
Undocumented Z80 Documented//" (que habla de las funcionalidades y
opcodes no documentados del Z80), los puertos del Spectrum son
oficialmente de 8 bits (0-255) aunque realmente se pone o bien BC o
bien (A*256)+PUERTO en el bus de direcciones, por lo que en el fondo
se pueden acceder a todos los 65536 puertos disponibles.
La forma en que estas instrucciones afectan a los flags es la siguiente:
Flags
Instrucción |S Z H P N C|
----------------------------------
IN A, (n) |- - - - - -|
IN r, (C) |* * * P 0 -|
OUT (C), r |- - - - - -|
OUT (n), A |- - - - - -|
|
Aunque entre los 2 formatos OUT no debería haber ninguna diferencia funcional, cabe
destacar que "OUT (N), A" es 1 t-estado o ciclo de reloj más rápida que "OUT (C), A",
tardando 11 y 12 t-estados respectivamente.
Instrucciones de puerto repetitivas e incrementales
Al igual que LD carga un valor de un origen a un destino, y tiene sus correspondientes
instrucciones incrementales (LDI "carga e incrementa", LDD "carga y decrementa") o
repetitivas (LDIR "carga, incrementa y repite BC veces", LDDR "carga, decrementa, y
repite BC veces"), IN y OUT tienen sus equivalentes incrementales y repetidores.
Así:
- IND :
- Leemos en la dirección de memoria apuntada por HL ([HL]) el valor contenido en
el puerto C.
- Decrementamos HL.
- Decrementamos B.
- INI :
- Leemos en la dirección de memoria apuntada por HL ([HL]) el valor contenido en
el puerto C.
- Incrementamos HL.
- Decrementamos B.
- OUTD :
- Escribimos en el puerto C el valor de la dirección de memoria apuntada por HL
([HL]).
- Decrementamos HL.
- Decrementamos B.
- OUTI :
- Escribimos en el puerto C el valor de la dirección de memoria apuntada por HL
([HL]).
- Incrementamos HL.
- Decrementamos B.
Y sus versiones repetitivas INDR, INIR, OTDR y OTIR, que realizan la misma
función que sus hermanas incrementales, repitiéndolo hasta que BC sea cero.
Las afectaciones de flags de estas funciones son las siguientes:
Flags:
Flags
Instrucción |S Z H P N C|
----------------------------------
INI |? * ? ? 1 ?|
IND |? * ? ? 1 ?|
OUTI |? * ? ? 1 ?|
OUTD |? * ? ? 1 ?|
INDR |? 1 ? ? 1 ?|
INIR |? 1 ? ? 1 ?|
OTDR |? 1 ? ? 1 ?|
OTIR |? 1 ? ? 1 ?|
|
Nota: Pese a que la documentación oficial dice que estas instrucciones no
afectan al Carry Flag, las pruebas hechas a posteriori y recopiladas en
la información disponible sobre Opcodes No Documentados del Z80 sugieren que
sí que son modificados.
Algunos puertos E/S comunes
Para terminar con el tema de los puertos de Entrada y Salida, vamos a hacer
referencia a algunos puertos disponibles en el Sinclair Spectrum (algunos de
ellos sólo en ciertos modelos).
Como veremos en capítulo dedicado al teclado, existe una serie de puertos
E/S que acceden directamente a la lectura del estado de las diferentes teclas
de nuestro Spectrum. Leyendo del puerto adecuado, y chequeando en la respuesta
obtenida el bit concreto asociado a la tecla que queremos consultar podremos
conocer si una determinada tecla está pulsada (0) o no pulsada (1), como
podemos ver en el siguiente ejemplo:
; Lectura de la tecla "P" en un bucle
ORG 50000
bucle:
LD BC, $DFFE ; Semifila "P" a "Y"
IN A, (C) ; Leemos el puerto
BIT 0, A ; Testeamos el bit 0
JR Z, salir ; Si esta a 0 (pulsado) salir.
JR bucle ; Si no (a 1, no pulsado) repetimos
salir:
RET
|
El anterior ejemplo lee constantemente el puerto $DFFE a la espera de
que el bit 0 de la respuesta obtenida de dicha lectura sea 0, lo que quiere
decir que la tecla "p" ha sido pulsada.
Aunque los veremos en su momento en profundidad, estos son los puertos
asociados a las diferentes filas de teclas:
Puerto |
Bits: |
D4 |
D3 |
D2 |
D1 |
D0 |
65278d (FEFEh) |
Teclas: |
"V" |
"C" |
"X" |
"Z" |
CAPS |
65022d (FDFEh) |
Teclas: |
"G" |
"F" |
"D" |
"S" |
"A" |
64510d (FBFEh) |
Teclas: |
"T" |
"R" |
"E" |
"W" |
"Q" |
63486d (F7FEh) |
Teclas: |
"5" |
"4" |
"3" |
"2" |
"1" |
61438d (EFFEh) |
Teclas: |
"0" |
"9" |
"8" |
"7" |
"6" |
57342d (DFFEh) |
Teclas: |
"Y" |
"U" |
"I" |
"O" |
"P" |
49150d (BFFEh) |
Teclas: |
"H" |
"J" |
"K" |
"L" |
ENTER |
32766d (7FFEh) |
Teclas: |
"B" |
"N" |
"M" |
SYMB |
SPACE |
El bit 6 de los puertos que hemos visto para el teclado tiene un valor
aleatorio, excepto cuando se pulsa PLAY en el cassette, y es a través
de dicho bit de donde podremos obtener los datos a cargar.
La escritura en el puerto 00FEh permite acceder al altavoz (bit 4)
y a la señal de audio para grabar a cinta (bit 3). Los bits 0, 1 y 2
controlan el color del borde, como podemos ver en el siguiente ejemplo:
; Cambio del color del borde al pulsar espacio
ORG 50000
LD B, 6 ; 6 iteraciones, color inicial borde
bucle:
LD A, $7F ; Semifila B a ESPACIO
IN A, ($FE) ; Leemos el puerto
BIT 0, A ; Testeamos el bit 0 (ESPACIO)
JR NZ, bucle ; Si esta a 1 (no pulsado), esperar
LD A, B ; A = B
OUT (254), A ; Cambiamos el color del borde
suelta_tecla: ; Ahora esperamos a que se suelte la tecla
LD A, $7F ; Semifila B a ESPACIO
IN A, ($FE) ; Leemos el puerto
BIT 0, A ; Testeamos el bit 0
JR Z, suelta_tecla ; Saltamos hasta que se suelte
djnz bucle ; Repetimos "B" veces
salir:
RET
END 50000 ; Ejecucion en 50000
|
|
Ejecución del programa de cambio de borde |
El puerto 7FFDh gestiona la paginación en los modos de 128K,
permitiendo cambiar el modelo de páginas de memoria (algo que
no vamos a ver en este capítulo).
Los puertos BFFDh y FFFDh gestionan el chip de sonido en aquellos
modelos que dispongan de él, así como el RS232/MIDI y el interfaz
AUX.
Finalmente, el puerto 0FFDh gestiona el puerto paralelo de impresora,
y los puertos 2FFDh y 3FFDh permiten gestionar la controladora de disco en
aquellos modelos de Spectrum que dispongan de ella.
Podéis encontrar más información sobre los puertos de Entrada y
Salida en el capítulo 8 sección 23 del manual del +2A y +3, disponible
online en World Of Spectrum.
Tabla de instrucciones, ciclos y tamaños
A continuación se incluye una tabla donde se hace referencia a las instrucciones
del microprocesador Z80 (campo Mnemonic), los ciclos de reloj que tarda en ejecutarse
(campo Clck), el tamaño en bytes de la instrucción codificada (Siz), la afectación
de Flags (SZHPNC), el opcode y su descripción en cuanto a ejecución.
La tabla forma parte de un documento llamado "The Complete Z80 OP-Code Reference",
de Devin Gardner.
Mnemonic |
Clck |
Size |
SZHPNC |
OP-Code |
Description |
Notes |
ADC A,r |
4 |
1 |
***V0* |
88+rb |
Add with Carry |
A=A+s+CY |
ADC A,N |
7 |
2 |
|
CE XX |
|
|
ADC A,(HL) |
7 |
1 |
|
8E |
|
|
ADC A,(IX+N) |
19 |
3 |
|
DD 8E XX |
|
|
ADC A,(IY+N) |
19 |
3 |
|
FD 8E XX |
|
|
ADC HL,BC |
15 |
2 |
**?V0* |
ED 4A |
Add with Carry |
HL=HL+ss+CY |
ADC HL,DE |
15 |
2 |
|
ED 5A |
|
|
ADC HL,HL |
15 |
2 |
|
ED 6A |
|
|
ADC HL,SP |
15 |
2 |
|
ED 7A |
|
|
ADD A,r |
4 |
1 |
***V0* |
80+rb |
Add (8-bit) |
A=A+s |
ADD A,N |
7 |
2 |
|
C6 XX |
|
|
ADD A,(HL) |
7 |
1 |
|
86 |
|
|
ADD A,(IX+N) |
19 |
3 |
|
DD 86 XX |
|
|
ADD A,(IY+N) |
19 |
3 |
|
FD 86 XX |
|
|
ADD HL,BC |
11 |
1 |
--?-0* |
09 |
Add (16-bit) |
HL=HL+ss |
ADD HL,DE |
11 |
1 |
|
19 |
|
|
ADD HL,HL |
11 |
1 |
|
29 |
|
|
ADD HL,SP |
11 |
1 |
|
39 |
|
|
ADD IX,BC |
15 |
2 |
--?-0* |
DD 09 |
Add (IX register) |
IX=IX+pp |
ADD IX,DE |
15 |
2 |
|
DD 19 |
|
|
ADD IX,IX |
15 |
2 |
|
DD 29 |
|
|
ADD IX,SP |
15 |
2 |
|
DD 39 |
|
|
ADD IY,BC |
15 |
2 |
--?-0* |
FD 09 |
Add (IY register) |
IY=IY+rr |
ADD IY,DE |
15 |
2 |
|
FD 19 |
|
|
ADD IY,IY |
15 |
2 |
|
FD 29 |
|
|
ADD IY,SP |
15 |
2 |
|
FD 39 |
|
|
AND r |
4 |
1 |
***P00 |
A0+rb |
Logical AND |
A=A&s |
AND N |
7 |
2 |
|
E6 XX |
|
|
AND (HL) |
7 |
1 |
|
A6 |
|
|
AND (IX+N) |
19 |
3 |
|
DD A6 XX |
|
|
AND (IY+N) |
19 |
3 |
|
FD A6 XX |
|
|
BIT b,r |
8 |
2 |
?*1?0- |
CB 40+8*b+rb |
Test Bit |
m&{2^b} |
BIT b,(HL) |
12 |
2 |
|
CB 46+8*b |
|
|
BIT b,(IX+N) |
20 |
4 |
|
DD CB |
|
|
|
|
|
|
XX 46+8*b |
|
|
BIT b,(IY+N) |
20 |
4 |
|
FD CB XX |
|
|
|
|
|
|
XX 46+8*b |
|
|
CALL NN |
17 |
3 |
------ |
CD XX XX |
Unconditional Call |
-(SP)=PC,PC=nn |
CALL C,NN |
17/1 |
3 |
------ |
DC XX XX |
Conditional Call |
If Carry = 1 |
CALL NC,NN |
17/1 |
3 |
|
D4 XX XX |
|
If carry = 0 |
CALL M,NN |
17/1 |
3 |
|
FC XX XX |
|
If Sign = 1 (negative) |
CALL P,NN |
17/1 |
3 |
|
F4 XX XX |
|
If Sign = 0 (positive) |
CALL Z,NN |
17/1 |
3 |
|
CC XX XX |
|
If Zero = 1 (ans.=0) |
CALL NZ,NN |
17/1 |
3 |
|
C4 XX XX |
|
If Zero = 0 (non-zero) |
CALL PE,NN |
17/1 |
3 |
|
EC XX XX |
|
If Parity = 1 (even) |
CALL PO,NN |
17/1 |
3 |
|
E4 XX XX |
|
If Parity = 0 (odd) |
CCF |
4 |
1 |
--?-0* |
3F |
Complement Carry Flag |
CY=~CY |
CP r |
4 |
1 |
***V1* |
B8+rb |
Compare |
Compare A-s |
CP N |
7 |
2 |
|
FE XX |
|
|
CP (HL) |
7 |
1 |
|
BE |
|
|
CP (IX+N) |
19 |
3 |
|
DD BE XX |
|
|
CP (IY+N) |
19 |
3 |
|
FD BE XX |
|
|
CPD |
16 |
2 |
****1- |
ED A9 |
Compare and Decrement |
A-(HL),HL=HL-1,BC=BC-1 |
CPDR |
21/1 |
2 |
****1- |
ED B9 |
Compare, Dec., Repeat |
CPD till A=(HL)or BC=0 |
CPI |
16 |
2 |
****1- |
ED A1 |
Compare and Increment |
A-(HL),HL=HL+1,BC=BC-1 |
CPIR |
21/1 |
2 |
****1- |
ED B1 |
Compare, Inc., Repeat |
CPI till A=(HL)or BC=0 |
CPL |
4 |
1 |
--1-1- |
2F |
Complement |
A=~A |
DAA |
4 |
1 |
***P-* |
27 |
Decimal Adjust Acc. |
A=BCD format (dec.) |
DEC A |
4 |
1 |
***V1- |
3D |
Decrement (8-bit) |
s=s-1 |
DEC B |
4 |
1 |
|
05 |
|
|
DEC C |
4 |
1 |
|
0D |
|
|
DEC D |
4 |
1 |
|
15 |
|
|
DEC E |
4 |
1 |
|
1D |
|
|
DEC H |
4 |
1 |
|
25 |
|
|
DEC L |
4 |
2 |
|
2D |
|
|
DEC (HL) |
11 |
1 |
|
35 |
|
|
DEC (IX+N) |
23 |
3 |
|
DD 35 XX |
|
|
DEC (IY+N) |
23 |
3 |
|
FD 35 XX |
|
|
DEC BC |
6 |
1 |
------ |
0B |
Decrement (16-bit) |
ss=ss-1 |
DEC DE |
6 |
1 |
|
1B |
|
|
DEC HL |
6 |
1 |
|
2B |
|
|
DEC SP |
6 |
1 |
|
3B |
|
|
DEC IX |
10 |
2 |
------ |
DD 2B |
Decrement |
xx=xx-1 |
DEC IY |
10 |
2 |
|
FD 2B |
|
|
DI |
4 |
1 |
------ |
F3 |
Disable Interrupts |
|
DJNZ $+2 |
13/8 |
1 |
------ |
10 |
Dec., Jump Non-Zero |
B=B-1 till B=0 |
EI |
4 |
1 |
------ |
FB |
Enable Interrupts |
|
EX (SP),HL |
19 |
1 |
------ |
E3 |
Exchange |
(SP)<->HL |
EX (SP),IX |
23 |
2 |
------ |
DD E3 |
|
(SP)<->xx |
EX (SP),IY |
23 |
2 |
|
FD E3 |
|
|
EX AF,AF' |
4 |
1 |
------ |
08 |
|
AF<->AF' |
EX DE,HL |
4 |
1 |
------ |
EB |
|
DE<->HL |
EXX |
4 |
1 |
------ |
D9 |
Exchange |
qq<->qq' (except AF) |
HALT |
4 |
1 |
------ |
76 |
Halt |
|
IM 0 |
8 |
2 |
------ |
ED 46 |
Interrupt Mode |
(n=0,1,2) |
IM 1 |
8 |
2 |
|
ED 56 |
|
|
IM 2 |
8 |
2 |
|
ED 5E |
|
|
IN A,(N) |
11 |
2 |
------ |
DB XX |
Input |
A=(n) |
IN (C) |
12 |
2 |
***P0- |
ED 70 |
Input* |
(Unsupported) |
IN A,(C) |
12 |
2 |
***P0- |
ED 78 |
Input |
r=(C) |
IN B,(C) |
12 |
2 |
|
ED 40 |
|
|
IN C,(C) |
12 |
2 |
|
ED 48 |
|
|
IN D,(C) |
12 |
2 |
|
ED 50 |
|
|
IN E,(C) |
12 |
2 |
|
ED 58 |
|
|
IN H,(C) |
12 |
2 |
|
ED 60 |
|
|
IN L,(C) |
12 |
2 |
|
ED 68 |
|
|
INC A |
4 |
1 |
***V0- |
3C |
Increment (8-bit) |
r=r+1 |
INC B |
4 |
1 |
|
04 |
|
|
INC C |
4 |
1 |
|
0C |
|
|
INC D |
4 |
1 |
|
14 |
|
|
INC E |
4 |
1 |
|
1C |
|
|
INC H |
4 |
1 |
|
24 |
|
|
INC L |
4 |
1 |
|
2C |
|
|
INC BC |
6 |
1 |
------ |
03 |
Increment (16-bit) |
ss=ss+1 |
INC DE |
6 |
1 |
|
13 |
|
|
INC HL |
6 |
1 |
|
23 |
|
|
INC SP |
6 |
1 |
|
33 |
|
|
INC IX |
10 |
2 |
------ |
DD 23 |
Increment |
xx=xx+1 |
INC IY |
10 |
2 |
|
FD 23 |
|
|
INC (HL) |
11 |
1 |
***V0- |
34 |
Increment (indirect) |
(HL)=(HL)+1 |
INC (IX+N) |
23 |
3 |
***V0- |
DD 34 XX |
Increment |
(xx+d)=(xx+d)+1 |
INC (IY+N) |
23 |
3 |
|
FD 34 XX |
|
|
IND |
16 |
2 |
?*??1- |
ED AA |
Input and Decrement |
(HL)=(C),HL=HL-1,B=B-1 |
INDR |
21/1 |
2 |
?1??1- |
ED BA |
Input, Dec., Repeat |
IND till B=0 |
INI |
16 |
2 |
?*??1- |
ED A2 |
Input and Increment |
(HL)=(C),HL=HL+1,B=B-1 |
INIR |
21/1 |
2 |
?1??1- |
ED B2 |
Input, Inc., Repeat |
INI till B=0 |
JP $NN |
10 |
3 |
------ |
C3 XX XX |
Unconditional Jump |
PC=nn |
JP (HL) |
4 |
1 |
------ |
E9 |
Unconditional Jump |
PC=(HL) |
JP (IX) |
8 |
2 |
------ |
DD E9 |
Unconditional Jump |
PC=(xx) |
JP (IY) |
8 |
2 |
|
FD E9 |
|
|
JP C,$NN |
10/1 |
3 |
------ |
DA XX XX |
Conditional Jump |
If Carry = 1 |
JP NC,$NN |
10/1 |
3 |
|
D2 XX XX |
|
If Carry = 0 |
JP M,$NN |
10/1 |
3 |
|
FA XX XX |
|
If Sign = 1 (negative) |
JP P,$NN |
10/1 |
3 |
|
F2 XX XX |
|
If Sign = 0 (positive) |
JP Z,$NN |
10/1 |
3 |
|
CA XX XX |
|
If Zero = 1 (ans.= 0) |
JP NZ,$NN |
10/1 |
3 |
|
C2 XX XX |
|
If Zero = 0 (non-zero) |
JP PE,$NN |
10/1 |
3 |
|
EA XX XX |
|
If Parity = 1 (even) |
JP PO,$NN |
10/1 |
3 |
|
E2 XX XX |
|
If Parity = 0 (odd) |
JR $N+2 |
12 |
2 |
------ |
18 XX |
Relative Jump |
PC=PC+e |
JR C,$N+2 |
12/7 |
2 |
------ |
38 XX |
Cond. Relative Jump |
If cc JR(cc=C,NC,NZ,Z) |
JR NC,$N+2 |
12/7 |
2 |
|
30 XX |
|
|
JR Z,$N+2 |
12/7 |
2 |
|
28 XX |
|
|
JR NZ,$N+2 |
12/7 |
2 |
|
20 XX |
|
|
LD I,A |
9 |
2 |
------ |
ED 47 |
Load* |
dst=src |
LD R,A |
9 |
2 |
|
ED 4F |
|
|
LD A,I |
9 |
2 |
**0*0- |
ED 57 |
Load* |
dst=src |
LD A,R |
9 |
2 |
|
ED 5F |
|
|
LD A,r |
4 |
1 |
------ |
78+rb |
Load (8-bit) |
dst=src |
LD A,N |
7 |
2 |
|
3E XX |
|
|
LD A,(BC) |
7 |
1 |
|
0A |
|
|
LD A,(DE) |
7 |
1 |
|
1A |
|
|
LD A,(HL) |
7 |
1 |
|
7E |
|
|
LD A,(IX+N) |
19 |
3 |
|
DD 7E XX |
|
|
LD A,(IY+N) |
19 |
3 |
|
FD 7E XX |
|
|
LD A,(NN) |
13 |
3 |
|
3A XX XX |
|
|
LD B,r |
4 |
1 |
|
40+rb |
|
|
LD B,N |
7 |
2 |
|
06 XX |
|
|
LD B,(HL) |
7 |
1 |
|
46 |
|
|
LD B,(IX+N) |
19 |
3 |
|
DD 46 XX |
|
|
LD B,(IY+N) |
19 |
3 |
|
FD 46 XX |
|
|
LD C,r |
4 |
1 |
|
48+rb |
|
|
LD C,N |
7 |
2 |
|
0E XX |
|
|
LD C,(HL) |
7 |
1 |
|
4E |
|
|
LD C,(IX+N) |
19 |
3 |
|
DD 4E XX |
|
|
LD C,(IY+N) |
19 |
3 |
|
FD 4E XX |
|
|
LD D,r |
4 |
1 |
|
50+rb |
|
|
LD D,N |
7 |
2 |
|
16 XX |
|
|
LD D,(HL) |
7 |
1 |
|
56 |
|
|
LD D,(IX+N) |
19 |
3 |
|
DD 56 XX |
|
|
LD D,(IY+N) |
19 |
3 |
|
FD 56 XX |
|
|
LD E,r |
4 |
1 |
|
58+rb |
|
|
LD E,N |
7 |
2 |
|
1E XX |
|
|
LD E,(HL) |
7 |
1 |
|
5E |
|
|
LD E,(IX+N) |
19 |
3 |
|
DD 5E XX |
|
|
LD E,(IY+N) |
19 |
3 |
|
FD 5E XX |
|
|
LD H,r |
4 |
1 |
|
60+rb |
|
|
LD H,N |
7 |
2 |
|
26 XX |
|
|
LD H,(HL) |
7 |
1 |
|
66 |
|
|
LD H,(IX+N) |
19 |
3 |
|
DD 66 XX |
|
|
LD H,(IY+N) |
19 |
3 |
|
FD 66 XX |
|
|
LD L,r |
4 |
1 |
|
68+rb |
|
|
LD L,N |
7 |
2 |
|
2E XX |
|
|
LD L,(HL) |
7 |
1 |
|
6E |
|
|
LD L,(IX+N) |
19 |
3 |
|
DD 6E XX |
|
|
LD L,(IY+N) |
19 |
3 |
|
FD 6E XX |
|
|
LD BC,(NN) |
20 |
4 |
------ |
ED 4B XX XX |
Load (16-bit) |
dst=src |
LD BC,NN |
10 |
3 |
|
01 XX XX |
|
|
LD DE,(NN) |
20 |
4 |
|
ED 5B XX XX |
|
|
LD DE,NN |
10 |
3 |
|
11 XX XX |
|
|
LD HL,(NN) |
20 |
3 |
|
2A XX XX |
|
|
LD HL,NN |
10 |
3 |
|
21 XX XX |
|
|
LD SP,(NN) |
20 |
4 |
|
ED 7B XX XX |
|
|
LD SP,HL |
6 |
1 |
|
F9 |
|
|
LD SP,IX |
10 |
2 |
|
DD F9 |
|
|
LD SP,IY |
10 |
2 |
|
FD F9 |
|
|
LD SP,NN |
10 |
3 |
|
31 XX XX |
|
|
LD IX,(NN) |
20 |
4 |
|
DD 2A XX XX |
|
|
LD IX,NN |
14 |
4 |
|
DD 21 XX XX |
|
|
LD IY,(NN) |
20 |
4 |
|
FD 2A XX XX |
|
|
LD IY,NN |
14 |
4 |
|
FD 21 XX XX |
|
|
LD (HL),r |
7 |
1 |
------ |
70+rb |
Load (Indirect) |
dst=src |
LD (HL),N |
10 |
2 |
|
36 XX |
|
|
LD (BC),A |
7 |
1 |
|
02 |
|
|
LD (DE),A |
7 |
1 |
|
12 |
|
|
LD (NN),A |
13 |
3 |
|
32 XX XX |
|
|
LD (NN),BC |
20 |
4 |
|
ED 43 XX XX |
|
|
LD (NN),DE |
20 |
4 |
|
ED 53 XX XX |
|
|
LD (NN),HL |
16 |
3 |
|
22 XX XX |
|
|
LD (NN),IX |
20 |
4 |
|
DD 22 XX XX |
|
|
LD (NN),IY |
20 |
4 |
|
FD 22 XX XX |
|
|
LD (NN),SP |
20 |
4 |
|
ED 73 XX XX |
|
|
LD (IX+N),r |
19 |
3 |
|
DD 70+rb XX |
|
|
LD (IX+N),N |
19 |
4 |
|
DD 36 XX XX |
|
|
LD (IY+N),r |
19 |
3 |
|
FD 70+rb XX |
|
|
LD (IY+N),N |
19 |
4 |
|
FD 36 XX XX |
|
|
LDD |
16 |
2 |
--0*0- |
ED A8 |
Load and Decrement |
(DE)=(HL),HL=HL-1,# |
LDDR |
21/1 |
2 |
--000- |
ED B8 |
Load, Dec., Repeat |
LDD till BC=0 |
LDI |
16 |
2 |
--0*0- |
ED A0 |
Load and Increment |
(DE)=(HL),HL=HL+1,# |
LDIR |
21/1 |
2 |
--000- |
ED B0 |
Load, Inc., Repeat |
LDI till BC=0 |
NEG |
8 |
2 |
***V1* |
ED 44 |
Negate |
A=-A |
NOP |
4 |
1 |
------ |
00 |
No Operation |
|
OR r |
4 |
1 |
***P00 |
B0+rb |
Logical inclusive OR |
A=Avs |
OR N |
7 |
2 |
|
F6 XX |
|
|
OR (HL) |
7 |
1 |
|
B6 |
|
|
OR (IX+N) |
19 |
3 |
|
DD B6 XX |
|
|
OR (IY+N) |
19 |
3 |
|
FD B6 XX |
|
|
OUT (N),A |
11 |
2 |
------ |
D3 XX |
Output |
(n)=A |
OUT (C),0 |
12 |
2 |
------ |
ED 71 |
Output* |
(Unsupported) |
OUT (C),A |
12 |
2 |
------ |
ED 79 |
Output |
(C)=r |
OUT (C),B |
12 |
2 |
|
ED 41 |
|
|
OUT (C),C |
12 |
2 |
|
ED 49 |
|
|
OUT (C),D |
12 |
2 |
|
ED 51 |
|
|
OUT (C),E |
12 |
2 |
|
ED 59 |
|
|
OUT (C),H |
12 |
2 |
|
ED 61 |
|
|
OUT (C),L |
12 |
2 |
|
ED 69 |
|
|
OUTD |
16 |
2 |
?*??1- |
ED AB |
Output and Decrement |
(C)=(HL),HL=HL-1,B=B-1 |
OTDR |
21/1 |
2 |
?1??1- |
ED BB |
Output, Dec., Repeat |
OUTD till B=0 |
OUTI |
16 |
2 |
?*??1- |
ED A3 |
Output and Increment |
(C)=(HL),HL=HL+1,B=B-1 |
OTIR |
21/1 |
2 |
?1??1- |
ED B3 |
Output, Inc., Repeat |
OUTI till B=0 |
POP AF |
10 |
1 |
------ |
F1 |
Pop |
qq=(SP)+ |
POP BC |
10 |
1 |
|
C1 |
|
|
POP DE |
10 |
1 |
|
D1 |
|
|
POP HL |
10 |
1 |
|
E1 |
|
|
POP IX |
14 |
2 |
------ |
DD E1 |
Pop |
xx=(SP)+ |
POP IY |
14 |
2 |
|
FD E1 |
|
|
PUSH AF |
11 |
1 |
------ |
F5 |
Push |
-(SP)=qq |
PUSH BC |
11 |
1 |
|
C5 |
|
|
PUSH DE |
11 |
1 |
|
D5 |
|
|
PUSH HL |
11 |
1 |
|
E5 |
|
|
PUSH IX |
15 |
2 |
------ |
DD E5 |
Push |
-(SP)=xx |
PUSH IY |
15 |
2 |
|
FD E5 |
|
|
RES b,r |
8 |
2 |
------ |
CB 80+8*b+rb |
Reset bit |
m=m&{~2^b} |
RES b,(HL) |
15 |
2 |
------ |
CB 86+8*b |
|
|
RES b,(IX+N) |
23 |
4 |
------ |
DD CB |
|
|
|
|
|
|
XX 86+8*b |
|
|
RES b,(IY+N) |
23 |
4 |
------ |
FD CB |
|
|
|
|
|
|
XX 86+8*b |
|
|
RET |
10 |
1 |
------ |
C9 |
Return |
PC=(SP)+ |
RET C |
11/5 |
1 |
------ |
D8 |
Conditional Return |
If Carry = 1 |
RET NC |
11/5 |
1 |
|
D0 |
|
If Carry = 0 |
RET M |
11/5 |
1 |
|
F8 |
|
If Sign = 1 (negative) |
RET P |
11/5 |
1 |
|
F0 |
|
If Sign = 0 (positive) |
RET Z |
11/5 |
1 |
|
C8 |
|
If Zero = 1 (ans.=0) |
RET NZ |
11/5 |
1 |
|
C0 |
|
If Zero = 0 (non-zero) |
RET PE |
11/5 |
1 |
|
E8 |
|
If Parity = 1 (even) |
RET PO |
11/5 |
1 |
|
E0 |
|
If Parity = 0 (odd) |
RETI |
14 |
2 |
------ |
ED 4D |
Return from Interrupt |
PC=(SP)+ |
RETN |
14 |
2 |
------ |
ED 45 |
Return from NMI |
PC=(SP)+ |
RLA |
4 |
1 |
--0-0* |
17 |
Rotate Left Acc. |
A={CY,A}<- |
RL r |
8 |
2 |
**0P0* |
CB 10+rb |
Rotate Left |
m={CY,m}<- |
RL (HL) |
15 |
2 |
|
CB 16 |
|
|
RL (IX+N) |
23 |
4 |
|
DD CB XX 16 |
|
|
RL (IY+N) |
23 |
4 |
|
FD CB XX 16 |
|
|
RLCA |
4 |
1 |
--0-0* |
07 |
Rotate Left Cir. Acc. |
A=A<- |
RLC r |
8 |
2 |
**0P0* |
CB 00+rb |
Rotate Left Circular |
m=m<- |
RLC (HL) |
15 |
2 |
|
CB 06 |
|
|
RLC (IX+N) |
23 |
4 |
|
DD CB XX 06 |
|
|
RLC (IY+N) |
23 |
4 |
|
FD CB XX 06 |
|
|
RLD |
18 |
2 |
**0P0- |
ED 6F |
Rotate Left 4 bits |
{A,(HL)}={A,(HL)}<- ## |
RRA |
4 |
1 |
--0-0* |
1F |
Rotate Right Acc. |
A=->{CY,A} |
RR r |
8 |
2 |
**0P0* |
CB 18+rb |
Rotate Right |
m=->{CY,m} |
RR (HL) |
15 |
2 |
|
CB 1E |
|
|
RR (IX+N) |
23 |
4 |
|
DD CB XX 1E |
|
|
RR (IY+N) |
23 |
4 |
|
FD CB XX 1E |
|
|
RRCA |
4 |
1 |
--0-0* |
0F |
Rotate Right Cir.Acc. |
A=->A |
RRC r |
8 |
2 |
**0P0* |
CB 08+rb |
Rotate Right Circular |
m=->m |
RRC (HL) |
15 |
2 |
|
CB 0E |
|
|
RRC (IX+N) |
23 |
4 |
|
DD CB XX 0E |
|
|
RRC (IY+N) |
23 |
4 |
|
FD CB XX 0E |
|
|
RRD |
18 |
2 |
**0P0- |
ED 67 |
Rotate Right 4 bits |
{A,(HL)}=->{A,(HL)} ## |
RST 0 |
11 |
1 |
------ |
C7 |
Restart |
(p=0H,8H,10H,...,38H) |
RST 08H |
11 |
1 |
|
CF |
|
|
RST 10H |
11 |
1 |
|
D7 |
|
|
RST 18H |
11 |
1 |
|
DF |
|
|
RST 20H |
11 |
1 |
|
E7 |
|
|
RST 28H |
11 |
1 |
|
EF |
|
|
RST 30H |
11 |
1 |
|
F7 |
|
|
RST 38H |
11 |
1 |
|
FF |
|
|
SBC r |
4 |
1 |
***V1* |
98+rb |
Subtract with Carry |
A=A-s-CY |
SBC A,N |
7 |
2 |
|
DE XX |
|
|
SBC (HL) |
7 |
1 |
|
9E |
|
|
SBC A,(IX+N) |
19 |
3 |
|
DD 9E XX |
|
|
SBC A,(IY+N) |
19 |
3 |
|
FD 9E XX |
|
|
SBC HL,BC |
15 |
2 |
**?V1* |
ED 42 |
Subtract with Carry |
HL=HL-ss-CY |
SBC HL,DE |
15 |
2 |
|
ED 52 |
|
|
SBC HL,HL |
15 |
2 |
|
ED 62 |
|
|
SBC HL,SP |
15 |
2 |
|
ED 72 |
|
|
SCF |
4 |
1 |
--0-01 |
37 |
Set Carry Flag |
CY=1 |
SET b,r |
8 |
2 |
------ |
CB C0+8*b+rb |
Set bit |
m=mv{2^b} |
SET b,(HL) |
15 |
2 |
|
CB C6+8*b |
|
|
SET b,(IX+N) |
23 |
4 |
|
DD CB |
|
|
|
|
|
|
XX C6+8*b |
|
|
SET b,(IY+N) |
23 |
4 |
|
FD CB |
|
|
|
|
|
|
XX C6+8*b |
|
|
SLA r |
8 |
2 |
**0P0* |
CB 20+rb |
Shift Left Arithmetic |
m=m*2 |
SLA (HL) |
15 |
2 |
|
CB 26 |
|
|
SLA (IX+N) |
23 |
4 |
|
DD CB XX 26 |
|
|
SLA (IY+N) |
23 |
4 |
|
FD CB XX 26 |
|
|
SRA r |
8 |
2 |
**0P0* |
CB 28+rb |
Shift Right Arith. |
m=m/2 |
SRA (HL) |
15 |
2 |
|
CB 2E |
|
|
SRA (IX+N) |
23 |
4 |
|
DD CB XX 2E |
|
|
SRA (IY+N) |
23 |
4 |
|
FD CB XX 2E |
|
|
SLL r |
8 |
2 |
**0P0* |
CB 30+rb |
Shift Left Logical* |
m={0,m,CY}<- |
SLL (HL) |
15 |
2 |
|
CB 36 |
|
(SLL instructions |
SLL (IX+N) |
23 |
4 |
|
DD CB XX 36 |
|
are Unsupported) |
SLL (IY+N) |
23 |
4 |
|
FD CB XX 36 |
|
|
SRL r |
8 |
2 |
**0P0* |
CB 38+rb |
Shift Right Logical |
m=->{0,m,CY} |
SRL (HL) |
15 |
2 |
|
CB 3E |
|
|
SRL (IX+N) |
23 |
4 |
|
DD CB XX 3E |
|
|
SRL (IY+N) |
23 |
4 |
|
FD CB XX 3E |
|
|
SUB r |
4 |
1 |
***V1* |
90+rb |
Subtract |
A=A-s |
SUB N |
7 |
2 |
|
D6 XX |
|
|
SUB (HL) |
7 |
1 |
|
96 |
|
|
SUB (IX+N) |
19 |
3 |
|
DD 96 XX |
|
|
SUB (IY+N) |
19 |
3 |
|
FD 96 XX |
|
|
XOR r |
4 |
1 |
***P00 |
A8+rb |
Logical Exclusive OR |
A=Axs |
XOR N |
7 |
2 |
|
EE XX |
|
|
XOR (HL) |
7 |
1 |
|
AE |
|
|
XOR (IX+N) |
19 |
3 |
|
DD AE XX |
|
|
XOR (IY+N) |
19 |
3 |
|
FD AE XX |
|
|
La leyenda para interpretar esta tabla es la siguiente:
Símbolo |
Significado |
n |
Immediate addressing |
nn |
Immediate extended addressing |
e |
Relative addressing (PC=PC+2+offset) |
(nn) |
Extended addressing |
(xx+d) |
Indexed addressing |
r |
Register addressing |
(rr) |
Register indirect addressing |
|
Implied addressing |
b |
Bit addressing |
p |
Modified page zero addressing (see RST) |
* |
Undocumented opcode |
A B C D E |
Registers (8-bit) |
AF BC DE HL |
Register pairs (16-bit) |
F |
Flag register (8-bit) |
I |
Interrupt page address register (8-bit) |
IX IY |
Index registers (16-bit) |
PC |
Program Counter register (16-bit) |
R |
Memory Refresh register |
SP |
Stack Pointer register (16-bit) |
b |
One bit (0 to 7) |
cc |
Condition (C,M,NC,NZ,P,PE,PO,Z) |
d |
One-byte expression (-128 to +127) |
dst |
Destination s, ss, (BC), (DE), (HL), (nn) |
e |
One-byte expression (-126 to +129) |
m |
Any register r, (HL) or (xx+d) |
n |
One-byte expression (0 to 255) |
nn |
Two-byte expression (0 to 65535) |
pp |
Register pair BC, DE, IX or SP |
qq |
Register pair AF, BC, DE or HL |
qq' |
Alternative register pair AF, BC, DE or HL |
r |
Register A, B, C, D, E, H or L |
rr |
Register pair BC, DE, IY or SP |
s |
Any register r, value n, (HL) or (xx+d) |
src |
Source s, ss, (BC), (DE), (HL), nn, (nn) |
ss |
Register pair BC, DE, HL or SP |
xx |
Index register IX or IY |
+ - * / ^ |
Add/subtract/multiply/divide/exponent |
& ~ v x |
Logical AND/NOT/inclusive OR/exclusive OR |
<- -> |
Rotate left/right |
( ) |
Indirect addressing |
( )+ -( ) |
Indirect addressing auto-increment/decrement |
{ } |
Combination of operands |
# |
Also BC=BC-1,DE=DE-1 |
## |
Only lower 4 bits of accumulator A used |
Unos apuntes sobre esta tabla:
1.- En instrucciones como "ADC A, r" podemos ver una defición del OPCODE como "88+rb".
En este caso, el opcode final se obtendría sumando a "88h" un valor de 0 a 7 según
el registro al que nos referimos:
Registro |
Valor RB |
A |
7 |
B |
0 |
C |
1 |
D |
2 |
E |
3 |
H |
4 |
L |
5 |
(HL) |
6 |
Por ejemplo, "ADC A, B" se codificaría en memoria como "88+0=88".
2.- En los saltos hay 2 tiempos de ejecución diferentes (por ejemplo, 10/1). En este
caso el valor más alto (10) son los t-estados o ciclos que toma la instrucción cuando
el salto se realiza, y el más bajo (1) es lo que tarda la instrucción cuando no se
salta al destino. Como véis, a la hora de programar una rutina que tenga saltos o
bifurcaciones, es interesante programarla de forma que el caso más común, el que se
produzca la mayoría de las veces, no produzca un salto.
3.- La descripción de las afectaciones de flags son las siguientes:
--------+-------+----------------------------------------------
| F | -*01? |Flag unaffected/affected/reset/set/unknown |
| S | S |Sign flag (Bit 7) |
| Z | Z |Zero flag (Bit 6) |
| HC | H |Half Carry flag (Bit 4) |
| P/V | P |Parity/Overflow flag (Bit 2, V=overflow) |
| N | N |Add/Subtract flag (Bit 1) |
| CY | C|Carry flag (Bit 0) |
+---------------+---------------------------------------------+
|
Instrucciones no documentadas del Z80
En Internet podemos encontrar gran cantidad de documentación acerca del Z80
y su juego de instrucciones, incluyendo las especificaciones oficiales del
microprocesador Z80 de Zilog.
No obstante, existen una serie de instrucciones u opcodes que el microprocesador
puede ejecutar y que no están detallados en la documentación oficial de Zilog.
Con respecto a esto, tenemos la suerte de disponer de algo que los programadores
de la época del Spectrum no tenían: una descripción detallada de las instrucciones
no documentadas del Z80. Aunque la mayoría son instrucciones repetidas de sus
versiones documentadas, hay algunas instrucciones curiosas y a las que tal vez
le podamos sacar alguna utilidad.
¿Por qué existen estos opcodes y no fueron documentados? Supongo que algunos de
ellos no fueron considerados como "merecedores de utilidad alguna" y los ingenieros
de Zilog no los documentaron, o tal vez sean simplemente un resultado no previsto
de la ejecución del Z80 porque los diseñadores no pensaron que al microprocesador
pudieran llegarle dichos códigos. El caso es que para el microprocesador existen
"todos" los opcodes, otra cosa es qué haga al leerlos y decodificarlos. En este
caso algunos de ellos realizan funciones válidas mientras que otros son el equivalente
a ejecutar 2 instrucciones NOP, por ejemplo.
¿Cuál es la utilidad de estas instrucciones para los programadores? Para ser
sinceros, como programadores con un ensamblador o un ensamblador cruzado, poca.
Si haces tus programas desde cero con un programa ensamblador, éste se encargará
de la conversión de instrucciones estándar a opcodes, aunque no viene mal conocer
la existencia de estas instrucciones. Para los programadores de emuladores y de
desensambladores, el conocimiento de estos opcodes es vital.
El juego Sabre Wulf, por ejemplo, utiliza una de estas instrucciones en la determinación
del camino de uno de los enemigos en pantalla (la instrucción SLL, que veremos a
continuación), hasta el punto en que los primeros emuladores de Spectrum emulaban
mal este juego hasta que incluyeron dicha instrucción en la emulación.
Los "undocumented opcodes" son esencialmente opcodes con prefijos CB, ED, DD o
FD
que hacen unas determinadas operaciones y que no están incluídos en la "lista oficial"
que hemos visto hasta ahora. Todos los ejemplos que veremos a continuación están
extraídos del documento "The Undocumented Z80 Documented", de Sean Young.
Prefijo CB
Por ejemplo, los opcodes CB 30, CB 31, CB 32, CB 33, CB 34, CB 35, CB 36 y CB 37
definen una nueva instrucción: SLL.
OPCODE |
INSTRUCCION |
CB 30 |
SLL B |
CB 31 |
SLL C |
CB 32 |
SLL D |
CB 33 |
SLL E |
CB 34 |
SLL H |
CB 35 |
SLL L |
CB 36 |
SLL (HL) |
CB 37 |
SLL A |
SLL (Shift Logical Left) funciona exactamente igual que SLA salvo porque pone a
1 el bit 0 (mientras que SLA lo ponía a 0).
Prefijos DD y FD
En general, una instrucción precedida por el opcode DD se ejecuta igual que sin él
excepto por las siguientes reglas:
- Si la instrucción usaba el registro HL, éste se sustituye por IX (excepto en las
instrucciones EX DE, HL y EXX).
- Cualquier uso de (HL) se reemplaza por (IX+d), excepto JP (HL).
- Cualquier acceso a H se reemplaza por IXh (byte alto de IX), excepto en el uso de
(IX+d).
- Cualquier acceso a L se reemplaza por IXl (byte alto de IX), excepto en el uso de
(IX+d).
Por ejemplo:
Sin el prefijo DD |
Con el Prefijo DD |
LD HL, 0 |
LD IX, 0 |
LD H, A |
LD IXh, A |
LD H, (HL) |
LD H, (IX+d) |
El caso de FD es exactamente igual que el de DD, pero usando el registro IY en lugar del
IX.
Prefijo ED
Hay una gran cantidad de instrucciones ED XX indocumentadas. Muchos de ellos realizan
la misma función que sus equivalentes sin ED delante, mientras que otros simplemente
son leídos y decodificados, resultando, a niveles prácticos, equivalentes a 2
instrucciones
NOP. Veamos algunos de ellos:
OPCODE |
Instrucción |
ED 4C |
NEG |
ED 4E |
IM 0 |
ED 44 |
NEG |
ED 45 |
RETN |
ED 5C |
NEG |
ED 5D |
RETN |
ED 64 |
NEG |
ED 65 |
RETN |
ED 66 |
IM 0 |
ED 6C |
NEG |
ED 6D |
RETN |
ED 6E |
IM 0 |
ED 70 |
IN (C) / IN F,(C) |
ED 71 |
OUT (C),0 |
ED 74 |
NEG |
ED 75 |
RETN |
ED 76 |
IM1 |
ED 77 |
NOP |
ED 7C |
NEG |
ED 7D |
RETN |
ED 7E |
IM2 |
ED 7F |
NOP |
Aparte de los duplicados de NOP, NEG, IM0, etc, podemos ver un par de instrucciones
curiosas y que nos pueden ser de utilidad. Por ejemplo:
Esta instrucción lee el puerto C, pero no almacena el resultado de la lectura en
ningún lugar. No obstante, altera los flags del registro F como corresponde al
resultado leído. Puede ser interesante si sólo nos interesa, por ejemplo, si el
valor leído es cero o no (flag Z), y no queremos perder un registro para almacenar
el resultado.
Prefijos DDCB y FDCB
Las instrucciones DDCB y FDCB no documentadas almacenan el resultado de la
operación de la instrucción equivalente sin prefijo (si existe dicho resultado)
en uno de los registros de propósito general: B, C, D, E, H, L, ninguno o A,
según los 3 bits más bajos del último byte del opcode (000=B, 001=C, 010=D, etc).
Así, supongamos el siguiente opcode sí documentado:
Si hacemos los 3 últimos bits de dicho opcode 010 (010), el resultado de la
operación se copia al registro D (010 = D en nuestra definición anterior), con
lo que realmente, en lugar de "RLC (IX+01h)" se ejecuta:
LD D, (IX+01h)
RLC D
LD (IX+01h), D
|
La notación que sugiere Sean Young para estos opcodes es: "RLC (IX+01h), D".
Con el prefijo FDCB ocurre igual que con DDCB, salvo que se usa el registro IY
en lugar de IX.
De la teoria a la practica
Con este capítulo hemos cubierto el 99% de las instrucciones soportadas
por el microprocesador Z80. Con la excepción de los Modos de Interrupciones
del Z80 y sus aplicaciones, ya tenemos a nuestra disposición las piezas
básicas para formar cualquier programa o rutina en ensamblador.
No obstante, todavía quedan por delante muchas horas de programación
para dominar este lenguaje, así como diferentes técnicas, trucos,
rutinas y mapas de memoria que nos permitan dibujar nuestros gráficos,
realizar rutinas complejas, utilizar el sistema de interrupciones del
microprocesador para realizar controles de temporización de nuestros
programas, o reproducir sonido.
FICHEROS
LINKS