Sensori e microcontrollori
In precedenza abbiamo introdotto i microcontrollori descrivendoli come oggetti tecnologici molto interessanti, evidenziando anche la loro attitudine ad interagire col mondo reale che ci circonda. Ricordiamoci che la costituzione strutturale di un μC può essere ricondotta a tre parti principali:
• Il core (nucleo) del processore : è l'elaboratore, il cervello del
μC.
• La memoria di programma : formata da istruzioni, in qualche modo
paragonabile alla memoria del nostro cervello.
• Gli input ed output : utilizzati per acquisire i dati esterni o per
trasferire segnali verso l'esterno.
Gli input e output (I/O) sono di fatto i punti attraverso i quali i μC comunicano con il mondo esterno. I pin di input possono essere collegati a periferiche di input come
• pulsanti (interruttori)
• potenziometri
• fotoresistenze
mentre le periferiche di output sono ad esempio
• cicalini
• led
• motori
In genere, si utilizza il termine trasduttori (o sensori) per indicare le periferiche di input, mentre le periferiche di output vengono talvolta chiamate attuatori.
Per effettuare alcuni esempi pratici abbiamo fatto riferimento alla scheda
a microcontrollore Arduino
Uno descrivendone gli aspetti fondamentali, senza però, mettere in evidenza
quella che può essere considerata una differenza fondamentale tra un computer
ed una scheda μC; cioè l'obbligatoria presenza su quest'ultima di un
convertitore analogico-digitale.
Infatti, il nucleo del μC, può comprendere solo codice binario composto
da sequenze di 0 e 1. Quello che deve essere fatto capire al nucleo del
μC deve essere tradotto per forza in binario, che sia la temperatura
dell'acqua oppure la luminosità di una stanza.
Per questo motivo all'interno di un μC deve esserci un convertitore analogico-digitale direttamente collegato ai pin analogici A0÷A5 così come si vede sulla scheda di Arduino. Questi pin leggono dei valori di tensione analogica e la convertono in un valore digitale (numerico).
Un μC riconosce come 1 una tensione alta (HIGH) di solito 5V e come 0 una tensione bassa (LOW); in questo caso coincidente con gli 0V.
Oltre ad una memoria e ad una ALU (Arithmetic Logic Unit) nel nucleo di un microcontrollore sono installati dei registri interni (come un qualsiasi microprocessore). Essi hanno lo scopo di ospitare gli operandi che devono essere sottoposti alle operazioni aritmetiche eseguite dalla ALU. Alcuni registri possono essere collegati direttamente ai pin di I/O oppure lalla porta seriale. Ad esempio, i registri collegati ai pin di Arduino, sono di tre tipi:
• un primo registro serve ad fornire la direzione dei dati che significa
input o output, nel codice può essere visto perché utilizzato nella dichiarazione
pinMode();
• un secondo registro è detto PORT. Quando un pin viene configurato
di output, si usa il registro PORT attraverso la funzione digitalWrite();
• un terzo registro viene chiamato registro PIN. Quando un pin è stato
configurato per leggere valori, per farlo utilizza il registro PIN. Questo
registro è quello che viene coinvolto nell'esecuzione dell'istruzione pinRead().
Dopo queste osservazioni, si deduce che l'abbinamento tra sensori di input e convertitore analogico-digitale rende i μC meglio predisposti all'acquisizione e all'elaborazione di dati fisici, provenienti dall'esterno, rispetto ai normali computer.
Per quanto è stato già visto negli esempi fatti in precedenza, queste istruzioni vengono usate sempre durante l'esecuzione del codice programma che, ricordiamo, è sempre strutturato in due parti fontamentali; la funzione
void setup(){
//codice da eseguire solo una
volta
}
e la successiva funzione
void loop(){
//codice da eseguire ciclicamente
}
Simboli
Nella scrittura di uno sketch il simbolo principale è
; (punto e virgola)
esso viene usato come terminatore di
un'istruzione. Ciascuna istruzione termina con un punto e virgola. E' possibile
mettere anche più di una istruzione per linea a patto di separarle con il
punto e vigola.
Un'altra simbologia molto importante è costituita dalle
{ } (parentesi graffe)
Le parentesi graffe si usano per contrassegnare blocchi di istruzioni; sono particolarmente consigliate nell'esecuzione dei cicli oppure nella scrittura delle funzioni ad esempio
void loop(){
Serial.println('hallo');
}
Commenti
Si tratta di porzioni di testo che vengono ignorate dal processore ma che sono molto utili per ricordare a se stessi (e agli altri) a cosa serve una determinata parte di codice. I commenti possono essere di due tipi:
// (commenti in singola linea)
/*
...
*/ (commenti multilinea)
Come si vede i simboli usati per commentare il codice sono gli stessi usati in C, Java, JavaScript.
Costanti
Arduino comprende una serie di parole chiave predefinite con valori speciali. HIGH e LOW si usano, ad esempio, quando si vuole accendere o spegnere un pin. INPUT ed OUTPUT si usano invece per impostare un determinato pin come input oppure output. Come suggerisce il nome , le costanti, una volta fissate, non possono essere mai essere cambiate durante l'esecuzione di un programma.
Variabili
Le variabili sono aree di memoria dotate di un nome dove possono essere conservati i dati. Nel codice sorgente costituito dallo sketch, è possibile manipolare questi dati, riferendosi ad essitramite il nome della variabile. Come suggerisce il nome , le variabili possono essere cambiate tutte le volte che si vuole durante l'esecuzione di un programma. Dato che Arduino è un tipo di controllore molto semplice, quando si dichiara una variabile si deve sempre specificare anche il tipo , cioè, la dimensione del valore che si vuole conservare. Ecco i tipo di dati disponibili:
boolean - può avere uno dei due valori : true o false;
char - contiene un solo carattere, come per esempio 'A'. Un tipo di variabile char occupa, come nel caso dei computer, 1 byte di memoria;
byte - contiene un numero compreso tra 0 e 255;
int - questo tipo di dato utilizza 2 byte per rappresentare un numero compreso tra -32.768 e +32.767; è il tipo di dato che viene usato più spesso;
unsigned int - come nel caso del tipo int, utilizza 2 byte ma il prefisso unsigned significa che non può contenere numeri negativi, quindi il suo intervallo va da 0 a 65.535;
long - la sua dimensione è doppia rispetto al tipo int, dunque occupa 4 byte e contiene numeri compresi tra -2.147.483.648 e 2.147.483.647;
unsigned long - è la versione senza numeri negativi di long, dunque può contenere numeri interi che vanno da 0 a 2.294.967.295;
float - è un tipo di dato piuttosto grande e può contenere valori in virgola mobile, un modo come un altro per indicare cifre seguite da decimali. Ciascuna variabile float occupa 4 byte;
double - numeri in virgola mobile a doppia precisione con un valore massimo pari a 1,7976931348623157×10308. Un tipo di dato molto oneroso da mantenere in memoria.
string - una serie di caratteri ASCII che si usano per conservare informazioni testuali. Le stringhe possono essere usate per inviare messaggi attraverso la porta seriale oppure per essere mostrati su un monitor LCD. La memoria usata consiste in 1 byte per carattere. La sua forma di dichiarazione è simile a quella usata in liguaggio C.
char st[]="Arduino";
in questo caso la memoria predisporrà uno spazio per 7 caratteri + uno spazio terminatore (null).
array - è un elenco di variabili cui si può accedere attraverso un indice. Si usano per creare tabelle di valori a cui si possa accedere facilmente. Ad esempio se si vuole conservare i diversi livelli di luminosita a cui è sensibile una fotoresistenza si può usare un array semplice come il seguente
int lux[6]={0,20,40,60,80,100}
come in linguaggio C, per accedere al primo elemento si usa lux[0],
per accedere all'ultimo si usa lux[5].
Gli array sono l'ideale quando si desidera fare la stessa cosa su più elementi
dello stesso tipo, perché si può scrivere quello che si deve fare una volta
e quindi eseguire su ogni variabile dell'array semplicemente cambiando indice,
ad esempio usando un ciclo.
Visibilità delle variabili
Le variabili in uno sketch Arduino hanno lo stesso tipo di visibilità usato in altri linguaggi.
Le variabili possono essere locali o globali a secondo di dove sono dichiarate.
Una variabile globale è una variabile che può essere vista ed usata da ogni
funzione presente nel programma.
La variabili locali sono visibili solo dalla funzione in cui sono dichiarate.
Nell'ambiente Arduino, qualsiasi variabile dichiarata al di fuori di una
funzione, come ad es. setup() o loop()
è una variabile globale. Qualsiasi variabile dichiarata all'interno di una
funzione è locale (e accessibile) solo all'interno di tale funzione.
Strutture di controllo
Arduino comprende parole chiave che controllano il flusso logico del programma. Questi costrutti di programmazione sono gli stessi già visti in altri linguaggi.
if-else : questa struttura è molto importante perché permette al dispositivo di prendere delle decisioni autonome. if deve essere seguito da una domanda specificata in forma di espressione e contenuta tra parentesi. Se l'espressione è vera, viene eseguito il blocco di codice che segue. Se è falsa viene eseguito il blocco di codice che segue l'else. Il costrutto if può essere usato anche senza la clausola else.
if(x==1){
digitalWrite(LED,HIGH);
}
for : permette di ripetere un blocco di codice per un numero specifico di volte.
for(int i=0;i<10;i++){
Serial.println("allarme!");
}
switch : è un costrutto utile perché permette di sostituire lunghi elenchi di controlli if. E' importante ricordare l'istruzione break al termine di ciascun ramo case altrimenti Arduino eseguirà le istruzioni dei rami case successivi fino a raggiungere un break o la fine dello switch.
switch(val){
case 22 : digitalWrite(13,HIGH);break;
case 45 : digitalWrite(12,HIGH);break;
case 80 : digitalWrite(9,HIGH);break;
default: //eseguito se non si
sono verificare le occorrenze precedenti
digitalWrite(12,LOW);
}
while : è un costrutto simile all'if, esegue un blocco di codice mentre è vera una determinata condizione. Tuttavia, if esegue il blocco una sola volta mentre while continua ad eseguire il blocco finchè la condizione è vera.
//fa lampeggiare il led finchè val<128
val=analogRead(1);
while(val<128){
digitalWrite(13,HIGH); delay(100);
digitalWrite(13,HIGH); delay(100);
val=analogRead(1);
}
do-while : è come il ciclo while, ma in questo caso il codice viene eseguito subito prima che la condizione venga valutata.Questa struttura si utilizza quando si vuole che il codice all'interno del blocco venga eseguito almeno una volta prima di verificaree la condizione logica.
do{
digitalWrite(13,HIGH); delay(100);
digitalWrite(13,HIGH); delay(100);
val=analogRead(1);
}while(val<128);
break : questo termine permette di uscire da un ciclo e di proseguire con l'esecuzione del codice che appare dopo il ciclo.
//fa lampeggiare il led finchè val<128
do{
if(digitalRead(7)==HIGH)break;
digitalWrite(13,HIGH); delay(100);
digitalWrite(13,HIGH); delay(100);
val=analogRead(1);
}while(val<128);
continue : quando viene usato all'interno di un ciclo, continue permette di saltare il resto del codice al suo interno e costringe a ricontrollare la condizione.
for(lux=0;lux<128;lux++){ //salta i valori
tra 40 e 100;
if(x>40 && x<100) continue;
analogWrite(pinPWM,lux);
delay(10);
}
continue è simile a break, ma break abbandona il ciclo , mentre continue prosegue con la successiva ripetizione del ciclo.
return - interrompe l'esecuzione di una funzione e ne restituisce il risultato
int computa(){
int temp=0;
temp=(analogRead(0)+45)/100;
return temp;
}
Gli operatori aritmetici, relazionali e logici rimangono quelli già studiati nell'informatica generale, mentre la libreria delle funzioni matematiche coincide con le stesse istruzioni viste in Java/JavaScript/C/C++ che può essere usata rispettando la stessa sintassi. Ad es.
y=pow(x,2);//imposta y al valore di x elevato al quadrato
Trattandosi di un dispositivo particolare bisognerà far sempre attenzione a quelle che possono essere considerate delle funzioni dedicate all'I/O specifico di Arduino.
Tra le funzioni matematiche speciali utili al funzionamento del dispositivo si possono annoverare
constrain(x,a,b);
che restituisce il valore di x compreso tra a e b; se x<a viene restituito
a.
Se x>b viene restituito b. Ad es.
val=constrain(analogRead(0),0,255);
che rifiuta valori maggiori si 255.
map(valore,basso1,alto1,basso2,alto2);
Mappa un valore dell'intervallo compreso fra basso1 e alto1 e lo converte in un valore compresto tra basso2 ed alto2. Ad es.
val=map(analogRead(0),0,1023,100,200);
converte un valore compreso tra 0 e 1023 in un valore compreso tra 100 e 200.
Funzioni di input e output
pinMode(pin, mode) : riconfigura un pin digitale in modo che si comporti come un input oppure come un output.
pin(7, INPUT) //converte il pin 7 in un input
dimenticare di impostare i pin come output con pinMode() è causa comune
di output difettoso.
Anche se in genere viene utilizzato nella funzione setup() , pinMode() può
essere usato in un ciclo se è necessario modificare il comportamento del
pin.
digitalWrite(pin, value) : attiva o disattiva un pin digitale . Perché digitalWrite abbia effetto, i pin devono essere resi esplicitamente degli output usando pinMode()
digitalWrite(9,HIGH) //attiva il pin 9 a 5V
Si noti che HIGH e LOW di solito corrispondono a on ed off, rispettivamente, a seconda di come viene usato il pin. Per esempio un LED collegato tra 5V e un pin si accende quando questo pin è LOW e si spegne quando questo pin è HIGH.
int digitalRead(pin) : legge lo stato di un pin di input, restituisce HIGH se al pin è applicato un voltaggio oppure LOW se non ne è applicato alcuno.
val=digitalRead(8)//inserisce il valore di pin 8 nella variabile val
int analogRead(pin) : legge il voltaggio applicato ad un pin di input analogico e restituisce un numero compreso tra 0 e 1023, che rappresenta voltaggio compresi tra 0V e 5V.
val=analogRead(0);//inserisce in val il valore dell'input analogico sul pin 0
analogWrite(pin, value) : cambia la frequenza PWM su uno dei pin PWM. pin può essere solo un pin che supporta PWM, cioè, uno tra i pin 3, 5, 6, 9, 10 o 11. value deve essere un numero compreso tra 0 e 255.
analogWrite(6, 128);//attenua al %50 il LED sul pin 6
shifOut(dataPin, clockPin, bitOrder, value) : invia dati ad uno shift register, un dispositivo che si usa per espandere il numero di output digitali. Questo protocollo utilizza un pin per i dati ed uno per il clock. bitOrder indica l'ordinamento dei byte (meno significativo e più significativo) e value è il byte vero e proprio che deve essere inviato.Ad es.
shiftOut(dataPin, clockPin, LSBFIRST,255);
unsigned long pulseIn(pin, value) : misura la durata di un impulso proveniente da uno degli input digitali. Questo è utile, per esempio, per leggere alcuni sensori ad infrarossi o accelerometri che inviano i loro valori sotto forma di impulsi di durata variabile.
t=pulseIn(7,HIGH); // misura il tempo in cui rimane attivo l'impulso successivo
Funzioni temporali
Arduino comprende alcune funzioni che servono a misurare il tempo trascorso e anche per mettere in pausa l'esecuzione del programma
unsigned long millis() : restituisce il numero di millisecondi che sono passati da quando è partito lo sketch.
tempo=millis()-itempo; //conta il tempo trascorso a partire da itempo
delay(ms) : mette in pausa il programma per la quantità di secondi specificata
delay(1000);// mette in pausa lo sketch per 1 secondo
delayMicroseconds(μs) : mette in pausa il programma per la quantità di microsecondi specificata
delayMicroseconds(2000);// aspetta 2 millisecondi
Numeri casuali
Se bisogna generare dei numeri random, si può usare
randomSeed(seed);
reinizializza il generatore di numeri casuali di Arduino. Anche se la distribuzione dei numeri restituiti da random() è essenzialmente casuale, la sequenza è prevedibile. Quindi bisognerebbe reimpostare il generatore su un valore a caso. Un buon seed (seme) è un valore letto da un ingresso analogico non connesso, come un pin non connesso che riceve un rumore casuale dall'ambiente circostante :onde radio, raggi cosmici interferenze elettromagnetiche etc..
randomSeed(analogRead(5));//genera un numero casuale basato sul rumore del pin 5
long random(max);
long random(min,max);
sono istruzioni che restituiscono un valore intero long pseudocasuale tra min e max-1 se min non viene specificato il minimo è 0.
Serial monitor
E' possibile far comunicare Arduino con dei dispositivi esterni attraverso la porta USB
Serial.begin(speed) : prepara Arduino a cominciare a spedire e ricevere dati seriali. Generalmente con il monitor seriale dell'IDE si usano 9600 bit al secondo anche se sono disponibili altre velocità, in genere non superiori a 115.200 bps. Ad es.
Serial.begin(9600);
Serial.print(data) : invia dei dati alla porta seriale. La codifica è facoltativa; se non viene fornita, i dati sono trattati come testo semplice
Serial.print(65);// stampa 65
Serial.print(65,DEC);// stampa 40
Serial.print(65,HEX);// stampa 41
Serial.print(65,OCT);// stampa 101
Serial.print(65,BIN);// stampa 1000001
Serial.write(65);// stampa 'A' corrispondente al
65 nel codice ASCII
Serial.println(data) : è uguale a Serial.print() ma aggiunge un ritorno a capo (ritorno carrello + nuova linea) come se si fosse digitato e poi premuto un invio.
int Serial.avaiable() : restituisce quanti byte
non letti sono disponibili sulla porta seriale da leggere attraverso read().
Dopo che si è letto tutto il possibile con read(), Serial.avaible() restituisce
0 finchè sulla porta seriale non arrivano nuovi dati
int Serial.read() : ricava un byte di dati seriali in arrivo, ad es.
int dati=Serial.read();
Serial.flush() : poiché i dati possono arrivare dalla porta seriale più rapidamente di quanto il programma sia in grado di elaborarli, Arduino conserva in un buffer tutti i dati che arrivano. Se bisogna svuotare il buffer per permettere che venga riempito di nuovi dati bisogna usare la funzione flush().
Se stiamo usando Tinkercad l'accesso al serial monitor può essere trovato all'interno della voce di menu "code" cliccando sopra l'icona Serial monitor