Fandom

Verdiana Wiki

MipOS

647pagine in
questa wiki
Crea nuova pagina
Commenti0 Condividi
No Title

No Title

No information

mipOS ovvero Multum In Parvo Operating System è un RTOS (real-time operating system) progettato per microcontrollori con limitate risorse (soprattutto in termini di memoria).

Il tipico impiego è nell'ambito di applicazioni per dispositivi di calcolo denominati system on chip o in breve SoC. I SoC sono largamente impiegati per la realizzazione di apparecchi elettronici, sia per il mercato dell'elettronica di consumo, che quello industriale.

Il firmware per questi sistemi è normalmente concepito come un applicativo monolitico (senza un sistema operativo vero e proprio) che esegue codice normalmente governato da un loop principale e da una serie di routine che implementano delle macchine a stati. La porzione di codice che gestisce gli aspetti strutturali, quali i contesti di esecuzione, la sincronizzazione e la gestione dell'hardware e delle risorse, si confonde con quello pertinente la parte puramente applicativa.

Un sistema operativo pensato per le applicazioni embedded, che risponda a precisi requisiti di compattezza, consente la realizzazione di firmware più robusto e strutturato. Ne migliora la modularità, la portabilità e la resilienza. mipOS è stato progettato e scritto per rispondere a requisiti di “leggerezza” e “completezza”, sintetizzati nella sua denominazione “multum in parvo” (molto in poco).

mipOS nasce come un esercizio “accademico” fatto nella stesura dell'articolo “Realizzare un sistema multitasking in C” [1] pubblicato su Computer Programming N.149 - Settembre 2005 dall'autore A. Calderone.

Quell'implementazione embrionale è stata ampliata e migliorata nel tempo, rendendolo adatto alle applicazioni reali, soprattutto nell'ambito dei sistemi di controllo. La sua evoluzione trae linfa dalle esigenze delle applicazioni reali, sulle quali viene continuamente testato e sviluppato.


La portabilità Modifica

mipOS è stato concepito per girare su qualunque sistema a microcontrollore che abbia caratteristiche minime di pochi KB di RAM / ROM. La portabilità di mipOS è garantita dal fatto che è quasi interamente scritto in linguaggio C. Essendo distribuito nella forma di file sorgenti, può essere “profilato” sulla piattaforma sulla quale è destinato a funzionare. Le risorse utilizzate sono quindi le sole necessarie all'impiego specifico.

Il supporto nativo è garantito per i microcontrollori della famiglia ST7 e compatibili, e per i dispositivi che usano un core basato su ARM Cortex M3 (come per esempio la famiglia di microcontrollori STM32). Recentemente è stato aggiunto il supporto per la famiglia di microcontrollori STM8.

mipOS supporta la piattaforma basata su microprocessori Intel x86 e compatibili. Inoltre può girare anche in processi user space in ambiente Windows oppure Linux, supportando di fatto la simulazione e il debugging del firmware su questi sistemi, ovvero avvantaggiandosi dell'ausilio di strumenti di debugging e analisi tipici degli ambienti di sviluppo più avanzati; sono supportati IAR Embedded Workbench, Microsoft Visual Studio, Metrowerks CodeWarrior, GNU Compiler Collection (GCC), Cosmic C.

L'architettura Modifica

mipOS ha un'architettura di tipo micro-kernel. Nello spazio di nucleo sono implementati lo scheduler e il dispatcher, che assolvono il compito di gestire il ciclo di vita dei task. Fa parte del micro-kernel anche l'implementazione dei mutex, della gestione degli eventi, delle code di messaggi e dei timers.

Il sistema supporta moduli opzionali (denominati mipos-optmod) che implementano file-system, memory manager, driver per la comunicazione, console dei comandi, ecc. Questi moduli sono esterni al micro-kernel e girano a propria volta in task, pertanto detti, di sistema

Tra i moduli opzionali disponibili sono: mipOS ram disk; mipOS file system; mipOS fast-stdio; mipOS memory manager; mipOS CAN bus driver; mipOS RS232 driver (console); mipOS USB (usb driver) mipOS Command Line Interface (interprete dei comandi)

mipOS adotta due diverse politiche di scheduling e di conseguenza due tipi di task: lo scheduler di tipo cooperative con priorità, dove i task in esecuzione si sospendono cedendo volontariamente il controllo al sistema (questi task sono denominati non real time o non-RT) e uno scheduler per i task real-time (o RT), che possono invece interrompere (preemption) i task non-RT.


Licenza d'uso Modifica

mipOS è gratuito per qualunque applicazione che abbia scopo didattico, pacifico e non commerciale. Qualunque altro utilizzo deve essere espressamente concordato con l'autore.


mipOS internals: il micro-kernel Modifica

Il cuore di mipOS è il suo micro-kernel. Il codice del micro-kernel implementa lo scheduler e il dispatcher. Lo scheduler ha come compito quello di gestire il ciclo di vita dei task e garantirne la corretta sincronizzazione. Lo stato di un task è scandito da una macchina a stati finiti alimentata dagli eventi generati dalla chiamate di sistema (internamente denominate signal).

Quando lo scheduler seleziona il task che è candidato per l'esecuzione, entra in gioco il dispatcher che effettua lo scambio di contesto, ovvero manda in esecuzione o riprende l'esecuzione sospesa del task selezionato. mipOS supporta due tipologie di task che sono gestite da due distinti meccanismi di scheduling e dispatching: il primo “cooperative” non real-time (non-RT), pensato per attività di sistema che non debbano soddisfare requisiti di temporali stringenti; il secondo “preemptive” per task di tipo real-time (RT), che possono interrompere (preemption) i task convenzionali (non-RT). Essi sono tipicamente chiamati a svolgere attività legate ad eventi critici, per i quali il requisito fondamentale è la reattività. Uno dei compiti tipici di un task RT è completare il lavoro iniziato nel contesto delle interrupt service routine (ISR). Del set di syscall messe a disposizione da mipOS, solo un sottoinsieme è utilizzabile da entrambi i contesti (task RT / non-RT). Alcune chiamate bloccanti per un task non-RT non possono essere invocate all'interno di un task RT. Inoltre questo deve garantire di svolgere il proprio compito in un tempo prefissato a priori, affinché sia preservata la stabilità dell'intero sistema. Un meccanismo di watchdog consente di rilevare e reagire alla violazione di questo requisito.


I task e la loro rappresentazione Modifica

I task sono rappresentati, all’interno del micro-kernel, da una struttura denotata col nome di task descriptor o in breve TD.

Un TD si compone per difetto dei seguenti attributi: - l'entry point della routine di esecuzione (il puntatore alla funzione di start-up del task), e i relativi parametri di start-up, passati come argomento dal dispatcher alla prima esecuzione;

- la struttura utilizzata per salvare lo stato della CPU quando il task viene sospeso in attesa di un evento;

- lo stato della FSM associato al task: (ready, running, ecc...);

- il puntatore allo stack (privato) del task;

- i campi per la gestione degli eventi (signal);

- i campi usati per la gestione degli eventi nel dominio del tempo (il contatore dei tick di sistema e l'RTC);

- altri campi utilizzabili dallo scheduler per la gestione delle risorse associate al task.

La dimensione della tabella dei TD, è fissata in compile time e dipende dal numero massimo di task concorrenti istanziabili nel sistema. La prima entry di questa tabella (con indice 0) individua uno pseudo-task detto di idle. A questa entry in realtà è associata una routine eseguita in caso di "inattività" del sistema (nessun task in esecuzione).

L'utente può registrare una propria callback function, invocata dal sistema nel contesto di esecuzione della routine idle che coincide con quello del micro-kernel (lo stack è quello usato da scheduler e dispatcher).


Lo stato dei task Modifica

La tabella dei TD è la struttura principale utilizzata per le attività di scheduling. Questa scelta deriva dalla necessità di minimizzare l'uso di risorse (per esempio creando strutture dinamiche come alberi o liste), ed è giustificato dall'utilizzo limitato del numero di task in un tipico sistema embedded.

Lo scheduler processa ciascuna entry della tabella dei TD, applicando a quelli "validi" azioni scandite da una macchina a stati finiti, il cui schema (semplificato) è mostrato di seguito:

NOT_READY -- create --> READY

ZOMBIE -- restart -- > READY

READY -- run --> RUNNING

RUNNING -- suspend --> SUPENDEND

SUSPENDED -- resume --> READY

RUNNING -- terminate --> ZOMBIE

'ANY STATE' -- delete --> NOT_READY

Un task in un qualunque istante può trovarsi in uno dei seguenti stati: READY, SUSPENDED, RUNNING, ZOMBIE e NOT_READY.

READY è lo stato di un task pronto all’esecuzione. È applicato dallo scheduler un criterio di priorità per il quale se più task sono contemporaneamente nello stato READY, viene schedulato quello a priorità più alta. Se più task a più alta priorità hanno priorità identica, allora viene schedulato il task che attende da più "tempo" la CPU (starvation avoidance).

SUSPENDED è lo stato di un task che attende un evento o signal (i signal sono essenzialmente dei flag del TD, usati per marcare il verificarsi degli eventi). La sospensione di un task consiste nel salvare lo stato della CPU in un campo del TD associato al task correntemente in esecuzione. Un task si sospende invocando una primitiva che causa sotto certe condizioni (per esempio il locking di un mutex già "lockato") la cessione del controllo allo scheduler. Nel seguito chiameremo queste primitive (potenzialmente) "bloccanti".

RUNNING è lo stato di un task che è in esecuzione. Lo scheduler ottiene il controllo della CPU solo se il task lo cede sospendendosi oppure nel caso un task-RT debba essere processato (preemption). Non è possibile che più task risultino in questo stato contemporaneamente.

ZOMBIE è lo stato di un task che termina la propria routine principale di esecuzione, ma non viene cancellato. Un task può cancellare se stesso prima di ritornare dalla routine di start-up, oppure può essere cancellato da un altro task mediante apposita syscall. Il micro-kernel non è responsabile della decisione di cancellare un task, tale prerogativa spetta all’utente. Per altro, esiste la possibilità di far ripartire un task zombie; questo giustifica la transizione di stato da ZOMBIE a READY.

Esiste infine uno speciale stato detto NOT_READY che denota una entry nella tabella dei TD inutilizzata. Lo scheduler considera "non valido o riutilizzabile" un TD con stato NOT_READY e/o con entry_point nullo. In fase di inizializzazione ogni entry viene posta in questo stato.

La macchina a stati eseguita dallo scheduler tratta le transizioni di stato secondo azioni ben precise:

La creazione di un nuovo task (create) è l’associazione di una entry non utilizzata della tabella dei TD al task stesso. L’indice di tale entry diventa l’identificatore del task (TID). La creazione del task che definiamo di root (il primo task eseguito dal kernel) è affidata alla routine mipos_start, usata per mandare in esecuzione il micro-kernel stesso. Il TID assegnato al task di root vale sempre 1.

Il dispatching di un task (run) in stato READY selezionato dallo scheduler, è ottenuto con l’operazione detta "cambio di contesto" (context switch). Quando la routine di esecuzione principale del task termina (terminate) lo scheduler ottiene il controllo: lo stato del task viene impostato a ZOMBIE; e quel task non viene più processato, ovvero il corrispondente descrittore non viene riassegnato, fino alla cancellazione (oppure fino alla riesecuzione) del task stesso.

Un task può sospendersi invocando una syscall "bloccante" (suspend). Un task si sospende in attesa di un qualche evento. Il kernel periodicamente monitora il verificarsi dell’evento atteso. Se tale evento si verifica, lo scheduler rimette il task in stato READY, in attesa di cedergli appena possibile la CPU.

Un task non-RT può essere prelazionato (quindi sospeso), a meno di non dichiarare regioni critiche non interrompibili. I task RT sono tipicamente risvegliati da eventi scatenati nel contesto di esecuzione di ISR (interrupt service routine). Questi tipicamente completano il lavoro svolto in risposta ad eventi asincroni generati da richieste di interruzione (IRQ).

Se un task viene cancellato, il suo descrittore è reimpostato a NOT_READY. Il descrittore viene così rilasciato tornando a essere riassegnabile per la creazione di un nuovo task.

La cancellazione (delete) può essere effettuata invocando una specifica syscall, da qualunque task del sistema. Un task può cancellare se stesso, prima di terminare, specificando come TID un valore nullo.

Frammento di codice tratto dal kernel di mipOS che esegue la FSM associata ai task non-RT

 do {
   INIT_KERNEL_CRITICAL_SECTION();
   ENTER_KERNEL_CRITICAL_SECTION();
     ++ KERNEL_ENV.tick_counter;
     //
     // Move task_context_ptr to scheduled task
     //
     KERNEL_ENV.task_context_ptr = p_task;
     //
     // Task State Machine (TSM)
     //
     switch ( p_task->status )  {
       //
       //  Task state is "running", but its start-up routine terminated
       //  and the task hasn't ever been deleted, now it is a zombie !
       case TASK_RUNNING:
         p_task->status = TASK_ZOMBIE;
         LEAVE_KERNEL_CRITICAL_SECTION();
         goto scheduler;
       //
       //  Task state is "suspended":
       //   - if it is waiting for expiration of a timer, then
       //     decrement the timer_tick_count; if timer_tick_count reaches
       //     zero then rise the SIGALM to the task;
       //   - if it is waiting for exiration of a hw timer
       //     and rtc_timeout reaches zero, then fire the SIGTMR to the task;
       //   - if pending signal is waiting for the task, set the wkup flag and
       //     restore task execution
       //
       case TASK_SUSPENDED:
         if (p_task->signal_waiting & SIGALM) {
           if (p_task->timer_tick_count>0) {
             --p_task->timer_tick_count;
           }
           else {
             p_task->signal_pending |= SIGALM;
           }
         }
         //
         if (p_task->signal_waiting & SIGTMR) {
           if (p_task->rtc_timeout &&
               KERNEL_ENV.rtc_counter>=p_task->rtc_timeout)
           {
             p_task->signal_pending |= SIGTMR;
             p_task->rtc_timeout = 0;
           }
         }
         //
         if (p_task->signal_pending & p_task->signal_waiting) {
           p_task->flags.wkup = 1;
           p_task->status = TASK_READY;
           LEAVE_KERNEL_CRITICAL_SECTION();
           goto scheduler;
         }
         break;
       //
       // task is ready (first execution), so start it
       case TASK_READY:
         p_task->status = TASK_RUNNING;
         LEAVE_KERNEL_CRITICAL_SECTION();
         goto dispatcher;
       // unexpected task status (BUG ?!): panic !!!
       case TASK_NOT_RUNNING:
       case TASK_ZOMBIE:
       default:
         KERNEL__PANIC("invalid process status: system halted !");
     }
   LEAVE_KERNEL_CRITICAL_SECTION();


Il dispatching dei task Modifica

Quando una chiamata di sistema sospende il task chiamante, ne preserva lo stato e restituisce il controllo al kernel. La routine di sistema SAVE_CPU_REGS fotografa lo stato del processore, salvando il contenuto dei registri (compreso il program counter) in un’istanza della variabile di tipo registers_state_t.

La funzione duale, RESTORE_CPU_REGS, che accetta come parametro il riferimento all'istanza della struttura registers_state_t, recupera lo stato salvato, saltando nel punto di ritorno di chiamata della precedente invocazione della funzione SAVE_CPU_REGS. Controllando il valore di ritorno di SAVE_CPU_REGS il codice chiamante può determinare se si provenga da un salto o da una invocazione per il salvataggio dello stato. Nel primo caso SAVE_CPU_REGS restituisce il valore del secondo argomento passato a RESTORE_CPU_REGS (con l’eccezione che se il parametro fosse 0 il valore restituito sarebbe 1). Nel secondo caso SAVE_CPU_REGS restituisce zero. Il meccanismo è portabile, infatti le due primitive hanno sintassi e semantica analoga alle primitive ANSI C conosciute come setjmp e longjmp.

Implementazione delle routine SAVE_CPU_REGS e RESTORE_CPU_REGS per sistemi con core Cortex-M3

; ------------------------------------------------------------------------------
; int SAVE_CPU_REGS(registers_state_t env)
;                                      ^- R0
; returns R0
; ------------------------------------------------------------------------------
SAVE_CPU_REGS
; Note: Registers are always stored in their numerical order,
; not as specified. (Example stmia r0, {r1, r3} is
; the same of stmia r0, {r3, r1} )
; R0 point to env
; Note2: r0 is incremented by 12*4 bytes,
;        We don't save status register (should we need to save it ?)
STMIA R0!, {R1-R12}
MRS R1, PSP
MRS R2, MSP
MOV R3, LR
STMIA R0!, {R1-R4}
MOV R0, #0 ; return 0
BX LR      ; MOV PC, LR
; ------------------------------------------------------------------------------
; void RESTORE_CPU_REGS(registers_state_t env, int value)
;                                          ^- R0     ^- R1
; The jmp_buf is assumed to contain the following, in order:
; R1-R12, PSP, MSP, LR
; ------------------------------------------------------------------------------
RESTORE_CPU_REGS
 MOV R5, R0         ; save pointer to env into R5
 MOV R6, R1         ; save value parameter into R6
 ADD R0, R0, #48    ; (12*4) r0->point to xSP, LR registers copy
 LDMIA R0!, {R1-R4} ; load stack pointers and link register
 MSR PSP, R1        ; restore stack pointers
 MSR MSP, R2
 MOV LR, R3         ; restore link register
 ISB
 PUSH { R6 }        ; save 'value' onto stack
 MOV R0, R5         ; restore R1-R12 registers from env
 LDMIA R0!, {R1-R12}
 POP { R0 }         ; recover 'value' from the stack
 BNE _ret1          ; if 0 return 1, else 'value'
                    ; Note: register LR was been set by _ctxm3_setjmp
 MOV R0, #1         ;       Execution restart from that address
                    ;       and _ctxm3_setjmp returns our R0 value
 _ret1
 BX LR              ; return to saved LR
 END


Le funzioni SAVE_CPU_REGS e RESTORE_CPU_REGS vengono dunque usate per effettuare lo scambio di contesto: una chiamata di sistema salva lo stato della CPU (il contenuto dei suoi registri) in un attributo del TD del task in esecuzione, chiamando internamente la funzione SAVE_CPU_REGS. Successivamente cede il controllo allo scheduler caricandone lo stato attraverso la primitiva RESTORE_CPU_REGS.

Lo scheduler processa la tabella dei descrittori, per selezionare un nuovo task pronto all'esecuzione (READY). Non appena un task si rende disponibile all'esecuzione viene invocato il dispatcher, che carica lo stato del nuovo task usando la routine RESTORE_CPU_REGS.

Oltre allo stato dei registri, è necessario garantire a ogni task e al kernel stesso una copia privata dello stack. Il dispatcher, prima di mandare in esecuzione un task, ottiene che questi operi su uno stack privato.

Per modificare lo stack pointer sono utilizzate due routine interne denominate SAVE_AND_SET_STACK_POINTER e RESTORE_STACK_POINTER. Queste sono implementate tipicamente in assembly e dipendono strettamente dall'architettura di riferimento.

L'implementazione per l'architettura x86 (riportata come esempio) è la seguente:

/*---------------------------------------------------*/
#ifdef MSVC
/*---------------------------------------------------*/
#define SAVE_AND_SET_STACK_POINTER(__OLD_SP, __STACK_P)\
/*---------------------------------------------------*/\
 __asm { mov ebx, [__OLD_SP] }\
 __asm { mov eax, esp }\
 __asm { mov [ebx], eax }\
 __asm { mov eax, __STACK_P }\
 __asm { mov esp, eax }\
/*---------------------------------------------------*/
#define RESTORE_STACK_POINTER(__OLD_SP)\
/*---------------------------------------------------*/\
 __asm { mov eax, __OLD_SP }\
 __asm { mov esp, [eax] }\
/*---------------------------------------------------*/
#elif defined (__GNUC__)
/*---------------------------------------------------*/
#define SAVE_AND_SET_STACK_POINTER(__OLD_SP, __STACK_P)\
/*---------------------------------------------------*/\
__asm__ __volatile__ ( \
  "movl " # __OLD_SP ", %ebx\n\t"\
  "movl %esp, %eax\n\t"\
  "movl %eax, (%ebx)\n\t"\
  "movl " # __STACK_P ", %eax\n\t"\
  "movl %eax, %esp\n\t")
/*---------------------------------------------------*/
  #define RESTORE_STACK_POINTER(__OLD_SP)\
/*---------------------------------------------------*/\
__asm__ __volatile__ ( \
  "movl " # __OLD_SP ", %eax\n\t"\
  "movl %eax, %esp\n\t")
#endif

Il dispatcher cede il controllo a un task nei seguenti modi:

- esecuzione (o riesecuzione) di un task (chiamando la funzione entry point del task);

- ripristino dell’esecuzione di un task sospeso tramite la chiamata alla routine RESTORE_CPU_REGS che ripristina lo stato salvato con SAVE_CPU_REGS fatta da una syscall sospensiva (task non-RT).

Più precisamente, nel primo caso, il dispatcher salva lo stack pointer (SP) del kernel; imposta il nuovo stack assegnando al registro SP un puntatore all’area dello stack il cui indirizzo è un attributo del TD, quindi esegue la funzione di start-up del task.

Nel secondo caso, si tratta di un task la cui esecuzione era stata sospesa. Lo stato dei registri è contenuto nel TD, quindi per restituire il controllo procede con l’esecuzione della primitiva RESTORE_CPU_REGS, simmetricamente a quanto fatto dalla routine bloccante che aveva ceduto a propria volta il controllo allo scheduler.

Il punto di ritorno di un task (quando la routine di start-up termina) è gestito dal dispatcher a valle della chiamata della routine stessa.

Il dispatcher in questo caso ripristina lo SP del kernel e cede il controllo allo scheduler.

I task realtime sono risvegliati tramite chiamata di DPC nel contesto di una ISR o tramite RT-timers periodici, e cedono la CPU sospendendosi sulla routine di attesa di DPC. Essi sono più prioritari dei task non real-time e sono quindi in grado di prelazionare gli stessi quando devono andare in esecuzione.

Collegamenti esterni Modifica

Fonti Modifica



Ad blocker interference detected!


Wikia is a free-to-use site that makes money from advertising. We have a modified experience for viewers using ad blockers

Wikia is not accessible if you’ve made further modifications. Remove the custom ad blocker rule(s) and the page will load as expected.

Inoltre su Fandom

Wiki casuale