-
Notifications
You must be signed in to change notification settings - Fork 0
Example Macros
- MR Table Generation With $_eval() Function
- Normalize Memory Register Number
- Normalize Memory Register Name
- Set MR From (2) 16 Bit Constants
- Branch on Switch State LOW
- Branch on Switch State HIGH
- Macro Synonym
- Invert a Core Register (1.0-CR)
- Multiply Two 16 Bit Numbers
- 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
- 4-Way Switch Statement
- 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
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.
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
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}"))
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) ++
wrdld acc32, ${constHi}
ori acc32, ${constLo}
cpy_mc ${mrTo}, acc32
$endmacro
; 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 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
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 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
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 (integral) number of delay samples, assuming 48kHz sample rate.
$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)) ; 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 a 32-bit word
$macro WORD32_BYTES(msb, b2, b1, lsb) (${msb}<<24)|(${b2}<<16)|(${b1}<<8)|${lsb}
; 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
; 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
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