Aller au contenu principal

Les fonctions

Ce que ce chapitre apporte

  • Découper un programme en fonctions nommées d'après ce qu'elles font.
  • Déclarer une fonction avec des paramètres et une valeur de retour, puis l'appeler.
  • Distinguer une fonction qui agit sur la carte d'une fonction qui calcule une valeur.
  • Prévoir l'effet d'un passage par valeur sur la variable de l'appelant.
  • Passer un tableau et sa taille à une fonction.

Un programme Arduino qui grossit devient vite illisible, parce que tout s'entasse dans loop(). Une fonction donne un nom à un morceau de traitement : allumerSeul(VERT, 3000) se lit, trois digitalWrite suivis d'un delay demandent à être déchiffrés. C'est la différence entre un programme qu'on relit et un programme qu'on réécrit. Ce chapitre montre aussi deux comportements du C qui surprennent : une fonction ne modifie pas les variables qu'on lui passe, et un tableau n'y arrive jamais entier.

Donner un nom à un traitement

Un feu tricolore écrit sans fonction répète la même séquence trois fois : éteindre les trois LEDs, en allumer une, attendre. Une fonction la décrit une fois.

typeRetour nomDeLaFonction(type parametre1, type parametre2) {
  // instructions
  return valeur;          // absent si le type de retour est void
}
Arduino
const int ROUGE = 9, ORANGE = 10, VERT = 11;
void allumerSeul(int broche, unsigned long duree) {
digitalWrite(ROUGE, LOW);
digitalWrite(ORANGE, LOW);
digitalWrite(VERT, LOW);
digitalWrite(broche, HIGH); // seule la LED demandée reste allumée
delay(duree);
}
void setup() {
pinMode(ROUGE, OUTPUT);
pinMode(ORANGE, OUTPUT);
pinMode(VERT, OUTPUT);
}
void loop() {
allumerSeul(VERT, 3000);
allumerSeul(ORANGE, 1000);
allumerSeul(ROUGE, 3000);
}

loop() se lit maintenant comme la description du feu. Et la fonction garantit une propriété que le code recopié ne garantissait pas : à aucun instant deux feux ne sont allumés ensemble.

Définitions

Les paramètres sont les variables déclarées dans l'en-tête de la fonction : broche et duree.

Les arguments sont les valeurs fournies à l'appel : VERT et 3000. Chaque argument est copié dans le paramètre correspondant, dans l'ordre.

Où placer les fonctions
L'environnement Arduino ajoute automatiquement les déclarations manquantes, ce qui permet d'écrire une fonction après loop() et de l'appeler avant. En C++ ordinaire, une fonction doit être connue avant d'être appelée. Placer les fonctions avant setup() fonctionne dans les deux cas.

Agir ou calculer

Deux sortes de fonctions coexistent, et les distinguer rend un programme plus facile à corriger.

Une fonction qui agit modifie l'état de la carte : elle allume, attend, envoie un message. Elle ne rend rien, et son type de retour est void.

Une fonction qui calcule reçoit des valeurs et en rend une autre par return, sans rien toucher d'autre. Sa réponse ne dépend que de ses arguments.

Arduino
// Calcule : convertit une lecture du capteur de température LM35 en degrés Celsius.
float enCelsius(int lecture) {
float tension = lecture * (5.0 / 1023.0); // la lecture ramenée en volts
return tension * 100.0; // le LM35 délivre 10 mV par degré
}
void setup() {
Serial.begin(9600);
}
void loop() {
float temperature = enCelsius(analogRead(A0));
Serial.println(temperature, 1); // une décimale
delay(1000);
}
Pourquoi une fonction de calcul n'affiche rien
enCelsius pourrait afficher le résultat au lieu de le rendre. Elle deviendrait alors inutilisable dans un test de seuil, dans une moyenne, dans un envoi sur une autre liaison. Rendre la valeur laisse à l'appelant le choix de ce qu'il en fait. Pour une lecture de 62, elle rend 30,3.

Le passage par valeur

Définition

En C, un argument est copié dans le paramètre. La fonction travaille sur cette copie : modifier le paramètre ne modifie pas la variable de l'appelant.

Arduino
void doubler(int x) {
x = x * 2; // modifie la copie seulement
}
int doublee(int x) {
return x * 2; // rend le résultat
}
void setup() {
Serial.begin(9600);
int n = 21;
doubler(n);
Serial.println(n); // 21 : n n'a pas changé
n = doublee(n);
Serial.println(n); // 42 : c'est l'appelant qui range le résultat
}
void loop() {}

Pour qu'une fonction change une valeur, elle la rend, et c'est l'appelant qui l'affecte. Cette règle a une exception, et elle concerne les tableaux.

Passer un tableau

Un tableau n'est pas copié quand on le passe à une fonction : celle-ci reçoit seulement l'adresse de son premier élément. Deux conséquences en découlent.

La fonction peut modifier les éléments du tableau de l'appelant, puisqu'elle travaille sur la même mémoire. Et elle ne connaît pas la taille du tableau : sizeof y mesure l'adresse, pas le tableau. D'où la règle : la taille voyage toujours avec le tableau, en second paramètre.

Arduino
float moyenne(const int valeurs[], int n) { // const : la fonction s'interdit de modifier le tableau
long somme = 0;
for (int i = 0; i < n; i++) {
somme += valeurs[i];
}
return (float)somme / n;
}
int lectures[] = {512, 498, 530, 505};
const int NB = sizeof(lectures) / sizeof(lectures[0]); // calculé là où le tableau est déclaré
void setup() {
Serial.begin(9600);
Serial.println(moyenne(lectures, NB));
}
void loop() {}
sizeof dans la fonction
Écrire sizeof(valeurs) / sizeof(valeurs[0]) à l'intérieur de moyenne compile sans avertissement. Sur l'Uno, une adresse occupe 2 octets, comme un int : le quotient vaut 1, et la fonction calcule la « moyenne » du seul premier élément, quelle que soit la taille du tableau.

Exercices type

Pourquoi la fonction allumerSeul éteint-elle les trois LEDs avant d'en allumer une, plutôt que d'éteindre seulement la précédente ?

Parce qu'elle ne sait pas quelle LED était allumée avant son appel. En éteignant tout, elle garantit l'état final quel que soit l'état de départ : une seule LED allumée, celle qui est demandée.

Une version qui éteindrait « la précédente » dépendrait de l'ordre des appels dans loop(). Le jour où cet ordre change, deux feux se retrouvent allumés ensemble, et le défaut ne se voit que sur le montage.

Après int n = 21; doubler(n);, n vaut toujours 21. Comment corriger sans changer le rôle de la fonction ?

doubler reçoit une copie de n et ne modifie que cette copie. La correction consiste à lui faire rendre le résultat, et à l'affecter chez l'appelant :

Arduino
int doublee(int x) {
return x * 2;
}
void setup() {
int n = 21;
n = doublee(n); // n vaut 42
}
void loop() {}
Une fonction moyenne(int valeurs[]) calcule la taille avec sizeof(valeurs) / sizeof(valeurs[0]). Sur un tableau de quatre lectures, elle rend la première lecture. Pourquoi ?

Dans la fonction, valeurs n'est pas le tableau mais l'adresse de son premier élément. Sur l'Uno, une adresse occupe 2 octets, un int aussi : le quotient vaut 1, et la boucle ne traite qu'un élément.

La taille se calcule là où le tableau est déclaré, puis se passe en paramètre : moyenne(lectures, NB).

Le capteur LM35 renvoie une lecture de 62 sur A0. Que rend enCelsius(62) ?

La tension vaut 62×5/10230,30362 \times 5 / 1023 \approx 0{,}303 V. Le LM35 délivrant 10 mV par degré, cela fait 30,3 °C.

Une fonction de calcul se vérifie ainsi, à la main sur une valeur connue, avant d'être branchée sur un capteur réel. C'est précisément ce que permet le fait qu'elle rende sa valeur au lieu de l'afficher.

Vérification

Vérification rapideon peut se reprendre

1.Quel type de retour prend une fonction qui allume une LED sans rien rendre ?

2.Après void mettreAZero(int x) { x = 0; } et l'appel mettreAZero(n), que vaut n ?

3.Que reçoit une fonction à laquelle on passe un tableau ?

4.Pourquoi préférer qu'une fonction de conversion rende sa valeur plutôt que de l'afficher ?

5.Dans l'environnement Arduino, peut-on appeler une fonction définie plus bas que loop() ?

La méthode

  1. Repérer les séquences répétées dans loop() et leur donner un nom de verbe qui dit ce qu'elles font.
  2. Décider si la fonction agit ou calcule : une fonction qui calcule rend sa valeur et n'affiche rien.
  3. Garantir l'état final dans une fonction qui agit, sans supposer ce qui a été fait avant son appel.
  4. Faire rendre le résultat quand une valeur doit changer, et l'affecter chez l'appelant.
  5. Passer la taille avec tout tableau, calculée là où le tableau est déclaré.
  6. Vérifier une fonction de calcul à la main sur une valeur connue avant de la brancher sur un capteur.

Synthèse

  • Une fonction donne un nom à un traitement ; loop() devient alors la description du comportement.
  • Les arguments sont copiés dans les paramètres, dans l'ordre.
  • Une fonction qui agit est de type void ; une fonction qui calcule rend sa valeur par return.
  • Passage par valeur : modifier un paramètre ne modifie pas la variable de l'appelant.
  • Un tableau passé à une fonction n'y arrive que par son adresse : la fonction peut le modifier, mais ignore sa taille.
  • sizeof dans une fonction mesure une adresse ; la taille se passe en paramètre.
  • Placer les fonctions avant setup() fonctionne dans tous les environnements.