Nous avons déjà rencontré les fonctions de nombreuses fois. Tout d’abord, dans « La programmation, qu’est ce que c’est », nous avons vu les fonctions setup()
et loop()
. Ces deux fonctions sont nécessaires au fonctionnement d’un sketch Arduino mais ne prennent aucun argument et ne retournent rien. Nous avons également vu des fonctions qui ont été définies et utilisées dans « Enseigne de magasin » et dans de nombreux autres articles. Il est temps d’entrer un peu dans les détails à propos des fonctions.
Pourquoi utiliser des fonctions
Les fonctions ont plusieurs rôles. Tout d’abord, elles permettent de faciliter la vie du programmeur. Une bibliothèque de fonctions est fournie avec l’IDE Arduino et elle permettent de manipuler facilement les entrées/sorties et les autres dispositifs du micro-contrôleur. De plus, elle permettent de s’abstraire du micro-contrôleur. Par exemple, le micro-contrôleur employé dans un Arduino Mega est assez différent de celui employé dans un Arduino Uno. Un Due est encore plus différent. Malgré tout, grâce aux fonctions, on peut construire des programmes qui fonctionneront sur tous les Arduino sans se soucier de leur différences. Enfin, comme cela a été présenté dans « La programmation, qu’est ce que c’est », elle permettent de regrouper des recettes que l’on peut ensuite réutiliser.
La syntaxe d’une fonction
Une fonction peut donc retourner une valeur, possède un nom et des arguments. La valeur retournée a un type, voir « Types, constantes et variables » pour savoir ce que sont les types. Si la fonction ne retourne aucune valeur, comme setup()
et loop()
, le type de retour est void
qui signifie rien ou vide.
Le nom doit respecter les règles de nommage des identifiants [1]. Enfin les arguments sont une liste de déclarations de variables séparées par des virgules et placées entre parenthèses.
Supposons par exemple que nous ayons 3 DEL, une rouge, une jaune et une verte connectées respectivement au broches 4, 5 et 6 de l’Arduino et qui forment un feu. Chacune de ces 3 DEL est allumée quand la sortie est HIGH. Définissons maintenant une fonction pour allumer le feu rouge :
void feuRouge()
{
digitalWrite(5, LOW); // éteint le feu jaune
digitalWrite(6, LOW); // éteint le feu vert
digitalWrite(4, HIGH); // allume le feu rouge
}
Notez le type de la valeur de retour : void
. La fonction ne retourne rien du tout. Son nom : feuRouge
et, enfin, la paire de parenthèses vide : la fonction ne prend aucun argument. Ensuite entre les accolades, nous trouvons les instructions qui forment la recette de la fonction. Utiliser la fonction, on dit l’appeler, que nous venons de définir est simple. Le fait d’avoir défini cette fonction nous fournit une nouvelle instruction : feuRouge();
que nous pouvons utiliser n’importe où dans le sketch.
Utiliser des arguments
Si le fait de remplacer les trois digitalWrite(...);
par un feuRouge();
améliore déjà la lisibilité du programme, l’emploi d’un argument va nous permettre de n’avoir qu’une fonction pour allumer la bonne DEL et éteindre les autres en fonction de l’état voulu. Il nous faut tout d’abord un moyen de donner un état. Pour cela, nous allons utiliser un enum
comme nous l’avons vu dans « Trois façons de déclarer des constantes » :
enum { VERT, JAUNE, ROUGE };
Il suffit ensuite de rajouter un argument de type byte
à notre fonction. Cet argument servira a spécifier un état pour le feu. Dans la fonction, il suffira, selon la valeur de l’argument, d’allumer la bonne DEL. Nous allons pour cela utiliser un switch ... case
, voir « Instructions conditionnelles : le switch ... case ».
void allumeFeu(const byte lequel)
{
// extinction de toutes les DEL
digitalWrite(4, LOW);
digitalWrite(5, LOW);
digitalWrite(6, LOW);
// allumage de la bonne
switch (lequel) {
case VERT: digitalWrite(4, HIGH); break;
case JAUNE: digitalWrite(5, HIGH); break;
case ROUGE: digitalWrite(6, HIGH); break;
}
// si lequel ne correspond à rien, toutes les DEL sont éteintes
}
Notez la déclaration de l’argument : const byte lequel
. const
Cela signifie que l’argument lequel
est constant à l’intérieur de la fonction, sa valeur n’y sera donc pas modifiable. byte
est le type.
Utiliser ensuite cette fonction pour un cycle d’allumage d’un feu permet un programme d’une grande limpidité comparé à une série de digitalWrite()
:
void loop()
{
allumeFeu(VERT);
delay(20000);
allumeFeu(JAUNE);
delay(2000);
allumeFeu(ROUGE);
delay(10000);
}
Un argument est passé par valeur c’est à dire que la valeur que lequel
contient est une copie de la valeur qui est passé à la fonction. Supposons par exemple que nous définissions une fonction comme ceci :
void f(byte a)
{
a = 3;
}
et que nous l’appelions de cette manière :
byte b = 7;
f(b);
l’argument a
est une copie de la variable b
. Par conséquent, la modification de a
qui intervient dans f(...)
ne touche que la copie, copie qui est ensuite oubliée lorsque f(...)
se termine. La variable b
n’est donc pas modifiée par f(...)
.
Il est bien évidemment possible d’utiliser plusieurs arguments. Enrichissons notre fonction pour pouvoir également spécifier la durée de l’allumage :
void allumeFeu(const byte lequel, const unsigned long duree)
{
// extinction de toutes les DEL
digitalWrite(4, LOW);
digitalWrite(5, LOW);
digitalWrite(6, LOW);
// allumage de la bonne
switch (lequel) {
case VERT: digitalWrite(4, HIGH); break;
case JAUNE: digitalWrite(5, HIGH); break;
case ROUGE: digitalWrite(6, HIGH); break;
}
// si lequel ne correspond à rien, toutes les DEL sont éteintes
// Attend duree millisecondes avant de sortir
delay(duree);
}
Utiliser la fonction dans loop()
devient encore plus simple :
void loop()
{
allumeFeu(VERT, 20000);
allumeFeu(JAUNE, 2000);
allumeFeu(ROUGE, 10000);
}
Nous pouvons encore faire mieux. Supposons que nous ayons plusieurs feux. La fonction allumeFeu(...)
que nous avons définie n’est plus utilisable en l’état puisqu’elle ne concerne qu’un feu. Comme chaque feu nécessite 3 broches, il serait un peu fastidieux d’ajouter 3 arguments, un pour chaque broche à la fonction. Au lieu de cela, nous allons représenter un feu par une structure comme cela a été présenté dans « Les structures » et déclarer deux constantes de ce type. Comme ceci :
struct FeuTricolore {
byte vert;
byte jaune;
byte rouge;
};
const struct FeuTricolore feu1 = { 3, 4, 5 };
const struct FeuTricolore feu2 = { 7, 8, 9 };
Nous allons ensuite rajouter un argument de type struct FeuTricolore
à notre fonction et enlever le temps d’attente :
void allumeFeu(const struct FeuTricolore feu, const byte lequel)
{
// extinction de toutes les DEL
digitalWrite(feu.vert, LOW);
digitalWrite(feu.jaune, LOW);
digitalWrite(feu.rouge, LOW);
// allumage de la bonne
switch (lequel) {
case VERT: digitalWrite(feu.vert, HIGH); break;
case JAUNE: digitalWrite(feu.jaune, HIGH); break;
case ROUGE: digitalWrite(feu.rouge, HIGH); break;
}
// si lequel ne correspond à rien, toutes les DEL sont éteintes
}
L’écriture du loop
d’une programme comme celui de « Feux tricolores » devient beaucoup facile à comprendre :
void loop()
{
// Allumage du vert sur la feu 1 et du rouge sur le feu 2
allumeFeu(feu1, VERT);
allumeFeu(feu2, ROUGE);
delay (TempsAttenteFeuVert) ; // feu1 vert pendant 30 secondes
allumeFeu(feu1, JAUNE);
delay (TempsAttenteFeuOrange) ; // durée 5 secondes
allumeFeu(feu1, ROUGE);
delay (TempsAttenteFeuRougeSeul) ; // Temporisation du chauffard !
// Concerne feu2
allumeFeu(feu2, VERT);
delay (TempsAttenteFeuVert) ; // feu1 vert pendant 30 secondes
allumeFeu(feu2, JAUNE);
delay (TempsAttenteFeuOrange) ; // durée 5 secondes
allumeFeu(feu2, ROUGE);
delay (TempsAttenteFeuRougeSeul) ; // Temporisation du chauffard !
}
Passer un argument par référence
Il arrive assez fréquemment que la fonction doivent modifier un ou plusieurs arguments afin que l’original le soit et non la copie comme nous l’avons vu précédemment. Nous désirons, par exemple, ajouter à notre feu tricolore le clignotement d’une des DEL. Nous avons besoin d’une variable d’état pour indiquer quelle DEL clignote et d’une variable permettant de mémoriser la dernière date de changement d’état. Nous avons déjà vu ce principe dans « Comment gérer le temps dans un programme ? ». Pour décrire quelle DEL clignote, il faut également que l’enum
puisse indiquer qu’aucune DEL ne clignote. Nous allons donc ajouter AUCUN
comme valeur possible de l’enum
:
enum { VERT, JAUNE, ROUGE, AUCUN };
La structure décrivant un feu reçoit deux autres membres : clignote
et dateDernierChangement
:
struct FeuTricolore {
const byte vert;
const byte jaune;
const byte rouge;
byte clignote;
unsigned long dateDernierChangement;
};
Comme nous ne pouvons plus rendre toute la structure constante, les champs constants sont déclarés const
dans la structure.
La fonction allumeFeu
reste inchangée. Nous allons ajouter une seconde fonction demarreClignotement
prenant comme premier argument le feu concerné et comme second argument la DEL concernée. Comme le membre clignote
doit être modifié par la fonction, il faut passer le premier argument par référence plutôt que par copie. Pour indiquer cette façon de passer un argument, le type de l’argument doit être suffixé par un et commercial : &
. Comme ceci :
void demarreClignotement(struct FeuTricolore& feu, byte couleur)
{
feu.clignote = couleur;
feu.dateDernierChangement = 0; // on démarre le clignotement tout de suite
}
Si nous appelons maintenant demarreClignotement
avec feu1
comme argument, les membres clignote
et dateDernierChangement
de feu1
seront modifiés par la fonction à cause du passage d’arguments par références.
Définissons maintenant une troisième fonction pour réaliser le clignotement. Comme cette fonction doit modifier dateDernierChangement
, voir « Comment gérer le temps dans un programme ? », il faut également que l’argument soit passé par référence.
void clignotement(struct FeuTricolore& feu)
{
byte pinClignote = 255; // aucune DEL ne clignote
switch (feu.clignote) {
case VERT: pinClignote = feu.vert; break;
case JAUNE: pinClignote = feu.jaune; break;
case ROUGE: pinClignote = feu.rouge; break;
}
if (pinClignote != 255)
{
unsigned long date = millis();
// le clignotement se fait à 1,1Hz
if (date - feu.dateDernierChangement > 455)
{
digitalWrite(pinClignote, ! digitalRead(pinClignote));
feu.dateDernierChangement = date;
}
}
}
clignotement
doit, bien entendu, être appelé de manière répétitive dans loop()
pour chaque feu.
Le passage par référence peut être intéressant même si l’argument n’est pas modifié. En effet, passer une variable du type de la structure
FeuTricolore
par valeur est coûteux. Cette structure occupe 8 octets et un passage par valeur nécessite leur copie. Un passage par référence ne nécessite que la copie de l’emplacement, on dit plus volontiers l’adresse, de la variable en mémoire, c’est à dire le numéro de la case où la variable est rangée. Sur un Arduino Uno, cela nécessite de copier 2 octets. Sur un Mega, cela nécessite 3 octets.Avec un passage par référence sans modification de l’argument, il est préférable de déclarer cet argument
const
Retourner un résultat
Une fonction peut fournir un résultat. Pour indiquer cela, il suffit de remplacer le void
situé avant le nom de la fonction par le type de variable retourné. On peut par exemple imaginer une fonction qui va retourner la broche du feu qui clignote ou bien 255 si aucun feu ne clignote. La broche est un byte
et la fonction retourne donc un byte
. Cette fonction serait définie comme suit :
byte pinQuiClignote(struct FeuTricolore& feu)
{
byte pinClignote = 255; // aucune DEL ne clignote
switch (feu.clignote) {
case VERT: pinClignote = feu.vert; break;
case JAUNE: pinClignote = feu.jaune; break;
case ROUGE: pinClignote = feu.rouge; break;
}
return pinClignote;
}
Notez bien le return
à la fin. C’est l’instruction qui retourne la valeur. Elle provoque également la sortie de la fonction. C’est à dire que d’éventuelles instructions qui suivraient ne seraient jamais exécutées par l’Arduino. Il est possible d’utiliser plusieurs return et on pourrait réécrire cette fonction comme suit :
byte pinQuiClignote(struct FeuTricolore& feu)
{
switch (feu.clignote) {
case VERT: return feu.vert;
case JAUNE: return feu.jaune;
case ROUGE: return feu.rouge;
}
return 255;
}
Les break
peuvent être supprimés car l’exécution de la fonction n’ira pas au delà d’un return
.
Vous avez sans doute remarqué que cette fonction est un sous ensemble de la fonction clignotement(...)
. Il est donc naturel d’appeler cette fonction au lieu de dupliquer du code identique. On peut donc réécrire clignotement(...)
comme ceci :
void clignotement(struct FeuTricolore& feu)
{
byte pinClignote = pinQuiClignote(feu);
if (pinClignote != 255)
{
unsigned long date = millis();
// le clignotement se fait à 1,1Hz
if (date - feu.dateDernierChangement > 455)
{
digitalWrite(pinClignote, ! digitalRead(pinClignote));
feu.dateDernierChangement = date;
}
}
}
C’est terminé pour les fonctions. La prochaine fois nous verrons les tableaux.