Skip to content

Example Macros

Cabintech edited this page Jun 8, 2025 · 51 revisions

There are 2 basic uses for macros:

  1. Simple 'utility' style macros to reuse and parameterize small snippets of code either because some sequence of operations is very common, or to void common mistakes in complex operations (e.g. solve it once correctly and reuse). These types of macros can speed up code development by making useful abstractions and reduce the level of cognitive load required to write and update FXCode assembler. Well chosen macro names can make reading and understanding FXCore source code easier. Reuse of proven snippets of code reduces copy/paste errors and promotes shared code between programs.
  2. The other use case for macros is to wrap (and possibly parameterize) large blocks of program-specific functionality. Even if the code is not reused (e.g. the macro is invoked only once), it can create more structured and readable source code by abstracting large blocks of complex code. It is good programming practice for much the same reason as subroutines in a high level language, even if that subroutine is called only once. Because the FXCore processor cannot branch backwards (e.g. it is impossible to form a loop or a subroutine) it is sometimes necessary to duplicate large blocks of code. For example, if a complex function needs to be applied to 2 channels it may be necessary to duplicate the code block, once for each channel. By encapsulating the code block in a macro and parameterizing the channel-specific details, the code block only appears once in the source. Bug fixes or enhancements in that code will always be included in both channels without any potential for copy/paste errors or forgetting to update one of the duplicated code blocks.

By using both utility-style macros and large functional block macros, we find that much of our primary program code is a rather short sequence of macro invocations, forming a high level and easily understood summary of how the overall program works. It is worth noting that the programmer must fully understand the fully expanded code. In particular it must be understood what resources a particular macro uses (does it overwrite ACC32, or use various core registers as temp values, etc). If that is not understood then there is a danger that code using such macros will fail because of values that get overwritten unexpectedly. For that reason we tend to fully parameterize our utility macros so that the caller (invoking code) must specify any temp registers or other resources that the macro will overwrite, and the macro generates code using (only) those resources. Because of the accumulator-centric nature of the FXCore instruction set, it is universally understood that anything except the most trivial macro will overwrite ACC32. Any invocation of any macro should assume that ACC32 will not be preserved unless the macro specifically leaves its results there intentionally.

The examples in this chapter of the documentation consists of utility type macros as they are generally useful for many types of FXCore programs. They are drawn from our own common library of macros that we us across our FXCore code base.

MR Table Generation With $_eval() Function

This example shows how the $_eval() function can be used to create a table in a series of memory registers (MRx) starting at an arbitrary base address. This can be useful when a macro is used in different programs that need to locate the table at different MR locations, or to create a series of tables in a single program at different MR locations.

This macro produces a table with 4 entries that are set to the values 3/X, 6/X, 9/X, and 12/X where X is the value passed as the 2nd arg. The first arg is the memory register of the first entry (e.g. the table base address).

$macro DIVIDER_TABLE(baseMRNum, value) ++
.mreg	mr$_eval(${baseMRNum}+0)	3/${value}
.mreg	mr$_eval(${baseMRNum}+1)	6/${value}
.mreg	mr$_eval(${baseMRNum}+2)	9/${value}
.mreg	mr$_eval(${baseMRNum}+3)	12/${value}
$endmacro

For example, the following use of the macro:

.equ	DIV32_TABLE_BASE	10	; MR of first entry of 32/x table
$DIVIDER_TABLE(DIV32_TABLE_BASE, 32)

.equ	DIV48_TABLE_BASE	15	; MR of first entry of 48/x table
$DIVIDER_TABLE(DIV48_TABLE_BASE, 48)

Would produce the following assembler code:

.equ	DIV32_TABLE_BASE		10	; MR of first entry of table
;--- BEGIN MACRO: DIVIDER_TABLE
.mreg	mr10	3/32
.mreg	mr11	6/32
.mreg	mr12	9/32
.mreg	mr13	12/32
;--- END MACRO: DIVIDER_TABLE


.equ	DIV48_TABLE_BASE	15		; MR of first entry of table
;--- BEGIN MACRO: DIVIDER_TABLE
.mreg	mr15	3/48
.mreg	mr16	6/48
.mreg	mr17	9/48
.mreg	mr18	12/48
;--- END MACRO: DIVIDER_TABLE

Normalize Memory Register Number

Extracts the numeric portion of a memory register name, or returns the input unchanged if it does not start with "mr". This can be used when the input may be a full MR name ("mr46") or just the MR number ("46").

; $MR_NUMBER(mr46) will substitute "46"
; $MR_NUMBER(46) will substitute "46"

$macro MR_NUMBER(mr) $_eval(IF (STR_STARTS_WITH("${mr}", "mr"), STR_SUBSTRING("${mr}", 2), "${mr}"))

Normalize Memory Register Name

Returns a memory register name from an MR number or full MR name.

; $MR_NAME(46) returns "mr46"
; $MR_NAME(mr46) returns "mr46"

$macro MR_NAME(mr) mr$MR_NUMBER(${mr})

Set MR From (2) 16 Bit Constants

; Set an MR to a 32 bit value from (2) 16 bit constants
; Uses ACC32
$macro COPY_CONST_TO_MR(mrTo, constHi, constLo) ++
wrdld	acc32, ${constHi} 
ori		acc32, ${constLo}
cpy_mc	${mrTo}, acc32
$endmacro

Branch on Switch State LOW

; Branch to a target label if the given (debounced) SWITCH is LOW
; Uses ACC32
$macro IF_SWITCH_LOW(switch, label) ++
cpy_cs		acc32, SWITCH 
andi		acc32, ${switch}
jz			acc32, ${label}
$endmacro

Branch on Switch State HIGH

; Branch to a target label if the given (debounced) SWITCH is HIGH.
; The first arg should be one of the SWxDB assembler constants.
; Uses ACC32
$macro IF_SWITCH_HIGH(switch, label) ++
cpy_cs		acc32, SWITCH 
andi		acc32, ${switch}
jnz			acc32, ${label}
$endmacro

Macro Synonym

This is an example of providing an alternate name (synonym) for a macro

; Short hand for IF_SWITCH_HIGH()
$macro IF_SWITCH(a,b) ++
$IF_SWITCH_HIGH(${a},${b})
$endmacro

Invert a Core Register (1.0-CR)

; Invert a positive value in "cr" and leave result in acc32.
; The invert is defined as 1 minus the original value for any
; value between 0 and max pos (0x7FFFFFFF).
; Uses ACC32
$macro INVERT(cr) ++
wrdld	acc32, 0x7FFF 	; Load max pos value 
ori		acc32, 0xFFFF	
subs	acc32, ${cr}	; Leaves result in acc32
$endmacro

Multiply Two 16 Bit Numbers

Multiplying two 16 bit integers in FXCore takes several instructions, this macro simplifies this common operation.

; ACC32 = Multiply the lower 16 bits of two core registers
$macro MULT_16(cr1, cr2, crTemp) ++
sl			${cr1}, 16			; Move arg1 to upper 32 bits 
cpy_cc		${crTemp}, acc32	; Save in temp 
sl			${cr2}, 15			; Move arg 2, not sure why 15 instead of 16 bits 
multrr		acc32, ${crTemp}	; acc32 = upper 32 bits of 64 bit result
$endmacro

Convert Msec to Number of Samples (At 48k)

Convert msec to number (integral) number of delay samples, assuming 48kHz sample rate.

$macro MS_TO_SAMPLES_48K(msec) ((${msec}/1000)/(1/48000))

Convert Msec to Number of Samples (At Current Sample Rate)

Convert (fixed constant) msec to samples based on current sampling rate. Unlike MS_TO_SAMPLES_48K this macro expands to executable code that accounts for the current sampling rate. The results are left in ACC32.

The msec arg must be a fixed constant (or constant expression) that evaluates to less than 2048.

$macro MS_TO_SAMPLES(msec) ++

r0.u	= $_eval(FLOOR((${msec})*12)) ; Samples assuming 12kHz

acc32	= BOOTSTAT		
acc32	= acc32 andi 3          ; Mask all but PLL (sampling rate) bits [1:0]
acc32	= acc32 add -2          ; If PLL=2 then rate is 32k 
if acc32 =0 goto _mts_k32       ; Special case, 32k is not a multiple of 12k

acc32	= acc32 add 3           ; Get original PLL value plus 1, now a multiplier for 12k rate
acc32	= acc32 sl 15           ; Do multiply in upper 16 bits
acc32	= acc32 mult r0
goto _mts_end

_mts_k32: 
acc32.u	= $_eval(FLOOR((${msec})*32)) ; Samples at 32k
acc32	= acc32 sr 16

_mts_end:
$endmacro

Encode 4 Bytes Into 32-Bit Word

; Encode 4 bytes into a 32-bit word
$macro WORD32_BYTES(msb, b2, b1, lsb) (${msb}<<24)|(${b2}<<16)|(${b1}<<8)|${lsb}

4-Way Switch Statement

; Multi-target branch based on value of a CR. A value of 0 will
; branch to label target0, a value of 1 will branch to target1,
; etc. Any value is >=3 will branch to the last (default) label.
; Uses ACC32.
$macro SWITCH4(crValue, target0, target1, target2, default) ++
jz			${crValue}, ${target0}

cpy_cc		acc32, ${crValue}
addi		acc32, -1			
jz			acc32, ${target1}

cpy_cc		acc32, ${crValue}		
addi		acc32, -2			
jz			acc32, ${target2}

jmp			${default}
$endmacro

3-Way Switch Statement

; Multi-target branch based on value of a CR. A value of 0 will
; branch to label target0, a value of 1 will branch to target1,
; etc. If the value is >=2 will branch to the last (default) label.
; Uses ACC32.
$macro SWITCH3(crValue, target0, target1, default) ++
jz			${crValue}, ${target0}
cpy_cc		acc32, ${crValue}		
addi		acc32, -1			
jz			acc32, ${target1}
jmp			${default}
$endmacro

MR Symbolic Name and Initial Value

This macro is shorthand for the common practice of defining a symbolic name for a MR location, and initializing that location to a specific value. This example demonstrates the use of string expressions in the _eval() predefined macro.

$macro defMR(name, mr, initVal) ++
.rn     ${name}     mr$_eval(IF (STR_STARTS_WITH("${mr}", "mr"), STR_SUBSTRING("${mr}", 2), "${mr}"))
.mreg   ${name}     ${initVal}
$endmacro

The "mr" argument can either be a simple integer number 0-127, or "mr" followed by 0-127. E.g. these generate the same code:

$defMR(myreg, 110, 0)
$defMR(myreg, mr110, 0)

The above macros would both produce the same 2 statements:

.rn     myreg     mr110
.mreg   myreg     0

Note a math expression cannot be used for the "mr" argument, e.g. "100+10" will cause an error. If an expression is needed, evaluate it with $_eval() e.g. $defMR(myreg, $_eval(100+10), 0)

Copy SFR to MR

; Copy a Special Function Register (SFR) to a Memory Register (MR)
; Uses ACC32
$macro COPY_SFR_TO_MR(mr, sfr) ++
cpy_cs	acc32, ${sfr} 
cpy_mc	${mr}, acc32 
$endmacro

Copy MR to SFR

; Copy a Memory Register (MR) to a Special Function Register (SFR)
; Uses ACC32
$macro COPY_MR_TO_SFR(sfr, mr) ++
cpy_cm	acc32, ${mr} 
cpy_sc	${sfr}, acc32 
$endmacro

Copy MR to MR (uses ACC32)

; Copy a MR to another MR (uses ACC32)
$macro COPY_MR_TO_MR(mrTo, mrFrom) ++
cpy_cm	acc32, ${mrFrom}
cpy_mc	${mrTo}, acc32 
$endmacro

Copy MR to MR

; Copy MR to MR with a temp register (does not use acc32)
$macro COPY_MR_TO_MR_TEMP(mrTarget, mrSource, crTemp) ++
cpy_cm		${crTemp}, ${mrSource} 	
cpy_mc		${mrTarget}, ${crTemp}
$endmacro

Clone this wiki locally