LOCODUINO

Aide
Forum de discussion
Dépôt GIT Locoduino
Flux RSS

mardi 19 mars 2024

Visiteurs connectés : 38

Les fonctions

.
Par : Jean-Luc

DIFFICULTÉ :

<

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.

11 Messages

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 »

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)

Bien utiliser l’IDE d’Arduino (1)

Bien utiliser l’IDE d’Arduino (2)

Piloter son Arduino avec son navigateur web et Node.js (1)

Piloter son Arduino avec son navigateur web et Node.js (2)

Piloter son Arduino avec son navigateur web et Node.js (3)

Piloter son Arduino avec son navigateur web et Node.js (4)

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 concevoir rationnellement votre système

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

Ces tableaux qui peuvent nous simplifier le développement Arduino

Les chaînes de caractères

Trucs, astuces et choses à ne pas faire !

Processing pour nos trains

Arduino : toute première fois !

Démarrer en Processing (1)

TCOs en Processing (1)

TCOs en Processing (2)

L’assembleur (1)

L’assembleur (2)

L’assembleur (3)

L’assembleur (4)

L’assembleur (5)

L’assembleur (6)

L’assembleur (7)

L’assembleur (8)

L’assembleur (9)

Les derniers articles

L’assembleur (9)


Christian

L’assembleur (8)


Christian

L’assembleur (7)


Christian

L’assembleur (6)


Christian

L’assembleur (5)


Christian

L’assembleur (4)


Christian

L’assembleur (3)


Christian

L’assembleur (2)


Christian

L’assembleur (1)


Christian

TCOs en Processing (2)


Pierre59

Les articles les plus lus

Les Timers (I)

Les interruptions (1)

Instructions conditionnelles : le if ... else

Ces tableaux qui peuvent nous simplifier le développement Arduino

Bien utiliser l’IDE d’Arduino (1)

Comment gérer le temps dans un programme ?

Les structures

Calculer avec l’Arduino (1)

Instructions conditionnelles : le switch ... case

Les Timers (II)