Etiqueta: rtos
Separating user space from kernel space on ARM Cortex-M3
1 . Introduction
ARM Cortex-M processors are in SoCs of several application domains, especially in much of what we call smart devices. This publication continues the previous one, in which I had demonstrated the implementation of a minimum preemptive scheduling mechanism for an ARM Cortex-M3, taking advantage of the special hardware resources for context switching.
Some important features of this architecture will now be used to improve the kernel development, such as the separation of user and kernel threads – with 2 stack pointers: MSP and PSP, besides the use of Supervisor Calls to implement system calls.
Although some concepts about operating systems are addressed because they are inherent to the subject, the objective is to explore the ARM Cortex-M3, a relatively inexpensive processor with wide applicability, and how to take advantage of it to develop more robust systems.
2. Special registers
There are 3 special registers on the ARM Cortex-M3. You can consult the ARM documentation to understand the role of each of them in the processor. The most important in this publication is CONTROL .
- Program Status Registers (APSR, IPSR, and EPSR)
- Interrupt Mask Registers (PRIMASK, FAULTMASK and BASEPRI)
- Control (CONTROL) .
Special registers can only be accessed through the privileged instructions MRS (ARM Read Special Register) and MSR (ARM Set Special Register):
// Load in R0 the current value contained in the special register
MRS R0, SPECIAL
// Load in the special register the value contained in R0)
MSR SPECIAL, R0
The CONTROL register has only 2 configurable bits. When an exception handler (e.g, SysTick_Handler) is running, the processor will be in privileged mode and using the main stack pointer (MSP) , and CONTROL [1] = 0, CONTROL [0] = 0 . In other routines that are not handlers, this register can assume different values depending on the software implementation (Table 1).
In the small kernel shown before, the application tasks (Task1, Task2 and Task3) were also executed in privileged mode and using the main stack pointer (MSP). Thus, an application program could change the special registers of the core if it wanted to.
3. Kernel and user domains
In the last publication I highlighted the fact that register R13 is not part of the stackframe, as it stores the address of the current stack pointer. The R13 is a “banked” register, meaning that it is physically replicated, and takes a value or another depending on the state of the core.
CTRL [1] (0 = MSP / 1 = PSP) | CTRL [0] (0 = Priv, 1 = Non priv ) | state |
0 | 0 | Privileged handler * / Base mode |
0 | 1 | Unprivileged |
1 | 0 | Privileged thread |
1 | 1 | User thread |
* in exception handlers, this mode will always be active even if CTRL [0] = 1.
With two stack pointers, one for application and another for the kernel, means that a user thread can not easily corrupt the kernel stack by a programming application error or malicious code. According to the ARM manuals, a robust operating system typically has the following characteristics:
- interrupt handlers use MSP (by default)
- kernel routines are activated through SysTick at regular intervals to perform task scheduling and system management in privileged mode
- user applications use PSP in non-privileged mode
- memory for kernel routines can only be accessed in privileged mode* and use MSP
* for now we will not isolate the memory spaces
4. System Calls
Putting it simple, a system call is a method in which a software requests a service from the kernel or OS on which it is running. If we intend to separate our system into privilege levels, it is inevitable that the application level needs call the kernel to have access to, for example, hardware services or whatever else we think is critical to the security and stability of our system.
A common way to implement system calls in ARM Cortex-M3 (and in other ARMv7) is to use the software interrupt Supervisor Call (SVC). The SVC acts as an entry point for a service that requires privileges to run. The only input parameter of an SVC is its number (ASM instruction: SVC #N), which we associate with a function call (callback). Unlike other exceptions triggered via software available, like PendSV (Pendable Supervisor Call), the SVC can be triggered in user mode by default.

5. Design
5.1 Using two stack pointers
To use the two available stack pointers (MSP and PSP) it is essential to understand 2 things:
- The control register manipulation: it is only possible to write or read the CONTROL register in handler mode (within an exception handler) or in privileged threads.
- The exceptions mechanism: when an interrupt takes place, the processor saves the contents of registers R0-R3, LR, PC and xPSR, as explained in the previous publication. The value of the LR when we enter an exception indicates the mode the processor was running, when the thread was interrupted. We can manipulate this value of LR together with the manipulation of the stack pointer to control the program flow.
LR | BX LR |
0xFFFFFFF9 | Returns to “base” mode, privileged MSP. (CONTROL = 0b00) |
0xFFFFFFFD | Returns to user mode (PSP, with the privilege level of entry) (Control = 0b1x) |
0xFFFFFFF1 | Returns to the previous interruption, in case a higher priority interruption occurs during a lower priority. |
5.1.1. One kernel stack for each user stack
Each user stack will have a correspondent kernel stack (one kernel stack per thread). Thus, each Task is associated to a kernel stack and a user stack. Another approach would be only one kernel stack for the entire the system (one kernel stack per processor). The advantage of using the first approach is that from the point of view of who implements the system, the programs that run in the kernel follow the same development pattern as the application programs. The advantage of the second approach is less memory overhead and less latency in context switching.

5.2 Kernel entry and exit mechanisms
In the previous publication, the interruption triggered by SysTick handled the context switching, i.e., it interrupted the running thread, saved its stackframe, searched for the next thread pointed to by the next field in the TCB (thread control block) structure and resumed it.
With the separation between user and supervisor spaces, we will have two mechanisms to get in and out the kernel, the system calls, explicitly called in code, and the interruption by SysTick that implements the scheduling routine. Although still using a round-robin scheme in which each task has the same time slice, the threads of the kernel also work cooperatively with a user thread that evoked it, that is: when there is nothing more to be done, the kernel can explicitly return. If the thread of the kernel takes longer than the time between one tick and another, it will be interrupted and rescheduled. User tasks could also use a similar mechanism, however, for simplicity of exposure, I chose to leave user tasks only in a fixed round-robin scheme, with no cooperative mechanisms.
5.2.1. Scheduler
The flowchart of the preemptive scheduler to be implemented is in Figure 4. The start-up of the kernel and user application is also shown for clarity. The kernel starts upo and voluntarily triggers the first user task. At every SysTick interruption, the thread has its state saved and the next scheduled task is resumed according to the state in which it was interrupted: kernel or user mode.

5.2.2 System Calls
System Calls are needed when the user requests access to a privileged service. In addition, I also use the same mechanism for a kernel thread to cooperatively return to the user thread.
Note that I chose not to run the kernel threads within the SVC handler, which would be more intuitive, as well as more usual. The reasons for this are because I wanted to take advantage of the processor’s own interrupt mechanism that when returning POPs registers RO-R3, LR, PC and xPSR, and also to avoid the interruption nesting during the preemption of kernel tasks.
If I had chosen to use only one kernel stack for all threads, the implementation within the handler itself I think would be better. Choices, design choices…

6. Implementation
Below I explain the codes created to implement previously described features proof of concept. Most of the kernel itself is written in assembly, except for a portion of the supervisor calls handler that is written in C with some inline assembly. In my opinion, more cumbersome and susceptible to errors than writing in assembly is to embed assembly in C code. The toolchain used is the GNU ARM.
6.1. Stacks
There is nothing special here, except that now in addition to the stack user declare another array of integers for the stack of the kernel . These will be associated in the Thread Control Block.
int32_t p_stacks[NTHREADS][STACK_SIZE]; // user stack int32_t k_stacks[NTHREADS][STACK_SIZE]; // kernel stack
6.2. Task Scheduler
The main difference from this to the scheduler shown in the last publication is that we will now handle two different stack pointers: the MSP and the PSP. Thus, when entering an exception handler, the portion of the stackframe saved automatically depends on the stack pointer used when the exception took place. However, in the exception routine, the active stack pointer is always the MSP. Thus, in order to be able to handle a stack pointer when we are operating with another, we will cannot use the PUSH and POP pseudo-instructions because they have the active stack pointer as their base address . We will have to replace them with the instructions LDMIA (load multiple and increment after) for POP, and STMDB (store multiple decrement before) for PUSH, with the writeback sign “!” at the base address [1] .
// Example of POP
MRS R12, PSP // reads the value of the process stack pointer in R12
LDMIA R12!, {R4-R11} // R12 contains the base address (PSP)
/ * the address contained in R12 now stores the value from R4; [R12] + 0x4
contains the value of R5, and so on until [R12] + 0x20 contains the
value of R11.
the initial value of R12 is also increased by 32 bytes
* /
MSR PSP, R12 // PSP is updated to the new value of R12
// Example of PUSH
MSR R12, MSP
STMDB R12!, {R4-R11}
/ * [R12] - 0x20 contains R4, [R12] - 0x16 contains R5, ..., [R12] contains R4
the initial value of R12 is decremented by 32 bytes * /
MSR MSP, R12 // MSP is updated to the new value of R12
Another difference is that the TCB structure now needs to contain a pointer to each of the stack pointers of the thread it controls, and also a flag indicating whether the task to be resumed was using MSP or PSP when it was interrupted.
// thread control block struct tcb { int32_t* psp; //psp saved from the last interrupted thread int32_t* ksp; //ksp saved from the last interrupted kernel thread struct tcb *next; //points to next tcb int32_t pid; //task id int32_t kernel_flag; // 0=kernel, 1=user }; typedef struct tcb tcb_t; tcb_t tcb[NTHREADS]; //tcb array tcb_t* RunPtr;
The scheduler routines are shown below. The code was written so it is clear in its intention, without trying to save instructions. Note that in line 5 the value of LR at the entry of the exception is only compared with 0xFFFFFFFD, if false it is assumed that it is 0xFFFFFFFF9, this is because I guarantee that there will be no nested interrupts (SysTick never interrupts an SVC, for example), so the LR should never assume 0xFFFFFFF1. If other than a proof of concept, the test should be considered.
.global SysTick_Handler .type SysTick_Handler, %function SysTick_Handler: CPSID I // atomic begins CMP LR, #0xFFFFFFFD // were we at an user thread? BEQ SaveUserCtxt // if yes B SaveKernelCtxt //if no SaveKernelCtxt: MRS R12, MSP STMDB R12!, {R4-R11} //push R4-R11 MSR MSP, R12 LDR R0,=RunPtr LDR R1, [R0] LDR R2, [R1,#4] STR R12, [R2] //saves stack pointer B Schedule SaveUserCtxt: MRS R12, PSP STMB R12!, {R4-R11} MSR PSP, R12 LDR R0,=RunPtr LDR R1, [R0] STR R12, [R1] B Schedule Schedule: LDR R1, =RunPtr //R1 <- RunPtr LDR R2, [R1] LDR R2, [R2,#8] //R2 <- RunPtr.next STR R2, [R1] //updates RunPtr LDR R0, =RunPtr LDR R1, [R0] LDR R2, [R1,#16] CMP R2, #1 //if kernel_flag=1 BEQ ResumeUser //yes, resume user thread B ResumeKernel //no, resume kernel thread ResumeUser: LDR R1, =RunPtr //R1 <- RunPtr LDR R2, [R1] LDR R2, [R2] LDMIA R2!, {R4-R11} //Pop sw stackframe MSR PSP, R2 MOV LR, #0xFFFFFFFD //LR=return to user thread CPSIE I //atomica end BX LR ResumeKernel: LDR R1, =RunPtr //R1 <- RunPtr LDR R2, [R1] LDR R2, [R2, #4] MSR MSP, R2 LDMIA R2!, {R4-R11} //Retrieves sw stackframe MSR MSP, R2 MOV LR, #0xFFFFFFF9 //LR=return to kernel thread CPSIE I //atomic end BX LR
6.3 System Calls
The implementation of system calls uses the SVC Handler. As stated, SVC has a unique input parameter (ARM makes it sounds like an advantage…), that is the number we associate with a callback. But then how do we pass the arguments forward to the callback, if we can make system calls with only one input parameter? They need to be retrieved from the stack. The AAPCS (ARM Application Procedure Call Standard) which is followed by compilers, says that when a function (caller) calls another function (callee), the callee expects its arguments to be in R0-R3. Likewise, the caller expects the callee return value to be in R0. R4-R11 must be preserved between calls. R12 is the scratch register and can be freely used.
No wonder that when an exception takes place the core saves (PUSH) the registers R0-R3, LR, PC and xPSR from the interrupted function, and when returning put them (POP) again in the core registers. It is fully prepared to get back to the same point when it was interrupted. But if we change the context, that is, after the interruption we do not return to the same point we were before, there will be a need to explicitly save the remaining stackframe so this thread can be resumed properly later. It is essential to follow the AAPCS if we want to evoke functions written in assembly from C code and vice-versa.
To system calls, I defined a macro function in C that receives the SVC code and the arguments for the callback (the syntax of inline assembly depends on the compiler used).
#define SysCall(svc_number, args) { __ASM volatile ("MOV R0, %0 " :: "r" (args) ); __ASM volatile ("svc %[immediate]"::[immediate] "I" (svc_number) : ); }
(There is a reason I created a macro and not a common function: it has to do with the return point to the user thread and the fact that the kernel callbacks are not executed within the exception, which requires changing the context. If I had created a common function for the system call, the user stack pointer would be saved within the call, and upon returning from the kernel, the SVC would be executed again. If you know how to run the system call outside the handler routine without using a macro, please let me know!)
The args value is stored in R0. The SVC call is made with the immediate “svc_number”. When the SVC is triggered, R0-R3 will be automatically saved to the stack. The code was written as follows, without saving instructions, for clarity:
global SVC_Handler .type SVC_Handler, %function SVC_Handler: MRS R12, PSP //saves psp CMP LR, #0xFFFFFFFD BEQ KernelEntry B KernelExit //saves user context KernelEntry: MRS R3, PSP STMDB R3!, {R4-R11} MSR PSP, R3 LDR R1,=RunPtr LDR R2, [R1] STR R3, [R2] LDR R3, =#0 STR R3, [R1, #16] //kernel flag = 0 MOV R0, R12 //gets r0 from CORE to retrieve SVC number B svchandler_main //branch to C routine KernelExit: //retrieves user context LDR R0, =RunPtr LDR R1, [R0] LDR R2, [R1] LDMIA R2!, {R4-R11} MOV LR, #0xFFFFFFFD MSR PSP, R2 LDR R12, =#1 //kernel flag = 1 STR R12, [R1, #16] BX LR
The rest of the routine for entering the kernel is written in C [2, 3]. Note that in the routine written in assembly a simple branch occurs (line 20) and therefore we have not yet returned from the exception handler .
The svc_number, in turn, is retrieved by walking two bytes (hence the cast to char) out of the address of the PC that is 6 positions above R0 in the stack [1, 2, 3]. Note that it was necessary to assign to R0 the value contained in PSP shortly after entering the interrupt, before saving the rest of the stack (lines 4 and 19 of the assembly code).
After retrieving the system call number and its arguments, the MSP is overwritten with the value stored in the TCB. Then we change the value of LR so the exception returns to the base mode. The callback does not run within the handler. When the BX LR instruction is executed, the remaining of the stackframe is automatically activated onto the core registers.
#define SysCall_GPIO_Toggle 1 //svc number for gpio toggle #define SysCall_Uart_PrintLn 2 //svc number for uart print line void svchandler_main(uint32_t * svc_args) { uint32_t svc_number; uint32_t svc_arg0; uint32_t svc_arg1; svc_number = ((char *) svc_args[6])[-2]; // recupera o imediato svc_arg0 = svc_args[0]; svc_arg1 = svc_args[1]; switch(svc_number) { case SysCall_GPIO_Toggle: k_stacks[RunPtr->pid][STACK_SIZE-2] = (int32_t)SysCallGPIO_Toggle_; //PC k_stacks[RunPtr->pid][STACK_SIZE-8] = (int32_t)svc_arg0; //R0 k_stacks[RunPtr->pid][STACK_SIZE-1] = (1 << 24); // T=1 (xPSR) __ASM volatile ("MSR MSP, %0" : : "r" (RunPtr->ksp) : ); __ASM volatile ("POP {R4-R11}"); __ASM volatile ("MOV LR, #0xFFFFFFF9"); __ASM volatile ("BX LR"); //returns from exception break; case SysCall_Uart_PrintLn: k_stacks[RunPtr->pid][STACK_SIZE-2] = (int32_t)SysCallUART_PrintLn_; k_stacks[RunPtr->pid][STACK_SIZE-8] = (int32_t)svc_arg0; k_stacks[RunPtr->pid][STACK_SIZE-1] = (1 << 24); // T=1 __ASM volatile ("MSR MSP, %0" : : "r" (RunPtr->ksp) : ); __ASM volatile ("POP {R4-R11}"); __ASM volatile ("MOV LR, #0xFFFFFFF9"); __ASM volatile ("BX LR"); //returns from exception break; default: __ASM volatile("B SysCall_Dummy"); break; break; } }
A callback looks like this:
static void SysCall_CallBack_(void* args) { BSP_Function((int32_t*) args); //BSP function with one argument int32 exitKernel_(); // leaves cooperatively }
6.4. Start-up
The start-up is a critical point. The system starts in base mode. The stacks are assembled. The first task to be performed by the kernel after booting the system is to configure SysTick, switch to user mode and trigger the first user thread .
The assembly routines for the star-up are as follows:
.equ SYSTICK_CTRL, 0xE000E010 .equ TIME_SLICE, 999 .global kStart .type kStart, %function kStart: LDR R0, =RunPtrStart LDR R1, [R0] LDR R2, [R1,#4] MSR MSP, R2 // MSP <- RunPtr.ksp POP {R4-R11} //loads stackframe 0 at call stack POP {R0-R3} POP {R12} ADD SP, SP, #4 POP {LR} //LR <- PC = UsrAppStart ADD SP, SP, #4 BX LR // branches to UsrAppStart //this function manages the stack to run the first user thread .global UsrAppStart .type UsrAppStart, %function UsrAppStart: LDR R1, =RunPtr //R1 <- RunPtr LDR R2, [R1] LDR R2, [R2] MSR PSP, R2 BL SysTickConf //configures systick MOV R0, #0x3 MSR CONTROL, R0 //thread unprivileged mode ISB // inst set barrier: guarantees CONTROL is updated before going POP {R4-R11} //loads stackframe 0 POP {R0-R3} POP {R12} ADD SP, SP, #4 POP {LR} //LR <- PC ADD SP, SP, #4 BX LR SysTickConf: LDR R0, =SYSTICK_CTRL MOV R1, #0 STR R1, [R0] // resets counter LDR R1, =TIME_SLICE STR R1, [R0,#4] // RELOAD <- TIME_SLICE STR R1, [R0,#8] // CURR_VALUE <- TIME_SLICE MOV R1, #0x7 // 0b111: // 1: Clock source = core clock // 1: Enables irq // 1: Enables counter STR R1, [R0] BX LR //get back to caller
7. Test
For a little demonstration, we will write on the PC screen via UART. The callback for the system call was written as follows:
static void SysCallUART_PrintLn_(const char* args) { __disable_irq(); uart_write_line(UART, args); // waits until transmission is done while (uart_get_status(UART) != UART_SR_TXRDY); __enable_irq(); exitKernel_(); // exit kernel cooperatively }
It is necessary to be careful when using multitasking to use any shared resource, since we have not yet inserted any inter-process communication mechanism. However, the operation is at a “Guarded Region”, and it will not be interrupted by SysTick. The main program is as follows:
#include <commondefs.h> //board support package, std libs, etc. #include <kernel.h> #include <tasks.h> int main(void) { kHardwareInit(); kAddThreads(Task1, (void*)"Task1nr", Task2, (void*)"Task2nr", Task3, (void*)"Task3nr"); RunPtrStart = &tcbs[0]; RunPtr = &tcbs[1]; uart_write_line(UART, "Inicializando kernel...nr"); delay_ms(500); //delay to print screen : P kStart(); while(1); }
The tasks (main threads) look like this:
void Task1(void* args) { const char* string = (char*)args; while(1) { SysCall(SysCall_Uart_PrintLn, string); } }
In the figure below, the running system:

8. Conclusions
The use of two stack pointers, one for application and another for the kernel isolates these spaces not allowing the application to corrupt the kernel stack. The privileges prevent the user from overwriting special registers, keeping the kernel safe from application programming errors or malicious code.
Adding another stack pointer to the system required changes to the scheduling routine because we now manipulate two stacks in the domain of two different stack pointers, and both can be preempted. In addition, a cooperative mechanism has also been added for kernel exiting.
The one kernel stack per user stack approach makes the development of kernel or application routines to follow the same pattern from the perspective of who is writing the system. The price to pay is memory overhead and more latency when switching contexts. To mitigate the last, cooperative mechanisms can be added as shown. To mitigate the memory overhead more attention should be put when modeling the tasks (or concurrent units), so they are efficiently allocated.
The system call mechanism is used as an entry point to hardware services, or whatever else we deem critical for the security and stability of the system. This will make even more sense by separating not only the stacks at privilege levels but also the memory regions with the MPU .
For the next publications we will: 1) create IPC mechanisms, 2) add priority levels to the tasks that are running on a fixed round-robin scheme 3) configure the MPU
9. References
[1] http://infocenter.arm.com/help/topic/com.arm.doc.ddi0337e/DDI0337E_cortex_m3_r1p1_trm.pdf
[2] The definitive Guide to ARM Cortex M3, Joseph Yiu
Separando espaços de supervisor e usuário no ARM Cortex-M3
1. Introdução
Os processadores ARM Cortex-M estão integrados a SoCs para domínios diversos, principalmente em muito daquilo que chamamos de dispositivos smart. Esta publicação é uma continuação da anterior, em que demonstrei a implementação de um mecanismo preemptivo mínimo para um ARM Cortex-M3, aproveitando-se dos recursos de hardware especiais para a troca de contexto.
Alguns recursos importantes desta arquitetura serão agora demonstrados como a separação de threads de usuário e kernel – com 2 stack pointers: MSP e PSP e a utilização de Supervisor Calls para implementar chamadas ao sistema.
Apesar de alguns conceitos sobre sistemas operacionais serem abordados porque são inerentes ao assunto, o objetivo é a exploração do ARM Cortex-M3, um processador relativamente barato e de larga aplicabilidade, e como aproveitá-lo para desenvolver sistemas mais robustos.
2. Registradores especiais
São 3 os registradores especiais no ARM Cortex-M3. Você pode consultar a documentação da ARM para entender o papel de cada um deles no processador. O mais importante nesta publicação é o CONTROL.
- Program Status Registers (APSR, IPSR, e EPSR)
- Interrupt Mask Registers (PRIMASK, FAULTMASK e BASEPRI)
- Control (CONTROL).
Registradores especiais somente podem ser acessado através das instruções privilegiadas MRS (ARM Read Special Register) e MSR (ARM Set Special Register):
//Carrega em R0 o atual valor contido no registrador especial MRS R0, SPECIAL //Carrega no registrador especial o valor contido em R0) MSR SPECIAL, R0
O registrador CONTROL tem somente 2 bits configuráveis. Quando um serviço de excepção (e.g., SysTick_Handler) estiver a ser executado, o processador estará em modo privilegiado e utilizando o main stack pointer (MSP), e CONTROL[1] = 0, CONTROL[0] = 0. Em outras rotinas que não sejam handlers, este registrador pode assumir diferentes valores a depender da implementação do software (Tabela 1).
No pequeno kernel mostrado anteriormente, as tasks da aplicação (Task1, Task2 e Task3) também eram executadas em modo privilegiado e utilizando o stack pointer principal (MSP). Assim, um programa da aplicação poderia alterar os registradores especiais do core se quisesse.
3. Sistema com 2 stack pointers e diferentes privilégios
Na última publicação ressaltei o fato de o registrador R13 não fazer parte do stackframe, pois é ele justamente que guarda o endereço do stack pointer. O R13 é um registrador do tipo “banked” (não conheço uma boa tradução para português), significando que ele é fisicamente replicado, e assume um valor ou outro a depender do estado do core.
CTRL[1] (0=MSP/1=PSP) | CTRL[0] (0=Priv, 1=Non priv) | Estado |
0 | 0 | Privileged handler* / Base mode |
0 | 1 | Unprivileged |
1 | 0 | Privileged thread |
1 | 1 | User thread |
*em rotinas que atendem às exceções este modo estará sempre ativo mesmo que o CTRL[0] = 1.
Com dois stack pointers, um para aplicação e outro para o kernel, significa que uma thread de usuário não poderá facilmente corromper o stack pointer do kernel por um erro de programação na aplicação. Os privilégios por sua vez evitam que a aplicação sobrescreva registradores especiais. De acordo com os manuais da ARM, um sistema operacional robusto tipicamente tem as seguintes características:
- serviços de interrupção utilizam o MSP (por default da arquitetura)
- rotinas do kernel são ativadas através do SysTick em intervalos regulares para executar, em modo privilegiado, o escalonamento das tarefas e gerenciamento do sistema
- aplicações de usuário são executadas como threads, usam o PSP em modo não-privilegiado
- a memória para as stacks do kernel e handlers são apontadas pelo MSP, sendo que os dados stack só podem ser acessados em modo privilegiado*
- a memória para a stack das aplicações é apontada pelo PSP
*por enquanto não vamos isolar os espaços de memória
4. Chamadas ao sistema (System Calls)
Em uma perspectiva simples, uma chamada de sistema é um método no qual um software requisita um serviço/mecanismo do kernel ou SO sobre o qual está rodando: gerenciamento de processos (escalonamento, mecanismos de IPC), acesso a algum recurso de hardware, sistema de arquivos, entre outros, a depender da arquitetura do sistema operacional.
Se pretendemos separar nosso sistema em níveis de privilégio é inevitável que o nível da aplicação precise fazer chamadas ao kernel para ter acesso a, por exemplo, serviços de hardware ou o que mais julgarmos crítico para a segurança e estabilidade do sistema.
Uma maneira comum de implementar system calls no ARM Cortex-M3 (e em outros cores baseados em ARMv7) é a utilização da interrupção por software chamada Supervisor Call (SVC). O SVC funciona como um ponto de entrada para um serviço que necessita de privilégios para ser executado. O único parâmetro de entrada de um SVC é o seu número (instrução ASM: SVC #N), ao qual associamos a uma chamada de função (callback). Ao contrário da outra excepção disparada via software disponível, que é o PendSV (Pendable Supervisor Call), o SVC pode ser disparado em modo usuário. Apesar de serem destinadas a diferentes usos – o PendSV é tipicamente usado como forma de “agendar” tarefas do kernel menos críticas e não perder ticks do sistema, é possível configurar o core para que usuários também possam disparar o PendSV.

5. Design
5.1 Utilização de dois stack pointers
Para a utilizar os dois stack pointers disponíveis (MSP e PSP) é fundamental entender 2 coisas:
- A manipulação do registrador de controle: só é possível escrever ou ler o registrador de controle em modo handler (na rotina que atende à uma exceção) ou em threads privilegiadas.
- O mecanismo de exceções: quando uma interrupção ocorre, o processador guarda na stack o conteúdo dos registradores R0-R3, LR, PC e xPSR, como explicado na publicação anterior. O valor do LR quando entramos em uma exceção indica o modo que o processador estava a rodar, quando a thread foi interrompida. Podemos manipular este valor de LR juntamente com a manipulação dos stack pointer para controlar o fluxo do programa.
LR | BX LR |
0xFFFFFFF9 | Volta para modo “base”, MSP privilegiado. (CONTROL=0b00) |
0xFFFFFFFD | Volta para user mode (PSP, com o nível de privilégio da entrada) (Control = 0b1x) |
0xFFFFFFF1 | Volta para a interrupção anterior, no caso de uma interrupção de maior prioridade ocorrer durante uma de menor prioridade. |
5.1.1. Um kernel stack para cada user stack
Cada stack dedicada ao usuário terá uma stack de kernel correspondente (one kernel stack per thread). Assim, cada Task (thread principal) terá stacks de kernel e usuário associadas. Uma outra abordagem seria uma stack somente de kernel no sistema (one kernel stack per processor). A vantagem de utilizar a primeira abordagem é que além das threads do kernel poderem ser preemptivas, do ponto de vista de quem implementa o sistema, os programas que rodam no kernel seguem o mesmo padrão de desenvolvimento dos programas de aplicação. A vantagem da segunda abordagem é menor overhead de memória e menor latência nas trocas de contexto.

5.2 Mecanismos de entrada e saída do kernel
Na publicação anterior a interrupção feita pelo SysTick manejava a troca de contexto, i.e., interrompia a thread em execução, salvava seu stackframe, buscava a próxima thread apontada pelo campo next na estrutura TCB (thread control block) e a resumia.
Com a separação dos espaços de usuário e supervisor, teremos dois mecanismos para entrada e saída do kernel: as system calls, explicitamente chamadas em código, e a interrupção por SysTick que implementa a lógica de escalonamento (scheduling). Outro ponto a destacar, é que apesar de continuar utilizando um esquema round-robin simples, em que cada tarefa tem o mesmo tempo de execução, as threads do kernel também funcionarão de forma cooperativa com a a thread do usuário que a evocou, isto é: quando não houver mais nada a ser feito, o kernel pode explicitamente retornar. Se a thread do kernel demorar mais que o tempo entre um tick e outro, será interrompida e reagendada. As tarefas do usuário também poderiam utilizar-se de mecanismo similar, entretanto, por simplicidade para exposição, optei por deixar as tarefas de usuário somente em esquema round-robin fixo.
5.2.1. Escalonador (task scheduler)
O fluxograma do escalonador preemptivo a ser implementado está na Figura 4. O start-up do kernel e aplicação do usuário é também mostrado para maior clareza. O kernel é inicializado e voluntariamente inicia a primeira task de usuário. A cada interrupção por SysTick, a thread tem seu estado salvo e a próxima task agendada é resumida consoante ao estado em que foi interrompida: kernel ou user mode.

5.2.2 Chamadas ao sistema (System Calls)
As chamadas ao sistema ocorrem quando o usuário requisita acesso a um serviço privilegiado. Além disso, também utilizo o mesmo mecanismo para que tarefas privilegiadas do kernel retornem cooperativamente à tarefa do usuário.
Perceba que optei por não executar o as threads de kernel dentro do próprio handler, o que seria mais intuitivo, além de usual. As razões para isto são porque quis aproveitar o próprio mecanismo de interrupção do processador, que no seu retorno faz o POP dos registradores RO-R3, LR, PC e xPSR, e também para evitar o aninhamento de interrupções durante a preempção de tarefas do kernel. Caso eu tivesse optado por utilizar somente uma kernel stack para todas as threads, a implementação dentro do próprio handler julgo que seria melhor.

6. Implementação
Abaixo explico os códigos criados para implementar as provas de conceito das funcionalidades anteriormente descritas. A maior parte do kernel propriamente dito é escrito em assembly, exceto para uma porção do handler de supervisor calls que é escrita em C com algum inline assembly. Na minha opinião, mais trabalhoso e suscetível a erros que escrever em assembly é embutir assembly no código C. A toolchain utilizada é a GNU ARM.
6.1. Stacks
Não há nada em especial aqui, exceto que agora além da stack de usuário, declaramos outro array de inteiros para a stack do kernel. Estes serão associados no Thread Control Block.
int32_t p_stacks[NTHREADS][STACK_SIZE]; // stack de usuário int32_t k_stacks[NTHREADS][STACK_SIZE]; // stack do kernel
6.2. Task Scheduler
A principal diferença deste para o escalonador mostrado na última publicação é que agora manejaremos 2 stack pointers distintos: o MSP e o PSP. Assim, quando entramos em um handler de exceção, a porção do stackframe salvo automaticamente depende do stack pointer utilizado quando a exceção foi disparada. Entretanto na rotina de exceção o stack pointer ativo é sempre o MSP. Desta forma, para podermos manejar um stack pointer quando estamos a operar com outro, não poderemos utilizar as instruções PUSH e POP porque estas têm como endereço base o stack pointer ativo. Teremos de substituí-las pelas instruções LDMIA (load multiple and increment after, i.e, após o primeiro load incrementa-se o endereço base) para o POP, e STMDB (store multiple decrement before, antes do primeiro store decrementa-se o endereço base) para o PUSH, com o sinal de writeback “!” no endereço base [1].
// Exemplo de POP MRS R12, PSP // lê o valor do process stack pointer em R12 LDMIA R12!, {R4-R11} // R12 contém o endereço base (PSP) /* o endereço contido em R12 agora armazena o valor de R4, [R12] + 0x4 contém o valor de R5, e assim por diante até que [R12] + 0x20 contém o valor de R11. o valor inicial de R12 também é incrementado em 32 bytes */ MSR PSP, R12 // PSP é atualizado para o novo valor de R12 // Exemplo de PUSH MSR R12, MSP STMDB R12!, {R4-R11} /* [R12] - 0x20 contém R4, [R12] - 0x16 contém R5, ..., [R12] contém R4 o valor inicial de R12 é decrementado em 32 bytes */ MSR MSP, R12 // MSP é atualizado para o novo valor de R12
Outra diferença é que agora a estrutura TCB precisa conter um ponteiro para cada um dos stack pointers da thread que controla, e também uma flag indicando se a tarefa a ser resumida estava utilizando o MSP ou o PSP quando foi interrompida.
// thread control block struct tcb { int32_t* psp; //psp salvo da ultima user thread interrompida int32_t* ksp; //ksp salvo da ultima kernel thread interrompida struct tcb *next; //aponta para o proximo tcb int32_t pid; //id da task (é o "i" do kSetInitStack(i)) int32_t kernel_flag; // 0=kernel, 1=user }; typedef struct tcb tcb_t; tcb_t tcb[NTHREADS]; //array de tcbs tcb_t* RunPtr;
Abaixo a rotina que escrevi para implementação. O código foi escrito de forma a ficar claro em sua intenção, sem tentar economizar instruções. Perceba que na linha 5 o valor de LR assumido na entrada da excepção só é comparado com 0xFFFFFFFD, caso falso assume-se que ele é 0xFFFFFFFF9, isto porque garanto que não haverá interrupções aninhadas (o SysTick nunca interrompe um SVC, por exemplo), assim o LR nunca deve assumir 0xFFFFFFF1. Para propósitos que não uma prova de conceito, o teste deveria ser considerado.
.global SysTick_Handler .type SysTick_Handler, %function SysTick_Handler: CPSID I //atomica inicio CMP LR, #0xFFFFFFFD //verifica se retornou de uma user thread BEQ SaveUserCtxt //se sim, branch para save user context B SaveKernelCtxt //se nao SaveKernelCtxt: MRS R12, MSP STMDB R12!, {R4-R11} //push R4-R11 MSR MSP, R12 LDR R0,=RunPtr //RunPtr aponta para a tcb atual LDR R1, [R0] LDR R2, [R1,#4] STR R12, [R2] //salva stack pointer B Schedule SaveUserCtxt: MRS R12, PSP STMB R12!, {R4-R11} MSR PSP, R12 LDR R0,=RunPtr LDR R1, [R0] STR R12, [R1] B Schedule Schedule: LDR R1, =RunPtr //R1 <- RunPtr LDR R2, [R1] LDR R2, [R2,#8] //R2 <- RunPtr.next STR R2, [R1] //atualiza valor de RunPtr LDR R0, =RunPtr LDR R1, [R0] LDR R2, [R1,#16] CMP R2, #1 //verifica se kernel_flag=1 BEQ ResumeUser //sim, resume user thread B ResumeKernel //nao, resume kernel thread ResumeUser: LDR R1, =RunPtr //R1 <- RunPtr LDR R2, [R1] LDR R2, [R2] LDMIA R2!, {R4-R11} //Resgata sw stackframe MSR PSP, R2 MOV LR, #0xFFFFFFFD //LR=return to user thread CPSIE I //atomica fim BX LR ResumeKernel: LDR R1, =RunPtr //R1 <- RunPtr atualizado LDR R2, [R1] LDR R2, [R2, #4] MSR MSP, R2 LDMIA R2!, {R4-R11} //Resgata sw stackframe MSR MSP, R2 MOV LR, #0xFFFFFFF9 //LR=return to kernel thread CPSIE I //atomic fim BX LR
6.3 System Calls
A implementação dos system calls utiliza o SVC Handler. Como dito, o único parâmetro de entrada do SVC é o número que associamos a um callback. Mas então como passamos os argumentos para o callback? Eles precisam ser buscados na stack. O padrão AAPCS (ARM Application Procedure Call Standard), que é seguido pelos compiladores, diz que quando uma função (caller) chama outra função (callee), o callee espera que seus argumentos estejam em R0-R3. Da mesma forma, o caller espera que o retorno do callee esteja em R0. R4-R11 precisam ser preservados entre uma chamada e outra. R12 é o scratch register e pode ser usado livremente.
Não à toa, quando uma exceção ocorre o core salva (PUSH) na stack os registradores R0-R3, LR, PC e xPSR da função que foi interrompida, e no retorno os lança (POP) novamente nos registradores do core. Se trocarmos de contexto, isto é, após a interrupção não retornarmos ao mesmo ponto do programa que foi interrompido, precisamos explicitamente salvar o restante da stackframe para que a thread seja resumida de forma íntegra. É fundamental seguir o AAPCS se quisermos evocar funções escritas em assembly em código C e vice-versa.
Para executar chamadas ao sistema defini uma função macro em C que recebe o código do SVC e os argumentos para a callback (a sintaxe de assembly inline depende do compilador utilizado).
(Há uma razão de eu ter criado uma macro e não uma função comum: tem a ver com o ponto de retorno à user thread e o fato de os callbacks do kernel não serem executados dentro da exceção, o que exige a troca de contexto. Se criasse uma função comum para o system call, o stack pointer do usuário seria salvo dentro da chamada, e ao retornar do kernel, o SVC seria novamente executado.)
#define SysCall(svc_number, args) { \ __ASM volatile ("MOV R0, %0 " :: "r" (args) ); \ __ASM volatile ("svc %[immediate]"::[immediate] "I" (svc_number) : ); \ }
O valor de args é armazenado em R0. A chamada do SVC é feita com o imediato “svc_number”. Quando o SVC é disparado R0-R3 serão automaticamente salvos na stack. O código foi escrito da seguinte forma, sem economizar instruções, para clareza:
.global SVC_Handler .type SVC_Handler, %function SVC_Handler: MRS R12, PSP //salva psp CMP LR, #0xFFFFFFFD BEQ KernelEntry B KernelExit //salva contexto do usuário KernelEntry: MRS R3, PSP STMDB R3!, {R4-R11} MSR PSP, R3 LDR R1,=RunPtr LDR R2, [R1] STR R3, [R2] LDR R3, =#0 STR R3, [R1, #16] //kernel flag = 0 MOV R0, R12 //psp da chamada no r0 do CORE pra recuperar svc number B svchandler_main //branch para rotina em C KernelExit: //recarrega frame do usuário LDR R0, =RunPtr LDR R1, [R0] LDR R2, [R1] LDMIA R2!, {R4-R11} MOV LR, #0xFFFFFFFD MSR PSP, R2 LDR R12, =#1 //kernel flag = 1 STR R12, [R1, #16] BX LR
O restante da rotina para entrada no kernel é escrito em C [2, 3]. Perceba que na rotina escrita em assembly um branch simples ocorre (linha 20) e portanto ainda não retornamos do handler de exceção.
#define SysCall_GPIO_Toggle 1 //codigo svc para gpio toggle #define SysCall_Uart_PrintLn 2 //codigo svc para uart print line void svchandler_main(uint32_t * svc_args) { uint32_t svc_number; uint32_t svc_arg0; uint32_t svc_arg1; svc_number = ((char *) svc_args[6])[-2]; // recupera o imediato svc_arg0 = svc_args[0]; svc_arg1 = svc_args[1]; switch(svc_number) { case SysCall_GPIO_Toggle: k_stacks[RunPtr->pid][STACK_SIZE-2] = (int32_t)SysCallGPIO_Toggle_; //PC k_stacks[RunPtr->pid][STACK_SIZE-8] = (int32_t)svc_arg0; //R0 k_stacks[RunPtr->pid][STACK_SIZE-1] = (1 << 24); // T=1 (xPSR) __ASM volatile ("MSR MSP, %0" : : "r" (RunPtr->ksp) : ); __ASM volatile ("POP {R4-R11}"); __ASM volatile ("MOV LR, #0xFFFFFFF9"); __ASM volatile ("BX LR"); //retorna da excecao break; case SysCall_Uart_PrintLn: k_stacks[RunPtr->pid][STACK_SIZE-2] = (int32_t)SysCallUART_PrintLn_; k_stacks[RunPtr->pid][STACK_SIZE-8] = (int32_t)svc_arg0; k_stacks[RunPtr->pid][STACK_SIZE-1] = (1 << 24); // T=1 __ASM volatile ("MSR MSP, %0" : : "r" (RunPtr->ksp) : ); __ASM volatile ("POP {R4-R11}"); __ASM volatile ("MOV LR, #0xFFFFFFF9"); __ASM volatile ("BX LR"); //retorna da excecao break; default: __ASM volatile("B SysCall_Dummy"); break; break; } }
O svc_number, por sua vez, é recuperado ao andarmos dois bytes (por isso o cast para char) descrescentes a partir endereço do PC que está 6 posições acima de R0 na stack [1, 2, 3]. Note que foi preciso assinalar a R0 o valor contido em PSP logo após a entrada na interrupção, antes de salvarmos o restante da stack (linhas 4 e 19 do código assembly).
Após recuperar o número do SVC e os argumentos, inicializamos a stack do kernel, MSP é sobrescrito com o valor armazenado no TCB, mudamos o valor de LR para que a exceção ao retornar vá para o modo base, pois não iremos executar o callback dentro do handler. Quando a instrução BX LR é executada o restante do stackframe é automaticamente ativado nos registradores do core.
Um callback tem a seguinte cara:
static void SysCall_CallBack_(void* args) { BSP_Function((int32_t) args); //funcao do BSB com unico argumento int32 exitKernel_(); // //qualquer chamada ao svc aqui ira sair do kernel (Figura 5) }
6.4. Start-up
O start-up é um ponto crítico. O sistema inicializa em modo base. As stacks são montadas. A primeira tarefa a ser executada pelo kernel após a inicialização do sistema é configurar o SysTick, mudar para o modo de usuário e disparar a primeira thread de usuário.
//inicializacao da primeira stack tcb_t* RunPtrStart; RunPtrStart = tcb[0]; void kFirstThreadInit(void) { kSetInitStack(0); /* esta função segue o mesmo padrão da última postagem */ k_stacks[0][STACK_SIZE-2] = (int32_t) UsrAppStart; // LR = UsrAppStart } // RunPtr e o restante dos tcbs é configurado da mesma forma da última publicacao
As rotinas em assembly para o start–up são as seguintes:
.equ SYSTICK_CTRL, 0xE000E010 .equ TIME_SLICE, 999 .global kStart // esta é a função principal de init .type kStart, %function kStart: LDR R0, =RunPtrStart LDR R1, [R0] LDR R2, [R1,#4] MSR MSP, R2 // MSP <- RunPtr.ksp POP {R4-R11} //carrega stackframe 0 na callstack POP {R0-R3} POP {R12} ADD SP, SP, #4 POP {LR} //LR <- PC = UsrAppStart ADD SP, SP, #4 BX LR // vai para UsrAppStart //esta função prepara a stack para disparar a primeira user thread .global UsrAppStart .type UsrAppStart, %function UsrAppStart: LDR R1, =RunPtr //R1 <- RunPtr LDR R2, [R1] LDR R2, [R2] MSR PSP, R2 BL SysTickConf //configura systick MOV R0, #0x3 MSR CONTROL, R0 //habilita thread unprivileged mode ISB /* inst sect barrier: garante que CONTROL estara atualizado nas proximas instrucoes*/ POP {R4-R11} //carrega stackframe 0 na callstack POP {R0-R3} POP {R12} ADD SP, SP, #4 POP {LR} //LR <- PC ADD SP, SP, #4 BX LR SysTickConf: LDR R0, =SYSTICK_CTRL MOV R1, #0 STR R1, [R0] // zera contador LDR R1, =TIME_SLICE STR R1, [R0,#4] // RELOAD <- TIME_SLICE STR R1, [R0,#8] // CURR_VALUE <- TIME_SLICE (mas tanto faz) MOV R1, #0x7 // 0b111: // 1: Clock source = core clock // 1: Habilita irq // 1: Habilita contador STR R1, [R0] BX LR //volta pro caller
7. Teste
Para fazer um pequeno teste, vamos escrever na tela do PC através da UART. O callback para chamada de sistema foi escrito da seguinte forma:
static void SysCallUART_PrintLn_(const char* args) { __disable_irq(); uart_write_line(UART, args); while (uart_get_status(UART) != UART_SR_TXRDY); //espera fim da transmissão __enable_irq(); exitKernel_(); }
É preciso tomar cuidado na hora de utilizar multithreading concorrendo para utilizar serviços de hardware, já que ainda não inserimos nenhum mecanismo de semáforo. Entretanto, fiz a operação atômica e não será interrompida pelo SysTick. O programa principal é o seguinte:
#include <commondefs.h> //board support package, libs padrão, etc. #include <kernel.h> #include <tasks.h> int main(void) { kHardwareInit(); //configura clock, interrupcoes, uart, entre outros kAddThreads(Task1, (void*)"Task1\n\r", Task2, (void*)"Task2\n\r", Task3, (void*)"Task3\n\r"); RunPtrStart = &tcbs[0]; RunPtr = &tcbs[1]; uart_write_line(UART, "Inicializando kernel...\n\r"); delay_ms(500); //delay para dar tempo de tirar o printscreen da tela 😛 kStart(); while(1); }
As tasks (threads principais) têm a seguinte cara:
void Task1(void* args) { const char* string = (char*)args; while(1) { SysCall(SysCall_Uart_PrintLn, string); } }
Na figura abaixo o sisteminha em execução:

8. Conclusões
A utilização de dois stack pointers, um para aplicação e outro para o kernel isola estes espaços não permitindo que a aplicação corrompa a stack do kernel. Os privilégios por sua vez evitam que a aplicação sobrescreva registradores especiais. Adicionar mais um stack pointer ao sistema exigiu mudanças na rotina do escalonamento porque agora manipulamos duas stacks no domínio de 2 stack pointers distintos, em que ambos estão sujeitos à preempção. Além disso, também foi adicionado um mecanismo cooperativo para que as tarefas do kernel liberem o processador para o usuário.
O mecanismo de system calls é utilizado como ponto de entrada a serviços de hardware, ou ao que mais julgarmos crítico para a segurança e estabilidade do sistema. Isto fará ainda mais sentido ao separarmos não só as stacks em níveis de privilégio mas também as regiões da memória com a MPU.
Para as próximas publicações ainda iremos: 1) incluir mecanismos de semáforos e IPCs, 2) configurar a MPU e 3) adicionar níveis de prioridades às tarefas que até então rodam em um esquema round-robin fixo.
9. Referências
[1] http://infocenter.arm.com/help/topic/com.arm.doc.ddi0337e/DDI0337E_cortex_m3_r1p1_trm.pdf
[2] The definitive Guide to ARM Cortex M3, Joseph Yiu
Um primeiro kernel preemptivo no ARM Cortex-M3
1 Introdução
Quando se fala em sistemas operativos para software embarcado, em geral pensa-se que eles são um overkill (implementação cujo custo não compensa) para a maioria das soluções. No entanto entre uma aplicação completamente “hosted” (que faz uso de um SO multithread e de propósito geral) e outra completamente “standalone” ou “bare-metal” há muitas variações.
Em publicações anteriores eu explorei a noção de tarefas cooperativas, desde um loop fazendo chamadas a partir de um vetor de ponteiros para funções, até algo um pouco mais complexo com os processos sendo manejados em um buffer circular, com critérios temporais explícitos. Desta vez vou mostrar uma implementação preemptiva mínima, para também abrir o caminho para algo um pouco mais complexo, como nas outras publicações.
2 Preemptivo versus Cooperativo
Em um sistema cooperativo o processador não realiza a troca de contexto entre uma tarefa e outra, sendo necessário que a tarefa por si só libere o processador para a próxima ocupá-lo. Não há nada de errado nisto, este esquema Run-To-Completion é suficiente e/ou necessário para muitas aplicações, e muitos sistemas embarcados são assim executados, inclusive alguns muito complexos. Mais antigamente até sistemas não-embarcados utilizavam-se de kernel cooperativo (Windows 3.x, NetWare 4.x, entre outros). Se uma tarefa travar, o sistema todo fica comprometido quando falamos de um modo estritamente cooperativo (portanto em um sistema operacional para servidores como era o NetWare, isto não parece bom).
No modo preemptivo, tarefas são interrompidas e posteriormente resumidas – i.e, um contexto (conjunto de estados dos registradores do processador) é salvo e depois recuperado. Isto leva a maior complexidade na implementação do sistema mas, se bem implementado, aumenta a robustez e a possibilidade de atender a requisitos de tempo mais estreitos.
3 Call stack
Um processador é, para todos os efeitos, uma máquina de estados. Com alguma simplificação, cada estado do nosso programa pode ser definido como o conjunto de valores dos registradoresdo core. Este conjunto dita qual rotina do programa está ativa. Portanto, ativar uma tarefa significa lançar valores na call stack de forma que esta tarefa será processada. Para trocar de contexto é necessário salvar os dados da call stack naquele ponto do programa. Estes dados “congelados” representam um estado do programa e portanto são chamados de stack frame.
Para resumir uma tarefa, um stack frame previamente salvo é carregado novamente na call stack – os registradores reais do core.
No ARM Cortex-M3, registadores de 32-bit que definem o estado ativo do processador são: R0-R12 para uso geral e R13-R15 registos de uso especial.
3.1. Arquiteturas load-store
Uma arquitetura do tipo load-store é aquela em que um dado da memória precisa de ser carregado (load) aos registradores do core antes de ser processado. Também, o resultado deste processamento antes de ser armazenado (store) na memória deve estar em um registo. Na figura abaixo (retirada de[1]) a arquitetura do processador: pipeline de 3 estágios, barramentos de dados e controle distintos, e seu banco de registros representado abaixo.
As duas operações básicas de acesso a memória no Cortex-M3:
// lê o dado contido no endereço apontado por Rn + offset e o coloca em Rn.
LDR Rd, [Rn, #offset]
//armazena dado contido em Rn no endereço apontado por Rd + offset
STR Rn, [Rd, #offset]
É importante ainda compreender no mínimo as instruções assembly do Cortex-M3 mostradas abaixo. Sugiro as fontes [1] e [2] como boas referências, além deste ou este link.
MOV Rd, Rn // Rd = Rn MOV Rd, #M// Rd = M, sendo o imediato um valor de 32 bit (aqui representado por M) ADD Rd, Rn, Rm //Rd = Rn + Rm ADD Rd, Rn, #M //Rd = Rn + M SUB Rd, Rn, Rm //Rd = Rn - Rm SUB Rd, Rn, #M //Rd = Rn - M // pseudo-instrucao para salvar Rn em uma posicão da memória. // Após um PUSH, o valor do stack pointer é decrementado em 4 bytes, e o PUSH {Rn} //Ao contrário do PUSH, o POP incrementa o SP após carregar o dado em Rn. POP {Rn} B label //pula para a rotina label BX Rm //pula para a rotina especificada indiretamente por Rm BL label //pula para label e passa endereco da prox. instrucao ao LR //para retornar LR=Link Register CPSID I //habilita interrupções CPSIE I //desabilita interrupções
Vamos operar o M3 no modo Thumb, onde as instruções têm na verdade 16 bits. Segundo a ARM, isto é feito para melhorar a densidade de código mantendo os benefícios de uma arquitetura 32-bit. O bit 24 do PSR é sempre 1.
3.2. Stacks e o stack pointer (SP)
Stack é um modelo de uso da memória. Seu funcionamento se dá no formato Last In–First Out (último a entrar, primeiro a sair). É como se eu organizasse uma pilha de documentos para ler. É conveniente que o primeiro documento a ser lido esteja no topo da pilha, e o último ao final.
Normalmente dividimos a memória entre heap e stack. Como dito, na “call stack” estarão contidos aqueles dados temporários que determinam o próximo estado do processador. No heap estão armazenados dados cuja natureza não é temporária no curso do programa (isto não significa “não-volátil”). O stack pointer é uma espécie de pivô que mantém o controle do fluxo do programa, ao apontar para alguma posição da stack.


4 Multitarefas no ARM Cortex M3
O M3 oferece dois stack pointers (Main Stack Pointer e Process Stack Pointer) para isolar os processos do usuário dos processos do kernel. Todo serviço de interrupção é executado no modo kernel. Não é possível ir do modo usuário ao modo kernel (na verdade chamados de thread mode e privileged mode) sem passar por uma interrupção – mas é possível ir do modo privilegiado ao modo usuário alterando o registro de controle.

O core também tem hardware dedicado para a troca de tarefas. O serviço de interrupção SysTick pode ser usado para implementar a troca de contextos síncrona. Ainda existem outras interrupções assíncronas por software (traps) como o PendSV e o SVC. Assim, o SysTick é utilizado para as tarefas síncronas no kernel, enquanto o SVC serve às interrupções assíncronas, quando a aplicação faz uma chamada ao sistema. O PendSV é uma trap (interrupção via software) que por padrão só pode ser disparada também no modo privilegiado. Normalmente sugere-se [1] ativá-la no SysTick, porque assim é possível manter o controle dos ticks para atender aos critérios de tempo. A interrupção por SysTick é logo servida, não correndo-se riscos de perder algum tick do relógio. Uma implementação de S.O. seguro, utilizaria os dois stack pointers para separar threads de usuário e kernel, além de também separar os domínios de memória se houver uma MPU (Memory Protection Unit) disponível.

Em um primeiro momento iremos utilizar somente o MSP em modo privilegiado.
5. Construção do kernel
Kernel é um conceito um tanto amplo, mas creio que não exista um S.O. cujo kernel não seja o responsável pelo escalonamento das tarefas. Além disso, minimamente deve haver também mecanismos de IPC (comunicação inter-processos). É interessante notar a forte hardware-dependência do escalonador que será mostrado, devido à sua natureza low-level.
5.1. Stackframes e troca de contexto
Lembre-se: call stack = valores armazenados no banco do core; stack ou stackframe = estado (valores) destes registradores salvos na memória.
Quando um SysTick é atendido, parte da call stack é salva pelo hardware (R0, R1, R2, R3, R12, R14 (LR) e R15 (PC) e PSR). Vamos chamar esta porção salva pelo hardware de hardware stackframe. O restante é o software stackframe [3], que devemos explicitamente salvar e recuperar com as instruções PUSH e POP.
Para pensar nosso sistema, podemos esquematizar uma troca de contexto completa, delineando as posições-chave que o stack pointer assume durante a operação (na figura abaixo os endereços de memória aumentam, de baixo pra cima. Quando SP aponta para R4 está alinhado com um endereço menor que o PC da sua stack)

Quando uma interrupção ocorre, SP estará apontando para o topo do stack (SP(O)) a ser salvo. É inevitável que seja assim porque é assim que o M3 funciona. Numa interrupção o hardware irá a salvar os primeiro 8 registradores mais altos da call stack nos 8 endereços abaixo do stack pointer, parando em (SP(1)). Quando salvarmos os registradores que restam, o SP agora estará apontando para o R4 da stack atual (SP(2)). Ao reassinalarmos o SP para o endereço que aponta ao R4 do próximo stack (SP(3)), o POP joga os valores de R4-R11 à call stack e o stack pointer agora está em (SP(4)). Finalmente, o retorno da interrupção resgata os valores do hardware stackframe à call stack, e o SP(5) está no topo da stack que acabou de ser ativada. (Se estiver se perguntando onde está o R13: ele armazena o valor do stack pointer )
A rotina para troca de contexto é escrita em assembly e implementa exatamente o que está descrito na Figura 7.

PS: Quando uma interrupção ocorre, o LR assume um código especial. 0xFFFFFFF9, se a thread interrompida estava a utilizar o MSP ou 0xFFFFFFFD se a thread interrompida utilizava o PSP.
5.1 Inicializando as stacks para cada tarefa
Para que a estratégia acima funcione, precisamos inicializar as stacks de cada tarefa, de acordo. O sp começa apontando para R4. Este é por definição o stack pointer inicial de uma task, pois é o endereço mais baixo de um frame.
Além disso, precisamos criar uma estrutura de dados que aponte corretamente para as stacks que serão ativadas a cada serviço de SysTick. Normalmente chamamos esta estrutura de TCB (thread control block). Por enquanto não utilizamos nenhum critério de escolha e portanto não há parâmetros de controle além do next: quando uma tarefa é interrompida, a próxima da fila será resumida e executada.

A função kSetInitStack inicializa a stack de cada thread “i“. O stack pointer na TCB associada aponta para o dado relativo ao R4. Os dados da stack são inicializados com o número do registro a que devem ser carregados para facilitar o debug. O PSR só precisa ser inicializado com o bit 24 em 1, que é o bit que identifica o modo Thumb. As tarefas são do tipo void Task(void* args).

Para adicionarmos uma tarefa à stack, precisamos inicialmente do endereço da função principal da task. Além disso, também passaremos um argumento. O primeiro argumento fica em R0. Se mais argumentos forem necessários outros registradores podem ser usados, conforme o AAPCS (ARM Application Procedure Call Standard).

5.3. Inicializando o kernel
Não basta inicializar as stacks e esperar a interrupção do SysTick. O sp da estrutura TCB só guardará um valor válido de stack pointer quando a tarefa for interrompida.
Num sistema preemptivo, temos dois tipos de threads a rodar: background e foreground. O background comporta as rotinas do kernel, entre elas, a troca de contexto. A cada SysTick, é vez do kernel utilizar o processador. No foreground estarão as aplicações.
Se a tarefa já não tiver sido executada, o stack pointer guardado em sp não será válido. Assim precisamos fazer parecer que a tarefa foi executada, interrompida e guardada – para ser reativada depois. Eu usei a seguinte estratégia:
- Uma interrupção é forçada (PendSV). Hardware stackframe inicial é salvo.
- tcb[0].sp é carregado em SP. SP agora tem o endereço do dado R4 da stackframe
- O R4–R11 do core são carregados com os valores da stackframe inicializada.
- ISR retorna, recupera o hardware stack frame e o SP estará no topo da stack. O PC agora está carregado com o endereço da primeira chamada a ser feita, e o programa segue o fluxo.

Em [2] é sugerido uma outra forma de inicializar o kernel, bem mais didática:

Dispensa-se a interrupção e a call stack é carregada ativando o LR com o valor do PC da stack. Após finalmente levar o SP ao topo da stack, o BX LR executa a task e retorna.
Se utilizarmos o primeiro método apresentado, kStart fica simplesmente:
// com a lib CMSIS void kStart(void) { SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk; }
6. Juntando as peças
Para ilustrar vamos executar, em Round-Robin, 3 tarefas que chaveiam a saída de 3 diferentes pinos e incrementam um contador cada. O tempo entre uma troca de contexto e outro será de 1000 ciclos do relógio principal. Perceba que estas funções rodam dentro de um “while(1) { }”. É como se tivéssemos vários programas principais sendo executados no foreground. Cada stack tem 64 elementos de 4 bytes (256 bytes).

Abaixo a função main do sistema. O hardware é inicializado. As tarefas são adicionadas e as stacks inicializadas com a função kAddThreads. O RunPtr recebe o endereço da thread 0. Após configurar o SysTick para disparar a cada 1000 ciclos de clock, inicializa-se o kernel. Depois de executar a primeira tarefa e ser interrompido, o sistema fica quicando entre uma tarefa e outra, com a troca de contexto a rodar no background.

6.1. Debug
Você vai precisar pelo menos de um simulador para implementar mais facilmente o sistema, pois precisará acessar os registradores do core (call stack) e ver os dados movimentando-se nas stacks. Se o sistema estiver a funcionar, a cada pausa do debugger os contadores devem ter praticamente o mesmo valor.
Na foto abaixo, utilizo uma placa Arduino Due com o processador Atmel SAM3X8E e um debugger Atmel ICE conectado na JTAG da placa. No osciloscópio pode-se ver as formas de onda das saídas chaveando de cada uma das 3 tasks.


7 Considerações finais
A implementação de um kernel preemptivo exige razoável conhecimento da arquitetura do processador a ser utilizado. Carregar os registradores da call stack e salvá-los de forma “artesanal” nos permite ter um maior controle do sistema às custas da complexidade de manejarmos as stacks.
O exemplo aqui apresentado é um exemplo mínimo onde cada tarefa recebe o mesmo tempo para ser executada. Após esse tempo, a tarefa é interrompida e suspensa – os dados da call stack são salvos. Este conjunto salvo chamamos de stackframe – uma “fotografia” da ponto do programa que estava a ser executado. A próxima tarefa a ser executada é carregada do ponto de onde parou e resumida. O código foi escrito de forma a explicitar os conceitos.
Na próxima publicação iremos adicionar prioridade às tarefas e separar as threads entre modo usuário e modo privilegiado – utilizando os dois stack pointers – requisito fundamental para um sistema mais seguro.
Referências
O texto desta postagem bem como as figuras não referenciadas são do autor. [1] The definitive guide to ARM Cortex-M3 , Joseph Yiu [2] Real-Time Operating Systems for the ARM Cortex-M, Jonathan Valvano [3] https://www.embedded.com/taking-advantage-of-the-cortex-m3s-pre-emptive-context-switches/
Escalonamento cooperativo em software embarcado (2/2)
Escalonador cooperativo para soft real-time
Os sistemas com requisitos de tempo-real são classificados em soft, firm e hard, apesar de estes critérios não serem bem estabelecidos. Em sistemas ‘hard’, os requisitos de tempo precisam ser estritamente atendidos sob pena da falha total. No soft/firm, o não cumprimento das deadlines é tolerado em alguma medida, ocasionando degradação da qualidade do sistema sem levar à falha total.
Na última publicação descrevi algumas arquiteturas de escalonadores (schedulers) que na sua melhor forma executava toda tarefa agendada no mesmo intervalo de tempo, e também poderia reagendar ou descartar a tarefa.
Vou estender esta última arquitetura um pouco mais, agora ao invés de informar ao escalonador somente o endereço da função, também vou informar uma frequência de execução que deve ser cumprida.
Como agora precisamos contar os ticks do relógio para atender aos requisitos temporais, precisaremos de uma referência de tempo. Para isso podemos usar um temporizador que gere uma interrupção a cada Q segundos. O serviço que atende esta interrupção informa ao escalonador que houve um tick de relógio. O menor intervalo de tempo entre um tick e outro é por vezes chamado de quantum e é uma escolha importante de projeto.
O escalonador será composto basicamente por um buffer circular que aponta para os processos, e um disparador que fica responsável por arbitrar qual processo está pronto para ser disparado.
A figura abaixo ilustra a arquitetura proposta:
Cada estrutura de processo é declarada com um período (em ticks do sistema). Quando o processo é adicionado ao buffer circular, um inteiro é inicializado, em tempo de execução, com o período informado na chamada de sistema. A cada interrupção gerada pelo tick do relógio, este número é decrementado e pode ser visto como uma deadline. A cada ciclo de máquina, o árbitro varre pelo processo com o menor prazo de execução, para dispará-lo em seguida (colocado na posição inicial do buffer). A função evocada retorna REPEAT ou ONESHOT, caso queira ou não ser reagendada, ou FAIL como código de erro. O código abaixo mostra o cabeçalho do programa (desisti de escrever os códigos no texto da publicação, o editor do WordPress é muito ruim!):
Na estrutura process a variável deadline é com sinal para poder registrar o atraso, quando ocorrer. Além disso, se a tarefa for reagendada, o contador do novo processo apontado no buffer vai iniciar subtraindo este atraso para ajustar o atraso total do sistema.
Quando o disparador varre o buffer para selecionar o processo com a menor deadline, ele também organiza a fila em ordem decrescente de deadlines. Se o processo anterior retornou FAIL, o escalonador analisa se o deadline do próximo processo a ser executado é menor que o período do atual. Caso verdadeiro ele dispara o processo mais uma vez. Por isso foi necessário organizar o buffer sempre em ordem crescente de atraso, para garantir que o processo atual seja comparado com o mais crítico da fila.
Quando a menor deadline da fila for maior que 0, será necessário esperar até o contador chegar a zero, e uma boa prática é utilizar este tempo para colocar o processador em um modo de baixo consumo (verificando no manual do processador se neste modo ele ainda é sensível à interrupção que gera o tick!).
A configuração do temporizador que realiza a interrupção para gerar o tick do relógio depende da arquitetura. O código abaixo escrevi para um ATMega328p rodando a 16MHz. Ele está gerando o tick a cada 4ms.
Abaixo as rotinas para habilitar e sair do modo IDLE:
A função schInit() inicializa o scheduler, zerando os índices do buffer e inicializando o temporizador para geração do tick do sistema.
O programa principal de um sistema utilizando este scheduler teria a seguinte cara:
Aviso: o código acima tem propósitos didáticos e não é um artefato validado. Não há nenhuma garantia de funcionamento livre de erros, e o autor não se responsabiliza pelo uso.
O texto desta postagem é original. Os seguintes livros foram consultados para sua elaboração: [1] Programação de sistemas embarcados, Rodrigo Almeida e outros. [2] Real-Time Systems Development, Rob Williams [3] Patterns for Time-Triggered Embedded Systems, Michael J. Pont
Escalonamento cooperativo de tarefas em software embarcado (1/2)
Arquiteturas para escalonamento cooperativo de tarefas
Na publicação anterior escrevi sobre o uso do chamado super-loop e suas limitações quando falamos em atender requisitos de tempo em uma planta. Além disso, também mostrei que podemos utilizar interrupções disparadas por temporizadores para garantir que tarefas sejam executados em intervalos definidos. Neste caso, para cada tarefa periódica precisamos de um temporizador. Em sistemas um pouco mais complexos, ambas as arquiteturas são muito suscetíveis a erros e de difícil manutenção e reuso/extensão.
Um escalonador é um bloco de código que permite ao programador inserir tarefas que serão chamadas em intervalos regulares sem que seja necessário um serviço de interrupção (ISR) dedicado a cada uma delas (podemos pensá-lo como um ISR compartilhado). Quando comparado ao super-loop, o escalonador permite que o programador não se preocupe em manejar os delays entre uma tarefa e outra para garantir a periodicidade desejada. Quando comparado a utilização com temporizadores, o uso de um escalonador é menos custoso pois não necessitamos de um temporizador para cada tarefa periódica.
Existem dois tipos gerais de escalonadores os cooperativos e os preemptivos. Os escalonadores cooperativos simplesmente executam uma tarefa após a outra, com ou sem deadline no tempo. Os escalonadores preemptivos indexam prioridades a cada uma das tarefas, e caso um evento que chame uma tarefa de maior prioridade ocorra durante a execução de uma menos prioritária, esta é pausada e retomada após a conclusão daquela. Por isso estes escalanadores são ditos multitarefas. (A tarefa atual não precisa terminar para que outra seja executada. Guardadas as devidas proporções e aplicabilidade, o MS Windows é multitarefas pois o usuário do PC pode alternar entre uma tarefa e outra, e o sistema operacional chaveia o contexto do microprocessador – alterna entre um processo e outro – dando a impressão de que todas as tarefas estão sendo executadas ao mesmo tempo. O antigo MS-DOS é monotarefa. )
Vou explorar aqui algumas implementações para escalonadores cooperativos.
Escalonamento cooperativo utilizando máquinas de estados
Independente da arquitetura do escalonador, se ele for cooperativo suas operações podem ser descritas da seguinte forma:
-
as tarefas são agendadas para ocorrer em um determinado período de tempo
-
quando o agendamento ocorre, a tarefa é armazenada em uma lista de espera
-
quando a tarefa atual termina, a próxima é executada (se houver)
-
após a completude das tarefas, o controle do sistema volta ao escalonador
Se a solução a ser implementada não tiver padrões rígidos de resposta temporal, a utilização de uma máquina de estados garante a facilidade no reuso e manutenção do código.
Supomos um programa que utilize um display, um teclado e uma porta serial. A porta serial responde aos comandos vindos pelo teclado ou pela serial. O display é atualizado periodicamente. A máquina de estados pode ser modelada da seguinte forma (figura retirada de [1]):
Uma implementação em C para esta FSM poderia ser:
#include <serial.h>
#include <keypad.h>
#include <display.h>
#define LE_TECLADO 0
#define LE_SERIAL 1
#define ESCREVE_SERIAL 2
// programa principal
void main(void)
{
kpInit();
displayInit();
serialInit();
while (1)
{
// atualiza display
displayUpdate();
switch(state)
case LE_TECLADO:
if (kpRead() != 0) // chegou comando
{
/* mais processamento */
state = ESCREVE_SERIAL;
}
else
{
state = LE_TECLADO;
}
break;
case LE_SERIAL:
if (serialRead() != 0) // chegou comando
{
state = ESCREVE_SERIAL;
}
else
{
state = LE_SERIAL;
}
break;
case ESCREVE_SERIAL:
/* processa comando e responde */
break;
default:
state=LE_TECLADO;
break;
}
Perceba que como a função de atualizar o display precisa ser executada intermitentemente entre um estado e outro, ela pode ser acomodada antes do switch-case.
Neste caso, as funções serão executadas até a completude antes de passar a vez. Uma característica desejável de um sistema embarcado é o determinismo. Podemos então configurar um período de tempo fixo para todas as tarefas ocorrerem. Este período obviamente precisa ser maior que o pior caso de tempo de execução.
// programa principal void main(void) { kpInit(); displayInit(); serialInit(); while (1) { // atualiza display displayUpdate(); timerInit(300); // liga contador que vira a cada 300 ms switch(state) { case LE_TECLADO: if (kpRead() != 0) // chegou comando { /* algum processamento */ state = ESCREVE_SERIAL; } else { state = LE_TECLADO; } break; case LE_SERIAL: if (serialRead() != 0) // chegou comando { state = ESCREVE_SERIAL; } else { state = LE_SERIAL; } break; case ESCREVE_SERIAL: /* processa comando e responde */ break; default: state=LE_TECLADO; break; } timerWait(); // espera o contador virar }
A desvantagem aqui é o tempo ocioso que o processador espera “somente” para padronizar as tarefas. Porém perceba que passamos de uma arquitetura cujo tempo de execução era difícil de prever para uma com tempo bem definido, e isto é um ganho enorme. O tempo livre aliás poderia ser utilizado para colocar o sistema em baixo consumo de energia, o acordando novamente com a interrupção do temporizador. Esta arquitetura é uma boa escolha para hardwares limitados em memória.
Escalonamento cooperativo com ponteiros para funções
Collins Walls no seu blog descreve o que diz ser um RTOS de uma linha, que basicamente utiliza um ponteiro para funções.
#define NTASKS 3
// pool de funções
void (*tasklist[NTASKS])() = {alpha, beta, gamma};
int taskcount;
void main()
{
while (1)
{
// dispatcher
for (taskcount=0; taskcount<NTASKS; taskcount++)
{
(*tasklist[taskcount])();
}
}
Apesar de simples, esta estrutura nos fornece muitos conceitos úteis. O uso de ponteiros para função já nos permite imaginar que as informações das tarefas a serem executada estarão armazenadas em algum espaço da memória, e o escalonador fica responsável por buscá-las e executá-las, sob alguma lógica de controle. Neste caso, é simplesmente a ordem do endereço das funções na matriz de ponteiros tasklist. Mas poderíamos adicionar mais critérios de escolha. Talvez um deadline temporal, informação de prioridade ou delay inicial.
Poderíamos garantir que todas as tarefas fossem executadas em um mesmo intervalo de tempo, com a mesma técnica aplicada na máquina de estados.
Poderíamos também criar uma pequena API:
// para adicionar tarefas
#define N_TASK 4 // numero de tarefas
typedef void(* ptrFunc)();
int pos;
void() addTask(ptrFunc newFunc)
{
if (pos < N_TASK-1) // verifica se ha espaço no vetor
{
tasklist[pos] = newFunc;
}
pos++; //incremente posicao
}
// para disparar o escalonador
void sch_loop(void)
{
int i;
while(1)
{
for(i=0;i<N_TASK-1;i++)
{
(*tasklist[i])();
}
}
}
// inicializa sch
void sch_init()
{
pos=0;
}
E aí nosso código principal seria:
void main();
{
schInit();
addTask(alpha);
addTask(beta);
addTask(gama);
schLoop();
}
Esta codificação parece ser desnecessária para executar três funções em fila, sem nenhum critério. O super-loop resolveria. Concordo, porém é o esqueleto de uma arquitetura que pode ser estendida para criar um scheduler mais robusto.
Executando processos uma única vez ou indefinidamente
No código apresentado as tarefas serão executadas na mesma ordem, ad infinitum. É interessante informarmos ao escalonador se a tarefa que acaba de retornar quer ou não ser executada ciclicamente. Para isso podemos criar códigos de retorno para as funções:
#define ONESHOT 0;
#define FAIL 1;
#define REPEAT 2;
Para executar processos uma única vez, precisamos poder removê-los do vetor de processos dinamicamente. Para isso, vamos transformar o vetor tasklist num buffer circular. As funções são inseridas no início do buffer e executadas em ordem. Caso uma função retorne REPEAT ela é inserida ao final do buffer. As tarefas são (re)agendadas dinamicamente.
Assim, precisamos alterar a função addTask para controlar o buffer tasklist de forma circular:
char addTask(ptrFunc newFunc)
{
if ((last+1)%(N_TASK+1) != first)
{
tasklist[last] = newFunc;
last=(last+1)%(N_TASK+1);
return 0;
}
else
{
return 1; //error
}
}
A função schLoop agora precisa checar pelo valor de retorno do processo que acabou de executar. Se ele for REPEAT, ele é armazenado ao final do buffer.
void schLoop()
{
while (1)
{
if (first != last) // tem algo
{
if ((*tasklist[first])() == REPEAT)
{
addTask(tasklist[first]); // reagenda
}
first=(fist+1)%N_TASK; // proximo processo
}
}
}
No próximo texto irei estender este escalonador para atender requisitos temporais de um sistema soft real-time.
O texto desta postagem é original. Os seguintes livros foram consultados para sua elaboração:
[1] Programação de sistemas embarcados, Rodrigo Almeida e outros.
[2] Real-Time Systems Development, Rob Williams
[3] Patterns for Time-Triggered Embedded Systems, Michael J. Pont