ARMEXE con ARM CORTEX-M3

ArmExe è un RTOS estremamente ridotto, occupa, infatti, non più di 1.5 Kb di memoria flash, ed è pensato e realizzato esclusivamente per ARM Cortex-M3.

Senza dubbio ArmExe è un kernel compatto  dotato  di caratteristiche di real-time in grado di sfruttare le diverse prerogative che offre la versione Cortex-M3 di ARM. ArmExe è stato realizzato specificatamente per Luminary Micro Stellaris ARM Cortex-M3. A questo riguardo, il kernel sfrutta diversi vantaggi architetturali del Cortex-M3, tra cui le nuove potenzialità del set di istruzioni di Thumb-2. ArmExe utilizza diversi accorgimenti per massimizzare l’efficienza di un sistema di questo tipo. Così, il kernel utilizza un sistema che tiene separati gli stack allocati per ogni task rispetto allo stack di sistema. In ArmExe, allo scopo di pesare il comportamento di un task, permette di utilizzare, per ogni task, una priorità: in questo modo si assegna un certo time budget ad un task. Al fine di garantire anche un uso efficiente delle risorse, ArmExe è in grado di supportare, oltre al nesting degli interrupt, anche l’assegnazione di un livello di privilegio ad ogni task. ArmExe è perfettamente integrato con l’ambiente di lavoro di Keil, ossia uVision. Questo è una caratteristica che apporta indubbi vantaggi per l’utilizzatore, ma, al contempo, ne rappresenta una decisa limitazione. uVision permette di configurare il kernel, in base alle singole necessità, per mezzo di un ambiente grafico: il uVision Configuration Wizard. Al contrario, per portare il kernel verso un altro ambiente di lavoro, anche se non rappresenta un grosso problema, è necessario apportare alcune modifiche. È opportuno ricordare che ArmExe è stato realizzato con l’evaluation board EKK-LM3S811 (figura 1) e, a questo proposito, nel package in distribuzione risultano presenti diversi esempi allo scopo di mostrare i suoi diversi impieghi.

Figura 1: piccola demo.

Figura 1: piccola demo.

ARCHITETTURA DI ARMEXE

ArmExe è un kernel estremamente ridotto in grado di offrire un’interessante varietà di servizi. Il kernel è stato realizzato alcuni anni fa per sfruttare le caratteristiche del Cortex-M3, in particolare la possibilità di supportare l’annidamento degli interrupt (NVIC), al fine di realizzare un modulo software con interessanti prerogative. La figura 5 mostra un diagramma temporale dell’ interrupt nesting. Secondo l’autore del microkernel, ArmExe è uno dei pochi RTOS in grado di sfruttare la nidificazione degli interrupt attraverso una sola interfaccia. Il Cortex-M3 di Arm è un processore che ha introdotto, tra l’altro, un’ottimizzazione spinta per gli interrupt annidati. La figura 2 mostra i diversi stati di un task all’interno di ArmExe, mentre la figura 3 pone in evidenza la sequenza di startup del kernel.

Figura 2: stati per ArmExe.

Figura 2: stati per ArmExe.

 

Figura 3: startup di ArmExe.

Figura 3: startup di ArmExe.

Inoltre, a questo proposito, il listato 1 mostra i diversi moduli software utilizzati a questo scopo: vediamo che, al termine della procedura di start-up, ArmExe invoca la funzione start_idle_task, utilizzata per l’idle loop.

// initializes the ARM executive
// this routine executes in kernel mode with exclusive access to everything.
// At the exit, the system will be in thread mode running the idle task.
// the current stack will be the main stack. A new stack will be created for the idle task.
__declspec(noreturn) void ArmExeInit(uint systick_period)
{
// clear some global variables in case of reset
#if ARMEXE_KEIL_LIBRARY_SAFE == 1
kernel_ok = 0;
#endif
armexe_task_id = TASKS_ID_START;
task_running = NULL;
task_current_id = 0;
ISR_counter = 0;
systick_counter = 0;
// initialize the TCB lists
InitTCBLists();
// initialize idle task
initialize_idle_task();
// initialize interrupt table
InitIntVectors();
// initialize the interrupt priorities
InitIntPriorities();
// initialize systick interrupt
initialize_systick(systick_period);
// go to thread mode and change stack to idle_task stack
start_idle_task();
}
Listato 1 – Inizializzazione ArmExe

Può essere opportuno ricordare che l’uso di NVIC permette di minimizzare anche la complessità del kernel utilizzando un’architettura dello stack pointer a banchi. L’approccio utilizzato in ArmExe è stato quello di preferire un’architettura con due stack pointer: uno per le operazioni privilegiate, definito anche come handler mode, e l’altro riservato all’utente, o task mode. Per mezzo dell’accesso privilegiato della CPU, e disponendo di due stack pointer, il contesto ne risulta fortemente migliorato con prestazioni davvero interessanti. La parte del controllo degli accessi del kernel ne esce potenziata; infatti, ArmExe è in grado di controllare il flusso di esecuzione delle applicazioni impedendo istruzioni che possono, potenzialmente, arrecare danno alla CPU. Ad esempio, con ArmExe non è possibile disabilitare direttamente gli interrupt, o cambiare l’allocazioni delle risorse protette, quando siamo in user mode. In effetti, attraverso opportune chiamate di sistema, SVC, è possibile interagire direttamente con le risorse. In questo caso, con SVC, il codice messo in esecuzione in modalità utente, è possibile cambiare i diritti di esecuzione per passare alla modalità privilegiata. Questo perché l’istruzione SVC è, in pratica, un interrupt software in grado modificare il flusso di esecuzione, in maniera trasparente, da user a supervisor (handler mode) come qualsiasi gestore di interrupt. In questo caso, il sistema permette una commutazione dello stack pointer verso il kernel SP. Quasi tutti i compilatori, ad esempio GCC o Keil, permettono di invocare l’istruzione SVC direttamente in C. In effetti, chiamate di questo tipo possono essere facilmente inserite in una porzione di codice. Ricorrendo ad uno schema di questo tipo, la protezione del kernel risulta maggiore. Nella nostra realizzazione è utilizzato un sistema basato su priorità. La priorità stessa può essere modificata dall’utente, ma l’uso accurato di un sistema di questo tipo è in grado di garantire i diritti d’accesso per un interrupt nesting, oltre a garantire la protezione della condivisione, attraverso il controllo sugli accessi, delle risorse. Tipicamente, un RTOS fornisce un servizio per disabilitare gli interrupt quando si vuole accedere a delle risorse condivise. Il design di ArmExe ha ha privilegiato la priorità dei task; così, in ArmExe non ci sono sezioni di codice in cui diventa necessario disabilitare gli interrupt, ma le sezioni critiche sono gestite direttamente con le priorità. Quando si vuole accedere ad una risorsa critica occorre aumentare la priorità del chiamante senza per questo compromettere la Task Scheduler Table: certamente, il codice non deve provocare il blocco del kernel. La funzione di SVC, in ArmExe è l’unico modo per chiamare il kernel, ed dispone di una priorità maggiore. Quando il kernel è in esecuzione nessuno può sospenderlo. A questo riguardo, al fine di evitare commutazioni tra user/supervisor il sistema alza la priorità dell’interrupt handler kernel; al contrario, si abbassa la priorità solo quando il gestore dello user interrupt è invocato. In AtmExe è anche possibile disabilitare, o abilitare, la preemtpion del kernel intervenendo sul file config.h. Abilitando la preemption del kernel, i task con la stessa priorità sono sottoposti al time slice per partizionarne l’esecuzione. ArmExe garantisce anche la gestione delle risorse condivise attraverso l’uso di semafori binari, mutexes o eventi.

UTILIZZARE ARMEXE

Utilizzare ArmExe non richiede una conoscenza approfondita di un kernel; in realtà, come tutti i microkernel, anche in ArmExe sono presenti una discreta varietà di primitive in grado di gestire la propria applicazione. Come si è affermato, anche in ArmExe è presente un file di configurazione allo scopo di definire i vari servizi del kernel allo scopo di intercalarlo in una board con un’applicazione reale. È possibile definire la quantità di task utilizzati nel sistema con il task idle con le relative dimensioni dei stack. Non solo, attraverso questo file di configurazione è possible impostare lo stack per I diversi task utente, l’abilitazione o meno della pre-emption, il periodo del system tick e la frequenza del clock. La figura 4 mostra il file Config.h utilizzato per modificare i singoli parametri di ArmExe attraverso il configuration Wizard, mentre il listato 2 mostra un estratto del file.

// <o> Maximum Number of Tasks <2-255>
// <i> Maximum number of tasks for ArmExe (2-255).
The idle task uses one of the slots
#define ARMEXE_MAX_TASKS 6
// <o> Idle Task stack size <256-65535:32>
#define IDLE_TASK_STACK_SIZE 384
// <o> User default stack (ARMEXE_STACK_DEFAULT) <256-65535:32>
// <i> This is the recommended stack size constant to be put in
// ArmExeCreateTask
#define ARMEXE_STACK_DEFAULT 384
// </h>
// <h> RTOS Parameters
// <o> Preemption <0=> Disabled <1=> Enabled
// <i> Enable or disable RTOS pre-emption
#define ARMEXE_ENABLE_PREEMPTION 1
// <o> Systick value in microseconds (ARMEXE_SYSTICK_TIME)
// <50-1000000:50>
/ <i> It have sense only is the kernel is initialized as
ArmExeInit(ARMEXE_SYSTICK_TIME)
#define ARMEXE_SYSTICK_TIME 200
// <o> CPU clock frequency (in Hz) (CLOCK_FREQUENCY)
// <1-50000000:1000000>
// <i> This constant is for the TICKS() macro
#define CLOCK_FREQUENCY 50000000
// <o> Enable uVision C Library thread safe code <0=> Disabled
// <1=> Enabled
#define ARMEXE_KEIL_LIBRARY_SAFE 0
// <o> Number of mutexes required by the C library <1-255:1>
// <i> This is used only if the library thread safe code flag above
// is enabled
#define LIBRARY_MUTEX_CNT 4
// </h>
Listato 2 - Config.h

Inoltre, il programma tore, per utilizzare correttamente il kernel, ha la necessità di definire alcune funzioni di weak: in sostanza, è necessario mettere a punto due moduli software utilizzati in precisi punto di interconnessione del codice. La prima funzione, signal_error, deve essere invocata quando il kernel trova un errore classificato come fatale. In questo caso occorre inviare un messaggio appropriato e, successivamente, si entra in modalità di debugging. L’altra funzione è Init_Task, un modulo utilizzato dal kernel per creare il task iniziale; il task ha una priorità più alta di qualsiasi altro task presente nel sistema. Inutile dire che queste due funzioni devono essere scritte direttamente dall’utilizzatore visto che sono funzioni di weak. Nella distribuzione è presente poi il file ArmExe.h. Il file contiene diverse macro e costanti: Tick_Time, Tick_MS e Tick_US.

Figura 4: configuration Wizard.

Figura 4: configuration Wizard.

 

Figura 5: NVIC.

Figura 5: NVIC.

LAVORARE CON ARMEXE

Compilare il kernel non è di per sé molto complicato. Occorre per prima cosa inserire il file ArmExe.h nella funzione main(), invocare la funzione ArmExeInit(TICK_TIME) e definire init_task al fine di creare e definire task nel nostro sistema. È consentito definire la funzione signal_error() al fine di catturare tutti i messaggi di tipo fatale che potrebbero verificarsi nel sistema. ArmExe è stato realizzato in ambiente uVision della Keil e per questa ragione è perfettamente integrato nella sua interfaccia grafica. Ad ogni modo è anche possibile utilizzare un ambiente di lavoro differente. Così, in uVision dobbiamo determinare il parametro “—entry Reset_Handler” fissato attraverso le opzioni del linker. Inoltre, è necessario anche inserire, nel nostro progetto, i file di ArmExe: Startup.s, SVC.s, IntHandler.s e ArmExe.c. Ricordiamo che il progetto è stato realizzato con Luminary Micro. Successivamente, terminata la fase di implementazione della nostra applicazione, siamo pronti a provare il nostro lavoro.

 

 

2 Commenti

  1. Giordana Francesca Brescia Giordana Francesca Brescia 24 febbraio 2019
  2. Stefano Lovati Stefano Lovati 25 febbraio 2019

Scrivi un commento

EOS-Academy