cursos:ensamblador:lenguaje_1

¡Esta es una revisión vieja del documento!


Lenguaje Ensamblador del Z80 (I)

En esta tercera entrega del curso explicaremos la sintaxis utilizada en los programas en ensamblador. Para ello comenzaremos con una definición general de la sintaxis del lenguaje para el ensamblador Pasmo, que será el “traductor” que usaremos entre el lenguaje ensamblador y el código máquina del Z80.

Posteriormente veremos en detalle los registros: qué registros hay disponibles, cómo se agrupan, y el registro especial de Flags, enlazando el uso de estos registros con las instrucciones de carga, de operaciones aritméticas, y de manejo de bits, que serán las que trataremos hoy.

Esta entrega del curso es delicada y complicada: por un lado, tenemos que explicar las normas y sintaxis del ensamblador cruzado PASMO antes de que conozcamos la sintaxis del lenguaje ensamblador en sí, y por el otro, no podremos utilizar PASMO hasta que conozcamos la sintaxis del lenguaje.

Además, el lenguaje ensamblador tiene disponibles muchas instrucciones diferentes, y nos resultaría imposible explicarlas todas en un mismo capítulo, lo que nos fuerza a explicar las instrucciones del microprocesador en varias entregas. Esto implica que hablaremos de PASMO comentando reglas, opciones de instrucciones y directivas que todavía no conocemos.

Es por esto que recomendamos al lector que, tras releer los capítulos 1 y 2 de nuestro curso, se tome esta entrega de una manera especial, leyéndola 2 veces. La “segunda pasada” sobre el texto permitirá enlazar todos los conocimientos dispersos en el mismo, y que no pueden explicarse de una manera lineal porque están totalmente interrelacionados. Además, la parte relativa a la sintaxis de PASMO será una referencia obligada para posteriores capítulos (mientras continuemos viendo diferentes instrucciones ASM y ejemplos).

Así pues, os recomendamos leer este capítulo dos veces, una para absorber las normas de PASMO, y otra para absorber la sintaxis del lenguaje y las instrucciones que explicaremos (terminando de comprender conceptos de la primera lectura).

En la primera parte de curso se introdujo PASMO, el ensamblador cruzado que recomendamos para el desarrollo de programas para Spectrum. Este ensamblador traduce nuestros ficheros de texto .asm con el código fuente de programa (en lenguaje ensamblador) a ficheros .bin (o .tap/.tzx) que contendrán el código máquina directamente ejecutable por el Spectrum. Suponemos que ya tenéis instalado PASMO (ya sea la versión Windows o la de Linux) en vuestro sistema y que sabéis utilizarlo (os recomiendo que releáis la primera entrega de nuestro curso si no recordáis su uso), y que podéis ejecutarlo dentro del directorio de trabajo que habéis elegido (por ejemplo: /home/usuario/z80asm o C:\z80asm).

El ciclo de desarrollo con PASMO será el siguiente:

  • Con un editor de texto, tecleamos nuestro programa en un fichero .ASM con la sintaxis que veremos a continuación.
  • Salimos del editor de texto y ensamblamos el programa con: “pasmo ejemplo1.asm ejemplo1.bin”.
  • El fichero bin contiene el código máquina resultante, que podemos POKEar en memoria, cargar como un bloque CM (LOAD “” CODE) tras nuestro programa BASIC, o bien si el programa está realizado enteramente en ensamblador, hacer un pequeño cargador (o usar bin2tap o –bastap) y cargar directamente el programa en el Spectrum.

Todo esto se mostró bastante detalladamente en su momento en el número 1 de nuestro curso.

Con esto, sabemos ensamblar programas creados adecuadamente, de modo que la pregunta es: ¿cómo debo escribir mi programa para que PASMO pueda ensamblarlo? Es sencillo, escribiremos nuestro programa en un fichero de texto con extensión (por ejemplo) .asm. En este fichero de texto se ignorarán las líneas en blanco y los comentarios, que en ASM de Z80 se introducen con el símbolo “;” (punto y coma), de forma que todo lo que el ensamblador encuentre a la derecha de un ; será ignorado (siempre que no forme parte de una cadena). Ese fichero de texto será ensamblado por PASMO y convertido en código binario.

Lo que vamos a ver a continuación son las normas que debe cumplir un programa para poder ser ensamblado en PASMO. Es necesario explicar estas reglas para que el lector pueda consultarlas en el futuro, cuando esté realizando sus propios programas. No te preocupes si no entiendes alguna de las reglas, cuando llegues al momento de implementar tus primeras rutinas, las siguientes normas te serán muy útiles:

  • Normas para las instrucciones:
    • Pondremos una sóla instrucción de ensamblador por línea.
    • Como existen diferencias entre los “fin de línea” entre Linux y Windows, es recomendable que los programas se ensamblen con PASMO en la misma plataforma de S.O. en que se han escrito. Si PASMO intenta compilar en Windows un programa ASM escrito en un editor de texto de Linux (con retornos de carro de Linux) es posible que obtengamos errores de compilación (aunque no es seguro). Si os ocurre al compilar los ejemplos que os proporcionamos (están escritos en Linux) y usáis Windows, lo mejor es abrir el fichero .ASM con notepad y grabarlo de nuevo (lo cual lo salvará con formato de retornos de carro de Windows). El fichero “regrabado” con Notepad podrá ser ensamblado sin problemas.
    • Además de una instrucción por línea podremos añadir etiquetas (para referenciar a dicha línea, algo que veremos posteriormente) y comentarios (con ';').


  • Normas para los valores numéricos:
    • Todos los valores numéricos se considerarán, por defecto, escritos en decimal.
    • Para introducir valores números en hexadecimal los precederemos del carácter “$”, y para escribir valores numéricos en binario lo haremos mediante el carácter “%”.
    • Podremos también especificar la base del literal poniendoles como prefijo las cadena &H ó 0x (para hexadecimal) o &O (para octal).
    • Podemos especificar también los números mediante sufijos: Usando una “H” para hexadecimal, “D” para decimal, “B” para binario u “O” para octal (tanto mayúsculas como minúsculas).


  • Normas para cadenas de texto:
    • Podemos separar las cadenas de texto mediante comillas simples o dobles.
    • El texto encerrado entre comillas simples no recibe ninguna interpretación, excepto si se encuentran 2 comillas simples consecutivas, que sirven para introducir una comilla simple en la cadena.
    • El texto encerrado entre comillas dobles permite introducir caracteres especiales al estilo de C/C++ como \n, \r o \t (nueva línea, retorno de carro, tabulador…).
    • El texto encerrado entre comillas dobles también admite \xNN para introducir el carácter correspondiente a un número hexadecimal NN.
    • Una cadena de texto de longitud 1 (un carácter) puede usarse como una constante (valor ASCII del carácter) en expresiones como, por ejemplo, 'C'+10h.


  • Normas para los nombres de ficheros:
    • Si vemos que nuestro programa se hace muy largo, podemos partir el fichero en varios ficheros e incluirlos mediante directivas INCLUDE (para incluir ficheros ASM) o INCBIN (para incluir código máquina ya compilado). Al especificar nombres de ficheros, deberán estar entre dobles comillas o simples comillas.


  • Normas para los identificadores:
    • Los identificadores son los nombres usados para etiquetas y símbolos definidos mediante EQU y DEFL.
    • Podemos utilizar cualquier cadena de texto, excepto los nombres de las palabras reservadas de ensamblador.


  • Normas para las etiquetas:
    • Una etiqueta es un identificador de texto que ponemos poner al principio de cualquier línea de nuestro programa.
    • Podemos añadir el tradicional sufijo “:” a las etiquetas, pero también es posible no incluirlo si queremos compatibilidad con otros ensambladores que no lo soporten (por si queremos ensamblar nuestro programa con otro ensamblador que no sea pasmo).
    • Para PASMO, cualquier referencia a una etiqueta a lo largo del programa se convierte en una referencia a la posición de memoria donde se ensambla la instrucción o dato donde hemos colocado la etiqueta. Podemos utilizar así etiquetas para hacer referencia a nuestros gráficos, variables, datos, funciones, lugares a donde saltar, etc.


  • Directivas:
    • Tenemos a nuestra disposición una serie de directivas para facilitarnos la programación, como DEFB o DB para introducir datos en crudo en nuestro programa, END para finalizar el programa, IF/ELSE/ENDIF en tiempo de compilación, INCLUDE e INCBIN, MACRO y REPT.
    • Una de las directivas más importantes es ORG, que indica la posición origen donde almacenar el código que la sigue. Podemos utilizar diferentes directivas ORG en un mismo programa.
    • Iremos viendo el significado de las directivas conforme las vayamos usando, pero es aconsejable consultar el manual de PASMO para conocer más sobre ellas.


    • Podemos utilizar los operadores típicos +, -, *. /, así como otros operadores de desplazamiento de bits como » y «.
    • Tenemos disponibles operadores de comparación como EQ, NE, LT, LE, GT, GE o los clásicos =, !=, <, >, ⇐, >=.
    • Existen también operadores lógicos como AND, OR, NOT, o sus variantes &, |, !.
    • Los operadores sólo tienen aplicación en tiempo de ensamblado, es decir, no podemos multiplicar o dividir en tiempo real en nuestro programa usando * o /. Estos operadores están pensados para que podamos poner expresiones como ((32*10)+12), en lugar del valor numérico del resultado, por ejemplo.

Veamos un ejemplo de programa en ensamblador que muestra el uso de algunas de estas normas, para que las podamos entender fácilmente mediante los comentarios incluidos:

; Programa de ejemplo para mostrar el aspecto de
; un programa típico en ensamblador para PASMO.
; Copia una serie de bytes a la videomemoria con
; instrucciones simples (sin optimizar).
ORG 40000
 
valor     EQU  1
destino   EQU  18384
 
  ; Aqui empieza nuestro programa que copia los
  ; 7 bytes desde la etiqueta "datos" hasta la
  ; videomemoria ([16384] en adelante).
 
  LD HL, destino     ; HL = destino (VRAM)
  LD DE, datos       ; DE = origen de los datos
  LD B, 6            ; numero de datos a copiar
 
bucle:               ; etiqueta que usaremos luego
 
  LD A, (DE)         ; Leemos un dato de [DE]
  ADD A, valor       ; Le sumamos 1 al dato leído
  LD (HL), A         ; Lo grabamos en el destino [HL]
  INC DE             ; Apuntamos al siguiente dato
  INC HL             ; Apuntamos al siguiente destino
 
  DJNZ bucle         ; Equivale a:
                     ; B = B-1
                     ; if (B>0) goto Bucle
  RET
 
datos DEFB 127, %10101010, 0, 128, $FE, %10000000, FFh
 
END

Algunos detalles a tener en cuenta:

  • Como veis, se pone una instrucción por línea.
  • Los comentarios pueden ir en sus propias líneas, o dentro de líneas de instrucciones (tras ellas).
  • Podemos definir “constantes” con EQU para hacer referencia a ellas luego en el código. Son constantes, no variables, es decir, se definen en tiempo de ensamblado y no se cambian con la ejecución del programa. Su uso está pensado para poder escribir código más legible y que podamos cambiar los valores asociados posteriormente de una forma sencilla (es más fácil cambiar el valor asignado en el EQU, que cambiar un valor en todas sus apariciones en el código).
  • Podemos poner etiquetas (como “bucle” y “datos” -con o sin dos puntos, son ignorados-) para referenciar a una posición de memoria. Así, la etiqueta “bucle” del programa anterior hace referencia a la posición de memoria donde se ensamblaría la siguiente instrucción que aparece tras ella. Las etiquetas se usan para poder saltar a ellas (en los bucles y condiciones) mediante un nombre en lugar de tener que calcular nosotros la dirección del salto a mano y poner direcciones de memoria. Es más fácil de entender y programar un “DJNZ bucle” que un “DJNZ 40008”, por ejemplo. En el caso de la etiqueta “datos”, nos permite referenciar la posición en la que empiezan los datos que vamos a copiar.
  • Los datos definidos con DEFB pueden estar en cualquier formato numérico, como se ha mostrado en el ejemplo: decimal, binario, hexadecimal tanto con prefijo “$” como con sufijo “h”, etc.

Podéis ensamblar el ejemplo anterior mediante:

pasmo --tapbas ejemplo.asm ejemplo.tap

Una vez cargado y ejecutado el TAP en el emulador de Spectrum, podréis ejecutar el código máquina en BASIC con un “RANDOMIZE USR 40000”, y deberéis ver una pantalla como la siguiente:

Salida del ejemplo 1

Los píxeles que aparecen en el centro de la pantalla se corresponden con los valores numéricos que hemos definido en “datos”, ya que los hemos copiado desde “datos” hasta la videomemoria. No os preocupéis por ahora si no entendéis alguna de las instrucciones utilizadas, las iremos viendo poco a poco y al final tendremos una visión global y concreta de todas ellas.

Como ya vimos en la anterior entrega, todo el “trabajo de campo” lo haremos con los registros de la CPU, que no son más que variables de 8 y 16 bits integradas dentro del Z80 y que por tanto son muy rápidos para realizar operaciones con ellos.

El Z80 tiene una serie de registros de 8 bits con nombres específicos:

  • A: El Registro A (de 8 bits) es el acumulador. Es un registro que se utiliza generalmente como destino de muchas operaciones aritméticas y de comparaciones y testeos.
  • B, C, D, E, H, L: Registros de propósito general, utilizables para gran cantidad de operaciones, almacenamiento de valores, etc.
  • I: Registro de interrupción, no lo utilizaremos en nuestros primeros programas. No debemos modificar su valor, aunque en el futuro veremos su uso en las interrupciones del Spectrum.
  • R: Registro de Refresco de memoria: lo utiliza internamente la CPU para saber cuándo debe refrescar la RAM. Su valor cambia sólo conforme el Z80 va ejecutando instrucciones, de modo que podemos utilizarlo (leerlo) para obtener valores pseudo-aleatorios entre 0 y 255.


Además, podemos agrupar algunos de estos registros en pares de 16 bits para determinadas operaciones:

  • AF: Formado por el registro A como byte más significativo (Byte alto) y por F como byte menos significativo (Byte bajo). Si A vale FFh y B vale 00h, AF valdrá automáticamente “FF00h”.
  • BC: Agrupación de los registros B y C que se puede utilizar en bucles y para acceder a puertos. También se utiliza como “repetidor” o “contador” en las operaciones de acceso a memoria (LDIR, LDDR, etc.).
  • DE, HL: Registros de 16 bits formados por D y E por un lado y H y L por otro. Utilizaremos generalmente estos registros para leer y escribir en memoria en una operación única, así como para las operaciones de acceso a memoria como LDIR, LDDR, etc.


Aparte de estos registros, existen otra serie de registros de 16 bits:

  • IX, IY: Dos registros de 16 bits pensados para acceder a memoria de forma indexada. Gracias a estos registros podemos realizar operaciones como: “LD (IX+desplazamiento), VALOR”. Este tipo de registros se suele utilizar pues para hacer de índices dentro de tablas o vectores. El desplazamiento es un valor numérico de 8 bits en complemento a 2, lo que nos permite un rango desde -128 a +127 (puede ser negativo para acceder a posiciones de memoria anteriores a IX).
  • SP: Puntero de pila, como veremos en su momento apunta a la posición actual de la “cabeza” de la pila.
  • PC: Program Counter o Contador de Programa. Como ya vimos en la anterior entrega, contiene la dirección de la instrucción actual a ejecutar. No modificaremos PC directamente moviendo valores a este registro, sino que lo haremos mediante instrucciones de salto (JP, JR, CALL…).


Por último, tenemos disponible un banco alternativo de registros, conocidos como Shadow Registers o Registros Alternativos, que se llaman igual que sus equivalentes principales pero con una comilla simple detrás: A', F', B', C', D'. E', H' y L'.

En cualquier momento podemos intercambiar el valor de los registros A, B, C, D, E, F, H y L con el valor de los registros A', B', C', D', E', F', H' y L' mediante la instrucción de ensamblador “EXX”, como veremos más adelante. La utilidad de estos Shadow Registers es almacenar valores temporales y proporcionarnos más registros para operar: podremos intercambiar el valor de los registros actuales con los temporales, realizar operaciones con los registros sin perder los valores originales (que al hacer el EXX se quedarán en los registros Shadow), y después recuperar los valores originales volviendo a ejecutar un EXX.

Ya conocemos los registros disponibles, veamos ahora ejemplos de operaciones típicas que podemos realizar con ellos:

  • Meter valores en registros (ya sean valores numéricos directos, de memoria, o de otros registros).
  • Incrementar o decrementar los valores de los registros.
  • Realizar operaciones (tanto aritméticas como lógicas) entre los registros.
  • Acceder a memoria para escribir o leer.

Por ejemplo, las siguientes instrucciones en ensamblador serían válidas:

  LD C, $00       ; C vale 0
  LD B, $01       ; B vale 1
                  ; con esto, BC = $0100
  LD A, B         ; A ahora vale 1
  LD HL, $1234    ; HL vale $1234 o 4660d
  LD A, (HL)      ; A contiene el valor de (4660)
  LD A, (16384)   ; A contiene el valor de (16384)
  LD (16385), A   ; Escribimos en (16385) el valor de A
  ADD A, B        ; Suma: A = A + B
  INC B           ; Incrementamos B (B = 1+1 =2)
                  ; Ahora BC vale $0200
  INC BC          ; Incrementamos BC
                  ; (BC = $0200+1 = $0201)

Dentro del ejemplo anterior queremos destacar el operador “()”, que significa “el contenido de la memoria apuntado por”. Así, “LD A, (16384)” no quiere decir “mete en A el valor 16384” (cosa que además no se puede hacer porque A es un registro de 8 bits), sino “mete en A el valor de 8 bits que contiene la celdilla de memoria 16384” (equivalente a utilizar en BASIC las funciones PEEK y POKE, como en LET A=PEEK 16384).

Cabe destacar un gran inconveniente del juego de instrucciones del Z80, y es que no es ortogonal. Se dice que el juego de instrucciones de un microprocesador es ortogonal cuando puedes realizar todas las operaciones sobre todos los registros, sin presentar excepciones. En el caso del Z80 no es así, ya que hay determinadas operaciones que podremos realizar sobre unos registros pero no sobre otros.

Así, si el Z80 fuera ortogonal, podríamos ejecutar cualquiera de estas operaciones:

  LD BC, 1234h
  LD HL, BC
  LD SP, BC
  EX DE, HL
  EX BC, DE
  ADD HL, BC
  ADD DE, BC

Sin embargo, como el Z80 no tiene un juego de instrucciones (J.I. desde este momento) ortogonal, hay instrucciones del ejemplo anterior que no son válidas, es decir, que no tienen dentro de la CPU un microcódigo para que el Z80 sepa qué hacer con ellas:

  LD SP, BC      ; NO: No se puede cargar el valor un registro en SP,
                 ; sólo se puede cargar un valor inmediato NN
 
  EX BC, DE      ; NO: Existe EX DE, HL, pero no EX BC, DE
 
  ADD DE, BC     ; NO: Sólo se puede usar HL como operando destino
                 ; en las sumas de 16 bytes con registros de propósito
                 ; general.

Si el J.I. fuera ortogonal, se podría realizar cualquier operación con cualquier registro, como por ejemplo:

  LD BC, DE
  LD DE, HL
  LD SP, BC      ; Se podría realizar

Pero “LD SP, BC” es una excepción, y no existe como instrucción del Z80. Y como el caso de “LD SP, BC” existen muchos otros de instrucciones que aceptan unos registros como operandos pero no otros.

La única solución para programar sin tratar de utilizar instrucciones no permitidas es la práctica: con ella acabaremos conociendo qué operaciones podemos realizar y sobre qué registros se pueden aplicar, y realizaremos nuestros programas con estas limitaciones en mente. Iremos viendo las diferentes excepciones caso a caso, pero podemos encontrar las nuestras propias gracias a los errores que nos dará el ensamblador al intentar ensamblar un programa con una instrucción que no existe para el Z80.

No os preocupéis: es sólo una cuestión de práctica. Tras haber realizado varios programas en ensamblador ya conoceréis, prácticamente de memoria, qué instrucciones son válidas para el microprocesador y cuáles no.

Hemos hablado del registro de 8 bits F como un registro especial. La particularidad de F es que no es un registro de propósito general donde podamos introducir valores a voluntad, sino que los diferentes bits del registro F tienen un significado propio que cambia automáticamente según el resultado de operaciones anteriores.

Por ejemplo, uno de los bits del registro F, el bit nº 6, es conocido como “Zero Flag”, y nos indica si el resultado de la última operación (para determinadas operaciones, como las aritméticas o las de comparación) es cero o no es cero. Si el resultado de la anterior operación resultó cero, este FLAG se pone a uno. Si no resultó cero, el flag se pone a cero.

¿Para qué sirve pues un flag así? Para gran cantidad de tareas, por ejemplo para bucles (repetir X veces una misma tarea poniendo el registro BC al valor X, ejecutando el mismo código hasta que BC sea cero), o para comparaciones (mayor que, menor que, igual que).

Veamos los diferentes registros de flags (bits del registro F) y su utilidad:

Los indicadores de flag del registro F

  • Flag S (sign o signo): Este flag se pone a uno si el resultado de la operación realizada en complemento a dos es negativo (es una copia del bit más significativo del resultado). Si por ejemplo realizamos una suma entre 2 números en complemento a dos y el resultado es negativo, este bit se pondrá a uno. Si el resultado es positivo, se pondrá a cero. Es útil para realizar operaciones matemáticas entre múltiples registros: por ejemplo, si nos hacemos una rutina de multiplicación o división de números que permita números negativos, este bit nos puede ser útil en alguna parte de la rutina.
  • Flag Z (zero o cero): Este flag se pone a uno si el resultado de la última operación que afecte a los flags es cero. Por ejemplo, si realizamos una operación matemática y el resultado es cero, se pondrá a uno. Este flag es uno de los más útiles, ya que podemos utilizarlo para múltiples tareas. La primera es para los bucles, ya que podremos programar código como:
            ; Repetir algo 100 veces
            LD B, 100
    bucle:
            (...)        ; código
 
            DEC B        ; Decrementamos B (B=B-1)
            JR NZ bucle  
            ; Si el resultado de la operación anterior no es cero (NZ = Non Zero), 
            ; saltar a la etiqueta bucle y continuar. DEC B hará que el flag Z 
            ; se ponga a 1 cuando B llegue a cero, lo que afectará al JR NZ.
            ; Como resultado, este trozo de código (...) se ejecutará 100 veces.

Como veremos en su momento, existe una instrucción equivalente a DEC B + JR NZ que es más cómoda de utilizar y más rápida que estas 2 instrucciones juntas (DJNZ), pero se ha elegido el ejemplo que tenéis arriba para que veáis cómo muchas operaciones (en este caso DEC) afectan a los flags, y la utilidad que estos tienen a la hora de programar.

Además de para bucles, también podemos utilizarlo para comparaciones. Supongamos que queremos hacer en ensamblador una comparación de igualdad, algo como:

   IF C = B THEN GOTO 1000
   ELSE          GOTO 2000

Si restamos C y B y el resultado es cero, es que ambos registros contienen el mismo valor:

     LD A, C              ; A = C
     ; Tenemos que hacer esto porque no existe
     ; una instruccion SUB B, C . Sólo se puede
     ; restar un registro al registro A.
 
     SUB B                ; A = A-B
     JP Z, Es_Igual       ; Si A=B la resta es cero y Z=1
     JP NZ, No_Es_Igual   ; Si A<>B la resta no es cero y Z=0
     (...)
 
    Es_Igual:
     (...)
    No_Es_Igual:
     (...)
  • Flag H (Half-carry o Acarreo-BCD): Se pone a uno cuando en operaciones BCD existe un acarreo del bit 3 al bit 4. Es muy probable que no lleguemos a utilizarlo nunca.
  • Flag P/V (Parity/Overflow o Paridad/Desbordamiento): En las operaciones que modifican el bit de paridad, este bit vale 1 si el número de unos del resultado de la operación es par, y 0 si es impar. Si, por contra, el resultado de la operación realizada necesita más bits para ser representado de los que nos provee el registro, tendremos un desbordamiento, con este flag a 1. Este mismo bit sirve pues para 2 tareas, y nos indicará una u otra (paridad o desbordamiento) según sea el tipo de operación que hayamos realizado. Por ejemplo, tras una suma, su utilidad será la de indicar el desbordamiento.
    El flag de desbordamiento se activará cuando en determinadas operaciones pasemos de valores 11111111b a 00000000b, por “falta de bits” para representar el resultado o viceversa . Por ejemplo, en el caso de INC y DEC con registros de 8 bits, si pasamos de 0 a 255 o de 255 a 0.
  • Flag N (Substract o Resta): Se pone a 1 si la última operación realizada fue una resta. Se utiliza en operaciones aritméticas.
  • Flag C (Carry o Acarreo): Este flag se pone a uno si el resultado de la operación anterior no cupo en el registro y necesita un bit extra para ser representado. Este bit es ese bit extra. Veremos su uso cuando tratemos las operaciones aritméticas, en esta misma entrega.

Así pues, resumiendo:

  • El registro F es un registro especial cuyo valor no manejamos directamente, sino que cada uno de sus bits tiene un valor especial y está a 1 o a 0 según ciertas condiciones de la última operación realizada que afecte a dicho registro.
  • Por ejemplo, si realizamos una operación y el resultado de la misma es cero, se pondrá a 1 el flag de Zero (Z) del registro F, que no es más que su bit número 6.
  • No todas las operaciones afectan a los flags, iremos viendo qué operaciones afectan a qué flags conforme avancemos en el curso, en el momento en que se estudia cada instrucción.
  • Existen operaciones que se pueden ejecutar con el estado de los flags como condición. Por ejemplo, realizar un salto a una dirección de memoria si un determinado flag está activo, o si no lo está.

Las operaciones que más utilizaremos en nuestros programas en ensamblador serán sin duda las operaciones de carga o instrucciones LD. Estas operaciones sirven para:

  • Meter un valor en un registro.
  • Copiar el valor de un registro en otro registro.
  • Escribir en memoria (en una dirección determinada) un valor.
  • Escribir en memoria (en una dirección determinada) el contenido de un registro.
  • Asignarle a un registro el contenido de una dirección de memoria.

La sintaxis de LD en lenguaje ensamblador es:

 LD DESTINO, ORIGEN

Así, gracias a las operaciones LD podemos:

  • Asignar a un registro un valor numérico directo de 8 o 16 bits.
        LD A, 10         ; A = 10
        LD B, 200        ; B = 200
        LD BC, 12345     ; BC = 12345
  • Copiar el contenido de un registro a otro registro:
        LD A, B          ; A = B
        LD BC, DE        ; BC = DE
  • Escribir en posiciones de memoria:
        LD (12345), A    ; Memoria[12345] = valor en A
        LD (HL), 10      ; Memoria[valor de HL] = 10
  • Leer el contenido de posiciones de memoria:
        LD A, (12345)    ; A = valor en Memoria[12345]
        LD B, (HL)       ; B = valor en Memoria[valor de HL]

Nótese cómo el operador () nos permite acceder a memoria. En nuestros ejemplos, LD A, (12345) no significa meter en A el valor 12345 (cosa imposible al ser un registro de 16 bits) sino almacenar en el registro A el valor que hay almacenado en la celdilla número 12345 de la memoria del Spectrum.

En un microprocesador con un juego de instrucciones ortogonal, se podría usar cualquier origen y cualquier destino sin distinción. En el caso del Z80 no es así. El listado completo de operaciones válidas con LD es el siguiente:

Leyenda:

 N  = valor numérico directo de 8 bits (0-255)
 NN = valor numérico directo de 16 bits (0-65535)
 r  = registro de 8 bits (A, B, C, D, E, H, L)
 rr = registro de 16 bits (BC, DE, HL, SP)
 ri = registro índice (IX o IY).

Listado:

 ; Carga de valores en registros
 LD r, N
 LD rr, NN
 LD ri, NN
 
 ; Copia de un registro a otro
 LD r, r
 LD rr, rr
 
 ; Acceso a memoria
 LD r, (HL)
 LD (NN), A
 LD (HL), N
 LD A, (rr)      ; (excepto rr=SP)
 LD (rr), A      ; (excepto rr=SP)
 LD A, (NN)
 LD rr, (NN)
 LD ri, (NN)
 LD (NN), rr
 LD (NN), ri
 
 ; Acceso indexado a memoria
 LD (ri+N), r
 LD r, (ri+N)
 LD (ri+N), N

Además, tenemos una serie de casos “especiales”:

 ; Manipulación del puntero de pila (SP)
 LD SP, ri
 LD SP, HL
 
 ; Para manipular el registro I
 LD A, I
 LD I, A

Veamos ejemplos válidos y cuál sería el resultado de su ejecución:

 ; Carga de valores en registros
 ; registro_destino = valor
 LD A, 100          ; LD r, N
 LD BC, 12345       ; LD rr, NN
 
 ; Copia de registros en registros
 ; registro_destino = registro_origen
 LD B, C            ; LD r, r
 LD A, B            ; LD r, r
 LD BC, DE          ; LD rr, rr
 
 ; Acceso a memoria
 ; (Posicion_memoria) = VALOR o bien
 ;  Registro = VALOR en (Posicion de memoria)
 LD A, (HL)         ; LD r, (rr)
 LD (BL), B         ; LD (rr), r
 LD (12345), A      ; LD (NN), A
 LD A, (HL)         ; LD r, (rr)
 LD (DE), A         ; LD (rr), r
 LD (BC), 1234h     ; LD (BC), NN
 LD (12345), DE     ; LD (NN), rr
 LD IX, (12345)     ; LD ri, (NN)
 LD (34567), IY     ; LD (NN), ri
 
 ; Acceso indexado a memoria
 ; (Posicion_memoria) = VALOR o VALOR = (Posicion_memoria)
 ; Donde la posicion es IX+N o IY+N:
 LD (IX+10), A      ; LD (ri+N), r
 LD A, (IY+100)     ; LD r, (ri+N)
 LD (IX-30), 100    ; LD (ri+N), N

Haré hincapié de nuevo en el mismo detalle: debido a que el juego de instrucciones del Z80 no es ortogonal, en ocasiones no podemos ejecutar ciertas operaciones que podrían sernos útiles con determinados registros. En ese caso tendremos que buscar una solución mediante los registros y operaciones válidas de que disponemos.

Un detalle muy importante respecto a las instrucciones de carga: en el caso de las operaciones LD, el registro F no ve afectado ninguno de sus indicadores o flags en relación al resultado de la ejecución de las mismas (salvo en el caso de “LD A, I” y “LD A, R”).

Esto quiere decir que una operación como “LD A, 0”, por ejemplo, no activará el flag de Zero del registro F.

Un detalle más sobre nuestra CPU: a la hora de trabajar con datos de 16 bits (por ejemplo, leer o escribir de memoria) conviene tener en cuenta que nuestro Z80 es una CPU del tipo LOW-ENDIAN, es decir, que si almacenamos en la posición de memoria 0000h el valor “1234h”, el contenido de las celdillas de memoria sería:

Posición Valor
0000h 34h
0001h 12h

En otro tipo de procesadores del tipo Big-Endian, los bytes aparecerían escritos en memoria de la siguiente forma:

Posición Valor
0000h 12h
0001h 34h

Debemos tener en cuenta este dato a la hora de escribir valores de 16 bits en memoria y recuperarlos posteriormente mediante operaciones de acceso a la memoria.

Entre las operaciones disponibles, tenemos la posibilidad de incrementar (INC) y decrementar (DEC) en 1 unidad el contenido de determinados registros de 8 y 16 bits, así como de posiciones de memoria apuntadas por HL o por IX/IY más un offset (desplazamiento de 8 bits).

Por ejemplo:

 LD A, 0      ; A = 0
 INC A        ; A = A+1 = 1
 LD B, A      ; B = A = 1
 INC B        ; B = B+1 = 2
 INC B        ; B = B+1 = 3
 LD  BC, 0
 INC BC       ; BC = 0001h
 INC B        ; BC = 0101h (ya que B=B+1 y es la parte alta)
 DEC A        ; A = A-1 = 0

Veamos las operaciones INC y DEC permitidas:

   INC r
   DEC r
   INC rr
   DEC rr

Donde r puede ser A, B, C, D, E, H o L, y 'rr' puede ser BC, DE, HL, SP, IX o IY. Esta instrucción incrementa o decrementa el valor contenido en el registro especificado.

   INC (HL)
   DEC (HL)

Incrementa o decrementa el byte que contiene la dirección de memoria apuntada por HL.

   INC (IX+N)
   DEC (IX+N)
   INC (IY+N)
   DEC (IY+N)

Incrementa o decrementa el byte que contiene la dirección de memoria resultante de sumar el valor del registro IX o el registro IY con un valor numérico de 8 bits en complemento a dos.

Por ejemplo, las siguientes instrucciones serían válidas:

 INC A          ; A = A+1
 DEC B          ; B = B-1
 INC DE         ; DE = DE+1
 DEC IX         ; IX = IX-1
 INC (HL)       ; (HL) = (HL)+1
 INC (IX-5)     ; (IX-5) = (IX-5)+1
 DEC (IY+100)   ; (IY+100) = (IY+100)+1

Unos apuntes sobre la afectación de los flags ante el uso de INC y DEC:

  • Si un registro de 8 bits vale 255 (FFh) y lo incrementamos, pasará a valer 0.
  • Si un registro de 16 bits vale 65535 (FFFFh)y lo incrementamos, pasará a valer 0.
  • Si un registro de 8 bits vale 0 y lo decrementamos, pasará a valer 255 (FFh).
  • Si un registro de 16 bits vale 0 (0h) y lo decrementamos, pasará a valer 65535 (FFh).
  • En estos desbordamientos no se tomará en cuenta para nada el bit de Carry (acarreo) de los flags (registro F), ni tampoco lo afectarán tras ejecutarse.
  • Las operaciones INC y DEC sobre registros de 16 bits (BC, DE, HL, IX, IY, SP) no afectan a los flags. Esto implica que no podemos usar como condición de flag zero para un salto el resultado de instrucciones como “DEC BC”, por ejemplo.
  • Las operaciones INC y DEC sobre registros de 8 bits y sobre la memoria no afectan al flag de acarreo, pero sí que pueden afectar al flag de Zero (Z), al de Paridad/Overflow (P/V), al de Signo (S) y al de Half-Carry (H).

Lo siguiente que vamos a ver es una tabla de afectación de flags (que podréis ver en muchas tablas de instrucciones del Z80, y a las que conviene que os vayáis acostumbrando). Esta tabla indica cómo afecta cada instrucción a cada uno de los flags:

                         Flags
   Instrucción       |S Z H P N C|
 ----------------------------------
   INC r             |* * * V 0 -|
   INC [HL]          |* * * V 0 -|
   INC [ri+N]        |* * * V 0 -|
   INC rr            |- - - - - -|
   DEC r             |* * * V 1 -|
   DEC rr            |- - - - - -|

Donde:

 r = registro de 8 bits
 rr = registro de 16 bits (BC, DE, HL, IX, IY)
 ri = registro índice (IX, IY)
 N = desplazamiento de 8 bits (entre -128 y +127).

Y respecto a los flags:

 - = El flag NO se ve afectado por la operación.
 * = El flag se ve afectado por la operación acorde al resultado.
 0 = El flag se pone a cero.
 1 = El flag se pone a uno.
 V = El flag se comporta como un flag de Overflow acorde al resultado.
 ? = El flag toma un valor indeterminado.

Las operaciones aritméticas básicas para nuestro Spectrum son la suma y la resta, tanto con acarreo como sin él. A partir de ellas deberemos crearnos nuestras propias rutinas para multiplicar, dividir, etc.

Nuestro microprocesador Z80 puede realizar sumas de 8 y 16 bits internamente. La instrucción utilizada para ello es “ADD” y el formato es:

 ADD DESTINO, ORIGEN

Las instrucciones disponibles para realizar sumas se reducen a:

   ADD A, s
   ADD HL, ss
   ADD ri, rr

Donde:

 s:  Cualquier registro de 8 bits (A, B, C, D, E, H, L), 
     cualquier valor inmediato de 8 bits (en el rango 0-255 o -128+127 
     en complemento a dos), cualquier dirección de memoria apuntada por 
     HL, y cualquier dirección de memoria apuntada por un registro 
     índice con desplazamiento de 8 bits.
 ss: Cualquier registro de 16 bits de entre los siguientes: BC, DE, HL, SP.
 ri: Uno de los 2 registros índices (IX o IY).
 rr: Cualquier registro de 16 bits de entre los siguientes excepto el mismo 
     registro índice origen: BC, DE, HL, IX, IY, SP.

Esto daría la posibilidad de ejecutar cualquiera de las siguientes instrucciones:

 ; ADD A, s
 ADD A, B        ; A = A + B
 ADD A, 100      ; A = A + 100
 ADD A, [HL]     ; A = A + [HL]
 ADD A, [IX+10]  ; A = A + [IX+10]
 
 ; ADD HL, ss
 ADD HL, BC      ; HL = HL + BC
 ADD HL, SP      ; HL = HL + SP
 
 ; ADD ri, rr
 ADD IX, BC      ; IX = IX + BC
 ADD IY, DE      ; IY = IY + DE
 ADD IY, IX      ; IY = IY + IX
 ADD IX, IY      ; IX = IX + IY

Por contra, estas instrucciones no serían válidas:

 ADD B, C      ; Sólo A puede ser destino
 ADD BC, DE    ; Sólo puede ser destino HL
 ADD IX, IX    ; No podemos sumar un registro índice a él mismo

La afectación de los flags ante las operaciones de sumas es la siguiente:

  • Para “ADD A, s”, el registro N (Substraction) se pone a 0 (lógicamente, ya que sólo se pone a uno cuando se ha realizado una resta). El registro P/V se comporta como un registro de Overflow e indica si ha habido overflow (desbordamiento) en la operación. El resto de flags (Sign, Zero, Half-Carry y Carry) se verán afectados de acuerdo al resultado de la operación de suma.
  • Para “ADD HL, ss” y “ADD ri, rr”, se pone a 0 el flag N, y sólo se verá afectado el flag de acarreo (C) de acuerdo al resultado de la operación.

O, en forma de tabla de afectación:

                         Flags
   Instrucción       |S Z H P N C|
 ----------------------------------
 ADD A, s            |* * * V 0 *|
 ADD HL, ss          |- - ? - 0 *|
 ADD ri, rr          |- - ? - 0 *|

Las sumas realizadas por el Spectrum se hacen a nivel de bits, empezando por el bit de más a la derecha y yendo hacia la izquierda, según las siguientes reglas:

 0 + 0 = 0
 0 + 1 = 1
 1 + 0 = 1
 1 + 1 = 10 (=0 con acarreo)

Al sumar el último bit, se actualizará el flag de acarreo si es necesario.

Por ejemplo:

       *
   00000100
 + 00000101
  -----------
   00001001

  (* = acarreo de la suma del bit anterior, 1+1=10)

Si la suma del último bit (bit 7) requiere un bit extra, se utilizará el Carry Flag del registro F para almacenarlo. Supongamos que ejecutamos el siguiente código:

 LD A, %10000000
 LD B, %10000000
 ADD A, B

El resultado de la ejecución de esta suma sería: A=128+128=256. Como 256 (100000000b) tiene 9 bits, no podemos representar el resultado con los 8 bits del registro A, de modo que el resultado de la suma sería realmente: A = 00000000 y CarryFlag = 1.

En el caso de las restas, sólo es posible realizar (de nuevo gracias a la no ortogonalidad del J.I.) la operación “A=A-origen”, donde “origen” puede ser cualquier registro de 8 bits, valor inmediato de 8 bits, contenido de la memoria apuntada por [HL], o contenido de la memoria apuntada por un registro índice más un desplazamiento. El formato de la instrucción no requiere 2 operandos, ya que el registro destino sólo puede ser A:

   SUB ORIGEN

Concretamente:

   SUB r        ; A = A - r
   SUB N        ; A = A - N
   SUB [HL]     ; A = A - [HL]
   SUB [rr+d]   ; A = A - [rr+d]

Por ejemplo:

 SUB B        ; A = A - B
 SUB 100      ; A = A - 100
 SUB [HL]     ; A = A - [HL]
 SUB [IX+10]  ; A = A - [IX+10]

Es importante recordar que en una operación “SUB X”, la operación realizada es “A=A-X” y no “A=X-A”.

Por otra parte, con respecto a la afectación de flags, tenemos la siguiente:

 Flags:      S Z H P N C
 -----------------------
 Afectación: * * * V 1 *

Es decir, el flag de N (substraction) se pone a 1, para indicar que hemos realizado una resta. El flag de P/V (Parity/Overflow) se convierte en indicar de Overflow y queda afectado por el resultado de la resta. El resto de flags (Sign, Zero, Half-Carry y Carry) quedarán afectados de acuerdo al resultado de la misma (por ejemplo, si el resultado es Cero, se activará el Flag Z).

Sumar con acarreo dos elementos significa realizar la suma de uno con el otro y, posteriormente, sumarle el estado del flag de Carry. Es decir:

 "ADC A, s"    equivale a    "A = A + s + CarryFlag"
 "ADC HL, ss"  equivale a    "HL = HL + ss + CarryFlag"

(“s” y “ss” tienen el mismo significado que en ADD y SUB).

La tabla de afectación de flags sería la siguiente:

                        Flags
  Instrucción       |S Z H P N C|
 ----------------------------------
 ADC A,s            |* * * V 0 *|
 ADC HL,ss          |* * ? V 0 *|

La suma con acarreo se utiliza normalmente para sumar las partes altas de palabras de 16 bytes. Se suma la parte baja con ADD y luego la parte alta con ADC para tener en cuenta el acarreo de la suma de la parte baja.

Al igual que en el caso de la suma con acarreo, podemos realizar restas con acarreo, que no son más que realizar una resta de los 2 operandos, tras lo cual restamos además el valor del bit de Carry Flag:

 "SBC A, s"    equivale a    "A = A - s - CarryFlag"
 "SBC HL, ss"  equivale a    "HL = HL - ss - CarryFlag"

La tabla de afectación de flags (en este caso con N=1, ya que es una resta):

                        Flags
  Instrucción       |S Z H P N C|
 ----------------------------------
 SBC A,s            |* * * V 1 *|
 SBC HL,ss          |* * ? V 1 *|

A lo largo del presente texto hemos hablado de números en complemento a dos. Complemento a dos es una manera de representar números negativos en nuestros registros de 8 bits, utilizando para ello como signo el bit más significativo (bit 7) del byte.

Si dicho bit está a 0, el número es positivo, y si está a 1 es negativo. Así:

 01111111    (+127)
 01111110    (+126)
 01111101    (+125)
 01111100    (+124)
 (...)
 00000100    (+4)
 00000011    (+3)
 00000010    (+2)
 00000001    (+1)
 00000000    (0)
 11111111    (-1)
 11111110    (-2)
 11111101    (-3)
 11111100    (-4)
 (...)
 10000011    (-125)
 10000010    (-126)
 10000001    (-127)
 10000000    (-128)

Podemos averiguar cuál es la versión negativa de cualquier número positivo (y viceversa), invirtiendo el estado de los bits y sumando uno:

 +17  = 00010001

 -17 =  11101110   (Invertimos unos y ceros)
     =        +1   (Sumamos 1)
     =  11101111   (-17 en complemento a dos)

Como veremos en unos minutos, se eligió este sistema para representar los números negativos para que las operaciones matemáticas estándar funcionaran directamente sobre los números positivos y negativos. ¿Por qué no utilizamos directamente la inversión de los bits para representar los números negativos y estamos sumando además 1 para obtenerlos? Sencillo: si no sumáramos uno y simplemente invirtiéramos los bits, tendríamos 2 ceros (00000000 y 11111111) y además las operaciones matemáticas no cuadrarían (por culpa de los dos ceros). La gracia del complemento a dos es que las sumas y restas binarias lógicas (ADD, ADC, SUB y SBC) funcionan:

Sumemos -17 y 32:

  -17 = 11101111
+ +32 = 00100000
 -----------------
      1 00001111

El resultado es 00001111, es decir, 15, ya que 32-17=15. El flag de carry se pone a 1, pero lo podemos ignorar, porque el flag que nos indica realmente el desbordamiento (como veremos a continuación) en operaciones de complemento a dos es el flag de Overflow.

Sumemos ahora +17 y -17:

  +17 = 00010001
+ -17 = 11101111
 ----------------------
      1 00000000

Como podéis ver, al sumar +17 y -17 el resultado es 0. Si representáramos los números negativos simplemente como la inversa de los positivos, esto no se podría hacer:

  +17 = 00010001
+ -17 = 11101110    <--- (solo bits invertidos)
 ----------------------
      1 11111111    <--- Nos da todo unos, el "cero" alternativo.

En complemento a dos, las sumas y restas de números se pueden realizar a nivel lógico mediante las operaciones estándar del Z80. En realidad para el Z80 no hay más que simples operaciones de unos y ceros, y somos nosotros los que interpretamos la información de los operandos y del resultado de una forma que nos permite representar números negativos.

En otras palabras: cuando vemos un uno en el bit más significativo de un resultado, somos nosotros los que tenemos que interpretar si ese bit representa un signo negativo o no: si sabemos que estamos operando con números 0-255, podemos tratarlo como un resultado positivo. Si estábamos operando con números en complemento a dos, podemos tratarlo como un resultado en complemento a dos. Para el microprocesador, en cambio, no hay más que unos y ceros.

Para acabar, veamos cuál es la diferencia entre el Flag de Carry (C) y el de Overflow (V) a la hora de realizar sumas y restas. El primero (C) se activará cuando se produzca un desbordamiento físico a la hora de sumar o restar 2 números binarios (cuando necesitemos un bit extra para representarlo). El segundo (V), se utilizará cuando se produzca cualquier sobrepasamiento operando con 2 números en complemento a dos.

Como acabamos de ver, en complemento a dos el último bit (el bit 7) nos indica el signo, y cuando operamos con 2 números binarios que nosotros interpretamos como números en complemento a dos no nos basta con el bit de Carry. Es el bit de Overflow el que nos dará información sobre el desbordamiento a un nivel lógico.

En pocas palabras, el bit de Carry se activará si pasamos de 255 a 0 o de 0 a 255 (comportándose como un bit de valor 2 elevado a 8, o 256), y el bit de overflow lo hará si el resultado de una operación en complemento a dos requiere más de 7 bits para ser representado.

Mediante ejemplos:

255+1:

   11111111
+  00000001
  -----------
 1 00000000

   C=1 (porque hace falta un bit extra)
   V=0

127+1:

   01111111
+  00000001
  -----------
   10000000

   C=0 (no es necesario un bit extra en el registro)
   V=1 (en complemento a dos, no podemos representar +128)

En el ejemplo anterior, V se activa porque no ha habido desbordamiento físico (no es necesario un bit extra para representar la operación), pero sí lógico: no podemos representar +128 con 7 bits+signo en complemento a dos.

Como ya se ha explicado, disponemos de un banco de registros alternativos (los Shadow Registers), y podemos conmutar los valores entre los registros estándar y los alternativos mediante unas determinadas instrucciones del Z80.

El Z80 nos proporciona una serie de registros de propósito general (así como un registro de flags), de nombres A, B, C, D, E, F, H y L. El micro dispone también de unos registros extra (set alternativo conocido como Shadow Registers) de nombre A', B', C', D', E', F', H' y L', que aprovecharemos en cualquier momento de nuestro programa. No obstante, no podremos hacer uso directo de estos registros en instrucciones en ensamblador. No es posible, por ejemplo, ninguna de las siguientes instrucciones:

 LD B', $10
 INC A'
 LD HL', $1234
 LD A', ($1234)

La manera de utilizar estos registros alternativos es conmutar sus valores con los registros estándar mediante la instrucción “EXX”, cuyo resultado es el intercambio de B por B', C por C', D por D', E por E', H por H' y L por L'. Supongamos que tenemos los siguientes valores en los registros:

Registro Valor Registro Valor
B A0h B' 00h
C 55h C' 00h
D 01h D' 00h
E FFh E' 00h
H 00h H' 00h
L 31h L' 00h

En el momento en que realicemos un EXX, los registros cambiarán de valor por la “conmutación” de bancos:

Registro Valor Registro Valor
B 00h B' A0h
C 00h C' 55h
D 00h D' 01h
E 00h E' FFh
H 00h H' 00h
L 00h L' 31h

Si realizamos de nuevo EXX, volveremos a dejar los valores de los registros en sus “posiciones” originales. EXX (mnemónico ensamblador derivado de EXchange), simplemente intercambia los valores entre ambos bancos.

Aparte de la instrucción EXX, disponemos de una instrucción EX AF, AF' , que, como el lector imagina, intercambia los valores de los registros AF y AF'. Así, pasaríamos de:

Registro Valor Registro Valor
A 01h A' 00h
F 10h F' 00h

a:

Registro Valor Registro Valor
A 00h A' 01h
F 00h F' 10h

Realizando de nuevo un EX AF, AF' volveríamos a los valores originales en ambos registros.

De esta forma podemos disponer de un set de registros extra con los que trabajar. Por ejemplo, supongamos que programamos una porción de código donde queremos hacer una serie de cálculos entre registros y después dejar el resultado en una posición de memoria, pero no queremos perder los valores actuales de los registros (ni tampoco hacer uso de la pila, que veremos en su momento). En ese caso, podemos hacer:

; Una rutina a la que saltaremos gracias a la
; etiqueta que definimos aquí:
MiRutina:
 
    ; Cambiamos de banco de registros:
    EXX
    EX AF, AF'
 
    ; Hacemos nuestras operaciones
    LD A, (1234h)
    LD B, A
    LD A, (1235h)
    INC A
    ADD A, B
    ; (...etc...)
    ; (...aquí más operaciones...)
 
    ; Grabamos el resultado en memoria
    LD (1236h), A
 
    ; Recuperamos los valores de los registros
    EX AF, AF'
    EXX
 
    ; Volvemos al lugar de llamada de la rutina
    RET

Aparte de EXX y EX AF, AF' tenemos disponibles 3 instrucciones de intercambio más que no trabajan con los registros alternativos, sino entre la memoria y registros, y la pila (o memoria en general) y los registros HL, IX e IY.

Instrucción Resultado
EX DE, HL Intercambiar los valores de DE y HL.
EX (SP), HL Intercambiar el valor de HL con el valor de 16 bits de la posición de memoria apuntada por el registro SP (por ejemplo, para intercambiar el valor de HL con el del último registro que hayamos introducido en la pila).
EX (SP), IX Igual que el anterior, pero con IX.
EX (SP), IY Igual que el anterior, pero con IY.

La primera de estas instrucciones nos puede ser muy útil en nuestros programas en ensamblador, ya que nos permite intercambiar los valores de los registros DE y HL. Las 3 instrucciones restantes permiten intercambiar el valor apuntado por SP (en memoria) por el valor de los registros HL, IX o IY.

Como ya hemos comentado cuando hablamos del carácter Low-Endian de nuestra CPU, al escribir en memoria (también en la pila) primero se escribe el Byte Bajo y luego el Byte Alto. Posteriormente lo leeremos de la misma forma, de tal modo que si los bytes apuntados en la pila (en memoria) son “FF 00h”, al hacer el EX (SP), HL, el registro HL valdrá “00FFh”.

Nótese que aprovechando la pila (como veremos en su momento) también podemos intercambiar los valores de los registros mediante:

 PUSH BC
 PUSH DE
 POP BC
 POP DE

Si queréis comprobarlo, podéis hacerlo mediante el siguiente programa:

 ; Ejemplo que muestra el intercambio de registros
 ; mediante el uso de la pila (PUSH/POP).
 ORG 40000
 
 ; Cargamos en DE el valor 12345 y
 ; realizamos un intercambio de valores
 ; con BC, mediante la pila:
 LD DE, 12345
 LD BC, 0
 
 PUSH DE
 PUSH BC
 POP DE
 POP BC
 
 ; Volvemos, ahora BC=DE y DE=BC
 RET

Lo ensamblamos:

 pasmo --tapbas cambio.asm cambio.tap

Tras esto lo cargamos en un emulador de Spectrum (como un fichero TAP), nos vamos al BASIC y tecleamos “PRINT AT 10, 10; USR 40000”. En pantalla aparecerá el valor “12345”, ya que las rutinas llamadas desde BASIC devuelven sus resultados en BC, y nosotros hemos hecho un intercambio mediante la pila, entre DE y BC.

En su momento veremos cómo funciona la pila, por ahora basta con saber que tenemos la posibilidad de intercambiar registros mediante el uso de la misma. Podríamos haber optado por no explicar este pequeño truco hasta haber hablado de la pila, pero nos parece más conveniente el hecho de tener toda la información sobre ensamblador agrupada de forma que cuando en el futuro (una vez terminado el curso completo) busquéis información sobre instrucciones para intercambiar valores de registros, podáis encontrarla toda junta, como un libro o una guía de referencia. Como hemos comentado al principio de esta entrega, resulta muy complicado explicar un lenguaje tan interrelacionado de forma que no se solapen diferentes áreas, de modo que la comprensión total de muchos de los conceptos se alcanzará con una segunda lectura del curso completo.

En esta entrega hemos visto la sintaxis de los programas en ensamblador (o, al menos, la sintaxis general de PASMO, el ensamblador que recomendamos), así como una descripción completa del juego de registros del Z80, incluyendo entre ellos el registro de flags F.

Además, hemos comenzado a ver nuestras primeras instrucciones del lenguaje ensamblador, en especial las instrucciones de carga, incremento y decremento, y aritméticas.

En la próxima entrega continuaremos detallando las diferentes instrucciones del Z80, ejemplos de uso y su efecto sobre los flags del registro F. Hasta entonces, os recomiendo la lectura del fichero “progcard.txt” adjunto con este artículo, donde encontraréis una gran referencia de instrucciones y flags.

  • cursos/ensamblador/lenguaje_1.1186413109.txt.gz
  • Última modificación: 06-08-2007 15:11
  • por sromero