5.12.3.1 Writing Stand-alone Assembly Routines

It is possible to write assembly routines that can be called from C code. The following guidelines must be adhered to when writing a C-callable assembly routine.

  • Include the <xc.inc> assembly header file if you need to use SFRs in your code. If this is included using #include, ensure the source file are preprocessed.
  • Select, or define, a suitable psect for the executable assembly code (see Compiler-Generated Psects for an introductory guide).
  • Select a name (label) for the routine using a leading underscore character.
  • Ensure that the routine’s label is globally accessible from other modules.
  • Select an appropriate C-equivalent prototype for the routine on which argument passing can be modeled.
  • If values need to be passed to, or returned from the routine, write a reentrant routine if possible; otherwise use ordinary variables for value passing. The compiled stack cannot be used to pass arguments.
  • Optionally, use a signature value to enable type checking when the function is called.
  • Use bank selection instructions and mask addresses of any variable symbols.

The following example shows a Mid-range device assembly routine that can add an 8-bit argument with the contents of PORTB and return this as an 8-bit quantity. The code is similar for other devices.

#include <xc.inc>
GLOBAL _add            ; make _add globally accessible
SIGNAT _add,4217       ; tell the linker how it should be called
; everything following will be placed into the code psect
PSECT code
; our routine to add to ints and return the result
_add:
    ; W is loaded by the calling function;
    BANKSEL (PORTB)               ; select the bank of this object
    addwf BANKMASK(PORTB),w	; add parameter to port

    ; the result is already in the required location (W) so we can
    ; just return immediately
    return

The code has been placed in a predefined psect, code, that is available after including <xc.inc>. This section is part of the CODE linker class, so it will be automatically placed in the area of memory set aside for code without you having to adjust the default linker options. This section can be used by any device to hold executable code.

If you prefer to create your own psect, you could use the following for a Mid-range device:
PSECT mytext,local,class=CODE,delta=2
The delta flag used with this section indicates that the memory space in which the psect will be placed is word addressable (value of 2), which is true for PIC10/12/16 devices. For PIC18 devices, program memory is byte addressable, but instructions must be word-aligned, so you would instead use a section definition such as the following, which uses a delta value of 1 (which is the default setting), but the reloc (alignment) flag is set to 2, to ensure that the section starts on a word-aligned address.
PSECT text0,class=CODE,reloc=2

See Psect Directive for detailed information on the flags used with the PSECT assembler directive.

The mapping between C identifiers and those used by assembly are described in Interaction between Assembly and C Code. In assembly domain we must choose the routine name _add as this then maps to the C identifier add. Since this routine will be called from other modules, the label is made globally accessible, by using the GLOBAL assembler directive (Global Directive).

A SIGNAT directive (Signat Directive) was used so that the linker can check that the routine is correctly called.

If you want to pass arguments to a C-callable assembly routine, the routine must conform to the scheme used by the compiler to load the arguments; however, the compiled stack cannot be used, even if that is the scheme normally used by the compiler. Compiled stack parameters are allocated unique memory by the compiler when processing the C part of the program. If hand-written assembly code was to define these parameters, they would never be processed and not allocated unique memory, which could lead to data corruption. If the assembly routine only has one byte-sized argument that the compiler would normally pass in the W register, you can have the assembly routine use this register for the argument. See Function Parameters for the convention used to pass arguments. The assembly routine can be written to use the software stack for arguments, but the C prototype for the assembly routine must indicate that the routine is reentrant. See Writing Reentrant Assembly Routines With Parameters for how this can be done. If desired, you can use ordinary C variables to pass arguments and alternatively use a void type for the argument list in the assembler routine's C prototype. Any C code that calls such routines will need to assign the arguments to these variables before calling the routine. The routine can then read the variables as required. In the example above, the assembly routine was written to use the W register for the single argument, allowing the routine to be callable from C code.

The BANKSEL directive and BANKMASK macro have been used to ensure that the correct bank was selected and that all addresses are masked to the appropriate size.

To call an assembly routine from C code, a declaration for the routine must be provided. Here is a C code snippet that declares the operation of the assembler routine, then calls the routine.

// declare the assembly routine so that the call from C code is correct
extern unsigned char add(unsigned char a);

int main(void) {
    volatile unsigned char result;
    result = add(5);  // call the assembly routine
}