6.1 Compiled Stack Directives

The assembler directives that control the compiled stack all begin with the letters FN.

The FNCONF directive is used once per program. It's three arguments indicate the name of the psect that should be used to hold the compiled stack, the symbol prefix to be used for auto-style objects, and the symbol prefix to be used for parameters objects.

In this example, the directive reads:
FNCONF udata_acs,?au_,?pa_
which indicates that the udata_acs psect (located in Access bank data memory) will be used to hold all the objects on the stack. The linker will form the stack in one contiguous chunk of memory. The location of the psect holding the stack will affect how the stack objects must be accessed. If there are a large number of stack-based objects and particularly if they are accessed often, placing the stack in the PIC18 Access bank will mean they can be accessed without any bank selection instructions. If the stack becomes too large, however, it will need to be placed in banked memory. Mid-range and Baseline devices have little common memory, and this might be needed for other purposes, so banked memory is often used for the compiled stack on these devices.
The ?au_ symbol specified as the second argument to the FNCONF directive will be prefixed to the name of a routine to create the base symbol for that routine's auto-style objects. The ?pa_ prefix will be used to form the base symbol for each routine's parameter objects. These symbols are illustrated in the add routine, which begins with the following code:
PSECT code
;add needs 4 bytes of parameters, but no autos
FNSIZE add,0,4       ;two 2-byte parameters
GLOBAL ?pa_add
;add the two 'int' parameters, returning the result in the first parameter location
add:
    movf       ?pa_add+2,w,c
    addwf      ?pa_add+0,f,c
The FNSIZE directive takes three arguments, those being the name of a routine, the total number of bytes required for that routine's auto-like objects, and the total number of bytes for its parameter-like objects. In this case, the FNSIZE directive indicates that the add routine needs no auto-style objects and 4 bytes of parameters. The directive can be placed anywhere in your code, but it is often located near the routine it configures.

The linker will automatically create a symbol associated with the block of 4 bytes used by the routine's parameters. In this case, that symbol will be ?pa_add, based on the prefix used with the FNCONF directive. Although this symbol is defined by the linker, it still needs to be declared in each module that needs it. This has been done in the above code by the GLOBAL ?pa_add directive. Each byte of the parameter memory can be accessed by using an offset from the ?pa_add symbol. The code above shows the first and third bytes of this memory being accessed. What these 4 bytes represent is entirely up to you. In the example, the parameter memory is used to hold two, 2-byte-wide objects, but the same FNSIZE arguments could instead be used to create one, 4-byte-wide object, for example.

Later in the example code, the following code begins the definition of the main routine.
GLOBAL ?au_main
GLOBAL result
result  EQU    ?au_main+0          ;create an alias for this auto location
  
GLOBAL incval
incval EQU     ?au_main+2          ;create an alias for this auto location
  
FNROOT main                        ;this is the root of a call graph
FNSIZE main,4,0                    ;main needs two 2-byte 'autos' (for result and incval)
FNCALL main,add                    ;main calls add
FNCALL main,incr                   ;main calls incr

PSECT code
main:
    clrf       result+0,c
    clrf       result+1,c

The main routine requires four bytes of auto objects and so the FNSIZE directive has again been used to indicate this. The first byte of main's auto area can be accessed using the symbol ?au_main; however, in this instance, an EQU was defined so that the more readible name result could be used, as shown in the clrf instructions.

To be able to allocate memory on the stack, the linker needs to know how the program is structured in terms of calls. To allow it to form the program's call graph, several other directives are used.

The FNROOT directive, shown above, indicates that the specified routine forms the root node in a callgraph. (Alternatively, the resetVec label could have also been as the root node—it make no difference in this example.) The memory allocated to stack objects can be overlapped with that of other routines within the same callgraph, but no overlapping will take place between the stack objects of routines that are in different callgraphs. Typically you will define one callgraph root for the main part of your program and then one for each interrupt routine. This way, the stack memory associated with interrupt routines is kept separate and no data corruption can occur.

The FNCALL directive is used as many times as required to indicate which routines are called and from where. From this, the linker can form the callgraph nodes. In the above code sequence, the FNCALL directive was used twice. The first indicates that the main routine calls add; the second that main calls incr. As you develop your program, you need to ensure that there is an FNCALL directive for each unique call that takes place in the code. If the called routine does not define any compiled stack objects, the directive is not required, but it is good practice to include it anyway, in case there are changes made to the program.

You may devise whatever convention you like to pass arguments to routines. In the add routine, the LSB of the first parameter has an offset of 0; the MSB of the first parameter an offset of 1, etc., thus the expressions ?pa_add+0 and ?pa_add+1 represent the two bytes of the first 'int' parameter and ?pa_add+2 and ?pa_add+3 represent the two bytes of the second parameter. The first auto-style object defined by main is referenced using the expressions ?au_main+0 (or result+0) and ?au_main+1 (or result+1).

Typically, routines that need to return a value do so by storing that value into the memory taken up by their parameters. As the parameters should no longer be used once the routine returns, this reuse is not normally an issue, but you do need to consider the routine’s return value when you allocate memory for the routine's parameters. The parameter memory you request for a routine using the FNSIZE directive must be the larger of the total size of the routine's parameters and the size of the routine's return value.