-
Notifications
You must be signed in to change notification settings - Fork 0
Example Macros
There are 2 basic uses for macros:
- 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.
- 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
- MR Table Generation With $_eval() Function
- $MR_NUM(): Normalize Memory Register Number
- $MR_NAME(): Normalize Memory Register Name
- $COPY_CONST_TO_MR(): Load MR From (2) 16 Bit Constants
- Branch on Switch State LOW
- Branch on Switch State HIGH
- Invert a Core Register (1.0-CR)
- Convert Msec to Number of Samples (At 48k)
- Convert Msec to Number of Samples (At Current Sample Rate)
- Encode 4 Bytes Into 32-Bit Word
- $CASE4(): 4-Way Switch Statement
- CASE3(): 3-Way Switch Statement
- MR Symbolic Name and Initial Value
- Copy SFR to MR
- Copy MR to SFR
- Copy MR to MR (uses ACC32)
- Copy MR to MR
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:
-
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. - RN statement to name the Memory Register location. If no name is supplied a unique name is generated.
- 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:
- There are no fixed MR address values in the source code, so additional
$nextMR()macros can be inserted without impacting any other code. - The
deltaxanddeltaymemory 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. - All the entries of the "eighths" table except the first one have no names (1st argument) specified.
- Notice the use of the
$_count()macro to skip over the 12 uninitialized and unnamed MR locations. - MR addresses will be used sequentially starting at 0.
- 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))
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
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}"))
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 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
; 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
; 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 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 (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 (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 a 32-bit word
$macro WORD32_BYTES(msb, b2, b1, lsb) (${msb}<<24)|(${b2}<<16)|(${b1}<<8)|${lsb}
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
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
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 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 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 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 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
Copyright © Cabintech Global LLC