Skip to content

Example Macros

Cabintech edited this page Sep 15, 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.

Index to Examples

$nextMR(): Auto Sequencing of MR Locations

One of the tasks in creating FXCore applications is defining Memory Registers for various uses including temporary storage, lookup tables, etc. With (only) 128 registers available this can become a limited resource in complex applications. As a program evolves and features are added, it can become more difficult to identify MRs that are unused, or create a block of MRs (for say, a lookup table) that does not overlap any other usage. Looking for MR allocations that can be scattered through the program listing can be time consuming.

One solution is to use a macro to manage the sequencing and allocation of Memory Registers. To do this effectively the macro needs to manage the MR naming, initialization, and numerical sequencing. The macro shown here allows MR locations to be defined by name but without a fixed MR address. The macro will generate the address during the macro expansion phase of assembly, and if that numerical address needs to be used for relative MR address calculation, it is available as an EQU value (so the actual MR address is not hard coded anywhere in the source code).

Each use of the macro will use the next sequential MR number, starting at 0. The macro is defined as:

$macro nextMR(mrName<=, initValue<=) ++
.equ   $_eval(if("${mrName}"!="","${mrName}_n","nextmr${:unique}_n")) $_count(nextmr,get,)
.rn    $_eval(if("${mrName}"!="","${mrName}",  "nextmr${:unique}"))   mr$_count(nextmr,add,1)
$_eval(if("${initValue}"!="",".mreg $_eval(if("${mrName}"!="","${mrName}","nextmr${:unique}")) ${initValue}", ""))
$endmacro

This macro takes two arguments, (1) the name to be assigned to the generated MR location, and (2) the initial value to be loaded into that location upon program load. If no name is required the first argument can be omitted and a name will be generated. If no initial value is required the second argument can be omitted and no MREG statement will be generated.

As can be seen in the macro definition, the $_eval(if ...) function is used to conditionally generate source code and the ${:unique} macro variable is used to generate unique names when no name is supplied.

This macro generates 3 assembler statements for each invocation:

  1. EQU statement for the numerical register number (useful for code that needs the integer value to do relative MR table lookups). The name of the EQU will be the name supplied as the first argument, post-fixed with _n. If no name is supplied a unique name is generated.
  2. RN statement to name the Memory Register location. If no name is supplied a unique name is generated.
  3. MREG statement to define an initial value for the MR upon program load. If no initial value was supplied, this line will be empty.

The macro will generate sequential MR locations from 0 to 128. Note that the macro uses a counter named nextmr. To alter the starting location or otherwise manipulate the sequence, this counter value can be explicitly modified with the $_count() macro.

Example usage: This example defines a number of Memory Registers including an 8-location fully initialized lookup table (using the $_eval() macro to generate the table values), and a block of 12 uninitialized MR locations:

$nextMR(slope, 0.7) ; Some misc memory registers
$nextMR(deltax,)
$nextMR(deltay,)

; Lookup table of 1/8 constants from 0.0 to 1.0
$nextMR(eighthsTable, $_count(eighths, add, $_eval(1/8))) 
$nextMR(, $_count(eighths, add, $_eval(1/8)))
$nextMR(, $_count(eighths, add, $_eval(1/8)))
$nextMR(, $_count(eighths, add, $_eval(1/8)))
$nextMR(, $_count(eighths, add, $_eval(1/8)))
$nextMR(, $_count(eighths, add, $_eval(1/8)))
$nextMR(, $_count(eighths, add, $_eval(1/8)))
$nextMR(, $_count(eighths, add, $_eval(1/8)))

$nextMR(alias1, 0)
$nextMR(alias2, 0)

; Table of 12 uninitialized values
$nextMR(deltaTable,) 
$_count(nextmr, inc, 12); Move counter past the table

$nextMR(overSigma, 0.91)
$nextMR(costRatio, 6)

; Load address of the Eighths table using the generated EQU name of the first entry
acc32 = 0
acc32 = acc32 xor eighthsTable_n
; ... add desired table index ... e.g.
acc32 = acc32 add 3
; Load value from the table (e.g. 3rd table entry)
r1 = [acc32]

; Last line of source code
$_log(This program is using $_count(nextmr,get,) auto-assigned Memory Registers)

Some notes on the above sample:

  1. There are no fixed MR address values in the source code, so additional $nextMR() macros can be inserted without impacting any other code.
  2. The deltax and deltay memory registers have no initialize value (2nd argument) specified. Notice in the generated code (below) that the macro does not generate any MREG statement for them.
  3. All the entries of the "eighths" table except the first one have no names (1st argument) specified.
  4. Notice the use of the $_count() macro to skip over the 12 uninitialized and unnamed MR locations.
  5. MR addresses will be used sequentially starting at 0.
  6. The last line will write a message to the console during assembly showing how many MR locations were auto-assigned.

The above generates the following assembler code:

.equ    slope_n    0
.rn     slope      mr0
.mreg   slope      0.7

.equ    deltax_n  1
.rn     deltax    mr1

.equ    deltay_n  2
.rn     deltay    mr2

; Lookup table of 1/8 constants from 0.0 to 1.0
.equ    eighthsTable_n  3
.rn     eighthsTable    mr3
.mreg   eighthsTable    0

.equ    nextmr8_n       4
.rn     nextmr8         mr4
.mreg   nextmr8         0.125

.equ    nextmr9_n       5
.rn     nextmr9         mr5
.mreg   nextmr9         0.25

.equ    nextmr10_n      6
.rn     nextmr10        mr6
.mreg   nextmr10        0.375

.equ    nextmr11_n      7
.rn     nextmr11        mr7
.mreg   nextmr11        0.5

.equ    nextmr12_n      8
.rn     nextmr12        mr8
.mreg   nextmr12        0.625

.equ    nextmr13_n      9
.rn     nextmr13        mr9
.mreg   nextmr13        0.75

.equ    nextmr14_n      10
.rn     nextmr14        mr10
.mreg   nextmr14        0.875


.equ    alias1_n        11
.rn     alias1          mr11
.mreg   alias1          0

.equ    alias2_n        12
.rn     alias2          mr12
.mreg   alias2          0


; Table of 12 uninitialized values
.equ    deltaTable_n    13
.rn     deltaTable      mr13
.mreg   deltaTable      0

.equ    overSigma_n     26
.rn     overSigma       mr26
.mreg   overSigma       0.91

.equ    costRatio_n     27
.rn     costRatio       mr27
.mreg   costRatio       6


; Load address of the Eighths table using the generated EQU name of the first entry
xor        ACC32,ACC32
xori       ACC32,EIGHTHSTABLE_N
; ... add desired table index ... e.g.
addi       ACC32,3
; Load value from the table (e.g. 3rd table entry)
cpy_cmx    R1,ACC32


Notice how the MR address skip from 13 to 26 to leave 12 addresses unassigned for deltaTable.

Note on integer initialization values: The FXCore assembler tends to interpret constants as S.31 values from -1.0 to +0.9999, even if those constants appear to be integers. The FXCore documentation indicates the use of FLOOR(), CEIL(), and TRUNCATE() functions to make integer values, but due to some anomalies in the assembler math implementation even simple explicit integer constants get interpreted incorrectly, so a statement such as:

.mreg mr1 16355

will cause an assembler error:

ERROR: Line number 00000: "16355" - Value is outside the allowed range for an S.31 of -1.0 to 0.999999999534338

In order to force the assembler to understand the value as a simple integer, an odd mathematical trick has to be played:

.mreg mr1 (CEILING(16355))/(2^31-2)

This will avoid the assembler error and initialize the MR location to the value 0x00003FE3 as intended. It is useful to put that math in a macro for easier use:

$macro INT(expr) (CEILING(${expr}))/(2^31-2)

With that macro definition, an MR location can be reserved and initialized with a simple integer like:

$nextMR(mr1, $INT(16355))

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

$MR_NUM(): Normalize Memory Register Number

This macro 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") and (only) the numeric portion is needed.

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

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

$MR_NAME(): 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})

$COPY_CONST_TO_MR(): Load 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) ++
acc32.u = ${constHi} 
acc32   = acc32 ori ${constLo}
${mrTo} = acc32
$endmacro

$IF_SWITCH_LOW(): 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) ++
acc32 = SWITCH 
acc32 = acc32 andi ${switch}
acc32 jz ${label}
$endmacro

$IF_SWITCH_HIGH(): 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) ++
acc32 = SWITCH 
acc32 = acc32 andi ${switch}
if acc32 !=0 goto ${label}
$endmacro

INVERT(): Invert a Core Register (Calculate 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) ++
acc32.u = 0x7FFF                ; Load max pos value 
acc32   = acc32 ori 0xFFFF	
acc32   = acc32 subs ${cr}      ; Leaves result in acc32
$endmacro

Convert Msec to Number of Samples (At 48k)

Convert msec to number (integral) number of delay samples, assuming 48kHz sample rate. This is just the evaluation of a constant expression, no executable code is generated.

$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)) ; = (msec/1000) / (1/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_${:unique}	; Special case, 32k not a multiple of 12k

acc32	= acc32 add 3		; Get original PLL value plus 1, now a multiplier by 12k rate
acc32	= acc32 sl 15		; Do multiply in upper 16 bits
acc32	= acc32 mult r0
goto _mts_end_${:unique}

_mts_k32_${:unique}: 
acc32.u	= $_eval(FLOOR((${msec})*32)) ; # samples at 32k
acc32	= acc32 sr 16

_mts_end_${:unique}:
$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}

$CASE4(): 4-Way Switch Statement

This macro implements a multi-target branch based on value in a CR. A value of 0 will branch to label target0, a value of 1 will branch to target1, etc. Any value >=3 will branch to the last (default) label.

$macro CASE4(crValue, target0, target1, target2, default) ++
if ${crValue} = 0 goto ${target0}

acc32 = ${crValue} add -1		
if acc32 = 0 goto ${target1}

acc32 = acc32 add -1		
if acc32 = 0 goto ${target2}

goto ${default}
$endmacro

CASE3(): 3-Way Switch Statement

This implements a 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.

$macro CASE3(crValue, target0, target1, default) ++
if ${crValue} = 0 goto ${target0}

acc32 = ${crValue} add -1			
if acc32 = 0 goto ${target1}

goto ${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