Klive Z80 Assembler
Pragmas

Pragmas

The compiler understands several pragmas that — thought they are not Z80 instructions — they influence the emitted code. Each pragma has two alternative syntax constructs, one with a dot prefix and another without.

For example, you can write ORG or .ORG to use the ORG pragma.

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 segments, each with one emitted byte representing the corresponding LD operation. The first segment will start at 0x8000 (default), the second at 0x8100, and 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 cause an error message

The ENT pragma

The ENT pragma defines the entry code of the program when you start it. 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 instruction as the entry address of the code. Should you omit the ENT pragma from this code, the entry point would be0x6200, as that 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 Code, which allows you to create a LOAD block that automatically starts the code. When you run the code from the IDE, the address specified with the ENT pragma is used. However, the auto LOAD block uses the RANDOMIZE USR address pattern, and you may 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 from the IDE. Nonetheless, the exported code 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 representing 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 (opens in a new tab).)

The BANK pragma lets you declare that you want to put the Z80 Assembly code in a specific memory bank. When you export the compiled output, the Export code command of the IDE 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 Klive 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 by 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 start 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 we're wasting the first 256 bytes of the page, the Export 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 is the code for bank #1
; ...
.bank 3
; Here is 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 to assign a value to an identifier. The label before EQU is the name of the identifier (or symbol), and the expression used in EQU is the variable's value. Here is a short example:

      .org #6200
      ld hl,Sym1
Sym1: .equ #4000
      ld bc,Sym2
Sym2: .equ $+4

This sample is equivalent to 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 the use of the same symbol with multiple value assignments, VAR assigns a new value to the symbol every time it is used.

Note: The VAR pragma accepts extra syntax alternatives: =, :=

The INJECTOPT pragma

The INJECTOPT pragma expects an identifier-like option tag after the starting pragma keyword. This keyword identifies an option for injecting the code into an emulated machine. The Klive Assembler supports two options:

cursork

When you run the ZX Spectrum virtual machine from the IDE, 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

subroutine

This option instructs the IDE to call your code (terminated with RET) and not to jump directly to its start address.

Note: You can use any other options; the compiler will not raise an exception; it ignores the unknown options.

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 considers 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 corresponding 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 copyright 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 copyright sign) plus the terminating zero: 0x7f, 0x20, 0x62, 0x69, 0x20, 0x6d, 0x65, 0x00.

Note: 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 copyright sign) with Bit 7 of the last character (0x65) set (so it becomes 0xE5): 0x7f, 0x20, 0x62, 0x69, 0x20, 0x6d, 0xE5.

Note: DEFC has extra syntax variants: dc, .dc, DC, and .DC are also accepted.

The DEFH pragma

The DEFH pragma uses a string with an even number of hexadecimal digits to emit a byte-array representation of the input. Each character pair in the string is replaced with the corresponding byte. Take a look at this code:

.defh "12E4afD2"

Here, the DEFH pragma emits 4 bytes: 0x12, 0xe4, 0xaf, 0xd2.

Note: 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

Note: 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, and 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 16-bit word. The first argument of the pragma sets the count, and 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 from the current address to that specified in the first argument. It fills up the skipped bytes with 0xFF by default, but the fill value can be set with the second argument:

.skip $+#05      ; fills next 5 bytes with 0xFF
.skip $+#04, #3a ; fills next 4 bytes with 0x3A

The EXTERN pragma

The EXTERN pragma is kept for future extension. The current compiler accepts it but does not act 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 different contexts. The MODEL pragma lets you specify which model 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 command will start the virtual machine, turn the machine into Spectrum 48K mode, and ignite 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 in a green background on Spectrum 48K and cyan on 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, 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 pane. List one or more expressions separated by a comma after the .trace token. TRACEHEX works like TRACE, but it displays integer numbers and strings in hexadecimal format.

Let's 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 the 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.

Note: 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.

Spaces within the pattern are ignored and considered 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 zeros as prefixes (right-aligned) or zeros as suffixes (left-aligned), so the pattern would be adjusted to contain entire bytes.

The . (dot), - (dash), and _ (underscore) sign 0, and any other characters stand for 1. Every 8 bits in the pattern emit a byte.

Here are a few samples:

.dgx "....OOOO"         ; #0F
.dgx ">....OOOO"        ; #0F
.dgx "<----OOOO"        ; #0F
.dgx "___OOOO"          ; #1E
.dgx "....OOOO ..OO"    ; #0F, #30
.dgx ">....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.

Note: DEFG has extra syntax variants: dg, .dg, DG, and .DG are also accepted.

Spaces within the pattern are ignored and considered helpers. Other characters are converted into bits one by one. The pixels in a byte are planted with the LSB as the most significant bit, and multiple bytes are planted LSB byte first.

The . (dot), - (dash), and _ (underscore) sign 0, and 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

Note: Unlike 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 your provided text. 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 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

Note: The compiler does not allow negative file offset or length. It else raises an error if you define a segment that does not fit into the binary file. You can use an alternative syntax for .includebin. The compiler accepts these tokens and their uppercase versions, too: includebin, .include_bin, and include_bin.

The COMPAREBIN pragma

When 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

Note: 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 more are 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, utilizing 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.