Macros may be thought of as "parametrized super instructions". Macros are sequences of tokens that have a name. If that name is used in the source file, the macro is "expanded", that is, it is replaced by the tokens that were specified when the macro was defined.
In it's simplest form, a macro does not have parameters. Here's an example:
.macro asr ; Arithmetic shift right
cmp #$80 ; Put bit 7 into carry
ror ; Rotate right with carry
.endmacro
The macro above consists of two real instructions, that are inserted into the code, whenever the macro is expanded. Macro expansion is simply done by using the name, like this:
lda $2010
asr
sta $2010
When using macro parameters, macros can be even more useful:
.macro inc16 addr
clc
lda addr
adc #$01
sta addr
lda addr+1
adc #$00
sta addr+1
.endmacro
When calling the macro, you may give a parameter, and each occurrence of the name "addr" in the macro definition will be replaced by the given parameter. So
inc16 $1000
will be expanded to
clc
lda $1000
adc #$01
sta $1000
lda $1000+1
adc #$00
sta $1000+1
A macro may have more than one parameter, in this case, the parameters are separated by commas. You are free to give less parameters than the macro actually takes in the definition. You may also leave intermediate parameters empty. Empty parameters are replaced by empty space (that is, they are removed when the macro is expanded). If you have a look at our macro definition above, you will see, that replacing the "addr" parameter by nothing will lead to wrong code in most lines. To help you, writing macros with a variable parameter list, there are some control commands:
.IFBLANK
tests the rest of the line and
returns true, if there are any tokens on the remainder of the line. Since
empty parameters are replaced by nothing, this may be used to test if a given
parameter is empty.
.IFNBLANK
tests the
opposite.
Look at this example:
.macro ldaxy a, x, y
.ifnblank a
lda #a
.endif
.ifnblank x
ldx #x
.endif
.ifnblank y
ldy #y
.endif
.endmacro
This macro may be called as follows:
ldaxy 1, 2, 3 ; Load all three registers
ldaxy 1, , 3 ; Load only a and y
ldaxy , , 3 ; Load y only
There's another helper command for determining, which macro parameters are
valid:
.PARAMCOUNT
This command is
replaced by the parameter count given, including intermediate empty macro
parameters:
ldaxy 1 ; .PARAMCOUNT = 1
ldaxy 1,,3 ; .PARAMCOUNT = 3
ldaxy 1,2 ; .PARAMCOUNT = 2
ldaxy 1, ; .PARAMCOUNT = 2
ldaxy 1,2,3 ; .PARAMCOUNT = 3
Macro parameters may optionally be enclosed into curly braces. This allows the inclusion of tokens that would otherwise terminate the parameter (the comma in case of a macro parameter).
.macro foo arg1, arg2
...
.endmacro
foo ($00,x) ; Two parameters passed
foo {($00,x)} ; One parameter passed
In the first case, the macro is called with two parameters: '($00
'
and 'x)'. The comma is not passed to the macro, since it is part of the
calling sequence, not the parameters.
In the second case, '($00,x)' is passed to the macro, this time including the comma.
Sometimes it is nice to write a macro that acts differently depending on the
type of the argument supplied. An example would be a macro that loads a 16 bit
value from either an immediate operand, or from memory. The
.MATCH
and
.XMATCH
functions will allow you to do exactly this:
.macro ldax arg
.if (.match (.left (1, {arg}), #))
; immediate mode
lda #<(.right (.tcount ({arg})-1, {arg}))
ldx #>(.right (.tcount ({arg})-1, {arg}))
.else
; assume absolute or zero page
lda arg
ldx 1+(arg)
.endif
.endmacro
Using the
.MATCH
function, the macro is able to
check if its argument begins with a hash mark. If so, two immediate loads are
emitted, Otherwise a load from an absolute zero page memory location is
assumed. Please note how the curly braces are used to enclose parameters to
pseudo functions handling token lists. This is necessary, because the token
lists may include commas or parens, which would be treated by the assembler
as end-of-list.
The macro can be used as
foo: .word $5678
...
ldax #$1234 ; X=$12, A=$34
...
ldax foo ; X=$56, A=$78
Macros may be used recursively:
.macro push r1, r2, r3
lda r1
pha
.if .paramcount > 1
push r2, r3
.endif
.endmacro
There's also a special macro to help writing recursive macros:
.EXITMACRO
This command will stop macro expansion
immediately:
.macro push r1, r2, r3, r4, r5, r6, r7
.ifblank r1
; First parameter is empty
.exitmacro
.else
lda r1
pha
.endif
push r2, r3, r4, r5, r6, r7
.endmacro
When expanding this macro, the expansion will push all given parameters until an empty one is encountered. The macro may be called like this:
push $20, $21, $32 ; Push 3 ZP locations
push $21 ; Push one ZP location
Now, with recursive macros,
.IFBLANK
and
.PARAMCOUNT
, what else do you need?
Have a look at the inc16 macro above. Here is it again:
.macro inc16 addr
clc
lda addr
adc #$01
sta addr
lda addr+1
adc #$00
sta addr+1
.endmacro
If you have a closer look at the code, you will notice, that it could be written more efficiently, like this:
.macro inc16 addr
inc addr
bne Skip
inc addr+1
Skip:
.endmacro
But imagine what happens, if you use this macro twice? Since the label "Skip" has the same name both times, you get a "duplicate symbol" error. Without a way to circumvent this problem, macros are not as useful, as they could be. One solution is, to start a new lexical block inside the macro:
.macro inc16 addr
.proc
inc addr
bne Skip
inc addr+1
Skip:
.endproc
.endmacro
Now the label is local to the block and not visible outside. However,
sometimes you want a label inside the macro to be visible outside. To make
that possible, there's a new command that's only usable inside a macro
definition:
.LOCAL
. .LOCAL
declares one
or more symbols as local to the macro expansion. The names of local variables
are replaced by a unique name in each separate macro expansion. So we could
also solve the problem above by using .LOCAL
:
.macro inc16 addr
.local Skip ; Make Skip a local symbol
clc
lda addr
adc #$01
sta addr
bcc Skip
inc addr+1
Skip: ; Not visible outside
.endmacro
Starting with version 2.5 of the assembler, there is a second macro type
available: C style macros using the .DEFINE
directive. These macros are
similar to the classic macro type described above, but behaviour is sometimes
different:
.DEFINE
may not
span more than a line. You may use line continuation (see
.LINECONT
) to spread the definition over
more than one line for increased readability, but the macro itself
may not contain an end-of-line token.
.DEFINE
share
the name space with classic macros, but they are detected and replaced
at the scanner level. While classic macros may be used in every place,
where a mnemonic or other directive is allowed,
.DEFINE
style macros are allowed anywhere in a line. So
they are more versatile in some situations.
.DEFINE
style macros may take
parameters. While classic macros may have empty parameters, this is
not true for
.DEFINE
style macros.
For this macro type, the number of actual parameters must match
exactly the number of formal parameters.
To make this possible, formal parameters are enclosed in braces when
defining the macro. If there are no parameters, the empty braces may
be omitted.
.DEFINE
style macros may not
contain end-of-line tokens, there are things that cannot be done. They
may not contain several processor instructions for example. So, while
some things may be done with both macro types, each type has special
usages. The types complement each other.
Let's look at a few examples to make the advantages and disadvantages clear.
To emulate assemblers that use "EQU
" instead of "=
" you may use the
following .DEFINE
:
.define EQU =
foo EQU $1234 ; This is accepted now
You may use the directive to define string constants used elsewhere:
; Define the version number
.define VERSION "12.3a"
; ... and use it
.asciiz VERSION
Macros with parameters may also be useful:
.define DEBUG(message) .out message
DEBUG "Assembling include file #3"
Note that, while formal parameters have to be placed in braces, this is not true for the actual parameters. Beware: Since the assembler cannot detect the end of one parameter, only the first token is used. If you don't like that, use classic macros instead:
.macro message
.out message
.endmacro
(This is an example where a problem can be solved with both macro types).
When using the
-t option, characters are translated
into the target character set of the specific machine. However, this happens
as late as possible. This means that strings are translated if they are part
of a
.BYTE
or
.ASCIIZ
command. Characters are translated as soon as they are
used as part of an expression.
This behaviour is very intuitive outside of macros but may be confusing when doing more complex macros. If you compare characters against numeric values, be sure to take the translation into account.