Le funzioni in Arduino

Negli sketch degli articoli precedenti abbiamo visto alcuni esempi di funzione, di cui abbiamo apprezzato l’utilità nell’eseguire alcune elaborazioni, senza soffermarci troppo sul loro comportamento interno.

In particolare, oltre alle ormai note funzioni setup e loop di Arduino, abbiamo visto come generare numeri casuali con la funzione random o come gestire i pin analogici con analoWrite.

In questo articolo affronteremo più nel dettaglio come creare le nostre funzioni con Arduino e cercheremo di capire perché sono utili e perché migliorano la qualità dei nostri sketch.

La funzione come sottoprogramma

Le funzioni che abbiamo utilizzato finora, come ad es. pinMode, digitalRead e digitalWrite (oltre a quelle già citate nell’introduzione), in un certo senso hanno costituito una sorta di scatola nera che, dati in ingresso certi input, restituisce in output determinati valori; inoltre le abbiamo invocate tutte le volte che ne avevamo la necessità. Da qui si evince il ruolo della funzione come sotto programma all’interno del programma principale: sostanzialmente si incapsula un blocco di istruzioni che possono risolvere problematiche comuni e, invece di riscrivere ogni volta le medesime righe di codice, si procede a una chiamata alla funzione che le contiene.

I vantaggi di un approccio del genere sono:

  • riuso del codice: una volta creata una funzione, la si può riutilizzare tutte le volte che si vuole; se viene raccolta in una libreria di funzioni, la sua utilità si estende al di là del singolo sketch;
  • manutenibilità del programma: isolando un set di istruzioni in una funzione, correzione di errori e/o modifiche del suo comportamento devono essere apportate in un unico punto, con un notevole risparmio di tempo
  • testabilità: eseguire dei test in un programma composto da più funzioni è più semplice che su un sorgente con lo stesso codice ripetuto molte volte o in più file
  • produttività: riutilizzare funzioni già scritte in precedenza velocizza di molto la stesura del codice, dato che non è più necessario riscriverle

Dichiarazione di una funzione

Dichiarare una funzione in Arduino è molto semplice. La sintassi è la seguente:

<tipo_valore di ritorno> nome_funzione(<param_1, ..., param_n) {
   
    //blocco di istruzioni della funzione
    return <valore>;
}

Si dichiara una funzione assegnandole un nome dopo aver indicato il tipo del valore restituito; tra parentesi tonde vengono dichiarati eventuali parametri, detti anche argomenti della funzione; infine, tra parentesi graffe viene inserito il codice interno alla funzione stessa, che può eseguire alcune elaborazioni utilizzando anche i parametri passati in ingresso. L’istruzione return restituisce al chiamante eventuali risultati di queste elaborazioni.

Nota: è molto importante dare nomi significativi a una funzione, in modo che si capisca a colpo d’occhio a cosa serve.

Funzioni void

Le funzioni che non restituiscono alcun valore, vengono dichiarate con il tipo void, indicante appunto che il valore di ritorno è vuoto. Ad esempio:

void stampaCiao() {
    //chiama Serial.println per stampare "ciao" sul monitor seriale
    Serial.println("Ciao!");
}//fine del blocco di istruzioni

Abbiano dichiarato una funzione di tipo void senza argomenti, denominata stampaCiao e al suo interno abbiamo chiamato Serial.println per stampare una stringa sul monitor seriale. La funzione non restituisce nulla, quindi non è stato necessario inserire la parola chiave return alla fine del blocco di istruzioni.

Non molto utile a dir la verità; apprezzeremo in seguito quando all’occorrenza è bene racchiudere un set di istruzioni in una funzione void, ma due esempi già incontrati spesso sono setup e loop, senza le quali Arduino non può sostanzialmente fare nulla.

Funzioni con valori di ritorno

Una funzione con valore di ritorno, deve contenere al suo interno una u più chiamate all’istruzione return, che deve appunto restituire al chiamante un valore dello stesso tipo della funzione. Vediamo alcuni semplicissimi esempi di funzioni non void:

int returnRandomInt() {
    //restituisce un intero a caso tra uno e dieci
    return random(1, 10);
}

char returnRandomCar() {
    //restituisce un carattere a caso compreso tra a e z
    return (char)random(97, 122);
}

bool returnRandomBool() {
    //chiama la funzione returnRandomInt ed esegue il cast al tipo bool
    return (bool)returnRandomInt();
}

Il primo esempio è interessante, perché all’interno della funzione returnRandomInt, eseguiamo un’altra chiamata a funzione di random della libreria di Arduino, e lo facciamo direttamente nel return, senza assegnare il valore a una variabile di comodo. Nel secondo esempio, invece, otteniamo un numero random dal set di caratteri ASCII rappresentanti le lettere minuscole dell’alfabeto, ed eseguiamo un cast a al tipo char prima di restituire il valore ottenuto. Infine, altro esempio interessante, chiamiamo la prima funzione che abbiamo dichiarato dentro returnRandomBool; dato che qualunque valore maggiore di zero per Arduino assume il significato di VERO, se la funzione returnRandomInt genera zero, verrà restituito false, altrimenti true.

Provate a togliere il cast nelle ultime due funzione e a spiegare cosa succede.

Ambito delle variabili

Finora non abbiamo fatto uso di variabili. Queste ultime hanno due ambiti, locale e globale; in generale una variabile locale è utilizzabile soltanto all’interno del blocco della funzione, mentre una variabile globale anche al suo esterno. L’ambito di visibilità (o scope) di una variabile può avere più strati di nidificazione, cioè una variabile locale può essere a sua volta globale rispetto al blocco di istruzioni più esterno. Vediamo un esempio per capire meglio:

//variabile globale
int a = 0;

void ambito1() {
    //variabile locale ad ambito1
    int a = 1;
    Serial.println("ambito1: ");
    Serial.println(a);
}

void ambito2() {

    //variabile locale ad ambito2
    int a = 2;
    Serial.println("ambito2: ");
    Serial.println(a);

    ambito1();
}

void ambito3() {

    //variabile locale ad ambito3
    int a = 3;
    Serial.println("ambito3: ");
    Serial.println(a);

    ambito2();
}

void testAmbitiVariabile() {
    ambito3();
    Serial.println("scope globale: ");
    Serial.println(a);
}

Abbiamo dichiarato una variabile di tipo intero denominata a nello scope globale; successivamente abbiamo inserito la stessa dichiarazione di variabile in tre funzioni diverse, stampandone il valore sul monitor seriale. In ogni funzione è stata inserita una chiamata alla funzione precedente e, infine, in testAmbitiVariabile abbiamo stampato la variabile globale più esterna,

Provata a capire qual è l’output del programma prima di caricare lo sketch finale di esempio sul vostro Arduino.

Passaggio di parametri

Come abbiamo già anticipato, una funzione può ricevere in ingresso alcuni valori, tramite gli argomenti della funzione stessa. Ad una funzione Arduino è possibile passare due tipi di parametri

  • per valore
  • per riferimento

Passaggio per valore

Nel passaggio di parametri per valore ad una funzione, viene creata una copia del valore stesso all’interno dello scope locale. Quando il controllo viene restituito al chiamante, le copie vengono distrutte senza che il valore iniziale delle variabili passate in ingresso sia stato modificato. Vediamo un esempio:

int x = 5;
int y = 18;

void riassegnaValori(int x, int y) {
    //riassegnamo i valori di x e y
    x = 43;
    y = 61;

    Serial.print("x: ");
    Serial.println(x);
    
    Serial.print("y: ");
    Serial.println(y);

}

riassegnaValori(x, y);

Serial.print("x: ");
Serial.println(x);

Serial.print("y: ");
Serial.println(y);

Abbiamo dichiarato le variabili x e y prima della funzione riassegnaValori. All’interno di questa, abbiamo assegnato nuovi valori a x e y e li abbiamo stampati a schermo. Infine, dopo la chiamata a riassegna valori con passaggio di parametri, abbiamo ristampato x e y.

Che valori assumono di volta in volta x e y?

Passaggio per riferimento

A differenza del caso precedente, passando i valori per riferimento, la funzione può modificare i parametri passati in ingresso. Questo avviene perché il riferimento non è altro che la locazione di memoria della variabile e non una copia della stessa, per cui ogni cambiamento nell’uno, si riflette nell’altra. Un argomento passato per riferimento deve essere dichiarato con la & subito prima del nome. Riscriviamo l’esempio precedente precedente utilizzando argomenti per riferimento:

int x = 5;
int y = 18;

void riassegnaValori(int &x, int &y) {
    //riassegnamo i valori di x e y
    x = 43;
    y = 61;

    Serial.print("x: ");
    Serial.println(x);
    
    Serial.print("y: ");
    Serial.println(y);

}

riassegnaValori(x, y);

Serial.print("x: ");
Serial.println(x);

Serial.print("y: ");
Serial.println(y);

Quali sono le differenze rispetto al paragrafo precedente?

Passaggio di un array a funzione

È possibile passare anche un array a una funzione, tuttavia, in questo caso, dietro le quinte viene in realtà passato un puntatore all’indirizzo del primo elemento dell’array. Vedremo in seguito i puntatori, uno strumento molto potente che ci permette di ottimizzare il codice per Arduino, gestendone direttamente la memoria. Ma dato che si tratta di un argomento avanzato, limitiamoci a vedere un esempio di passaggio di array a funzione:

const int DIM = 26;

char lettere[DIM];

void creaAlfabeto(char vettore[]) {
    for(int i=0; i<DIM; i++) {
        int carattere = i + 65;
        lettere[i] = carattere;        
    }
}

creaAlfabeto(lettere);

Serial.println("Alfabeto: ");
for(int i=0; i<DIM; i++) {
    Serial.print(lettere[i]);
}

Abbiamo dichiarato la costante DIM, per stabilire le dimensioni di un array di caratteri denominato alfabeto. Viene poi dichiarata la funzione creaAlfabeto a cui viene passato l’argomento vettore di tipo array di carattere. All’interno della funzione l’array viene popolato con i caratteri ASCII rappresentanti le lettere maiuscole dell’alfabeto. Infine chiamiamo la funzione stessa passando il riferimento al primo elemento dell’array lettere e ne stampiamo a schermo i valori con un ciclo for, a dimostrazione che gli elementi del vettore sono stati modificati da creaAlfabeto e che ad essere passata come parametro non era una copia.

Uno sketch di esempio

Possiamo raccogliere tutti gli esempi sopra in un unico programma; l’unica cosa che dobbiamo fare e raccogliere la dichiarazione delle funzioni all’esterno di setup e loop, così come la dichiarazione delle variabili globali. Come al solito inizializziamo la seriale per stampare a schermo i risultati e chiamiamo le funzioni dentro setup, in modo che vengano eseguite una sola volta (premete il tasto di reset di Arduino per rieseguire lo sketch).


//variabili globalei
const int DIM = 26;
char lettere[DIM];
int a = 0;

void stampaCiao() {
    //chiama Serial.println per stampare "ciao" sul monitor seriale
    Serial.println("Ciao!");
}//fine del blocco di istruzioni

int returnRandomInt() {
    //restituisce un intero a caso tra uno e dieci
    return random(1, 10);
}

char returnRandomCar() {
    //restituisce un carattere a caso compreso tra a e z
    return (char)random(97, 122);
}

bool returnRandomBool() {
    //chiama la funzione returnRandomInt ed esegue il cast al tipo bool
    return (bool)returnRandomInt();
}

void ambito1() {
    //variabile locale ad ambito1
    int a = 1;
    Serial.print("ambito1: ");
    Serial.println(a);
}

void ambito2() {

    //variabile locale ad ambito2
    int a = 2;
    Serial.print("ambito2: ");
    Serial.println(a);

    ambito1();
}

void ambito3() {

    //variabile locale ad ambito3
    int a = 3;
    Serial.print("ambito3: ");
    Serial.println(a);

    ambito2();
}

void testAmbitiVariabile() {
    ambito3();
    Serial.print("scope globale: ");
    Serial.println(a);
}

void riassegnaValori(int x, int y) {
    //riassegnamo i valori di x e y
    x = 43;
    y = 61;

    Serial.print("x: ");
    Serial.println(x);
    
    Serial.print("y: ");
    Serial.println(y);

}

void riassegnaValoriPerRif(int &x, int &y) {
    //riassegnamo i valori di x e y
    x = 43;
    y = 61;

    Serial.print("x: ");
    Serial.println(x);
    
    Serial.print("y: ");
    Serial.println(y);

}

void creaAlfabeto(char vettore[]) {
    for(int i=0; i<DIM; i++) {
        int carattere = i + 65;
        lettere[i] = carattere;        
    }
}


void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  
  stampaCiao();

  Serial.print("returnRandomInt: ");
  Serial.println(returnRandomInt());

  Serial.print("returnRandomCar: ");
  Serial.println(returnRandomCar());

  Serial.print("returnRandomBool: ");
  Serial.println(returnRandomBool());
  
  testAmbitiVariabile();
  
  int x = 5;
  int y = 18;

  riassegnaValori(x, y);
  
  Serial.print("x: ");
  Serial.println(x);
  
  Serial.print("y: ");
  Serial.println(y);
  
  riassegnaValoriPerRif(x, y);

  Serial.print("x: ");
  Serial.println(x);
  
  Serial.print("y: ");
  Serial.println(y);
  
  creaAlfabeto(lettere);

  Serial.print("Alfabeto: ");
  for(int i=0; i<DIM; i++) {
      Serial.print(lettere[i]);
  }

}

void loop() {
  // put your main code here, to run repeatedly:

}

Ed ecco l’output del listato sul monitor seriale:

Sketch Funzioni Arduino
Le funzioni in Arduino

Provate a capire se qualche istruzione che è stata scritta più volte nel setup può essere incapsulata in una funzione. Se la risposta è no provate a spiegare il perché. Inserite dove ritenete opportuno, altre istruzioni di stampa a schermo per rendere più chiaro l’output delle chiamate.

Conclusioni

In questo articolo abbiamo visto cosa sono le funzioni, a cosa servono e perché è importante dichiararle appena possibile nei nostri progetti Arduino. Nelle guide successive toccheremo con mano più in concreto la loro utilità, quando dovremo ricalcolare i valori restituiti da alcuni sensori in base a determinate formule di conversione.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.