When developing multithreaded applications using Keil RTX, one important consideration is setting the stack size for each thread appropriately. Choosing a stack size that is too small can lead to stack overflow errors and unpredictable program behavior. On the other hand, setting the stack size much larger than needed wastes valuable RAM resources, especially on embedded systems where memory is limited.
What is the Stack?
Every thread in an application gets its own stack. The stack is a region of memory used to store temporary data like function parameters, return addresses, and local variables. It works on the principle of Last In First Out (LIFO) – the last item pushed on the stack is the first item popped out.
When a function is called, its parameters and local variables get pushed onto the top of the stack. When the function returns, these items get popped off the stack. The CPU uses the stack pointer register to keep track of the top of the stack.
Stack Overflow
A stack overflow occurs when a thread’s stack reaches its maximum capacity. When this happens, there is no more room to push additional data onto the stack. Symptoms of a stack overflow include the program crashing or behaving erratically.
Some common causes of stack overflows are:
- Calling many nested functions that use large stack frames
- Large or uncontrolled recursive functions
- Data structures that are too large for the stack
- An interrupt occurring when the stack is near full capacity
Setting the Stack Size in Keil RTX
In Keil RTX, the stack size for each thread is configured when creating the thread. The osThreadDef macro is used to define a thread control block which includes parameters like stack size, priority, and entry function.
For example: osThreadDef(thread1, osPriorityNormal, 1, 500);
This defines a thread control block for “thread1” with normal priority, 1 instance, and a stack size of 500 bytes.
The stack size parameter is critical to get right. An undersized stack will lead to hard-to-debug crashes, while an oversized stack wastes RAM.
Calculating the Required Stack Size
So how do we determine the optimal stack size to specify for a thread? Unfortunately there is no one-size-fits-all formula. It requires an understanding of the thread’s requirements.
Some tips for estimating stack size:
- Analyze the thread’s code to total up stack usage of local variables in each function
- Account for stack needed by interrupts – they use the current thread’s stack
- Recursion requires extra stack space proportional to max recursion depth
- Buffer allocated on stack must fit within stack size
- OS overhead like context switching uses up some stack
- Allocating too much is better than too little – start generous then optimize
Certain compilers and debuggers provide tools to help estimate required stack size. For example, Keil uVision IDE has a Stack Analyzer feature that can calculate a thread’s worst-case stack usage.
Dynamically Checking for Stack Overflows
Once an application is running, there are techniques to dynamically detect if any threads are experiencing stack overflows:
- Enable the stack overflow detection feature in Keil RTX. This will trigger an error handler if overflow occurs.
- Inspect stack pointer register values in debugger to ensure there is adequate margin.
- Instrument code to print out stack usage statistics at runtime.
- Fill unused stack with known pattern like 0xAA, check for corruption indicating overflow.
If stack overflow is detected, there are a few options to remedy:
- Increase stack size and re-test.
- Optimize code/data to reduce stack usage.
- Switch to a dynamic stack allocation model.
- Divide tasks across multiple threads with smaller stacks.
Allocating Stacks Statically vs Dynamically
By default, Keil RTX allocates thread stacks statically at compile time. The advantage is it simplifies memory management. However, the drawback is stack sizes are fixed even if the thread ends up not using all of it.
An alternative approach is to allocate stacks dynamically at runtime only when needed. This adds complexity but allows more flexibility in sizing stacks and reclaiming unused memory.
Static allocation is recommended for most embedded applications. But dynamic allocation may be useful for systems requiring optimized memory usage or extremely versatile threading capabilities.
Stack Size Tradeoffs and Optimization
Tuning thread stack sizes requires balancing several tradeoffs:
- Smaller stacks → reduced RAM usage but risk of overflow
- Larger stacks → safer but waste memory if unused
- Overprovision if stack needs unknown → simpler but less optimal
- Precisely analyze stack requirements → more optimized but time consuming
There are various techniques that can help optimize stack usage:
- Pass large data structures via pointers instead of values on stack
- Minimize local variables scope to smallest possible
- Split tasks across multiple threads to reduce per-thread stack size
- Use stack canary values to detect/prevent overflows
Obtaining the best stack size configuration requires iterating – start generous and work down to an efficient size that still provides ample overflow margin.
Conclusion
Setting appropriate thread stack sizes is a key consideration when developing multithreaded applications with Keil RTX. Undersized stacks lead to crashes while oversized stacks waste RAM. Analyzing code stack requirements, intelligent allocation, and usage optimization helps tune stack sizes for efficiency and stability.