cursos:ensamblador:esqueleto_programa

Esqueleto de programa y librería de funciones básicas


Algo muy importante para aprender ensamblador de Z80 es experimentar. No sólo es recomendable probar los ejemplos y alterarlos para ver qué resultados producen nuestros cambios, sino que se hace imprescindible comprobar cómo funcionan las diferentes instrucciones.

Para ello, necesitamos saber lo siguiente:

  1. Tener el esqueleto de un programa mínimo, que no haga nada, al cual podamos añadirle instrucciones para hacer nuestras pruebas. Podremos utilizar un esqueleto de programa similar para cada prueba que queramos realizar.
  2. Saber cómo ensamblar ese programa para obtener un fichero TAP que podamos probar en un emulador.
  3. Tener alguna forma de imprimir por pantalla para tener un feedback visual en las pruebas que hagamos.

Vamos a empezar por el “esqueleto” de programa mínimo. Abrimos un editor de texto, pegamos el siguiente código y lo guardamos como prueba.asm.

    ORG 35000
 
    call $0daf     ; CLS (borrar pantalla)
 
    ; código que queremos probar aqui
    ret
 
    END 35000

A continuación, podemos ensamblar este programa con pasmo --tapbas prueba.asm prueba.tap. Obtendremos un fichero TAP el cual podremos cargar en un emulador.

Nótese que las directivas ORG y RET están indentadas (4 espacios hacia la derecha). Esto no es necesario para pasmo (podríamos situarla al principio de la línea) pero otros assemblers como sjasmplus así lo requieren. Es recomendable intentar escribir el código lo más “standard” y compatible posible, así que se recomienda indentar estas directivas.

Al ejecutarlo, como lo único que tenemos es un RET, se devolverá el control al BASIC y todo lo que podremos ver en pantalla es un “0 OK, 40:1” (o similar) en la parte inferior.



Ya está, tenemos así todo lo necesario para hacer cualquier prueba en ensamblador que queramos, introduciendo el código que deseemos entre el ORG y el RET. El RET devolverá el control al BASIC una vez haya terminado la ejecución de nuestro programa.

Por otra parte, en ocasiones, en nuestras pruebas, nos vendría bien tener una forma de tener información visual sobre la ejecución del código.

Por ejemplo, supongamos que estamos en el capítulo sobre los flags y el comando CP y queremos hacer un programa de prueba para comparar si A y B contienen el mismo valor (si A = B). Podríamos hacer esto:

    ORG 35000
 
    ; código que queremos probar, como
    ; por ejemplo, pruebas con CP y jr:
 
    ld a, 10
    ld b, 20
    cp b
    jr nz, es_diferente
 
es_igual:
    ; A == B
    jr fin
 
es_diferente:
   ; A != B
 
fin:
    ret
 
    END 35000


Utilizando un emulador que tenga un “debugger” o “depurador” de Z80 podríamos ejecutar el código paso a paso para ver si se produce el salto o no, pero es posible que en estos momentos iniciales de aprendizaje nos resulte complicado hacer esto.

Lo que podemos hacer, de una manera muy sencilla, es utilizar referencias visuales, como por ejemplo llamar a las rutinas de la ROM que permiten imprimir en pantalla, como rst 16 (o rst $10).

Veámos cómo funciona rst 16 ensamblando y ejecutando el siguiente programa:

    ORG 35000
 
    call $0daf     ; CLS (borrar pantalla)
 
    ld a, '*'
    rst 16         ; Imprimir A en la posicion del cursor
 
    ret
 
    END 35000

En el listado hay 3 líneas nuevas añadidas a nuestro “esqueleto vacío de programa”.

La primera (call $0daf), realiza un borrado de pantalla, resetea el cursor a (0,0) y también abre el “canal 2” (la pantalla) para poder imprimir texto.

Las otras 2 (ld a, '*' y rst 16) se utilizan para llamar a una rutina de la ROM que imprime en la posición actual del cursor el carácter que esté en el registro A.

El resultado de ejecutar el programa de arriba sería:



Podemos imprimir no sólo este carácter, sino todos cuantos deseemos, con sucesivas llamadas a rst 16:

    ORG 35000
 
    call $0daf     ; CLS (borrar pantalla)
 
    ld a, '*'
    rst 16
    ld a, 'A'
    rst 16
    ld a, 'S'
    rst 16
    ld a, 'M'
    rst 16
    ld a, '*'
    rst 16
 
    ret
 
    END 35000



Algo muy importante al respecto de rst 16 es que por defecto, imprime en el “CANAL 1”, que son las 2 líneas de la parte inferior de la pantalla del Spectrum (donde aparecen los mensajes). Si queremos imprimir en la parte superior de la pantalla, tenemos que “abrir” el “CANAL 2”. Esto ya lo hace la rutina CLS de la ROM que estamos usando.

Si queremos imprimir texto, pero no borrar la pantalla, al quitar el call $0daf necesitaremos poner en su lugar la siguiente llamada para abrir el CANAL 2:

    ORG 35000
 
    ld a, 2
    call 5633     ; Abrir CANAL 2 para RST

En cualquier caso, lo habitual es borrar la pantalla con CLS por lo que se abrirá el canal 2 automáticamente al hacerlo y no necesitaremos hacerlo con la llamada anterior.

Por otra parte, con rst 16 podremos también saltar el cursor a la siguiente línea con el carácter 13:

    ld a, 13
    rst 16



Aprovechando rst 16 en nuestros programas para depurar:

Con esta nueva funcionalidad ya podemos poner algo de feedback visual en nuestras pruebas. Volvamos a nuestro ejemplo de comparación, y modifiquémoslo para que imprima “=” si A==B o que imprima “!” si es A!=B (o podríamos haber usado “1” y “2”, o cualquier otro valor arbitrario).

    ORG 35000
 
    call $0daf     ; CLS (borrar pantalla)
 
    ld a, 10
    ld b, 20
    cp b
    jr nz, es_diferente
 
es_igual:
    ; A = B
    ld a, '='
    rst 16
    jr fin
 
es_diferente:
    ; A != B
    ld a, '!'
    rst 16
 
fin:
    ret
 
    END 35000

En este caso, en pantalla aparecerá “!” porque, efectivamente, 10 es distinto de 20 y hemos realizado el salto a la etiqueta “es_diferente”.


Si por cuestiones de legibilidad no te gusta ver en el código “Números Mágicos” (como call $0daf), ya que son difíciles de recordar, y hacen el listado ilegible, puedes utilizar directivas EQU para definir constantes más sencillas de leer.

Este código es menos legible:

    ORG 35000
 
    call $0daf     ; Rutina CLS de la ROM
 
    ld a, "*"
    rst 16
 
    ret
 
    END 35000

Que su misma versión utilizando constantes autodescriptivas:

    ORG 35000
 
    call ROM_CLS
 
    ld a, "*"
    rst 16
 
    ret
 
ROM_CLS EQU $0daf
 
    END 35000

Recomendamos encarecidamente definir EQU's para variables del sistema, rutinas de la ROM, etc.

De esa forma, cuando revisites tu código más adelante sabrás exáctamente qué hace ese call sin la necesidad de haber dejado un comentario.


Otra opción muy sencilla para tener feedback visual podría ser, en lugar de imprimir caracteres por pantalla, cambiar el color del borde utilizando por ejemplo la rutina de la ROM.

Se puede cambiar el borde cargando en el registro A un valor del 0 al 7 y llamando a la dirección $229b:

    ld a, N         ; A = color
    call $229b      ; Llamar a la rutina de la ROM

El valor de N puede ser:

Valor Color
0 Negro
1 Azul
2 Rojo
3 Magenta
4 Verde
5 Cyan
6 Amarillo
7 Blanco

Así pues, en lugar de imprimir '=' o '!' en nuestro ejemplo anterior, podríamos simplemente poner el color del borde en AZUL para igual y ROJO para diferente:




En otras ocasiones nos interesará saber qué valor tiene un registro o una posición de memoria, así que más que imprimir un carácter, nos puede interesar saber el valor numérico en sí. Para esto podemos aprovechar dos rutinas de la ROM que nos permitirán imprimir valores desde 0 hasta 65535.

Estas rutinas reciben el valor a imprimir en el registro BC y se usan de la siguiente forma:

    ld bc, valor_a_imprimir   ; (0-65535)
    call $2d2b
    call $2de3

La primera rutina ($2d2b) sirve para meter el valor de BC en la “pila de la calculadora” de BASIC, y la segunda rutina ($2de3) sirve para imprimir en pantalla el último número introducido en dicha pila.

Así pues, podemos poner cualquier valor en BC, hacer esas 2 llamadas a la ROM, y lo veremos aparecer en pantalla en la posición del cursor:

    ORG 40000
 
    call ROM_CLS
 
    ld bc, 1234        ; Queremos imprimir un valor directo
    call ROM_STACK_BC
    call ROM_PRINT_FP
 
    ld a, 13
    rst 16             ; Retorno de carro
 
    ld hl, 5678        ; Queremos imprimir el valor de HL
                       ; No existe "ld bc, hl", asi que hacemos
    ld b, h            ; B = H y C = L
    ld c, l            ; por lo que => BC = HL
    call ROM_STACK_BC
    call ROM_PRINT_FP
 
    ld a, 13
    rst 16             ; Retorno de carro
 
    ld bc, (variable)  ; Imprimir valor de variable (memoria)
    call ROM_STACK_BC
    call ROM_PRINT_FP
 
    ld a, 13
    rst 16             ; Retorno de carro
 
    ld bc, (RAMTOP)    ; Imprimir valor de RAMTOP (variable sistema)
    call ROM_STACK_BC
    call ROM_PRINT_FP
 
    ld a, 13
    rst 16             ; Retorno de carro
 
    ld a, 255
    ld b, 0
    ld c, a            ; Imprimir el valor de A (B=0)
    call ROM_STACK_BC
    call ROM_PRINT_FP
 
    ret
 
variable      DEFW 65535
 
RAMTOP        EQU  $5cb2
ROM_CLS       EQU  $0daf
ROM_STACK_BC  EQU  $2d2b
ROM_PRINT_FP  EQU  $2de3
 
    END 40000

El resultado de la ejecución del programa anterior sería:



Nótese cómo gracias a estas 2 rutinas podemos ver tanto el valor de cualquier registro de 16 bits (copiando su valor a BC), como de cualquier dirección de memoria (ya sea una variable de nuestro programa o el contenido de cualquier otra celdilla, como RAMTOP), o de un registro de 8 bits (haciendo 0 la parte alta de BC).

En el ejemplo podemos ver que RAMTOP vale 39999, justo el valor que hace el CLEAR cuando lanzamos nuestro programa con ORG 40000.

Ahora tenemos la capacidad de realizar cuantas pruebas queramos mediante nuestro “esqueleto de programa” (con ORG, RET y END) y con la posibilidad de sacar algo de feedback visual por pantalla para nuestras pruebas.

Otra cosa interesante que puede verse en el listado anterior es que en el juego de instrucciones del procesador Z80 no existe una instrucción ld bc, hl. Necesitamos poner en BC el valor a imprimir por pantalla, así que podemos hacerlo copiando la parte alta de HL en la parte alta de BC (ld b, h) y después la parte baja de HL en la parte baja de BC (ld c, l), que resulta en la copia exacta del contenido de HL en BC. Como veremos, en el microprocesador Z80 no podemos utilizar todas las instrucciones con todos los operandos, aunque hay pequeños trucos para saltarse estas limitaciones, como este.


Acabamos de ver cómo imprimir un número decimal (con valores desde 0 a 65535), y es evidente la enorme utilidad que tiene esto para el proceso de aprendizaje del lenguaje ensamblador. Esta rutina de impresión de valores numéricos nos proporciona la posibilidad de ver el contenido de un registro o de una dirección de memoria en nuestro programa sin necesidad de un debugger.

Veamos cómo podríamos crear una pequeña “librería” de funciones.

Una “librería de funciones” (lo cual es una mala traducción del vocablo inglés “Library”, la traducción correcta “biblioteca de funciones”) es un conjunto de funciones útiles que creamos en un fichero separado (por ejemplo, “utils.asm”) que después podemos incluir en nuestros programas con la directiva INCLUDE (ejemplo: INCLUDE “utils.asm”) y así podemos utilizar sus constantes EQU, las variables que tiene definidas con DEFB/DEFW, y cualquier rutina o etiqueta que contenga.

Las librerías permiten reutilizar código entre proyectos, y hacerlos más legibles, además de que cualquier mejora en el código de una librería permite que esa mejora llegue a todos los programa que estamos realizando al actualizar la librería en ellos.

Creemos un fichero utils.asm con el siguiente contenido:

;=== Libreria utils.asm: Funciones utiles varias para pruebas de ===
;=== lenguaje ensamblador Z80 en Spectrum ===
 
;-----------------------------------------------------------------------
; PrintNum: Imprime en la pantalla un valor en decimal usando la ROM.
;
; ENTRADA:  BC = valor a imprimir por pantalla
; SALIDA, MODIFICA: NADA
;-----------------------------------------------------------------------
PrintNum:
    push af
    push bc
    push de
    push hl
    call $2d2b                    ; ROM_STACK_BC
    call $2de3                    ; ROM_PRINT_FP
    pop hl
    pop de
    pop bc
    pop af
    ret
 
;-----------------------------------------------------------------------
; CLS: Borrado de la pantalla con el color indicado en (CLS_COLOR)
; CLSCOLOR: Direccion de memoria de ATTR_P (atributo para CLS)
; BORDER: Cambio del color del borde
; PAUSE: Realiza una pausa de BC/50 segundos
; MULT_HLDE: Calcula HL=HL*DE
; PIXEL_ADDR2: Devuelve en HL/A direccion de pixel BC (YX)
; PrintNum4digits: Imprime el numero en BC (0-9999).
;-----------------------------------------------------------------------
CLS             EQU  $0daf        ; ROM_CLS
CLS_COLOR       EQU  $5c8d        ; Variable del Sistema ATTR_P
BORDER          EQU  $229b        ; Rutina del borde
PAUSE           EQU  $1f3d        ; PAUSAR BC/50 segundos (50 => 1s)
MULT_HLDE       EQU  $30a9        ; Multiplica HL*DE => HL=HL*DE
PIXEL_ADDR2     EQU  $22b1        ; Devuelve direccion de pixel BC
PrintNum4digits EQU  $1a1b        ; Imprime valor de BC (0-9999)
 
;-----------------------------------------------------------------------
; PrintChar: Hacer desde nuestro programa "call PrintChar", es el
;            equivalente a hacer un "call $0010", es decir, rst 16.
;            Es un simple envoltorio de abreviatura para preservar AF.
;
; ENTRADA:   A = caracter a imprimir por pantalla
;-----------------------------------------------------------------------
PrintChar:
    push af
    rst 16
    pop af
    ret
 
;-----------------------------------------------------------------------
; PrintSpace y PrintCR: para abreviar codigo, imprimen SPACE o ENTER.
;
; ENTRADA, SALIDA, MODIFICA: NADA
;-----------------------------------------------------------------------
PrintSpace:
    push af
    ld a, ' '
    rst 16
    pop af
    ret
 
PrintCR:
    push af
    ld a, 13
    rst 16
    pop af
    ret

Ahora ya podemos utilizar esta librería en nuestros programas, haciéndolos mucho más legibles, así:

    ORG 33500
 
    call CLS
 
    ld bc, 1234            ; Queremos imprimir un valor directo
    call PrintNum
 
    ld a, 'd'
    call PrintChar         ; Imprimir sufijo 'd' despues del numero
 
    call PrintCR           ; Salto de linea
 
    ld bc, (variable)      ; Imprimir valor de variable (memoria)
    call PrintNum
 
    ret
 
variable      DEFW 65535
 
    ;; Incluimos nuestra libreria aqui
    INCLUDE "utils.asm"
 
    END 33500

Es evidente que el programa se ha simplificado mucho, a costa de añadir saltos y retornos a rutinas externas, algo que no es un problema cuando estamos realizando pruebas y programas de aprendizaje. No tener gran cantidad de código en pantalla para tareas que son repetitivas (como imprimir cadenas) nos facilitará ver el código que realmente nos interese, y evitará errores al programar ya que las llamadas a la funciones de la librería simplifican todo el proceso de desarrollo.

Con esta pequeña librería tal y como la acabamos de crear, disponemos de las siguientes funciones:


Rutina Utilidad
BORDER Permite cambiar el color del borde al valor indicado en A.
CLS Limpia la pantalla. Para ello utiliza el valor del atributo que haya alojado en la posición de memoria CLS_COLOR.
CLS_COLOR EQU (constante) que apunta a la dirección de la variable del sistema que permite establecer el color deseado al borrar la pantalla, en formato (COLOR_PAPEL*8)+COLOR_TINTA, llamada ATTR-P.
PrintChar Imprime en pantalla, en la posicion actual del cursor, el caracter contenido en A. Es el equivalente a rst 16 pero salva el valor de AF para que no se modifique en la rutina de la ROM.
PrintCR Realiza un salto de linea (imprime el caracter 13 o _CR con rst 16).
PrintSpace Imprime en pantalla, en la posicion actual del cursor, un espacio (con rst 16).
PrintNum Imprime en pantalla, en la posicion actual del cursor, el valor decimal del contenido del registro BC, es decir, valores entre 0 y 65535. Utiliza para ello la pila de la calculadora BASIC, lo cual no es especialmente óptimo.
PrintNum4digits Imprime en pantalla, en la posicion actual del cursor, el valor decimal del contenido del registro BC, siempre que este valga entre 0 y 9999. Es la rutina que usa el sistema para imprimir el número de línea de BASIC (OUT_NUM_1), no usa la pila de la calculadora y es más rápida que la anterior, pero limitada a 4 digitos. Según nuestras necesidades, podemos usar PrintNum o PrintNum4Digits, siendo esta última una mejor opción para cualquier cosa cuyo valor esté controlado.
PAUSE Pausar la ejecución del programa durante N segundos. El valor de N lo metemos en el registro BC multiplicado por 50, de forma que con BC = 500, por ejemplo, pausaremos durante 10 segundos.
MULT_HLDE Calcula HL=HL*DE siempre que el resultado de la multiplicación no exceda de 65535. Incluso pese a esta limitación, es perfectamente útil para realizar multiplicaciones en nuestros juegos ya que los valores de coordenadas, velocidades, posiciones, etc no suelen ser valores tan altos.


A continuación vamos a añadir las siguientes funciones adicionales que nos serán enormemente útiles durante el aprendizaje y práctica del ensamblador:


Rutina Utilidad
PrintBin Imprime en pantalla, en la posición del cursor, el valor del registro A en binario, con el prefijo '%'. Muy útil para ver el estado de los bits de registros concretos.
PrintHex Imprime en pantalla el valor del registro A en hexadecimal, con el prefijo '$'. Muy útil para ver el valor de un registro de 8 bits.
PrintHex16 Imprime en pantalla el valor del registro BC en hexadecimal, con el prefijo '$'. Muy útil para ver el valor de un registro o posición de memoria de 16 bits.
PrintString Imprime una cadena definida en memoria con (por ejemplo con DEFB/DB) acabada en $ff como último byte (es el indicador de fin de cadena, para ellos se ha definido también una constante _EOS o -End Of String-). Como veremos en el ejemplo, la cadena soporta utilizar códigos de color, posicionamiento en coordenadas (y,x), flash, etc.
PrintNum2digits Imprime en la pantalla el valor de A, pero sólo los últimos 2 dígitos (0-99). Es ideal para imprimir valores como números de pantalla, vidas o tiempos, y además no usa la pila de la calculadora BASIC por lo que es mucho más rápida (además sólo imprimir 2 dígitos y del registro de 8 bits A). Para realizar la conversión de valor número a 2 ASCIIs a imprimir, utiliza una rutina auxiliar y después llama a rst 16 para imprimirlos.
Byte2ASCII_Dec2Digits Convierte el valor del registro H en una cadena de texto ASCII de max. 2 caracteres (0-99) decimales, y los devuelve en DE.
CursorAt Mueve el cursor a las coordenadas de pantalla X,Y especificadas en DE (D=X, E=Y). Es importante que X esté entre 0 y 31 e Y entre 0 y 21 que son los límites del canal 2 (parte superior de la pantalla).
Wait_For_Key Se queda esperando en un bucle hasta que se detecte una tecla pulsada, y devuelve su código ASCII en “A”. Ideal para poder poner pausas controladas en nuestro código que avancemos con pulsaciones de teclado.
Wait_For_No_Key Se queda esperando en un bucle hasta que no haya ninguna tecla pulsada. A veces se utiliza antes de leer el teclado para asegurarse de que no nos saltamos una pausa porque el usuario tenía alguna tecla pulsada cuando la realizamos.
PrintFlags Imprime en la posición actual del cursor el contenido del registro de Flags en binario. Recordemos que los Flags son: “S Z - H - P/V N C”. Función utilísima para estudiar el comportamiento de las instrucciones y detectar en ocasiones problemas en saltos condicionales y comparaciones.
PrintFlag Imprime en la posición actual del cursor el contenido (0/1) del Flag especificado en el registro A. Se deben utilizar las constantes _FLAG_Z, _FLAG_PV o _FLAG_C entre otras como posible valor de A.
PIXEL_ADDR2 Punto interno de la rutina de la ROM PIXEL_ADDRESS. Esta rutina recibe en BC las coordenadas de un punto (B=Y en 0-192, C=X en 0-255) y devuelve en HL la dirección de memoria de la celdilla de la VideoRam que contiene en pixel, y en A el número de pixel donde está.
_INK, _PAPER, _AT, etc Constantes (también colores como _RED, _BLUE, etc) para utilizar como códigos de control en las cadenas.
_EOS Constante para indicar fin de cadena ($ff).
_CR Constante para indicar fin de línea.

Ampliaremos nuestra librería utils.asm añadiendo el código de estas funciones.

Editamos el fichero y añadimos al final del mismo el código que veremos a continuación. En este momento no vamos a explicar cómo funcionan las diferentes rutinas, ya que todavía no tenemos los conocimientos necesarios para entenderlos y las reproducimos sólo para que podamos incluirlas en nuestro utils.asm y usarlas en nuestros programas:

;-----------------------------------------------------------------------
; CursorAt: Mueve el cursor a la posicion indicada por DE (XY)
; ENTRADA: DE = XY (D=X, E=Y)
; SALIDA, MODIFICA: NADA
;-----------------------------------------------------------------------
CursorAt:
    push af
    ld a, _AT                     ; Codigo control "AT Y,X"
    rst 16
    ld a, e                       ; Y
    rst 16
    ld a, d                       ; X
    rst 16
    pop af
    ret
 
;-----------------------------------------------------------------------
; PrintString: Imprime en la pantalla una cadena acabada en un byte de
; valor $ff usando rst 16.
;
; ENTRADA:  DE = Dirección de la cadena a imprimir.
; SALIDA:   NADA
; MODIFICA: El valor de DE no se preserva (se incrementa)
;-----------------------------------------------------------------------
PrintString:
    push af
print_string_loop:
    ld a, (de)                    ; Leemos el caracter apuntado por DE
    CP _EOS                       ; chequeamos si es $ff (fin de cadena)
    jr z, end_print_string        ; Si lo es, se activa el ZEROFLAG => SALIR
    rst 16                        ; No lo es, no hemos saltado, imprimirlo
    inc de                        ; Avanzar al siguiente caracter de la cadena
    jr print_string_loop          ; Repetir hasta que alguno sea 0 y salgamos
end_print_string:
    pop af
    ret
 
;-----------------------------------------------------------------------
; PrintBin: Imprime en la pantalla un valor en binario usando rst 16.
; Añade el prefijo '%' delante del numero impreso.
;
; ENTRADA:  A = valor a imprimir por pantalla en binario
; SALIDA, MODIFICA: NADA
;-----------------------------------------------------------------------
PrintBin:
    push af
    push bc                       ; Preservamos los registros que se usaran
 
    ld c, a                       ; Guardamos en C copia de A
    ld b, 8                       ; Imprimiremos el estado de los 8 bits
 
    ld a, '%'
    rst 16                        ; Imprimir prefijo
 
printbin_loop:
    ld a, '1'                     ; Para bit = 1, imprimiremos '1'
    bit 7, c                      ; Chequeamos el estado del bit 7
    jr nz, printbin_es_uno        ; Dejamos A = 255
    ld a, '0'                     ; A = '0'
 
printbin_es_uno:
    rst 16                        ; Imprimimos (A): contiene '0' o '1'
    rlc c                         ; Rotamos C a la izq para que podamos
                                  ; usar de nuevo el BIT 7 en el bucle
    djnz printbin_loop            ; Repetimos 8 veces
 
    pop bc
    pop af
    ret
 
;-----------------------------------------------------------------------
; PrintHex: Imprime en la pantalla un numero de 1 byte en hexadecimal.
; Para ello convierte el valor numérico en una cadena llamando a
; Byte2ASCII_Hex y luego llama a rst 16 para imprimir cada caracter por
; separado. Imprime un $ delante y ESPACIO detrás.
;
; ENTRADA: A = valor a imprimir por pantalla en hexadecimal
;
; SALIDA, MODIFICA: NADA
;-----------------------------------------------------------------------
PrintHex:
    push hl
 
    ld h, a                       ; Guardamos A
    ld a, '$'
    rst 16                        ; Imprimimos un "$"
    ld a, h                       ; Recuperamos A
 
    push af
    push de
 
    call Byte2ASCII_Hex           ; Convertimos A en Cadena HEX
    ld hl, Byte2ASCII_output      ; HL apunta a la cadena
 
    ld a, (hl)
    rst 16                        ; Imprimimos primer valor HEX
    inc hl                        ; Avanzar en la cadena
    ld a, (hl)
    rst 16                        ; Imprimimos segundo valor HEX
 
    pop de
    pop af
    pop hl
    ret
 
;-----------------------------------------------------------------------
; PrintHex16: Imprime en la pantalla un numero de 2 bytes en hexadecimal.
;
; ENTRADA: BC = valor a imprimir por pantalla en hexadecimal
; SALIDA, MODIFICA: NADA
;-----------------------------------------------------------------------
PrintHex16:
    push af
    push hl
 
    ld a, '$'
    rst 16                        ; Imprimimos un "$"
 
    ld h, b
    call Byte2ASCII_Hex           ; Convertimos A en Cadena HEX
    ld hl, Byte2ASCII_output      ; HL apunta a la cadena
    ld a, (hl)
    rst 16                        ; Imprimimos primer valor HEX
    inc hl                        ; Avanzar en la cadena
    ld a, (hl)
    rst 16                        ; Imprimimos segundo valor HEX
 
    ld h, c
    call Byte2ASCII_Hex           ; Convertimos A en Cadena HEX
    ld hl, Byte2ASCII_output      ; HL apunta a la cadena
    ld a, (hl)
    rst 16                        ; Imprimimos primer valor HEX
    inc hl                        ; Avanzar en la cadena
    ld a, (hl)
    rst 16                        ; Imprimimos segundo valor HEX
 
    pop hl
    pop af
    ret
 
;-----------------------------------------------------------------------
; Byte2ASCII_Hex: Convierte el valor del registro H en una cadena
; de texto de max. 2 caracteres hexadecimales, para poder imprimirla.
; Rutina adaptada de Num2Hex en http://baze.au.com/misc/z80bits.html .
;
; ENTRADA:   H = Numero a convertir
; SALIDA:    [Byte2ASCII_output] = Espacio de 2 bytes con los ASCIIs
; MODIFICA:  NADA
;-----------------------------------------------------------------------
Byte2ASCII_Hex:
    push af
    push de
    push hl
    ld de, Byte2ASCII_output
    ld a, h
    call B2AHex_Num1
    ld a, h
    call B2AHex_Num2
    jp B2AHex_Exit
 
B2AHex_Num1:
    rra
    rra
    rra
    rra
 
B2AHex_Num2:
    or $f0
    daa
    add a, $a0
    adc a, $40
    ld (de), a
    inc de
    ret
 
B2AHex_Exit:
    pop hl
    pop de
    pop af
    ret
 
Byte2ASCII_output DB 0, 0
;-----------------------------------------------------------------------
 
;-----------------------------------------------------------------------
; PrintNum2digits: Imprime en la pantalla un numero de 1 byte en
; base 10, pero solo los 2 últimos digitos (0-99). Para ello convierte
; el valor numerico en una cadena llamando a Byte2ASCII_2Dig y luego
; llama a rst 16 para imprimir cada caracter por separado.
;
; ENTRADA:  A = valor a "imprimir" en 2 digitos de base 10.
; SALIDA:   NADA
; MODIFICA: NADA
;-----------------------------------------------------------------------
PrintNum2digits:
    push af
    push de
    call Byte2ASCII_Dec2Digits    ; Convertimos A en Cadena Dec 0-99
    ld a, d
    rst 16                        ; Imprimimos primer valor HEX
    ld a, e
    rst 16                        ; Imprimimos segundo valor HEX
 
    pop de
    pop af
    ret
 
;-----------------------------------------------------------------------
; Byte2ASCII_Dec2Digits: Convierte el valor del registro H en una
; cadena de texto de max. 2 caracteres (0-99) decimales.
;
; ENTRADA:  A = Numero a convertir
; SALIDA:   DE = 2 bytes con los ASCIIs
; MODIFICA: A, FLAGS
;
; Basado en rutina dtoa2d de:
; http://99-bottles-of-beer.net/language-assembler-%28z80%29-813.html
;-----------------------------------------------------------------------
Byte2ASCII_Dec2Digits:
    ld d, '0'                     ; Starting from ASCII '0'
    dec d                         ; Because we are inc'ing in the loop
    ld e, 10                      ; Want base 10 please
    and a                         ; Clear carry flag
 
dtoa2dloop:
    inc d                         ; Increase the number of tens
    sub e                         ; Take away one unit of ten from A
    jr nc, dtoa2dloop             ; If A still hasn't gone negative, do another
    add a, e                      ; Decreased it too much, put it back
    add a, '0'                    ; Convert to ASCII
    ld e, a                       ; Stick remainder in E
    ret
 
;-----------------------------------------------------------------------
; PrintFlags: Imprime en la pantalla en binario el registro F (flags).
;
; ENTRADA:  F = registro de Flags
; SALIDA, MODIFICA: NADA
;-----------------------------------------------------------------------
PrintFlags:
    push af
    push bc
 
    push af                       ; Metemos AF en al pila pero sacamos BC
    pop bc                        ; Ahora C contiene el valor de F
    ld a, c
    call PrintBin
    pop bc
    pop af
    ret
 
;-----------------------------------------------------------------------
; PrintFlag: Imprime en la pantalla en binario el flag indicado.
;
; ENTRADA:  F = registro de Flags
;           A = FLAG a imprimir, con constantes como "FLAG_Z".
; SALIDA, MODIFICA: NADA
;-----------------------------------------------------------------------
PrintFlag:
    push af
    push bc
 
    push af                       ; Metemos AF en al pila pero sacamos BC
    pop bc                        ; Ahora C contiene el valor de F
                                  ; y B contiene el antiguo valor A
    ld a, c
loopPrintFlag:
    rlc a                         ; rotamos A veces el bit
    djnz loopPrintFlag
 
    and %00000001                 ; Borramos todos los bits menos bit 0
    add a, '0'                    ; Sumamos '0' para obtener ASCII
 
    rst 16
    pop bc
    pop af
    ret
 
;-----------------------------------------------------------------------
; Wait_For_Key: Pausa la ejecución hasta que pulse alguna tecla.
; Devuelve el ASCII de la tecla pulsada en A.
;
; ENTRADA: NADA
; SALIDA: ASCII de la tecla pulsada sacado de LAST_K
;-----------------------------------------------------------------------
Wait_For_Key:
    call Wait_For_No_Key
    push af
wait_for_key_loop:
    xor a                         ; A = 0
    in a, ($FE)
    or %11100000
    inc a
    jr z, wait_for_key_loop
    pop af
    ld a, ($5c08)                 ; Devolver Variable LAST-K en A
    ret
 
;-----------------------------------------------------------------------
; Wait_For_No_Key: Espera a que no haya ninguna tecla pulsada (para
; poder poner como condicion que el usuario suelte la tecla).
;
; ENTRADA, SALIDA, MODIFICA: NADA
;-----------------------------------------------------------------------
Wait_For_No_Key:
    push af
wait_for_no_key_loop:
    xor a
    in a, ($fe)
    or %11100000
    inc a
    jr nz, wait_for_no_key_loop
    pop af
    ret
 
;-----------------------------------------------------------------------
; Constantes definidas para usar con cadenas o RST
;-----------------------------------------------------------------------
_EOS        EQU $ff
_CR         EQU 13
_INK        EQU 16
_PAPER      EQU 17
_FLASH      EQU 18
_BRIGHT     EQU 19
_INVERSE    EQU 20
_OVER       EQU 21
_AT         EQU 22
_TAB        EQU 23
 
_BLACK      EQU 0
_BLUE       EQU 1
_RED        EQU 2
_MAGENTA    EQU 3
_GREEN      EQU 4
_CYAN       EQU 5
_YELLOW     EQU 6
_WHITE      EQU 7
 
_FLAG_S     EQU 1
_FLAG_Z     EQU 2
_FLAG_H     EQU 4
_FLAG_PV    EQU 6
_FLAG_N     EQU 7
_FLAG_C     EQU 8

Viendo el código de la librería, podemos ver varias cosas interesantes:

  • Al inicio de cada rutina, se debe documentar qué hace, qué parámetros de entrada y salida tiene y si modifica algún registro en su desarrollo. Esta información nos será muy útil después e incluso lo habitual es tenerla además fuera en una “documentación de la librería” para poder consultarla rápidamente.
  • Las funciones preservan los valores de los registros que utilizan internamente usando PUSH al principio de las mismas y POP al final. Si estuvieramos haciendo una librería para desarrollar juegos, en ocasiones nos puede interesar no preservar todos los registros en algunas de las rutinas que más críticas sean en velocidad si las funcione que las llaman tienen esto en cuenta.

A continuación vamos a ver un ejemplo de uso de algunas funciones de la librería (usaremos estas y otras de las funciones que incluye a lo largo del resto de capítulos del curso):

; Prueba de la libreria "utils.asm"
    ORG 33500
 
    ld a, _BLUE        ; Color para el borde
    call BORDER
 
    ld a, _BLUE*8+_WHITE
    ld (CLS_COLOR), a   ; Color para CLS (por defecto es $38)
 
    call CLS            ; CLS
 
    ld bc, 50*3
    call PAUSE          ; Pausar 3 segundos antes de continuar
 
    ld bc, 65535
    call PrintNum       ; Prueba decimal
 
    call PrintSpace     ; Imprimir espacio
 
    ld a, $f0
    call PrintHex       ; Prueba hexadecimal 8
 
    call PrintSpace     ; Imprimir espacio
 
    ld bc, $f01a
    call PrintHex16     ; Prueba hexadecimal 16
 
    call PrintSpace     ; Imprimir espacio
 
    ld a, 193           ; o "ld a, %11000001"
    call PrintBin       ; Prueba binario
 
    call PrintCR
    call PrintCR        ; 2 saltos de linea
 
    ld de, cadena1
    call PrintString    ; Cadena ("FLAGS y FLAG Z")
 
    call PrintFlags     ; Imprimir valor de Flags
    call PrintSpace     ; Imprimir un espacio
    ld a, _FLAG_Z
    call PrintFlag      ; Imprimir valor 0/1 FLAG_Z
 
    call PrintCR
    call PrintCR
 
    ld de, cadena2
    call PrintString    ; Cadena con saltos de linea
 
    ld de, cadena3
    call PrintString    ; Cadena con saltos de linea
 
    ld de, cadena4
    call PrintString    ; Cadena con codigos de control
 
    ld d, 25
    ld e, 21            ; X = 31, y = 21
    call CursorAt       ; mover Cursor
 
    ld a, '*'
    call PrintChar      ; Imprimir '*'
 
    ; Esperar pulsacion de tecla antes de salir e imprimirla
    call Wait_For_Key
    call PrintChar
 
    ret
 
cadena1 DEFB 'FLAGS (F) y ZF: ', $ff
 
cadena2 DEFB 'Esto es una cadena con salto', _CR, _EOS
 
cadena3 DEFB _CR, 'Acepta saltos', _CR, 'de linea', _CR, _CR
        DEFB 'usando _CR en la cadena', _EOS
 
cadena4 DEFB _CR, _CR, 'Codigos:', _AT, 13, 3, 'posicion,'
        DEFB ' ', _INK, _RED, _PAPER, _YELLOW, 'color', _CR, _CR
        DEFB _INK, _GREEN, _PAPER, _BLUE, ' '
        DEFB _FLASH, 1, 'FLASH 1', _FLASH, 0, ' FLASH 0', _EOS
 
    ; Incluimos nuestra "libreria" de funciones
    INCLUDE "utils.asm"
 
    END 33500

Y este es el resultado de ejecutar el programa anterior:



El programa borra la pantalla y espera 3 segundos. Tras eso realiza las diferentes impresiones y espera la pulsación de una tecla, imprimiendo su ASCII abajo a la derecha, pegado al asterisco.

Para imprimir el primer valor numérico hemos usado PrintNum con el objetivo de imprimir el valor “completo” del registro BC (0-65535). Esta rutina utiliza dos llamadas de la ROM y la pila de la calculadora para imprimirlo, pero es importante destacar que cuando tengamos claro que nuestro valor a imprimir tiene un rango limitado, deberemos usar PrintNum2digits (para imprimir A entre 0 y 99) o PrintNum4digits (para imprimir BC entre 0 y 9999), ya que estas dos rutinas son muchos más rápidas que PrintNum (especialmente la de 2 dígitos). Si necesitamos por temas de debugging imprimir un valor 0-255, también podemos utilizar PrintHex y PrintBin para imprimirlo en hexadecimal y binario respectivamente.

Nótese por otra parte que al ensamblar el programa, PASMO nos dará unos “warnings” similares a los siguientes:

# pasmo --tapbas testutils.asm testutils.tap
WARNING: Var _BLACK is never used on line 225 of file utils.asm
WARNING: Var _BRIGHT is never used on line 219 of file utils.asm
WARNING: Var _CYAN is never used on line 230 of file utils.asm
WARNING: Var _INVERSE is never used on line 220 of file utils.asm
WARNING: Var _MAGENTA is never used on line 228 of file utils.asm
WARNING: Var _OVER is never used on line 221 of file utils.asm
WARNING: Var _TAB is never used on line 223 of file utils.asm

No debemos preocuparnos por estos warnings. Pasmo simplemente nos está advirtiendo de que hemos definido unas etiquetas EQU en el código (como por ejemplo _MAGENTA) que después no hemos usado en ninguna otra parte del programa. Sólo son mensajes informativos para que podamos hacer limpieza de referencias y variables que no se usan en el programa que estamos ensamblando. En nuestro caso, esas variables deben de estar en la librería aunque no las estemos usando en este programa concreto por lo que podemos ignorar los mensajes.

Otro apunte importante sobre nuestro programa de ejemplo es que normalmente, en la mayoría de lenguajes de programación, se suele utilizar 0 (no '0', sino 0) como indicador de final de cadena, pero en nuestro caso hemos preferido usar $ff (asociado a la constante _EOS). Hemos usado este valor porque nuestra cadena puede contener ceros (0) que se usan para desactivar funciones como FLASH o BRIGHT, tal y como muestra el ejemplo en la última línea de la variable cadena4. Si usáramos 0 como indicador de final de cadena, la secuencia “_FLASH, 0” marcaría el fin de cadena para la rutina y no se imprimiría el resto.

Queremos destacar de nuevo que el objetivo de esta librería de funciones es recoger funciones básicas para el desarrollo del curso. En el momento en que nos planteemos desarrollar un juego o una aplicación de calidad profesional, habrá que reescribir esta librería de forma que no funcione mediante llamadas a la ROM. Gracias a tener el código en una librería, siempre podemos reescribir las funciones y hacer que trabajen directamente con la videoRAM, o más óptimas, o más rápidas, o de menor tamaño, y si mantenemos los mismos parámetros de entrada y de salida de cada función, no necesitaremos modificar el código que usa la librería, sólo re-ensamblar el programa.

Por otra parte, no es normal hacer una única librería (un único fichero utils.asm) sino que lo habitual es separar las rutinas de apoyo en diferentes ficheros por funcionalidad (teclado.asm, graficos.asm, texto.asm) de forma que podamos incluir en nuestro programa sólo aquellas que necesitemos.

Cuando hacemos un INCLUDE de un fichero asm en nuestro programa, el ensamblador “añade” todo el código del fichero en nuestro programa, como si lo hubiéramos incluído manualmente en ese punto. Eso quiere decir que el ensamblador realizará el ensamblado de todo el código y que el ejecutable del programa “engordará” con todo el código incluído. Incluso aunque sólo vayamos a utilizar, por ejemplo, la rutina PrintNum, en el binario resultante estará el código de todas las funciones del fichero ASM incluído, lo que significa que ocupará más y tardará más tiempo en cargar.

Por eso, esta librería utils.asm que contiene múltiples funciones variadas, la utilizaremos para nuestras pruebas, ya que realizando pruebas no nos importará que librería aumente el tamaño final en los menos de 300 bytes que ocupa el código de la librería, pero lo normal es no utilizarla en las versiones finales de nuestros programas o juegos. Además, la librería dista mucho de ser óptima, ya que utiliza rutinas de la ROM para realizar determinadas tareas, y más adelante desarrollaremos nuestras propias rutinas mucho más eficientes.

En cualquier caso, ya armados con esta utilísima librería, podemos empezar nuestro viaje por el desarrollo en ensamblador de Z80 para Spectrum.


En este capítulo hemos creado un pequeño esqueleto básico de programa y una librería de funciones básicas que podremos usar para probar toda la teoría que vamos a ver en los próximos capítulos.

Ahora es momento de zambullirnos en la arquitectura del Spectrum y de microprocesador Z80 y de conocer la sintaxis y comandos disponibles en lenguaje ensamblador de Z80.


  • cursos/ensamblador/esqueleto_programa.txt
  • Última modificación: 19-01-2024 11:32
  • por sromero