Introduzione al .NET Micro Framework

Valuta questo articolo 0 Voti

Autori: Lorenzo Maiorfi e Gianluca Ruta

Il Micro Framework è un’infrastruttura per microcontrollori a 32 bit i cui vantaggi principali consistono nel fornire una ricca API indipendente dall’hardware, un’efficiente controllo sulle periferiche  del microcontrollore, una runtime che gestisce aspetti che vanno dal bootstrap al debug del firmware, un ricco ambiente di sviluppo quale Visual Studio, il supporto all'emulazione del dispositivo oltre, naturalmente, a innumerevoli altri vantaggi intrinseci del Framework .NET, indipendentemente dal fatto che si parli della versione  "Micro" (per microcontrollori), della versione "Compact" (per palmari e telefoni, ma utilizzato anche sulla XBOX) o della versione "maggiore" del framework, destinato allo sviluppo di software per personal computer.

Rilasciato nella sua prima versione nel 2002, il Framework .NET rappresenta l'interpretazione di Microsoft alla tendenza già consolidatasi negli anni precedenti con Java in merito alla "virtualizzazione" del microprocessore. L'obiettivo principale del progetto è infatti costituito dalla realizzazione di una "virtual machine" (così chiamata nel gergo Java), denominata "CLR" (Common Language Runtime), in grado di compilare o interpretare (a seconda delle implementazioni) codice scritto in un linguaggio indipendente dall'hardware , denominato IL (Intermediate Language), traducendolo  "just-in-time" in istruzioni native per lo specifico microprocessore su cui si sta eseguendo l'applicazione.

I vantaggi della virtualizzazione sono numerosi ed evidenti, primi tra tutti quelli dell'indipendenza dall'hardware e della maggior possibilità di controllo su quanto viene eseguito, a fronte di un calo delle prestazioni assolutamente trascurabile. A tale proposito, sono invece numerosi i casi in cui è stato dimostrato che un'applicazione destinata ad essere eseguita dalla CLR (ossia "managed", come viene comunemente definita) può addirittura presentare performance superiori alla controparte "nativa", in quanto la possibilità da parte della runtime di ottimizzare l'esecuzione per lo specifico hardware su cui si sta eseguendo l'applicazione (tenendo conto ad es. della versione del sistema operativo, del numero di core/processori e della effettiva RAM a disposizione) permette di aumentare le prestazioni più di quanto esse non calino per gestire la compilazione "just-in-time" da parte dell'infrastruttura.

Nel mondo dei microcontrollori, in cui le architetture dominanti sono ben più numerose di quanto presente nel mondo del personal computer (in cui sostanzialmente troviamo x86, x64 e Itanium), i vantaggi derivanti dall'adozione di un framework basato su una runtime che virtualizzi l'hardware sono ancora più evidenti. Grazie al .NET Micro Framework possiamo infatti realizzare applicazioni che possono essere eseguite indistintamente su processori Atmel, NXP, Freescale, Analog Devices, ecc.

Per meglio comprendere come si incastrino tutti i tasselli di hardware, funzionalità del framework e applicazioni managed, diamo un'occhiata all’architettura del Micro Framework.


 

Il cuore del sistema: la HAL e la PAL

In un sistema embedded di piccole dimensioni non troviamo quasi mai un sistema operativo, soprattutto in considerazione dell'impatto che questo potrebbe avere sulle risorse. Per questo motivo il Micro Framework ne fa tipicamente a meno e, al contrario dei suoi fratelli maggiori (Compact Framework e Framework.NET), è in grado di prendere direttamente il controllo dell’hardware.

Il componente al livello più basso è detto HAL (Hardware Abstraction Layer) e si occupa di gestire le operazioni più a stretto contatto con il microprocessore, come la gestione dell’I/O, gli interrupt e i timer, nonché l'intero processo di bootstrap.

I componenti dell'HAL collaborano con i driver nativi che permettono di astrarre le funzionalità delle periferiche hardware. A seconda del produttore hardware il .NET Micro Framework porta con se' una serie di driver, come ad esempio quello per la gestione della seriale asincrona, delle comunicazioni sincrone (SPI e I2C), per pilotare i display LCD, per il monitoraggio dell'alimentazione, per la gestione delle porte I/O, per la gestione del modulo per la generazione PWM, per l'utilizzo di memorie Flash ed Eeprom e molto altro ancora. I driver nativi vengono tipicamente scritti dagli OEM che forniscono il "porting" del .NET Micro Framework specifico per la loro architettura.

Sopra la HAL siede un altro componente fondamentale denominato PAL (Platform Abstraction Layer) che utilizza le funzionalità dello strato HAL per implementare nel modo più efficiente possibile caratteristiche tipiche del Micro Framework quali il multi-threading ed il garbage-collector  (il servizio in grado di liberare automaticamente blocchi di memoria non più utilizzati da parte del firmware).

Al di fuori di questi componenti (HAL, PAL e driver a basso livello) il resto del Micro Framework è indipendente dall’hardware. Il porting del Micro Framework su un hardware differente si riduce perciò all’implementazione di entrambi i componenti o della sola PAL. Ad oggi, la famiglia di microcontrollori per i quali esiste il maggior numero di implementazioni in tal senso è senz'altro ARM (7 e 9).

Un altro compito importante della HAL è quello di provvedere alla modalità di esecuzione del codice che avviene su singolo thread o tramite ISR (Interrupt Service Routine). In sostanza Micro Framework rinuncia allo Scheduler dei thread in luogo di un multitasking di tipo cooperativo, in cui ciascun thread riceve una finestra temporale massima di 20 ms, che rappresenta quindi la "risoluzione" temporale minima per la realizzazione di applicazioni che abbiano necessità di garantire un ritardo massimo nell'esecuzione di determinate operazioni di controllo.

Il modello di sviluppo

Il modello di sviluppo del Framework .NET prevede la generazione di uno o più file, denominati assembly, che agiscono come contenitori fisici (ed autodocumentanti, grazie all'esposizione di un piccolo database interno) delle entità logiche , tipicamente classi, che sono le principali responsabili del comportamento del software sviluppato.

Senza addentrarci in temi che possono essere facilmente approfonditi dalle moltissime fonti reperibili in argomento (primo fra tutti Internet), vale la pena di focalizzare la nostra attenzione sul fatto che qualsiasi applicazione .NET può essere vista quindi come un insieme di moduli (principalmente classi ed in misura ridotta interfacce e "tipi valore") legati da rapporti di dipendenza del tipo chiamante/chiamato, oppure base/derivato, oppure contenitore/contenuto, in cui ciascuna entità è caratterizzata da un insieme di "membri" che possiamo riassumere sommariamente in field, metodi, proprietà ed eventi.

I field sono membri che rappresentano un elemento di informazione che costituisce lo stato del tipo cui appartengono. Ad esempio, in un'applicazione per la gestione del personale di un'azienda,  per un oggetto di tipo Impiegato è probabile che la data di assunzione sia rappresentata da un field, come illustrato nel frammento di codice che segue:

using System;

public class Impiegato
{
 DateTime _dataAssunzione;
}

I metodi rappresentano invece delle azioni che è possibile compiere sull'oggetto che li espone. Ad esempio per l'oggetto di tipo Impiegato è possibile prevedere un metodo denominato "Licenzia()". Le parentesi accanto al nome del metodo tipicamente rappresentano la lista dei parametri (vuota in questo caso) che intendiamo passare al metodo. Essi contribuiscono a creare un contesto per l'azione da svolgere. I metodi possono anche restituire un oggetto a titolo di valore di ritorno. Oltre a poter utilizzare tipi di oggetto differenti per i parametri del metodo, ciascun parametro è anche caratterizzato da una "direzione": ingresso (utilizzando la clausola "in", il default), uscita (con "out")o entrambi (con "ref"):

Le proprietà sono dei particolari metodi con semantica "get/set" che intervengono nel momento in cui il programma che le invoca acceda rispettivamente in lettura o in assegnazione, come ad esempio nel caso di una proprietà "Stipendio" esposta dalla classe Impiegato in cui un'eventuale assegnazione di un valore non valido (ad es. un valore negativo) può essere intercettato grazie all'esecuzione del metodo "set" relativo alla proprietà:

using System;

public class Impiegato
{
 public void Licenzia()
 {
 // [...]
 }

 public bool MetodoConValoreDiRitorno()
 {
 // [...]
 return true;
 }

 public void MetodoConParametroIn(string par1, int par2)
 {
 // [...]
 }

 public void MetodoConParametriOutERef(out int par1, ref string par2)
 {
 // [...]

 par1 = 10;
 par2 = par2.ToUpper();
 
 // [...]

 return;
 }
}

Un evento è un ulteriore tipo di membro che rappresenta un metodo in cui chiamante e chiamato si invertono di ruolo. In altri termini un oggetto che solleva un evento provoca l'esecuzione di una chiamata in uno o più "ascoltatori" (se presenti), nei confronti dei quali avviene una chiamata a metodo:

using System;

public class Impiegato
{
 decimal _stipendioBase;
 decimal _rimborsoSpese;

 public decimal Stipendio
 {
 get
 {
 return _stipendioBase + _rimborsoSpese;
 }

 set
 {
 if (value < 0) throw new ArgumentException("Valore non valido");

 _stipendioBase = value;
 }
 }
}

Oltre alle possibilità messe a disposizione da codice "managed", .NET Micro Framework prevede un meccanismo di Interoperabilità che permette di scrivere una funzione direttamente in modalità nativa, tipicamente per quelle parti di applicazione che dovessero essere particolarmente critiche in termini di performance o controllo dell'hardware. Nella versione attuale di Micro Framework la chiamata al codice nativo richiede l'uso del "Porting Kit", un SDK specifico tipicamente rivolto a chi realizza nuove implementazioni della piattaforma.

Ovviamente, pur risolvendo situazioni diversamente non affrontabili, l’uso dell'Interoperabilità complica notevolmente la portabilità del codice e può compromettere l’efficienza della CLR, motivo per cui questa soluzione deve essere utilizzata con grande attenzione.

La gestione della memoria

Uno dei pregi delle versioni del Framework.NET è il Garbage Collector che permette di mappare la memoria a livello di oggetto. Non appena un oggetto diventa orfano dei suoi reference diretti e indiretti, il Garbage Collector è libero di riusare quella zona di memoria. Il compito del Garbage Collector è duplice: da una parte si occupa di rendere disponibile la memoria inutilizzata, dall’altra di spostare i blocchi di memoria quando si libera lo spazio in mezzo; questa procedura viene chiamata garbage collection e avviene sospendendo temporaneamente l’esecuzione dei thread e riaggiornando i reference ai blocchi di memoria spostati.

Il vantaggio principale della garbage collection è quello di evitare la frammentazione della memoria, fenomeno derivante dalla continua allocazione e deallocazione di blocchi di memoria di diversa misura che, alla lunga, porta a non avere più la disponibilità di blocchi contigui di memoria sufficientemente capienti. Quando si parla di CLR e della garbage collection, in molti si spaventano per il tempo di elaborazione necessario a svolgere questo processo. Nella versione del Micro Framework è stata implementata una versione molto leggera con un algoritmo di Mark+Sweep non incrementale in luogo del tradizionale sistema generazionale.

Il CLR prevede un meccanismo chiamato Weak Reference che permette al Garbage Collector di reclamare memoria sotto condizioni di Memory Pressure, ossia quando si arriva vicini all’esaurimento di memoria. In aggiunta a questo prezioso meccanismo, il Micro Framework aggiunge l’Extended Weak Reference che permette agli oggetti di essere persistiti e recuperati in memoria durante un reboot. Se consideriamo che il boot è molto sfruttato negli ambienti embedded, anche per assicurare la stabilità sul lungo termine, questo meccanismo si rivela assolutamente molto prezioso.

La Base Class Library (BCL)

La presenza di una runtime in grado di astrarre lo specifico hardware consente inoltre l'indubbio vantaggio di poter definire delle "librerie" di classi relative allo svolgimento delle attività più comuni e frequenti. Chi sviluppa applicazioni con il .NET Micro Framework può infatti contare su una nutrita "Base Class Library" che, sfruttando lo strato HAL/PAL di cui sopra, permette di accedere a tutte le periferiche del microcontrollore utilizzando un modello ad oggetti coerente e funzionale. Grazie poi alla struttura object oriented di questa piattaforma ed alla sua architettura, lo sviluppatore può estendere facilmente le classi esistenti o crearne di nuove che potranno essere facilmente riutilizzate anche su piattaforme hardware differenti.

Troviamo classi per la gestione delle porte I/O, delle porte seriali, delle porte I2C ed SPI, del file system, dei convertitori ADC e DAC built-in, dei moduli PWM e, per i produttori che forniscono un porting più completo, troviamo classi per la gestione dell'USB host/device, della rete, del CAN, della comunicazione 1-wire e molto, molto altro ancora.

Di grande interesse sono inoltre le numerose classi dei namespace Microsoft.SPOT.Presentation, che costituiscono un insieme di funzionalità molto ricche per la generazione  di primitive grafiche come rettangoli, ellissi, linee, poligoni e immagini sui display per i cui controller esiste un driver nel porting che si sta utilizzando. C’è anche un ampio supporto per il testo, il suo allineamento, il font e la formattazione, sebbene con alcune limitazioni sull’uso dei colori e il calcolo dello spazio occupato sullo schermo. Ed ancora meritano di essere citati una buona scelta di controlli come TextFlow, Border, Canvas (un pannello), Image, ListBox, e StackPanel che permette un allineamento a stack per evitare di dover specificare le coordinate in cui posizionare i controlli. Grazie alle classi base è inoltre possibile costruire controlli personalizzati particolarmente attrattivi nel mondo embedded, specie se combinati, ad esempio, con un touch-screen, implementato nativamente dalla versione 4.0 del .NET Micro Framework.

L’ambiente di sviluppo e l'emulatore

Uno degli aspetti che in maniera più evidente caratterizza l'esperienza di sviluppo di firmware con .NET Micro Framework è senz'altro quello inerente l'ambiente di sviluppo utilizzato. A differenza di quanto avviene per molte case produttrici di sistemi per la programmazione di microcontrollori, Microsoft ha scelto di realizzare il kit di sviluppo per .NET Micro Framework come "plug-in" del noto Visual Studio. In particolare, la versione più recente del .NET Micro Framework (la 4.0), prevede l'installazione come estensione di Visual Studio 2008. E' inoltre imminente la release 4.1 che consentirà lo sviluppo all'interno del nuovo Visual Studio 2010. Vale la pena di sottolineare che il .NET Micro Framework SDK può essere installato all'interno di tutte le versioni di Visual Studio ma anche come plug-in di Visual C# Express Edition, il "fratello minore" gratuito di Visual Studio, dedicato allo sviluppo di applicazioni scritte con il linguaggio C#, scaricabile liberamente da http://www.microsoft.com/express/.

Gli elementi di Visual Studio con cui lo sviluppatore di applicazioni .NET Micro Framework si confronta sono sommariamente l'editor, l'emulatore e il debugger.

L'editor permette la scrittura di codice in linguaggio C# con un notevole supporto da parte dell'ambiente: indentazione automatica, autocompletamento, suggerimenti sulle modalità di invocazione, cross-reference di tutti gli oggetti definiti o utilizzati e molto altro ancora.

L'emulatore integrato consente di verificare il funzionamento del proprio firmware pur senza disporre del dispositivo reale. Le varie periferiche del dispositivo vengono simulate attraverso l'utilizzo di specifici oggetti software che in molti casi possono velocizzare le fasi di sviluppo più slegate delle peculiarità del dispositivo hardware di destinazione, quali quelle relative alla generazione delle interfaccie utenti o all'implementazione di logica di controllo o di elaborazione.

Il debugger, utilizzabile indifferentemente con l'emulatore o con il microcontrollore hardware, consente di eseguire  il firmware sviluppato in una modalità in cui, eseguendo l'applicazione passo-passo o bloccandola nei punti desiderati tramite dei "breakpoint", è possibile generare dei log di "tracing", visualizzare il valore di variabili anche complesse, seguire il flusso dell'applicazione nonché analizzare l'intero stack delle chiamate al fine di individuare eventuali percorsi "critici".