ESP32. Utiliser les Timers et alarmes avec du code Arduino

Partager sur facebook
Partager sur twitter
Partager sur linkedin
Partager sur pinterest
Partager sur email
Partager sur telegram

Une minuterie ou Timer en anglais est une interruption interne qui permet de déclencher une alarme et une action associée à un moment précis de manière répétée. Un Timer est considéré comme une interruption car il “interrompt” le thread principal pour exécuter le code qui lui est associé. Une fois le code associé exécuté, le programme reprend son cours là où il avait été arrêté. 

 

L’ESP32 contient deux groupes de minuteurs matériels. Chaque groupe dispose de deux minuteries matérielles à usage général, ce qui fait que l’ESP32 dispose au total 4 Timers qui sont numérotés de 0 à 3. Ce sont tous des minuteries génériques 64 bits basées. Chaque Timer dispose d’un diviseur de temps (Prescaler)16 bits (de de 2 à 65536) ainsi que des compteurs ascendants / descendants 64 bits qui peuvent être rechargés automatiquement (option reload de la fonction timerAlarmWrite).

Si vous avez besoin de déclencher des actions à partir d’un événement externe (bouton, détecteur de mouvement PIR, radar…), lisez ce tutoriel dédié aux interruptions externes

A LIRE AUSSI :
ESP32. Utiliser les interruptions externes avec du code Arduino

Installer le SDK ESP-IDF pour ESP32 sur IDE Arduino et PlatformIO

Si vous débutez avec les cartes de développement ESP32 vous devez d’abord installer le kit de développement ESP-IDF. Voici deux tutoriels pour débuter en fonction de votre éditeur de code

Suivez les instructions de ce tutoriel pour l’IDE Arduino

A LIRE AUSSI :
ESP32. Débuter avec ESP-IDF sur IDE Arduino, macOS, Windows, Linux

Et celui-ci pour PlatformIO (idéalement avec VSCode)

A LIRE AUSSI :
Débuter avec l'ESP32 et IDE PlatformIO. Programmation C++ d'objets connectés

Introduction aux Timers

Avant d’intégrer des Timers dans vos programmes, vous devez tenir compte de certaines contraintes techniques

  • Le code doit être extrêmement rapides à exécuter. Il est préférable d’actualiser l’état d’une variable et de réaliser le traitement dans la boucle loop(). On évitera par exemple de publier un message sur un serveur MQTT ou écrire sur le port série.
  • Le code s’exécute dès que la minuterie est dépassée
  • Chaque code est attaché à un Timer qui lui est dédié. L’ESP32 dispose de 4 Timers
  • Il est possible de partager le contenu des variables déclarées comme volatile
Attention,  certains Timers peuvent interférer avec d’autres fonctionnalités tel que les sorties PWM.

Comment partager une variable entre le Timer et le reste du code

L’idée est donc d’actualiser la valeur ou l’état d’une variable et ensuite de réaliser le traitement associé dans la boucle principale loop().

Pour cela, il faut la déclarer avec le mot-clé volatile. Cela désactive l’optimisation du code. En effet, par défaut, le compilateur va toujours chercher à libérer l’espace occupée par une variable non utilisée, ce qu’on ne veut pas ici.

volatile int count;

Diviseur de temps (Prescaler) et Tic

Le Timer utilise l’horloge du processeur pour calculer le temps écoulé. Il est différent pour chaque micro-contrôleur. La fréquence du quartz de l’ESP32 est de 80MHz.

L’ESP32 a deux groupes de minuteries. Tous les temporisateurs sont basés sur des compteurs de Tic 64 bits et des diviseurs de temps 16 bits (prescaler en anglais). Le prescaler est utilisé pour diviser la fréquence du signal de base (80 MHz pour un ESP32), qui est ensuite utilisé pour incrémenter ou décrémenter le compteur de la minuterie.

Pour compter chaque Tic, il suffit de régler le prescaler sur la fréquence du quartz. Ici 80. Pour plus de détail, lisez cet excellent article.

La minuterie compte tout simplement le nombre de Tic généré par le quartz. Avec un quartz cadencé à 80MHz, on aura 80.000.000 Tics.

En divisant la fréquence du quartz par le prescaler, on obtient le nombre de Tics par seconde

80.000.000 / 80 = 1.000.000 tic/sec

Comment ajouter un Timer à un projet Arduino pour ESP32 ?

Afin de configurer le timer, nous aurons besoin d’un pointeur vers une variable de type hw_timer_t .

hw_timer_t * timer = NULL;

La fonction timerbegin(id, prescaler, flag) permet d’initialiser le Timer. Il nécessite trois arguments

  • id le numéro du Timer de 0 à 3
  • prescaler la valeur du diviseur de temps
  • flag vrai pour compter sur le front montant, faux pour compter sur le front descendant
timer = timerBegin(0, 80, true);

Avant d’activer le minuteur, on doit lier celui-ci à une fonction qui sera exécutée à chaque fois que l’interruption est déclenchée. On appel pour cela la fonction timerAttachInterrupt(timer, fonction, declencheur). Cette méthode a trois paramètres :

  • timer c’est le pointeur vers le Timer que l’on vient de créer
  • fonction la fonction qui sera exécutée à chaque fois que l’alarme du Timer se déclenche
  • declencheur indique comment synchroniser le déclenchement du Timer par rapport à l’horloge.

2 types de déclencheurs (Trigger) sont possibles. Plus d’info ici.

  • Edge (true) Le Timer est déclenché sur la détection du front montant
  • Level (false) Le Timer est déclenché lorsque le signal de l’horloge change de niveau

Détection du signal de l'horloge ESP32 pour déclencher timerAttachInterrupt sur Level ou Edge

timerAttachInterrupt(timer, &onTime, true);

Déclencher une alarme

Une fois que le Timer est démarré, il ne reste plus qu’à programmer une alarme qui sera déclenchée à intervalle régulier.

Pour cela on dispose de la méthode timerAlarmWrite(timer, frequence, autoreload) qui nécessite 3 paramètres (code source)

  • timer le pointeur vers le Timer créé précédemment
  • frequence la fréquence de déclenchement de l’alarme en tics. Pour un ESP32, il y a 1 000 0000 de tics par seconde
  • autoreload true pour réinitialiser l’alarme automatiquement après chaque déclenchement.
timerAlarmWrite(timer, 1000000, true);

Enfin on démarre l’alarme à l’aide de la méthode timerAlarmEnable(timer)

timerAlarmEnable(timer);

Comment rendre le code “Temps réel” (optionnel) ?

Le framework ESP-IDF que l’on utilise pour développer le programme Arduino est construit sur une version modifiée de FreeRTOS, un système d’exploitation temps réel adapté aux micro-contrôleurs et aux systèmes embarqués d’une manière générale.

Pour que le code s’exécute de façon déterministe en temps réel, il est possible d’encadrer certaine portion critique.

Pour cela, il faut définir un objet de type portMUX_TYPE

portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

Ensuite, on encadre la portion de code critique comme ceci

portENTER_CRITICAL_ISR(&timerMux);
  ... code critique
portEXIT_CRITICAL_ISR(&timerMux);

Exécuter la fonction dans la IRAM avec l’attribut IRAM_ATTR

Comme pour toute interruption, il est préférable de placer le code exécuté par le Timer dans la RAM interne de l’ESP32 qui est beaucoup plus rapide que la mémoire Flash de la carte de développement.

L’exécution du code dans la RAM de l’ESP32 n’est pas obligatoire. Il est toutefois conseillé de le faire dès que le projet devient complexe.

Pour cela, il suffit de placer l’attribut IRAM_ATTR juste avant le nom de la fonction comme ceci

void IRAM_ATTR mafonctionrapide(){
   ... code exécuté dans la RAM de l'ESP32
}

Il est également possible de rendre critique l’exécution du code dans la RAM

void IRAM_ATTR mafonctionrapide(){
   portENTER_CRITICAL_ISR(&timerMux); 
      ... code critique exécuté dans la RAM de l'ESP32 
   portEXIT_CRITICAL_ISR(&timerMux);
}

Exemple de Timer faisant clignoter une LED

Commençons par un exemple simple d’une alarme déclenchée chaque seconde. A chaque déclenchement de l’alarme, on incremente un compteur. Si le compteur est pair, on allume la LED. On éteint la LED si le compteur est impair.

Circuit

La LED est connectée à la sortie 32.

ESP32 Timer Alarm blink LED

La LED doit être protégée par une résistance dont la valeur dépend de la tension et l’intensité de sortie de la broche (3,3V – 40mA) et de la tension d’alimentation maximale de la LED.

Vous pouvez utiliser ce calculateur pour déterminer la valeur de la résistance nécessaire pour votre circuit.

Calculateur de résistance série pour une ou plusieurs LED
Tension d'alimentation en Volt
Tension directe en Volt
Courant en mA
Résistance calculée en Ω
Puissance estimée en W

Ce calculateur permet de déterminer la résistance requise pour piloter une ou plusieurs LED connectées en série à partir d'une source de tension à un niveau de courant spécifié.

Remarque. Il est préférable d'alimenter le circuit avec une puissance nominale comprise entre 2 et 10 fois la valeur calculée afin d'éviter la surchauffe
calculateur resistance serie led resistor calculator
Couleur Longueur d'onde (nm) Tension (V) pour LED ⌀3mm Tension(V) pour LED ⌀5mm
Rouge 625-630  1,9-2,1 2,1-2,2
Bleu 460-470 3,0-3,2 3,2-3,4
Vert 520-525 2,0-2,2 2,0-2,2
Jaune 585-595 2,0-2,2 3,0-3,2
Blanc 460-470 3,0-3,2 1,9-2,1

Voir d’autres assortiments

Configuration PlatformIO pour une LoLin D32

Voici un exemple de fichier de configuration platformio.ini pour une carte de développement LoLin32 Pro

[env:lolin_d32_pro]
platform = espressif32
board = lolin_d32_pro
framework = arduino
monitor_speed = 115200

Téléverser le code Arduino du projet

Créer un nouveau croquis sur l’IDE Arduino ou un nouveau projet PlatformIO.

Sur l’IDE Arduino vous pouvez retirer la première ligne #include <Arduino.h>.

A chaque fois que la fonction onTimer est exécutée, on augmente la valeur de la variable volatile count. Dans le thread principal de la boucle loop(), dès que le déclencheur est supérieur à zéro, on incrémente le compteur totalInterrupts et on fait clignoter la LED s’il est pair ou non.

#include <Arduino.h>

volatile int count;    // declencheur de compteur
int totalInterrupts;   // compte le nombre de declenchement de l alarme

#define LED_PIN 32

hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

// Code avec section critique
void IRAM_ATTR onTime() {
   portENTER_CRITICAL_ISR(&timerMux);
   count++;
   portEXIT_CRITICAL_ISR(&timerMux);
}

// Code sans section critique
/*void IRAM_ATTR onTime() {
   count++;
}*/

void setup() {
   Serial.begin(115200);
  
   // Configure la sortie de la LED
   pinMode(LED_PIN, OUTPUT);
   digitalWrite(LED_PIN, LOW);

   // Configure le Prescaler a 80 le quart de l ESP32 est cadence a 80Mhz
   // 80000000 / 80 = 1000000 tics / seconde
   timer = timerBegin(0, 80, true);                
   timerAttachInterrupt(timer, &onTime, true);    
    
   // Regle le declenchement d une alarme chaque seconde
   timerAlarmWrite(timer, 1000000, true);           
   timerAlarmEnable(timer);
}

void loop() {
    if (count > 0) {
       // Mettre en commentaire enter/exit pour desactiver la section critique 
       portENTER_CRITICAL(&timerMux);
       count--;
       portEXIT_CRITICAL(&timerMux);

       totalInterrupts++;
       Serial.print("totalInterrupts");
       Serial.println(totalInterrupts);
       if ( totalInterrupts%2 == 0) {
         // Allume la LED si compteur pair
         digitalWrite(LED_PIN, HIGH);
       } else {
         // Sinon eteint la LED
         digitalWrite(LED_PIN, LOW);
       }
    }
}

Ouvrez le moniteur série pour visualiser le déclenchement des alarmes par le Timer.

Mesurer chaque seconde la température avec un BMP180 ou BME280

On va maintenant appliquer le principe pour déclencher régulièrement l’enregistrement de la température à l’aide d’un BMP180. Quelque soit le capteur, le principe restera le même.

Circuit

Ajoutez un baromètre BMP180, BME280 ou BME680 au circuit précédent. Ici, la broches SDA est connectée à la broche 18 et SCL sur la 5. La LED est toujours connectée à la sortie 32

ESP32 Timer Alarm LED BMP180

Configuration PlatformIO pour une LoLin32 Pro

Voici le fichier de configuration platformio.ini pour une carte de développement LoLin D32 Pro qui installe automatiquement la librairie Adafruit_BMP085 (525)

[env:lolin_d32_pro]
platform = espressif32
board = lolin_d32_pro
framework = arduino
monitor_speed = 115200
lib_deps =
    525

Téléverser le code Arduino

Créer un nouveau croquis ou projet PlatformIO. Modifiez les broches dans le code avant de téléverser :

  • PIN_LED par défaut 32
  • PIN_SDA broche SDA du bus I2C, dans le code 18
  • PIN_SCL broche SCL du bus I2C, dans le code 5
Par défaut, sur les cartes ESP32, la broche SDA est la 21. La broche SCL est la 22
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_BMP085.h>

Adafruit_BMP085 bmp;
bool BMP180connected = false;

volatile bool get_temp;

#define PIN_SDA 18
#define PIN_SCL 5
#define PIN_LED 32

hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

// La fonction onTime est execute dans la RAM de l ESP32
// Le code est declare comme critique pour FreeRTOS
void IRAM_ATTR onTime() {
    portENTER_CRITICAL_ISR(&timerMux);
    get_temp = true;
    portEXIT_CRITICAL_ISR(&timerMux);
}

void blinkLED(){
  digitalWrite(PIN_LED, HIGH);
  delay(500);
  digitalWrite(PIN_LED, LOW);
}

void setup() {
  Serial.begin(115200);
  
  // Demarre le bus I2C sur les broches
  Wire.begin(PIN_SDA, PIN_SCL);

  if (!bmp.begin()) {
    Serial.println("Could not find a valid BMP180 sensor, check wiring!");
  } else {
    // Passe a true pour eviter de cracher le programme
    BMP180connected = true;
  }

  // Configure LED Output
  pinMode(PIN_LED, OUTPUT);
  digitalWrite(PIN_LED, LOW);

   // Configure le Prescaler a 80 le quart de l ESP32 est cadence a 80Mhz
   // 80000000 / 80 = 1000000 tics / seconde
   timer = timerBegin(0, 80, true);                
   timerAttachInterrupt(timer, &onTime, true);    
    
   // Regle le declenchement d une alarme chaque seconde
   timerAlarmWrite(timer, 2000000, true);       
   timerAlarmEnable(timer);
}

void loop() {
    if ( get_temp ) {
       portENTER_CRITICAL(&timerMux);
       get_temp = false;
       portEXIT_CRITICAL(&timerMux);
    
       if ( BMP180connected ) {
          blinkLED();
          Serial.printf("Temperature is %.1f°C \n", bmp.readTemperature()); 
        }
   }
}

Ouvrez le moniteur série pour visualiser l’acquisition d’une mesure à chaque fois que l’alarme est déclenchée par le Timer. La LED clignote puis la mesure est publiée sur le port série.

Explication du code

Le fonctionnement du Timer et de l’alarme est identique au code précédent.

Ici, on utilise les broches 18 et 5 d’une carte de développement LoLin32 pour connecter le bus I2C

Wire.begin(PIN_SDA, PIN_SCL);

A chaque fois que l’alarme est déclenchée, on passe le drapeau (flag en anglais) get_temp à true.

On test si le capteur est disponible à l’aide du drapeau BMP180connected.
Si c’est bien le cas, on fait clignoter la LED durant 500ms en appelant la méthode blinkLED

void blinkLED(){
  digitalWrite(PIN_LED, HIGH);
  delay(500);
  digitalWrite(PIN_LED, LOW);
}

On construit la mesure de température en formatant la mesure avec un seul chiffre significatif avant de la publier sur le moniteur série à l’aide de la méthode Serial.printf().

Serial.printf("Temperature is %.1f°C \n", bmp.readTemperature()); 

Mises à jour

1/10/2020 Publication de l’article

Avez-vous aimé cet article ?
[Total: 1 Moyenne: 5]
Partager sur facebook
Partager sur twitter
Partager sur linkedin
Partager sur pinterest
Partager sur email
Partager sur telegram

Vous avez aimé ce projet ? Ne manquez plus aucun projet en vous abonnant à notre lettre d’information hebdomadaire!

quel modèle esp8266 choisir
Quel modèle d'ESP8266EX choisir en 2020 ?
guide choix esp32 development board
Quel ESP32 choisir en 2020 ?

Vous rencontrez un problème avec ce sujet ?

Peut-être que quelqu’un a déjà trouvé la solution, visitez le forum avant de poser votre question

Nous serions ravis de connaître votre avis

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

Domotique et objets connectés à faire soi-même
Vous avez aimé ce tutoriel

Ne manquez plus les prochains projets

Recevez chaque semaine le récapitulatif des tutoriels et projets.

Vous pouvez vous désabonner à tout moment.