I bet everyone is eager to dive right in and start with some multicolour goodness. I definitely am. We’re not quite there yet, though. Lets’s set up a simple start menu first. There’s a good reason for this, which will be apparent later 🙂
The next section expands on the starting code that gets run as soon as we jump into our program:
12 13 14 15 16
Start proc ; A named PROCedure (also our start point) ld sp, Start ; Put our stack right below the program Border(Black) ; Set the border to black using a helper macro call ClsAttr ; Call another named procedure to do a fast CLS (like GOSUB) Print(MenuText, MenuText.Length); Print text on the screen using ROM routines
Immediately, on line 13 we set our stack to just below the program. This is somewhat of a personal preference, but stacks are generally either kept above or below our programs, and there’s a possibility this might end up being a 128K-only game. If that’s the case, the top 16K of RAM becomes quite valuable as it can be switched in and out with other 16K banks. Having the stack in this area tends to put a crimp on this.
Line 14 calls a macro to set the border to black. Hopefully this is self-explanatory. I like to treat macros as opportunities to improve code readability—although the opposite can be true too! The macro, further down at line 45, is the standard way of doing this in Z80.
45 46 47 48
Border macro(Colour) ; Macro (makes the code more readable) to set border ld a, Colour ; Set a to the colour desired out (ULAPort), a ; and output it to the ULA Port (defined in constants) mend ; No RET is needed - this code is inserted inline
ULAPort is a constant I use instead of
254), purely to make the code self-descriptive. The other readability win here is that the macro is parameterized—whatever you pass in as the value of
Colour gets substituted. As I noted in the comments, there is no difference between writing
Border(0), and writing:
ld a, 0 ; Set a to black out ($FE), a ; and output it to the ULA Port
Incidentally, Zeus’s macro expansion is pretty clever. I could use my macro, unchanged, with
Border(b) (without any quotes around the
b is a valid Z80 register, and
ld a, b is a valid opcode, Zeus will assemble exactly that! It’s even smart enough to know that
Border(hl) would result in
ld a, hl—and grumble mightily that this is an invalid opcode.
But where were we? Oh yes. call
ClsAttr on line 15 is a function. The code in this function is big enough that we don’t want to repeat it unnecessarily by inlining it every time we clear the screen. Nor does it take any parameters, so let’s assemble it once, invoke it with
call, and let it return to the next line with
ret, exactly like
GOSUB/RETURN in BASIC. The function looks like this:
33 34 35 36 37 38 39 40 41
ClsAttr proc ; Do an attribute CLS using LDIR block copy xor a ; Set a to 0 (blank ink, black paper) ld hl, AttributeAddress ; Address to start copying from (start of attributes) ld de, AttributeAddress+1 ; Address to start copying to (next byte) ld bc, AttributeLength-1 ; Number of bytes to copy (767, all the attirbutes) ld (hl), a ; Set first byte to attribute value ldir ; Block copy bytes ret ; Return from the procedure (like RETURN) pend
This is a pretty easy method to copy the same value into a range of bytes. Again,
AttributeLength are defined as constants for readability.
On line 16,
Print(MenuText, MenuText.Length) is another macro. This one is highly parameterized, and as such really improves readability.
52 53 54 55 56 57 58 59
Print macro(TextAddress, TextLength) ; Macro to print text on the screen using ROM routines ld a, ChannelUpper ; Channel 2 (defined in constants) is the upper screen call CHAN_OPEN ; Open this channel (ROM routine) PrintLoop: ld de, TextAddress ; Address of string to print ld bc, TextLength ; Length of string to print call PR_STRING ; Print string (ROM routine) mend
This makes use of two ZX Spectrum ROM routines, to print a string of text on the upper screen. I won’t say too much about them, as they’re often used in other programs.
You’ll notice I’m using dot notation to refer to
MenuText.Length in the macro invocation.
MenuText is another procedure which I’ve used to encapsulate some data that will be assembled into bytes in RAM, and also some constants. All the constant definitions inside a
proc are local to that proc, so there’s an opportunity to namespace your labels for greater semantic clarity. I’m also making use of colour and attribute constants, so that the code resembles a BASIC
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
MenuText proc ; Named procedure to keep our print data tidy db At, 7, 13 ; These codes are the same as you would use db Paper, Black, Bright, 1 ; with Sinclair BASIC's PRINT command db Ink, Red, "Z" db Ink, Yellow, "A" db Ink, Cyan, "L" db Ink, Magenta, "A" db Ink, White, "X" db Ink, Green, "A" db At, 21, 6 db Ink, Yellow, "PRESS " db Ink, White, "SPACE" db Ink, Yellow, " TO START" Length equ $-MenuText ; Let Zeus do the work of calculating the length pend ; ($ means the current address Zeus is assembling to)
After printing the menu, the next section goes into an endless loop until the space key is pressed.
17 18 19 20 21 22 23
WaitForSpace: ; All labels inside procedures are local to that procedure halt ; Wait for the next 1/50th second interrupt (like PAUSE 1) ld bc, zeuskeyaddr(" ") ; Get the IO address to input in a, (c) ; Read those 5 keys and zeuskeymask(" ") ; AND with the bit for SPACE jr z SetupGame ; If it's zero the key is pressed jp WaitForSpace ; Otherwise check keys again
Again, this is standard Spectrum Z80 stuff for reading keys. Zeus has nice
zeuskeymask functions to make it slightly easier to code and read.
The final section does a clear screen and goes into another endless loop after the space key is pressed. This will probably get replaced in the next tutorial, but I wanted it to be clear that pressing space actually does something.
24 25 26 27 28 29
SetupGame: call ClsAttr ; Clear the screen to prove we pressed space EndlessLoop: halt jp EndlessLoop ; Go into an endless loop (for now...) pend
The code at the end is worth mentioning briefly—Zeus can create TAP, TZX, SNA and Z80 files directly from code. What I’m doing here is similar to what Pasmo does with it’s
--tapbas mode. Where Zeus comes into its own, though, is generating tape files for 128K Spectrums. We will see later, perhaps 🙂
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
BinPath equ "..\bin" ; Relative to main.asm TapFile equ BinPath+"\ZalaXa.tap" ; Filename of tap file ; Make tape file End equ $ ; Calculate the last byte of our program Size equ End-Start ; Count the bytes to save to tape output_tap TapFile, "ZalaXa", "seven-fff.com/zalaxa", Start, Size, 2, Start ; Make a .TAP file. Parameters: ; 1) the file name ; 2) the name of the BASIC loader program ; 3) a comment that goes in the TAP header ; 4) Start of machine code program ; 5) Length of machine code program ; 6) Zeus mode 2 files use the standard ROM loader ; 7) Tell the BASIC loader what to run with RANDOMIZE ; USR (like Zeus_PC tells the Zeus emulator)
Well, that was a very long, very explain-y post. Next time I will talk a little more about NIRVANA+ and introduce some multicolour code!