Comunicazione seriale
Come abbiamo visto nel paragrafo dedicato possiamo utilizzare un cavo USB collegato alla porta seriale del computer per caricare su Arduino i programmi contenti le istruzioni per il suo funzionamento. Possiamo utilizzare questo canale di comunicazione anche per:
inviare dinamicamente valori e comandi dal computer a strumenti attuatori come servomotori o altri dispositivi che possono produrre suono attraverso i loro movimenti fisici o comandi elettrici.
ricevere dinamicamente valori da uno o più sensori per mapparli su di un qualche parametro musicale o sonoro all'interno di un software per la sintesi o elaborazione del suono in tempo reale.
Inviare
I dati sono trasmessi in byte e possono dunque assumere valori compresi tra 0 e 255 espressi in tre diversi formati: integer, floating point o ASCII. Se inviamo un solo valore per volta possiamo utilizzare tutti e tre i formati mentre se vogliamo trasmettere più valori all'interno di un singolo pacchetto di dati possiamo farlo solo alltraverso il formato ASCII.
Singolo valore (int)
Colleghiamo Arduino e predisponiamo un sistema per testare la comunicazione tra i devices facendo accendere e spegnere un led (attuatore) collegato al pin 3 PWM con una resistenza da 220 Ohm.
- SuperCollider.
In SuperCollider possiamo scrivere valori su una porta seriale con l'oggetto SerialPort.
s.boot; SerialPort.closeAll; // Chiude le porte eventualmente aperte SerialPort.devices; // Riporta nella Post window un'Array di porte seriali disponibili // ============================================= // Scegliamo la porta alla quale inviare dati ( p = SerialPort.new( // Assegna una tra le porte disponibili "/dev/cu.usbmodem14201", // Specifica la porta seriale di Arduino 9600, // fissa la stessa baudrate dello sketch di arduino crtscts: true // verifica che ci sia un flusso di dati seriale ); ) // ============================================= // Inviamo i dati ( r = Routine.new({ inf.do({p.put(rrand(1,255).postln); // Scrive valori sulla porta specificata 0.25.wait; p.put(0); 0.25.wait; }) }).reset.play ) r.stop; p.close; // quando si carica uno sketch da arduino IDE dobbiamo chiudere la porta seriale in SuperCollider
Se apriamo il monitor seriale in Arduino si scollega automaticamente la porta in SuperCollider quindi possiamo monitonarne il funzionamento solo attraverso la prototipazione.
- Max.
In Max possiamo utilizzare l'oggetto serial che accetta come argomenti:
- l'indice della porta seriale sulla quale vogliamo scrivere sotto forma di lettera.
- la velocità di trasmissione in baud (deve essere la stessa specificata in Arduino IDE).
Scarica il patch.
Per ottenere l'indice della porta dobbiamo cliccare sul messaggio print e leggere l'elenco di porte disponibili nella Max window. Ovviamente dovremo specificare l'indice della porta che abbiamo scelto in Arduino IDE.
- Arduino IDE.
Scriviamo uno sketch che faccia leggere ad Arduino i valori scritti su una porta seriale da altri software e li mappi sul controllo della tensione di un pin PWM.
Scarica lo sketch Singolo_1.
int val; // variabile void setup() { Serial.begin(9600); // apre la connessiones seriale pinMode(3,OUTPUT); // assegna il PIN PWM } void loop() { if (Serial.available()) { // se c'è un dato da Max o SC val = Serial.read(); // scrivilo nella variabile Serial.print(val); // scrive sulla porta seriale analogWrite(3,val); // accende il led }; } // APRI LA PORTA SERIALE // I numeri sono stampati di fila ma arrivano separati...
- Nella funzione setup inizializziamo il sistema aprendo la connessione seriale e stabiliamo che il pin 3 PWM è in modalità OUTPUT ovvero modificherà la tensione in uscita che servirà a quelche dispositivo attuatore per eseguire un'azione. Il valore 9600 stabilisce la velocità di trasmissione dei dati ed è espresso in baud.
Nella funzione loop scriviamo un'istruzione che dice "se la porta seriale è disponibile":
- leggi i valori in ingresso e assegnali alla variabile val.
- scrivi i valori nel monitor seriale per un eventuale debugging.
- scrivi i valori sotto forma di impulsi elettrici sul pin 3 PWM.
Utilizziamo un pin PWM digitale perchè quelli analogici possono solo leggere valori in entrata.
Per visulizzare il moniotr seriale dobbiamo cliccare sul'icona della lente di ingrandimento in alto a destra.
Carichiamo lo sketch su Arduino.
Una porta seriale può essere aperta su un solo software alla volta, quindi se ad esempio vogliamo scrivere o leggere valori con SuperCollider dovremo prima chiudere la connessione eventuelmente aperta in Max nonchè anche il monitor seriale in Arduino IDE. Lo stesso dicasi se vogliamo utilizzare altri software.
Nella visualizzazione dei valori nel serial monitor possiamo notare che se utilizziamo la sintassi:
Serial.print(val); // senza spazi
i valori sono scritti in un'unica stringa consecutivamente uno dopo l'altro senza alcuno spazio che li separi mentre se sostituiamo la riga di codice conSerial.println(val); // righe separate
ogni valore sarà scritto su una riga diversa facilitandone la lettura. Se infine vogliamo aggiungere commenti possiamo utilizzare la seguente sintassi ricordando che lo scheduling anche in questo software procede dall'alto verso il basso.void loop() { if (Serial.available()) { val = Serial.read(); Serial.print("valore in output: "); // scrive stringhe, "print" non va a capo Serial.println(val); // "println" va a capo... analogWrite(3,val); }; }
Singolo valore (float)
Se vogliamo scrivere sulla porta seriale una cifra decimale (float) dobbiamo necessariamente convertirla in un numero intero (int) compreso tra 0 e 255 nel software che scrive sulla porta per poi trasformarla nuovamente in valore decimale nel software che legge dalla porta.
- SuperCollider.
In SuperCollider dobbiamo sia modificare il range che convertire il tipo di data da float a int invocando il metodo .round sui valori rimappati.
s.boot; SerialPort.closeAll; SerialPort.devices; p = SerialPort("/dev/cu.usbmodem14201", 9600, crtscts:true); ( var w, slid, val=0; w = Window("LED", Rect(100,500,200,100)); slid = Slider(w,Rect(10,10,180,40)); w.front; slid.action_({val = slid.value * 255; // Conversione del range val.round.postln }); // Routine che invia continuamente bit alla porta seriale r = Routine({ inf.do({ p.put(val.round); // float2int 0.05.wait // è necessario un downsampling }) }).reset.play; w.onClose_({r.stop;p.close}) ) r.stop; p.close;
Se apriamo il monitor seriale in Arduino si scollega automaticamente la porta in SuperCollider quindi possiamo monitonarne il funzionamento solo attraverso la prototipazione.
- Max.
Anche in Max dobbiamo adottare la stessa procedura. Scarica il patch.
- Arduino IDE.
Scarica lo sketch Singolo_2.
float val; // variabile float void setup() { Serial.begin(9600); pinMode(3,OUTPUT); } void loop() { if (Serial.available()) { val = Serial.read(); Serial.print("originali: "); Serial.println(val); // stampa originali int analogWrite(3,val); Serial.print("convertiti: "); val = val/255; // ri-mappa in float Serial.println(val); // stampa ri-convertiti }; }
In Arduino IDE i valori sono letti come int ma se poi vogliamo convertirli in float e assegnarli a una variabile (val) dobbiamo specificare che la variabile conterrà questo tipo di data. In questo caso la conversione è solo per la visualizzazione sul monitor seriale.
Singolo valore (ASCII)
Possiamo ottimizzare la trasmissione di dati attraveso la porta seriale convertendo i valori numerici in caratteri ASCII:
In Max possiamo realizzare direttamente questa conversione con l'oggetto atoi. Scarica il patch.
Mentre in SuperCollider dobbiamo effettuare una duplice conversione:
a = 23; // int a = a.asString; // int --> string a = a.ascii; // string --> ASCII
In questa conversione ad ogni singolo numero intero compreso tra 0 e 9 ne corrisponde un altro come possiamo leggere nella tabella.
0 = 48
1 = 49
2 = 50
Fino a quando non si supera la cifra singola (da 0 a 9) non c'è alcun problema nella trasmissione dei dati, ma siccome i valori trasmissibili sono compresi tra 0 e 255 avremo numeri composti da due e tre cifre.
12 = 49 50 (1-2)
31 = 51 49 (3-1)
130 = 49 51 48 (1-3-0)
In questi casi per ogni singolo valore numerico ne saranno trasmessi due (per quelli di due cifre) o tre (per quelli di tre cifre) in modo sequenziale (uno dopo l'altro) generando una stringa ininterrotta di numeri:
.. 48 51 57 55 53 54 49 52 51 48 50 53 49..
rendendone di fatto impossibile la riconversione. Verifichiamolo.
- Scarichiamo e lanciamo il patch 3_ascii_1.maxpat. Apriamo anche la Max window
- Scarichiamo e carichiamo su Arduino lo sketch Ascii_1.
int val; void setup() { Serial.begin(9600); pinMode(3,OUTPUT); } void loop() { if (Serial.available()) { val = Serial.read(); Serial.print("valore ASCII: "); Serial.println(val); analogWrite(3,val); // I valori non corrispondono... }; } // ARRIVANO VALORI ASCII E QUANDO SONO COMPOSTI DA DUE O TRE CIFRE // SONO STAMPATI IN RIGHE SEPARATE
- Apriamo il monitor seriale e inviamo alcuni valori da Max.
Per ovviare a questo inconveniente possiamo aggiungere il valore 13 alla fine di ogni lista che in formato ASCII significa \cr (a capo) separando in questo modo i singoli valori composti da 2 o tre cifre.
12 31 130 1 2 \cr 3 1 \cr 1 3 0 \cr 49 50 13 51 49 13 49 51 48 13
Inoltre dobbiamo sostituire nel codice di Arduino Serial.read() con Serial.parseInt() che legge in maniera corretta questo formato e converte automaticamente il valore in int.
int val; void setup() { Serial.begin(9600); pinMode(3,OUTPUT); } void loop() { if (Serial.available()) { val = Serial.parseInt(); // cerca il primo int // separato da 13 Serial.print("convertito: "); Serial.println(val); analogWrite(3,val); }; } // DOPO 1000ms CHE NON RICEVE DATA RIPORTA '0' // UNA PRIMA SOLUZIONE (C) E' INVIARE CONTINUAMENTE UN DATO // METTENDO UN METRO IN MAX, SPEGNENDO IL METRO SI RESETTA // IL SISTEMA
Per ragioni tecniche Serial.parseInt() scrive automaticamente il valore 0 (Timeout) dopo un secondo dall’ultimo dato ricevuto. Se non vogliamo che questo avvenga nello scorrere del flusso di valori abbiamo due possibilità:
- scrivere continuamente i valori al di sotto del tempo di Timeout ritardandolo di fatto alla fine.
- impostare in Arduino IDE un tempo di timeout molto lungo:
Serial.setTimeout(tempo) // in Millisecondi (1000 di default)
Per verificare:
- Scarichiamo e lanciamo il patch 4_ascii_2.maxpat. Apriamo anche la Max window
- Scarichiamo e carichiamo su Arduino lo sketch Ascii_2.
- Apriamo il monitor seriale e inviamo alcuni valori da Max.
Se invece volessimo utilizzare SuperCollider il codice è il seguente.
s.boot; SerialPort.devices; SerialPort.closeAll; p = SerialPort.new("/dev/cu.usbmodem14201", 9600, crtscts:true); a = rand(255).postln; // int a = a.asString; // int --> string a = a.ascii; // string --> ASCII // Routine che invia continuamente bit alla porta seriale ( a = 150.asString.ascii ++ 13; // converte, inserisce \cr (13) e crea un Int8Array r = Routine({ inf.do({ p.putAll(a); // Scrive sulla porta 0.05.wait // è necessario un downsampling }) }).reset.play; ) a = 0.asString.ascii ++ 13; a = 20.asString.ascii ++ 13; a = 100.asString.ascii ++ 13; a = 255.asString.ascii ++ 13; ( a = rand(255).asString.ascii ++ 13; a.postln; ) r.stop; p.close;
Se apriamo il monitor seriale in Arduino si scollega automaticamente la porta in SuperCollider quindi possiamo monitonarne il funzionamento solo attraverso la prototipazione.
Più valori (ASCII)
Se volessimo inviare più valori contemporaneamente a uno o più dispositivi attuatori come i led nella configurazione illustrata nell'immagine possiamo impacchettarli in una lista o in un Array nel software che utilizziamo per controllarli (in questo caso Max o SuperCollider).
- Scarichiamo e lanciamo il patch 5_ascii_3.maxpat. Apriamo anche la Max window
- Scarichiamo e carichiamo su Arduino lo sketch Ascii_3.
int val1; // una variabile per item int val2; int val3; int val4; int val5; int val6; void setup() { Serial.begin(9600); Serial.setTimeout(1000); pinMode(3,OUTPUT); // dichiara tutti i PINs PWM pinMode(5,OUTPUT); pinMode(6,OUTPUT); pinMode(9,OUTPUT); pinMode(10,OUTPUT); pinMode(11,OUTPUT); } void loop() { if (Serial.available()) { val1 = Serial.parseInt(); // legge gli interi in sequenza val2 = Serial.parseInt(); // li assegna a una variabile val3 = Serial.parseInt(); val4 = Serial.parseInt(); val5 = Serial.parseInt(); val6 = Serial.parseInt(); analogWrite(3,val1); // scrive sui PINs PWM analogWrite(5,val2); analogWrite(6,val3); analogWrite(9,val4); analogWrite(10,val5); analogWrite(11,val6); Serial.print(val1); // stampa Serial.print(" "); // spazio Serial.print(val2); Serial.print(" "); Serial.print(val3); Serial.print(" "); Serial.print(val4); Serial.print(" "); Serial.print(val5); Serial.print(" "); Serial.println(val6); // fine riga } }
Un modo più elegante di realizzare il codice precedente.
int pins[] = {3,5,6,9,10,11}; // Array con i numeri dei PINs PWM int vals[] = {0,0,0,0,0,0}; // Array con i valori di default int pinNum = 6; void setup() { Serial.begin(9600); Serial.setTimeout(1000); for(int i = 0; i < pinNum; i++){ // dichiara tutti i PINs PWM pinMode(pins[i],OUTPUT); } } void loop() { if (Serial.available()) { for(int i = 0; i < pinNum; i++){ // loop nel loop vals[i] = Serial.parseInt(); // cambia i valori // dell'array analogWrite(pins[i],vals[i]); // li scrive sui // PINs PWM Serial.print(vals[i]); // stampa Serial.print(" "); // spazio tra i numeri } Serial.println(); // fine riga } }
- Apriamo il monitor seriale e inviamo alcuni valori da Max. Ogni number box controllerà la luminosità di un led differente.
Se invece vogliamo inviare valori da SuperCollider:
s.boot; SerialPort.devices; SerialPort.closeAll; p = SerialPort.new("/dev/cu.usbmodem14201", 9600, crtscts:true); // Routine che invia continuamente bit alla porta seriale ( a = [0,0,0,0,0,0].asString.ascii ++ 13; // Converte, inserisce \cr (13) e crea un Int8Array r = Routine({ inf.do({ p.putAll(a); // Scrive sulla porta 0.05.wait // è necessario un downsampling }) }).reset.play; ) r.stop;d.stop;a = [0,0,0,0,0,0].asString.ascii ++ 13; // Stop e reset p.close; a = [ 0, 0, 0, 0, 0, 0].asString.ascii ++ 13; // Crea un Array con \cr a = [ 0, 20, 40, 60, 80,100].asString.ascii ++ 13; a = [255, 0,255, 0,255, 0].asString.ascii ++ 13; a = [ 0,255, 0,255, 0,255].asString.ascii ++ 13; a = [255,180,120, 80, 40, 0].asString.ascii ++ 13; a = Array.rand(6,0,255).asString.ascii ++ 13;
Se apriamo il monitor seriale in Arduino si scollega automaticamente la porta in SuperCollider quindi possiamo monitonarne il funzionamento solo attraverso la prototipazione.
Ricevere
Se vogliamo ricevere dati da uno o più sensori analogici possiamo collegarlo a uno dei sei pins dedicati di Arduino. Questo tipo di sensori in genere ha tre piedini o tre pins. Dovremo collegre il filo positivo e negativo (rosso e nero) a due di essi per fornire al sensore la corrente necessaria al funzionamento, mentre sul terzo ci sarà una tensione che cambierà dinamicamente al variare del parametro fisico monitorato fornendoci i valori da mappare (debitamente riscalati) su uno o più parametri di un algoritmo di sintesi o di elaborazione del suono in Max o SuperCollider.
Singolo sensore analogico
Nei prossimi esempi utilizzeremo un sensore di prossimità (4-30cm) a raggi infrarossi Sharp che necessita di una tensione di alimentazione compresa tra 4.5V e 5.5V (non dovremo mettere resistenze nel circuito), fornisce un segnale di uscita compreso tra 3.1V (4cm) e 0.3V (30cm) e consuma circa 33mA di corrente.
Verifichiamo dal datasheet del sensore l'ordine in cui sono posizionati i collegamenti.
Arduino IDE
Scarichiamo e carichiamo su Arduino lo sketch Sharp_1. Il codice è semplice in quanto dobbiamo solo definire su quale pin analogico abbiamo collegato il filo della tensione in uscita e aggiornare continuamente una variabile con i valori che arrivano.
const int pinAn = A0; // Dichiara il pin analogico dal quale // leggere i valori di tensione int val; // variabile che conterrà i valori void setup() { Serial.begin(9600); pinMode(pinAn, INPUT); // può essere omesso per i pin analogici } void loop() { val = analogRead(pinAn); // legge i valori dal pin Serial.flush(); // "pulisce" la memoria della // porta seriale Serial.println(val); // scrive sulla porta seriale // in valori ASCII separando i singoli // numeri dal carattere 10 (newline) delay(50); }
Median Filter
Se apriamo il monitor seriale possiamo leggere i valori in ingresso e capire entro quale range lavora il sensore.
Osservando attentamente i valori possiamo notare come si verifichino frequenti "sfarfallii" ovvero all'intero della sequenza numerica (numeric stream) ci siano numeri molto distanti dal valore che li precede e quello che li segue (rumore digitale):
...87 88 89 90 456 95 96 97 23 75 74 73 72 567...
rendendo di fatto problematico il mappaggio di questi valori su un parametro di controllo di un segnale audio.
Per eliminare questi valori e rendere più continuo il segnale possiamo applicare un filtro mediano (median filter) direttamente in Arduino (potremmo applicarlo anche in Max o SuperCollider) ma essendo una problematica esterna a questi software è preferibile risolverla all'origine).
A questo link possiamo scaricare una libreria di Arduino che fornisce diversi tipi di filtri dati tra i quali un filtro mediano. Per utilizzare una libreria in uno sketch dobbiamo:
Scaricare il file .zip
Andare in Sketch --> #include libreria --> Aggiungi libreria da file .ZIP... e scegliere la libreria.
Specificare nel codice l'utilizzo della libreria (scarica lo sketch Sharp_2).
#include
medianFilter Filter; const int pinAn = A0; int val; int valFilt; void setup() { Serial.begin(9600); Filter.begin(); } void loop() { val = analogRead(pinAn); valFilt = Filter.run(val); Serial.flush(); Serial.println(valFilt); delay(50); }
Riscalaggio e clipping
A questo punto possiamo verificare entro quale range sono compresi i valori per poi poterli eventualmente riscalare in modo corretto.
Il costruttore dichiara che nell'ambito entro il quale il sensore risponde linearmente vanno da 80 (30cm) a 530 (4cm) e che possiamo effettuare una conversione di questi in centimetri attraverso la formula:
Distanza (cm) = 2076 / (Valore_sensore - 11)
Possiamo semplicemente sostituire la seguente riga di codice:
Serial.println(2076 / (valFilt-11));
Notiamo che però anche i valori che oltrepassano i due limiti continuano a comparire. Possiamo effettuare un clipping del range con la funzione constrain(val,min,max);
Serial.println(constrain(valFilt,80,530));
Per riscalare i valori possiamo anche utilizzare la funzione map(val,minIn,maxIn,minOut,maxOut); che ci permette di ottenere facilmente valori compresi tra due valori limite qualsiasi. Nel nostro caso un ambito che può consentirci ulteriori riscalaggi in Max o SuperCollider potrebbe essere quello tra zero e mille (possiamo utilizzare solo numeri interi).
Serial.println(map(constrain(valFilt,80,530),80,530,1000,0));
Max
Se vogliamo utilizzare in Max i valori scritti sulla porta seriale da Arduino dobbiamo convertirli in int (per consentire la trasmissione a 8 bit sono stati scritti come valori ASCII). Scarichiamo e lanciamo il patch 6_analog_1.maxpat.
SuperCollider
Se vogliamo utilizzare in SuperCollider i valori scritti sulla porta seriale da Arduino dobbiamo utilizzare il metodo p.read che legge i valori dalla porta seriale specificata tra quelle presenti posto all'interno di una Routine infinita. I dettagli sono commentati nel codice. Ricodiamoci di uscire eventualmente da Max o dal monitor seriale di Arduino altrimenti saranno postati errori.
SerialPort.devices; p = SerialPort("/dev/tty.usbmodem14201", 9600, crtscts:true); ( r = Routine({ var byte, str, val; inf.do{|i| if(p.read == 10, // se il valore in ingresso è 10 (newline) {str = ""; // crea una stringa vuota e, while({byte = p.read; byte !=13 }, // fino a che il valore in // ingresso è diverso da // 13 (return) {str = str++byte.asAscii}); // riempila con i valori in // ingresso convertiti in // ASCII val = str.asInteger; // assegna i valori convertiti // in int alla variabile // locale 'val' ("valore in ingresso:"+val).postln; // stampa }); }; }).play; ) r.stop; p.close;
Esempio wa wa (Max)
In questo esempio il sensore controlla dinamicamente la frequenza di taglio di un filtro passa basso.
- Scarichiamo il file mph.aif
- Scarichiamo e lanciamo il patch 7_analog_2.maxpat
Ricordiamo che l'audio file e il patch devono essere nella stessa cartella.
Esempio scratch (SuperCollider)
In questo esempio il sensore controlla dinamicamente un puntatore che legge un audio file.
SerialPort.devices; p = SerialPort("/dev/tty.usbmodem14201", 9600, crtscts:true); // Connette la // porta Seriale s.boot; // Boot Server Audio b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav"); // Crea un Buffer e // carica un File // ------------------------------------ Synthdef e Synth ( SynthDef(\scrat,{arg pos,smooTh; Out.ar(0, BufRd.ar(b.numChannels, b.bufnum, VarLag.ar(K2A.ar(pos),smooTh,warp:\lin) ); ) }).add; {a = Synth(\scrat,[\pos,0,\smooTh,0.2])}.defer(0.3); ) // ------------------------------------ Legge, converte, riscala e invia i valori ( c = b.numFrames; // lunghezza in frames del Buffer r = Routine({ var byte, str, val; inf.do{|i| if(p.read == 10, {str = ""; while({byte = p.read; byte !=13 }, {str = str++byte.asAscii} ); val = str.asInteger; val = val.linlin(0,1000,0,c); // riscala lineare val.postln; // stampa a.set(\pos,val); // invia al Synth }); }; }).reset.play; ) r.stop; // ferma la Routine s.freeAll; // distrugge Synth e Buffer dal Server p.close; // chiude la porta seriale
Singolo sensore digitale
Nei prossimi esempi utilizzeremo un sensore di inclinazione (tilt sensor) che come si evince dal suo spreadsheet necessita di una tensione di alimentazione al di sotto dei 12V, consuma circa 20mA di corrente e necessita di una resistenza di 10 KOhm .
Questo sensore funziona come un pulsante (switch): al suo interno c'è una pallina che a seconda dell'inclinazione chiude uno tra due contatti riportando hight (1) oppure low (0).
Arduino IDE
Scarichiamo e carichiamo su Arduino lo sketch Inclinazione_1.zip. Il codice è semplice in quanto dobbiamo solo definire a quale pin digitale è collegato il filo della tensione in uscita dal sensore e aggiornare continuamente una variabile con i valori che arrivano.
const int Tilt = 7; // costante pin ingresso int val; // variabile val void setup() { pinMode(Tilt, INPUT); Serial.begin(9600); } void loop() { val = digitalRead(Tilt); // legge il valore Serial.print(val); // lo stampa delay(50); // downsampling }
La pallina all'interno del sensore è molto sensibile e i dati in ingresso possono sfarfallare esattamente come avviene con i sensori analogici. In questo caso per risolvere il problema e stabilizzare il data stream al posto di un median filter dobbiamo adottare tecniche di debouncing ovvero effettuare una sorta di sample and hold dei valori in ingresso.
Debouncing
Scarichiamo e carichiamo su Arduino lo sketch Inclinazione_2.zip.
Il codice seguente illustra come realizzare un debouncing specificando una zona morta temporale all'interno della quale i cambiamenti non sono presi in considerazione. Per verificare apriamo il monitor seriale.
const int Tilt = 7; // costante pin ingresso int val; // variabile valore attuale int prev = HIGH; // valore precedente unsigned long delta = 0; // tempo delta tra cambi stato unsigned long zonaM = 50; // zona morta in ms; void setup() { pinMode(Tilt, INPUT); Serial.begin(9600); } void loop() { val = digitalRead(Tilt); // legge il valore // se è diverso dal precedente aggiorna il tempo delta if (val != prev) {delta = millis();} // se è > della zona morta cambia stato if ((millis() - delta) > zonaM) {Serial.print(val);} // resetta la variabile ad ogni giro prev = val; delay(50); // downsampling }
Cercare nel Reference di Arduino IDE gli oggetti che non conosciamo.
Max
Scarichiamo e lanciamo il patch 8_digital_1.maxpat.
Il patch di Max illustra i due possibili modi di utilizzo dei dati provenienti da un sensore digitale:
- Come segnale continuo (a destra - in questo caso mappato sull'ampiezza di una sinusoide).
- Come trigger di un evento qualsiasi (a sinistra - filtrando i dati ridondanti con l'oggetto change).
SuperCollider
Anche in SuperCollider possiamo utilizzare due modalità.
SerialPort.devices; p = SerialPort("/dev/tty.usbmodem14201", 9600, crtscts:true); // ----------------------- Segnale continuo ( r = Routine({var str, val; inf.do{ str = ""; str = str++p.read.asAscii; val = str.asInteger; val.postln; } }).play; ) r.stop; p.close; // ----------------------- Trigger ( r = Routine({var str, val, prec=0; inf.do{ str = ""; str = str++p.read.asAscii; val = str.asInteger; if((val != prec), // se diverso dal precedente {val.postln; // esegue prec = val }); // riassegna 'prec' all'ultimo } }).play; ) r.stop; p.close;
Esempio trigger (Max)
Scarichiamo e lanciamo il patch 8_digital_1.maxpat.
Esempio gate (SuperCollider)
s.boot; SerialPort.devices; ( p = SerialPort("/dev/tty.usbmodem14201", 9600, crtscts:true); b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav"); SynthDef(\grain, {arg gate=0; var trate, dur, sig; trate = LFNoise1.kr(0.8).range(2,120); dur = 1.2 / trate; sig = TGrains.ar(2, Impulse.ar(trate), b, (1.2 ** WhiteNoise.kr(3).round(1)), LFNoise1.kr(3).range(0,BufDur.kr(b)), dur, WhiteNoise.kr(0.6), 0.1); Out.ar(0,sig * gate.lag(0.2)) }).add; ) ( a = Synth(\grain); r = Routine({var str, val, prec=0; inf.do{ str = ""; str = str++p.read.asAscii; val = str.asInteger; if((val != prec), {a.set(\gate,abs(1-val)); abs(1-val).postln; prec = val }); } }).play; ) r.stop; p.close;
Singolo sensore PWM
Nei prossimi esempi utilizzeremo un sensore di prossimità a ultrasuoni (HC-SR04) (2-400cm) che come si evince dal suo spreadsheet necessita di una tensione di alimentazione di 5V, consuma circa 15mA di corrente e il suo funzionamento è ottimale all'interno di un angolo di 30°.
Questo sensore utilizza due trasduttori ad ultrasuoni. Un impulso a 5 volt di almeno 10 μS (microsecondi) di durata viene applicato al pin Trigger generando nel primo trasduttore (Trig) un treno di 8 impulsi ultrasonici a 40 KHz che si allontanano dal sensore viaggiando nell’aria circostante. Il segnale sul Pin Echo intanto diventa alto ed inizia la registrazione del tempo di ritorno in attesa dell’onda riflessa. Se l’impulso non viene riflesso il segnale su Echo torna basso dopo 38 ms (millisecondi) e va interpretato come assenza di ostacolo, altrimenti si calcola il tempo che ha impiegato nel suo tragitto che corrisponde alla distanza (tempi maggiori corrispondono a distanze maggiori).
Arduino IDE
Scarichiamo e carichiamo su Arduino lo sketch Pwm_1.zip. In rete possiamo trovare numerosi tutorials sulla programmazione di Arduino per questo sensore, in questo caso ho deciso di utilizzare una libreria dedicata che si chiama NewPing. Possiamo analizzare direttamente il codice.
#include// Libreria per il sensore #include // libreria per il median filter medianFilter Filter; #define TRIGGER_PIN 5 // Pin PWM del trigger #define ECHO_PIN 6 // Pin PWM dell'echo #define MAX_DISTANCE 100 // Distanza massima in cm. NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); int SetDistance = 0; int ValueDist = 0; int valFilt; void setup() { Serial.begin(9600); Filter.begin(); } void loop() { delay(100); // Aspetta 100ms tra i pings. // (29ms minimo) unsigned int uS = sonar.ping(); // Manda il pings, rileva il // ritorno in microsecondi (uS). ValueDist = uS / US_ROUNDTRIP_CM; // Calcola la distanza (converte // il tempo di ping in distanza // in cm) valFilt = Filter.run(ValueDist); // Valori filtrati Serial.println(valFilt); // Scrive sulla porta seriale }
Aggiungiamo anche in questo caso un median filter per ottenere un flusso di valori più preciso e stabile. Per verificare il funzionamento del sensore possiamo aprire il monitor seriale ricordando che il range ottimale è compreso tra 2 e 100 centimetri.
Sample and Hold
Come possiamo notare dalla lettura del monitor seriale nello scorrere del flusso di dati si verificano delle imprecisioni quando l'onda ultrasonica riflessa non viene rilevata magari anche solo per un istante. Questo inconveniente genera uno o più zeri consecutivi. Per ovviare a questo inconveniente dobbiamo programmare un sample and hold che entra in azione ogni qualvolta il valore è 0.
Il concetto di base è: se il valore è uguale a zero riporta il valore precedente.
La realizzazione di questa tecnica in Max o in SuperCollider è illustrata all'interno dei codici seguenti.
Max
Scarichiamo e lanciamo il patch 10_pwm_1.maxpat.
Il patch è simile ai precedenti. Ci sono però tre accorgimenti che aiutano a stabilizzare ulteriormente il flusso di valori:
- I valori in ingresso subiscono un sottocampionamento.
- Inseriamo un ulteriore filtro (Sample and Hold) per eliminare eventuali "0".
- Realizziamo un'interpolazione lineare per "arrotondare" i valori (più alto sarà lo smoothing time meno sensibile sarà il sensore).
SuperCollider
Anche in SuperCollider dobbiamo realizzare gli stessi accorgimenti adottati in Max per stabilizzare il flusso di dati. I passaggi sno gli stessi ma come possiamo osservare da un'analisi del codice per effettuare uno smoothing lineare dobbiamo trasformare i valori in segnale di controllo
SerialPort.devices; ( p = SerialPort("/dev/tty.usbmodem14201", 9600, crtscts:true); d = {arg val=0; VarLag.kr(val,0.8)}.scope; // Smoothing r = Routine({ var byte, str, val, prec=0; inf.do{|i| if(p.read == 10, {str = ""; while({byte = p.read; byte !=13 }, {str = str++byte.asAscii}); val = str.asInteger; if((val == 0), {val = prec}, {prec = val }); // Filtra gli 0 val.postln; // Stampa d.set(\val,val.linlin(0,100,0,1)) // Invia allo smoothing }); 0.1.wait; // Downsampling }; }).play; ) ( r.stop; d.free; p.close; )
Crossfades
Scarichiamo e lanciamo il patch 11_pwm_2.maxpat.
In questo caso utilizziamo il flusso di dati in entrata e un tempo di interpolazione lungo per realizzare un crossfade dinamico tra due segnali differenti.
Soglie e triggers
In questo esempio vediamo come splittare un segnale continuo come quello che proviene da un sensore di prossimità in diversi ambiti numerici per generare trigger ad ogni passaggio da un ambito all'altro.
s.boot; s.plotTree; SerialPort.devices; ( p = SerialPort("/dev/tty.usbmodem14201", 9600, crtscts:true); Buffer.freeAll; // Elimina eventuali Buffers allocati in precedenza b = 3.collect{Buffer.read(s, // Genera 3 Buffers diversi Platform.resourceDir +/+ "sounds/a11wlk01.wav", rand(44100*4), // Inizio rrand(4410,88200))}; // Durata SynthDef(\player, {arg buf=0,trig=0; var sig, env; sig = PlayBuf.ar(1,buf,1,trig,0); env = EnvGen.kr(Env.asr(Rand(0.1,0.3),1,0.1),trig); Out.ar(0,sig*env) }).add; ) ( a = Synth(\player, [\buf,b[0]]); // Player Buffer 0 c = Synth(\player, [\buf,b[1]]); // Player Buffer 1 d = Synth(\player, [\buf,b[2]]); // Player Buffer 2 r = Routine({ var byte, str, val=0, prec=0; inf.do{|i| if(p.read == 10, {str = ""; while({byte = p.read; byte !=13 }, {str = str++byte.asAscii}); val = str.asInteger; if((val == 0), {val = prec}, {prec = val }); // Filtra gli 0 val.postln}); case {(val > 0) && (val < 10)} {a.set(\trig,1); // Tra 0 e 10 c.set(\trig,0); d.set(\trig,0)} {(val > 10) && (val < 20)} {a.set(\trig,0); // Tra 10 e 20 c.set(\trig,1); d.set(\trig,0)} {(val > 20) && (val < 30)} {a.set(\trig,0); // Tra 20 e 30 c.set(\trig,0); d.set(\trig,1)}; 0.1.wait; // Downsampling }; }).play; ) ( r.stop; b.free; a.free;c.free;d.free; p.close; )