<< Previous | Index | Next >> | |
|
This chapter gives the rules for mixing assembly language with Dynamic C code. A reference guide to the Rabbit Instruction Set is available from the Help menu of Dynamic C and is also documented in the Rabbit 2000/3000 Microprocessor Instruction Reference Manual.
11.1 Mixing Assembly and C
Dynamic C permits assembly language statements to be embedded in C functions and/or entire functions to be written in assembly language. C statements may also be embedded in assembly code. C-language variables may be accessed by the assembly code.
11.1.1 Embedded Assembly Syntax
Use the
#asm
and#endasm
directives to place assembly code in Dynamic C programs. For example, the following function will add two 64-bit numbers together. The same program could be written in C, but it would be many times slower because C does not provide an add-with-carry operation (adc
).The keywords
debug
andnodebug
can be placed on the same line as#asm
. Assembly code blocks arenodebug
by default. This saves space and unnecessary calls to the debugger kernel.All blocks of assembly code within a C function are assembled in nodebug mode. The only exception to this is when a block of assembly code is explicitly marked with
debug
. Any blocks markeddebug
will be assembled in debug mode even if the enclosing C function is markednodebug
.11.1.2 Embedded C Syntax
A C statement may be placed within assembly code by placing a "c" in column 1. Note that whichever registers are used in the embedded C statement will be changed.
#asm
InitValues::
c start_time = 0;
c counter = 256;
ret
#endasm11.1.3 Setting Breakpoints in Assembly
Starting with Dynamic C version 7.20, there are two ways to enable breakpoint support in a block of assembly code.
One way is to explicitly mark the assembly block as
debug
(the default condition isnodebug
). This causes the insertion of "rst 0x28" instructions between each assembly instruction. These rst 0x28 instructions may cause jump relative (i.e.,jr
) instructions to go out of range, but this problem can be solved by changing the relative jump (jr
) to an absolute jump (jp
).The other way to enable breakpoint support in a block of assembly code is to add a C statement before the desired assembly instruction. Note that the assembly code must be contained in a debug C function in order to enable C code debugging. Below is an example.
debug dummyfunction() {
#asm
function::
...
label:
...
c ; // add line of C code to permit a breakpoint before jump relative
jr nc, label
ret
#endasm}
NOTE Single stepping through assembly code is always allowed if the assembly window is open. 11.2 Assembler and Preprocessor
The assembler parses most C language constant expressions. A C language constant expression is one whose value is known at compile time. All operators except the following are supported:
Table 11-18. Operators Not Supported By The Assembler ?: conditional [ ] array index . dot -> points to * dereference 11.2.1 Comments
C-style comments are allowed in embedded assembly code. The assembler will ignore comments beginning with
;
-- text from the semicolon to the end of line is ignored.//
-- text from the double forward slashes to the end of line is ignored./* ... */
-- text between slash-asterisk and asterisk-slash is ignored.11.2.2 Defining Constants
Constants may be created and defined in assembly code with the assembly language keyword
db
(define byte).db
should be followed immediately by numerical values and strings separated by commas. For example, each of the following lines all define the string "ABC
."
db 'A', 'B', 'C'
db "ABC"
db 0x41, 0x42, 0x43The numerical values and characters in strings are used to initialize sequential byte locations.
If separate I&D space is enabled, assembly constants should either be put in their own assembly block with the
const
keyword or be done in C.
#asm const
myrootconstants::
db 0x40, 0x41, 0x42
#endasmor
const char myrootconstants[] = {`\x40', `\x41', `\x42'}
If separate I&D space is enabled,
db
places bytes in the base segment of the data space when it is used withconst
. If theconst
keyword is absent, i.e.,
#asm
myrootconstants::
db 0x40, 0x41, 0x42
#endasmthe bytes are placed somewhere in the instruction space. If separate I&D space is disabled (the default condition), the bytes are placed in the base segment (aka, root segment) interspersed with code.
The assembly language keyword
dw
defines 16-bit words, least significant byte first. The keyworddw
should be followed immediately by numerical values:
dw 0x0123, 0xFFFF, xyz
This example defines three constants. The first two constants are literals, and the third constant is the address of variable
xyz
.The numerical values initialize sequential word locations, starting at the current code address.
11.2.3 Multiline Macros
The Dynamic C preprocessor has a special feature to allow multiline macros in assembly code. The preprocessor expands macros before the assembler parses any text. Putting a
$\
at the end of a line inserts a new line in the text. This only works in assembly code. Labels and comments are not allowed in multiline macros.
#define SAVEFLAG $\
ld a,b $\
push af $\
pop bc#asm
...
ld b,0x32
SAVEFLAG
...
#endasm11.2.4 Labels
A label is a name followed by one or two colons. A label followed by a single colon is local, whereas one followed by two colons is global. A local label is not visible to the code out of the current embedded assembly segment (i.e., code before the
#asm
or after the#endasm
directive).Unless it is followed immediately by the assembly language keyword
equ
, the label identifies the current code segment address. If the label is followed byequ
, the label "equates" to the value of the expression after the keywordequ
.Because C preprocessor macros are expanded in embedded assembly code, Z-World recommends that preprocessor macros be used instead of
equ
whenever possible.11.2.5 Special Symbols
This table lists special symbols that can be used in an assembly language expression.
Table 11-19. Special Assembly-Language Symbols @SP
Indicates the amount of stack space (in bytes) used for stack-based variables. This does not include arguments. @RETVAL
Evaluates the offset from the frame reference point to the stack space reserved for the struct function returns. See Section 11.4.1.1 on page 123 for more information. @LENGTH
Determines the next reference address of a variable plus it size. 11.2.6 C Variables
C variable names may be used in assembly language. What a variable name represents (the value associated with the name) depends on the variable. For a global or static local variable, the name represents the address of the variable in root memory. For an
auto
variable or formal argument, the variable name represents its own offset from the frame reference point.The name of a structure element represents the offset of the element from the beginning of the structure. In the following structure, for example,
struct s {
int x;
int y;
int z;
};the embedded assembly expression
s+x
evaluates to 0,s+y
evaluates to 2, ands+z
evaluates to 4, regardless of where structures
may be.The following list of processor register names are reserved and may not be used as C variable names in assembly: A, B, C, D, E, F, H, L, AF, HL, DE, BC, IX, IY, SP, PC, XPC, IP IIR and EIR. Both upper and lower case instances are reserved.
In nested structures, offsets can be composite, as shown here.
struct s {
int x; // s + x = 0
struct a{ // s + a = 2
int b; // a + b = 0 s + a + b = 2
int c; // a + c = 2 s + a + c = 4
};
};11.3 Stand-Alone Assembly Code
A stand-alone assembly function is one that is defined outside the context of a C language function. Before Dynamic C version 7.25, stand-alone assembly functions were always placed in root memory.
A stand-alone assembly function has no
auto
variables and no formal parameters. It can, however, have arguments passed to it by the calling function. When a program calls a function from C, it puts the first argument into a primary register. If the first argument has one or two bytes (int, unsigned int, char, pointer
), the primary register is HL (with register H containing the most significant byte). If the first argument has four bytes (long, unsigned long, float
), the primary register is BC:DE (with register B containing the most significant byte). Assembly-language code can use the first argument very efficiently. Only the first argument is put into the primary register, while all arguments--including the first, pushed last--are pushed on the stack.C function values return in the primary register, if they have four or fewer bytes, either in HL or BC:DE.
Assembly language allows assumptions to be made about arguments passed on the stack, and auto variables can be defined by reserving locations on the stack for them. However, the offsets of such implicit arguments and variables must be kept track of. If a function expects arguments or needs to use stack-based variables, Z-World recommends using the embedded assembly techniques described in the next section.
11.3.1 Stand-Alone Assembly Code in Extended Memory
Starting with Dynamic C 7.25, stand-alone assembly functions may be placed in extended memory by adding the
xmem
keyword as a qualifier to#asm
, as shown below. Care needs be taken to make sure that branch instructions do not jump beyond the current xmem window. To help prevent such bad jumps, the compiler limits xmem assembly blocks to 4096 bytes. Code that branches to other assembly blocks in xmem should always useljp
orlcall
.#asm xmem
main::
...
lcall fcn_in_xmem
...
lret
#endasm#asm xmem
fcn_in_xmem::
...
lret
#endasm11.3.2 Example of Stand-Alone Assembly Code
The stand-alone assembly function
foo()
can be called from a Dynamic C function.int foo ( int ); // A function prototype can be declared for stand-alone
// assembly functions, which will cause the compiler
// to perform the appropriate type-checking.
main(){
int i,j;
i=1;
j=foo(i);
}#asm
foo::
...
ld hl,2 // The return value expected by main() is put
ret // in HL just before foo() returns
#endasmThe entire program can be written in assembly.
#asm
main::
...
ret
#endasm11.4 Embedded Assembly Code
When embedded in a C function, assembly code can access arguments and local variables (either
auto
orstatic
) by name. Furthermore, the assembly code does not need to manipulate the stack because the functionsprolog
andepilog
already do so.11.4.1 The Stack Frame
The purpose and structure of a stack frame should be understood before writing embedded assembly code. A stack frame is a run-time structure on the stack that provides the storage for all
auto
variables, function arguments and the return address for a particular function. If the IX register is used for a frame reference pointer, the previous value of IX is also kept in the stack frame. The following figure shows the general appearance of a stack frame.The return address is always necessary. The presence of auto variables depends on the function definition. The presence of arguments and structure return space depends on the function call. (The stack pointer may actually point lower than the indicated mark temporarily because of temporary information pushed on the stack.)
The shaded area in the stack frame is the stack storage allocated for
auto
variables. The assembler symbol@SP
represents the size of this area.11.4.1.1 The Frame Reference Point
The frame reference point is a location in the stack frame that immediately follows the function's return address. The IX register may be used as a pointer to this location by putting the keyword
useix
before the function, or the request can be specified globally by the compiler directive#useix
. The default is#nouseix
. If the IX register is used as a frame reference pointer, its previous value is pushed on the stack after the function's return address. The frame reference point moves to encompass the saved IX value.11.4.2 Embedded Assembly Example
The purpose of the following sample program,
asm1.c
, is to show the different ways to access stack-based variables from assembly code.void func(char ch, int i, long lg);
main(){
char ch;
int i;
long lg;
ch = 0x11;
i = 0x2233;
lg = 0x44556677L;func(ch,i,lg);
}void func(char ch, int i, long lg){
auto int x;
auto int z;x = 0x8888;
z = 0x9999;#asm
// @SP+i
gives the offset ofi
from the stack frame on entry.
// On the Z180, this is how HL is loaded with the value ini
.
// (The assembler combinesi
and @SP into one constant.)
ld hl,@SP+i
add hl,sp
ld hl,(hl)// On the Rabbit, this code does the same:
ld hl,(sp+@SP+i)
// This works if
func()
is useix, however, if the IX register
// has been changed by the user code, this code will fail.
ld hl,(ix+i)// This method works in either case because the assembler
// adjusts the constant @SP, so changing the function to
// nouseix with the keywordnouseix
, or the compiler
// directive#nouseix
will not break the code. But, if SP has
// been changed by user code, (e.g. a push) it won't work.
ld hl,(sp+@SP+lg+2)
ld b,h
ld c,L
ld hl,(sp+@SP+lg)
ex de,hl
#endasm
}11.4.2.1 The Disassembled Code Window
A program may be debugged at the assembly level by clicking the Assemb radio button on Dynamic C's toolbar to open the Disassembled Code window. Single stepping and breakpoints are supported in this window. When the Disassembled Code window is open, single stepping occurs instruction by instruction rather than statement by statement. The figure below shows the Registers, Stack and Disassembled Code windows for the example code,
asm1.c
, just before the function call.11.4.2.2 Instruction Cycle Time
The Disassembled Code window shows the memory address on the far left, followed by the code bytes for the instruction at the address, followed by the mnemonics for the instruction. The last column shows the number of cycles for the instruction, assuming no wait states. The total cycle time for a block of instructions will be shown at the lowest row in the block in the cycle-time column, if that block is selected and highlighted with the mouse. The total assumes one execution per instruction, so the user must take looping and branching into consideration when evaluating execution times.
11.4.3 Local Variable Access
Accessing static local variables is simple because the symbol evaluates to the address directly. The following code shows, for example, how to load static variable
y
into HL.
ld hl,(y) ; load hl with contents of y
11.4.3.1 Using the IX Register
Access to stack-based local variables is fairly inefficient. The efficiency improves if IX is used as a frame pointer. The arguments will have slightly different offsets because of the additional two bytes for the saved IX register value.
Now, access to stack variables is easier. Consider, for example, how to load
ch
into register A.
ld a,(ix+ch) ; a ch
The IX+offset load instruction takes 9 clock cycles and opcode is three bytes. If the program needs to load a four-byte variable such as lg, the IX+offset instructions are as follows.
ld hl,(ix+lg+2) ; load LSB of lg
ld b,h ; longs are normally stored in BC:DE
ld c,L
ld hl,(ix+lg) ; load MSB of lg
ex de,hlThis takes a total of 24 cycles.
The offset from IX is a signed 8-bit integer. To use IX+offset, the variable must be within +127 or -128 bytes of the frame reference point. The
@SP
method is the only method for accessing variables out of this range. The@SP
symbol may be used even if IX is the frame reference pointer.11.4.3.2 Functions in Extended Memory
If the
xmem
keyword is present, Dynamic C compiles the function to extended memory. Otherwise, Dynamic C determines where to compile the function. Functions compiled to extended memory have a 3-byte return address instead of a 2-byte return address.Because the compiler maintains the offsets automatically, there is no need to worry about the change of offsets. The
@SP
approach discussed previously as a means of accessing stack-based variables works whether a function is compiled to extended memory or not, as long as the C-language names of local variables and arguments are used.A function compiled to extended memory can use IX as a frame reference pointer as well. This adds an additional two bytes to argument offsets because of the saved IX value. Again, the IX+offset approach discussed previously can be used because the compiler maintains the offsets automatically.
11.5 C Calling Assembly
Dynamic C does not assume that registers are preserved in function calls. In other words, the function being called need not save and restore registers.
11.5.1 Passing Parameters
When a program calls a function from C, it puts the first argument into HL (if it has one or two bytes) with register H containing the most significant byte. If the first argument has four bytes, it goes in BC:DE (with register B containing the most significant byte). Only the first argument is put into the primary register, while all arguments--including the first, pushed last--are pushed on the stack.
11.5.2 Location of Return Results
If a C-callable assembly function is expected to return a result (of primitive type), the function must pass the result in the "primary register." If the result is an
int,
unsigned int,
char
, or a pointer, return the result in HL (register H contains the most significant byte). If the result is along, unsigned long,
orfloat
, return the result in BCDE (register B contains the most significant byte). A C function containing embedded assembly code may, of course, use a Creturn
statement to return a value. A stand-alone assembly routine, however, must load the primary register with the return value before theret
instruction.11.5.2.1 Returning a Structure
In contrast, if a function returns a structure (of any size), the calling function reserves space on the stack for the return value before pushing the last argument (if any). Dynamic C functions containing embedded assembly code may use a C
return
statement to return a value. A stand-alone assembly routine, however, must store the return value in the structure return space on the stack before returning.Inline assembly code may access the stack area reserved for structure return values by the symbol
@RETVAL
, which is an offset from the frame reference point.The following code shows how to clear field
f1
of a structure (as a returned value) of typestruct s
.It is crucial that @SP be added to
@RETVAL
because@RETVAL
is an offset from the frame reference point, not from the current SP.11.6 Assembly Calling C
A program may call a C function from assembly code. To make this happen, set up part of the stack frame prior to the call and "unwind" the stack after the call. The procedure to set up the stack frame is described here.
Save all registers that the calling function wants to preserve. A called C function may change the value of any register. (Pushing registers values on the stack is a good way to save their values.)
If the function return is a
struct
, reserve space on the stack for the returned structure. Most functions do not return structures.Compute and push the last argument, if any.
Compute and push the second to last argument, if any.
Continue to push arguments, if there are more.
Compute and push the first argument, if any. Also load the first argument into the primary register (HL for
int, unsigned int, char
, and pointers, or BCDE for long, unsigned long,
andfloat
) if it is of a primitive type.Issue the call instruction.
The caller must unwind the stack after the function returns.
Recover the stack storage allocated to arguments. With no more than 6 bytes of arguments, the program may pop data (2 bytes at time) from the stack. Otherwise, it is more efficient to compute a new
SP
instead. The following code demonstrates how to unwind arguments totaling 36 bytes of stack storage.If the function returns a
struct
, unload the returned structure.Restore registers previously saved. Pop them off if they were stored on the stack.
If the function return was not a
struct
, obtain the returned value from HL or BCDE.11.7 Interrupt Routines in Assembly
Interrupt Service Routines (ISRs) may be written in Dynamic C (declared with the keyword
interrupt
). But since an assembly routine may be more efficient than the equivalent C function, assembly is more suitable for an ISR. Even if the execution time of an ISR is not critical, the latency of one ISR may affect the latency of other ISRs.Either stand-alone assembly code or embedded assembly code may be used for ISRs. The benefit of embedding assembly code in a C-language ISR is that there is no need to worry about saving and restoring registers or reenabling interrupts. The drawback is that the C interrupt function does save all registers, which takes some amount of time. A stand-alone assembly routine needs to save and restore only the registers it uses.
11.7.1 Steps Followed by an ISR
The CPU loads the IP register with the priority of the interrupt before the ISR is called. This effectively turns off interrupts that are of the same or lower priority. Generally, the ISR performs the following actions:
Save all registers that will be used, i.e. push them on the stack. Interrupt routines written in C save all registers automatically. Stand-alone assembly routines must push the registers explicitly.
Determine the cause of the interrupt. Some devices map multiple causes to the same interrupt vector. An interrupt handler must determine what actually caused the interrupt.
Remove the cause of the interrupt.
If an interrupt has more than one possible cause, check for all the causes and remove all the causes at the same time.
When finished, restore registers saved on the stack. Naturally, this code must match the code that saved the registers. Interrupt routines written in C perform this automatically. Stand-alone assembly routines must pop the registers explicitly.
Restore the interrupt priority level so that other interrupts can get the attention of the CPU. ISRs written in C restore the interrupt priority level automatically when the function returns. However, stand-alone assembly ISRs must restore the interrupt priority level explicitly by calling
ipres
.Return. There are three types of interrupt returns:
ret, reti
, andretn
.11.7.2 Modifying Interrupt Vectors
Prior to Dynamic C 7.30, interrupt vector code could be modified directly. By reading the internal and external interrupt registers, IIR and EIR, the location of the vector could be calculated and then written to because it was located in RAM. This method will not work if separate I&D space is enabled because the vectors must be located in flash. To accommodate separate I&D space, the way interrupt vectors are set up and modified has changed slightly. Please see the Rabbit 3000 Designer's Handbook for detailed information about how the interrupt vectors are set up. This section will discuss how to modify the interrupt vectors after they have been set up.
For backwards compatibility, "modifiable" vector relays are provided in RAM. In C, they can be accessed through the SetVectIntern and SetVectExtern functions. In assembly, they are accessed through
INTVEC_BASE
+ <vector offset> orXINTVEC_BASE
+ <vector offset>. The values for <vector offset> are defined insysio.lib
, and are listed here for convenience.
Table 11-20. Internal Interrupts and their offset from INTVEC_BASE
Table 11-21. External Interrupts and their offset from XINTVEC_BASE The following example from
RS232.LIB
illustrates the new I&D space compatible way of modifying interrupt vectors.The following code fragment to set up the interrupt service routine for the periodic interrupt from Dynamic C 7.25 is not compatible with separate I&D space:
#asm xmem
;*** Old method ***
ld a,iir ; get the offset of interrupt table
ld h,a
ld l,0x00
ld iy,hl
ld (iy),0c3h ; jp instruction entry
inc iy
ld hl,periodic_isr ; set service routine
ld (iy),hl
#endasmThe following code fragment shows an I&D space compatible method for setting up the ISR for the periodic interrupt in Dynamic C 7.30:
#asm xmem
;*** New method ***
ld a, 0xc3 ;jp instruction entry
ld hl, periodic_isr ;set service routine
ld (INTVEC_BASE+PERIODIC_OFS), a ;write to the interrupt table
ld (INTVEC_BASE+PERIODIC_OFS+1), hl
#endasmWhen separate I&D space is enabled,
INTVEC_BASE
points to a proxy interrupt vector table in RAM that is modifiable. The code above assumes that the actual interrupt vector table pointed to by the IIR is set up to point to the proxy vector. When separate I&D space is disabled,INTVEC_BASE
and the IIR point to the same location. The code above is an example only, the default configration for the periodic interrupt is not modifiable.The following example from
RS232.LIB
illustrates the new I&D space compatible way of modifying interrupt vectors.The following function
serAclose()
from Dynamic C 7.25, is not compatible with separate I&D space:#asm xmem
serAclose::
ld a,iir ; hl=spaisr_start, de={iir,0xe0}
ld h,a
ld l,0xc0
ld a,0xc9 ; ret in first byte
ipset 1
ld (hl),a
ld a,0x00 ; disable interrupts for port
ld (SACRShadow), a
ioi ld (SACR), a
ipres
lret#endasm
This version of
serAclose()
in Dynamic C 7.30 is compatible with separate I&D space:#asm xmem
serAclose::
ld a, 0xc9
ipset 1
ld (INTVEC_BASE + SERA_OFS), a ; ret in first byte of spaisr_start
ld a, 0x00 ; disable interrupts for port
ld (SACRShadow),a
ioi ld (SACR),a
ipres
lret#endasm
If separate I&D space is enabled, using the modifiable interrupt vector proxy in RAM adds about 80 clock cycles of overhead to the execution time of the ISR. To avoid that, the preferred way to set up interrupt vectors is to use the new keyword,
interrupt_vector
, to set up the vector location at compile time.When compiling with separate I&D space, modify applications that use
SetVectIntern()
,SetVectExtern2000()
orSetVectExtern3000()
to useinterrupt_vector
instead.The following code, from
/Samples/TIMERB/TIMER_B.C
, illustrates the change that should be made.void main()
{
. . .
#if __SEPARATE_INST_DATA__
interrupt_vector timerb_intvec timerb_isr;
#else
SetVectIntern(0x0B, timerb_isr); // set up ISR
#endif. . .
}If
interrupt_vector
is used multiple times for the same interrupt vector, the last one encountered by the compiler will override all previous ones.
interrupt_vector
is syntactic sugar for using the origin directives and assembly code. For example, the line:interrupt_vector timerb_intvec timerb_isr;
#rcodorg timerb_intvec apply
#asm
jp timerb_isr
#endasm#rcodorg rootcode resume
The following table lists the defined interrupt vector names that may be used with
interrupt_keyword
, as well as their corresponding ISRs.
Table 12. Interrupt Vector and ISR Names Fast and nonmodifiable User defined name User defined These interrupt vectors and their ISRs should never be altered by the user because they are reserved for the debug kernel. User defined name User defined Fast and nonmodifiable User defined name User defined User defined name User defined sera_intvec1
Fast and nonmodifiable User defined User defined User defined name User defined name User defined name
1 Please note that this ISR shares the same interrupt vector as DevMateSerialISR
. Usingspa_isr
precludes Dynamic C from communicating with the target.
11.8 Common Problems
Unbalanced stack. Ensure the stack is "balanced" when a routine returns. In other words, the SP must be same on exit as it was on entry. From the caller's point of view, the SP register must be identical before and after the call instruction.
Using the @SP approach after pushing temporary information on the stack. The
@SP
approach for inline assembly code assumes that SP points to the low boundary of the stack frame. This might not be the case if the routine pushes temporary information onto the stack. The space taken by temporary information on the stack must be compensated for.The following code illustrates the concept.
Registers not preserved. In Dynamic C, the caller is responsible for saving and restoring all registers. An assembly routine that calls a C function must assume that all registers will be changed.
Unpreserved registers in interrupt routines cause unpredictable and unrepeatable problems. In contrast to normal functions, interrupt functions are responsible for saving and restoring all registers themselves.
| |
<< Previous | Index | Next >> | |
Z-World, Inc. www.zworld.com Phone: 1.530.757.3737 Fax: 1.530.757.3792 |
Rabbit Semiconductor www.rabbitsemiconductor.com Phone: 1.530.757.8400 Fax: 1.530.757.8402 |