SpectNet IDE

Visual Studio 2017/2019 integrated ZX Spectrum IDE for the Community

Z80 Assembler » Pragmas

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, and include_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, and COMPAREBIN.

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.