The compiler understands several pragmas that — thought they are not Z80 instructions — they influence the emitted code. Each pragma has two alternative syntax, one with a dot prefix and another without it.
For example, you can write ORG
or .ORG
to use the ORG pragma.
I prefer using the dot-prefixed versions of pragmas.
The ORG pragma
With the ORG pragma, you define where to place the compiled Z80 code when you run it. For example, the following line sets this location to the 0x6000 address:
.org #6000
If you do not use ORG, the default address is 0x8000.
You can apply multiple ORG pragmas in your source code. Each usage creates a new segment in the assembler output. Take a look at this code:
ld h,a
.org #8100
ld d,a
.org #8200
ld b,a
This code generates three output segment, each with one emitted byte that represents the
corresponding LD
operation. The first segment will start at 0x8000 (default),
the second at 0x8100, whilst the third at 0x8200.
The XORG pragma
With the XORG pragma, you define the start address of a specific code section (the section started with the previous .ORG
) to use when exporting to Intel HEX format.
For example, the following line sets this location to the 0x0000 address; however, the code section starts at 0x6000.
.org #6000
.xorg #0
If you try to use multiple .XORG
within a code section, the assembler raises an error:
.org #6000
.xorg #0
ld a,b
; ...
.xorg #1000 ; This line will casue an error message
The ENT pragma
The ENT pragma defines the entry code of the program when you run it from Visual Studio. If you do not apply ENT in your code, the entry point will be the first address of the very first output code segment. Here’s a sample:
.org #6200
ld hl,#4000
.ent $
jp #6100
.org #6100
call MyCode
...
The .ent $
pragma will sign the address of the jp #6100
isntruction as the entry
address of the code. Should you omit the ENT pragma from this code, the entry point would be
0x6200, for this is the start of the very first output segment, even though there is another
segment starting at 0x6100.
The XENT pragma
The IDE provides a command, Export Z80 Program, which allows you to create a LOAD block
that automatically starts the code. The Run Z80 Program and Debug Z80 Program command
simply jump to the address you specify with the ENT pragma. However, the auto LOAD block uses
the RANDOMIZE USR address
pattern where you need to define a different entry address that
can be closed with a RET
statement. The XENT pragma sets this address.
Here’s a sample:
start:
.org #8000
.ent #8000
call SetBorder
jp #12ac
SetBorder:
.xent $
ld a,4
out (#fe),a
ret
The IDE will use #8000 — according to the .ent #8000
pragma — when starting
the code with the Run Z80 Program. Nonetheless, the Export Z80 Program will offer #8006
— according to the .xent $
pragma — as the startup code address.
The DISP pragma
The DISP pragma allows you to define a displacement for the code. The value affects the
$
token that represents the current assembly address. Your code is placed according
to the ORG of the particular output segment, but the assembly address is always displaced
with the value according to DISP. Take a look at this sample:
.org #6000
.disp #1000
ld hl,$
The ld hl,$
instruction will be placed to the 0x6000 address, but it will be equivalent
with the ld hl,#7000
statement due to the .disp #1000
displacement.
Of course, you can use negative displacement, too.
The BANK pragma
The ZX Spectrum 128K/2A/+2A/+3/+3E models handle 16K memory pages (banks) that can be paged into particular memory slots. (You can find more information about this here.)
The BANK pragma allows you to declare that you want to put the Z80 Assembly code in a specific memory bank. When you export the compiled output, the Export program function of SpectNetIDE creates a loader that reads the code and places it on the specified memory page.
The BANK pragma accepts two parameters. The first is the bank number (so it must be between 0 and 7). The second one is
an optional offset value (between 0 and 16383), which indicates the start offset within the bank. If you omit this, the default
value is zero. By default, the SpectNetIDE assembler assumes that the start address of the code in the bank is $C000
. Nonetheless,
you can specify any other value.
Note: You need to apply the
.model Spectrum128
pragma at the top of your code so that you can use.bank
.
Using BANK without an offset
Let’s assume you have this code:
.model Spectrum128
; ...
.bank 3
call yellow
ret
yellow:
ld a,6
out (#fe),a
ret
The compiler emits this code (and later, the loader takes care that it goes to bank #3):
0000: call #C004 ; yellow
0003: ret
0004: ld a,#06 ; this is yellow (#C004)
0006: out (#FE),a
The offset values at the beginning of the lines show the byte offset within the 16K memory bank.
Using BANK with an offset
Let’s modify the previous code adding an offset value:
.model Spectrum128
; ...
.bank 3, #100
call yellow
ret
yellow:
ld a,6
out (#fe),a
ret
Now, the compiler emits similar code, but its starts address is #C100 (#100 away from the default #C000):
0100: call #C104 ; yellow
0103: ret
0104: ld a,#06 ; this is yellow (#C104)
0106: out (#FE),a
Though it seems that we’re wasting the first 256 bytes of the page, the Export program command does not output those bytes. The loader knows that it should load the code from address #C100.
Using BANK with ORG
Though the default address to compile the code is #C000, you can change it. For example, Bank #2 is paged into the #8000-#BFFF memory range (slot 2), so it seems natural to use the #8000 address like this:
.model Spectrum128
; ...
.bank 2
.org #8000
call yellow
ret
yellow:
ld a,6
out (#fe),a
ret
As you expect, this is the output:
0000: call #8004 ; yellow
0003: ret
0004: ld a,#06 ; this is yellow (#8004)
0006: out (#FE),a
Using BANK with offset, and ORG
You can combine the offset of the bank with ORG:
.bank 2, #100
.org #8000
call yellow
ret
yellow:
ld a,6
out (#fe),a
ret
The output is probably different from the one you expect:
0100: call #8004 ; yellow
0103: ret
0104: ld a,#06 ; this is yellow (#8004)
0106: out (#FE),a
As you can see, the code stream is the same as in the previous case; however, here, the code starts at offset #100.
Using multiple BANK directives
As you may need multiple memory banks in your program, you can use multiple BANK
pragmas, like in this example:
.bank 1
; Here goes the code for bank #1
; ...
.bank 3
; Here goes the code for bank #3
; ...
Restrictions with BANK
BANK
cannot have a label.BANK
cannot be used with the ZX Spectrum 48 model type.- The
BANK
value must be between 0 and 7 - The offset must be between 0 and 16383
- You can use the
BANK
pragma for a particular bank page only once, so, for example, the following code raises an error message:
.bank 1
; ...
.bank 3
; ...
.bank 1 ; This line raises the error
; ...
Note: This is a temporary restriction. In the future, it may be removed.
The EQU pragma
The EQU pragma allows you assign a value to an identifier. The label before EQU is the name of the identifier (or symbol), the exression used in EQU is the value of the variable. This is a short sample:
.org #6200
ld hl,Sym1
Sym1: .equ #4000
ld bc,Sym2
Sym2: .equ $+4
This sample is equivalent with this one:
.org #6200
ld hl,#4000 ; Sym1 <-- #4000
ld bc,#620a ; Sym2 <-- #620a as an ld bc,NNNN operation and
an ld hl,NNNN each takes 3 bytes
The VAR pragma
The VAR pragma works similarly to EQU. However, while EQU does not allow using the same symbol with mulitple value assignments, VAR assigns a new value to the symbol every time it is used.
The VAR pragma accepts extra syntax alternatives:
=
,:=
The INJECTOPT pragma
When you run the ZX Spectrum virtual machine from the IDE with the Run program command, it injects the machine code into the memory and sets up the system as if you started the code from BASIC with the RUN command. By default, it sets the cursor to “L” mode. However, in several cases, you’d like to keep the cursor in “K” mode, for example, when you intend to start the code with the RANDOMIZE USER addr command (here, addr is the entry address). In this case, you can add the INJECTOP pragma to the code:
.injectopt cursork
The INJECTOPT pragma expects an identifier-like option tag after the starting pragma keyword, such as cursork
above. Omitting that tag will cause a syntax error.
Though right now INJECTOPT supports only the
cursork
tag, it does not raise an exception with other tags, ignores them.
The DEFB pragma
The DEFB pragma emits 8-bit expressions (bytes) from the current assembly position. here is a sample:
.org #6000
.defb #01, #02, $, #04
The DEFB pragma will emit these four bytes starting at 0x6000: 0x01, 0x02, 0x03, 0x04.
The $
expression will emit 0x03, because at the emission point the current assembly
address is 0x6003. The DEFB program takes into account only the rightmost 8 bits of any
expression: this is how $
results in 0x03.
DEFB has extra syntax variants:
db
,.db
,DB
, and.DB
are accepted, too.
The DEFW pragma
The DEFW pragma is similar to DEFB, but it emits 16-bit values with LSB, MSB order.
.defw #1234, #abcd
This simple code above will emit these four bytes: 0x34, 0x12, 0xcd, 0xab.
DEFW has extra syntax variants:
dw
,.dw
,DW
, and.DW
are accepted, too.
The DEFM pragma
The DEFM pragma emits the byte-array representation of a string. Each character in the string is replaced with the correcponding byte. Take a look at this code:
.defm "\C by me"
Here, the DEFM pragma emits 7 bytes for the seven characters (the first escape sequence represents the copyrigh sign) : 0x7f, 0x20, 0x62, 0x69, 0x20, 0x6d, 0x65.
DEFM has extra syntax variants:
dm
,.dm
,DM
, and.DM
are accepted, too.
The DEFN pragma
The DEFN pragma works just like the DEFM
pragma but it emits an additional 0x00
byte to terminate the string. Look at this code:
.defn "\C by me"
Here, the DEFN pragma emits 8 bytes for the seven characters (the first escape sequence represents the copyrigh sign) plus the terminating zero: 0x7f, 0x20, 0x62, 0x69, 0x20, 0x6d, 0x65, 0x00.
DEFN has extra syntax variants:
dn
,.dn
,DN
, and.DN
are also accepted.
The DEFC pragma
The DEFC pragma works just like the DEFM
pragma but it sets Bit 7 of the last
emitted character. Look at this code:
.defc "\C by me"
Here, the DEFC pragma emits 7 bytes for the seven characters (the first escape sequence represents the copyrigh sign) with Bit 7 of the last character (0x65) set (so it become 0xE5): 0x7f, 0x20, 0x62, 0x69, 0x20, 0x6d, 0xE5.
DEFC has extra syntax variants:
dc
,.dc
,DC
, and.DC
are also accepted.
The DEFH pragma
The DEFH pragma uses a string with even number of hexadecimal digits to emits a byte-array representation of the input. Each character pair in the string is replaced with the correcponding byte. Take a look at this code:
.defh "12E4afD2"
Here, the DEFH pragma emits 4 bytes: 0x12, 0xe4, 0xaf, 0xd2.
DEFH has extra aliases:
dh
,.dh
,DH
, and.DH
.
The DEFS pragma
You can emit zero (0x00
) bytes with this pragma. It accepts a single argument,
the number of zeros to emit. This code sends 16 zeros to the generated output:
.defs 16
DEFS has extra syntax variants:
ds
,.ds
,DS
, and.DS
are also accepted.
The FILLB pragma
With FILLB, you can emit a particular count of a specific byte. The first argument
of the pragma sets the count, the second specifies the byte to emit. This code emits 24
bytes of #A5
values:
.fillb 24,#a5
The FILLW pragma
With FILLW, you can emit a particular count of a specific 16-bit word. The first argument
of the pragma sets the count, the second specifies the word to emit. This code emits 8
words (16 bytes) of #12A5
values:
.fillw 8,#12a5
Of course, the bytes of a word are emitted in LSB/MSB order.
The SKIP pragma
The SKIP pragma — as its name suggests — skips the number of bytes as specified in its argument. It fills up the skipped bytes with 0xFF.
The EXTERN pragma
The EXTERN pragma is kept for future extension. The current compiler accepts it, but does not do any action when observing this pragma.
The MODEL pragma
This pragma is used when you run or debug your Z80 code within the emulator. With Spectrum 128K, Spectrum +3,
and Spectrum Next models, you can run the Z80 code in differend contexts. The MODEL pragma lets you
specify on which model you want to run the code. You can use the SPECTRUM48
, SPECTRUM128
,
SPECTRUMP3
, or NEXT
identifiers to choose the model (identifiers are case-insensitive):
.model Spectrum48
.model Spectrum128
.model SpectrumP3
.model Next
For example, when you create code for Spectrum 128K, and add the .model Spectrum48
pragma to the code,
the Run Z80 Code command will start the virtual machine, turns the machine into Spectrum 48K mode, and ignites
the code just after that.
Note: With the #ifmod
and #ifnmod
directives, you can check the model type. For example, the following
Z80 code results green background on Spectrum 48K, cyan an Spectrum 128K:
.model Spectrum48
#ifmod Spectrum128
BorderColor: .equ 5
RetAddr: .equ #2604
#else
BorderColor: .equ 4
RetAddr: .equ #12a2
#endif
Start:
.org #8000
ld a,BorderColor
out (#fe),a
jp RetAddr
The ALIGN pragma
This pragma allows you to align the current assembly counter to the specified byte boundary. You can use this pragma with an optional expression. Look at these samples:
.org #8000
nop
.align 4
nop
.align
The first pragma aligns the assembly counter to #8004, as this one is the next 4-byte boundary.
With no value specified, .align
uses #100, and thus the second .align
in the sample sets
the current assembly counter to the next page boundary, #8100.
The TRACE and TRACEHEX pragmas
These pragmas send trace information to the assembler output. In the Visual Studio IDE, these
messages are displayed in the Z80 Build Output window pane. You can list one or more expressions
separated by a comma after the .trace
token. TRACEHEX works just like TRACE, but id displays
integer numbers and strings in hexadecimal format.
Let’assume, you add these lines to the source code:
.trace "Hello, this is: ", 42
.tracehex "Hello, this is: ", 42
When you compile the source, the lines above display these messages:
TRACE: Hello, this is: 42
TRACE: 48656C6C6F2C20746869732069733A20002A
The RNDSEED pragma
With the rnd()
function, you can generate random numbers. The RNDSEED pragma sets the seed
value to use for random number generation. If you use this pragma with an integer expression,
the seed is set to tha value of that expression. If you do not provide the expression, the compiler
uses the system clock to set up the seed.
.rndseed ; sets the seed according to the system clock
.rndseed 123 ; sets the seed to 123
The DEFGX pragma
This pragma helps you define bitmaps in the code. This pragma excepts a string expression and utilizes that string as a pattern to generate bytes for the bitmap.
DEFGX has extra syntax variants:
dgx
,.dgx
,DGX
, and.DGX
are accepted, too.
If the very first character of the string pattern is <
, the pattern is left aligned,
and starts with the second character. Should the first character be >
, the pattern is
right aligned and starts with the second character. By default, (if no <
or >
is used)
the pattern is left-aligned.
Any space within the pattern are ignored, taken into account as helpers. Other characters are converted into bits one-by-one.
Before the conversion, the pragma checks if the pattern constitutes multiples of 8 bits. If not, it uses zero bit prefixes (right-aligned), or zero-bit suffixes (left-aligned) so that the pattern would be adjusted to contain entire bytes.
The .
(dot), -
(dash), and _
(underscore) sign 0, any other characters stand for 1.
Every 8 bits in the pattern emit a byte.
Here are a few samples:
.dg "....OOOO" ; #0F
.dg ">....OOOO" ; #0F
.dg "<----OOOO" ; #0F
.dg "___OOOO" ; #1E
.dg "....OOOO ..OO" ; #0F, #30
.dg ">....OO OO..OOOO" ; #03, #CF
The DEFG pragma
This pragma helps you define bitmaps in the code. This pragma excepts a string pattern (note: not a string expression!) and utilizes that string as a pattern to generate bytes for the bitmap.
DEFG has extra syntax variants:
dg
,.dg
,DG
, and.DG
are also accepted.
Any space within the pattern are ignored, taken into account as helpers. Other characters are converted into bits one-by-one. The pixels in a byte are planted with the LHS as the most-significant bit, and multiple bytes are planted LHS byte first.
The .
(dot), -
(dash), and _
(underscore) sign 0, any other characters stand for 1.
Every 8 bits in the pattern emit a byte.
Here are a few samples:
.dg ....OOOO ; #0F
.dg ___OOOO ; #1E
.dg ....OOOO ..OO" ; #0F, #30
.dg ....OO OO..OOOO ; #0F, #3C
Please note, unlinke in the pattern used with
DEFGX
, here, the leading>
and<
characters are taken as bit 1. They do not specify bit alignment.
The ERROR Pragma
You can raise custom error messages with this pragma. ERROR accepts an expression
and displays an error message with code Z0500
using the text you provide. Here is a sample:
.error "The value must be greater than" + str(minvalue)
The INCLUDEBIN Pragma
You can include a binary file into the source code to emit all bytes just as if you used the
.defb
pragma. You can include the entire file, or a single segment of it. The pragma has a
mandatory argument — the name of the binary file to include — and two optional
ones, the start offset of the segment, and its length, respectively. Let’s see a few examples:
.includebin "./myfile.bin"
.includebin "./myfile.bin" 2
.includebin "./myfile.bin" 2, 3
This snippet loads the myfile.bin
file from the same directory that contains the source
with the .includebin
directive.
Let’s assume that myfile.bin
contains these bytes:
#00, #01, #02, #03, #04, #05, #06, #07
The three lines of code above are the same as if we had written these code lines:
.defb #00, #01, #02, #03, #04, #05, #06, #07 ; .includebin "./myfile.bin"
.defb #02, #03, #04, #05, #06, #07 ; .includebin "./myfile.bin" 2
.defb #02, #03, #04 ; .includebin "./myfile.bin" 2, 3
Of course, the compiler does not allow negative file offset or length. It alse raises an error if you define a segment that does not fit into the binary file.
You can use alternative syntax for.includebin
. The compiler accepts these tokens and their uppercase versions, too:includebin
,.include_bin
, andinclude_bin
.
The COMPAREBIN pragma
When you are re-engineering a Z80 program from an exported disassembly, it is good to know that you do not break the original code. The .comparebin
pragma helps you to check that you still compile what you expect. It loads a binary file and compares that file with the output of the current code segment.
The pragma has a mandatory argument — the name of the binary file to include — and two optional ones, the start offset of the segment, and its length, respectively. Let’s see a few examples:
.comparebin "./myfile.bin"
.comparebin "./myfile.bin" 2
.comparebin "./myfile.bin" 2, 3
Of course, the compiler does not allow negative file offset or length. It also raises an error if you define a segment that does not fit into the binary file.
You can use alternative syntax for.comparebin
. The compiler accepts these tokens, too:comparebin
,.COMPAREBIN
, andCOMPAREBIN
.
When you compile the code, every .org
pragma opens a new segment that starts from the point defined by .org
.
You can put it into the code in as many places as you want. As the compiler parses the code, it records the positions of .comparebin
pragmas, the current output segment and its length at the point where .comparebin
is used. When the code compilation is ready — and there are no errors —, the compiler executes a check.
This check compares the emitted bytes with the recorded length to the bytes in the binary file.
- If the length of the segment is greater than the size of the file, the compiler raises an error.
- The comparison checks only the as many bytes as are in the output segment; if there are more bytes in the binary file, the remaining data is ignored.
- If the compared data do not match, the assembler raises an error with the first unmatching position.
Let’s assume, we have the origin.bin
file that contains these six bytes:
#00, #01, #02, #03, #04, #05
Take a look at this code:
.org #8000
.defb #00, #01, #02
.comparebin "origin.bin"
.org #8100
.defb #03, #04, #05
.comparebin "origin.bin"
.comparebin "origin.bin", 3
This code contains two segments (it has two .org
pragmas) and three .comparebin
.
- Though
origin.bin
has six bytes, the first comparison succeeds, as it utilizes only the three bytes emitted in the first segment. - The second comparison fails, as the file starts with #00, #01, #02, while the segment emits #03, #04, and #05.
- The third comparison succeeds, as it starts the examination from the 4th byte (offset 3) of the binary file.