Chi ci segue sa che in queste pagine a volte ci abbandoniamo ad argomenti apparentemente un po’ off-topics rispetto al mondo dello sviluppo basato su .NET Micro Framework e questo post rappresenta senz’altro una di quelle volte. Ciònondimeno, ci siamo proposti di valutare in maniera quanto più oggettiva possibile la scheda RaspberryPI come possibile alternativa per lo sviluppo di applicazioni embedded basate su stack .NET.
Per chi non conosce il RaspberryPI, si tratta del risultato di una straordinaria operazione ingegneristica e commerciale che ha portato una fino ad allora sconosciuta piccola compagnia Inglese a raggiungere, nei primi mesi dopo la “collocazione” sul mercato, lo spaventoso ritmo di vendite di 700 pezzi al secondo (http://www.thetechlabs.com/tech-news/raspberry-pi-sells-out-immediately/)!!! L’esemplare in nostro possesso ci è stato infatti consegnato dopo quasi 14 settimane dall’ordine…
Il motivo di tale successo è ovviamente dovuto al costo, veramente ridottissimo, di quello che il marketing pubblicizza come un personal computer ma che ad essere onesti andrebbe pubblicizzato semmai come una scheda a microcontrollore basata su core ARM11 con RAM (256MB) e GPU integrati. Vale la pena di ricordare che esistono al momento due modelli di RaspberryPI: il modello A costa 25 dollari ma non ha un’interfaccia di rete, mentre il modello B ne costa 35 ma è dotato anche di transceiver ethernet on-board. Nel nostro caso faremo riferimento a quest’ultimo, decisamente più appetibile come piattaforma per IoT o scenari simili.
Senza addentrarci in una recensione del prodotto, per il quale trovate in rete una quantità mostruosa di informazioni (il costo ridotto ha favorito una straordiaria velocità di diffusione di materiale in rete), vediamo come è possibile allestire una semplice applicazione che utilizzi i GPIO presenti a bordo della scheda, in particolare per quanto riguarda i contatti esposti sul connettore marcato come “P1”.
Il Pinout di tale connettore è illustrato nella figura seguente:
Come si può vedere, sono presenti numerosi pin multi-funzione, utilizzabili a seconda della configurazione come GPIO (anche con supporto agli interrupt, sebbene nel kernel attuale tale funzionalità non sia ancora esposta), PWM o come “endpoint” per comunicazioni I2C, SPI o UART. Proprio in merito a quest’ultima possibilità, vale la pena di notare che il kernel utilizza già a partire dal bootstrap la UART0 come console, il che significa che è possibile interagire con il RaspberryPI anche senza monitor né rete (ad esempio proprio per configurare quest’ultima) utilizzando ad esempio un bridge USB/Seriale (SiLabs, FTDI, Prolific, ecc.) collegato alla coppia di pin P1-08 e P1-10.
Come sistema operativo, nel nostro sistema di test abbiamo avviato il RaspberryPI con la distribuzione Linux più diffusa su questa piattaforma, la versione 6 di Debian (Squeeze). Poiché l’unica periferica di storage di cui è dotato il RasberryPI è un socket per schede SD, è necessario per prima cosa scaricare (via torrent o HTTP) l’immagine del sistema operativo e flasharla (con l’apposita utility) su una SD da 4GB o più.
Una volta configurati i parametri di rete, il layout delle tastiera, gli utenti e tutto quello che serve in generale per personalizzare l’ambiente in cui operare, la nostra sperimentazione si è concentrata sulla possibilità di utilizzare applicazioni .NET, in particolare eseguite all’interno della runtime Mono, per controllare le periferiche esposte sul connettore P1, iniziando per semplicità dalla gestione delle porte di I/O digitali.
L’installazione di tutti i package opzionali del sistema operativo, Mono compreso, può essere effettuata tramite l’utility apt-get, tipicamente mediante un comando del tipo:
sudo apt-get install mono-runtime
Sebbene sia possibile installare in maniera simile anche il package relativo a Monodevelop, vi sconsiglio vivamente dal farlo, dato che le scarse performance del RaspberryPI lo rendono di fatto inutilizzabile. Per sviluppare quindi un’applicazione “managed” per il RaspberryPI dovremo utilizzare Visual Studio o Monodevelop su un PC (Windows o Linux, nel caso di Monodevelop), per poi effettuare il deployment (via SSH, FTP, HTTP, ecc.) sul dispositivo target.
E’ importante sottolineare che nella versione attuale, la runtime Mono di Debian “squeeze” non consente di caricare ed eseguire assembly che dipendono dalla versione 4.0 della CLR, limitandosi alla compatibilità con .NET 2.0 (il che significa anche 3.0 e 3.5, dato che queste due versioni del framework .NET condividono la medesima struttura degli assembly .NET 2.0).
Se volete effettuare il debugging dell’applicazione direttamente sul RaspberryPI, dovrete necessariamente utilizzare Monodevelop (sempre su PC, per intenderci). Esiste infatti in alternativa la possibilità di utilizzare uno straordinario add-in per Visual Studio in grado di fare remote debugging su una macchina target anche con architettura differente (ad esempio debugger x86 con target ARM), ma questo richiede che sul target sia in esecuzione un servizio (monotools-server) ad oggi non disponibile su Debian 6 (se qualcuno dovesse scoprire il contrario è pregato di farmelo sapere!). La cosa curiosa è che i lsupporto epr il debugging remoto tramite “Soft Debugger” (che permette quindi l’indipendenza dall’architettura hardware effettiva del target) è disponibile in Monodevelop solo se prima di avviarlo settate una variabile d’ambiente specifica, tramite la seguente riga di comando (ad es. in un prompt dei comandi dal quale poi lancerete monodevelop):
set MONODEVELOP_SDB_TEST=1
In questo caso, all’interno del menu Run apparirà una voce nuova che permetterà di stabilire una connessione via TCP con il target in una delle due modalità Listen o Connect: nel primo caso il debugger esporrà un socket server al quale il client potrà connettersi mentre nel secondo i ruoli saranno invertiti. Dopo un po’ di sperimentazione siamo arrivati però alla conclusione che solo il modello in cui il debugger è client (quindi utilizzando il pulsante “Connect”, per intenderci) sembra funzionare correttamente; nell’altro caso infatti sembra che ci sia un problema nel caricamento dei simboli di debug che impedisce ad esempio di interropmpere l’esecuzione del programma in corrispondenza di un breakpoint.
Sebbene come già detto potete utilizzare indifferentemente Visual Studio o Monodevelop per compilare l’applicazione da lanciare sul RaspberryPI, va specificato che per il debugging (locale o remoto che sia) è INDISPENSABILE che i simboli di debug generati dalla compilazione siano in formato “mono” anziché “visual c++”, ossia MDB anziché PDB. Poiché però purtroppo sembra che su Windows non sia possibile (anche in questo caso se scoprite che lo è vi prego di farmelo sapere!) configurare Monodevelop in modo tale da utilizzare il compilatore Mono (MCS) anziché quello Microsoft (parte del Framework .NET), le scelte possibili per generare un MDB per il debuggin remoto restano solo 3, tutte abbastanza scomode:
Utilizzare Monodevelop su una macchina Linux: sulla mia VM VirtualBox con Ubuntu le performance lo rendono però praticamente inutilizzabile
Utilizzare l’utility pdb2mdb contenuta nella runtime Mono: non sono riuscito in alcun modo a generare un MDB caricabile poi dal debugger, che si lamenta della mcnata corrispondenza tra eseguibile e MDB
Utilizzare la compilazione invocando il compilatore MCS a riga di comando: unica soluzione effettivamente funzionante, anche se, ribadisco, piuttosto scomoda
Ad ogni modo, una volta trasferiti gli assembly “eseguibili” (exe o dll, per intenderci) e relativi MDB (nel nostro caso quindi generati dalla compilazione via MCS) sul RaspberryPI, è possibile avviare su quest’ultimo il debugger in modalità “Listen/Server”, utilizzando una riga di comando simile alla seguente:
mono –debug –debugger-agent=transport=dt_socket,address=0.0.0.0:12345,server=y myprogram.exe
Avviamo quindi il debugger remoto in Monodevelop tramite la voce di menu “Debug via Soft Debugger”, impostando come indirizzo e porta rispettivamente l’indirizzo IP del RaspberryPI e la porta che avrete scelto per stare in ascolto sul debugger server (12345 nell’esempio sopra): se tutto è andato come doveva vedrete il debugger fermarsi in corrispondenza del primo breakpoint “colpito”. Niente male, eh?
GPIO e i file-system driver
Veniamo ora alla gestione delle periferiche che equipaggiano il RaspberryPI, limitandoci, per questo post, ad affrontare la manipolazione degli ingressi e delle uscite digitali esposte dal connettore P1 di cui sopra. Sebbene il microcontrollore a bordo del RaspberryPI implementi un modello di gestione delle periferiche memory-mapped, consentendo in altri termini di operare sui GPIO (così come su molte altre periferiche simili) leggendo e scrivendo determinate locazioni di memoria (si veda a tal proposito questo post), il modello di interazione basato su un driver a “file-system” incarna a mio avviso la più interessante peculiarità di Linux in ambito “physical-computing”.
Tale modello, molto comune in Linux ma adottato anche in altri sistemi operativi quali Windows (soprattutto CE ma non solo), astrae le periferiche fisiche sotto forma di directory e file, rimandando alle funzionalità standard di interazione con il file-system del sistema operativo la responsabilità di esporre una API facilmente conusmabile da parte delle applicazioni client.
Nel caso dei GPIO, occorre innanzitutto “esportare” (“rendere visibile”, in altri termini) gli I/O di interesse, con un comando shell del tipo (per il GPIO4):
echo “4” > /sys/class/gpio/export
A questo punto “apparirà” come per magia una nuova pseudo-directory denominata “/sys/class/gpio/gpio4”, che conterrà degli pseudo-file quali “direction”, per impostare se il GPIO è un’uscita o un ingresso digitale), “active_low”, per stabilire la corrispondenza tra valore logico (1/0) e tensione di uscita (0-3.3V) e, soprattutto, “value”, che riporta il valore logico corrente sulla porta digitale. Quindi per impostare il GPIO4 come uscita è sufficiente eseguire:
echo “out” > /sys/class/gpio/gpio4/direction
E per attivare/disattivare tale uscita è necessario scrivere un “1” o uno “0” sullo pseudo-file “value”:
echo “1” > /sys/class/gpio/gpio4/value
Analogamente, un ingresso digitale può essere configurato e “valutato” come segue:
echo “out” > /sys/class/gpio/gpio4/direction
cat /sys/class/gpio/gpio4/value
Per effettuare la “rimozione” di un GPIO è sufficiente quindi farne l’unexport come segue:
echo “4” > /sys/class/gpio/unexport
Ricalcando questo modello, è banale realizzare un’applicazione C#/Mono in grado di pilotare ingressi e uscite digitali, come ad esempio in:
using System;
using System.IO;
using System.Threading;
namespace consoleapp_mono
{
class MainClass
{
public static void Main(string[] args)
{
Console.WriteLine(“Hello World, RaspberryPI! ({0})”, System.Environment.MachineName);
echo(“18”, “/sys/class/gpio/export”);
Thread.Sleep(500);
echo(“23”, “/sys/class/gpio/export”);
Thread.Sleep(500);
echo(“out”, “/sys/class/gpio/gpio18/direction”);
echo(“in”, “/sys/class/gpio/gpio23/direction”);
echo(“1”, “/sys/class/gpio/gpio23/active_low”);
while (true)
{
char invalue=cat(“/sys/class/gpio/gpio23/value”)[0];
echo(“1″,”/sys/class/gpio/gpio18/value”);
Thread.Sleep(invalue==’1′ ? 250 : 500);
echo(“0″,”/sys/class/gpio/gpio18/value”);
Thread.Sleep(invalue==’1′ ? 250 : 500);
}
}
static void echo(string message,string path)
{
using (FileStream stm=File.OpenWrite(path))
{
using (StreamWriter writer=new StreamWriter(stm))
{
writer.Write(message);
}
}
}
static string cat(string path)
{
using (FileStream stm=File.OpenRead(path))
{
using (StreamReader reader=new StreamReader(stm))
{
return reader.ReadToEnd();
}
}
}
}
}
RaspberryPI senza rivali?
Mettetela come volete, ma in questo momento non mi viene in mente nessun’altro dispositivo, oltre al RaspberryPI, in grado di coniugare le possibilità di controllo del mondo fisico senza necessità di periferiche esterne con la ricchezza di funzionalità di un sistema operativo potente al punto di gestire in caso di necessità una shell grafica (LXDE). Certo, c’è la BeagleBone, che ha performance probabilmente superiori, ma per quanto economica sia…costa come 3 RaspberryPI!