Faire plusieurs choses à la fois avec une carte Arduino

Mais une chose après l'autre

Image d'entête

par skywodd | | Licence (voir pied de page)

Catégories : Tutoriels Arduino | Mots clefs : Arduino Genuino Temps Multitâche

Cet article n'a pas été mis à jour depuis un certain temps, son contenu n'est peut être plus d'actualité.


Dans ce tutoriel, nous allons voir ensemble comment faire plusieurs choses "en même temps" avec une carte Arduino. Pour être précis, nous verrons comment construire un code non bloquant, capable de gérer plusieurs tâches simples en parallèle sans que ces tâches se bloquent mutuellement.

Sommaire

Bonjour à toutes et à tous !

Dans mon précédent article, nous avons vu ensemble comment faire des temporisations. Ces temporisations permettent de mettre en pause le programme pendant une durée définie. Nous avons aussi vu comment obtenir un temps relatif au démarrage du programme Arduino. J'avais alors fait remarquer qu'il était possible d'utiliser ce temps pour faire des temporisations non bloquantes, sans pour autant entrer dans les détails.

Dans ce tutoriel, nous allons voir comment faire une temporisation non bloquante. Nous mettrons ensuite en parallèle plusieurs de ces temporisations pour démontrer cet aspect "non bloquant".

Rappel : fonction bloquante et fonction non bloquante

Avant de commencer, revenons ensemble sur deux notions fondamentales en informatique : les fonctions bloquantes et les fonctions non bloquantes.

Une fonction "bloquante" est une fonction qui bloque l'exécution du programme durant son déroulement. Tant que la fonction n'est pas terminée, le reste du programme est en attente.

Une fonction "non bloquante" est une fonction qui ne bloque pas l'exécution du programme durant son déroulement. Ce type de fonction est très utile pour faire du temps réel.

Illustration fonctions bloquantes / non bloquantes

Fonction bloquante VS fonction non bloquante

Si une fonction attend quelque chose avant de faire son traitement pour une quelconque raison, on dit que cette fonction est bloquante.

Si au contraire, la fonction vérifie qu'une information ou une condition est bonne avant de faire son traitement, puis fait le traitement sans attendre quoi que ce soit, on dit qu'il s'agit d'une fonction non bloquante.

Pour illustrer cette différence, prenons un exemple. Vous avez un programme avec une fonction qui permet à l'utilisateur de faire un choix avec une série de boutons et un bouton "valider".

La version bloquante de la fonction consisterait à attendre que le bouton "valider" soit appuyé avant de lire les autres boutons et de retourner le choix de l'utilisateur.

La version non bloquante de la fonction consisterait à vérifier que le bouton "valider" est appuyé, et dans ce cas (et uniquement dans ce cas), de lire les autres boutons et de retourner le choix de l'utilisateur. Dans le cas où le bouton ne serait pas appuyé, la fonction retournerait une valeur spéciale pour prévenir le code parent qu'il va falloir rappeler la fonction plus tard.

Le multitâche c'est quoi exactement ?

Maintenant que nous avons revu les notions de base, posons-nous la question suivante avant de mettre les mains dans le code : qu'est-ce qu'être multitâche concrètement ?

Si l'on se limite à la définition du dictionnaire, être "multitâche" désigne la capacité d'une personne ou d'un système à "exécuter plusieurs tâches en parallèle".

On remarquera que la définition de "multitâche" ne précise pas que les tâches doivent être exécutées en parallèle simultanément. C'est une toute petite subtilité en apparence, mais dans les faits cela change tout.

Illustration multitâches multiprocesseurs

Système multitâches multiprocesseurs

Pour faire du "vrai" multitâche, avec plusieurs tâches exécutées simultanément, il faut avoir plusieurs processeurs pour exécuter plusieurs tâches simultanément, indépendamment les unes des autres.

Si le multitâche en informatique se limité à avoir plusieurs processeurs, nos PC actuels ne seraient capable de faire tourner que 4 programmes en parallèle dans le meilleur des cas, enfin 3 programmes plus le système d'exploitation. Cela serait un peu ennuyeux, terriblement inefficace et pas du tout pratique.

Vous vous en doutez, nos chers scientifiques et informaticiens ont trouvé une astuce ;)

Pour être précis, il y a deux astuces : le multitâche coopératif et le multitâche préemptif.

Dans les deux cas, l'idée est simple : au lieu d'attribuer une tâche par processeur, on alterne plusieurs tâches sur un même processeur. Chaque tâche est exécutée en séquence, les unes après les autres suivant un ordre et une logique bien définie.

Rembourser !

Certains crieront à la publicité mensongère, prétextant que le titre de ce tutoriel est une vile escroquerie, qu'il ne s'agit pas de multitâche, mais bien de séquencement de tâches.

Le fait est qu'en informatique et plus particulièrement en informatique embarquée, sur microcontrôleurs, avec un unique processeur, le "vrai" multitâche est impossible. Mais comme on le verra dans le chapitre suivant, on n’a pas forcément besoin de plusieurs processeurs pour faire plusieurs choses à la fois. ;)

Le multitâche coopératif

Le multitâche coopératif part d'un constat évident en informatique : un programme informatique passe le plus clair de son temps à attendre des ressources externes (saisie utilisateur, accès à un disque dur, communication réseau, etc.).

Les programmes qui utilisent constamment 100% du temps processeur sont très rares, tout le monde ne calcule pas des modèles mathématiques complexes sur son PC de bureau.

Le processeur est donc au repos une grande partie du temps, en attente d'une ressource externe. Pourquoi ne pas profiter de ce temps libre pour exécuter une autre tâche ? C'est le principe du multitâche coopératif.

Illustration multitâche coopératif

Système multitâche coopératif

Avec le multitâche coopératif, chaque tâche donne la main aux autres tâches dès qu'elle a fini son traitement ou qu'elle passe en attente d'une ressource externe. C'est la forme la plus simple de multitâche.

Avantages :

  • Simple à implémenter.

Inconvénients :

  • Si une tâche plante et ne rend jamais la main, toutes les autres tâches sont bloquées.

  • Chaque tâche attend que la précédente lui donne la main, ce qui crée des délais entre le déclenchement d'un événement et son traitement.

  • Plus le nombre de tâches augmente, plus le code devient un sac de noeuds impossible à démêler, car chaque tâche gère son propre séquencement.

  • Si vous avez plusieurs processeurs disponibles, ce type de multitâche est un cauchemar à paralléliser.

N.B. Dans les chapitres suivants, nous implémenterons une version très basique de ce genre de multitâche.

Le multitâche préemptif

Le multitâche préemptif prend une direction complètement opposée au multitâche coopératif.

Illustration multitâche préemptif

Système multitâche préemptif

L'idée du multitâche préemptif est d'utiliser une horloge pour découper le temps processeur en fenêtres de N millisecondes durant lesquelles une tâche peut s'exécuter avant de se faire remplacer au profit d'une autre tâche.

Quand une tâche passe en attente d'une ressource, le séquenceur passe à la tâche suivante. Quand la fenêtre de temps d'exécution d'une tâche arrive à terme, le séquenceur passe à la tâche suivante. Avec du multitâche préemptif, le séquenceur de tâches passe son temps à jongler avec les différentes tâches.

Avantages :

  • Une tâche longue (ou plantée) ne bloque pas les autres tâches.

  • Le nombre de tâches n'a pas d'incidence sur la lisibilité du code global, car toute la logique de gestion des tâches se retrouve centralisée dans un séquenceur, véritable dictateur informatique.

  • Le séquenceur peut utiliser pleinement tous les processeurs disponibles s'il y en a plus d'un.

Inconvénients :

  • Grosse complexité d'implémentation.

  • Le choix de l'algorithme de séquencement influe énormément sur la réactivité du système. Round robin, Rate monotonic scheduling, Earliest deadline first scheduling, FIFO, Shortest job first, Completly fair scheduling, il y a des dizaines d'algorithmes de séquencement possible, chacun avec ses avantages et ses inconvénients.

  • Le séquenceur passe son temps à jongler avec les tâches, sauvegardant et restaurant à chaque fois l'environnement d'exécution de chacune des tâches. Cela prend du temps et à un coût en terme de puissance de calcul.

  • Horloge matérielle précise obligatoire.

N.B. Bien que cela soit réservé à des cas très précis, il est possible de faire du multitâche préemptif sur microcontrôleurs. Des bibliothèques de code nommées RTOS existent pour cela, la plus connue est sans aucun doute FreeRTOS.

Pour bien visualiser la différence entre multitâches coopératifs et multitâches préemptif, voici un petit parallèle dans la vie de tous les jours.

Vous avez envie de jouer à un jeu vidéo, mais vous avez aussi un devoir à rendre pour demain matin.

En multitâche coopératif, vous faites une partie de votre jeu vidéo, puis vous faite votre devoir. Problème : si vous bloquez sur votre jeu vidéo, le devoir n'avancera pas et vous allez vous retrouver avec une note de merde le lendemain matin.

En multitâche préemptif, vous commencez une partie de votre jeu vidéo, votre chère maman vient alors vérifier que vous avez bien commencé votre devoir, vous trouve en train de jouer et vous force à faire votre devoir. Après avoir terminé une partie du devoir, vous vous octroyez une petite pause bien méritée sur votre jeu vidéo, puis votre maman revient vous engueuler un peu plus tard pour que vous terminiez votre devoir.

Faire une temporisation non bloquante

Afin de pouvoir concevoir des applications non bloquantes, pouvant donc exécuter plusieurs tâches en parallèle, il va falloir comprendre la logique permettant de faire du code non bloquant.

Pour ce faire, le plus simple est de commencer avec un code bloquant par définition : une temporisation.

Un delay() est par définition bloquant, on a pu le voir dans mon précédent article. On va maintenant voir comment faire une temporisation non bloquante en utilisant la fonction millis().

En guise de démonstration, nous allons reprendre le montage "Blink". Le but sera de faire clignoter une LED sans utiliser delay(). En langage Arduino, cet exemple s'appelle communément "Blink without delay".

Rappel : la fonction millis()

La fonction millis() permet d'obtenir le nombre de millisecondes depuis le démarrage du programme.

1
unsigned long millis();

La fonction millis() n'accepte aucun paramètre et retourne un entier sur 32 bits (unsigned long) contenant le nombre de millisecondes depuis le démarrage du programme.

N.B. La fonction millis() retourne un nombre entier de taille fixe (32 bits), cela signifie qu'après un certain temps la valeur retournée pas millis() va déborder et revenir à zéro.

Si votre code est susceptible de rester en activité plus de 49 jours (temps avant débordement), je vous conseille vivement de lire le chapitre bonus de mon précédent article ;)

Le montage de démonstration

Matériel pour exemple Blink

Matériel nécessaire

Pour réaliser ce premier montage de démonstration, il va nous falloir :

  • Une carte Arduino UNO (et son câble USB),

  • Une LED,

  • Une résistance de 330 ohms – code couleur orange / orange / marron,

  • Une plaque d'essai et des fils pour câbler notre montage.

Vue schématique du montage de l'exemple Arduino Blink

Vue schématique du montage

Vue prototypage du montage de l'exemple Arduino Blink

Vue prototypage du montage

Pour commencer notre montage, nous allons câbler la résistance de 330 ohms en reliant une de ses extrémités (les résistances n'ont pas de sens à respecter) à la broche D13 de la carte Arduino au moyen d'un fil. L'autre extrémité de la résistance est reliée à la plaque d'essai sur une des lignes de celle-ci.

Une fois la résistance en place, on câble la LED en plaçant l'anode de celle-ci sur la même ligne de la plaque d'essai que la résistance. On place ensuite la cathode (le côté avec le méplat) sur une autre ligne de la plaque d'essai.

Montage de l'exemple Arduino Blink

Le montage fini

On achève ensuite le circuit en reliant la masse de la carte Arduino (GND) à la cathode de la LED avec un fil.

Le code de démonstration

Maintenant que nous avons notre montage, passons au code !

Le but de notre code va être :

  1. De mettre la broche avec notre LED en sortie.

  2. D'allumer la LED.

  3. D'attendre une seconde.

  4. D'éteindre la LED.

  5. D'attendre une seconde et de recommencer au point 2.

Le tout sans bloquer le programme aux points 3 et 5.

1
2
3
4
5
// Déclare la broche sur laquelle est câblée la LED
const int BROCHE_LED = 13;

// Nombre de millisecondes entre deux changements d'état de la LED 
const unsigned long BLINK_INTERVAL = 1000;

On va commencer notre code de démonstration avec la déclaration de deux constantes : BROCHE_LED et BLINK_INTERVAL.

La constante BROCHE_LED définira le numéro de la broche sur laquelle sera connectée notre LED. La constante BLINK_INTERVAL définira le nombre de millisecondes entre deux basculements de l'état de la LED.

N.B. Par convention, on nomme toujours les constantes en majuscule, avec un sous tiret entre chaque mot, si cela est nécessaire. De plus, pour faciliter la lecture du code, on utilise généralement un préfixe commun pour toutes les constantes d'un même usage.

1
2
3
4
5
// Précédente valeur de millis()
unsigned long previousMillis = 0;

// Précédent état de la LED
byte etatBrocheLed = LOW;

On va ensuite déclarer deux variables : previousMillis et etatBrocheLed.

La variable previousMillis contiendra la précédente valeur retournée par la fonction millis(). Au début du programme cette variable sera initialisée à zéro. Cette variable va nous permettre de savoir quand inverser l'état de la LED.

La variable etatBrocheLed contiendra l'état courant de la broche reliée à la LED. Au début du programme cette variable sera initialisée à LOW. Cette variable va nous permettre de savoir quel état assigner à la broche de la LED pour inverser son état.

1
2
3
4
5
6
7
8
9
// Fonction setup(), appelée au démarrage de la carte Arduino
void setup() {

  // Configure la broche de la LED en sortie
  pinMode(BROCHE_LED, OUTPUT);
  
  // Configure l'état initial de la LED
  digitalWrite(BROCHE_LED, etatBrocheLed);
}

Dans la fonction setup(), nous allons utiliser pinMode() pour mettre la broche de la LED en sortie et digitalWrite() pour assigner l'état par défaut de la LED au démarrage.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Fonction loop(), appelée continuellement en boucle tant que la carte Arduino est alimentée
void loop() {

  // Récupére la valeur actuelle de millis()
  unsigned long currentMillis = millis();
  
  // Si BLINK_INTERVAL ou plus millisecondes se sont écoulés
  if(currentMillis - previousMillis >= BLINK_INTERVAL) {
    
    // Garde en mémoire la valeur actuelle de millis()
    previousMillis = currentMillis;
    
    // Inverse l'état de la LED
    etatBrocheLed = !etatBrocheLed;
    digitalWrite(BROCHE_LED, etatBrocheLed);
  }
}

Pour finir, nous allons remplir la fonction loop() avec un peu de magie noire ;)

Le principe de ce code — et de tous les codes non bloquant en général — est de vérifier que toutes les conditions nécessaires sont réunies avant de faire un traitement. Ici la condition est qu'un certain nombre de millisecondes se soit écoulé avant d'inverser l'état de la LED.

L'algorithme est donc ici assez trivial :

  • On récupère la valeur actuelle de millis()

  • On regarde si N (ou plus) millisecondes se sont écoulées depuis le dernier traitement

  • Si oui

    • On garde en mémoire le temps actuel pour le prochain traitement

    • On fait le traitement (on inverse l'état de la LED)

  • On recommence du début

Au final, on obtient bien une temporisation, mais sans aucun blocage du programme. Il serait tout à fait possible de faire autre chose durant le temps d'attente entre deux inversions d'état de la LED.

L'extrait de code ci-dessus est disponible en téléchargement sur cette page (le lien de téléchargement en .zip contient le projet Arduino prêt à l'emploi).

Faire plusieurs temporisations non bloquantes en parallèle

Dans le chapitre précédent, nous avons fait clignoter une LED sans utiliser delay(). C'est un bon début !

Maintenant, nous allons compliquer un peu les choses. Nous allons faire clignoter trois LEDs à des fréquences différentes.

Illustration timing LEDs Blink without delay

Illustration du timing des LEDs

La premiére LED va clignoter toutes les secondes, la seconde LED va clignoter toutes les demi-secondes et la troisième LED va clignoter toutes les deux secondes.

Le montage de démonstration

Matériel pour exemple Blink avec trois LEDs

Matériel nécessaire

Pour réaliser ce second montage de démonstration, il va nous falloir :

  • Une carte Arduino UNO (et son câble USB),

  • Trois LEDs,

  • Trois résistances de 330 ohms – code couleur orange / orange / marron,

  • Une plaque d'essai et des fils pour câbler notre montage.

N.B. J'utilise trois LEDs de couleurs différentes simplement pour donner un peu de vie à ce tutoriel ;)

Vue schématique du montage de l'exemple Arduino Blink avec trois LEDs

Vue schématique du montage

Vue prototypage du montage de l'exemple Arduino Blink avec trois LEDs

Vue prototypage du montage

Pour commencer notre montage, nous allons câbler la résistance de 330 ohms en reliant une de ses extrémités (les résistances n'ont toujours pas de sens à respecter) à la broche D13 de la carte Arduino au moyen d'un fil. L'autre extrémité de la résistance est reliée à la plaque d'essai sur une des lignes de celle-ci.

Une fois la résistance en place, on câble la LED en plaçant l'anode de celle-ci sur la même ligne de la plaque d'essai que la résistance. On place ensuite la cathode (le côté avec le méplat) sur une autre ligne de la plaque d'essai.

Montage de l'exemple Arduino Blink avec trois LEDs

Le montage fini

On achève ensuite le circuit en reliant la masse de la carte Arduino (GND) à la cathode de la LED avec un fil.

On répète ensuite la même procédure avec deux autres LEDs, sur les broches D12 et D11 de la carte Arduino.

Le code de démonstration

Pour pouvoir gérer nos trois LEDs, on va devoir copier le code précédent en trois exemplaires.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/*
 * Code d'exemple "Blink Without delay" avec trois LEDs.
 */

// Déclare les broches sur lesquelles sont câblées les LEDs
const int BROCHE_LED_1 = 13;
const int BROCHE_LED_2 = 12;
const int BROCHE_LED_3 = 11;

// Nombre de millisecondes entre deux changements d'état des LED
const unsigned long BLINK_INTERVAL_1 = 1000;
const unsigned long BLINK_INTERVAL_2 = 500;
const unsigned long BLINK_INTERVAL_3 = 2000;

// Précédente valeur de millis() pour la LED 1, 2 et 3
unsigned long previousMillisLed1 = 0;
unsigned long previousMillisLed2 = 0;
unsigned long previousMillisLed3 = 0;

// Précédent état de la LED 1, 2 et 3
byte etatBrocheLed1 = LOW;
byte etatBrocheLed2 = LOW;
byte etatBrocheLed3 = LOW;

// Fonction setup(), appelée au démarrage de la carte Arduino
void setup() {

  // Configure les broches des LEDs en sortie
  pinMode(BROCHE_LED_1, OUTPUT);
  pinMode(BROCHE_LED_2, OUTPUT);
  pinMode(BROCHE_LED_3, OUTPUT);
  
  // Configure l'état initial des LEDs
  digitalWrite(BROCHE_LED_1, etatBrocheLed1);
  digitalWrite(BROCHE_LED_2, etatBrocheLed2);
  digitalWrite(BROCHE_LED_3, etatBrocheLed3);
}

// Fonction loop(), appelée continuellement en boucle tant que la carte Arduino est alimentée
void loop() {

  // Récupére la valeur actuelle de millis()
  unsigned long currentMillis = millis();
  
  // Si BLINK_INTERVAL_1 ou plus millisecondes se sont écoulés
  if(currentMillis - previousMillisLed1 >= BLINK_INTERVAL_1) {
    
    // Garde en mémoire la valeur actuelle de millis()
    previousMillisLed1 = currentMillis;
    
    // Inverse l'état de la LED 1
    etatBrocheLed1 = !etatBrocheLed1;
    digitalWrite(BROCHE_LED_1, etatBrocheLed1);
  }
  
  // Si BLINK_INTERVAL_2 ou plus millisecondes se sont écoulés
  if(currentMillis - previousMillisLed2 >= BLINK_INTERVAL_2) {
    
    // Garde en mémoire la valeur actuelle de millis()
    previousMillisLed2 = currentMillis;
    
    // Inverse l'état de la LED 2
    etatBrocheLed2 = !etatBrocheLed2;
    digitalWrite(BROCHE_LED_2, etatBrocheLed2);
  }
  
  // Si BLINK_INTERVAL_3 ou plus millisecondes se sont écoulés
  if(currentMillis - previousMillisLed3 >= BLINK_INTERVAL_3) {
    
    // Garde en mémoire la valeur actuelle de millis()
    previousMillisLed3 = currentMillis;
    
    // Inverse l'état de la LED 3
    etatBrocheLed3 = !etatBrocheLed3;
    digitalWrite(BROCHE_LED_3, etatBrocheLed3);
  }
}

Comme vous pouvez le voir, cela commence déjà à devenir compliqué de s'y retrouver dans tout ce code.

C'est un des principaux inconvénients de ce genre de code non bloquant. La logique devient très vite compliquée à suivre. C'est d'autant plus compliqué qu'ici le même code est dupliqué en trois exemplaires.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/*
 * Code d'exemple "Blink Without delay" avec trois LEDs.
 */

// Déclare les broches sur lesquelles sont câblées les LEDs
const int BROCHE_LED_1 = 13;
const int BROCHE_LED_2 = 12;
const int BROCHE_LED_3 = 11;

// Nombre de millisecondes entre deux changements d'état des LED
const unsigned long BLINK_INTERVAL_1 = 1000;
const unsigned long BLINK_INTERVAL_2 = 500;
const unsigned long BLINK_INTERVAL_3 = 2000;

// Fonction setup(), appelée au démarrage de la carte Arduino
void setup() {

  // Configure les broches des LEDs en sortie
  pinMode(BROCHE_LED_1, OUTPUT);
  pinMode(BROCHE_LED_2, OUTPUT);
  pinMode(BROCHE_LED_3, OUTPUT);
  
  // Configure l'état initial des LEDs
  digitalWrite(BROCHE_LED_1, LOW);
  digitalWrite(BROCHE_LED_2, LOW);
  digitalWrite(BROCHE_LED_3, LOW);
}

// Fonction loop(), appelée continuellement en boucle tant que la carte Arduino est alimentée
void loop() {
  
  // Sous traite les différentes tâches
  task_led1();
  task_led2();
  task_led3();
}

void task_led1() {
  static unsigned long previousMillisLed1 = 0;
  static byte etatBrocheLed1 = LOW;
  
  unsigned long currentMillis = millis();
  
  // Si BLINK_INTERVAL_1 ou plus millisecondes se sont écoulés
  if(currentMillis - previousMillisLed1 >= BLINK_INTERVAL_1) {
    
    // Garde en mémoire la valeur actuelle de millis()
    previousMillisLed1 = currentMillis;
    
    // Inverse l'état de la LED 1
    etatBrocheLed1 = !etatBrocheLed1;
    digitalWrite(BROCHE_LED_1, etatBrocheLed1);
  }
}

void task_led2() {
  static unsigned long previousMillisLed2 = 0;
  static byte etatBrocheLed2 = LOW;
  
  unsigned long currentMillis = millis();
  
  // Si BLINK_INTERVAL_2 ou plus millisecondes se sont écoulés
  if(currentMillis - previousMillisLed2 >= BLINK_INTERVAL_2) {
    
    // Garde en mémoire la valeur actuelle de millis()
    previousMillisLed2 = currentMillis;
    
    // Inverse l'état de la LED 2
    etatBrocheLed2 = !etatBrocheLed2;
    digitalWrite(BROCHE_LED_2, etatBrocheLed2);
  }
}

void task_led3() {
  static unsigned long previousMillisLed3 = 0;
  static byte etatBrocheLed3 = LOW;
  
  unsigned long currentMillis = millis();
  
  // Si BLINK_INTERVAL_3 ou plus millisecondes se sont écoulés
  if(currentMillis - previousMillisLed3 >= BLINK_INTERVAL_3) {
    
    // Garde en mémoire la valeur actuelle de millis()
    previousMillisLed3 = currentMillis;
    
    // Inverse l'état de la LED 3
    etatBrocheLed3 = !etatBrocheLed3;
    digitalWrite(BROCHE_LED_3, etatBrocheLed3);
  }
}

Pour rendre le code plus lisible et moins méli-mélo, une solution simple consiste à faire des fonctions dédiées pour chaque tâche afin de contenir un maximum de code lié à une même tâche dans un unique endroit du code.

De plus, dans notre cas, les variables pour la gestion de chaque LED peuvent être déclarées comme des variables locales static (qui conservent leurs valeurs entre deux appels de la fonction). Cela réduit le nombre de variables globales éparpillées en début de programme et améliore donc la lisibilité du code.

D'un point de vue sémantique, le code ci-dessus est strictement identique au code du chapitre précédent. Il y a simplement trois LEDs indépendantes au lieu d'une.

Les deux extraits de code ci-dessus sont disponibles en téléchargement sur cette page et cette page (le lien de téléchargement en .zip contient le projet Arduino prêt à l'emploi).

Le résultat

En exécutant le code, on voit bien les LEDs clignoter à des fréquences différentes. Cela prouve que le code fonctionne ;)

Conclusion

Ce tutoriel est désormais terminé.

Si ce tutoriel vous a plu, n'hésitez pas à le commenter sur le forum, à le diffuser sur les réseaux sociaux et à soutenir le site si cela vous fait plaisir.