Implementing System Calls with Cortex-M7!
Article Created on August 10, 2023
In this post, I will implement system calls for a Cortex-M7 microcontroller. I am using SAME70-Xplained microcontroller board, and the Microchip Studio IDE.
Introduction to System Calls
The idea is to separate user-space from kernel-space. As a developer of the kernel, I want to allow the user to complete some actions. However, those actions require special privilege and I do not want to give the privilege to the user. So, what I will do is implement some functions (or, system calls) and allow the user to call this system calls to complete those actions. For e.g., I want to allow the user to print strings to the UART console. But, printing to UART console requires special privilege. I want to retain that special prvilege in the kernel. So, I will implement a system call k_print
and allow the user to make the system call using a user-space equivalent of u_print
. To implement such a system call, we will use the supervisor call assembly instruction.
Image 1: User-Space and Kernel-Space
System Calls with ARM Cortex-M7
The separation of user-space and kernel-space in ARM Cortex-M7 is indicated by nPRIV
bit (Bit 0) in the CONTROL
register. When nPRIV
bit is set, the processor operates in unprivileged mode. For our purpose, we set the nPRIV
bit as the first instruction in the main
function. The transition to kernel-space from user-space is done through svc
assembly instruction. The instruction raises a supervisor call exception. Exception handlers can modify the access level.
First of all, I am going to define the system call. Here, I am going to use a print functionality as an example. The function k_print
is a function in the kernel-space to print to the UART console.
void k_print(char* str, len str_len)
{
// code to print the string 'str' of lenght 'str_len' to the UART console
}
Now, I am going to define a function which can be called by the user/application to initiate the system call. The u_print
function below is a user-space equivalent function call of the k_print
function. The u_print
function will initiate the system call by executing the svc
assembly instruction with 0x01
as the immediate value to the assembly instruction. Later, we will have to make sure that the value 0x01
corresponds to the k_print
system call.
void u_print(char* str, len str_len)
{
asm volatile ("svc 0x01");
}
The call to u_print
initiates an exception. Let’s say that the exception handler to the exception initiated by svc
assembly instruction is SVCall_Handler
. In the exception handler, we need to do the following:
- Check if the stack in use before the supervisor call was made was
MSP
orPSP
. To check the mode from where, the supervisor call was initiated, we need to check the 4th bit ofLR
register. - Load the stack pointer (
MSP
orPSP
) toR0
based on the check in the step above. - Branch to a function (
SVCall_Handler_Main
) where the actual system call will be handled. The value in the registerR0
will be the first argument to the function.
The code snippet below shows the implementation of the supervisor call exception handler.
void __attribute__ (( naked )) SVCall_Handler(void)
{
asm volatile(
"tst lr, #4 \n"
"ite eq \n"
"mrseq r0, msp \n"
"mrsne r0, psp \n"
"b %[SVCall_Handler_Main] \n"
: /* no output */
: [SVCall_Handler_Main] "i" (SVCall_Handler_Main) /* input */
: "r0" /* clobber */
);
}
The __attribute__ (( naked ))
makes sure that no stack is created for the function.
Now, I will implement the function where the supervisor call will be handled. The function should extract the immediate operand of the svc call (which in our case is 0x01). The svc_args
argument points to the exception stack frame. From Image 1, we can see that the program counter in the exception stack frame is at an offset of 6 elements from the top. To extract the immediate value, we need to add an offset of -2 to the program counter value.
Image 2: Exception Stack Frame without Floating-Point Storage
When the function call u_print
was made with the arguments str
and str_len
, the pointer str
was stored in R0
and the argument str_len
was stored in R1
. R0
corresponds to svc_args[0]
in the exception stack frame and R1
corresponds to svc_args[1]
in the stack frame. Hence, the function call to k_print
is made with svc_args[0]
and svc_args[1]
as the arguments. The code below shows the supervisor call handler.
void SVCall_Handler_Main(unsigned int *svc_args)
{
unsigned int svc_number;
svc_number = ((char *)svc_args[6])[-2];
switch(svc_number)
{
case 0:
//
break;
case 1:
k_print((const char *)svc_args[0], (int)svc_args[1]);
break;
default:
//
break;
}
}