Robotica educativa/Le basi dell'informatica

Indice del libro


«L'informatica è la scienza che risolve problemi. Talvolta con l'utilizzo del computer.»

Non è nota la paternità di questa definizione, ma fa comprendere quanto spesso fuorviante sia il suo approccio. Pertanto, per informatica, più che un approccio alla macchina, si preferisce intendere un approccio al problema.

Se si utilizzerà, in seguito, un elaboratore elettronico, o meno, dipende dal problema stesso e dall'approccio. Sovente, anche problemi complessi possono avere soluzioni molto semplici, pertanto — prima di digitare sulla tastiera — la domanda da porsi è: qual è la soluzione migliore per risolvere questo problema? Mi occorre un elaboratore elettronico? Se la risposta è sì, si inizierà il lavoro con le idee molto chiare e il tempo sarà stato sempre ben speso.

Il linguaggio C++

modifica

Come tra umani, tra umani e macchine esistono molteplici linguaggi. Alcuni orientati agli umani (quindi facili da imparare e utilizzare, come il BASIC), altri orientati alla macchina (come l'Assembler, il C e il C++).

Dovendo programmare un microprocessore, utilizzeremo il linguaggio C++, pensato per scrivere sistemi operativi. I primi passi sono un po' complicati, ma poi è tutta discesa, poiché i comandi sono davvero pochi.

Per quel che riguarda uno studio approfondito del C e del C++ si rimanda ai seguenti testi:

  Risorse

la conoscenza approfondita di tali linguaggi va ben oltre quanto richiesto per la programmazione di Arduino, poiché ogni dispositivo fornisce librerie con istruzione ad alto livello, facili da comprendere.

Il linguaggio C++ per Arduino

modifica

Nel caso del microcontrollore Arduino vengono forniti comandi aggiuntivi e librerie, che arricchiranno il vocabolario della macchina, senza che lo sforzo ricada su di noi.

Arduino, quando si crea un nuovo progetto, presenta il codice suddiviso in due parti (funzioni):

  1. il setup che contiene l'inizializzazione del programma;
  2. il loop il quale contiene il programma vero e proprio che verrà eseguito ciclicamente fino allo spegnimento o al reset.

Il tutto si presenta così:

void setup() {
  // inserisci qui il codice di configurazione, verrà eseguito una volta sola:

}

void loop() {
  // inserisci qui il codice principale, verrà eseguito ripetutamente:

}

Le funzioni sono di tipo void perché non restituiscono nulla come risultato. E nemmeno hanno argomenti in ingresso (i quali andrebbero dichiarati tra le parentesi tonde.

Arduino, invece, ha un set di istruzioni, pensate per la gestione di sensori e attuatori, con i quali si interfaccerà.

Gestione di ingressi/uscite

modifica

Gli ingressi (e le uscite) di Arduino possono essere digitali o analogici. Nel setup si definisce quale uso si farà di ciascun pin connesso a componenti esterni attraverso la funzione pinMode(pin, mode), dove:

  • pin è il numero del pin:
    • da 0 a 13 per i pin digitali;
    • da A0 ad A5 per i pin analogici (questi possono essere trattati come pin digitali, ma non il viceversa);
  • mode è la modalità con cui verrà utilizzato. Le costanti possibili sono:
    • INPUT per gli ingressi
    • OUTPUT per le uscite
    • INPUT_PULLUP per ingressi che necessitano di una resistenza, cosiddetta di pull-up.

Ingressi e uscite numeriche

modifica

Per accendere e spegnere un LED con un pulsante, occorre un ingresso (il pulsante) e un'uscita (il LED). Assumendo che il pulsante sia connesso al pin 7 e il LED al pin 13 si scriverà:

int ledPin = 13;  // il LED viene connesso al pin 13
int inPin = 7;    // il pulsante viene connesso al pin 7
int val = 0;      // variabile per memorizzare il valore letto

void setup() {
  pinMode(ledPin, OUTPUT);  // imposta il pin 13 come uscita
  pinMode(inPin, INPUT);    // imposta il pin 7 come ingresso
}

void loop() {
  val = digitalRead(inPin);   // legge l'ingresso
  digitalWrite(ledPin, val);  // imposta il LED col valore letto
}

Ingressi e uscite analogiche

modifica

Arduino Uno ha una risoluzione in ingresso di 10 bit. Questo significa che distingue   differenti livelli di tensione. Essendo alimentato con una tensione massima di   può distinguere valori di tensioni pari a  .

Le uscite analogiche, invece, hanno un range di 8 bit, pertanto i livelli saranno   (compreso lo 0). Possono essere utilizzate per pilotare dispositivi con modulazione PWM (modulazione a larghezza di impulsi) così da variare – tramite un potenziometro – la luminosità di un LED o la potenza di un motore.

Attenzione: spesso si passerà il valore letto in ingresso in uscita. Siccome l'uscita ha due bit in meno, questo andrà diviso per quattro, come nel codice che segue:

int ledPin = 9;     // il LED viene connesso al pin 9
int analogPin = 3;  // il il potenziometri viene connesso al pin 3
int val = 0;        // variabile per memorizzare il valore letto

void setup() {
  pinMode(ledPin, OUTPUT);  // imposta il pin 9 come uscita
                            // non occorre dichiarare l'ingresso se e' analogico
}

void loop() {
  val = analogRead(inPin);       // legge l'ingresso
  analogWrite(ledPin, val / 4);  // imposta la luminosita' del LED secondo il valore letto
}

Non tutte le uscite supportano questa funzione. Quelle possibili sono le 3, 5, 6, 9, 10 e 11. Si riconoscono per via del segno tilde (~) sulla scheda.

Infine, sui pin 5 e 6 la frequenza di lavoro è di circa 980 Hz. Sempre a proposito di questi due pin, i loro output PWM avranno un ciclo utile più alto di quello atteso, a causa dell’interazione con le funzioni millis() e delay(), che condividono lo stesso timer interno usato per generare queste uscite PWM. Questo si noterà in particolare con impostazioni basse del ciclo utile (comprese tra 0 e 10) e potrebbe risultare in un valore di 0 che non spegne completamente l’output sui pin 5 e 6.

Gestione avanzata di ingressi/uscite

modifica

Esistono diversi comandi che consentono una gestione avanzata e, allo stesso tempo semplice, di ingressi uscite. Di seguito vengono elencati i principali:

pulseIn() legge un impulso (sia esso HIGH o LOW) su un pin. Se il valore è settato su HIGH, attende che il pin vada nello stato HIGH e, nel frattempo, misura il tempo trascorso. Funziona sugli impulsi di lunghezza compresa tra 10 microsecondi e 3 minuti (per valori così alti, si consiglia l'utilizzo di variabili del tipo unsigned long).

Può essere usato con due modalità: con e senza timeout, parametro opzionale che interrompe la funzione in caso di tempistiche eccessivamente lunghe. Restituisce la durata dell’impulso (in microsecondi) oppure 0 se nessun impulso è partito prima del timeout.

tone() genera un’onda quadra alla frequenza specificata, può essere specificata anche una durata, altrimenti il segnale prosegue fino all'esecuzione del comando noTone() (utile in contesti di emergenza).

Il pin può essere connesso ad un buzzer piezoelettrico o altro speaker per riprodurre suoni. Può essere generato solo un suono alla volta. Se un tono è già in riproduzione su un pin differente, la chiamata a tone() non avrà alcun effetto. Se il tono è in riproduzione sullo stesso pin, la chiamata ne imposterà una frequenza differente.

Non è possibile generare toni inferioni a 31Hz. Per riprodurre toni differenti su pin multipli, occorre eseguire noTone() su un pin prima di eseguire tone() su un altro pin.

Gestione del tempo

modifica

Sostanzialmente ci sono due tipologie di funzioni da conoscere:

  • delay e delayMicrosecond, le quali fermano l'attività del microcontrollore per i millisecondi e microsecondi stabiliti (rispettivamente);
  • millis e micros, che invece restituiscono il tempo trascorso dall'accensione, rispettivamente in millisecondi e microsecondi. A causa dei numeri elevati che devono gestire, queste perdono il conteggio dopo circa 70 minuti e 50 giorni.

Ecco alcuni esempi:

// Attende un secondo
delay(1000);

// Attende un millisecondo
delayMicroseconds(1000);

// Mostra il tempo trascorso dall'accensione
unsigned long tempo = millis();

// Lo stesso, ma in microsecondi
unsigned long tempo = micros();

Con millis è possibile generare un'attesa con il seguente frammento di codice:

// Verrà usata in seguito (no spoiler!)
unsigned long tempoPrecedente = 0;

// Rileva il tempo trascorso
unsigned long tempoAttuale = millis();

// Si decide di aspettare un secondo (1000 ms)
const long intervallo = 1000;

if (tempoAttuale - tempoPrecedente >= intervallo) {
    // aggiorna il tempoPrecedente con il tempoAttuale
    tempoPrecedente = tempoAttuale;
    
    // qui si inserisce il codice da eseguire durante l'attesa
}

Gli interrupt

modifica

Alcune operazioni svolte dal microcontrollore richiedono tempo. Altre, come delay, lo mettono in attesa. Per forzare la sua attività al verificarsi di un evento (come un allarme che deve bloccare un ciclo di lavorazione) esistono gli interrupt.

Gli interrupt consentono di svolgere alcune attività importanti in sottofondo e sono abilitati per impostazione predefinita. Alcune funzioni sono disabilitate e le comunicazioni in arrivo possono essere ignorate. Tuttavia, gli interrupt possono alterare leggermente la tempistica del codice e possono essere disabilitati per sezioni di codice particolarmente critiche.

Il loro utilizzo è molto semplice:

void loop() {
  noInterrupts();
  // qui viene inserito il codice critico che non deve essere interrotto
  interrupts();
  // qui tutto il resto del codice
}

Il primo parametro di attachInterrupt() è il numero di interrupt. Normalmente si usa digitalPinToInterrupt(pin) per trasformare un pin numerico in un interrupt specifico. Per esempio, se ci si collega al pin 3, si utilizza digitalPinToInterrupt(3) come primo parametro di attachInterrupt().

Sulle schede Arduino Uno (le più diffuse) è possibile utilizzare come interrupt i pin 2 e 3.

Utilizzo degli interrupt

modifica

Gli interrupt sono utili per far sì che le cose avvengano automaticamente nei microcontrollori e possono contribuire a risolvere i problemi di temporizzazione. L'uso di un interrupt può essere utile per la lettura di un trasduttore girevole o per il monitoraggio di un ingresso da parte dell'utente.

Se si volesse garantire che un programma rilevi sempre gli impulsi di un trasduttore rotante, così da non perdere mai un impulso, diventerebbe molto difficile scrivere un programma che faccia qualsiasi altra cosa, perché il programma dovrebbe eseguire costantemente il controllo delle linee del trasduttore per rilevare gli impulsi quando si presentano. Anche altri sensori presentano una dinamica simile, come, per esempio, il sensore sonoro che vuole rilevare un clic o il sensore a infrarossi che cerca di rilevare la caduta di una moneta. In tutte queste situazioni, l'uso di un interrupt può consentire al microcontrollore di svolgere altre operazioni senza perdere il segnale di ingresso.

Le routine degli interrupt

modifica

Gli interrupt sono funzioni speciali che hanno alcune limitazioni particolari, che la maggior parte delle altre funzioni non ha. Un interrupt non può ricevere parametri e non dovrebbe restituire nulla.

In generale, gli interrupt dovrebbero essere più corti e veloci possibile. Se lo script utilizza più istruzioni per le operazioni, è possibile eseguirne solo una alla volta; gli altri interrupt verranno eseguiti dopo il termine di quello corrente, in un ordine che dipende dalla loro priorità. millis() si basa sugli interrupt per il conteggio, quindi non si incrementerà mai all'interno di un'istruzione per le operazioni. Poiché delay() richiede gli interrupt per funzionare, non funzionerà se richiamato all'interno di un interrupt. micros() funziona inizialmente, ma inizierà a comportarsi in modo irregolare dopo  . delayMicroseconds() non utilizza alcun contatore, quindi funzionerà normalmente.

In genere le variabili globali vengono utilizzate per passare i dati tra l'interrupt e il programma principale. Per assicurarsi che le variabili condivise tra l'interrupt e il programma principale vengano aggiornate correttamente, occorre dichiararle come volatili.

Parametri

modifica

La funzione per dichiarare l'interrupt si presenta così:

attachInterrupt(digitalPinToInterrupt(pin), ISR, mode)

dove:

  • pin è il pin di Arduino (il 2 o il 3);
  • ISR è la funzione da chiamare quando si verifica un interrupt; questa funzione non deve accettare alcun parametro e non deve restituire nulla. Questa funzione, talvolta viene definita routine di servizio dell'interrupt;
  • mode definisce quando l'interrupt deve essere attivato. Come valori ammessi vengono predisposte quattro costanti:
    • LOW per attivare l'interrupt quando il pin è a livello basso;
    • CHANGE per attivare l'interrupt quando il pin cambia stato;
    • RISING per attivare l'interrupt quando il pin passa da livello basso a livello alto;
    • FALLING per quando il pin passa da livello alto a livello basso.

Un esempio completo

modifica
// Definizione dei parametri
const byte ledPin = 13;
const byte interruptPin = 2;
volatile byte state = LOW;

// Predisposizione di ingressi, uscite e interrupt
void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(interruptPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(interruptPin), blink, CHANGE);
}

// Passa la variabile logica state al LED
void loop() {
  digitalWrite(ledPin, state);
}

// Se viene premuto un tasto, cambia il valore di state
// Richiamato tramite interrupt
void blink() {
  state = !state;
}

Comunicazioni con altri dispositivi

modifica

La porta seriale

modifica
 
Porta USB tipo B

La porta seriale è il più facile e comodo metodo per ricevere (e inviare) dati dalla scheda Arduino a un'altra macchina, sia essa il computer, sia essa un'altra scheda Arduino.

Tipicamente si ha a disposizione una scheda Arduino Uno. Questa ha una porta seriale i cui pin dedicati sono due:

  • 0, per la ricezione;
  • 1, per la trasmissione.

Attenzione: questi pin sono connessi alla porta USB, dalla quale Arduino viene programmato. Pertanto, se si desidera utilizzarli con apparati esterni, all'atto della programmazione, andranno scollegati per evitare effetti indesiderati.

Se, invece, si ha a disposizione un Arduino Mega, allora si avranno quattro porte seriali. La prima come per Arduino Uno (il che rende i codici compatibili), le altre tre per altri dispositivi.

Sempre in tema di precauzioni: Arduino lavora con tensioni di   o  , a seconda delle schede. Pertanto, la porta seriale di Arduino non deve mai essere connessa direttamente alla porta seriale RS232, che opera con livelli di tensione di   che danneggerebbero la scheda Arduino.

Comunicazione tramite porta seriale

modifica

È possibile utilizzare la comunicazione seriale con i pin Tx/Rx rispettando la logica TTL ( ). Per eseguire una comunicazione seriale con un dispositivo esterno TTL (come un'altra scheda Arduino) è sufficiente collegare il pin Tx al pin Rx del proprio dispositivo e il pin Rx al pin Tx. Da non dimenticare che anche le messe a terra delle schede devono essere connesse, diversamente mancherebbe un riferimento di  .