ARM Cortex-M processors provide two stack pointers, the main stack pointer (MSP) and process stack pointer (PSP), that enable dual-stacked operation without requiring an RTOS. This allows developers working on bare-metal embedded projects to take advantage of the flexibility and power of dual stacking while avoiding the complexity and overhead of an RTOS.
Introduction to Dual Stacking
Dual stacking refers to the ability to have two separate call stacks in a system – one for privileged threads and one for unprivileged threads. The MSP points to the privileged stack while the PSP points to the unprivileged stack. When code executes in privileged mode, the processor uses the MSP. When code executes in unprivileged mode, the processor uses the PSP.
This enables multiple threads to run independently without interfering with each other’s stack space. It also allows switching between threads safely and efficiently by simply changing which stack pointer is active on context switch.
Benefits of Dual Stacking Without RTOS
Using dual stacking without an RTOS provides several key benefits:
- Avoids RTOS complexity and overhead – No need to integrate and maintain a heavy OS.
- Full control and customization – Stack sizes, switching logic, APIs can all be fully customized.
- Optimized for application needs – Only features needed can be implemented unlike generic RTOS.
- Reduced memory requirements – An RTOS can consume significant RAM and flash.
- Easier to certify – RTOS certification introduces significant overheads.
Implementing Dual Stacking
The key steps to implement dual stacking without an RTOS are:
- Define two stack arrays – one for MSP and one for PSP.
- Write MSP and PSP initialization code.
- Create privileged and unprivileged thread code.
- Implement context switching logic.
- Use SVCall handler for thread requests.
1. Defining Stack Arrays
The first step is to define arrays for the MSP and PSP stacks in memory. These should be declared globally or statically allocated. Typical stack sizes range from a few KB to 16+ KB depending on application needs. /* Privileged stack */ static uint32_t msp_stack[MSP_STACK_SIZE]; /* Unprivileged stack */ static uint32_t psp_stack[PSP_STACK_SIZE];
2. Initializing the Stack Pointers
The stack pointers need to be initialized before creating threads. This is typically done in the reset handler before jumping to main(): void Reset_Handler() { /* Initialize the MSP */ __set_MSP((uint32_t)msp_stack + MSP_STACK_SIZE); /* Initialize the PSP if using it for threads */ __set_PSP((uint32_t)psp_stack + PSP_STACK_SIZE); /* Rest of reset handler code… */ /* Jump to main */ main(); }
3. Creating Threads
Thread functions for both privileged and unprivileged need to be defined. These should contain an endless loop with thread logic: /* Privileged thread */ void priv_thread() { while(1) { /* privileged thread code… */ } } /* Unprivileged thread */ __attribute__((naked)) void unpriv_thread() { __asm( ” MOVS R0, #1\n” ” MSR CONTROL, R0\n” ); /* unprivileged thread code… */ /* Privileged function call */ __asm( ” SVC #1\n” ); }
The unprivileged thread uses inline assembly to drop to unprivileged mode before executing main logic. The __attribute__((naked)) attribute prevents compiler optimizations that could corrupt state when switching modes.
4. Context Switching
To actually switch between threads, the PSP needs to be updated to point to the next thread’s stack on context switch: /* Privileged function to context switch threads */ void switch_context() { /* Save current context onto current stack */ push_registers_on_stack(); /* Update PSP to new stack */ __set_PSP(new_stack_address); /* Restore context of new thread */ pop_registers_from_stack(); }
The switch can be triggered by a timer interrupt, event flag, or other mechanism to initiate a context switch.
5. SVCall for Thread APIs
Since unprivileged threads cannot call privileged functions directly, the SVCall interrupt can be used to safely transition to privileged mode and provide a thread API: /* Privileged function to kill a thread */ void kill_thread(int id) { /* Kill thread logic */ } /* Unprivileged thread requests thread kill */ __asm(“SVC #1”); // SVCall with ID #1 /* SVCall Handler */ void handle_svc() { if(id == 1) { kill_thread(thread_id); } }
This allows abstracting privileged functions into APIs usable by unprivileged threads.
Conclusion
Implementing dual stacked operation on Cortex-M without an RTOS provides a flexible approach to gain the benefits of multithreading while minimizing complexity and overhead. With careful management of the MSP and PSP stack pointers along with robust context switching logic, developers can fully utilize the dual stacking capabilities of Cortex-M processors in embedded and IoT applications.