L’RTOS di Micrium

Micrium nasce per volontà di Jean Labrosse come kernel di tipo real-time ed è stato pubblicato per la prima volta sulla rivista Embedded System Programming con il nome di uC/OS. Il prodotto, nel corso del tempo, è diventato un vero punto di riferimento per le soluzioni di questo tipo.

Micrium è l’altro nome di uC/OS-II, l’evoluzione del progetto originario uC/OS, ed è attualmente certificato per usi avionici dal FAA da diversi anni. In effetti, con la denominazione uC/OS-II si vuole identificare la versione 2 del Micro-Controller Operating Systems, ovvero la variante commerciale del prodotto. Secondo le indicazioni del suo creatore, Jean Labrosse, Micrium è un real-time kernel che occupa una quantità di memoria estremamente ridotta ed è facilmente scalabile. Il sistema occupa qualcosa come 20 KB di memoria, di cui grosso modo, meno di 3 KB veramente indispensabili, con tutte le funzionalità di un tradizionale kernel (figura 1).

Il kernel di Labrosse consta, più o meno, di 5000 linee di codice scritte in Ansi C e solo una porzione estremamente ridotta è scritta in linguaggio Assembly allo scopo di assicurarne la portabilità. Il kernel di Micrium rientra tra il software open source, anche se non può essere utilizzato in applicazioni di tipo commerciali se non attraverso una regolare licenza commerciale.

Figura 1: le possibili applicazioni.

Figura 1: le possibili applicazioni.

CARATTERISTICE DI MICRIUM

Il Micrium, ovvero uC/OS-II, può gestire fino a 64 task con altrettanti livelli  di priorità con uno schedulatore basato su pre-rilascio di tipo priority-driven con 256 livelli di nidificazione delle ISR. Il kernel di Labrosse è anche deterministico, tanto da assicurare il suo utilizzo in ambienti real-time di tipo safety-critical, ed è royalty-free. Rispetto alla versione originale, pubblicata su C Users Journal, l’ultimo RTOS si discosta radicalmente da quella versione grazie alla presenza di diversi moduli, o librerie, software; in effetti, Labrosse propone il suo Micrium con un file system, µC/FS, network protocol, µC/TCP-IP, HTTP Server, uC/HTTPs, ed è anche possibile sfruttare le GUI, Graphics User Interface, in un sistema embedded. Non solo, è anche possibile sfruttare diverse librerie che gestiscono l’interfaccia USB o Can bus: a questo riguardo la figura 2 pone in risalto i diversi moduli disponibili.

Figura 2: moduli disponibili.

Figura 2: moduli disponibili.

Le ultime versioni del kernel assicurano il pieno supporto alla MMU e MPU allo scopo di garantire i necessari requisiti di safety per l’intero sistema. Queste nuove funzionalità non fanno parte della distribuzione standard del kernel, ma sono moduli acquistabili separatamente che assicurano la gestione, in modo protetto, della memoria e delle risorse di scheda. Grazie a queste nuove possibilità, il kernel di Labrosse si candida per applicazioni safety molto critiche quali applicazioni di tipo avioniche e il segmento automotive. Con le nuove librerie di casa Micrium, si garantisce un controllo dinamico per permettere la protezione, in tempo e spazio, delle diverse applicazioni che risultano in esecuzione in modo asincrono. Il modulo MPU, Memory Protection Unit, di Micrium assicura la necessaria protezione ai task per evitare usi non autorizzati della memoria. Il tutto utilizza la Memory Management Unit del processore: ogni applicazione, composta a sua volta da un’altra istanza del sistema operativo con relativa applicazione, presente in una determinata partizione, è eseguita dal processore isolando, nel contempo, le eventuali failure. Le failure appartengono ad ogni singola partizione senza per questo pregiudicare la corretta esecuzione sulle altre: ogni partizione è controllata in modo asincrono con precisi meccanismi di interazioni. Una MPU, al contrario, permette di gestire in modo trasparente e sicuro ogni processo inserito in un’applicazione impostando precisi criteri di protezione e di accesso, ovvero Micrium garantisce la comunicazione tra i diversi thread attraverso precise regole di utilizzo. Tutto questo rende flessibile ogni applicazione e permette di facilitare l’integrazione di moduli software anche di terze parti. Le nuove funzionalità si integrano perfettamente nelle varie linee guida attraverso l’opportuno processo di certificazione: FAA DO178B per il settore avionica, 510(k) per il segmento medico e l’IEC 61508.

I TASK IN MICRIUM

Il controllo delle risorse del processore e le funzionalità dell’applicazione, sono svolte attraverso le unità di esecuzione presenti nel nostro programma. Queste unità sono rappresentate dai task, ovvero moduli di compilazione, di solito rappresentate come una funzione, che sono trattate in modo particolare tanto da essere utilizzate come moduli totalmente asincrone rispetto agli altri presenti nel programma. In Micrium il controllo del processore è assegnato al task che si trova in cima alla coda dei processi, task in stato di ready, con una priorità maggiore. Secondo il realtime kernel di Labrosse, i task possono assumere diversi stati, ovvero waiting, dormant, ready, running e interrupted. Per eliminare un task dal sistema, è necessario invocare la funzione OSTaskDel con l’identificatore del task, ovvero la sua priorità. È possibile, una volta cancellato il task, ripristinarlo attraverso la funzione OSTaskCreate: in questo modo si rende possibile la creazione dinamica dei task. Un task è in stato waiting quando aspetta l’occorrenza di un evento, mentre quando un task si trova nello stato di ready, allora è candidato all’esecuzione, ovvero dispone di un livello di priorità inferiore a quello del task attualmente in stato run. Un task si trova nello stato di run quando dispone del controllo della CPU, mentre con interrupted il task si trova in una condizione di sospensione poiché è in corso la gestione di un evento asincrono. A questo riguardo la figura 3 mostra i diversi stati possibili con le regole per il passaggio di stato, mentre la tabella 1 pone in evidenza i diversi stati di un task.

Figura 3: stati in Micrium

Figura 3: stati in Micrium

 

Tabella 1: stati presenti.

Tabella 1: stati presenti.

VERSO ALTRE ARCHITETTURE

Il kernel C/OS-II può essere adattato per diverse piattaforme hardware, ognuna realizzata per rispondere ad ogni singola esigenza. In effetti, Micrium è stato, ma lo è tuttora, utilizzato sulle diverse soluzioni architetturali basate su ARM, in particolare la versione Cortex. La figura 4 mostra un diagramma a blocchi che pone in evidenza le relazioni tra l’applicazione, C/OS-II, il codice utilizzato per il porting e il BSP (Board Support Package).

Figura 4: struttura del codice.

Figura 4: struttura del codice.

Per portare Micrium su altri ambienti target, il procedimento è abbastanza semplice tanto da non richiedere sforzi particolari, ma, al contrario, è necessario solo eseguire diversi passi e intervenire sui file presenti nella distribuzione identificati come OS_CPU.H, OS_CPU_C.C e OS_CPU_A.ASM. Prima di procedere al porting è necessario verificare le condizioni presenti e i requisiti del processore. Per prima cosa occorre utilizzare un compilatore in grado di generare codice rientrante, ovvero si deve avere la possibilità di invocare una porzione di codice, funzione, più volte senza per questo pregiudicare il normale funzionamento. In realtà, ad onor del vero, la maggior parte dei compilatori disponibili in commercio, così come le distribuzioni open source, sono in grado di generare codice di questo tipo: un codice rientrante è una porzione di unità di compilazione condivisa ed eseguita tra più processi ed è la base della programmazione di tipo multitasking. È anche necessario avere l’accuratezza di utilizzare, nel limite del possibile, il linguaggio di programmazione C per scrivere le funzioni di abilitazione o disabilitazione delle interruzioni: nel processore ARM questa particolare funzionalità può essere svolta attraverso il registro CPSR. Il registro CPSR dispone di un bit che, opportunamente impostato, permette di abilitare o disabilitare le interruzioni. In qualsiasi sistema di tipo realtime che si rispetti si deve avere la possibilità di gestire le interruzioni e un timer di sistema per sovrintendere alle politiche di schedulazione e gli eventi asincroni. Non solo, particolare attenzione deve essere posta nella scelta del tipo di processore. I processori da 8 bit dispongono solo di 10 linee di indirizzamento e, ne consegue, in questo caso diventa oltremodo difficile pensare di inserire kernel uC/OS-II. Infine, siccome uC/OS-II utilizza lo stack come area di lavoro, è necessario utilizzare un processore con uno stack pointer e dei registri orientati alla sua gestione: a questo riguardo il processore ARM offre le istruzioni STMFD e LDMFD che permettono di inserire e togliere dati dallo stack. In Micrium tutte le definizioni e le configurazioni del nostro sistema devono risiedere nel file OS_CPU.h, ovvero il file deve contenere le indicazioni sui tipi utilizzati dal compilatore. In Micrium sono banditi i data type come unsigned int, ma è necessario specificare in modo diretto così:

unsigned int diventa INT16U in un processore ARM su 16-bit

Nel file OS_CPU.h sono anche definiti le eventuali macro per gestire l’abilitazione e la disabilitazione degli interrupt del processore con OS_ENTER_CRITICAL() e OS_EXIT_CRITICAL().

L’utilizzatore deve anche definire l’interrupt vector utilizzato dal cambio di contesto, OS_TASK_SW, e la direzione dello stack, OS_STK_GROWTH: il vettore utilizzato da OS_TASK_SW deve invocare la funzione OSCtxSw(). Come secondo passaggio è necessario scrivere una serie di routine in Assembly in grado di gestire alcune operazioni non particolarmente indicate nel linguaggio C quali OS_CPU_SR_Save(), OS_CPU_SR_Restore(), OSStartHighRdy(), OSCtxSw() e OSIntCtxSw(). La funzione OSStartHighRdy() è invocata dalla funzione OSStart() con l’obiettivo di mettere in esecuzione il task nello stato di ready con la più alta priorità. Il modulo OSStart() è responsabile del setup dell’ambiente descritto nel manuale uC/OS-II per mezzo di un opportuno pseudocodice. Lo pseudo-codice deve essere convertito in linguaggio assembly. OSStartHighRdy() carica lo stack pointer della CPU con il top-of-stack pointer del task con più alta priorità, deve ripristinare i registri del processore ed eseguire un ritorno dall’interrupt. Si nota che OSStartHighRdy() non ritorna mai a OSStart(). Il listato 1 mostra un breve esempio di questa routine.

OSStartHighRdy:
BL OSTaskSwHook
MOV R0,#1
LDR R1,=OSRunning
STRB R0,[R1]
LDR r4, addr_OSTCBCur @ Get current task TCB address
LDR r5, addr_OSTCBHighRdy @ Get highest priority task TCB address
LDR r5, [r5] @ get stack pointer
LDR sp, [r5] @ switch to the new stack
STR r5, [r4] @ set new current task TCB address
LDMFD sp!, {r4} @ YYY
MSR SPSR_cxsf, r4
LDMFD sp!, {r4} @ get new state from top of the stack
MSR CPSR_cxsf, r4 @ CPSR should be SVC32Mode
LDMFD sp!, {r0-r12, lr, pc } @ start the new task
Listato 1 – OSStartHighRdy

La funzione OSCtxSw() è responsabile del cambio di contesto ed è scritta generalmente in Assembly perché la maggior parte dei compilatori non sono in grado di manipolare i registri della CPU in modo diretto. La funzione OSCtxSw() si deve occupare di inserire il contenuto dei registri del task corrente sullo stack, deve cambiare lo stack pointer con il nuovo valore dello stack, rimettere a posto i registri del nuovo task e ritornare dall’interrupt: il listato 2 mostra un piccolo esempio.

OS_TASK_SW:
STMFD sp!, {lr} save pc
STMFD sp!, {lr} save lr
STMFD sp!, {r0-r12} save register file and ret address
MRS r4, CPSR
STMFD sp!, {r4} save current PSR
MRS r4, SPSR
STMFD sp!, {r4} save SPSR
# OSPrioCur = OSPrioHighRdy
LDR r4, addr_OSPrioCur
LDR r5, addr_OSPrioHighRdy
LDRB r6, [r5]
STRB r6, [r4]
Get current task TCB address
LDR r4, addr_OSTCBCur
LDR r5, [r4]
STR sp, [r5] store sp in preempted tasks’s TCB
# Get highest priority task TCB address
LDR r6, addr_OSTCBHighRdy
LDR r6, [r6]
LDR sp, [r6] get new task’s stack pointer
# OSTCBCur = OSTCBHighRdy
STR r6, [r4] set new current task TCB address
LDMFD sp!, {r4}
MSR SPSR_cxsf, r4
LDMFD sp!, {r4}
MSR CPSR_cxsf, r4
LDMFD sp!, {r0-r12, lr, pc}
Listato 2 – OS_Task_Sw

La funzione è chiamata da OS_TASK_SW attraverso l’OSSched(). La funzione OSIntCtxSw() è chiamata da OSIntExit() allo scopo di permettere un cambio di contesto da un’ISR. Siccome la funzione è chiamata da un’ISR si assume che tutti i registri del processore siano già stati salvati sull’area di stack della ISR. Le funzioni OSCtxSw() e OSIntCtxSw() si occupano di gestire il cambio di contesto tra task. A questo proposito la funzione OSIntCtxSw() è responsabile di preservare i dati del corrente task e di fare il recovery del valore dei registri nel task successivo. Infine, la funzione OSTickISR() si occupa del tick di sistema generato dal timer hardware. A questo punto è necessario definire alcune funzioni C, anche se tipicamente è richiesto la sola implementazione della OSTaskStkInit(), ovvero di OSInitHookBegin(), OSInitHookEnd(), OSTaskCreateHook(), OSTaskDelHook(), OSTaskIdleHook(), OSTaskStatHook(), OSTaskSwHook(), OSTCBInitHook() e OSTimeTickHook(). Queste funzioni permettono di estendere alcune funzionalità del kernel, anche se nella distribuzione le funzioni sono vuote. Micrium richiede la sola presenza della funzione OSTaskStkInit() ed è utilizzata per creare un task, il listato 3 e la figura 5 pongono in evidenza la sua possibile gestione.

Figura 5: Stack Frame.

Figura 5: Stack Frame.

 

OS_STK *OSTaskStkInit (void (*task)(void *pd),
void *p_arg, OS_STK *ptos, INT16U opt)
{
OS_STK *stk;
(void)opt; /* ‘opt’ is not used, prevent warning */
stk = ptos; /* Load stack pointer */
/* Registers stacked as if saved on exception */
*(stk) = (INT32U)0x01000000L; /* xPSR */
*(—stk) = (INT32U)task; /* Entry Point */
*(—stk) = (INT32U)0xFFFFFFFEL; /* R14 (LR) */
*(—stk) = (INT32U)0x12121212L; /* R12 */
*(—stk) = (INT32U)0x03030303L; /* R3 */
*(—stk) = (INT32U)0x02020202L; /* R2 */
*(—stk) = (INT32U)0x01010101L; /* R1 */
*(—stk) = (INT32U)p_arg; /* R0 : argument */
/* Remaining registers saved on process stack */
*(—stk) = (INT32U)0x11111111L; /* R11 */
*(—stk) = (INT32U)0x10101010L; /* R10 */
*(—stk) = (INT32U)0x09090909L; /* R9 */
*(—stk) = (INT32U)0x08080808L; /* R8 */
*(—stk) = (INT32U)0x07070707L; /* R7 */
*(—stk) = (INT32U)0x06060606L; /* R6 */
*(—stk) = (INT32U)0x05050505L; /* R5 */
*(—stk) = (INT32U)0x04040404L; /* R4 */
return (stk);
}
Listato 3 – OSTaskStkInit

Dalla versione 2.62 del kernel, Labrosse ha aggiunto un nuovo file utile in fase di debug. In effetti, il file permette la costruzione di un Kernel Aware debugger allo scopo di estrarre le informazioni utili alla sessione di debug per la parte RTOS. Nello specifico il nuovo file inserito, OS_DBG.C, contiene delle costanti che possono aiutare l’utilizzatore nella sua sessione di debug, il listato 4, a questo proposito, mostra alcune definizioni utilizzate.

OS_COMPILER_OPT INT16U const OSDebugEn = OS_DEBUG_EN; /* Debug constants are defined below */
#if OS_DEBUG_EN > 0
OS_COMPILER_OPT INT32U const OSEndiannessTest = 0x12345678L; /* Variable to test CPU endianness */
OS_COMPILER_OPT INT16U const OSEventMax = OS_MAX_EVENTS; /* Number of event control blocks */
OS_COMPILER_OPT INT16U const OSEventNameSize = OS_EVENT_NAME_SIZE; /* Size (in bytes) of event names */
#if (OS_EVENT_EN > 0) && (OS_MAX_EVENTS > 0)
OS_COMPILER_OPT INT16U const OSEventEn = OS_EVENT_EN;
OS_COMPILER_OPT INT16U const OSEventSize = sizeof(OS_EVENT); /* Size in Bytes of OS_EVENT */
OS_COMPILER_OPT INT16U const OSEventTblSize = sizeof(OSEventTbl);/* Size of OSEventTbl[] in bytes */
#else
OS_COMPILER_OPT INT16U const OSEventEn = 0;
OS_COMPILER_OPT INT16U const OSEventSize = 0;
OS_COMPILER_OPT INT16U const OSEventTblSize = 0;
#endif
OS_COMPILER_OPT INT16U const OSFlagEn = OS_FLAG_EN;
OS_COMPILER_OPT INT16U const OSFlagGrpSize = sizeof(OS_FLAG_GRP);/* Size in Bytes of OS_FLAG_GRP */
OS_COMPILER_OPT INT16U const OSFlagNodeSize = sizeof(OS_FLAG_NODE);/* Size in Bytes of OS_FLAG_NODE */
OS_COMPILER_OPT INT16U const OSFlagWidth = sizeof(OS_FLAGS); /* Width (in bytes) of OS_FLAGS */
OS_COMPILER_OPT INT16U const OSFlagGrpSize = 0;
OS_COMPILER_OPT INT16U const OSFlagNodeSize = 0;
Listato 4 – estratto informazioni presenti in OS_DBG.C

Scrivi un commento

Seguici anche sul tuo Social Network preferito!

Send this to a friend