The limited stack size of budget PICs is a problem area and I have adjusted my code to accommodate this reality. I currently adopt a rough paradigm of grouping closely related functions into a module and declaring all variables global static in the module (to reduce the amount of variables stored in the auto psect, and issues of mutability are only relevant in ISRs, which I account for.) I don't do this because it is good practice, but the reality is you have a finite amount of space to allocate all local function vars that exist in an entire project.

In the embedded world of 8/16 bit chips, is this an appropriate method, provided I'm sure to take necessary precautions? I also do things like allocate > 256 bytes of RAM for Ethernet buffers and have to access that memory via pointers so I can avoid the semantics of memory banking. Am I doing it wrong? My app works, but I am 100% open to suggestions for improvement.

  • \$\begingroup\$ Just some constructive criticism about your post. I think many people will gloss over your question because of such a big paragraph. You might want to consider editing your post to have some line breaks at meaningful points to make it easier to read. This will probably result in you getting better answers. \$\endgroup\$
    – Kellenjb
    Nov 4, 2010 at 4:33
  • \$\begingroup\$ Point taken, as well as corrective action :) \$\endgroup\$
    – Nate
    Nov 4, 2010 at 14:18
  • \$\begingroup\$ If your app works, that's great! "If it's stupid but it works, it's not stupid." -- Murphy's Law of Combat #6 \$\endgroup\$
    – davidcary
    Mar 14, 2011 at 1:20
  • 2
    \$\begingroup\$ "works" != "done" != "maintainable" \$\endgroup\$
    – Jason S
    Apr 18, 2012 at 3:07

3 Answers 3


Maybe i'm missing something here (paragraphs would help) but with the C18 compiler local variables within a function are generally allocated on the software stack so i have no idea what the following means:

but the reality is you have a finite amount of space to allocate all local function vars that exist in an entire project

By moving all your variables to globals within a module you are requiring there is space for all of them at the same time.

buffers and have to access that memory via pointers so I can avoid the semantics of memory banking.

What compiler are you using?

  • \$\begingroup\$ It seems he needs to increase stack allocation, I thought even c18 allowed that. It does cost money for a corporation to use and should have nice optimizer and so forth. I have never seen a cost money compiler not allow stack allocation. \$\endgroup\$
    – Kortuk
    Nov 4, 2010 at 4:55
  • 1
    \$\begingroup\$ PIC18 devices have a fixed stack. We use the Hi-Tech PICC18 compiler which actually optimizes the stack for me. The problem is, it is still possible to slow execution if care is not taken to keep call depth short. \$\endgroup\$
    – Nate
    Nov 4, 2010 at 13:54
  • 1
    \$\begingroup\$ i will look into this. if i was your boss i would have purchased a new compiler a while ago. \$\endgroup\$
    – Kortuk
    Nov 4, 2010 at 15:38
  • \$\begingroup\$ @nate you've still lost me, why would your solution to having a fixed length, relatively short stack be to make everything global so it takes up a memory location all the time rather than just when its needed on the stack? You should minimize call depth and use as many local variables as you can within the target size of your stack to minimize memory use. If theres one function that has a deep chain thats causing your stack to be large, optimize stack usage in that one function, not the entire project. \$\endgroup\$
    – Mark
    Nov 4, 2010 at 22:45
  • 1
    \$\begingroup\$ @Mark: My thinking is, if I have a display width of 21 characters on my LCD, and have a module that needs access to such a buffer in multiple functions, isn't it a better idea for me to declare it global static within the module and have all the functions use the same 21 bytes? I should clarify that I have plenty of RAM available. My thinking is, by reserving 21 bytes for a buffer within a module that can serve many functions, I actually require 1 byte on the stack (a pointer), instead of incurring the possibly problematic situation of a call depth > 3 and the associated overhead. \$\endgroup\$
    – Nate
    Nov 6, 2010 at 2:35

There are two approaches used for variable allocation on PIC compilers. Some use a software stack indexed off FSR2, while others simply use statically-overlaid variables. Both approaches have advantages and disadvantages. Using overlaid variables means that there is no possibility of a stack overflow at run-time. It also means that one can have roughly 64-128 bytes' worth of global variables that can be accessed from any instruction without having to worry about banking. Unfortunately, it precludes recursion, makes certain situations involving function pointers difficult, and often leads to code which is bloated with movlb instructions because compilers are often not very good at arranging banks efficiently.

The best arrangement of variables will depend upon what sort of compiler you're using. Unfortunately, code optimized for one compiler will often work poorly on another.

BTW, I have no idea why Microchip can't make a chip which would e.g. provide 16 bytes of indirect addressing off FSR2 and eight bytes each off FSR0 and FSR1, while leaving 64-96 bytes of the "common bank" available for user storage, but for whatever reason most of the PICs I've seen make the use of the "common" area an "all-or-nothing" proposition despite the fact that few routines are going to need more than 16 bytes of local stack frame.

  • \$\begingroup\$ Actually the C18 compiler uses a software stack indexed off of FSR1, not FSR2. \$\endgroup\$ Apr 18, 2012 at 18:28
  • \$\begingroup\$ @OlinLathrop: Even on parts with Extended Instruction Set? The Extended Instruction Set allows any of the first 64-96 bytes following the address given in FSR2 to be accessed directly, which would be useful if FSR2 was used as a software stack pointer. Code which tried to use FSR1 as a software stack pointer would be pretty hideous--is that really what PIC18 does!? \$\endgroup\$
    – supercat
    Apr 19, 2012 at 15:51
  • \$\begingroup\$ I don't know what C18 does when the extended instruction set is in use. I do know that for the normal instruction set is uses FSR1 as the stack pointer and FSR2 as a frame pointer. The application is only left with FSR0 for its own use. Yes, I think this sucks too. \$\endgroup\$ Apr 19, 2012 at 16:27
  • \$\begingroup\$ @OlinLathrop: That is mind-bogglingly horrible. Having both a stack pointer and frame pointer assembly-language coding more convenient (since local variables will live at a constant offset from the frame pointer), but at any given spot in the code the difference between the two pointers will generally be constant, so a compiler shouldn't need both pointers [if SP is known to be four bytes below where BP would be, code which would access (BP+6) can instead access (SP+10)]. Does C18 use an eight-word prologue and eight-word epilogue on every routine to handle the frame pointer? \$\endgroup\$
    – supercat
    Apr 19, 2012 at 16:37

What you describe is not a good idea. However, if you have it working in a particular project and it has been well tested, then leave it alone.

I generally allocate variables on a PIC 18 four different ways:

  1. Globals. These are the few values that must be visible at the system level outside the individual modules. Another way to look at it is that these are the values used to communicate between modules. If you find a large fraction of your state needs to be global, then you may not have partitioned the system into modules well.

    A example of global state could be final filtered A/D values. Analog signals are read faster than the results are needed in the AD module. This applies two poles of low pass filtering to each signal and also possibly some scaling. The filter state is private to the AD module with only the final filtered and scaled values declared as global since these are all the rest of the system needs to know about.

    I usually use the access bank for global state. This should be a collection of individual variables from various modules, so usually fits easily. If you need to export whole buffers for some reason, then these should each be in their own section, and of course any other module accessing such buffers needs to know they are not in the access bank.

  2. Static locals. These hold the values that need to be persistant, but are private to individual modules. In the example above, the intermediate filter values would be in this catagory. They need to stick around between calls to the module, or are used to communicate between separately callable routines of the module.

    I usually put these in banked memory, with all local state of a module in the same bank. I define the constant LBANK (local bank) at the top of the module to define which bank the local variables of that module will be in. This is guaranteed by defining linker sections in the linker file .BANKn where N is the bank number. Then in the code variables are defined in those named banks to ensure the linker will place them there. Knowing the bank at build time is useful to allow for smart bank management.

  3. Dynamic. I keep a data stack and use FSR2 as the stack pointer. With the right choice of stack layout, pushing and popping RAM bytes can be done with single instructions. I wrap these in push and pop macros so I don't have to think about the stack mechanics every time and to make the code more readable.

    C18 does this with what are called "automatic" variables in C. However, while C18 seems to generate reliable code, its choice of memory management can only be called brain dead at best. There are only 3 FSRs, making them precious. C18 grabs two of these for its own use, leaving the application only with FSR0. C18 does have a data stack, but incredibly, the stack layout is chosen so that push and pop aren't both single instructions. It also has a caller-clean argument passing model, which needlessly eats memory for most normal subroutine calls, but that's a digression for another day.

  4. General registers. I usually define the first 16 bytes of memory as globals called REG0-REG15. Since these are in the access bank, they can be used much like general registers of other machines. These are the workhorses of temporary state. I also use them for subroutine arguments and return values most of the time. For example, the UART_PUT subroutine sends the byte in REG0, and the UART_GET subroutine returns the next received byte in REG0. Subroutines generally preserve these except those they explicitly return data in. These are the temporary scratch values used internal to subroutines. To help subroutines preserve them, I have macros so that you list the set of these general registers that a subroutine will trash in one place in the subroutine definition. These are then automatically pushed onto the data stack on subroutine entry and popped right before the return.

  • \$\begingroup\$ I find myself annoyed that HiTech C doesn't have a memory model/calling convention which recognizes one 256-byte bank as the "default", and requires that subroutines leave that bank selected on exit (but can assume it's selected on entry). A lot of applications could fit their commonly-used non-array variables in 320-384 bytes, avoiding 90% of banking instructions, if such a convention were available. \$\endgroup\$
    – supercat
    Apr 18, 2012 at 14:57
  • \$\begingroup\$ @supercat: Setting the bank is only one instruction on a PIC 18, which is less significant if you're already calling a subroutine anyway. It takes at least 4 cycles to get into and out of a subroutine, plus whatever cycles work the subroutine does. Forcing it to a particular bank on exit could just as well waste instructions as save them. My system uses the model that the bank is unknown on entry and that subroutines are allowed to trash the bank, unless of course the subroutine is specifically documented to do something different. \$\endgroup\$ Apr 18, 2012 at 15:39
  • \$\begingroup\$ My concern isn't so much with time as code size; only if a routine which requires an extra instruction on exit to set the "main" bank (used for params, locals, and some globals if they fit) is never code whose first following banked access would be to that bank, would a convention requiring that routines set that bank on exit not be a net "win". Requiring the bank be set on entry may be more nebulous, but in its favor would be the fact the last instruction preceding many calls is apt to be a "movwf" to a parameter stored in that bank. \$\endgroup\$
    – supercat
    Apr 18, 2012 at 17:16
  • \$\begingroup\$ I'm not saying all projects should necessarily use such a convention, but if a project uses a total of less than about 320 bytes (and many do), there should be no need for any banking instructions. If all but a few small parts fit within a 320-byte space, those parts might need more banking code on entry/exit than they otherwise would, but there could still be a net win. It's been awhile since I profiled my code, but despite the fact that I try to be mindful of the PIC's architecture, I think I profiled one of my applications... \$\endgroup\$
    – supercat
    Apr 18, 2012 at 17:19
  • \$\begingroup\$ ... as being 20% bloated by either banking instructions or MOVFF instructions to/from WREG. The PIC's single-address architecture is a good thing when it means that all the memory one needs is in the "working set" most of the time. But unless a compiler does a good job with banking, that's often not going to be the case. BTW, I just thought of a way of massively improving the PIC's usefulness, given space for 512 opcodes: take 32 bytes of the address space which are presently used for the overly-large FSR2 stack frame and have them dereference them 1-16 bytes each off FSR0 and FSR1. \$\endgroup\$
    – supercat
    Apr 18, 2012 at 17:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.