AvrX un kernel per AVR

AvrX è un kernel real-time multitasking scritto per la famiglia AVR da Barello. Grazie alla disponibilità del codice sorgente e alle sue caratteristiche di real-time, è in grado di sopperire alle esigenze delle applicazioni embedded. AvrX è utilizzato in diversi contesti, dalle applicazioni di movimentazione a quelle più squisitamente ludiche.

AvrX è scritto in assembler e questo al giorno d’oggi potrebbe sembrare un limite perché ne impedisce il porting verso architetture differenti. In realtà, la soluzione adottata in AVR-X da Barello è sicuramente un vantaggio: in questo modo infatti il kernel è in grado di garantire le prestazioni che le applicazioni embedded richiedono sempre con maggiore incisività. Non solo, la dimensione del kernel, grazie all’assembler, risulta abbastanza contenuta, da 500 a 700 words a seconda della versione scelta. AvrX propone una serie di primitive  (API) che permettono di soddisfare ogni esigenza; ad esempio, esistono interfacce per la gestione dei task e quelle che garantiscono la comunicazione sincrona tra diversi processi. A questo proposito la tabella 1 mostra le diverse interfacce.

Tabella 1 – Alcuni servizi in AvrX

Tabella 1 – Alcuni servizi in AvrX

Barello propone AvrX in due differenti versioni: una è interamente scritta in assembler, interfacce comprese, ma attualmente non è più supportata; la versione supportata offre le interfacce scritte in C, sebbene il kernel continua a essere scritto in assembler. Da un punto di vista prestazionale non esistono differenze tra queste due versioni, perché il carico delle interfacce è sicuramente trascurabile. Per costruire AvrX può essere utilizzato l’ambiente di sviluppo basato su IAR, società svedese leader del settore, o, in alternativa, è possibile impiegare tranquillamente la cross-factory GCC. Il kernel di Barello garantisce una gestione efficiente delle interruzioni. Infatti, in AvrX è stata eliminata ogni interazione con il run-time del linguaggio C. Non solo, nella versione scritta in C (AvrX 2.6 per IAR systems e GCC C compiler) si è cercato di intervenire sulle ottimizzazioni per rantire le stesse prestazioni della versione assembler;  inoltre, il kernel risulta abbastanza compatto ed efficiente. La versione scritta in C è stata realizzata utilizzando il modello di memoria small (puntatori a 16 bit) e si è fatto ricorso all’interfaccia C nativa. Un aspetto da non trascurare è la migliorata gestione degli eventi asincroni e la dimensione abbastanza ridotta del kernel. L’occupazione di memoria del kernel risulta estremamente contenuta; infatti, si stima che AvrX necessiti, per le funzioni base, di circa 1500 byte di memoria, mentre ogni task richiede non più di 35 byte.

Figura 1: relazioni tra i processi.

Figura 1: relazioni tra i processi.

Uso del kernel

Prima di utilizzare AvrX è necessario definire alcune funzioni utilizzate direttamente dal kernel stesso, oltre alla presenza dei diversi task utilizzati dall’applicazione. Per prima cosa occorre definire un gestore dell’interrupt del timer (AvrXTimerHandler), la funzione che deve gestire  il reset (CPU_reset) e, infine, creare uno o più task così come richiesti dall’applicazione. Nel corpo principale della nostra applicazione (la funzione main) devono essere chiamate tutte le funzioni necessarie per inizializzare correttamente il kernel con il suo ambiente multitasking. Il  listato 1 mostra la sequenza iniziale. Dal listato si nota che per prima cosa è necessario preparare il sistema per eseguire le funzioni del kernel.

int main(void)                   // Main runs under the AvrX Stack
{
 AvrXSetKernelStack(0);
/*
 outp((1<<SE), MCUCR);            // Enable “Sleep” instruction
 outp(TCNT0_INIT, TCNT0);
 outp(TMC8_CK256, TCCR0);         // Set up Timer0 for CLK/256 rate
 outp((1<<TOIE0), TIMSK);         // Enable Timer0 overflow interrupt
 outp(0xFF, LEDDDR);              // Make LED output and
 outp(0xFF, LED);                 // drive high (LEDs off)
*/
 MCUCR = 1<<SE;
 TCNT0 = TCNT0_INIT;
 TCCR0 = TMC8_CK256;
 TIMSK = 1<<TOIE0;
 LEDDDR = 0xFF;
 LED = 0xFF;
 AvrXRunTask(TCB(task1));
 AvrXRunTask(TCB(task2));
 AvrXRunTask(TCB(Monitor));
 InitSerialIO(UBRR_INIT);          // Initialize USART baud rate generator
 Epilog();                         // Switch from AvrX Stack to first task
 while(1);
}
Listato 1 – Sequenza iniziale in ambiente Avrx

A questo proposito vediamo la definizione dello stack pointer, l’inizializzazione  della ram, dei registri del sistema e, successvamente, occorre definire la struttura dei task (si utilizzano, in alternativa, AvrXInitTask o AvrXRunTask). In seguito ci si predispone per inizializzare le diverse risorse hardware, quali timer, porte o linee seriali.  Il listato pone in evidenza anche la porzione di codice utilizzata per eseguire un task. Al termine è necessario passare il controllo alla funzione epilog. Questa funzione è di fondamentale importanza. Infatti, epilog si preoccupa di far partire lo schedulatore che, a sua volta, pone in esecuzione  il primo task presente nella coda _RunQueue.  Il kernel permette di inserire anche il modulo di debugger (monitor). In questo caso, il task di debugger ha la priorità massima (valore numerico pari a 0): il monitor sarà eseguito prima di ogni processo. Tipicamente,  il codice dell’applicazione, come il Task Control Block (TCB), dovrebbe risiedere in un file separato e solo successivamente importato. Sono presenti in AvrX diverse macro C utilizzate per la corretta gestione del sistema. I  file avrx.inc e avrx.h presenti nella distribuzione di Barello mostrano le definizioni utilizzate. La configurazione minima di AvrX fornisce solo un modulo per il supporto dei semafori e di inizializzazione dei task. Gran parte delle funzionalità di AvrX sono considerate opzionali: infatti, Single Step monitor, Timer Queue Management e Message Queue Management sono tutti moduli opzionali. La tabella 2 mostra la dimensione di ogni modulo presente nella configurazione completa. Dalla tabella si nota che il modulo di debugger ha una dimensione di 1200 byte, quindi circa il 14% della dimensione totale. Il gestore della coda dei messaggi, Message Queue Manager, ha una dimensione così piccola, perché la maggior parte delle sue funzioni sono già fornite nel modulo base.

Tabella 2 – Occupazione di memoria dei vari servizi

Tabella 2 – Occupazione di memoria dei vari servizi

Struttura di AvrX

Un task in AvrX, come nella maggioranza dei kernel real-time, è identificato semplicemente come una funzione C: un punto di ingresso, la definizione di eventuali variabili locali e un corpo con un ciclo infinito. All’interno del ciclo sono presenti diversi meccanismi di comunicazioni tra task o momenti di sincronizzazione. A questo scopo, il task può utilizzare chiamate come AvrXWaitTimer o AvrXWaitMessage.  Il listato 2 mostra un esempio di un task che, dopo aver invocato una primitiva del timer (delay), rimane nell’attesa di un evento collegato al semaforo (AvrXSetSemaphore).

Mutex Timeout;
AVRX_TASKDEF(myTask, 10, 3)
{
    TimerControlBlock MyTimer;
    while (1)
    {
         AvrXDelay(&MyTimer, 10); // 10ms delay
         AvrXSetSemaphore(&Timeout);
    }
}
Listato 2 – Esempio di un task
Tabella 3

Tabella 3

Notiamo dal listato che la macro AVRX_TASKDEF ha tre argomenti: il nome del task (della funzione), il task addizionale e la priorità. Un altro esempio della gestione dei task in AvrX è mostrato nel listato 4.

AVRX_SIGINT(SIG_OVERFLOW0)
{
   IntProlog();                  // Switch to kernel stack/context
   EndCriticalSection();         // Re-enable interrupts
   outp(TCNT0_INIT, TCNT0);      // Reset timer overflow count
   AvrXTimerHandler();           // Call Time queue manager
   Epilog();                     // Return to tasks
}
Listato 3 – Gestione eventi in AvrX
/*
Task 1 simply flashes the light off for 1/5 second and then on
for 4/5th
for a 1 second cycle time.
*/
AVRX_IAR_TASKDEF(task1, 0, 6, 3)
AVRX_GCC_TASKDEF(task1, 8, 3)
{
  while (1)
  {
      AvrXStartTimer(&timer1, 800);         // 800 ms delay
      AvrXWaitTimer(&timer1);
      LED = LED ^ 0x01;
//      outp(inp(LED) ^ 0x01, LED);
      AvrXStartTimer(&timer1, 200);         // 200 ms delay
      AvrXWaitTimer(&timer1);
      LED = LED ^ 0x01;
//      outp(inp(LED) ^ 0x01, LED);
  }
}
Listato 4 – Task in AvrX

I task che hanno la stessa priorità sono schedulati secondo la politica round robin. Si ricorda che il kernel non utilizza un time slice. In AvrX un cambio di contesto è permesso alla presenza di questi servizi: un task si ferma su un semaforo non ancora libero o rilasciato, mediante l’invocazione di una delay, richiesta di accesso a una coda di messaggi o quando il task volontariamente  rilascia il controllo al sistema. Il  cambio di contesto è gestito attraverso due funzioni: Prolog ed Epilog. La prima registra nello stack tutte le informazioni di un processo, mentre la seconda si preoccupa di prendere dallo stack le informazioni necessarie per eseguire un processo una volta che si sono conclusi i controlli dello schedulatore (ad esempio, il task deve essere in cima alla coda ready). Altro aspetto da non sottovalutare è la gestione di un interrupt con AvrX. Il linguaggio  C, con il suo ambiente run-time, fornisce una serie di funzioni per gestire correttamente un interrupt. Con GCC è possibile gestire un evento asincrono in base al nome del vettore. Il file presente nell’ambiente di lavoro, sigavr.h mostra una lista di possibili vettori o sorgenti di interrupt. La gestione del contesto, in GCC, è fatta attraverso  il ricorso al suo run-tme, ma con AvrX, al contrario, il  gestore dell’interrupt è costituito da una serie di chiamate alle funzioni di sistema (si veda il listato 3).

Dal listato è possibile notare che la gestione del contesto è in carico alle funzioni IntProlog e Epilog, funzioni di AvrX. La figura 2 mostra il contesto minimo con un processore AVR.

Figura 2: contesto minimo.

Figura 2: contesto minimo.

Altro argomento da non sottovalutare in un embedded system, in particolare con dimensioni ridotte, è sicuramente la gestione della memoria. Ogni task ha la necessità di disporre di 35 byte di memoria per le sue definizioni, mentre può utilizzare una dimensione variabile di stack: la sua ampiezza varia in base alle dichiarazioni utilizzate nel codice. Infatti, la dimensione dello stack, oltre a dipendere dalla quantità di memoria richiesta dai singoli task, risulta essere direttamente proporzionale alle strutture utilizzate dal programma o dalle variabili automatiche. Non è possibile, in AvrX, controllare  i limiti dello stack. Non esiste un controllo svolto da AvrX in fase di run-time. L’unica cosa che è possibile fare è svolgere una misura di massima, magari utilizzando un emulatore, allocare una quantità “notevole” di memoria o utilizzare un sistema dinamico che implicitamente è in grado di controllare  i limiti: marcare zone di stack con valori conosciuti. Ricordiamo che il kernel AvrX è rientrante e perciò utilizza lo stack per tutte le interruzioni.  Il cambio di contesto in AvrX si preoccupa di trattare tutti i registri  del processore per garantire il pieno supporto al C. Il contesto minimo è formato da 32 registri, SREG e PC: quindi, in totale 35 byte (figura 2). La funzione AvrXInitTask si preoccupa di inizializzare tutti i registri a un valore predefinito. Per minimizzare l’uso della ram, il task stack è utilizzato solo per curare il contesto del processore, mentre il kernel e gli interrupt utilizzano  il kernell stack. Esiste una particolare area di memoria utilizzata come zona di deposito di tutte le informazioni associati ai task: il PID, o Process ID Block. Questa struttura utilizza sei byte e registra le informazioni del puntatore al contesto, oltre alla coda, lo stato e la priorità di ogni processo. Qualsiasi registro utilizzato in un interrupt, o in una struttura o funzione globale, non può essere utilizzato da AvrX. Il kernel interviene sul contesto del singolo processo e utilizza tre registri come flag: R13 contiene il SysLevel per determinare  il livello di annodamento, R14 per le informazioni del Timer Queue Reentry e R15 che contiene l’identificatore del processo corrente. La tabella 1 mette in evidenza alcuni servizi di AvrX divisi per funzionalità.

Possibile  applicazione

Il kernel di Barello è utilizzato in diversi ambienti. Le tipiche applicazioni di AvrX riguardano la gestione dei processi automatici. Ad esempio, la figura 3 mostra l’uso dello schedulatore in una tipica applicazione.

Figura 3: ruolo dello schedulatore.

Figura 3: ruolo dello schedulatore.

Dalla figura vediamo come lo schedulatore coordina tutte le informazioni in suo possesso per mettere in esecuzione un processo. Lo schedulatore è rappresentato in figura da un cerchio. In base alle informazioni che ricava dalla coda dei messaggi, decide quando sostituire  il processo corrente con il primo presente nella coda ready. In questo caso sposta, quando occorre, il primo thread dalla coda ready ponendolo in esecuzione (stato di run). L’utente, attraverso la creazione di un task, registra le singole funzioni C dell’applicazione in processi. Di conseguenza, le funzioni encryptrion e decryption ancora prima di essere task all’interno di AvrX sono funzioni C. Solo dopo aver registrato le funzioni come task, è possibile spedire messaggi secondo le politiche permesse dal kernel alla coda dei messaggi, la message queue. Ogni messaggio in AvrX dispone di una particolare struttura, la Message Control Block, utilizzata per identificare la funzione, o task, che ha spedito il messaggio. Esistono task che spediscono messaggi alla coda e altri che sono in attesa di un messaggio particolare. Ogni volta che un task in attesa riceve un messaggio,  il kernel, per mano del suo schedulatore, decide se il processo è pronto per essere eseguito. Il processo messo in esecuzione dal kernel ha il pieno controllo del processore e, in base alle funzioni AvrX, può comunicare con i restanti processi. Ogni eventuale interrupt, periodico o asincrono, interrompe il processo corrente e lo schedulatore riprende il controllo della CPU. A questo punto, lo schedulatore verifica se siano mutate le condizioni precedenti all’interruzione ed, eventualmente, sostituisce il processo corrente con quello che ha richiesto la CPU.

 

 

Scrivi un commento