Implementing a Soft Stack in Data Memory on the MAXQ2000
OverviewThe MAXQ2000 microcontroller, like other MAXQ devices in Maxim's RISC microcontroller family, is based on the MAXQ20 core. MAXQ20-based microcontrollers typically implement a 16-bit-wide hardware stack with a fixed number of levels (16 on the MAXQ2000) stored in a dedicated internal memory separate from data and code space. This hardware stack is used to save and restore the microcontroller's operating state across subroutine calls and interrupt operations.
While perfectly adequate for small, tightly-focused applications, the hardware stack quickly runs out of space when deeply nested subroutines (or subroutines that save and restore more than a few working registers on the stack) are used in larger assembly applications. Applications written in the C programming language (using compilers such as IAR's Embedded Workbench®) or Rowley Associates' Crossworks for MAXQ avoid this problem by utilizing a "soft stack" contained in data memory. This soft stack stores call/return addresses and local working variables for subroutines. However, there is no built-in mechanism on the MAXQ20 core to locate the stack in data memory which can be used in assembly-only applications.
This application note demonstrates a simple method to implement a soft stack in data memory for assembly-based applications. The code presented in this application note can be used on the MAXQ2000 and other MAXQ20-based microcontrollers. The example code is written using the macro preprocessing features of MAX-IDE, Maxim's project-based application development and debugging environment for the MAXQ family.
The latest installation package and documentation for the MAX-IDE environment are available for free download:
Hardware Stack Operations in the MAXQ20 CoreThere are two different types of stack operations used by the MAXQ20 core:
- PUSH operations (which include the opcodes PUSH, LCALL, and SCALL) are used to store data on the stack. These operations preincrement the stack pointer SP by 1, and then store data at the stack location pointed to by the SP pointer (@SP).
- POP operations (which include the opcodes POP, POPI, RET, and RETI) are used to retrieve data from the stack. These operations retrieve data from the stack location pointed to by SP, and then postdecrement the stack pointer by 1.
Besides the various opcodes which utilize the stack, there are two additional circumstances under which the microcontroller automatically uses the hardware stack:
- When an interrupt is serviced, the current program execution point is pushed onto the stack before execution of the interrupt service routine (which is pointed to by the interrupt vector register IV) begins.
- When certain debugging commands are invoked (such as Read Register and Write Data Memory) which require execution of code in the Utility ROM to complete, the current execution point is pushed onto the stack before control is transferred to the Utility ROM. Once the debug routine in the Utility ROM has finished, the execution point is popped back off the stack and the previous state of the processor is restored.
Note that the MAXQ2000 (as with other devices based on the MAXQ20 core) does not provide any error detection or warning when either of the following stack error conditions occur:
- A stack overflow: occurs when a value is pushed on the stack which is already full. This error causes the oldest value in the stack to be overwritten.
- A stack underflow: occurs when a value is popped from the stack when the stack is empty. This error causes an invalid data value to be returned. For example, if a RET is executed when the stack is empty, execution will be transferred to an incorrect (potentially random) address.
Creating a Soft Stack in Data MemoryThe first step to creating a soft stack in data memory is to define what portion of the data memory will be used. Then the data memory pointer (DP, DP, or BP[Offs]) to track the current location of the top of the stack must be defined. Note: Take care that the application software does not use the data memory dedicated for the stack for other purposes (e.g., variables or buffers).
One simple way to define and initialize such a soft stack involves the BP[Offs] register pair and one equate.
SS_BASE equ 0100h ss_init: move DPC, #1Ch ; Set all pointers to word mode move BP, #SS_BASE ; Set base pointer to stack base location move Offs, #0 ; Set stack to start retIf the base pointer (BP) register is set to the SS_BASE location, then the Offs register can be used to point to the current top of the stack. Since the Offs register is only 8 bits wide, hardware automatically limits the stack to the range (BP .. (BP+255)) in data memory. If a push occurs when Offs is equal to 255 (overflow), or if a pop occurs when Offs is equal to 0 (underflow), then the Offs register will simply wrap around to the other end of the stack. This action simulates the way that the hardware stack operates and allows a simple soft stack implementation; this does not detect underflow and overflow conditions.
The ss_init routine must be called by the main application before the soft stack is used. It sets the BP[Offs] pointer to word mode, which must be done because the soft stack is implemented as a 16-bit wide stack. It also points the BP[Offs] register pair to the beginning of the stack.
Soft Stack OperationsThe built-in op codes which use the stack (PUSH, POP, CALL, RET, etc.) cannot be redefined to use the soft stack, since their meanings are hardwired into the MAXQ assembler. Moreover, it might still be necessary to use the standard hardware stack in certain parts of the application, such as interrupt service routines. For these reasons, the soft stack will be accessed by macros which replicate the standard op codes.
mpush MACRO Reg move @BP[++Offs], Reg ; Push value to soft stack endm mpop MACRO Reg move Reg, @BP[Offs--] ; Pop value from soft stack endm mcall MACRO Addr LOCAL return move @BP[++Offs], #return ; Push return destination to soft stack jump Addr return: endm mret MACRO jump @BP[Offs--] ; Jump to popped destination from soft stack endmWe will now discuss how these macros operate.
mpush <reg>This macro is used in the same manner as the PUSH op code. It allows an 8-bit or 16-bit register, or an immediate value to be pushed to the stack.
mpush A ; Save the value of the A register mpush A ; Save A mpush A ; Save A ... ; code which destroys A-A mpop A ; Restore the value of A (pop in reverse order) mpop A ; Restore A mpop A ; Restore A
mpop <reg>This macro is used in the same manner as the POP op code. It allows an 8-bit or 16-bit register to be loaded from the stack, as shown above. Note that if a 16-bit value is pushed and this value is popped into an 8-bit register, only the low byte will be stored in the register. The high byte will be lost. This is identical to the behavior of the built-in hardware stack.
subroutine: mpush A ; Save the current value of A ... ; Code which destroys A mpop A ; Restore A mret
The mcall macro is used in the same manner as the CALL op code to execute a subroutine. This subroutine must use the mret macro (and not the standard RET op code) to return once it has completed execution.
mcall mySub ... mySub: mpush A ; Save A mpush A ; Save A ... ; Perform calculations, etc. mpop A mpop A mret
Extending the Size of the Soft StackSome applications require a soft stack larger than 256 levels. It is possible to implement a stack of any size (up to the limits of available data memory) by using one of the other data pointers (DP or DP).
SS_BASE equ 0000h SS_TOP equ 01FFh ss_init: move DPC, #1Ch ; Set all data pointers to word mode move DP, #SS_BASE ; Set pointer to stack base location retThe code shown above reserves the locations 0000h through 01FFh in data memory, thus creating a stack that can hold up to 511 levels. (One location in the stack memory space is left unused; this allows a shorter, more efficient implementation of the mpush/mpop/mcall/mret macros).
Simply changing the data pointer and leaving everything else in the code the same will extend the stack. Nonetheless, since DP is not restricted to a certain range in data memory, there is nothing to prevent a push or pop operation from incrementing or decrementing DP beyond the assigned boundaries of the soft stack. To avoid this, some simple underflow/overflow checking can be added.
Adding Underflow and Overflow CheckingTo keep the macros (which are expanded into code each time that they are used) as short as possible, the underflow and overflow checking is performed in subroutines which are called by the macros using the hardware stack.
mpush MACRO Reg call ss_check_over ; Check for possible overflow move @++DP, Reg ; Push value to soft stack endm mpop MACRO Reg call ss_check_under ; Check for possible underflow move Reg, @DP-- ; Pop value from soft stack endm mcall MACRO Addr LOCAL return call ss_check_over ; Check for possible overflow move @++DP, #return ; Push return destination to soft stack jump Addr return: endm mret MACRO call ss_check_under ; Check for possible underflow jump @DP-- ; Jump to popped destination from soft stack endm ss_check_under: push A push AP push APC push PSF move APC, #80h ; Set Acc to A, standard mode, no auto inc/dec move Acc, DP ; Get current value of stack pointer cmp #SS_BASE jump NE, ss_check_under_ok nop ; < Error handler should be implemented here > ss_check_under_ok: pop PSF pop APC pop AP pop A ret ss_check_over: push A push AP push APC push PSF move APC, #80h ; Set Acc to A, standard mode, no auto inc/dec move Acc, DP ; Get current value of stack pointer cmp #SS_TOP jump NE, ss_check_over_ok nop ; < Error handler should be implemented here > ss_check_over_ok: pop PSF pop APC pop AP pop A retThe above code causes the current stack position to be checked before a push or pop (or call or ret) operation occurs. If an overflow or underflow error is detected, the response will vary from application to application. Normally, this sort of error is something that should only occur during application development; it should not occur if the code is written correctly. If the error does occur, it should generally be considered a fatal error, as it would be if an underflow/overflow occurred while using the hardware stack. Possible responses to this error include halting and transmitting an error message or blinking an LED. A useful trick during development is to set a breakpoint in MAX-IDE inside each of these two routines (i.e., at the "Error handler should be implemented here" line) to allow immediate feedback if an underflow or overflow occurs.
However, sometimes the application can recover from a stack underflow or overflow (for example, by reloading and restarting application subtasks from the beginning). In that situation it might be desirable to simply set a flag which indicates that a stack error has occurred. Possible candidates for such flags include the two general-purpose flags (GPF0 and GPF1) located in the PSF register. Since there are two bit flags available, one of them could be used to indicate an overflow and the other to indicate an underflow.