Nucleus Plus

Nucleus Plus è un real-time kernel utilizzato in ambito embedded per applicazioni ad ampio spettro; è impiegato in applicazioni di telefonia mobile ed applicazioni per il controllo industriale. In questo articolo vedremo alcune caratteristiche della sua struttura interna.

Nucleus Plus è un kernel che possiamo definire real-time, di tipo multitasking progettato appositamente per applicazioni critiche di tipo embedded. Quando si parla di sistemi real-time ci si vuole riferire a quei sistemi dove la risposta ad un evento, siano questi di tipo interno o esterno, è di assoluta importanza. Nella letteratura si distinguono sistemi real-time di tipo hard o soft. Circa il 95% del suo nucleo è scritto in ANSI C; per questa ragione esistono diverse versioni di questo kernel poiché risulta facilmente portabile su architetture hardware differenti, anche perché la sua struttura interna è abbastanza modulare. L’approccio che un eventuale utente ha verso il kernel è trasparente:infatti, Nucleus Plus è utilizzato come una normale libreria C. Il file oggetto che si ottiene, al termine del linking, è trasportabile su target, o messo in ROM, attraverso un loader residente o direttamente mediante un programmatore. Il  kernel è normalmente fornito in codice sorgente, in questo modo è possibile apportarvi le necessarie modifiche secondo le diverse esigenze. Il suo footprint è abbastanza contenuto: può avere, infatti, una dimensione di circa 15-20 Kb. Inoltre, fornisce un completo sistema multitasking comprendente un gestore di task, la comunicazione intertask, la sincronizzazione,  il gestore della memoria (MMU) e quello del timer di sistema. Esiste poi la possibilità di definire la propria architettura includendo componenti che, per le esigenze architetturali del sistema, risultano indispensabili. Per questa ragione è possibile inserire funzionalità quali il  TCP/IP, W i Fi, USB o MPEG. La tabella 1 pone in evidenza le diverse possibilità.

Figura 1: sequenza di inizializzazione.

Figura 1: sequenza di inizializzazione.

 

Tabella 1: componenti per Nucleus Plus

Tabella 1: componenti per Nucleus Plus

Utilizziamo Nucleus  plus

Nucleus Plus è fornito con il relativo codice sorgente per facilitare le modifiche al sistema operativo in base alle singole esigenze. Per fare questo occorre modificare il file chiamato int.s; si tratta di un file assembler che contiene le dipendenze utilizzate per inizializzare  il target quali lo stack o la definizione della tabella dei vettori. In questo file trovano posto le routine che sovrintendono all’inizializzazione della zona di memoria utilizzata come BSS o le funzioni che inizializzano le varabili d’ambiente utilizzate in Nucleus, si trova la funzione utilizzata per copiare i dati inizializzati dalla memoria non volatile in RAM o fornisce le funzioni utilizzate per fare il set-up dell’ambiente run-time necessario all’applicazione utente scritta in C, esistono funzioni che gestiscono i vari interrupt presenti nel sistema. Successivamente occorre definire un’apposita funzione C, chiamata Application_Initialize, utilizzata per definire l’ambiente di Nucleus. Questa funzione è posta in esecuzione dal kernel prima di ogni altra funzione e ancora prima di mettere in esecuzione l’intero sistema attraverso lo schedulatore. La terza fase consiste nel definire i vari task della nostra applicazione: se sono utilizzati servizi di Nucleus occorre includere  il file nucleus.h.  A questo punto non ci rimane altro che compilare e fare il link della nostra applicazione, compreso anche il file int.s. L’ultimo passaggio è portare la nostra applicazione sul target o mediante un opportuno loader o direttamente su memorie non volatili per mezzo di un apposito programmatore.

Application_initialize

Questa funzione è utilizzata per definire le risorse che il nostro kernel deve utilizzare perché risultano presenti nella nostra applicazione. Queste possono essere task, mailbox, pipe, semaphore, event buffer, memory pools o altri oggetti presenti in Nucleus. I servizi invocati  in questa procedura di inizializzazione non devono essere sospensivi; in questa fase non sono messi in esecuzione  i task, ma sono solo allocati sullo stack e inseriti nel sistema. Inoltre, occorre precisare che almeno un task, o un gestore di interrupt, deve essere presente.

Target  System  Considerations

La dimensione di Nucleus Plus varia da circa 20 Kb per una architettura CISC ed arriva fino a 40 Kb per un RISC. Nucleus Plus utilizza, poi, almeno 1,5 Kb per la sua struttura dati. In questo conteggio non vanno considerate la memoria necessaria per i task applicativi, per le code, pipe o di altri oggetti Nucleus Plus utilizzati dall’applicazione. Un’altra considerazione da tenere presente è l’uso di Nucleus con un target monitor residente; qualora si utilizzasse un monitor residente, allora l’applicazione costruita intorno a Nucleus dovrà essere necessariamente caricata in una zona di memoria non occupata dal monitor e, attraverso la definizione del sistema, l’applicazione utilizzerà solo gli interrupt definiti lasciando al monitor i vettori che sono utili per gestire i breakpoint  o per le varie prerogative utilizzate da un debugger, quali le istruzioni di singola traccia.

Opzioni  di configurazione

Esiste la possibilità in Nucleus di incrementare le prestazioni del sistema operativo. Infatti, attraverso le opzioni di configurazioni utilizzate al momento della compilazione presenti nel codice, quali il simbolo NU_NO_ERROR_CHECKING (in questo caso si disabilitano  i controlli su ogni chiamata di Nucleus), è possibile incrementare sensibilmente le sue prestazioni in termini di spazio e di tempo. Esiste, infine, tutta una serie di opzioni presenti nel codice che ci permettono di intervenire sulle prestazioni del kernel o che ci possono aiutare per le attività di porting verso altre architetture. Per esempio, è possibile abilitare i controlli sullo stack o ottenere una stringa ASCII utilizzata come diagnostica se un errore di sistema di tipo fatale è sollevato nella nostra applicazione.

Inizializzazione del sistema

La prima funzione che Nucleus esegue è Init_Initialize  presente nel modulo int.s. Certamente,  il vettore di reset deve riferire l’indirizzo di questa funzione. La Init_Initialize è responsabile dell’inizializzazione della fase iniziale del sistema, si preoccupa di inizializzare la tabella dei vettori, dello stack di sistema e delle variabili di Nucleus. Al termine di questa funzione il controllo è trasferito alla routine inc_initailize. Non è possibile, e non si deve fare, passare, in maniera deliberata o meno, il controllo dell’applicazione alla funzione init_initialize, ma si deve invocare questa funzione solo a fronte di un reset del sistema. Dopo la init_initialize, si passa a inc_initialize. La inc_initialize deve preoccuparsi di invocare le funzioni di inizializzazione di ogni singolo componente presente in Nucleus. Quando la funzione inc_initialize è conclusa, il controllo passa alla routine che il progettista software deve preoccuparsi di mettere a punto: la funzione Application_Initialize. In questa funzione si definisce l’architettura di Nucleus che deve interagire con il nostro applicativo. Per fare questo è necessario definire  i  vari task che devono essere presenti nel nostro sistema, le code, pipe, i semafori  o quant’altro la nostra applicazione ha la necessità di disporre. Non tutte le risorse che Nucleus permette di gestire devono essere, per forza di cose, presenti. Al termine di questa procedura di inizializzazione,  il controllo ritorna a inc_initialize e si attiva il processo di scheduling dei task presenti in questo modo il kernel è perfettamente funzionante.

Implementazione di un driver  di I/O

Un’ applicazione che utilizza un kernel real-time ha la necessità di interagire con periferiche esterne; per questo motivo si ha l’esigenza di gestire la componentistica esterna attraverso un sistema di device driver. Ogni sistema operativo, che sia complesso o meno, utilizza un sistema di questo tipo, ma, nel contempo, ognuno permette di costruire sistemi di I/O con diverse tecniche. Nucleus Plus for nisce una serie di interfacce standard quali inizializzazioni, di lettura o di scrittura. La tabella 2 pone in evidenza le funzioni che Nucleus mette a disposizione per creare e gestire un driver di I/O.

Tabella 2: funzioni di Nucleus Plus per la gestione di un driver

Tabella 2: funzioni di Nucleus Plus per la gestione di un driver

Le interfacce che Nucleus fornisce utilizzano una comune struttura di controllo. Ogni driver ha poi un singolo punto di ingresso definito al momento della sua creazione. Questa struttura permette di identificare  i vari servizi che il driver fornisce con tutti i parametri  necessari.  Può anche succedere che i servizi sono diversi, o anche in un numero superiore, in questo caso occorre linkare alla struttura di controllo una struttura supplementare. Nucleus Plus fornisce anche un meccanismo di protezione dei suoi dati interni per evitare accessi simultanei.  I drive in Nucleus Plus possono essere chiamati dai task presenti nel sistema, siano essi propri del sistema o task applicativi. Nucleus Plus consente la sospensione delle sue attività senza impattare sulle sue prestazioni e permette di utilizzare le varie risorse del sistema siano essi semafori, mailbox o ogni diversa risorsa. I driver  in Nucleus Plus sono creati e cancellati in maniera dinamica e non esisto no limiti sul numero dei driver presenti nel sistema. Ogni driver richiede una propria struttura chiamata Control Block. Questo è una zona di memoria disponibile in una applicazione. L’applicazione, una volta definito in maniera statica il driver, deve utilizzare le varie funzioni disponibili per creare (inserire il driver nella struttura del sistema), leggere o scrivere dati; inoltre, sempre ricorrendo a chiamate di sistema, è possibile inizializzare e terminare il driver (deallocare  il driver nel sistema). È possibile anche ottenere una lista dei driver presenti nel sistema. La creazione di un driver all’interno di Nucleus è permessa ricorrendo alla chiamata nu_create_driver. In questo modo il driver è inserito nella struttura di Nucleus in questo mod diventa disponibile a tutte le risorse. Quando non si ha più la necessità di utilizzare un driver è possibile eliminarlo dalla lista del sistema utilizzando la chiamata NU_Delete_Driver. Le applicazioni presenti in Nucleus possono richiede l’uso di un driver attraverso il servizio nu_request_driver. In questo modo il sistema passa all’applicazione, in sostanza, l’indirizzo di accesso al driver o il suo punto di ingresso. Le informazioni del driver sono poi contenute in una sua struttura. Tipicamente Nucleus Plus fornisce questa serie di servizi:

INITIALIZATION

Questa richiesta deve essere fatta solo dopo che il driver è stato creato e prima di ogni altra richiesta. Questa è utilizzata per inizializzare la struttura di controllo interna del driver.

ASSIGN

Un servizio del genere si utilizza per prevenire accessi simultanei al driver da differenti task. In sostanza si ottiene un accesso esclusivo al driver da parte di un task.

RELEASE

Questa richiesta permette di riassegnare il driver al task in cima alla coda delle richieste dell’uso del driver.

INPUT

Una richiesta di input istruisce  il driver a fornire un determinata quantità di dati da uno specifico dispositivo verso l’applicazione.

OUTPUT

Una richiesta di questo tipo istruisce il driver a spedire una certa quantità di dati ad uno specifico dispositivo.

TERMINATE

Le richieste di questo tipo sono dipendenti dal driver e sono tipicamente opzionali. Per esempio, alcuni driver possono richiedere una particolare procedura per reinizializzare un driver, magari per cambiare alcuni parametri di gestione di una seriale.

STATUS

Anche le richieste di questo tipo dipendono dal tipo di driver. È utilizzata quando si ha la necessità di interrogare  il driver sui parametri di programmazione impostati dall’applicazione.

Implementazione del driver

A questo punto disponiamo tutte le informazioni per scrivere  il nostro driver.  I driver in Nucleus sono tipicamente strutturati come una funzione C: uno switch è utilizzato per smistare tutte le possibili richieste. Per questa ragione, un driver deve avere una funzione simile al template mostrato nel listato 2. Invece,  il listato 3 mostra una possibile implementazione di un driver che gestisce le richieste di I/O in ambiente MS-DOS. La politica di gestione dei caratteri in ingressi utilizza la politica di polling.

void Application_Initialize(void *first_available_memory)
 {
VOID *pointer;
/* Create a system memory pool that will be used to allocate task
stacks, queue areas, etc. */
 NU_Create_Memory_Pool(&System_Memory,“SYSMEM”, first_available_memory, 20000,
50, NU_FIFO);
/* Create each task in the system. */
/* Create task 0. */
NU_Allocate_Memory(&System_Memory,&pointer,1000, NU_NO_SUSPEND);
NU_Create_Task(&Task_0, “TASK 0”, task_0, 0, NU_NULL, pointer, 1000, 1, 20,
NU_PREEMPT, NU_START);
/* Create task 2. */
 NU_Allocate_Memory(&System_Memory,&pointer,1000, NU_NO_SUSPEND);
 NU_Create_Task(&Task_2, “TASK 2”, task_2, 0, NU_NULL, pointer, 1000, 7, 0,
NU_PREEMPT, NU_START);
/* Create communication queue. */
 NU_Allocate_Memory(&System_Memory, &pointer, 100*sizeof(UNSIGNED), NU_NO_SUSPEND);
 NU_Create_Queue(&Queue_0, “QUEUE 0”, pointer, 100, NU_FIXED_SIZE, 1, NU_FIFO);
/* Create synchronization semaphore. */
NU_Create_Semaphore(&Semaphore_0, “SEM 0”, 1, NU_FIFO);
/* Create event flag group. */
NU_Create_Event_Group(&Event_Group_0, “EVGROUP0”);
}
/* Define task 0. Task 0 increments the Task_Time variable every
18 clock ticks. Additionally, task 0 sets an event flag that
task 2 is waiting for, on each iteration of the loop. */
void task_0(UNSIGNED argc, VOID *argv)
{
STATUS status;
/* Access argc and argv just to avoid compilation warnings.*/
status = (STATUS) argc + (STATUS) argv;
/* Set the clock to 0. This clock ticks every 18 system timer ticks. */
  Task_Time = 0;
  while(1)
   {
   /* Sleep for 18 timer ticks. The value of the tick is programmable in
   IND.ASM and is relative to the speed of the target system. */
   NU_Sleep(18);
   /* Increment the time. */
   Task_Time++;
   /* Set an event flag to lift the suspension on task 2.*/
   NU_Set_Events(&Event_Group_0, 1, NU_OR);
   }
}
 /* Define the task that waits for the event to be set by task 0. */
 void task_2(UNSIGNED argc, VOID *argv)
 {
 STATUS status;
 UNSIGNED event_group;
 /* Access argc and argv just to avoid compilation warnings. */
 status = (STATUS) argc + (STATUS) argv;
 /* Initialize the event detection counter. */
 Event_Detections = 0;
 /* Continue this process forever. */
 while(1)
   {
   /* Wait for an event and consume it. */
   status = NU_Retrieve_Events(&Event_Group_0, 1,
      NU_OR_CONSUME, &event_group, NU_SUSPEND);
   /* If the status is okay, increment the counter. */
   if (status == NU_SUCCESS)
      Event_Detections++;
   }
}
Listato 1 – Esempio di una funzione “Application_Initialize_Function”
VOID Driver_Entry(NU_DRIVER *driver, NU_DRIVER_REQUEST *request)
{
/* Process according to the request made. */
   switch(request -> nu_function)
      {
     case NU_INITIALIZE:
         /* Initialization processing.
         Note: nu_info_ptr field of “driver” is
         available for the driver’s use. */
         break;

     case NU_ASSIGN:
          /* Assign processing. */
          break;

     case NU_RELEASE:
         /* Release processing. */
         break;

     case NU_INPUT:
        /* Input processing. */
        break;

     case NU_OUTPUT:
         /* Output processing. */
         break;

     case NU_STATUS:
         /* Status processing. */
         break;

     case NU_TERMINATE:
         /* Terminate processing. */
         break;

     default:
        /* Bad request processing. */
        break;
}
Listato 2 – Struttura di un driver in Nucleus
/* Entry function of the minimal terminal driver example. */
VOID Terminal_Driver(NU_DRIVER *driver, NU_DRIVER_REQUEST *request)
{
char *pointer;

/* Process according to the request made. */
switch(request -> nu_function)
{
   case NU_INITIALIZE:
        /* Do nothing for initialization. */
        break;

   case NU_INPUT:
        /* Wait for the user to press a key. */
        while (!kbhit( ))
           {
                  /* Sleep a tick to allow other tasks to run. */
                  NU_Sleep(1);
           }
       /* Setup input character pointer. */
       pointer = (char *)request ->
       nu_request_info.nu_input.nu_buffer_ptr;
       /* Character present, read it into the supplied destination. */
       pointer = (char) getch( );
       /* Indicate successful completion. */
       request -> nu_status = NU_SUCCESS;
       break;

   case NU_OUTPUT:
       /* Setup output character pointer. */
       pointer = (char *) request ->
       nu_request_info.nu_output.nu_buffer_ptr;
       /* Call putch to print supplied character. */
       putch((int) *pointer);
       /* Indicate successful completion. */
       request -> nu_status = NU_SUCCESS;
       break;

   default:
      /* Bad request processing. */
      request -> nu_status = NU_INVALID_ENTRY;
      break;

   }
/* End of driver request, return to caller. */
}
Listato 3 – Implementazione del driver

Conclusioni

Nucleus Plus garantisce l’estrema portabilità del suo codice verso altre soluzioni architetturali. Per svolgere questo lavoro con successo occorre modificare l’inizializzazione di sistema, il task management e il gestore del timer. Non solo è possibile portare Nucleus verso un altro processore, ma anche, cosa estremamente flessibile, portare il sistema operativo, e la nostra applicazione, verso differenti piattaforme a parità di CPU. Nucleus Plus è uno strumento estremamente flessibile per le nostre applicazioni real-time di tipo time-critical. L’estrema versatilità della sua architettura permette poi di integrare nel sistema device driver per dispositivi periferici quali file system.

 

 

Scrivi un commento

EOS-Academy

Ricevi GRATIS le pillole di Elettronica

Ricevi via EMAIL 10 articoli tecnici di approfondimento sulle ultime tecnologie