Repository navigation
Sesion 2: Empezando con ensamblador del Z80
- Playlist con todos los vídeos del curso RA2017
El punto de partida es justo el final del nivel 3 del curso DEZ80. Ahora estamos un poquito por encima del bajo nivel, utilizando una serie de herramientas automatizadas: Ensamblador y CPCTelera. Nos automatizan el trabajo y nos lo simplifican
Primero tenemos que tener instalado CPCTelera. En mi caso ya la instalé en el Nivel 1
- Creamos un proyecto nuevo, que llamaremos practica:
cpct_mkproject practica

Esta es la estructura de directorios creada para el proyecto:

El fichero main.c lo eliminamos, porque no trabajaremos en C, sino en ensamblador. Creamos el fichero main.s, donde se deben definir las áreas _DATA y _CODE necesarias para que el ensamblador del SDCC coloque el código y los datos en memoria
.area _DATA
.area _CODEA continuación definimos la etiqueta global _main::. El sufijo :: sirve para indicar que la etiqueta es global. Es equivalente a utilizar la directiva .global
.globl _main
_main:Para comprobar que todo funciona vamos a hacer el programa "Hola mundo" que muestre 4 píxeles rojos en la dirección 0xC000 de la memoria de vídeo
.area _DATA
.area _CODE
;;===============================
;; Main program entry
;;===============================
_main::
ld a, #0xFF
ld (0xC000),a
jr .Para ensamblarlo ejecutamos el comando make

En el directorio del proyecto aparecen los ficheros practica.cdt y practica.dsk, que son los contenedores que simulan una cinta y un disco de Amstrad. Contienen el binario del proyecto

En el directorio obj están todos los objetos de compilación generados:

Algunos ficheros interesantes son:
-
practica.map: Contiene el mapa de todos los símbolos definidos en el programa. Por ejemplo, ahí podemos ver la dirección asignada a la etiqueta
_main, donde está nuestro programa:
Value Global Global Defined In Module
----- -------------------------------- ------------------------
00004000 _main
Saber la dirección en donde están las diferentes funciones nos permitirá poner Breakpoints en WinApe
- practica.bin: Es el binario final con el código máquina

Para ejecutar el programa tecleamos el comando: cpct_winape -a. Se arranca Winape, se carga automáticamente el fichero .dsk y ejecuta el programa. Nos aparecerá esta pantalla:

Si abrimos el depurardor podemos ver en la dirección 0x4000 el código máquina de nuestro programa: 3E FF 32 00 C0 18 FE

Para trabajar en nuestro proyecto desde VSCODE basta con abrir la carpeta practica

Desde un terminal podemos ejecutar los comandos para ensamblar y ejecutar Winape. O bien podemos utilizar las tareas ya creadas, accesibles desde el menú Terminal/Run Task...
He instalado los plugins Z80 assembly para el resaltado de sintáxis en ensamblador y Hex editor para ver los ficheros binarios en código máquina
Ya tenemos el entorno listo para empezar a trabajar. Vamos a crear un cuadrado cyan de 8x8 píxeles, que utilizaremos de base como nuestro personaje, y que poco a poco iremos refinando y añadiendo cosas. Para ello utilizamos la función cpct_drawSolidBox de cpctelera
Para que el ensamblador tenga acceso al símbolo cpct_drawSolidBox_asm y que pueda llamar a la función, es necesario declarar esa etiqueta como global usando:
;;-- CPCtelera symbols
.globl cpct_drawSolidBox_asmAl realizar el linkado la herramienta toma la dirección de esa función de la bibliotca de cpctelera. El programa para dibujar el cuadrado de 8x8 en la posición 0xC320 de la memoria de vídeo es el siguiente:
.area _DATA
;;-- CPCtelera symbols
.globl cpct_drawSolidBox_asm
.area _CODE
;;===============================
;; Main program entry
;;===============================
_main::
;; Draw a Box
ld de, #0xC320 ;; Location
ld a, #0x0F ;; Cyan
ld bc, #0x0802 ;; Height, Width: 8x8 pixels
call cpct_drawSolidBox_asm
jr .Ahora lo ensamblamos y ejecutamos con winape. Para hacer el proceso más sencillo, utilizamos este alias:
alias do="make && cpct_winape -a"Lo que se hace es llamar a make y en caso de que se haya ensamblado correctamente, se ejecuta el comando cpct_winape -a
Nos aparecerá esto:

En la documentación de la función vemos los parámetros que le tenemos que pasar:
- DE: Dirección de video a partir de la que dibujar el recuadro
- A: Patrón de color a usar. En nuestro caso 4 píxeles azules
- BC: La altura y la anchura. ¡OJO! La altura es en píxeles y la anchura en bytes!!!
En el fichero practica.map encontramos las etiquetas definidas y los valores asignados:
Value Global Global Defined In Module
----- -------------------------------- ------------------------
00004000 _main
0000400D cpct_drawSolidBox_asm cpct_sprites
Nuestro programa (_main) está situado a partir de la dirección 0x4000. La función cpct_drawSolidBox_asm está situada a partir de la dirección 0x400D. El símbolo está definido dentro del módulo cpct_sprites
En el fichero main.lst vemos el código máquina que se ha generado para cada parte de nuestro fichero main.s
0000 11 _main::
12
13 ;; Draw a Box
0000 11 20 C3 [10] 14 ld de, #0xC320 ;; Location
0003 3E 0F [ 7] 15 ld a, #0x0F ;; Cyan
0005 01 02 08 [10] 16 ld bc, #0x0802 ;; Height, Width: 8x8 pixels
0008 CD 00 00 [17] 17 call cpct_drawSolidBox_asm
18
000B 18 FE [12] 19 jr .Las direcciones de etiquetas globales se dejan a 0 porque el ensamblador no conoce sus direcciones de memoria. Será luego el linker el que les asigne el valor correcto. Así por ejemplo, en la instrucción de call cpct_drawSolidBox_asm se genera el siguiente código máquina: CD 00 00. La dirección del call es 0000
En el fichero main.rst , generado por el linker, ya podemos ver las direcciones resueltas y sus valores reales:
4000 11 _main::
12
13 ;; Draw a Box
4000 11 20 C3 [10] 14 ld de, #0xC320 ;; Location
4003 3E 0F [ 7] 15 ld a, #0x0F ;; Cyan
4005 01 02 08 [10] 16 ld bc, #0x0802 ;; Height, Width: 8x8 pixels
4008 CD 0D 40 [17] 17 call cpct_drawSolidBox_asm
18
400B 18 FE [12] 19 jr .El código que pinta el persona lo llevamos a una función: drawHero(). Para definir funciones simplemente ponemos su etiqueta y escribirmos el código. Añadimos una función de cabecera para indicar lo que hace. La llamamos desde main:
.area _DATA
;;-- CPCtelera symbols
.globl cpct_drawSolidBox_asm
.area _CODE
;;===============================
;; drawHero
;;
;; DESTROYs: AF, BC, DE,
;;===============================
drawHero:
;; Draw a Box
ld de, #0xC320 ;; Location
ld a, #0x0F ;; Cyan
ld bc, #0x0802 ;; Height, Width: 8x8 pixels
call cpct_drawSolidBox_asm
ret
;;===============================
;; Main program entry
;;===============================
_main::
call drawHero
jr .Para dibujar el persona necesitamo definir dos variables, que contengan las posiciones x,y del personaje. ¡OJO! La variable x está en bytes, mientras que la de y en pixeles. Los rangos son por tanto:
- x: [0-79]
- y: [0-199]
Las variables las definidmos en el área _DATA, usando las directivas .db
.area _DATA
;;===================================
;; VARIABLES
;;===================================
hero_x: .db #39 ;;-- x position (in bytes [0-79])
hero_y: .db #80 ;;-- y posicion (in pixels [0-199])
.area _CODE
...Las variables de posición x,y se tienen que convertir a memoria de vídeo. Usamos la función cpct_getScreenPtr-asm. La nueva función drawHero() queda ahora así:
;;===============================
;; drawHero
;;
;; DESTROYs: AF, BC, DE, HL
;;===============================
drawHero:
;;-- Convert the hero_x, hero_y variables into
;;-- the corresponding address in the Video memory
ld de, #0xC000 ;;-- Video initial address
ld a,(hero_x)
ld c,a ;;-- Hero x position
ld a,(hero_y)
ld b,a ;;-- Hero y position
call cpct_getScreenPtr_asm
;; HL contains the video address
;; move it to the DE register
ex de, hl
;; Draw a Box (our hero!)
ld a, #0x0F ;; Cyan
ld bc, #0x0802 ;; Height, Width: 8x8 pixels
call cpct_drawSolidBox_asm
retAl ejecutarlo, este es el resultado:

Vamos a mover nuestro personaje con las teclas. Para ello tenemos que leer el teclado. Implementamos la función checkUserInput
;;===============================
;; checkUserInput: Read the keyboard and update the position
;; of our hero
;;
;; DESTROYs:
;;===============================
checkUserInput:
;;-- Scan the whole keyboard
call cpct_scanKeyboard_asm
;;-- Check for key 'D' being pressed
ld hl, #Key_D ;;-- hl = Key_D Keycode
call cpct_isKeyPressed_asm
cp #0 ;; check A == 0
jr z, d_not_pressed ;;-- Jump if D is not pressed
;;-- D is pressed
;;-- Increase the hero_x value by 1 (Hero_x = Hero_x + 1)
ld a, (hero_x) ;;-- A = hero_x
inc a ;;-- A = A + 1
ld (hero_x),a ;;-- hero_x = A
d_not_pressed:
retSe usa la función cpct_scanKeyboard de cpctelera, que realiza una lectura del teclado y almacena el estado de las teclas pulsadas
Llamando a la función cpct_isKeyPressed_asm se comprueba si una tecla particular se ha apretado. En nuestro ejemplo hacemos que el personaje se mueva hacia la derecha con la tecla D por lo que le pasamos el valor Key_D en HL. Si la tecla está pulsada se incrementa la variable hero_pos
Los códigos de las teclas están accesible mediante este include:
.include "keyboard/keyboard.s"
Para mover el personaje hay que borrarlo y dibujarlo en la nueva posición. Modificamos la función drawHero para pasarle como parámetro (a través del registro A) el patrón de color: 0x0F (Cyan) para el personaje y 0x00 (fondo) para borrarlo
;;===============================
;; drawHero
;;
;; INPUT:
;; A => Color Pattern
;;
;; DESTROYs: AF, BC, DE, HL
;;===============================
drawHero:
;;-- Store the color patter
push af
;;-- Convert the hero_x, hero_y variables into
;;-- the corresponding address in the Video memory
ld de, #0xC000 ;;-- Video initial address
ld a,(hero_x)
ld c,a ;;-- Hero x position
ld a,(hero_y)
ld b,a ;;-- Hero y position
call cpct_getScreenPtr_asm
;; HL contains the video address
;; move it to the DE register
ex de, hl
;; Draw a Box (our hero!)
pop af ;; Read the color pattern
ld bc, #0x0802 ;; Height, Width: 8x8 pixels
call cpct_drawSolidBox_asm
retPara no perder el valor del registro A, lo metemos en la pila (push AF) y lo recuperamos (pop AF) antes de llamar a la función drawSolidBox
Este es el bucle principal del programa:
;;===============================
;; Main program entry
;;===============================
_main::
;; Erase previous hero
ld a,#0x00 ;; Color pattern: 0 (Background)
call drawHero
call checkUserInput ;; Check if user pressed keys
;;-- Draw the hero
ld a,#0x0F ;; Color pattern: Cyan
call drawHero
;;-- Wait for the raster to finish
call cpct_waitVSYNC_asm
jr _mainTras dibujar el personaje en la nueva posición, se llama a la función waitVSYNC para esperar a que el raster llegue al final. Cuando eso ocurre se repite el ciclo: se borra el personaje actual, y se dibuja en la nueva posición
Usar la función waitVSYNC hace que el personaje se dibuje sin parpadeos, ya que cuando pasa el raster el fotograma es estable
.area _DATA
;;===================================
;; VARIABLES
;;===================================
hero_x: .db #39 ;;-- x position (in bytes [0-79])
hero_y: .db #80 ;;-- y posicion (in pixels [0-199])
;;-- CPCtelera symbols
.globl cpct_drawSolidBox_asm
.globl cpct_getScreenPtr_asm
.globl cpct_scanKeyboard_asm
.globl cpct_isKeyPressed_asm
.globl cpct_waitVSYNC_asm
.include "keyboard/keyboard.s"
.area _CODE
;;===============================
;; checkUserInput: Read the keyboard and update the position
;; of our hero
;;
;; DESTROYs:
;;===============================
checkUserInput:
;;-- Scan the whole keyboard
call cpct_scanKeyboard_asm
;;-- Check for key 'D' being pressed
ld hl, #Key_D ;;-- hl = Key_D Keycode
call cpct_isKeyPressed_asm
cp #0 ;; check A == 0
jr z, d_not_pressed ;;-- Jump if D is not pressed
;;-- D is pressed
;;-- Increase the hero_x value by 1 (Hero_x = Hero_x + 1)
ld a, (hero_x) ;;-- A = hero_x
inc a ;;-- A = A + 1
ld (hero_x),a ;;-- hero_x = A
d_not_pressed:
ret
;;===============================
;; drawHero
;;
;; INPUT:
;; A => Color Pattern
;;
;; DESTROYs: AF, BC, DE, HL
;;===============================
drawHero:
;;-- Store the color patter
push af
;;-- Convert the hero_x, hero_y variables into
;;-- the corresponding address in the Video memory
ld de, #0xC000 ;;-- Video initial address
ld a,(hero_x)
ld c,a ;;-- Hero x position
ld a,(hero_y)
ld b,a ;;-- Hero y position
call cpct_getScreenPtr_asm
;; HL contains the video address
;; move it to the DE register
ex de, hl
;; Draw a Box (our hero!)
pop af ;; Read the color pattern
ld bc, #0x0802 ;; Height, Width: 8x8 pixels
call cpct_drawSolidBox_asm
ret
;;===============================
;; Main program entry
;;===============================
_main::
;; Erase previous hero
ld a,#0x00 ;; Color pattern: 0 (Background)
call drawHero
call checkUserInput ;; Check if user pressed keys
;;-- Draw the hero
ld a,#0x0F ;; Color pattern: Cyan
call drawHero
;;-- Wait for the raster to finish
call cpct_waitVSYNC_asm
jr _main- Resultado:

El suelo está implementado en la función drawGround()
;;===============================
;; drawGround
;;
;; INPUT:
;; A => Color Pattern
;;
;; DESTROYs: AF, BC, DE, HL
;;===============================
drawGround:
push AF ;;-- Save the color pattern
;;-- Draw the half-left side of the ground
ld de, #0xC000 ;;-- Video initial address
ld bc, #0x5800 ;;-- Position: 0,0x50
call cpct_getScreenPtr_asm
ex de, hl ;;-- DE=video address for the ground
pop AF ;;-- Color pattern
push AF ;;-- Save the color pattern
ld bc, #0x0240 ;;-- B: height (Pixels), C: width [1-64]
call cpct_drawSolidBox_asm
;;-- Draw the half-right side of the ground
ld de, #0xC000 ;;-- Video initial address
ld bc, #0x5840 ;;-- Position: 0x50,0x58
call cpct_getScreenPtr_asm
ex de, hl ;;-- DE=video address for the ground
pop AF ;;-- Color pattern
ld bc, #0x0210 ;;-- B: height (Pixels), C: width [1-64]
call cpct_drawSolidBox_asm
retHay que llamar 2 veces a la función drawSolidBox, ya que no permite usar anchos mayores de 64. Para cubrir todo el suelo hay que dibujar dos veces
El bucle principal queda así:
;;===============================
;; Main program entry
;;===============================
_main::
;;-- Draw the Ground in red color
ld a, #0xFF ;;-- Red pattern
call drawGround
main_loop:
;; Erase previous hero
ld a,#0x00 ;; Color pattern: 0 (Background)
call drawHero
call checkUserInput ;; Check if user pressed keys
;;-- Draw the hero
ld a,#0x0F ;; Color pattern: Cyan
call drawHero
;;-- Wait for the raster to finish
call cpct_waitVSYNC_asm
jr main_loop
En la función checkUserInput se comprueba la tecla A tras la D, y se decrementa la variable hero_x si se ha pulsado
Para que el personaje esté en la misma horizontal hay que restringir el valor de hero_x a [0, 79]. Dentro de la función checkUserInput modicamos el valor de la variable sólo si está dentro del rango
;;===============================
;; checkUserInput: Read the keyboard and update the position
;; of our hero
;;
;; DESTROYs:
;;===============================
checkUserInput:
;;-- Scan the whole keyboard
call cpct_scanKeyboard_asm
;;-- Check for key 'D' being pressed
ld hl, #Key_D ;;-- hl = Key_D Keycode
call cpct_isKeyPressed_asm
cp #0 ;; check A == 0
jr z, d_not_pressed ;;-- Jump if D is not pressed
;;-- D is pressed
;;-- Increase the hero_x value by 1 (Hero_x = Hero_x + 1)
ld a, (hero_x) ;;-- A = hero_x
;;- Check if the hero_x has reached its maximum value
cp #78
jr z,checkUserInput_end ;;-- Max value reached: end
;;- No max reached: increase the hero_x by 1
inc a ;;-- A = A + 1
ld (hero_x),a ;;-- hero_x = A
jr checkUserInput_end
d_not_pressed:
;;-- Check for the key 'A' being pressed
ld hl, #Key_A ;;-- hl = Key_A Keycode
call cpct_isKeyPressed_asm
cp #0 ;; check A == 0
jr z, a_not_pressed ;;-- Jump if D is not pressed
;;-- A is pressed
;;-- Decrease the hero_y value by 1 (Hero_x = Hero_x - 1)
;;-- (only is decreased if it is greather than 0)
ld a, (hero_x)
cp #0 ;;-- hero_x == 0?
jr z, checkUserInput_end ;;-- Yes: Finish
;;-- Decrease the hero_x
dec a
ld (hero_x), a
a_not_pressed:
checkUserInput_end:
ret- Resultado

El movimiento del personaje se consigue mediante la actualización de la variable hero_x. Al actualizarla hay que comprobar también que no se rebasen sus límites. Por ello creados las funciones moveHeroRight y moveHeroLeft
;;=============================================
;; moveHeroRight: Move the hero to the right
;; (inside the line limits)
;;
;; DESTROYS:
;;=============================================
moveHeroRight:
ld a, (hero_x) ;;-- A = hero_x
;;- Check if the hero_x has reached its maximum value
cp #80-2 ;; Check against right limit
jr z,do_not_move_r
;;- No max reached: increase the hero_x by 1
inc a ;;-- A = A + 1
ld (hero_x),a ;;-- hero_x = A
do_not_move_r:
ret
;;=============================================
;; moveHeroLeft: Move the hero to the left
;; (inside the line limits)
;;
;; DESTROYS:
;;=============================================
moveHeroLeft:
;;-- Decrease the hero_y value by 1 (Hero_x = Hero_x - 1)
;;-- (only is decreased if it is greather than 0)
ld a, (hero_x)
cp #0 ;;-- hero_x == 0?
jr z, do_not_move_l ;;-- Yes: Finish
;;-- Decrease the hero_x
dec a
ld (hero_x), a
do_not_move_l:
retLa función de comprobar el teclado queda ahora más limpia. Sólo se hacen cosas relacionadas con el teclado y se llama a las acciones correspondientes
;;===============================
;; checkUserInput: Read the keyboard and update the position
;; of our hero
;;
;; DESTROYs:
;;===============================
checkUserInput:
;;-- Scan the whole keyboard
call cpct_scanKeyboard_asm
;;-- Check for key 'D' being pressed
ld hl, #Key_D ;;-- hl = Key_D Keycode
call cpct_isKeyPressed_asm
cp #0 ;; check A == 0
jr z, d_not_pressed ;;-- Jump if D is not pressed
;;-- D is pressed
call moveHeroRight
d_not_pressed:
;;-- Check for the key 'A' being pressed
ld hl, #Key_A ;;-- hl = Key_A Keycode
call cpct_isKeyPressed_asm
cp #0 ;; check A == 0
jr z, a_not_pressed ;;-- Jump if D is not pressed
;;-- A is pressed
call moveHeroLeft
a_not_pressed:
;;-- Check for the key 'W' being pressed
ld hl, #Key_W ;;-- hl = Key_W Keycode
call cpct_isKeyPressed_asm
cp #0 ;; check A == 0
jr z, w_not_pressed ;;-- Jump if D is not pressed
;;-- W is pressed
call startJump
w_not_pressed:
retEl salto se activa con la tecla 'w'. Al hacerlo, se actualiza la variable hero_y con un cierto incremento en cada fotograma, hasta que se acaba el salto. La variable hero_jump se usa para identificar todos los fotogramas del salto. Inicialmente vale -1 que significa que no se está saltando
Los incrementos a aplicar en cada fotograma se almacenan en la tabla jumptable (que se llama tabla de salto):
;;-- HERO DATA
hero_x: .db #39 ;;-- x position (in bytes [0-79])
hero_y: .db #80 ;;-- y posicion (in pixels [0-199])
hero_jump: .db #-1 ;;-- Are we jumping?
;;-- Jump table
jumptable:
.db #-3, #-2, #-1, #-1
.db #-1, #0, #0, #0
.db #1, #2, #2, #3
.db #0x80En el bucle principal se llama a la función jumpControl para actualizar la posición y del personaje si se está realizando el salto
main_loop:
;; Erase previous hero
ld a,#0x00 ;; Color pattern: 0 (Background)
call drawHero
call jumpControl ;;-- Do jumps
call checkUserInput ;; Check if user pressed keys
;;-- Draw the hero
ld a,#0xFF ;; Color pattern: Cyan
call drawHero
;;-- Wait for the raster to finish
call cpct_waitVSYNC_asm
jr main_loopLa función jumpControl acceder a la tabla, leer el incremento actual, se lo aplica a la variable hero_y y actualiza hero_jump para apuntar al siguiente valor de la tabla. El numero 0x80 se usa para indicar el final de la tabla
;;=====================================
;; Control the hero jump
;;=====================================
jumpControl:
ld a,(hero_jump) ;; A=Hero jump status
cp #-1 ;; Are we jumping?
ret z ;; No jumping, return
;;-- Move Hero
;;-- Calculate HL = jumptable[A]
ld hl, #jumptable ;;-- HL ponts to the jumptable
ld b, #0
ld c, a ;; BC = A
add hl,bc ;; HL = HL + Satus. Address of the current jump position
;;-- Calculate the new hero_y pos
ld b, (hl) ;;-- Get the current jump increment
ld a,b
cp #0x80 ;;-- Check if it is the last position
jr z,update_jump_index
;;-- NOT the last position
;;-- update the hero_y
ld a, (hero_y) ;;-- Get the hero_y
add b ;;-- A = A + B (current pos)
ld (hero_y),a ;;-- Updated hero_y
;;-- Increment hero_jump index
ld a, (hero_jump)
inc a
ld (hero_jump),a
jr continue_jump
update_jump_index:
ld a,#-1
ld (hero_jump),a
continue_jump:
retDesde la función checkUserInput se llama a la función startJump cada vez que se aprieta la tecla W. En la función se comprueba si ya se está saltando. Si es así, se continua
;;=============================================
;; startJump
;;
;; DESTROYS:
;;=============================================
startJump:
ld a,(hero_jump)
cp #-1 ;; Are we already jumping?
ret nz ;; Yes: ret
ld a, #0
ld (hero_jump), a
ret- Resultado:



