- Elettronica Open Source - https://it.emcelettronica.com -

Il controllo dei sistemi Real-Time

Un sistema embedded di tipo  [1]real-time [1] deve essere progettato utilizzando un linguaggio che deve descrivere il flusso comportamentale del proprio ambiente, ma anche ricorrendo a tecnologie software che siano in grado di tracciare un suo modello descrittivo: l’uno non sostituisce l’altro.

I sistemi embedded sono sempre più presenti nella vita di tutti i giorni tanto da stimolare studi e ricerche al fine di ottenere la migliore soluzione possibile. Un sistema embedded è stato dagli albori dell’era informatica realizzato utilizzando il linguaggio principe dei processori, in altre parole l’Assembly. Solo successivamente, per via dell’aumento della complessità, è stato preferito un linguaggio strutturato ad alto livello come il C [2]. Da alcuni anni, però, l‘interesse crescente nei confronti di sistemi di programmazione alternativi che possono garantire affidabilità e robustezza delle applicazioni ha spinto le comunità di ricerca e le industrie informatiche a trovare alcune soluzioni in grado offrire linguaggi con caratteristiche di tipo real-time. Qualsiasi sistema può essere classificato in due categorie: da una parte ci sono quelli che non hanno esigenze temporali stringenti e, dall’altra parte, ci sono i sistemi che hanno la necessità di ottenere un risultato entro un tempo prestabilito (ossia all’interno di una deadline).

Nel primo caso conta il tempo medio di risposta, ossia il tempo per svolgere l’operazione pianificata entro un tempo prestabilito, mentre nella seconda ipotesi conta il limite superiore, ossia la computazione deve concludersi all’interno di un quanto di tempo prestabilito e non indicativo. Un sistema di tipo real-time è costituito da diverse unità di esecuzione con compiti elaborativi differenti. In un sistema di questo tipo non tutte le singole componenti del sistema avranno gli stessi requisiti in termini di temporali e potrà capitare che alcuni di questi componenti requisiti di questo tipo non ne abbiano per nulla. Esistono diversi modi per definire questi elementi, o unità di esecuzione. Ad esempio, Nilsen Kelvin diversi anni fa elaborò una classificazione dei componenti all’interno di un sistema (task o thread) e, a questo proposito, divise le unità di esecuzione tra task periodici, task sporadici, task spontanei, thread real-time ciclici e runnable Thread. Non entriamo in merito alla loro classificazione perché lo spazio non ci consente di approfondire questo aspetto. La premessa in apertura dell’articolo è importante perché sicuramente il linguaggio non può fare a meno di un ambiente di lavoro che sia in grado di descrivere il sistema, ma in questo articolo vogliamo porre l’attenzione sul primo aspetto tralasciando le applicazioni costruite con uno strumento di definizione dell’ambiente applicativo. Ada e Java sono stati pensati per offrire all’utilizzatore un diretto e completo supporto alla concorrenza: il task in Ada e il Thread in Java. I due linguaggi offrono più o meno funzionalità equivalenti, dalla possibilità di definire unità di esecuzione concorrente al controllo della mutua esclusione o della sincronizzazione, i due approcci sono sostanzialmente differenti. Non possiamo discorrere in dettaglio sulle loro differenze, però possiamo porre l’attenzione che esistono in sostanza due linguaggi che consentono di descrivere un modello comportamentale. Questi due linguaggi utilizzano costrutti del linguaggio stesso e non utilizzano librerie esterne, in questo modo si riducono i gradi di libertà di ogni utilizzatore costringendo il ricorso agli elementi propri del linguaggio.

ADA E JAVA

Il linguaggio Java, al pari del linguaggio messo a punto dall’Amministrazione della Difesa USA, è in grado di offrire un Run Time System sufficientemente piccolo da poter essere utilizzato in un ambiente embedded; in effetti, questo modulo può occupare, a seconda delle implementazioni, da poche decide di Kb a qualche centinaia di Kb. Non solo, Java consente di garantire alti gradi di portabilità del codice e di poter utilizzare la stessa implementazione in differenti target con prestazioni di sicuro interesse, oltre a semplificare il processo di implementazione di sistemi fault tolerant in modo da poter ridistribuire un programma, in caso di guasto di uno o più device, in maniera del tutto trasparente. Java consente anche di semplificare la programmazione utilizzando una filosofia di progetto estremamente semplice e modulare, ossia la programmazione a oggetti. In questo modo, Java e Ada, si pongono in contrapposizione ai linguaggi tipicamente procedurali come il C o pseudo object oriented come il C++. La possibilità di utilizzare l’oggetto come elemento fondamentale che interagisce con altri non è altro che una rappresentazione del mondo reale: ereditarietà e polimorfismo consentono al progettista di sistemi di tradurre l’implementazione in maniera più chiara e semplice. In sostanza, la OOP (Object Oriented Programming) è diventata il paradigma dominante del mondo dello sviluppo del software poiché permette di modellare la realtà come un insieme organizzato di oggetti. L’uso di librerie predefinite e l’assenza di puntatori come nel C, anche se in Ada occorre riferirsi al profilo Ravenscar con Ada 95 o 2005, consente di utilizzare la memoria con maggiore sicurezza. In effetti, in questo modo in Java si usa il concetto di riferimento impedendo al programmatore di manipolarlo. La presenza del Run-Time Checking, dei type safety o dell’exception handling non fanno che aumentare il grado di affidabilità e sicurezza. In sostanza, grazie all’uso di queste tecniche il mondo reale diventa più controllabile e predicibile. In letteratura, si parla delle quattro caratteristiche fondamentali di un sistema realtime: predicibilità (Predictability), schedulabilità (Schedulability), efficienza e robustezza. Certamente, come in un mondo reale, la nostra applicazione è suscettibile a diverse stimoli e la necessità di controllare gli eventi diventa un imperativo categorico. Questi due linguaggi utilizzano unità di esecuzione elementari per interagire con l’esterno e per suddividere il carico su diversi eventuali core. Un task Ada è un’unità di compilazione che comprende una specifica, una descrizione e diversi oggetti. Il listato 1 pone in evidenza una possibile dichiarazione di un task Ada, come si evince dal listato si utilizza un package con una dichiarazione di un tipo task, mentre il listato 2 mostra il comportamento del task che risulta costituito da un loop infinito che visualizza una stringa.

package Outputter_Pkg is
   task type Outputter;
   type Outputter_Ref is access
   Outputter;
end Outputter_Pkg;
}
Listato 1 - Dichiarazione di un task

 

with Ada.Text_IO; use Ada.Text_IO;
package body Outputter_Pkg is
  task body Outputter is
   begin
    loop
     Ada.Text_IO.Put_Line
     (“Hello”);
  end loop;
  end Outputter;
end Outputter_Pkg;
Listato 2 – Loop infinito

Al contrario, l’approccio con Java è differente. Il task è rappresentato da un Thread. A questo scopo si utilizza la classe Thread formata da diversi metodi utilizzati per controllare l’applicazione e, in particolare, il Thread. Così, l’istruzione equivalente in Java è mostrata dal listato 3.

class Outputter extends Thread{
public void run(){
while (true){
System.out.println(“Hello”);
}
}
}
Listato 3 - Dichiarazione Java

La tecnica mostrata è chiamata come subclassing Thread, ma un approccio di questo tipo non è sempre consigliabile. Infatti, Java supporta solo l’ereditarietà singola e di conseguenza una classe che estende Thread non può essere prevista in un’altra classe. Java risolve questo problema fornendo un interfaccia Runnable, con un (astratto) metodo run (). Un Thread Java implementa Runnable fornendo un metodo run() che restituisce, e fornisce anche un costruttore, e accetta un parametro Runnable. Il listato 4 mostra una possibile realizzazione. Il linguaggio Ada fornisce una maggiore flessibilità rispetto a Java.

class RunnableOutputter
 implements Runnable{
  public void run(){
   while (true){
    System.out.println(“Hello”);
         }
  }
Listato 4 – Runnable in Java

Per prima cosa è possibile dichiarare un task in diversi modi: task type, oggetto task e un puntatore al tipo task. È bene ricordare che in Java estendere la classe Thread è del tutto analogo alla dichiarazione di un access ad un task type in Ada. In secondo luogo, in Ada si possono dichiarare task come oggetti o tipi di task in modo nidificato, con visibilità standard per i nomi in ambiti esterni. Un linguaggio senza un adeguato supporto di tipo run-time non è perfettamente utilizzabile. Nel panorama embedded esistono differenti esempi di ambienti di compilazione/esecuzione con Ada o Java.

ADA CON AVR32

In un sistema real-time la risposta ad un evento asincrono deve essere garantita entro un tempo prestabilito. Per questa ragione diventa necessario utilizzare un sistema che permette di controllare il tempo di esecuzione al fine di gestire correttamente un evento esterno. Una possibile implementazione del linguaggio Ada su Atmel AVR32 deve tenere conto di questi elementi. Prima di tutto ricordiamo che l’AVR32 è un processore RISC su 32 bit con un’architettura ottimizzata al fine di garantire alta densità e alta computazione passando attraverso un basso consumo. Il processore dispone di 13 registri ad uso generale (da R0 a R12), un registro (LR) per conservare l’indirizzo di ritorno delle funzioni, il program counter e un registro di sistema (SR). La figura 1 pone in evidenza l’architettura interna del processore.

Figura 1: AVR UCR 3 core.

Figura 1: AVR UCR 3 core.

Il componente di casa Atmel dispone di quattro livelli di interrupt, un Non-Maskable Interrupt, NMI, e diverse eccezioni. Il core UC3 è particolarmente utilizzato per applicazioni di controllo dove il tempo di esecuzione è deterministico. Il componente utilizza una pipeline integrata a tre stadi con una SRAM interna che non passa attraverso il bus di sistema garantendo un ciclo unico in lettura/scrittura della memoria. Il core UC3 dispone anche di un interrupt controller programmabile che permette di configurare la priorità di diversi gruppi di interrupt. Il core, prima ancora di passare in modalità interrupt, pone i registri da R8 a R12, PC, LR e il registro di sistema nello stack pointer. Nel componente è poi presente un registro a 32 bit Count/Compare System Register particolarmente utile per misurare il tempo di esecuzione. Il registro Count al reset viene caricato con un valore pari a zero ed è incrementato ad ogni ciclo di clock della CPU, in caso di overflow ripassa per lo zero in maniera trasparente. Il registro Compare è utilizzato per impostare l’interrupt da associare all’evento; in sostanza, quando il registro Count risulta uguale a Compare il microprocessore scatena un interrupt. La misura dei tempi è essenziale in un sistema che possiamo definire deterministico e predicibile. Al fine di poter gestire la misura del tempo di esecuzione diventa necessario modificare/integrare alcune definizioni presenti nell’Annex D dello standard di Ada 2005. È possibile compiere le modifiche direttamente nei package D.14 e D.14.1 presenti nella libreria piuttosto che aggiungerne dei nuovi. Il paragrafo D.14 dello standard prevede il package Ada.Execution_Time che contiene una funzione in grado di supportare la misura del tempo di esecuzione:

function Interrupt_Clock
( Priority : System .
Interrupt_Priority )
return CPU_Time ;

Questa funzione restituisce il tempo totale speso dagli interrupt handler per uno specifico interrupt priority dal momento dello start-up. Al contrario, il paragrafo D.14.1 presenta il package Ada.Execution_Time.Timers:

Pseudo_Task_Id : aliased
constant
    Ada . Task_Identification .
Task_Id
             := Ada .
Task_Identification .
Null_Task_Id ;
    type Interrupt_Timer
    (I : System .
Interrupt_Priority )
is new Timer ( Pseudo_Task_Id ‘
Access ) with private ;

Nessuna delle operazioni di Timer che non prevalgano, come si suppone che il sottostante stesso meccanismo sarà utilizzato sia per attività e timer interrupt e che la ragione solo per avere un tipo separato per i timer di interrupt è la differenza di discriminante. Il controllo del tempo di esecuzione può essere fissato in un nuovo package, ad esempio System.BB.TMU (Time Management Unit) presente all’interno del run-time della configurazione bare board. Questo, per inciso, è stata la scelta dell’università norvegese e di Atmel che hanno definito un run-time environment per Ada su Atmel AVR32. Il package definisce il tipo CPU_Time che rappresenta il tempo di esecuzione su 64 bit come tipo privato Timer.

type Timer is
record
    Active_TM : Timer_Id ;
    Base_Time : CPU_Time ;
    pragma Volatile ( Base_Time
);
    Timeout : CPU_Time ;
    Handler : Timer_Handler ;
    Data : System . Address ;
    Active : Boolean ;
    Acquired : Boolean ;
end record ;

JAVA CON AVR

Come il linguaggio Ada anche Java ha la necessità di disporre di un ambiente di lavoro, ossia di una virtual machine con il compito di eseguire il codice Java nella nostra applicazione embedded. Così, NanoVM implementa una java virtual machine per il processore Atmel della serie AVR ATmega8. Esistono diverse applicazioni di questa virtual machine, in modo particolare nel segmento dei piccoli robot. La figura 2 mostra l’applicazione per Asuro Robot.

Figura 2: Asuro Robot.

Figura 2: Asuro Robot.

Grazie alla presenza della JVM è possibile programmare la nostra applicazione embedded utilizzando la distribuzione standard di Sun: l’applicazione presente sul target interpreta il bytecode di Java ed esegue le sue istruzioni. Di solito, una JVM include anche un bootlader e una serie di classi native inserite in una memoria flash presente su target. Le scelte sono diverse, ad esempio la NanoVM rimpiazza completamente i 8 KB della zona programmi disponibile all’intermo della CPU come mostra la figura 3.

Figura 3: Java su AVR.

Figura 3: Java su AVR.

In questa particolare realizzazione si utilizza la memoria EEPROM. NanoVM lavora direttamente con il formato byte code di Java interpretato dalla JVM durante il run-time per questa particolare ragione il codice deve essere traslato direttamente nel codice specifico di AVR.