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