The SpectNetIDE assembler provides you a powerful way to declare macros, and apply them in the code. While in most programming languages and assemblers the macros are preprocessor constructs and use simple text replacement, the SpectNetIDE implementation is different.
Unlike in C, C++ (and some Z80 Assemblers), SpectNetIDE macros emit only code (through instructions and pragmas), they cannot be used as user-defined functions. When you pass parameters to macros, any expression in the parameters is evaluated instantly, so you cannot use unknown symbols or variables — ones that will get their values only somewhere later in the code.
Getting Started with Macros
The best way to show you what macros can do is real code. Let’s start with a simple parameterless macro:
Delay:
.macro()
DelayLoop:
djnz DelayLoop
.endm
You can easily use this macro in your code:
ld b,#24
Delay()
; ...and later
ld b,#44
Delay()
The assembler will emit the code like this:
ld b,#24
DelayLoop_1: djnz DelayLoop_1
; ...and later
ld b,#44
DelayLoop_2: djnz DelayLoop_2
As you expect, it takes care that the DelayLoop
label remains local within the scope of the macro; otherwise
it would lead to a duplicated label name.
This macro is named Delay
, and it uses the value of the B register to create a djnz
loop.
You can easily apply this macro
Now, let’s enhance this macro with an argument:
Delay:
.macro(wait)
ld b,{{wait}}
DelayLoop:
djnz DelayLoop
.endm
As the body of the macro suggests, `` is a placeholder for the wait
argument. While other assemblers do not
use a separate markup for a placeholder — they’d just use wait
— SpectNetIde applies this markup for
two reasons: first, it is visually better and more eye-catching; second, it allows the compiler to provide better
performance.
You can use this macro passing an argument value for wait
:
Delay(#24)
Delay(d)
As you expect, the compiler now emits this code:
ld b,#24
DelayLoop_1: djnz DelayLoop_1
; ...and later
ld b,d
DelayLoop_2: djnz DelayLoop_2
Macros allow you to pass anything that could be an operand in a Z80 instruction, so this is entirelly valid:
Delay((ix+23))
As you can imagine, this macro invocation results as if you wrote this:
ld b,(ix+23)
DelayLoop: djnz DelayLoop
SpectNetIDE macros do not stop here. You can define macros that recveive an entire Z80 instruction as an argument:
RepeatLight:
.macro(count, body)
ld b,{{count}}
DelayLoop:
{{body}}
djnz DelayLoop
.endm
This macro is to repeat the body
in count
number of times. This is how you can invoke it:
RepeatLight(4, "add a,c")
Observe, the second argument of the macro is a string that names the add a,c
operation. The result of this
macro is this set of instructions:
ld b,4
DelayLoop_1:
add a,c
djnz DelayLoop_1
Instead of a run time loop, you can apply a compile time loop within the macro:
RepeatLight:
.macro(count, body)
.loop {{count}}
{{body}}
.endl
.endm
The RepeatLight(3, "add a,c")
line invokes the macro and the macro’s body translates to this:
.loop 3
add a,c
.endl
As you already learned, the compiler handles this as if you wrote:
add a,c
add a,c
add a,c
SpectNetIDE allows you to pass a set of lines as a macro argument. You can invoke RepeatLight
like this:
RepeatLigth(3, "add a,c" & "add a,10")
Or, you can make it with variables:
FirstOp = "add a,c"
SecondOp = "add a,10"
RepeatLight(3, FirstOp & SecondOp)
The
&
operator between the two string value concatenates them with a next line character set (#0A and #0D). If you’d apply the+
operator, the above code would fail: the assembler accepts only a single instruction in a text line, and would reject multiple instructions.
In the context of macros, you can use several special functions, such as lreg()
and hreg()
. These work during
parse time, and retrieve the lower register, and higher register of an 16-bit register pair:
LdHl:
.macro(reg16)
ld h,hreg({{reg16}})
ld l,lreg({{reg16}})
.endm
Here, you can apply the LdHl
macro like this:
LdHl(de)
LdHl(bc)
The compiler translates these macro invocations into these Z80 instructions:
ld h,d
ld l,e
ld h,b
ld l,c
When you invoke a macro, you can pass less parameters than the macro declares. Within the macro body,
you can use the def()
function to check whether the specified parameter has been passed:
Push:
.macro(r1, r2, r3, r4)
.if def({{r1}})
push {{r1}}
.endif
.if def({{r2}})
push {{r2}}
.endif
.if def({{r3}})
push {{r3}}
.endif
.if def({{r4}})
push {{r4}}
.endif
.endm
The Push
macro in this code snippet allows you to create a push
instruction for up to 4 register pairs.
Look at these usages:
Push(af)
; ... and later
Push(bc, de, ix)
This is what the compiler generates:
push af
; ... and later
push bc
push de
push ix
You can opt to not pass a macro parameter for a specific argument. Look at this macro declaration:
LdBcDeHl:
.macro(bcVal, deVal, hlVal)
.if def({{bcVal}})
ld bc,{{bcVal}}
.endif
.if def({{deVal}})
ld de,{{deVal}}
.endif
.if def({{hlVal}})
ld hl,{{hlVal}}
.endif
.endm
You can invoke this macro in these ways, leaving a parameter empty to sign that you do not intend to use it:
LdBcDeHl(,#1000,#2000)
; ... and later
LdBcDeHl(#3000,,#4000)
The compiler understands your intention and generates this output:
ld de,#1000
ld hl,#2000
; ... and later
ld bc,#3000
ld hl,#4000
It’s time to deep into the nitty-gritty details of creating and using macros in SpectNetIDE.
Macro Declaration
Macros must have a name. Each macro is named according to the label preceding its declaration either in the same
line as the .macro
token, or before it as a hanging label. Macros can have zero, one, or more named arguments
separated with a comma. The macro declaration is closed with the .endm
token:
MacroWithHangingLabel:
.macro(myParam, otherParam)
; Macro body
.endm
MyMacro: macro()
; Macro body
.endm
Even if a macro does not have arguments, its declaration must contain the parentheses.
The macro body can contain Z80 instructions, pragmas, or statements. The only exception are the ENT and XENT pragmas.
As you already experienced, the assembler supports syntax variants for the macro-related keywords. The compiler
accepts these tokens: .macro
, macro
, .MACRO
, MACRO
, .endm
, endm
, .ENDM
, ENDM
, .mend
,
mend
, .MEND
, and MEND
.
Within the macro’s body, you can refer to the arguments of the macros wrapping them into double curly braces:
Mul10:
.macro(reg8)
push af
ld a,{{reg8}}
add a,a
push bc
ld b,a
add a,a
add a,a
add a,b
pop bc
ld {{reg8}},a
.endm
Arguments are identifiers, thus the corresponding naming rules are applied to them. You cannot use a reserved word
(for example a mnemonic like ldir
or a register name like hl
) as a macro argument.
Macro Parameters
You can invoke a macro with as many parameters as many argument its declaration has, or even with less parameters. If the macro invocation has more parameters than arguments, the compiler raises an error.
Let’s assume, you’ve created this macro declaration:
MyMacro: .macro(arg1, arg2, arg2)
; Macro body
.endm
All of these usages are valid:
MyMacro()
MyMacro(a)
MyMacro(a, b)
MyMacro(a, b, c)
Nonetheless, these usage is invalid since it passes more than three parameters:
MyMacro(a, b, c, d) ; ERROR: To many parameters
Sometimes it is convenient to omit not the last parameters but one in the beginning or the middle of the
parameter list. You can do that: an empy comma separator signs that the preceeding parameter is empty. Using this
notation, all these invocations of MyMacro
is valid:
MyMacro(,b)
MyMacro(a,,c)
MyMacro(,,)
Within the macro declaration, you can use the def()
function to check if a particular argument has a value.
LdBcDeHl:
.macro(bcVal, deVal, hlVal)
.if def({{bcVal}})
ld bc,{{bcVal}}
.endif
.if def({{deVal}})
ld de,{{deVal}}
.endif
.if def({{hlVal}})
ld hl,{{hlVal}}
.endif
.endm
The def()
function accepts only a macro argument reference (the name of the argument wrapped in double curlay braces).
This function evaluates to true only when the macro argument is not empty.
You can use the logical NOT operator (!
) combined to def()
to check if an argument is empty.
MyMacro: .macro(arg)
.if !def({{arg}})
; generate something for empty arg
.endif
.endm
Passing Parameters to Macros
You can pass anything as a macro parameter that is a valid operand of a Z80 instruction. This means the following options:
- Names of 8-bit registers and 16-bit register pairs (e.g.
a
,b
,ixl
,hl
sp
,af
, etc.) - Names of conditions (e.g.
z
,nz
,pe
,m
, etc.) - Memory address indirection (e.g.
(#4000)
,(#4000+#20)
) - Register pair indirection (e.g.
(bc)
,(de)
,(hl)
, etc.) - Indexed indirection (e.g.
(ix+#20)
,(iy-12)
, etc.) - C-port (
(c)
) - Expression (e.g.
(MyId << 1) + 23
,#4000
,12*sin(pi()/4)
,"ld " + "a,b"
, etc.)
You should be careful when you use parentheses in expressions. Let’s assume, you declare this macro:
SetHlValue:
.macro(value)
ld hl,{{value}}
.endm
When you use it, the first invocation uses an expression, the second has a memory address indirection:
SetHlValue(#4000+#20)
SetHlValue((#4000+#20))
The compiler translates them to these instructions:
ld hl,#4020
ld hl,(#4020)
To avoid such issues, you can use the square brackets to group parts of expressions. When you invoke the
SetHlValue
macro with this way, both usage with generate a ld hl,#4020
statement:
SetHlValue(#4000+#20)
SetHlValue([#4000+#20])
When you pass parameters to macros, any expression in the parameters is evaluated instantly, so you cannot use unknown symbols or variables — ones that will get their values only somewhere later in the code.
The compiler replaces the macro argument references to their current values passed in parameters. Whenever you use an expression, its value is converted into a string and put into the place of the macro argument.
Passing Instructions in a Macro Parameter
Within a macro declaration, you can use macro argument reference in stead of an entire Z80 instruction. Take a look at this macro:
ShortDi:
.macro(body)
di
{{body}}
ei
.endm
Here, the body
argument is expected to get something that the compiler can understand as an entire
instruction. When you invoke the macro, you need to pass a string expression so that the compiler can
replace the `` reference. Here is an example:
ShortDi("in a,(#fe)")
As you expect, the compiler generates this output:
di
in a,(#fe)
ei
You are not obliged to use Z80 instructions, the compiler accepts pragmas, too:
ShortDi(".db #00")
Well, the output is not pretty useful, nonetheless, the compiler generates this:
di
.db #00
ei
Passing Multiple Lines in a Macro Parameter
If you can pass multiple lines in a macro parameter where the corresponding
argument reference is used in stead of an entire instruction line, the compiler
will apply all those lines. To do that, the individual lines should be separated
by new line characters (\r\n
). The &
operator, when applied for two strings,
does this step for you, as it concatenates the two strings with \r\n
between
them. Let’s assume, you invoke the ShortDi
macro with this code:
ShortDi("ld a,#7f" & "in a,(#fe)")
Now, the compiler will generate this output:
di
ld a,#7f
in a,(#fe)
ei
Because you can pass expressions as macro parameters, you can invoke the macro in this way, too:
FirstOp = "ld a,#7f"
SecondOp = "in a,(#fe)"
ShortDi(FirstOp & SecondOp)
You can pass not only instructions and pragmas to macros, but also statements:
LoopOp = ".loop 3" & "nop" & ".endl"
ShortDi(LoopOp)
The compiler will emit this code:
di
nop
nop
nop
ei
Labels, Symbols, and Variables in Macros
Macros have a local scope for all labels, symbols, and variables created within their body, including the label
attached to the .endm
statement. The name of the macro is a label that also represents the start of the macro.
Take a look at this macro definition:
GetBoundaries:
.macro(instr)
{{instr}}
ld de,EndLabel
ld hl,GetBoundaries
EndLabel:
.endm
Here, the ld hl,GetBoundaries
instruction fills HL with the start address of the macro, while the
ld de,EndLabel
instruction puts the address of the next instruction following the macro int DE
Let’s assume, you use the macro this way:
.org #8000
GetBoundaries("nop")
GetBoundaries("ld ix,#ABCD")
The compiler will create this output:
#8000 GetBoundaries_1 nop
#8001 ld de,#8007 ; EndLabel_1
#8004 ld hl,#8000 ; GetBoundaries_1
#8007 EndLabel_1
GetBoundaries_2 ld ix,#ABCD
#800B ld de,#8011 ; Endlabel_2
#800E ld hl,#8007 ; GetBoundaries_2
#8011 EndLabel_2
Symbols and variables within the context work exactly as they do with loops. Do not forget: Symbols are constant values, while variables may change!
Invoking Macros from Macros
SpectNetIde allows you to invoke a macro from another macro, too. Here is a short sample:
Delay:
.macro(wait)
ld b,{{wait}}
WaitLoop: djnz WaitLoop
.endm
BorderPulse:
.macro(col1, wait1, col2, wait2)
ld a,{{col1}}
out (#fe),a
Delay({{wait1}})
ld a,{{col2}}
out (#fe),a
Delay({{wait2}})
.endm
Here, the BorderPulse
macro uses Delay
as a helper macro. The BorderPulse(2, 10, 3, 20)
invocation
produces this output:
ld a,2
out (#fe),a
ld b,10
WaitLoop_1: djnz WaitLoop_1
ld a,3
out (#fe),a
ld b,20
WaitLoop_2: djnz WaitLoop_2
Macro-Related Parse-Time Functions
The SpectNetIde Assembler allows using several parse-time functions with macro arguments the
similar way as you can use the def()
function to check whether a macro argument has been passed
to the macro invocation.
These functions check if the argument is an operand the name of the function suggest. Each of them returns true, provided the function recognizes the operand; otherwise, false.
The assembler support these functions:
Name | Description |
---|---|
isreg8std() |
The operand is an 8-bit register, one of these: a , b , c , d , e , h , l , i , r , xh (ixh ), xl (ixl ), yh (iyh ), or yl (iyl ) |
isreg8std() |
The operand is a standard 8-bit register, one of these: a , b , c , d , e , h , or l |
isreg8spec() |
The operand is a special 8-bit register, i , or r |
isreg8idx() |
One of these 8-bit index registers: xh (ixh ), xl (ixl ), yh (iyh ), or yl (iyl ) |
isreg16() |
Any of these 16-bit registers: af , bc , de , hl , sp , ix or iy |
isreg16std() |
Any of the standard 16-bit registers: bc , de , hl , or sp |
isreg16idx() |
Any of the ix or iy registers |
isregindirect() |
The operand is one of these: (bc) , (de) , (hl) , or (sp) |
isindexedaddr() |
The operand is an indexed address like (ix) , (iy) , (ix+#12) , (iy-#23) , and so on |
iscport() |
The operand is (c) (e.g. in the out (c),a instruction) |
iscondition() |
The operand is one of these conditions: z , nz , c , nc , po , pe , p , or m |
isexpr() |
The operand is an expression, for example: 1 + 2 , #1000 , myvalue + 23 , etc. |
isreg <reg> () |
The operand is the register as given in <reg> . You can use these names: a , af , b , c , bc , d , e , de , h , l , hl , i , r , xh , xl , ix , yh , yl , iy , and sp . For example, isrega() tests if the specified register is A. |
When you pass
'c'
as a macro argument, both theisreg8()
andiscondition()
parse-time functions accept it, as the'c'
token can be either an 8-bit register or a condition (carry flag is set).
Here is a short sample:
MyRegMacro: .macro(arg)
.if isreg8({{arg}})
ld a,{{arg}}
.else
.error "Only 8-bit registers are allowed"
.endif
.endm
MyRegMacro
allows using only an 8-bit register as its argument. If you provide another type of
parameter, the macro raises an error.