5.5 Using a Compiled Stack

Not to be confused with a software stack, which is a dynamic stack allocation accessed via a stack pointer, a compiled stack is a memory area that is designated for the static allocation of local objects that should only consume memory for the duration of the routine with which they are associated. This is similar to the stack-based auto and parameter objects defined by functions in C programs. When using the PIC Assembler, the allocation of memory on the stack is performed by the linker. The stack-based objects requested by each routine are assembled into blocks. Blocks from routines that are not active at the same time can be overlaid in memory, based on information in the program's callgraph, which is constructed from directives added to your program.

A more detailed example of using a compiled stack with the PIC Assembler is provided in the MPLAB XC8 PIC Assembler User's Guide for Embedded Engineers. A summary of this information is provided here.

The FNCONF directive (see 4.9.25 Fnconf Directive) should be used once per program. It's three arguments indicate to the linker 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 parameter objects.

The FNROOT directive (see 4.9.27 Fnroot Directive) should be used once for each routine that forms the root node in a callgraph. 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 FNSIZE directive (see 4.9.28 Fnsize Directive) should be specified for each routine that needs to have objects plaed on the compiled stack. It 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. The directive can be placed anywhere in your code, but it is often located near the routine it configures.

The FNCALL directive (see 4.9.24 Fncall Directive) is used as many times as required to indicate which routines (that use the compiled stack) are called and from where. From this information, the linker can form the callgraph. As you develop your program, you will need to ensure that there is an FNCALL directive for each unique call that takes place in your code. If a called routine does not define any compiled stack objects, the directive is not required for that routine, but it is good practice to include it anyway, in case there are subsequent changes made to the program.

The linker will create the special symbols to be used for auto-like and parameter objects for all routines that used the FNSIZE directive. The following example of a cut-down program shows the stack being set up, with the main and add routines requesting that they each require space on the stack. The main routines reads the special symbols created by the linker for its auto-like objects; the add routine uses special symbols for its parameters..
FNCONF udata_acs,?au_,?pa_        ;setup the stack
FNROOT main                       ;the main routine is a callgraph root

PSECT code
FNSIZE main,4,0                   ;the main routine needs 4 bytes of auto-like objects
GLOBAL ?au_main                   ;make the symbol created by the linker globally accessible
main:
    ...
loop:
    movff   ?au_main+0,?pa_add+0  ;load the first byte of the first parameter
    movff   ?au_main+1,?pa_add+1  ;load the second byte of the first parameter
    movff   ?au_main+2,?pa_add+2  ;load the first byte of the second parameter
    movff   ?au_main+3,?pa_add+3  ;load the second byte of the second parameter
FNCALL main,add                   ;the main routine will call the add routine in the callgraph
    call    add                   ;the actual call
    ...

FNSIZE add,0,4                    ;the add routine needs 4 bytes of parameters
GLOBAL ?pa_add                    ;make the symbol created by the linker globally accessible
add:
    movf    ?pa_add+2,w,c         ;the add routine uses its parameter symbols
    addwf   ?pa_add+0,f,c
    ...