Translating C to PIC: Functions

Properties of the call conventions

On the PIC24, the function call conventions described in the MPLAB XC16 C Compiler User’s Guide are followed for all function calls generated by the C compiler.

Register usage

Parameter passing

If possible, function arguments are passed in registers W0 to W7. 8-bit and 16-bit parameters are passed in a single register, 32-bit parameters are passed in two registers, and 64-bit parameters are passed in four registers.

The parameters are placed, in order, in the registers W0 to W7. If a parameter cannot fit within the remaining registers, it is placed on the stack in right-to-left order. It is possible for the first and third arguments to be passed in registers, while the second argument is passed on the stack.

In C, an array is passed as a pointer to its first element. If space permits, a structure is passed in sequential registers, even if the structure contains arrays. Otherwise the address of the structure is passed.

C also permits variadic functions, such as printf, which have a variable number of arguments. Effectively these functions receive a list of parameters as its last argument (written as ... in the function header). This variably-size parameter list, which always includes the last parameter before the ..., is never placed in registers.

An example

struct sillyStruct { int8_t buff[80] } ;

int16_t f(int8_t a, int32_t b, struct sillyStruct c, int32_t d[80], int16_t e) ;
parameter location
a W0
b W2:W1
c on stack (offset -86)
d W3 (as address)
e W4

Return value

The simpler return values, such as numbers, are returned in registers W0 to W3 as needed. A 16-bit value, such as an uint16_t, will be returned in W0. A 64-bit value, such as a double, will use all four registers.

When an aggregate value, such as a struct or an union, is returned from a function; W0 will contain the address of the returned value.

Stack frame

When programs are compiled under the XC16 compiler, the default action for function invocation is to allocate a stack frame. The frame pointer, the address of the base of the stack frame, is contained in W14. Since W14 is a callee saved, there is no problem with calling routines that don’t allocate a stack frame.

Here is the order for storing information on the stack.

purpose size in bytes
Parameters that couldn’t fit in registers varies, but typically 0
saved PC, return address 4
saved R14, dynamic link 2
local variables, saved registers, arguments to other procedures varies

By definition, the stack frame begins at the address contained in the frame pointer. On the PIC24 with the XC compilers, this is the location of the first local variable. This means that local variables and saved registers are typically addressed as positive offsets from R14, such as [R14+2]. If any parameters have been passed on the stack, they will be addressed as negative offsets, such as such as [R14-8].

For a small gain in efficiency, it is possible to avoid allocating a stack frame. In this case variables are accessed at negative offsets from the stack pointer.

Actions of the called function

Function prologue

On entry to the function, all of its arguments are either contained in registers or stored near the top of the stack. At the very top of the stack is the two-word return address.

When a stack frame is being used, the first instruction of a function is
      LNK   #n
where n is the amount of storage needed for local variables and saved registers. The LNK #n instruction performs the following actions:

  1. Pushes W14, the present frame pointer, onto the stack. This now becomes the dynamic link.
  2. Sets W14 to the address of the top of the stack. This now becomes the bottom of the new stack frame.
  3. Increases the stack pointer, W15, by n. This creates space for local variables and saved registers within the stack frame.

The other action usually performed during the function prologue is saving registers. If the function modifies any callee-saved registers, W8 to W13, they must be saved. Also, the function usually needs to save any caller-saved registers used to pass parameters in its own function calls. Otherwise, these parameters will be lost.

Inside the function

All local variables stored in the stack frame will be accessed by positive offsets of W14, the frame pointer. Most parameters will be accessed in the registers in which they were passed, but those that are stored on the stack will be accessed by negative offsets of the frame pointer.

Function epilogue

Before the function exits, any modified callee-saved registers must be restored and return values must be placed in appropriate registers.

Then the function calls the ULNK instruction which does the following:

  1. Copies the frame pointer, W14, to the stack pointer, W15. This deallocates the present stack frame.
  2. Pops the top of the stack, which now contains the dynamic link, into the frame pointer. This restores the stack frame of the caller function.

The only remaining action of the epilogue is to call the RETURN instruction which pops the two words containing the return address into the program counter. This causes control to transfer back to the calling function.

Actions of the calling function

Function invocation

Before the function is called, all its arguments must be evaluated and placed in the appropriate registers or pushed on the stack. Now a call instruction, typically RCALL, will be made. The call instruction will push the present program counter, the address of the next instruction, onto the stack and then set the program counter to its argument.

The function will also need to save copies of any caller-saved registers it needs after the call is completed.

Function return

If any parameters were passed on the stack, the calling function should adjust the stack pointer to “remove” them. Then, the calling function may also copy the return value to its own local storage. Finally, the calling function may need to restore any caller-saved registers it was using.

An example

Let’s look at an example, an inefficient recursive function for squaring a positive number.

int square(int n) {
    int r = 0 ;
    if (n != 0) {
        r = square(n-1) + n + n - 1 ;
    return r ;

Implemented on the PIC

Here’s the code generated by the XC16 compiler at optimization level 0, with a few comments added by me.

     LNK     #0x4
     MOV     W0, [W14+2]            ;; n is saved is [W14+2]
     CLR     W0
     MOV     W0, [W14]              ;; r is saved in [W14]
     MOV     [W14+2], W0            ;; testing if n == 0
     SUB     W0, #0x0, [W15]
     BRA     Z, 1f
     MOV     [W14+2], W0
     DEC     W0, W0
     RCALL   square                 ;; calling square with n-1
     MOV     [W14+2], W1
     ADD     W0, W1, W1
     MOV     [W14+2], W0
     ADD     W1, W0, W0
     DEC     W0, [W14]
1:   MOV     [W14], W0

This version is a shorter.

;; start of prologue
     LNK     #2
     MOV     W0, [W14]              ;; n is saved is [W14]
;; end of prologue
     CLR     W7                     ;; r is saved in W7
     CP0     W0                     ;; testing if n == 0
     BRA     Z, 1f
;; start of invocation
     DEC     W0, W0                 ;; Setting first parameter to n-1
     RCALL   square                 ;; calling square with n-1
;; end of invocation
;; On return, W0 is square(n-1)
     MOV     [W14], W6              ;; must restore the old n (as W6)
     ADD     W0, W6, W7             ;; W7 == square(n-1) + n
     ADD     W7, W6, W7             ;; W7 == square(n-1) + n + n
     DEC     W7, W7                 ;; W7 == square(n-1) + n + n - 1
;; start of epilogue
1:   MOV     W7, W0
;; end of epilogue

Implemented in Java

If you put the keyword static in front of the square function header, you get a Java static method. Here is the JVM bytecode for that method. When this method is running, local variable 0 is parameter n and local variable 1 is r. The operand stack is shown to the right of the JVM instructions. Also, square is static method 2 of the class.

  static int square(int);
       0: iconst_0                  //      [[ 0
       1: istore_1                  //      [[
       2: iload_0                   //      [[ n
       3: ifeq          19          //      [[
       6: iload_0                   //      [[ n
       7: iconst_1                  //      [[ n               1
       8: isub                      //      [[ n-1
       9: invokestatic  #2          //      [[ sq(n-1)
      12: iload_0                   //      [[ sq(n-1)         n
      13: iadd                      //      [[ sq(n-1)+n
      14: iload_0                   //      [[ sq(n-1)+n       n
      15: iadd                      //      [[ sq(n-1)+n+n
      16: iconst_1                  //      [[ sq(n-1)+n+n     1
      17: isub                      //      [[ sq(n-1)+n+n-1
      18: istore_1                  //      [[
      19: iload_1                   //      [[ r
      20: ireturn

Chapter 6 of the JVM specification contains a detailed description of the Java Virtual Machine instruction set.

Calling main

We do need some assembly language code to get a C program running.

Typical C startup with crt0 on a multi-user system

Implemented on the IBM/360

Mark Smotherman of Clemson University is an expert on the history of computer architecture. His page on IBM S/360 Subroutines describes a calling convention that does not use a stack. This dates from a time when recursive subroutines were considered just a tad too radical.

Startup for calling main on the PIC

If the PIC assembly language programmer does not provide a __reset entry point, the following actions will be performed to call main:

This list is a summary of a more detailed description in the MPLAB® XC16 Assember, Linker and Utilities User’s Guide.