13. Using Assembly Language

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 Microprocessor Instruction Reference Manual available on the Rabbit website:

13.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.

13.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).

void eightadd( char *ch1, char *ch2 ){
#asm
   ld   hl,(sp+@SP+ch2)     ; get source pointer
   ex   de,hl               ; save in register DE
   ld   hl,(sp+@SP+ch1)     ; get destination pointer
   ld   b,8                 ; number of bytes
   xor  a                   ; clear carry
   loop:
   ld   a,(de)              ; ch2 source byte
   adc  a,(hl)              ; add ch1 byte
   ld   (hl),a              ; store result to ch1 address
   inc  hl                  ; increment ch1 pointer
   inc  de                  ; increment ch2 pointer
   djnz loop                ; do 8 bytes
   ; ch1 now points to 64 bit result
#endasm
}

The keywords debug and nodebug can be placed on the same line as #asm. Assembly code blocks are nodebug 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 marked debug will be assembled in debug mode even if the enclosing C function is marked nodebug.

13.1.2  Embedded C Syntax

A C statement may be placed within assembly code by placing a “c” in column 1. Note that the registers  used in the embedded C statement will be changed.

#asm
InitValues::
c  start_time = 0;
c  counter = 256;
   ret
#endasm

13.1.3  Setting Breakpoints in Assembly

There are two ways to enable software breakpoint support in assembly code.

One way is to explicitly mark the assembly block as debug (the default condition is nodebug). 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). Below is an example.

#asm debug
function::
...
ret
#endasm

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 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 assem­bly window is open.

Dynamic C 10.21 introduces support for the hardware breakpoint capability available with the Rabbit 4000 microprocessor. For more information on hardware breakpoints refer to Section 14 and Section 16.5 in this manual and/or the microprocessor user’s manual specific to your Rabbit (e.g., Rabbit 4000 Microprocessor User’s Manual).

13.1.4  Assembly and 32-bit Pointer Registers (PW, PX, PY, PZ)

Assembly programmers should note that far variables defined in C are interpreted as physical addresses by the assembler and near variables are interpreted as segmented logical addresses. Specifically, the instruction:

ld pd, klmn  ; where pd is a 32-bit pointer register, and klmn is a 32-bit constant

does not work as would first be expected if used with a variable. For example, the following code snippet illustrates the problem:

Example (prints ‘Y’ not ‘X’ as may be expected):

char far * ptr;
char far foo;

int main()
{
   foo = 'Y';
   ptr = &foo;

#asm
   ; The following code is INCORRECT!!!
   ld px, ptr  ; ptr is in root, so px gets segmented version of ptr’s address
   ld a, 'X'
   ld (px), a  ; This does NOT store register a’s contents to the address “&ptr” (i.e., foo)
#endasm

   printf("%c\n", foo);
}

The incorrect code shown above illustrates how a programmer might write inline assembly to access a variable via a pointer. However, since the assembler treats near addresses as logical addresses, the format of the value produced by loading the variable “ptr” directly into a pointer register is not correct for the sub­sequent store instruction. To correctly implement the assembly in the above sample, do the following:

#asm
   ;Corrected version of incorrect code above
   ldl px, ptr    ; ptr is in root, so load low word to a 32-bit register
                  ; (high word is loaded with 0xFFFF to flag root address)
   ld px, (px)    ; this loads foo’s far physical address
   ld a, 'X'
   ld (px), a
#endasm

Replacing the first assembly block with the above listing will produce the expected result of printing “X.” The “ldl” instruction correctly loads the root address of “ptr” into px, making the subsequent “ld” instruc­tion load foo’s far physical address into px. The above code has the virtue of being not only correct, but also small (11 bytes), fast (24 clocks) and spartan with regard to its register requirements (only 2 registers are needed).

Like the “ldl” instruction, the instructions “convc” and “convd” also convert logical addresses, though not to the equivalent physical address, but rather to the offset into the physical device.

13.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 13-1.  Operators Not Supported By The Assembler

Operator Symbol

Operator Description

?:

conditional

.

dot

->

points to

*

dereference

13.2.1  Comments

C-style comments are allowed in embedded assembly code. The assembler will ignore comments begin­ning 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 */

13.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 define the string “ABC”.

db 'A', 'B', 'C'
db "ABC"
db 0x41, 0x42, 0x43

The 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
#endasm

or

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 with const. If the const keyword is absent, i.e.,

#asm
   myrootconstants::
   db 0x40, 0x41, 0x42
#endasm

the bytes are placed somewhere in the instruction space. If separate I&D space is disabled (the default con­dition), the bytes are placed in the base segment (aka, root segment) interspersed with code.

Therefore, so that data will be treated as data when referenced in assembly code, the const keyword must be used when separate I&D space is enabled. For example, this won't work correctly without const:

#asm const
label::
   db 0x5a
#endasm

main(){
   ;
#asm
   ld a,(label)     // ld 0x5a to reg a
#endasm
}

The assembly language keyword dw defines 16-bit words, least significant byte first. The keyword dw 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.

13.2.3  Multiline Macros

The Dynamic C preprocessor has a special feature to allow multiline macros in assembly code. The pre­processor 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
   ...
#endasm

13.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 is outside of that embbeded assembly segment).

Unless it is followed immediately by the assembly language keyword equ, the label identifies the current code segment address. If the label is followed by equ, the label “equates” to the value of the expression after the keyword equ.

Because C preprocessor macros are expanded in embedded assembly code, Rabbit recommends that pre­processor macros be used instead of equ whenever possible.

13.2.5  Special Symbols

This table lists special symbols that can be used in an assembly language expression.

Table 13-2.  Special Assembly Language Symbols

Symbol

Description

@SP

Indicates the amount of stack space (in bytes) used for stack-based variables. This does not include arguments.

@PC

Constant for the current code location. For example:

ld hl, @PC

loads the code address of the instruction. ld hl,@PC+3 loads the address after the instruction since it is a 3 byte instruction.

@RETVAL

Evaluates the offset from the frame reference point to the stack space reserved for the struct function returns. See Section 13.3.3.2 for more information on the frame reference point.

@LENGTH

Determines the next reference address of a variable plus its size.

13.2.6  C Variables

C variable names may be used in assembly language. What a variable name represents (the value associ­ated 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 rep­resents its own offset from the frame reference point.

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. The Rabbit 4000 has additional processor register names that are reserved: JK, PX, PY, PZ, PW, BCDE, JKHL, SU and HTR. Both upper and lower case instances are reserved for processor register names.

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, and s+z evaluates to 4, regardless of where structure “s” may be.

In nested structures, offsets can be composite, as shown here.

struct s{      // offset into s
   int x;      // 0
   struct a {  // 2 (i.e., sizeof(x))
     int b;   // 2, offset is 0 relative to a
     int c;   // 4, offset is 2 relative to a
   };
};

Just like in the first definition of structure “s”, the assembly expression s+x evaluates to 0; s+a evaluates to 2 and s+b evaluates to 2 (both expressions evaluate to the same value because both “a” and “b” are offset “0” from “a”); and finally, s+c evaluates to 4 because s+a evaluates to 2 and a+c evaluates to 2.

13.3 Stand-Alone Assembly Code

A stand-alone assembly function is one that is defined outside the context of a C language function.

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 and is not a pointer  (long, unsigned long, float), the primary register is BC:DE (with register B containing the most significant byte). If the first argument is a four byte pointer (far *), the primary register is PX.  Assembly-language code can use the first argument very effi­ciently. 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 , BC:DE, or PX.

Assembly language allows assumptions to be made about arguments passed on the stack, and auto vari­ables 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, Rabbit recommends using the embedded assembly techniques described in the next section.

13.3.1  Stand-Alone Assembly Code in Extended Memory

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 so 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 use ljp or lcall.

#asm xmem
main::
...
lcall fcn_in_xmem
...
lret
#endasm

#asm xmem
fcn_in_xmem::
...
lret
#endasm

13.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
#endasm

The entire program can be written in assembly.

#asm
main::
...
ret
#endasm

Embedded Assembly Code

When embedded in a C function, assembly code can access arguments and local variables (either auto or static) by name. Furthermore, the assembly code does not need to manipulate the stack because the functions prolog and epilog already do so.

13.3.3  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.

13.3.3.1  Stack Frame Diagram

Figure 13.1 shows the general appearance of a stack frame.

Figure 13.1  Assembly Code Stack Frame

stackframe.png

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 sym­bol @SP represents the size of this area.

13.3.3.2  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.

13.3.4  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
   // This is equivalent to the C statement: x = 0x8888
   ld hl, 0x8888
   ld (sp+@SP+x), hl

   // This is equivalent to the C statement: z = 0x9999
   ld hl, 0x9999
   ld (sp+@SP+z), hl


   // @SP+i gives the offset of i from the stack frame on entry.
   // On the Rabbit, this is how HL is loaded with the value in i.
   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 keyword
   // nouseix, 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
}

13.3.5  The Disassembled Code Window

A program may be debugged at the assembly level by opening the Disassembled Code window (aka, the Assembly window). Single stepping and breakpoints are supported in this window. When the “Disassem­bled Code” window is open, single stepping occurs instruction by instruction rather than statement by statement. The figure below shows the “Disassembled Code” window for the example code, asm1.c.

Figure 13.2  Disassembled Code Window

discodewin2.png

The Disassembled Code window shows the memory address on the far left, followed by the opcode bytes, 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 bottom of the window when the block is selected. The total assumes one execution per instruction, so the user must take looping and branching into consideration when evaluating execution times.

13.3.6  Local Variable Access

Accessing static local variables is simple because the symbol evaluates to the address directly. The follow­ing code shows, for example, how to load static variable y into HL.

ld hl,(y)              ; load hl with contents of y

 13.3.6.1 Using the IX Register as a Frame Pointer

Using IX as a frame pointer is a convenient way to access stack variables in assembly. Using SP requires extra bookkeeping when values are pushed on or popped off the stack.

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,hl

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

 13.3.6.2 Using Index Registers as Pointers to Aggregate Types

The members of Dynamic C aggregate types (structures and unions) can be accessed from within a block of assembly code using any of the index registers:

The assembly notation for accessing a member of a structure or union is:

  ( index_register + [ aggregate_type_reference ] + member_name )

where aggregate_type_reference may be any one of a typedef for, an instance of, or a pointer to an instance of the aggregate type. If member_name is an aggregate type (e.g. a nested structure) then members of the nested aggregate type are accessed as follows:

  ( index_register + [ aggregate_type_reference ] + member_name + member_of_member_name )

where member_of_member_name is a member of struct member_name which is itself a member of the aggregate_type_reference. To access additional levels of nested structures, add "+ member_name" as nec­essary.

The following Rabbit 4000+ example illustrates assembly code access of both near data and far data in both a base structure and its nested structure, using a mix of struct typedef, struct pointer and struct instance references:

typedef struct {
  int x;
  int y;
} TNest;

 

typedef struct {
  TNest nest;
  long time;
} TStruct;

 

void func(TStruct *s, TStruct far *t)
{
#asm nodebug
  ; e.g. use IY to access near (root) data:
  ld    iy, (sp+@SP+s)
  ld    hl, (iy+[TStruct]+nest+y)
;   . . .
  ; e.g. use PW to access far data:
  ld    pw, (sp+@SP+t)

  ld    bcde, (pw+[t]+time)

;   . . .
#endasm
}

 

void main(void)
{
  auto TStruct s_local;
  static far TStruct t_local;

 

  _n_memset(&s_local, 0, sizeof s_local);
  s_local.nest.y = 0x1234;

 

  _f_memset(&t_local, 0, sizeof t_local);
  t_local.time = 0x12345678;

 

  func(&s_local, &t_local);

 

#asm nodebug
  ; e.g. use IY to access near (root) data:
  ld    iy, @SP+s_local
  add   iy, sp
  ld    hl, (iy+[s_local]+nest+y)
;   . . .
  ; e.g. use PW to access far data:
  ld    pw, t_local

  ld    bcde, (pw+[t_local]+time)
;   . . .
  ; e.g. use PW to access near (root) data:
  ld    hl, @SP+s_local
  add   hl, sp
  ldl   pw, hl
  ld    hl, (pw+[t_local]+nest+y)
;   . . .
#endasm
}

 13.3.6.3 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 vari­ables 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 dis­cussed previously can be used because the compiler maintains the offsets automatically.

13.4 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.

13.4.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.

13.4.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 a long, unsigned long, or float, return the result in BCDE (register B contains the most significant byte). A C function containing embedded assembly code may, of course, use a C return statement to return a value. A stand-alone assembly routine, however, must load the primary register with the return value before the ret instruction.

13.4.3  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, how­ever, 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 type struct s.

typedef struct ss {
   int f0;                  // first field
   char f1;                 // second field
} xyz;
xyz my_struct;
   ...
my_struct = func();
   ...
xyz func(){
#asm
   ...
   xor a                      ; clear register A.
   ld hl,@SP+@RETVAL+ss+f1    ; hl <- the offset from SP to f1 field of returned struct
   add hl,sp                  ; hl now points to f1.
   ld (hl),a                  ; load a (now 0) to f1.
   ...
#endasm
}

It is crucial that @SP be added to @RETVAL because @RETVAL is an offset from the frame reference point, not from the current SP.

13.5 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.

  1. 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.)

  2. If the function return is a struct, reserve space on the stack for the returned structure. Most func­tions do not return structures.

  3. Compute and push the last argument, if any.

  4. Compute and push the second to last argument, if any.

  5. Continue to push arguments, if there are more.

  6. 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, and float) if it is of a primitive type.

  7. Issue the call instruction.

The caller must unwind the stack after the function returns.

  1. Recover the stack storage allocated to arguments. With no more than 6 bytes of arguments, the pro­gram 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.

    ; Note that HL is changed by this code!
    ; Use “ex de,hl” to save HL if HL has the return value
    ;;;ex de,hl      ; save HL (if required)
       ld hl,36      ; want to pop 36 bytes
       add hl,sp     ; compute new SP value
       ld sp,hl      ; put value back to SP
    ;;;ex de,hl      ; restore HL (if required)

  2. If the function returns a struct, unload the returned structure.

  3. Restore registers previously saved. Pop them off if they were stored on the stack.

  4. If the function return was not a struct, obtain the returned value from HL or BCDE.

13.6 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 regis­ters it uses.

13.6.1  Steps Followed by an ISR

The CPU loads the Interrupt Priority register (IP) 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:

  1. 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.

  2. Push and pop the LXPC as a defensive programming strategy to avoid corrupting large memory support. For example, the LCALL instruction clears the LXPC so it is essential that this register is saved before issuing an LCALL and restored after the LRET.

  3. Determine the cause of the interrupt. Some devices map multiple causes to the same interrupt vec­tor. An interrupt handler must determine what actually caused the interrupt.

  4. Remove the cause of the interrupt.

  5. If an interrupt has more than one possible cause, check for all the causes and remove all the causes at the same time.

  6. 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.

  7. 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.

  8. The interrupt priority level must be restored immediately before the return instructions ret or reti. If the interrupts are enabled earlier, the system can stack up the interrupts. This may or may not be acceptable because there is the potential to overflow the stack.

  9. Return. There are two types of interrupt returns: ret and reti.

The value in IP is shown in the status bar at the bottom of the Dynamic C window. If a breakpoint is encountered, the IP value shown on the status bar reflects the saved context of IP from just before the breakpoint.

13.6.2  Modifying Interrupt Vectors

This section will discuss how to modify the interrupt vectors after they have been set up. For detailed information about how the interrupt vectors are set up and operate, please see the Rabbit 4000 Designer’s Handbook.

Users can modify interrupt vector code under all program models in one of two ways

As noted, the 8-bit CPU registers are called IIR and EIR corresponding to internal interrupts and external interrupts, respectively. Likewise, the macros are called INTVEC_BASE and XINTVEC_BASE. When Rabbit's BIOS finishes initial tasks, the macros and registers correlate directly. Therefore, if a user applica­tion does not modify the interrupt vector registers then that user may employ the macros for the entire pro­gram execution. If the application alters the interrupt vector registers during execution (not recommended practice), however, the application must use the values of those registers instead of the macros.

For detailed information on the operation of interrupt vectors, consult the chip manual for your board, e.g., The Rabbit 4000 Microprocessor User’s Manual. The remainder of this section explains how to modify interrupt vectors after initialization.

In C, the user can modify interrupt vectors through SetVectIntern() and SetVectExtern(). In assembly, the user accomplishes the same through INTVEC_BASE + <vector offset> or XINTVEC_BASE + <vector offset>. The possible values for <vector offset> are defined as macros in lib\..\bioslib\sysio.lib, listed below for convenience:

Table 13-3.  Internal Interrupts and their Offset from INTVEC_BASE

INPUTCAP_OFS        

SERC_OFS          

NETA_OFS

SERD_OFS

PERIODIC_OFS

SERE_OFS

PWM_OFS

SERF_OFS

QUAD_OFS

SLAVE_OFS

RST10_OFS

SLV_OFS

RST18_OFS

SMV_OFS

RST20_OFS

SYSCALL_OFS

RST28_OFS

TIMERA_OFS

RST38_OFS

TIMERB_OFS

SECWD_OFSS

TIMERC_OFS

SERA_OFS

WPV_OFS

SERB_OFS

  

 

Table 13-4.  External Interrupts and their
Offset from XINTVEC_BASE

BKPT_OFS            

DMA5_OFS            

DMA0_OFS

DMA6_OFS 

DMA1_OFS

DMA7_OFS

DMA2_OFS

EXT0_OFS

DMA3_OFS

EXT1_OFS

DMA4_OFS

 

The following code fragments set up the interrupt service routine for the timer B interrupt:

#asm
   ;*** Dynamic Method ***
   clr hl
   ld a, iir
   ld h, a
   ld de, TIMERB_OFS                  ; Load offset of interrupt
   add hl, de
   ld de, 0xC3 | timerb_isr << 8      ; Jump opcode and LSB of address.
   Ld bc, timerb_isr >> 8 ; MSB of address
   ld (hl), bcde
#endasm

#asm
   ;*** Static Method ***
   ld a, 0xC3                         ; Jump opcode
   ld hl, timerb_isr                  ; Service routine
   ld (INTVEC_BASE + PERIODIC_OFS), a
   ld (INTVEC_BASE + PERIODIC_OFS + 1), hl
#endasm

The static method shown above is equivalent to using SetVectIntern() or SetVectExtern(), although these functions perform more safety checks that writing assembly code would circumvent. Please see the Dynamic C Function Reference Manual for more information on using SetVectIntern() and SetVectExtern().

13.7 Common Problems

If you have problems with your assembly code, consider the possibility of any of the following situations:

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.

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.

; SP still points to the low boundary of the call frame
push hl             ; save HL

; SP now two bytes below the stack frame!
...
ld hl,@SP+x+2       ; Add 2 to compensate for altered SP
add hl,sp           ; compute as normal
ld a,(hl)           ; get the content
...
pop hl              ; restore HL

; SP again points to the low boundary of the call frame

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 them­selves.

Jump relative (JR) instructions allow easier code relocation because the jump is relative to the current program counter. For example, RAM functions are usually written in assembly and are relocated to RAM from flash. A jump (JP) instruction would not work in this case because the jump would be to a flash location and not the intended RAM location. Using JR instead of JP will jump to the intended RAM location.