#14 – Comunicazione Seriale con Arduino

La possibilità di poter mantenere una comuncazione seriale con Arduino è uno degli aspetti più interessanti della sua programmazione.

Questo apre infatti la strada ad un controllo “remoto” del dispositivo, o comunque una possibile modifica del comportamento del codice secondo le nostre esigenze.

La comunicazione è di tipo seriale (vedremo a breve cosa significa), e può essere fatta tramite USB, o anche, come vedremo, tramite jumpers, e bluetooth.

Oltre ad essere tra Arduino e computer, può essere infatti realizzata tra Arduino e Arduino, o anche con altre schede programmabili (Raspberry Pi, …).

Comunicazione Seriale

Immaginate di dovere trasferire dell’informazione all’esterno della vostra scheda.

Dall’articolo sulla rappresentazione dell’informazione, sappiamo che tutta l’informazione nella nostra scheda è immagazzinata ed elaborata in binario. È quindi naturale pensare ad una comunicazione binaria.

Poiché in binario abbiamo solo due stati possibili (1 e 0), e abbiamo a disposizione dei pin che possono essere messi a +5V o 0V è naturale pensare di associare al +5V lo stato logico 1, mentre allo 0V lo stato logico 0.

Alternando sul pin +5V e 0V, potremmo trasmettere l’informazione ad un dispositivo in ricezione. Una comunicazione di questo tipo è detta Seriale TTL (perché utilizziamo le tensioni VCC e GND proprie del circuito).

Supponiamo di voler trasmettere il numero decimale 42 (dec) = 0101010 (bin). Potremmo mettere il pin a +5V ogni volta che scandendo la stringa da destra a sinistra, leggiamo un 1, e rimetterlo a 0V quando troviamo uno 0:

Trasmissione segnale (asse temporale)

Alcuni problemi appaiono subito evidenti:

  • Come fa la scheda ricevente ad accorgersi che la trasmissione è iniziata/terminata?
  • Se abbiamo più 1 (o 0) da trasmettere di seguito, come fa la scheda ricevente a distinguerli?
  • Come facciamo ad essere certi che il messaggio che abbiamo inviato non sia stato danneggiato?

Principalmente, ci sono due diversi approcci al problema:

  • utilizzare un altro pin, in cui creiamo un segnale di sincronizzazione. In questo modo avremo una comunicazione seriale sincrona.
Comunicazione Seriale Sincrona
  • conoscere a priori parametri specifici sulla trasmissione. Poiché non utilizziamo segnali di sincronizzazione, abbiamo una comunicazione seriale asincrona.

La prima opzione è alla base della comunicazione seriale parallela, che è quella presente sui BUS dei nostri dispositivi (computer, telefoni, …). Viene chiamata parallela perché non abbiamo solo un segnale Data, ma molti.

Sebbene permetta uno scambio dati veloce e sicuro, utilizza tanti “canali”. Poiché noi abbiamo pochi pin, vorremmo utilizzarne il meno possibile, anche a costo di dover trasmettere più lentamente.

Per una comunicazione monodirezionale, è possibile infatti utilizzare un solo canale (e quindi un solo pin) per comunicare, utilizzando la seconda opzione.

Comunicazione Seriale Asincrona

Dato che non possiamo appoggiarci ad un segnale di sincronizzazione, dobbiamo fare in modo che la scheda ricevente riesca a riconoscere quando trasmettiamo informazione da memorizzare, e quando no.

Per farlo, si introducono dei parametri specifici, che devono essere noti e uguali a priori in entrambe le schede in comunicazione. Sono:

  • Bit di dati (data bits)
  • Bit di sincronizzazione (synchronization bits)
  • Bit di parità (parity bits)
  • Velocità di trasmissione (baud rate)

Velocità di trasmissione

La velocità di trasmissione indica quanto velocemente trasmettiamo i bit di informazioni. La sua unità di misura è infatti in bit/secondo (bps).

Nello specifico, indica quanto a lungo il trasmettitore tiene il pin in stato 1 o 0, prima di passare a trasmettere il bit successivo (la durata di trasmissione del singolo bit è infatti il reciproco della velocità di trasmissione).

Può essere un valore qualsiasi, purché ragionevole. Esiste infatti soltanto un limite superiore: la velocità di elaborazione del microcontrollore (e dell’impostazione del pin).

Tipici valori sono: 1200, 2400, 4800, 9600, 19200, 38400, 57600 e 115200. L’ultima è la più elevata ammessa per Arduino.

Normalmente, se non si ha necessità di comunicazioni molto veloci, si utilizza 9600.

Bit di sincronizzazione

Un altro aspetto fondamentale è far capire al ricevitore quando inizia e quando finisce la trasmissione. Vengono utilizzati a questo proposito i bit di sincronizzazione.

Innanzitutto, occorre impostare uno stato iniziale della linea. La scelta più saggia è quella di tenere la linea a +5V quando non la utilizziamo.

Ogni “pacchetto” da trasmettere, inizia quindi con bit apposito, chiamato “bit di inizio”. Ovviamente, questo bit porta la linea da 1 a 0, e indica che la trasmissione sta per iniziare.

Terminata la trasmissione, ci possono essere uno o due bit di stop, che riportano la linea nello stato 1.

Bit di dati

Si tratta di quanti dati vengono inviati per ogni “pacchetto”.

Ci si potrebbe chiedere perché sia necessario frammentare il messaggio da inviare in pacchetti di dimensione fissa. Diciamo che il protocollo vuole permettere la comunicazione bidirezionale sullo stesso canale. Se una scheda ha un messaggio molto lungo da inviare, non vogliamo che occupi il canale per un tempo molto lungo. Con la frammentazione, ogni scheda “parlerà a turno“. La questione di trovare “la tempistica corretta per parlare” non verrà trattata qui.

Noi utilizzeremo una comunicazione di tipo “full-duplex”, ovvero utilizzeremo solo canali in modalità mododirezionale. Per una comunicazione bidirezionale, come vedremo, utilizzeremo quindi due canali.

Inoltre una trasmissione a pacchetto permette l’implementazione di un sistema di controllo dell’integrità per l’informazione ricevuta.

I bit di dati possono essere da 5 a 9. Standard 8, ovvero un byte per pacchetto.

Può essere molto furbo utilizzarne 7, se stiamo comunicando utilizzando soltanto caratteri ASCII. Ogni carattere è infatti lungo 7 bit, e quindi non frammenteremmo i caratteri nei pacchetti, ottimizzando la trasmissione.

Per i più curiosi, andiamo velocemente ad analizzare l’output del protocollo seriale di Arduino, quando inviamo la stringa “Test”.

8 bit di dati, no parità, 1 bit di stop

Possiamo osservare come l’ultimo bit è sempre a 0. Questo perché come abbiamo detto, i bit vengono trasmessi dal meno significativo al più significativo, e sono 7. Se prendiamo ad esempio T = 0x54 = 0101 0100

Se utilizziamo invece 7 bit di dati, evitiamo di trasferire un inutile 0:

7 bit di dati, no parità, 1 bit di stop

Bit di parità

Se le schede sono state configurate per un controllo di errore, allora questo bit del pacchetto è effettivamente presente.

Per trovare il valore di questo bit si procede come segue:

  1. Si sommano tutti i data bits del pacchetto
  2. Se il risultato è pari, si scrive 1, altrimenti 0

Risulta subito evidente che è un controllo di errore piuttosto debole: nessuno ci garantisce che il bit in posizione 3 sia corretto, ma solo che nel complesso il pacchetto non ha subito una variazione del numero di 1.

Per controlli più lenti ma accurati, si utilizzano algoritmi di “checksum” più avanzati (per i più curiosi, invito a leggere l’algoritmo CRC).

Dato che il checksum è fondamentale solo su linee disturbate, o se utilizziamo un singolo canale in modo bidirezionale, noi non lo utilizzeremo.

Esempio di comunicazione seriale asincrona monodirezionale

Comunicazione Seriale con Arduino

Come è lecito aspettarsi, se abbiamo due canali invece di uno, i collegamenti andranno incrociati:

Schema dei collegamenti tra due schede Arduino

Il motivo è ovvio: se una scheda trasmette, l’altra su quella linea deve ricevere.

È bene notare come anche i GND sono stati collegati: questo è fondamentale per un corretto funzionamento delle interfacce seriali!

Se comunichiamo invece tramite USB, i collegamenti sono già fatti per noi. Nell’articolo su come alimentare Arduino, abbiamo infatti già visto che un cavo USB versione 1 o 2, contiene 4 fili, di cui 2 per l’alimentazione e 2 sono appunto per la comunicazione seriale (DATA+ e DATA-).

Adesso… il codice!

Su Arduino, la libreria di comunicazione seriale asincrona è già scritta.

Si tratta di un oggetto di nome “Serial”. L’oggetto “Serial”, contiene le seguenti funzioni (le più importanti):

Alcuni parametri opzionali sono stati omessi, e potete trovarli seguendo i link appositi sui nomi delle funzioni.

Il codice standard per le operazioni di lettura/scrittura è il seguente:

void setup() {
  Serial.begin(9600); //Inizializziamo la comunicazione seriale
}
void loop() {
   if (Serial.available() > 0){
      //Leggiamo con uno dei comandi di lettura
      //Serial.read(), Serial.parseInt(), Serial.parseFloat(), ...
   }
}
/*
Poiché abbiamo parlato di connessioni con databits diversi, scopriamo che 
esiste un'altra versione di Serial.begin(), con il prototipo:

void Serial.begin(int speed, byte config) 

che accetta un valore di config nella forma SERIAL_DATABITPARITASTOPBITS, dove:
- N = nessuna parità
- E = parità pari
- O = parità dispari

SERIAL_5N1
SERIAL_6N1
SERIAL_7N1
SERIAL_8N1 (il default)
SERIAL_5N2
SERIAL_6N2
SERIAL_7N2
SERIAL_8N2

SERIAL_5E1
SERIAL_6E1
SERIAL_7E1
SERIAL_8E1
SERIAL_5E2
SERIAL_6E2
SERIAL_7E2
SERIAL_8E2

SERIAL_5O1
SERIAL_6O1
SERIAL_7O1
SERIAL_8O1
SERIAL_5O2
SERIAL_6O2
SERIAL_7O2
SERIAL_8O2 
*/

Il seguente codice invece legge un carattere dalla seriale e lo riscrive:

void setup() {
  Serial.begin(9600); //Inizializziamo la comunicazione seriale
}
/*
 * Tenere presente che quando scrivete con il Monitor Seriale,
 * vengono inviati anche i caratteri di ritorno (Invio). Questi
 * sono identificati dal carattere speciale \n
 */
void loop() {
   char carattere;
   if (Serial.available() > 0){ //Controlliamo se ci sono dati da leggere
      carattere = Serial.read(); //Leggiamo un carattere
      if (carattere != '\n'){ //Se il carattere non è l'invio
         Serial.println(carattere); //Lo scriviamo indietro
      }
   }
}

Per copiare esattamente la stringa indietro (a patto di usare il carattere di ritorno), utilizziamo:

#define N 20
void setup() {
  Serial.begin(9600); //Inizializziamo la comunicazione seriale
}
/*
 * Scrive indietro una stringa di al massimo N caratteri
 */
char stringa[N]; //Globale, perché deve mantenere i dati a ogni ciclo
int i=0;
void loop() {
   char tmp;
   if (Serial.available() > 0){ //Controlliamo se ci sono dati da leggere
      tmp = Serial.read(); //Leggiamo un carattere
      if (tmp == '\n'){ //Se è il carattere invio, la stringa è terminata
        stringa[i] = '\0'; //Aggiungo il terminatore
        i=0; //Ricomincio a salvare dall'inizio
        Serial.println(stringa); //Stampo la stringa
      }else{
        stringa[i] = tmp; //Salvo il valore
        i++; //Mi metto sulla prossima posizione
      }
   }
}

Per leggere un numero:

void setup() {
  Serial.begin(9600); //Inizializziamo la comunicazione seriale
}
/*
 * Legge un numero intero, e lo scrive indietro.
 * N.B.: disattivare il carattere di return nel monitor seriale
 */
void loop() {
   int n;
   if (Serial.available() > 0){ //Controlliamo se ci sono dati da leggere
      n = Serial.parseInt(); //Legge il numero
      Serial.println(n); //Stampa il numero
   }
}

Monitor seriale e Arduino IDE

Collegate la vostra scheda Arduino tramite USB. Compilate ed inviare il programma.

Per aprire il monitor seriale, ed iniziare la comunicazione con Arduino, andate su Strumenti->Monitor Seriale:

Aprire monitor seriale

Nella schermata del monitor seriale, controllate che il Baudrate uguale a quello impostato nel codice. Inoltre, è possibile disattivare il carattere di ritorno (A capo, NL), impostando su “Nessun fine riga”.

Finestra per la comunicazione seriale con Arduino

Come ultima precisazione, sottolineo che i Pin 0 e 1 di Arduino UNO sono direttamente connessi a Data+ e Data- dell’interfaccia USB (a loro volta connessi al modulo seriale). Non potrete quindi comunicare in seriale e contemporaneamente utilizzarli per altri scopi.

Proseguiamo ora con il prossimo articolo.