ARM Cortex-M33 TrustZone Context Switching Challenges
The ARM Cortex-M33 processor, as used in the NXP LPC5500 series, introduces TrustZone technology for secure and non-secure state separation. When integrating FreeRTOS with TrustZone, a common challenge arises in transitioning between secure and non-secure tasks. The core issue manifests when attempting to switch from a secure FreeRTOS task to a non-secure user task, resulting in HardFault exceptions or improper execution states.
The TrustZone architecture requires careful management of stack pointers, exception return values, and memory partitioning. The processor maintains separate stack pointers for secure (PSP_S) and non-secure (PSP_NS) contexts, along with distinct control registers. The transition between these states must preserve the architectural requirements while maintaining the integrity of both secure and non-secure environments.
EXC_RETURN Value Configuration and Stack Frame Integrity
The primary technical challenge lies in the proper configuration of the EXC_RETURN value and the associated stack frame structure. The EXC_RETURN value determines the processor state upon exception return, including the security state, stack pointer selection, and floating-point context status. The value 0xFFFFFFBD indicates a return to non-secure thread mode using the non-secure process stack pointer (PSP_NS) from a secure handler that was using the secure process stack pointer (PSP_S).
The stack frame must contain specific register values in precise locations to ensure proper context restoration. The xPSR register in the stack frame must have the Thumb bit (T) set and the Interrupt Program Status Register (IPSR) field cleared to zero. The program counter (PC) value must point to valid non-secure memory, and the link register (LR) should be set to an appropriate return address.
The following table illustrates the required stack frame structure for a successful secure to non-secure transition:
Offset | Register | Value Requirements |
---|---|---|
0x1C | xPSR | T bit set, IPSR=0 |
0x18 | PC | Non-secure address |
0x14 | LR | 0xFFFFFFFF |
0x10 | R12 | Context-dependent |
0x0C | R3 | Context-dependent |
0x08 | R2 | Context-dependent |
0x04 | R1 | Context-dependent |
0x00 | R0 | Context-dependent |
Secure and Non-Secure Stack Management with FreeRTOS
The implementation requires careful management of both secure and non-secure stacks. When a non-secure task needs to call secure functions, it must have an allocated secure stack space. The context switching mechanism must handle both PSP_NS and PSP_S registers, along with their associated stack limit registers.
The secure FreeRTOS implementation must initialize the PSP_NS for non-secure tasks and allocate secure stack space for each task that requires secure function calls. The context switching code, running in secure privileged mode, must save and restore both stack pointers during task switches.
The following sequence describes the proper initialization and switching process:
-
Secure Stack Allocation: During task creation, allocate secure stack space based on the maximum stack usage of secure APIs that the task might call. This space should be separate from the non-secure task stack.
-
Stack Pointer Initialization: Initialize both PSP_NS and PSP_S for each task. The PSP_NS should point to the non-secure task stack, while PSP_S should point to the allocated secure stack space.
-
Control Register Configuration: Set the CONTROL_NS register to use PSP_NS in non-secure mode. The secure context should maintain its own CONTROL register configuration.
-
Context Switching: During task switches, save and restore both PSP_NS and PSP_S values. The secure context switching code must handle both pointers atomically to prevent context corruption.
-
Exception Handling: Configure the PendSV handler to properly manage the transition between secure and non-secure states using the appropriate EXC_RETURN value.
The implementation must ensure that the secure stack space is sufficient for the deepest possible secure function call chain from any non-secure task. This requires careful analysis of the secure API usage patterns and potential recursion depths.
Implementing Robust Secure-Non-Secure Transitions
The actual implementation requires attention to several architectural details and potential pitfalls. The following steps outline the complete solution:
-
Secure World Initialization:
- Configure the Non-Secure Callable (NSC) region for secure entry points
- Initialize the Secure Fault Status Register (SFSR)
- Set up the Security Attribution Unit (SAU) or Implementation Defined Attribution Unit (IDAU)
- Configure the Memory Protection Controller (MPC) for proper memory partitioning
-
Non-Secure World Initialization:
- Set up the Non-Secure Vector Table
- Initialize the Non-Secure MPU (if used)
- Configure the Non-Secure SysTick and other peripherals
-
Task Creation and Management:
- Allocate secure stack space for each non-secure task that requires secure API access
- Initialize both PSP_NS and PSP_S during task creation
- Set up the initial stack frame with proper EXC_RETURN values
-
Context Switching Implementation:
- Modify the PendSV handler to handle both secure and non-secure context switches
- Implement proper stack pointer management for both security states
- Handle floating-point context preservation if FPU is used
-
Exception Handling:
- Configure fault handlers for both secure and non-secure worlds
- Implement proper error reporting and recovery mechanisms
- Handle stack overflow detection for both security states
-
Debugging and Validation:
- Implement comprehensive logging for secure-non-secure transitions
- Validate stack usage and memory protection settings
- Test all possible transition scenarios
The following code example demonstrates a basic PendSV handler implementation for secure to non-secure transition:
__attribute__((naked)) void PendSV_Handler(void)
{
__asm volatile(
"MRS R0, PSP \n" // Get current secure PSP
"STMDB R0!, {R4-R11} \n" // Save remaining registers
"PUSH {LR} \n" // Save LR (EXC_RETURN)
"BL SaveSecureContext \n" // Save secure context
"BL GetNextTask \n" // Get next task to run
"BL RestoreSecureContext \n" // Restore secure context
"POP {LR} \n" // Restore LR (EXC_RETURN)
"LDMIA R0!, {R4-R11} \n" // Restore registers
"MSR PSP, R0 \n" // Update PSP
"BX LR \n" // Return to thread mode
);
}
The implementation must be extended to handle both secure and non-secure contexts, including proper management of PSP_NS and PSP_S. The context switching code should verify the security state of the incoming and outgoing tasks and handle the transition appropriately.
Secure API Call Handling
When a non-secure task calls a secure API, the implementation must ensure proper stack management and context preservation. The following steps outline the process:
-
Secure Gateway Entry:
- Non-secure code calls a secure function through the NSC region
- The processor transitions to secure state
- The secure gateway function validates the call
-
Context Switching:
- Save non-secure context (PSP_NS, registers)
- Switch to secure stack (PSP_S)
- Execute secure function
-
Return to Non-Secure:
- Restore non-secure context
- Use appropriate EXC_RETURN value
- Return to non-secure state
The secure stack space for each task must be sufficient to handle the maximum possible stack usage of secure functions called from that task. This requires careful analysis of the call graph and potential recursion depths.
Debugging and Fault Analysis
When encountering HardFaults or other exceptions during secure-non-secure transitions, the following debugging steps should be taken:
-
Examine Fault Status Registers:
- Secure Fault Status Register (SFSR)
- HardFault Status Register (HFSR)
- Debug Fault Status Register (DFSR)
- Auxiliary Fault Status Register (AFSR)
-
Verify Stack Pointer Values:
- Confirm PSP_NS and PSP_S are valid
- Check stack alignment (8-byte aligned)
- Verify stack limits
-
Validate Memory Protection:
- Check SAU/IDAU configuration
- Verify MPC settings
- Confirm memory region permissions
-
Analyze Exception Return:
- Verify EXC_RETURN value
- Check security state transition
- Confirm stack pointer selection
-
Review Context Switching:
- Validate register preservation
- Check stack frame integrity
- Verify task state consistency
The implementation should include comprehensive logging and diagnostic features to aid in debugging. This includes logging of security state transitions, stack pointer values, and exception information.
Performance Considerations
The secure-non-secure transition overhead can impact system performance. The following optimizations should be considered:
-
Minimize Secure-Non-Secure Transitions:
- Batch secure operations
- Use secure callbacks
- Implement secure message queues
-
Optimize Context Switching:
- Reduce register save/restore overhead
- Use lazy stacking for FPU context
- Implement fast path for common cases
-
Memory Optimization:
- Align stack allocations
- Use memory pools for secure stacks
- Implement stack usage monitoring
-
Hardware Acceleration:
- Utilize TrustZone hardware features
- Optimize MPU/SAU configuration
- Use hardware stack limit checking
The implementation should be carefully profiled to identify and address performance bottlenecks. The use of hardware features and proper system design can significantly reduce the overhead of secure-non-secure transitions.
Security Considerations
The implementation must maintain the security properties of the TrustZone architecture:
-
Memory Protection:
- Proper SAU/IDAU configuration
- Secure MPU setup
- Non-secure MPU restrictions
-
Secure Boot:
- Implement secure boot sequence
- Verify secure firmware integrity
- Protect secure initialization code
-
Runtime Security:
- Validate secure API calls
- Implement secure watchdog
- Monitor security state transitions
-
Debug Security:
- Control debug access
- Implement secure debug authentication
- Protect sensitive information in memory
The security implementation should follow best practices for TrustZone-based systems, including proper key management, secure storage, and protected communication channels.
Conclusion
Implementing secure to non-secure task switching in FreeRTOS on ARM Cortex-M33 processors with TrustZone requires careful attention to architectural details and proper system design. The solution involves proper management of stack pointers, exception handling, and context switching while maintaining the security properties of the TrustZone architecture. By following the outlined implementation strategy and considering the performance and security aspects, developers can create robust and efficient embedded systems that leverage the full capabilities of ARM TrustZone technology.