13. Exceptions for System-Level Services

1. Introduction

In the ARM Cortex-M architecture, aside from the processor faults and interrupts, there are also exceptions designed for system-level services. These system-level exceptions are vital for achieving specific tasks and triggering events in the system. Two notable types of exceptions for system-level services are the Supervisor Call (SVCall) exception and the Pendable Service Call (PendSV) exception.

vector_table

Exception Types

SVCall (Supervisor Call) Exception

The SVCall exception is used to switch from user mode to privileged mode, usually to perform some system-level operations that are not accessible in user mode. For example, an application might use an SVC instruction to request access to a system resource, like a file or a peripheral device.

The SVC instruction also allows you to specify an immediate 8-bit value, which can be useful for passing an argument or a function index to the handler. This value is then read within the SVC Handler to decide the action to take, making it a flexible mechanism for system services.

PendSV (Pendable Service Call) Exception

The PendSV exception is often used for context switching in an RTOS (Real-Time Operating System). Unlike the SVC exception, it's pendable, meaning it can be set to pending and will only be executed when the processor returns to the thread mode and no other exceptions with higher priority are active. This provides a way to defer function execution to a safer time, which is crucial for tasks like context switching.

The PendSV exception does not support arguments through the instruction that triggers it. It's generally triggered in software by setting a bit in a control register, making it a good fit for system-level tasks that don't require immediate execution and can be deferred until the processor is idle.


2. SVC Exception

Supervisor Call (SVC) Instruction

The Supervisor Call (SVC) instruction is a special instruction in the ARM Cortex-M architecture used to trigger an SVC exception. When the processor encounters an SVC instruction, it switches from user mode to privileged mode and jumps to the SVC handler. This is typically used for entering a secure, system-level state to perform operations that are not permitted in user mode.

The SVC instruction is often followed by an 8-bit immediate value, like SVC #1 or SVC #0xFF. This immediate value can be used as an argument or an identifier that is read by the SVC handler to determine what specific service or routine to execute. For example, different immediate values could correspond to different system calls, like opening a file, writing to a peripheral, or allocating memory. The SVC handler can read this value from the stacked Program Status Register (PSR) when it takes control, giving it context for what action to take.

Why Use SVC?

  1. Security: By restricting certain actions to privileged mode, you can ensure that user-level code can't accidentally or maliciously interfere with system integrity.

  2. Modularity: Using SVC instructions can make the system more modular and easier to manage. Different system-level functions can be invoked through different SVC calls, making it easier to maintain the code.

  3. Parameter Passing: The 8-bit immediate value that can accompany the SVC instruction allows for a simple, yet effective way to pass information to the SVC handler. This is useful for specifying which of several possible system services to invoke.

  4. Controlled Environment: Switching to privileged mode via SVC allows the system to ensure that the requesting process meets the necessary conditions to perform the privileged action, like having the right permissions.

  5. RTOS Use: In Real-Time Operating Systems, SVC can be used to perform system calls that facilitate inter-process communication, task switching, and resource allocation, among other things.

When to Use SVC?

  1. System Calls: When a user application needs to request a service from the operating system, such as file I/O, process creation, or inter-process communication.

  2. Resource Access: When an application needs to access hardware or other system resources that are not directly accessible from user mode.

  3. Initialization: When setting up an environment that requires privileged operations before transitioning to user-level code.

  4. Security Checks: When you need to verify credentials or perform other security checks before allowing access to certain features or data.

  5. Custom Handlers: In cases where you have written custom exception handlers and you want to invoke them from your application code.

How to Extract SVC Number?

Unfortunately, the ARM Cortex-M architecture doesn't pass the SVC number directly to the handler in an easily accessible register. That's why these "tricks" are often used to find the value manually, as shown in the following code:

#include <stdio.h>
#include <stdint.h>

// Function prototypes
void SVC_Handler(void);
void SVC_Handler_c(uint32_t *pBaseOfStackFrame);

int main(void) {
    // Trigger the SVC interrupt with number 25
    __asm volatile ("SVC #25");

    uint32_t data;

    // Retrieve the data from R0 register after returning from SVC
    __asm volatile ("MOV %0, R0" : "=r" (data) ::);

    printf("Data = %ld\n", data);

    // Infinite loop
    for (;;) {}
}

// SVC Handler implemented as a naked function
__attribute__ ((naked)) void SVC_Handler(void) {
    // Get the value of the MSP (Main Stack Pointer)
    __asm("MRS R0, MSP");
    // Branch to the C handler function
    __asm("B SVC_Handler_c");
}

void SVC_Handler_c(uint32_t *pBaseOfStackFrame) {
    printf("In SVC handler\n");

    // Get the return address from the stack frame
    uint8_t *pReturnAddr = (uint8_t*) pBaseOfStackFrame[6];

    // Decrement the return address by 2 to point to the SVC opcode
    pReturnAddr -= 2;

    // Extract the SVC number (Least Significant Byte of the opcode)
    uint8_t svcNumber = *pReturnAddr;

    printf("SVC number is: %d\n", svcNumber);

    // Increment the SVC number by 4
    svcNumber += 4;

    // Replace the value of R0 in the stack frame
    pBaseOfStackFrame[0] = svcNumber;
}
  1. Triggering the SVC: In the main function, the __asm volatile ("SVC #25"); instruction is used to trigger an SVC exception with the number 25.

  2. Stack Frame: When an SVC is called, the processor saves important CPU registers onto the stack so that they can be restored later. This includes the return address, which points to the instruction immediately after the SVC instruction.

  3. SVC_Handler: The function SVC_Handler is marked as "naked" using __attribute__ ((naked)), meaning it doesn't save or restore any registers, providing direct access to the stack. The function uses inline assembly to get the Main Stack Pointer (MSP) value and passes control to SVC_Handler_c.

    • __asm("MRS R0, MSP"); fetches the MSP value into register R0.
    • __asm("B SVC_Handler_c"); branches to the C function SVC_Handler_c. This handler will use the R0 value as the argument.
  4. SVC_Handler_c: This function takes the MSP value to get the base address of the stack frame (pBaseOfStackFrame).

    • It then extracts the return address from the stack frame. The [6] index here requires some explanation: When an exception occurs, the Cortex-M processor pushes various registers onto the stack to save their current state. These include R0-R3, R12, the Link Register (LR), Program Counter (PC), and xPSR. The pushed values create a stack frame. In your C function SVC_Handler_c, pBaseOfStackFrame points to the start of this frame. The value at index [6] corresponds to the saved Program Counter (PC) register, which contains the return address.
    • It then adjusts it to point to the SVC instruction: The SVC instruction is encoded as a 16-bit halfword in Thumb state. When the processor enters the exception handler, the Program Counter (PC) points to the instruction after the SVC instruction. Subtracting 2 bytes (-2) from this address will point us to the SVC instruction itself.
    • The least significant byte of this address contains the SVC number (svcNumber = *pReturnAddr).
  5. Modify and Return: The SVC number is incremented by 4 and written back into the stack frame (pBaseOfStackFrame[0] = svcNumber). This value is later fetched into R0 in the main function via inline assembly and printed.

  6. Infinite Loop: Finally, the main function goes into an infinite loop.

The code essentially demonstrates how to execute an SVC, capture its number in the handler, modify it, and then pass it back to the application code.

Extracting the SVC number this way can be error-prone and highly architecture-specific. However, if you're using a vendor-specific SDK like STM32's HAL or CMSIS, you might be able to abstract away some of this complexity. Some Real-Time Operating Systems (RTOS) also offer better ways to handle system calls, often wrapping SVC or similar instructions in easier-to-use API functions. These approaches would still involve the same underlying mechanism but hide the complexity from you.


3. PendSV Exception

Introduction

PendSV, or "Pendable Service Call," is exception type 14 in the ARM Cortex-M architecture. This exception is unique in that it doesn't have a fixed reason for being triggered; rather, it's pendable, meaning it can be set to occur but can also be held off (or pended) until the CPU is available to process it. It has a configurable priority level, allowing you to decide where it fits in the hierarchy of system exceptions and interrupts.

Causes of PendSV Exception

PendSV doesn't occur automatically due to hardware conditions like a SysTick or a GPIO interrupt. Instead, it's usually triggered manually by software, either by setting a bit in a control register or through direct assembly instructions. This gives the programmer full control over when this exception occurs.

Why use PendSV?

The primary use-case for PendSV is in the context of an Operating System (OS), especially a Real-Time Operating System (RTOS). Here's why:

  1. Context Switching: PendSV is often used to perform a context switch, i.e., saving the state of the currently running task and restoring the state of the next task to run. This is critical in multitasking environments. This is useful because we typically don't want to SysTick interrupt to be time-consuming. We want it to be as short as possible, and if it requires a context switch, we can defer it to the PendSV exception.

  2. Low Priority: Since its priority is configurable, PendSV is usually set to the lowest possible priority. This ensures that all other higher-priority exceptions and interrupts are serviced before a context switch occurs.

  3. Cooperative Multitasking: If you're running multiple threads or tasks that need to share CPU time but don't have strict timing requirements, you can use PendSV to yield the processor voluntarily.

  4. Deferrable Work: Sometimes you want to defer some work until all higher-priority tasks are out of the way. PendSV is perfect for this kind of "cleanup" operation.