Espamática
ZX SpectrumRetroZ80 Assembly

ZX Spectrum Assembly, Pong – 0x01 Drawing on the screen

In this chapter of ZX Spectrum Assembly, we will learn how to paint on the screen and see its layout.

Drawing on the screen

The ZX Spectrum’s screen, the pixel area, is located from memory address $4000 to $57FF, both inclusive, for a total of 6144 bytes, or 256 * 192 pixels, 32 columns and 24 lines.

The ZX Spectrum divides the screen into three thirds of eight lines each, with eight scanlines (one pixel high horizontal line of the screen) per line. The memory addresses that refer to each screen byte (pixel area) are coded in this way:

010T TSSS LLLC CCCC

Where TT is the third (0 to 2), SSS is the scanline (0 to 7), LLL is the line (0 to 7) and CCCCCC is the column (0 to 31).

In this first step we will learn how to draw on the screen and see two routines that we will use in our ZX-Pong and possibly in our next developments.

The first thing we are going to do is create a folder called Pong, and inside it we are going to create another folder called Step01. Inside this last folder we will create the files main.asm and video.asm.

The two routines we are going to implement in video.asm, NextScan and PreviousScan, are taken from Santiago Romero’s Compiler Software Z80 Assembly Course, which can be found on the speccy.org wiki, and calculate the next and previous scanlines to a given position.

Both routines get the position in the VideoRAM from which the next or previous scanline is to be calculated in HL and return this position in the same register. They also change the AF value.

Let’s have a look at the NextScan routine:

NextScan:
inc  h
ld   a, h
and  $07
ret  nz

In the first instruction we increment the scanline, INC H, which is in bits 0 to 2 of H. We then load the value of H into A, LD A, H, and we are left with only the value of the bits of the scanline, AND $07.

If the value of the previous operation is not zero, the scanline has a value between 1 and 7, no further calculation is necessary and we exit the routine, RET NZ.

If the value is zero, the scanline was 7 before incrementing H:

0100 0111

Adding one sets the bits of the scanline to zero and increments the bits of the third line by one:

0100 1000

The next thing the routine does is:

ld   a, l
add  a, $20
ld   l, a
ret  c

We load the value of L into A, LD A, L, which contains the row and column. We add one to the row, ADD A, $20:

$20 = 0010 0000 = LLLC CCCC

We load the result into L, LD L, A, and if there is a carry we exit, RET C.

If there’s a carry, the line before adding $20 was 7. Adding one makes the line go to zero and we have to increment the third, but since it’s already been incremented by incrementing the scanline, we don’t do anything.

Finally, if we continue, it is because we are still in the same third, so we have to decrement it and leave it as it was. At this point, increasing the scanline has changed the line and increasing the line has not changed the third.

ld   a, h
sub  $08
ld   h, a
ret

We load the value of H in A, LD A, H, third and scanline. We subtract $08 from this value to decrease the third by one, SUB $08, and leave it as it was:

$08 = 0000 1000 = 010T TSSS

We load the result of the operation into H, LD H, A, and exit the routine with RET.

The full code of the routine is:

;------------------------------------------------------------------
; NextScan
; https://wiki.speccy.org/cursos/ensamblador/gfx2_direccionamiento
; Gets the memory location corresponding to the scanline.
; The next to the one indicated.
;     010T TSSS LLLC CCCC
; Input:  HL -> current scanline.
; Output: HL -> scanline next.
; Alters the value of the AF and HL registers.
;------------------------------------------------------------------
NextScan:
inc  h                     ; Increment H to increase the scanline
ld   a, h                  ; Load the value in A
and  $07                   ; Keeps the bits of the scanline
ret  nz                    ; If the value is not 0, end of routine  

; Calculate the following line
ld   a, l                  ; Load the value in A
add  a, $20                ; Add one to the line (%0010 0000)
ld   l, a                  ; Load the value in L
ret  c                     ; If there is a carry-over, it has changed
                           ; its position, the top is already adjusted 
                           ; from above. End of routine.

; If you get here, you haven't changed your mind and you have to adjust 
; as the first inc h increased it.
ld   a, h                  ; Load the value in A
sub  $08                   ; Subtract one third (%0000 1000)
ld   h, a                  ; Load the value in H
ret

At this point we will edit the main.asm file to test the NextScan routine.

The first step is to specify where to load the program, in our case at address $8000 (32768):

org  $8000

The next thing to do is to point HL in the direction of the screen where we want to start drawing, the top left corner:

ld   hl, $4000

Let us recall how a VideoRAM memory address is encoded:

010T TSSS LLLC CCCC

And we put $4000 in binary:

0100 0000 0000 0000

We can see that $4000 refers to third 0, row 0, scanline 0 and column 0.

We are going to draw a vertical column, from top to bottom, that fills the entire screen, so we need to loop 192 iterations, the number of scanlines the screen has, and we will load this value into B:

ld   b, $c0

Once we have reached this point, we can make the loop. To do this we will set a label to refer to. We load the pattern 00111100 ($3C) at the screen address pointed to by HL, get the location of the next scanline and go back to the start of the loop until B equals 0:

loop:
ld   (hl), $3c
call NextScan
djnz loop

As we can see, HL is in parentheses, but previously when we loaded $4000 in HL, it was not in parentheses. What is the difference?

When we write LD HL, $4000, we load the value $4000 into HL, i.e. HL = $4000. Conversely, when we write LD (HL), $3C, we load $3C into the memory location to which HL points, i.e. ($4000) = $3C.

After loading $3C into the location pointed to by HL, we get the address of the next scanline by calling the NextScan routine, CALL NextScan.

The last instruction, DJNZ loop, is the reason for choosing the B register to control the loop iterations. If we chose another 8-bit register, we would have to decrement it and then check that it has not reached zero, in which case we would jump to the loop:

dec  a
jr   nz, loop

DJNZ does all this in a single instruction using the B register, using one byte and 8 or 13 clock cycles depending on whether the condition is met or not. Using DEC and JR takes three bytes and 11 or 17 clock cycles.

All that remains is to tell the program the output, include the file containing the NextScan routine and tell PASMO the address to call when the program is loaded.

ret

include "video.asm"

end  $8000

Compile the programme with PASMO. From the command line, change to the directory containing the .asm files and type the following.

pasmo --name ZX-Pong --tapbas main.asm pong.tap --pong.log

Now we can load our programme into the ZX Spectrum emulator and see something like this:

ZX Spectrum Assembly, Pong
ZX Spectrum Assembly, Pong

As we can see, it has drawn a vertical column, but it’s so fast that we can’t see how it’s drawn. To see it, we can add the HALT instruction before DJNZ. HALT waits for an interrupt to occur, which in the case of the ZX Spectrum is triggered by the ULA.

The resulting code is:

org  $8000

ld   hl, $4000             ; HL = first scanline of the first line 
                           ; of the first third and column one of the
                           ; display (Column 0 to 31)
ld   b, $c0                ; B = 192. Number of scanlines on the screen

loop:
ld   (hl), $3c             ; Paint on screen 001111000
call NextScan              ; Goes to the next scanline
; halt                       ; Uncomment line to see what it looks like
djnz loop                  ; until B = 0

ret

include "video.asm"
end  $8000

We compile again and now we can see how it looks from scanline to scanline. If we want it to be fast again, we comment out the HALT statement.

In video.asm we now implement the routine that gets the memory address of the previous scanline:

PreviousScan:
ld   a, h
dec  h
and  $07
ret  nz

First we load the value of H, LD A, H, H, third and scanline into A, and then decrement H, DEC H. Then we keep the bits of the original scanline, AND $07, that we have in A, and if it was not in scanline 0 we exit the routine, RET NZ. A has the original value of H.

If it was on scanline 0, decrementing H moved us to scanline 7 of the previous line, and the third was decremented.

Now the line needs to be calculated:

ld   a, l
sub  $20
ld   l, a
ret  c

We load L, LD A, L, row and column in A, subtract $20, SUB $20, to decrement the row, and reload the value in L, LD L, A. We exit if there is a carry, RET C, since there is a change of a third, which occurred when the scanline was decremented.

If there is no carry, it is necessary to leave the third as it was:

ld   a, h
add  a, $08
ld   h, a
ret

We load the value of H (which contains the third and the scanline) into A, LD A, H, add $08 to increment the third, ADD A, $08, load the value into H, LD H, A, and exit the routine, RET.

The final code of the routine is as follows:

; -----------------------------------------------------------------
; PreviousScan
; https://wiki.speccy.org/cursos/ensamblador/gfx2_direccionamiento
; Gets the memory location corresponding to the scanline.
; The following is the first time this has been done; prior 
; to that indicated.
;     010T TSSS LLLC CCCC
; Input:  HL -> current scanline.	    
; Output: HL -> previous scanline.
; Alters the value of the AF, BC and HL registers.
;------------------------------------------------------------------
PreviousScan:
ld   a, h                  ; Load the value in A
dec  h                     ; Decrements H to decrement the scanline
and  $07                   ; Keeps the bits of the original scanline
ret  nz                    ; If not at 0, end of routine

; Calculate the previous line
ld   a, l                  ; Load the value of L into A
sub  $20                   ; Subtract one line
ld   l, a                  ; Load the value in L
ret  c                     ; If there is carry-over, end of routine

; If you arrive here, you have moved to scanline 7 of the previous line
; and subtracted a third, which we add up again
ld   a, h                  ; Load the value of H into A
add  a, $08                ; Returns the third to the way it was
ld   h, a                  ; Load the value in h
ret

Finally, we go back to main.asm to implement the PreviousScan test. Let’s add the new code after the DJNZ loop statement.

The first thing to do is to load into HL the address of the VideoRAM where we want to draw, in this case the bottom right corner:

ld   hl, $57ff

If we put $57FF into binary:

0101 0111 1111 1111

Reference is made to third 2, line 7, scanline 7 and column 31.

The loop goes back 192 iterations to draw to the top right corner. We load the value into B:

ld   b, $c0

And then we do the loop:

loopUp:
ld   (hl), $3c
call PreviousScan
halt
djnz loopUp

The only difference with the loop is in the CALL, which this time is made to PreviousScan instead of NextScan. HALT is uncommented so that you can see how it looks.

The full main.asm code is:

; Draw two vertical lines, one from bottom to top 
; and one from top to bottom.
; Top to bottom to test the NextScan and PreviousScan routines.
org  $8000

ld   hl, $4000             ; HL = scanline 0, line 0, third 0
                           ; and column 0 (column from 0 to 31)
ld   b, $c0                ; B = 192. Scanlines that the display has

loop:
ld   (hl), $3c             ; Paint on screen 001111000
call NextScan              ; Goes to the next scanline
; halt                      ; Uncomment to see the painting process
djnz loop                  ; Until B = 0
ld   hl, $57ff             ; HL = last scanline, line, third 
                           ; and column 31
ld   b, $c0                ; B = 192. Scanlines that the display has

loopUp:
ld   (hl), $3c             ; Paint on screen 001111000
call PreviousScan          ; Goes to the previous scanline
; halt                     ; Uncomment to see the painting process
djnz loopUp                ; Until B = 0
ret

include	"video.asm"

end  $8000

We compile again and see the result by loading the generated programme into the ZX Spectrum emulator:

pasmo --name ZX-Pong --tapbas main.asm pong.tap --pong.log

This time we paint two lines: the left one from top to bottom and the right one from bottom to top. If you remove the comment on the HALT lines, you will not be able to see in which direction the lines are painted.

ZX Spectrum Assembly, Pong
ZX Spectrum Assembly, Pong

ZX-Spectrum Assembly, Pong

In the next ZX Spectrum Assembly chapter, we will learn how to read the keyboard without using the ROM routines.

Download the source code from here.

ZX Spectrum Assembly, Pong by Juan Antonio Rubio García.
Translation by Felipe Monge Corbalán.
This work is licensed to Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
Any comments are always welcome.



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies

Descubre más desde Espamática

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo