Ce tutoriel est en cours de rédaction. Merci pour votre compréhension !

Les fonctions

En mathématiques, une fonction est l’expression formelle d’un calcul qui sera tout le temps réalisé de la même façon, mais dont le résultat peut varier selon un ou des paramètres. Par exemple, pour un seul paramètre noté x, considérons la fonction f(x) = 5x + 4 :

  • Pour x = 1, le résultat sera : f(1) = 5 x 1 + 4 = 9
  • Pour x = 2, le résultat sera : f(2) = 5 x 2 + 4 = 14
  • Pour x = 3, le résultat sera : f(3) = 5 x 3 + 4 = 19
  • Etc.

C’est ce principe qui est utilisé en programmation C/C++, avec quelques aménagements pratiques. Une fonction, également désignée sous le nom de procédure ou de sous-routine, est un “bloc” comprenant des instructions que l’on peut appeler à tout endroit du programme. Pour reprendre notre exemple ci-dessus, la même fonction en C/C++ aurait le listing suivant :

int32_t somme(int32_t x)
{
    return 5 * x + 4;
}

Détaillons la structure de cette fonction :

  • Elle est déclarée avec un type int32_t qui indique quelles valeurs elle peut renvoyer ;
  • Son nom est somme ;
  • Elle accepte un paramètre x de type int32_t ;
  • Ses instructions sont dans un bloc délimité par des accolades { } (comme tous les blocs en C/C++) ;
  • Elle renvoie le résultat du calcul 5 * x + 4 à l’aide de l’instruction return.

L’objectif premier des fonctions dans un langage informatique est de simplifier l’écriture des programmes en évitant de recopier plusieurs fois les séquences d’instructions qu’elles contiennent.

Valeurs de retour des fonctions

On peut distinguer deux types de fonctions selon les valeurs qu’elles retournent (ou pas).

Les fonctions qui renvoient un résultat

Le but d’une fonction est généralement d’effectuer un calcul, une comparaison, un traitement de de nous donner un résultat. Il est donc impératif d’anticiper le type du résultat (est-ce un nombre ? Un caractère ? Etc.) et de le préciser dans la déclaration de la fonction, par exemple :

int32_t somme()
{
    return 5 + 2;
}

Ou bien encore :

float somme()
{
    return 5.3 + 2.7;
}

En passant, on constate également que ces fonctions n’ont également pas de paramètres (voir plus loin).

Les fonctions qui ne renvoient pas de résultat

Une fonction effectue toujours un traitement, mais il n’est pas forcément nécessaire qu’elle renvoie un résultat. Dans ces circonstances, l’instruction return ne sera pas utilisée. Il sera néanmoins nécessaire de mentionner le type void comme valeur de retour de la fonction. Par exemple :

void afficher() // ne retourne rien, donc type void
{
    // Affiche le texte "Hello" suivi d'un saut de ligne sur le terminal série de l'IDE Arduino
    Serial.println("Hello");
}

Une fonction de type void peut bien sûr réaliser des calculs utiles au reste du programme, en modifiant le contenu de variables globales, par exemple comme ceci :

// Constantes
#define PI 3.14159265359
#define RAYON 200

// Variables globales
float perimetre = 0;
float surface = 0;

// Initialisations
void setup() {
  // calcule le périmètre et la surface du cercle
  calculs_cercle();
}

// Boucle principale
void loop() {
  ... //  Code non détaillé ici
}

// Fonction de calcul du périmètre et de la surface
void calculs_cercle()
{
    perimetre = 2 * PI * RAYON;
    surface = PI * RAYON * RAYON;
}

On constate que la fonction est utilisée dans le sketch en y incluant tout simplement son nom (on en reparle plus loin).

Paramètres des fonctions

Nous avons déjà vu, sans le dire, la notion de paramètres d’une fonction dans les paragraphes qui précèdent.

Les fonctions avec paramètres

Le (ou les) paramètres d’une fonction en C/C++ jouent exactement le même rôle que les variables pour les fonctions mathématiques. Le compilateur devra savoir ce que votre fonction manipule, donc il sera obligatoire de renseigner le type des paramètres. Par exemple, cette fonction calculera la surface d’un cercle avec son rayon passé en paramètre :

float surface_cercle(float rayon) // la fonction prend le float rayon en paramètre
{
    return 3.14159265359 * rayon * rayon; // Effectue le calcul en utilisant le paramètre rayon
}

Une fonction peut bien sûr avoir plus d’un paramètre, par exemple :

// Calcule l'ordonnée sur une droite d'équation y = a x + b
// La fonction prend trois paramètres en entrée
float ordonnee_droite(float x, float a, float b) 
{
    return a * x + b; 
}

Les fonctions sans paramètres

Une fonction peut aussi n’avoir aucun paramètre, pour reprendre un exemple déjà donné :

void afficher() // Ne retourne rien, donc type void et aucun paramètre
{
    Serial.println("Hello");
}

Manipuler des tableaux avec des fonctions

Pour ce sujet, nous vous renvoyons au tutoriel sur les tableaux.

Utiliser une fonction

Pour appeler une fonction, il suffit d’écrire son nom (sans le type) à l’emplacement souhaité dans le sketch, sans oublier les éventuels paramètres, dans le bon ordre et sans préciser leur type non plus.
Par exemple, imaginons que la tension lue sur la broche analogique A0 soit liée à la température ambiante par une relation linéaire. Le sketch suivant affiche la température (fictive !) toutes les secondes :

01 #define BROCHE A0          // Broche analogique à lire
02 #define DELAI 1000         // Intervalle de temps entre deux conversions de l'ADC
03 #define SERIAL_SPEED 9600  // Débit, en bits/s, du port série connecté à l'IDE Arduino
04 #define VREF_ADC 3.3       // Tension de référence de l'ADC
05 #define RES_ADC 1024       // Valeur max possiblement renvoyée par l'ADC
06
07 // Précalcul paramètres de l'équation linéaire pour l'estimation de la température
08 const float coeff_dir = -1.3 * (VREF_ADC / RES_ADC);
09 const float ordo_orig = 12.5;
10
11 // Initialisations
12 void setup() {
13   Serial.begin(SERIAL_SPEED);  // Initialisation du port série connecté à l'IDE Arduino
14   pinMode(BROCHE, INPUT);      // On active l'ADC câblé sur BROCHE
15 }
16 
17 // Boucle principale
18 void loop() {
19 
20 // Lecture de la valeur analogique sur BROCHE
21  uint16_t valeur_analogique = analogRead(BROCHE);
22
23 // Estimation de la température en appelant la fonction ordonnee_droite
24 float temperature = ordonnee_droite(valeur_analogique, coeff_dir, ordo_orig);
25
26 // Affichage de la température estimée sur le port série de l'IDE Arduino
27 Serial.print("Température estimée : "); // Ecriture de la chaîne "Température estimée : " sur le port série
28 Serial.print(temperature, 1); // Ecriture de la valeur de la température avec une seule décimale, à la suite sur le port série
29 Serial.println("C"); //  Ecriture de la chaîne "C" à la suite sur le port série, puis saut de ligne
30
31 // Temporisation de DELAI millisecondes
32 delay(DELAI);
33 }
34
35 // Calcul de l'ordonnée au point x pour une droite ayant pour coefficient
36 // directeur a et pour ordonnée à l'origine b.
37 // L'opération de "cast" (float)x convertit x du type "uint16_t" vers le type "float"
38 float ordonnee_droite(uint16_t x, float a, float b) {
39   return a * (float)x + b;
40 }

Commentaires

  • Les lignes 01 à 09 déclarent des constantes de deux façons différentes. Référez-vous à cette page pour en savoir plus.

  • Les lignes 13, 14, 21, 27 à 29 et 32 font toutes appel à des fonctions prédéfinies du framework Arduino, sujet que nous aborderons juste après.

  • La ligne 24 appelle notre fonction ordonnee_droite et enregistre son retour dans la variable temperature. On constate que les paramètres passés à la fonction n’ont pas les mêmes noms que ceux utilisés à la ligne 38 où le code de la fonction est écrit. Ce n’est pas un problème, vous êtes tout à fait libre d’utiliser les mêmes noms de paramètres dans une fonction et dans le programme qui l’appelle, ou pas.

  • Les types des arguments passés à ordonnee_droite et de son retour, ne figurent pas dans son appel (ligne 24) mais sont indiqués dans son code source (ligne 38 en particulier, le code source de la fonction occupant les lignes 38 à 40).

  • Attention, l’ordre (et le type) des arguments doit être respecté dans l’appel de la fonction, conformément à ce qui est indiqué dans son code source.
    Ainsi, dans notre exemple, la fonction calcule coeff_dir * (float)valeur_analogique + ordo_orig).
    Si la ligne 24 était écrite de cette façon float temperature = ordonnee_droite(valeur_analogique, ordo_orig, coeff_dir); alors la fonction calculerait ordo_orig * (float)valeur_analogique + coeff_dir). Cela donnerait un résultat complètement différent.

  • Vous vous interrogez peut-être sur ce que font les lignes 13, 27, 28 et 29 ?
    La ligne 13 active l’un des contrôleurs de communication série (ou UART) intégré dans le microcontrôleur et précise son débit, en bauds. L’UART concerné est câblé sur le port USB qui relie votre carte à microcontrôleur à votre ordinateur, ce qui permet à l’IDE Arduino d’afficher des messages dans son moniteur série.
    Les lignes 27 à 29 sont justement là pour indiquer quels messages l’UART doit envoyer au moniteur série et comment ceux-ci doivent être formattés.
    Si vous suivez les instructions pour compiler et exécuter le sketch sur une carte à microcontrôleur, vous observerez des messages de cette forme sur le moniteur série :

      07:11:55.828 -> Température estimée : 11.9C
      07:11:56.810 -> Température estimée : 11.7C
      07:11:57.805 -> Température estimée : 11.8C
      07:11:58.792 -> Température estimée : 11.9C
      07:11:59.786 -> Température estimée : 11.8C
      ...
    

Les fonctions prédéfinies du framework Arduino

Fonctions obligatoires

Un croquis (sketch) Arduino comprend obligatoirement les deux fonctions suivantes :

  • void setup(). Le rôle principal de cette fonction est d’initialiser les périphériques internes du microcontrôleur (par exemple, la définition des broches en tant qu’entrées ou sorties) et les périphériques externes à celui-ci (autres composants électroniques tels que capteurs, modules GPS, circuits de puissance, etc.) éventuellement soudés sur la carte électronique utilisée. Elle réalise également toutes les opérations qui n’ont besoin d’être éxécutées qu’une seule fois (par exemple le démarrage de classes-drivers, l’initialisation de variables globales, la calibration de capteurs …) au démarrage de la carte.

  • void loop(). Cette fonction est une boucle sans fin qui contient le code actif. Les instructions placées à l’intérieur de cette boucle sont exécutées et répétées indéfiniment. Le programme principal est presque exclusivement exécuté à l’intérieur car le fonctionnement du microcontrôleur (MCU) n’est pas géré par un système d’exploitation. Si un programme comportait un point d’arrêt final, le MCU s’arrêterait une fois cette fin atteinte et ne pourrait reprendre sa tâche qu’après une réinitialisation (reset). Le programme principal ne doit don pas quitter cette boucle car elle garantit que le microprocesseur du MCU continuera à travailler tant qu’il n’aura pas été mis hors tension.

Et les interruptions alors ?

A l’origine, le framework Arduino a été développé pour rendre la programmation de cartes à MCU accessible à des débutants. Pour cette raison, le modèle mis en avant dans la plupart des croquis repose exclusivement sur l’approche de la scrutation (ou polling en anglais), c’est à dire sur des programmes qui sont exécutés dans une boucle infinie loop() comme nous venons de l’expliquer.
Cette approche sacrifie sur l’autel de la simplicité d’apprentissage LE mécanisme le plus puissant offert par les MCU : l’exécution asynchrone via interruptions. Il est en effet possible de programmer le MCU de sorte qu’il mette en pause la fonction loop() pour exécuter pendant un bref instant une autre fonction, appelée routine de service d’interruption, en réponse à un évènement non prévisiible (un robot détecte un obstacle devant lui, un utilisateur apppuie sur un bouton, un accéléromètre enregistre un choc, etc.).
Arduino n’interdit cependant pas l’usage des interruptions, et nous en faisons la démonstration dans de nombreux exemples.

Fonctions spécifiques

A TERMINER

Liens et ressources

Notes au fil du texte