Assembler macros can be powerful tools that allow you to reuse sequences of code over and over. Creating a library of macros for your most commonly used functions will allow you to code faster and make less mistakes.
Unlike subroutines where the microcontroller will jump to a new section of code, the assembler will simply replace macros with the code it represents during assembly.
Macros are defined by placing code between the assembler directives myMacro can be defined asand . For example, a macro
.macro myMacro ... ; macro code .endmacro
Macros are very useful for blocks of code that need to be used in many programs that don't warrant a subroutine. For example, you may find that every program you write needs to have the Stack Pointer initialized. A macro for this can be written as
.macro setStack ldi r16,LOW(RAMEND) out SPL,r16 ldi r16,HIGH(RAMEND) out SPH,r16 .endmacro
Once a macro is defined, it can be used by simply placing the name of the macro in code
start: setStack ; initialize stack pointer ... ; program code loop: rjmp loop ; infinite loop
Passing Parameters to Macros
Macros can be written which accept parameters. When writing the code for such a macro, the first parameter passed is represented by @0, the second by @1, the third by @2, and so on... For example, the macro above can be redefined to accept a parameter to use as the Stack Pointer address.
.macro setStack ldi r16,LOW(@0) out SPL,r16 ldi r16,HIGH(@0) out SPH,r16 .endmacro
This macro can then be used as
start: setStack RAMEND ; initialize stack pointer ... ; program code loop: rjmp loop ; infinite loop
Perhaps you would like to add the register used to set The Stack Pointer as a parameter as well. This can be done as
.macro setStack ldi @1,LOW(@0) out SPL,@1 ldi @1,HIGH(@0) out SPH,@1 .endmacro
When a macro is called with multiple parameters, they must be separated with a comma.
start: setStack RAMEND,r16 ; initialize stack pointer ... ; program code loop: rjmp loop ; infinite loop
Conditional Directives And Error Handling
Conditional directives can be used to enhance the flexibility of your macros and catch errors. There are a number of conditional directives and outputs that you can use, shown in the table below.
|.ifndef||if not defined|
|.error||output an error|
|.message||output a message|
|.warning||output a warning|
For example, you may want to include a check that a passed parameter is within a valid range.
.macro setStack .if @0>RAMEND .error "Value greater than RAMEND used for setting stack" .else ldi @1,LOW(@0) out SPL,@1 ldi @1,LOW(@0) out SPL,@1 .endif .endmacro
Now if a value larger than the device's RAMEND is used, the assembler will generate an error and output a message at the command line indicating what the problem was.
Combining Macros and Subroutines
By placing subroutine calls in macro definitions we can create subroutines that are more flexible and easier to use.
For example, the delay10ms subroutine we wrote previously required loading a parameter into an input register before it was called. We can just make this part of a macro so the delay subroutine can be called in a single line. In addition, we can choose to save the registers we know the subroutine will modify by pushing them to the stack and restoring them when it is finished.
.macro delayms push r18 push r24 push r25 ldi r18,@0/10 rcall delay10ms pop r25 pop r24 pop r18 .endmacro
Now our subroutine is much easier to use.
delayms 500 ; delay for 500ms
Note that the input parameter for delay10ms represented how many times we would delay for 10ms. For example, to delay for 1s we would have to pass the parameter 100. By dividing it by 10 in the macro above, we have abstracted that detail. The macro lets us simply call the subroutine with the number of ms we want to delay.
If you will be using a macro in many different programs, you can create a new file to put the macro in and include it in any programs that need it. For example, the following could be placed in the file delayMacro.inc.
;************************************************************** ;* macro: delayms ;* ;* description: creates a delay for the specified number of ;* milliseconds ;* ;* inputs: @0 - number of milliseconds to delay for ;* ;* registers modified: none ;************************************************************** .macro delayms push r18 push r24 push r25 ldi r18,@0/10 rcall delay10ms pop r25 pop r24 pop r18 .endmacro
Using this, our program from the previous example can be simplified even further.
.include "m328pdef.inc" .include "delayMacro.inc" .def mask = r16 ; mask register .def ledR = r17 ; led register .def loopCt = r18 ; delay loop count .equ iVal = 39998 ; inner loop value .cseg .org 0x00 ldi r16,LOW(RAMEND) ; initialize out SPL,r16 ; stack pointer ldi r16,HIGH(RAMEND) ; to RAMEND out SPH,r16 ; " clr ledR ; clear led register ldi mask,(1<<PINB0) ; load 00000001 into mask register out DDRB,mask ; set PINB0 to output start: eor ledR,mask ; toggle PINB0 in led register out PORTB,ledR ; write led register to PORTB delayms 500 ; delay for 500ms rjmp start ; jump back to start .include "delay10ms.asm" ; include delay subroutine
Macros cannot be called before they are defined. Thus, you should not place their definitions in an include file that appears at the end of your code. Instead, you should create a separate include file for your macro definitions and place it at the beginning of your program.