LOCODUINO

Forum de discussion
Dépôt GIT Locoduino

vendredi 24 février 2017

34 visiteurs en ce moment

Les fonctions

. Par : Jean-Luc

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 :

  1. void feuRouge()
  2. {
  3. digitalWrite(5, LOW); // éteint le feu jaune
  4. digitalWrite(6, LOW); // éteint le feu vert
  5. digitalWrite(4, HIGH); // allume le feu rouge
  6. }

Télécharger

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 » :

  1. 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 ».

  1. void allumeFeu(const byte lequel)
  2. {
  3. // extinction de toutes les DEL
  4. digitalWrite(4, LOW);
  5. digitalWrite(5, LOW);
  6. digitalWrite(6, LOW);
  7. // allumage de la bonne
  8. switch (lequel) {
  9. case VERT: digitalWrite(4, HIGH); break;
  10. case JAUNE: digitalWrite(5, HIGH); break;
  11. case ROUGE: digitalWrite(6, HIGH); break;
  12. }
  13. // si lequel ne correspond à rien, toutes les DEL sont éteintes
  14. }

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() :

  1. void loop()
  2. {
  3. allumeFeu(VERT);
  4. delay(20000);
  5. allumeFeu(JAUNE);
  6. delay(2000);
  7. allumeFeu(ROUGE);
  8. delay(10000);
  9. }

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 :

  1. void f(byte a)
  2. {
  3. a = 3;
  4. }

et que nous l’appelions de cette manière :

  1. byte b = 7;
  2. 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 :

  1. void allumeFeu(const byte lequel, const unsigned long duree)
  2. {
  3. // extinction de toutes les DEL
  4. digitalWrite(4, LOW);
  5. digitalWrite(5, LOW);
  6. digitalWrite(6, LOW);
  7. // allumage de la bonne
  8. switch (lequel) {
  9. case VERT: digitalWrite(4, HIGH); break;
  10. case JAUNE: digitalWrite(5, HIGH); break;
  11. case ROUGE: digitalWrite(6, HIGH); break;
  12. }
  13. // si lequel ne correspond à rien, toutes les DEL sont éteintes
  14.  
  15. // Attend duree millisecondes avant de sortir
  16. delay(duree);
  17. }

Utiliser la fonction dans loop() devient encore plus simple :

  1. void loop()
  2. {
  3. allumeFeu(VERT, 20000);
  4. allumeFeu(JAUNE, 2000);
  5. allumeFeu(ROUGE, 10000);
  6. }

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 :

  1. struct FeuTricolore {
  2. byte vert;
  3. byte jaune;
  4. byte rouge;
  5. };
  6.  
  7. const struct FeuTricolore feu1 = { 3, 4, 5 };
  8. 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 :

  1. void allumeFeu(const struct FeuTricolore feu, const byte lequel)
  2. {
  3. // extinction de toutes les DEL
  4. digitalWrite(feu.vert, LOW);
  5. digitalWrite(feu.jaune, LOW);
  6. digitalWrite(feu.rouge, LOW);
  7. // allumage de la bonne
  8. switch (lequel) {
  9. case VERT: digitalWrite(feu.vert, HIGH); break;
  10. case JAUNE: digitalWrite(feu.jaune, HIGH); break;
  11. case ROUGE: digitalWrite(feu.rouge, HIGH); break;
  12. }
  13. // si lequel ne correspond à rien, toutes les DEL sont éteintes
  14. }

L’écriture du loop d’une programme comme celui de « Feux tricolores » devient beaucoup facile à comprendre :

  1. void loop()
  2. {
  3. // Allumage du vert sur la feu 1 et du rouge sur le feu 2
  4. allumeFeu(feu1, VERT);
  5. allumeFeu(feu2, ROUGE);
  6. delay (TempsAttenteFeuVert) ; // feu1 vert pendant 30 secondes
  7.  
  8. allumeFeu(feu1, JAUNE);
  9. delay (TempsAttenteFeuOrange) ; // durée 5 secondes
  10.  
  11. allumeFeu(feu1, ROUGE);
  12. delay (TempsAttenteFeuRougeSeul) ; // Temporisation du chauffard !
  13.  
  14. // Concerne feu2
  15. allumeFeu(feu2, VERT);
  16. delay (TempsAttenteFeuVert) ; // feu1 vert pendant 30 secondes
  17.  
  18. allumeFeu(feu2, JAUNE);
  19. delay (TempsAttenteFeuOrange) ; // durée 5 secondes
  20.  
  21. allumeFeu(feu2, ROUGE);
  22. delay (TempsAttenteFeuRougeSeul) ; // Temporisation du chauffard !
  23. }

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 :

  1. enum { VERT, JAUNE, ROUGE, AUCUN };

La structure décrivant un feu reçoit deux autres membres : clignote et dateDernierChangement :

  1. struct FeuTricolore {
  2. const byte vert;
  3. const byte jaune;
  4. const byte rouge;
  5. byte clignote;
  6. unsigned long dateDernierChangement;
  7. };

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 :

  1. void demarreClignotement(struct FeuTricolore& feu, byte couleur)
  2. {
  3. feu.clignote = couleur;
  4. feu.dateDernierChangement = 0; // on démarre le clignotement tout de suite
  5. }

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.

  1. void clignotement(struct FeuTricolore& feu)
  2. {
  3. byte pinClignote = 255; // aucune DEL ne clignote
  4. switch (feu.clignote) {
  5. case VERT: pinClignote = feu.vert; break;
  6. case JAUNE: pinClignote = feu.jaune; break;
  7. case ROUGE: pinClignote = feu.rouge; break;
  8. }
  9.  
  10. if (pinClignote != 255)
  11. {
  12. unsigned long date = millis();
  13. // le clignotement se fait à 1,1Hz
  14. if (date - feu.dateDernierChangement > 455)
  15. {
  16. digitalWrite(pinClignote, ! digitalRead(pinClignote));
  17. feu.dateDernierChangement = date;
  18. }
  19. }
  20. }

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 :

  1. byte pinQuiClignote(struct FeuTricolore& feu)
  2. {
  3. byte pinClignote = 255; // aucune DEL ne clignote
  4. switch (feu.clignote) {
  5. case VERT: pinClignote = feu.vert; break;
  6. case JAUNE: pinClignote = feu.jaune; break;
  7. case ROUGE: pinClignote = feu.rouge; break;
  8. }
  9. return pinClignote;
  10. }

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 :

  1. byte pinQuiClignote(struct FeuTricolore& feu)
  2. {
  3. switch (feu.clignote) {
  4. case VERT: return feu.vert;
  5. case JAUNE: return feu.jaune;
  6. case ROUGE: return feu.rouge;
  7. }
  8. return 255;
  9. }

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 :

  1. void clignotement(struct FeuTricolore& feu)
  2. {
  3. byte pinClignote = pinQuiClignote(feu);
  4.  
  5. if (pinClignote != 255)
  6. {
  7. unsigned long date = millis();
  8. // le clignotement se fait à 1,1Hz
  9. if (date - feu.dateDernierChangement > 455)
  10. {
  11. digitalWrite(pinClignote, ! digitalRead(pinClignote));
  12. feu.dateDernierChangement = date;
  13. }
  14. }
  15. }

C’est terminé pour les fonctions. La prochaine fois nous verrons les tableaux.

Réagissez à « Les fonctions »

Qui êtes-vous ?
Votre message

Pour créer des paragraphes, laissez simplement des lignes vides.

Lien hypertexte

(Si votre message se réfère à un article publié sur le Web, ou à une page fournissant plus d’informations, vous pouvez indiquer ci-après le titre de la page et son adresse.)

Rubrique « Programmation »

Comment gérer le temps dans un programme ?

La programmation, qu’est ce que c’est

Types, constantes et variables

Installation de l’IDE arduino

Répéter des instructions : les boucles

Les interruptions (1)

Instructions conditionnelles : le if ... else

Instructions conditionnelles : le switch ... case

Comment gérer l’aléatoire ?

Calculer avec l’Arduino (1)

Calculer avec l’Arduino (2)

Les structures

Systèmes de numération

Les fonctions

Trois façons de déclarer des constantes

Transcription d’un programme simple en programmation objet

Les chaînes de caractères

Trucs, astuces et choses à ne pas faire !

Le monde des objets (1)

Le monde des objets (2)

Le monde des objets (3)

Le monde des objets (4)

Les pointeurs (1)

Les pointeurs (2)

Les Timers (I)

Les Timers (II)

Les Timers (III)

Les Timers (IV)

Les Timers (V)

Les derniers articles

Les Timers (V)


Christian Bézanger

Trucs, astuces et choses à ne pas faire !


Dominique

Le monde des objets (4)


Thierry

Les pointeurs (2)


Thierry

Les chaînes de caractères


Thierry

Transcription d’un programme simple en programmation objet


Dominique, Guillaume, Jean-Luc

Les pointeurs (1)


Thierry

Comment gérer l’aléatoire ?


Dominique, Guillaume, Jean-Luc

Les Timers (IV)


Christian Bézanger

Les fonctions


Jean-Luc

Les articles les plus lus

Les interruptions (1)

Les Timers (I)

Comment gérer le temps dans un programme ?

Calculer avec l’Arduino (1)

Instructions conditionnelles : le if ... else

Répéter des instructions : les boucles

Les Timers (III)

Installation de l’IDE arduino

Types, constantes et variables

Les Timers (II)