Quatre façons de faire clignoter une (des) LED

Le programme “blink”, qui consiste à piloter une LED sur une carte à microcontrôleur, est l’équivalent du “Hello World” pour la programmation sur ordinateur de bureau ; c’est en général avec lui que l’on débute. Il permet en effet d’introduire les notions d’entrées-sorties à usage générique (GPIO) et de programmation par scrutation (polling) incontournables pour la programmation embarquée.
Dans ce tutoriel, nous allons présenter quatre façons différentes de faire clignoter la (ou les) LED utilisateur d’une carte NUCLEO en interagissant aussi avec le bouton utilisateur. Nous aborderons ainsi des techniques et notions de programmation embarquée qui pourront vous servir dans de nombreuses autres applications.

Matériel requis

  • Pour les exemples 1 et 2,n’importe quelle carte NUCLEO fera l’affaire.

  • Pour l’exemple 3, n’importe quelle carte NUCLEO fera l’affaire. Notez cependant que que le sketch donné est valable pour une carte équipée d’un MCU (microcontrôleur) fonctionnant à 80 MHz, telle que la NUCLEO-L476RG. On pourra bien sûr choisir une autre carte / fréquence à condition de modifier en conséquence les paramètres PSC_minus_1 et ARR_minus_1 du sketch.

  • Pour l’exemple 4, on a besoin de deux LED c’est pourquoi nous avons ajouté sur notre NUCLEO-L476RG un shield de base Grove et deux modules LED Grove tel que ceux de cet exemple. On ajoute également un module bouton Grove car le shield masque le bouton utilisateur de la carte.

1. Gestion de la LED et du bouton par scrutation

Ce premier exemple montre comment faire clignoter la LED utilisateur d’une carte NUCLEO à une fréquence de 1 Hertz (un cycle allumé-éteint par seconde). Le sketch fait appel à une boucle sans clause de sortie qui effectue toutes les opérations en séquence : test de l’état du bouton, LED allumée, attente, LED éteinte, attente, etc. C’est ce qu’on appelle le programmation par scrutation (polling)

Bibliothèque(s) requise(s)

Aucune bibliothèque n’est nécessaire pour cet exemple, le sketch qui suit est suffisant.

Le sketch Arduino

Le sketch pour cet exemple (et tous les autres) peut être téléchargé en cliquant ici.

Lancez l’IDE Arduino, ouvrez un nouveau sketch vide et copiez-y le code qui suit :

/*
  Objet du sketch : 
  1 - Commutation de USER LED à la fréquence de 1 Hz.
  2 - USER LED doit clignoter lorsque le bouton USER n'est pas enfoncé
  Gestion entièrement en "scrutation" ("polling")
*/

// Temps pendant lequel la LED reste allumée/éteinte
#define HALF_SECOND 500

// La fonction setup ne s'exécute qu'une seule fois au démarrage
void setup() {

  // Configure la broche digitale LED_BUILTIN en mode "sortie".
  pinMode(LED_BUILTIN, OUTPUT);

  // Configure la broche digitale USER_BTN en mode "entrée", "floating"
  // en pratique, d'après les câblages de la carte, le potentiel du bouton
  // est forcé à +VDD lorsqu'il n'est pas pressé.
  pinMode(USER_BTN, INPUT);

}

// La fonction loop est exécutée jusqu'à la fin des temps ...
void loop() {
  if (digitalRead(USER_BTN) == HIGH) // Si le bouton user n'est pas enfoncé ...
  {
    digitalWrite(LED_BUILTIN, HIGH); // Allume la LED (HIGH : tension +3V3)
    delay(HALF_SECOND); // Attend une demi seconde
    digitalWrite(LED_BUILTIN, LOW); // Eteint la LED (LOW : tension 0V)
    delay(HALF_SECOND); // Attend une demi seconde
  }
}

Après avoir vérifié que l’IDE Arduino est correctement configurée, notamment qu’elle est connectée au port COM attribué au ST-LINK de votre carte (sous Windows), cliquez sur “Téléverser”. Vous pouvez vérifier que la LED ne clignote que lorsque le bouton utilisateur est relâché.

2. Gestion de la LED par scrutation et du bouton par interruptions

Ce deuxième exemple enlève la gestion du bouton de la boucle principale. Le bouton est à présent géré avec le mécanisme des interruptions : lorsqu’il est enfoncé, la chute de tension sur sa broche génère une interruption qui suspend l’exécution de la fonction loop(), exécute la fonction BlinkSwitchISR(), puis poursuit l’exécution de la fonction loop(). On notera que la variable globale blink_state, modifiée par la routine de service de l’interruption BlinkSwitchISR(), est déclarée avec l’attribut volatile afin de la préserver du risque d’une élimination pure et simple par l’optimiseur au moment de la compilation du sketch.

Bibliothèque(s) requise(s)

Aucune bibliothèque n’est nécessaire pour cet exemple, le sketch qui suit est suffisant.

Le sketch Arduino

Le sketch pour cet exemple (et tous les autres) peut être téléchargé en cliquant ici.

Lancez l’IDE Arduino, ouvrez un nouveau sketch vide et copiez-y le code qui suit :

/*
  Objet du sketch :
  1 - Commutation de USER LED à la fréquence de 1 Hz.
  2 - USER LED doit clignoter lorsque le bouton USER n'est pas enfoncé
  Gestion de la LED en "polling"
  Gestion du bouton par interruption
*/

// Déclaration "volatile" INDISPENSABLE pour une variable utilisée dans une interruption
// Si pas fait, l'optimiseur du compilateur va probablement éliminer le code de l'interruption
volatile bool blink_state;

#define HALF_SECOND 500

void setup() {

  // Initialisation des broches pour la LED et le bouton
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(USER_BTN, INPUT);

  // Attache la fonction de service d'interruption "BlinkSwitchISR" à USER_BTN
  // Génère l'interruption lorsque l'état du bouton CHANGE
  attachInterrupt(digitalPinToInterrupt(USER_BTN), BlinkSwitchISR, CHANGE );

  // Au démarrage, le bouton n'a pas encore été enfoncé, on souhaite que la LED clignote
  blink_state = true;

}

void loop() {

  if (blink_state) //  Si le bouton user n'est pas enfoncé ...
  {
    digitalWrite(LED_BUILTIN, HIGH); // Allume la LED
    delay(HALF_SECOND); // Attend une demi seconde
    digitalWrite(LED_BUILTIN, LOW); // Eteint la LED
    delay(HALF_SECOND); // Attend une demi seconde
  }

}

/*
  Fonction de service de l'interruption attachée à USER_BTN.
  Inverse la variable blink_state lorsque le bouton change d'état.
*/
void BlinkSwitchISR(void)
{
  blink_state = !blink_state;
}

Après avoir vérifié que l’IDE Arduino est correctement configurée, notamment qu’elle est connectée au port COM attribué au ST-LINK de votre carte (sous Windows), cliquez sur “Téléverser”. Vous pouvez tester que la LED ne clignote que lorsque le bouton utilisateur est relâché.

3. Gestion de la LED et du bouton par interruptions

Ce troisième exemple montre comment gérer l’appui sur le bouton ET le clignotement de la LED par interruptions. La gestion du bouton est identique à celle du sketch qui précède. Pour la gestion du clignotement, on utilise un timer qui génère une interruption à la fin de chacune de ses périodes.
Le timer doit donc être configuré pour que sa soit période soit exactement une demi-seconde fin que la LED effectue un cycle allumée - éteinte (deux inversions) par seconde. Pour ce faire, il est nécessaire de choisir une carte NUCLEO pour fixer la fréquence Fmcu du MCU puis de calculer les paramètres de configuration du timer à partir de celle-ci. Si nous utilisons une NUCLEO-L476RG on a Fmcu = 80 000 000 Hz et on doit choisir PSC_minus_1 et ARR_minus_1 de sorte que Fmcu / ((PSC_minus_1 + 1) * (ARR_minus_1 + 1)) soit égal à 0,5 (Hz), en veillant toutefois à ce que ni PSC_minus_1, ni ARR_minus_1 ne dépasse 216-1 (soit 65535, c’est une contrainte imposée par la taille des registres du MCU STM32).

Bibliothèque(s) requise(s)

Aucune bibliothèque n’est nécessaire pour cet exemple, le sketch qui suit est suffisant.

Le sketch Arduino

Le sketch pour cet exemple (et tous les autres) peut être téléchargé en cliquant ici.

Lancez l’IDE Arduino, ouvrez un nouveau sketch vide et copiez-y le code qui suit :

/*
  Objet du sketch : 
  1 - Commutation de USER LED à la fréquence de 1 Hz
  2 - USER LED doit clignoter lorsque le bouton USER n'est pas enfoncé
  Gestion de l'ensemble par interruptions
  Carte utilisée ici : NUCLEO-L476RG
*/

// Déclaration "volatile" INDISPENSABLE pour une variable utilisée dans une interruption
// Si pas fait, l'optimiseur du compilateur va probablement éliminer le code de l'interruption
volatile bool blink_state; 

// Instanciation du timer
TIM_TypeDef *Instance = TIM1;
HardwareTimer MyTim(Instance);

// Prescaler (diviseur d'horloge) du timer 
const uint16_t PSC_minus_1 = 40000 - 1;

// Valeur maximum du timer (pour fixer sa période à 2 Hz)
const uint16_t ARR_minus_1 = 1000 - 1;

void setup() {
 
  // Initialisation du port série
  Serial.begin(9600);

  // Initialisation des brochesde la LED et du bouton
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(USER_BTN, INPUT);
 
  // Attache la fonction de service d'interruption "BlinkSwitchISR" à USER_BTN
  // Génère l'interruption lorsque l'état du bouton CHANGE
  attachInterrupt(digitalPinToInterrupt(USER_BTN), BlinkSwitchISR, CHANGE);

  // Au démarrage, le bouton n'a pas encore été enfoncé, on souhaite que la LED clignote 
  blink_state = true;

  // Affiche la fréquence du MCU
  Serial.print("Fréquence du microcontrôleur (MHz) : ");
  Serial.println(MyTim.getTimerClkFreq()/1000000,0);

  // Divise par la fréquence du timer (80 MHz) par PSC_minus_1 + 1
  MyTim.setPrescaleFactor(PSC_minus_1); 

  // Génère une alarme de dépassement tous les ARR_minus_1 + 1 ticks du timer
  // Attache la fonction TimerISR à l'interruption associée.
  MyTim.setMode(2, TIMER_OUTPUT_COMPARE);
  MyTim.setOverflow(ARR_minus_1, TICK_FORMAT);
  MyTim.attachInterrupt(TimerISR);
  
  MyTim.resume(); // démarre le timer
}

void loop() { /* Plus rien ici ! */}

/* 
 Fonction de service de l'interruption de dépassement du Timer
 Fait clignoter la LED chaque seconde si le bouton USER est relâché
*/
void TimerISR(void)
{
  if (blink_state) 
  {
   digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
  }
}

/*
  Fonction de service de l'interruption attachée à USER_BTN.
  Inverse la variable blink_state lorsque le bouton 
  change d'état.
*/
void BlinkSwitchISR(void)
{
  blink_state = !blink_state;
}

Après avoir vérifié que l’IDE Arduino est correctement configurée, notamment qu’elle est connectée au port COM attribué au ST-LINK de votre carte (sous Windows), cliquez sur “Téléverser”. Vous pouvez tester que la LED ne clignote que lorsque le bouton utilisateur est relâché.

4. Faire clignoter deux LED en parallèle

Ce quatrième et dernier exemple montre comment faire clignoter deux LED (en apparence) simultanément. Son principe est le suivant :

  • Un timer égrène le temps via une variable partagée qui fait office de compteur (time_cnt)
  • Dans la boucle principale, différentes fonctions (ou tâches) sont appelées (une pour chaque LED) pour différentes valeurs du compteur.

Ceci permet de partager le temps entre les tâches sans jamais bloquer l’une d’entre elles. On notera que le timer n’est pas indispensable ; on pourrait se servir de la fonction millis() comme dans cet exemple. Mais le timer offre un décompte du temps plus précis car généré matériellement plutôt que par le décompte des cycles CPU.

Précautions et précisions concernant le matériel requis

  • On a besoin de deux LED c’est pourquoi nous avons ajouté sur notre NUCLEO-L476RG un shield de base Grove et deux modules LED Grove tel que ceux de cet exemple. Un des modules est branché sur D4 et l’autre sur D3.

  • Nous vous rappelons que les LED sont polarisées ; si vous les branchez incorrectement, vous les détruirez probablement. La patte la plus longue des LED devra être insérée dans la borne “+” du module Grove et la plus courte dans sa borne “-“.

  • Si votre montage est correct mais que les LED ne semblent pas clignoter c’est qu’il faut probablement que vous ajustiez le petit potentiomètre placé sur leur module à l’aide d’un tournevis.

  • On utilise également un module bouton Grove branché sur D2.

Bibliothèque(s) requise(s)

Aucune bibliothèque n’est nécessaire pour cet exemple, le sketch qui suit est suffisant.

Le sketch Arduino

Le sketch pour cet exemple (et tous les autres) peut être téléchargé en cliquant ici.

Lancez l’IDE Arduino, ouvrez un nouveau sketch vide et copiez-y le code qui suit :

/*
  Objet du sketch :
  Réalise la fonction blink à trois fréquences différentes pour les trois LED de
  la carte NUCLEO-WB55RG, lorsqu'aucun de ses trois boutons n'est enfoncé.
  Gestion des LED en temps partagé, à l'aide d'un timer
  Gestion du bouton par interruption
*/

// Déclaration des broches des LED
#define LED1 D3
#define LED2 D4

// Déclaration de la broche du bouton
#define BTN D2

// Déclaration "volatile" INDISPENSABLE pour une variable utilisée dans une interruption
// si pas fait, l'optimiseur du compilateur va probablement éliminer le code de l'interruption
volatile uint8_t time_cnt;
volatile bool blink_state;

// Pour fixer les fréquences de clignotement des deux LED
// En nombre de périodes du timer (une période dure 5 ms )
#define SECOND_005 1
#define SECOND_05 10

// Instanciation du timer
TIM_TypeDef *Instance = TIM1;
HardwareTimer MyTim(Instance);

// Prescaler (diviseur d'horloge) du timer
const uint16_t PSC_minus_1 = 8000 - 1;

// Valeur maximum du timer,
// (20 interruptions de dépassement de compteur par seconde)
const uint16_t ARR_minus_1 = 500 - 1;

// Pour les calculs de délais de chaque LED
uint8_t blink1 = 0;
uint8_t blink2 = 0;

void setup()
{
  // Initialisation du port série
  Serial.begin(9600);

  // Initialisation des deux LED et du bouton
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(BTN, INPUT);

  // Attache la fonction de service d'interruption "BlinkSwitchISR" à BTN
  // Génère l'interruption lorsque l'état du bouton CHANGE
  attachInterrupt(digitalPinToInterrupt(BTN), BlinkSwitchISR, CHANGE );

  // Au démarrage, le bouton n'a pas encore été enfoncé, on souhaite que les LED clignotent
  blink_state = true;

  // Affiche la fréquence du MCU
  Serial.print("Fréquence du microcontrôleur (MHz) : ");
  Serial.println(MyTim.getTimerClkFreq() / 1000000, 0);

  // Divise par la fréquence du timer (80 MHz -> 1 kHz) par PSC_minus_1 + 1
  MyTim.setPrescaleFactor(PSC_minus_1);

  // Génère une alarme de dépassement tous les ARR_minus_1 + 1 ticks du timer
  // Attache la fonction TimerISR à l'interruption associée.

  MyTim.setMode(2, TIMER_OUTPUT_COMPARE);
  MyTim.setOverflow(ARR_minus_1, TICK_FORMAT);
  MyTim.attachInterrupt(TimerISR);

  // démarre le timer
  MyTim.resume();

}

void loop() {
  
  // Si le bouton user n'a pas été enfoncé ...
  if (blink_state)
  {

    // Toutes les 0.05 s depuis sa dernière inversion...
    // On ne teste pas l'égalité stricte 
    // (i.e. time_cnt - blink1 == SECOND_005).
    // En effet, le timer incrémentant time_cnt de façon idépendante, 
    // il est probable que la boucle infinie "rate" la valeur
    // n = 10 à l'instant précis où elle survient.

    if (time_cnt - blink1 > SECOND_005)
    {
      switch_LED(LED1); // On inverse LED1
      blink1 = time_cnt; // On mémorise la date de cet évènement 
    }

    // Toutes les 0.5 s depuis sa dernière inversion...
    if (time_cnt - blink2 > SECOND_05)
    {
      switch_LED(LED2); // On inverse LED2
      blink2 = time_cnt; // On mémorise la date de cet évènement 

      // On remet les compteurs de temps à zéro lorsque la LED avec
      // la plus longue période a changé d'état afin d'éviter un
      // dépassement de capacité des variables.
      
      time_cnt = 0;
      blink1 = 0;
      blink2 = 0;
    }
  }
}

/* Gestion du clignotement des LED */
void switch_LED(uint16_t led_pin) {
  digitalWrite(led_pin, !digitalRead(led_pin));
}

/*
  Fonction de service des interruptions du bouton.
  inverse la variable de contrôle du bouton
*/
void BlinkSwitchISR(void) {
  blink_state = !blink_state;
}

/*
  Fonction de service de l'interruption de dépassement du Timer
  Incrémente le compteur toutes les 50 millisecondes
*/
void TimerISR(void)
{
  time_cnt += 1;
}

Après avoir vérifié que l’IDE Arduino est correctement configurée, notamment qu’elle est connectée au port COM attribué au ST-LINK de votre carte (sous Windows), cliquez sur “Téléverser”. Vous pouvez observer que, lorsque le bouton bleu de la carte NUCLEO est relâché, les deux LED clignotent, chacune à sa fréquence .