Chapter 4: Stack and Subroutine Calls¶
Section 1: What is a Stack?¶
A stack is a special kind of data structure that operates on a Last In, First Out (LIFO) principle β the last item you place on the stack is the first one to be removed.
Real-World Analogy¶
Think of a stack of plates in a cafeteria: - You push a clean plate on top of the pile - You pop the top plate off when someone takes one - You can't grab a plate from the middle β only the top!
This "top-first" behavior is exactly how the stack works in your microcontroller.
Why Do We Need a Stack?¶
Stacks are used any time the program needs to pause what it's doing, remember something, and come back to it later.
In the PIC24, the stack is used to: - Store the return address when calling a subroutine - Temporarily hold register values during nested function calls - Save context during interrupts
If you didn't have a stack, calling one function from another β or returning from an interrupt β would be extremely difficult (if not impossible) to manage.
Key Operations¶
Term | Meaning |
---|---|
PUSH |
Store a value on the top of the stack |
POP |
Remove the top value from the stack |
CALL |
Push return address, jump to subroutine |
RETURN |
Pop return address, continue execution |
These operations are handled automatically during function calls β but you can also perform them manually in assembly.
In the next section, we'll explore how the PIC24 handles subroutine calls using the stack, and how to trace exactly what happens when a CALL
and RETURN
are executed.
Section 2: The Call Stack in Assembly¶
Whenever your program executes a subroutine call, the processor must remember where to return after the subroutine finishes. To do this, it uses the stack.
πΉ What Happens During a CALL
¶
When you execute a CALL
instruction:
CALL MyFunction
The pic 24 automatically performs the following: - Pushes the return address onto the stack. - Jumps to the MyFunction label.
Later when the subroutine finishes, a RETURN
instruction:
- Pops the return address off the stack
- Resumes execution from that address
RETURN
Stack Behavior During Function Calls¶
Let's walk through a basic example:
Main:
CALL DoSomething
; Execution resumes here after RETURN
DoSomething:
; Function logic here
RETURN
What the Stack Looks Like:
Stack Top | Contents |
---|---|
β¬ Growing Downward | |
Address of Main+2 |
(Return to next line) |
You Don't Have to Push Return Address Yourself¶
The best part? You don't need to manually push/pop the return address β the CALL and RETURN instructions handle it automatically for you. Thatβs what makes writing subroutines manageable.
Section 3: Stack Pointers in the PIC24¶
In the PIC24 architecture, the stack is accessed through special-purpose working registers. Understanding these is key to tracing subroutine behavior and building your own call structures.
πΉ W15 β The Stack Pointer (SP)¶
The register W15
is automatically used by the processor as the stack pointer. It always points to the top of the stack in memory.
- When you push data, it stores the value at
[W15]
and increments (post-increment) - When you pop data, it decrements first and then reads from
[W15]
(pre-decrement)
Example: Manual Push/Pop¶
MOV #0x1234, W0
MOV W0, [W15++] ; Push W0 onto stack
MOV [--W15], W1 ; Pop from stack into W1
πΉ W14 β The Frame Pointer (Optional)¶
By convention, W14
is often used as a frame pointer, especially in higher-level language support (like C). It can be useful when:
- Managing local variables relative to a base offset
- Navigating function call frames more easily in complex call chains
Note: The frame pointer is not required for basic assembly subroutines, but it can help with stack-traceability in deeper projects.
Summary of Stack Registers¶
Register | Role | Usage Example |
---|---|---|
W15 |
Stack Pointer (SP) | MOV W0, [W15++] |
W14 |
Frame Pointer (FP) | User-defined (optional) |
Section 4: Writing and Tracing a Subroutine¶
Letβs bring everything together and write a real subroutine that uses the call stack.
Weβll build a function that doubles a value passed in W0
, and returns the result in W0
β a simple example, but one that shows how function calls work under the hood.
πΉ Step 1: Main Program Calls the Function¶
MOV #7, W0 ; Load 7 into W0
CALL DoubleValue ; Call subroutine to double W0
; W0 now contains 14
When CALL DoubleValue
is executed, the processor:
- Pushes the return address onto the stack
- Jumps to the label DoubleValue
πΉ Step 2: Define the Subroutine¶
DoubleValue:
ADD W0, W0, W0 ; Double the value in W0
RETURN ; Return to the line after the CALL
RETURN
executes:
- The processor pops the return address from the stack
- Execution resumes immediately after the CALL
What the Stack Looks Like¶
Before CALL DoubleValue
, the stack might look like this:
Stack Top | Contents |
---|---|
β¬ Growing Downward | |
Address after CALL | Return address β resume here |
After RETURN
, the address is popped off and the stack returns to its previous state.
Interactive MicroSim: Stack Tray Visualizer¶
To help visualize how function calls and returns interact with the stack, try this MicroSim built on the cafeteria tray analogy.
π Try the Stack Tray MicroSim
This simulation allows you to:
- Press CALL to simulate a function call (tray pushed onto the stack)
- Press RETURN to simulate a function return (tray popped from the stack)
- Press RESET to clear the stack
It illustrates how the stack grows and shrinks with each nested function call β following LIFO (Last In, First Out) behavior.
Notes¶
- The value is passed in
W0
, and the result is returned inW0
. This is a common calling convention for small assembly routines. - For more complex functions, you may also use
W1
,W2
, or the stack itself to pass/return data.
Section 5: Visualizing the Stack¶
Understanding how the stack grows and shrinks is essential for mastering subroutine calls and returns. Even though the processor handles most of the mechanics automatically, being able to mentally trace stack behavior will help you debug and write more reliable code.
πΉ How the Stack Grows¶
The PIC24 stack grows downward in memory β that means each time something is pushed, the stack pointer (W15
) points to a lower address.
Every time you CALL
a function:
- The return address is pushed onto the stack
- Any manually saved data (like temporary registers) can also be pushed
When you RETURN
:
- The return address is popped, and execution resumes where it left off
- If you saved anything manually, you must also restore it before returning
Example: Two Nested Calls¶
Main:
CALL A
A:
CALL B
RETURN
B:
RETURN
Stack Behavior:¶
CALL A
β pushes return address forMain
- Inside
A
,CALL B
β pushes return address forA
RETURN
fromB
β pops toA
RETURN
fromA
β pops toMain
Section 6: Summary and Best Practices¶
Now that youβve seen how the stack works and how subroutines are built in PIC24 assembly, hereβs what you should remember going forward:
Key Takeaways¶
- The stack stores return addresses (and optionally local data) when calling subroutines.
CALL
andRETURN
handle stack push/pop automatically for return addresses.W15
is the stack pointer (SP) β it tracks the top of the stack and grows downward.W14
is commonly used as a frame pointer (FP) β useful for structured call frames or C-style stack frames.- You can manually push/pop data using
[W15++]
and[--W15]
.
Best Practices for Subroutines¶
- βοΈ Use
W0
to pass parameters and return values in small subroutines. - βοΈ Use
CALL
/RETURN
for clean function separation. - βοΈ If you manually push registers (e.g.
W1
,W2
), always restore them before returning. - β Avoid modifying
W15
directly β use auto-increment/decrement instead. - βοΈ Keep subroutines small and modular when possible.
- βοΈ Comment your subroutine entry and exit points β especially when multiple calls are nested.
In the next chapter, weβll expand your control over time with hardware timers and learn how to build precise time-based behavior in your programs using interrupts.
Quiz: Stack and Subroutines¶
What happens to the stack when the following code is executed?
CALL Func1
; do something
CALL Func2
; done
Func1:
RETURN
Func2:
RETURN
- The stack is unchanged β calls donβt affect it
- Two values are pushed to the stack and never removed
- One return address is pushed and overwritten
- Two return addresses are pushed, then popped in reverse order
Show Answer
The correct answer is D.
When CALL Func1
is executed, the return address is pushed onto the stack.
After Func1
returns, the address is popped.
The same happens with Func2
.
So, two return addresses are pushed and popped in reverse order β just like a stack (LIFO).
Prompt Practice¶
Write a subroutine called AddTen
that takes a number in W0
, adds 10 to it, and returns the result (also in W0
).
Your main program should call AddTen
with an initial value of 5
, and store the result in W1
.
Try writing it yourself before checking the solution!
Click to show solution
```asm ; Main Program MOV #5, W0 ; Load value into W0 CALL AddTen ; Call subroutine MOV W0, W1 ; Store result in W1
; Subroutine
AddTen: ADD W0, #10, W0 ; Add 10 to the value in W0 RETURN ```