ESP32. Utiliser les Timers et alarmes avec du code Arduino • Domotique et objets connectés à faire soi-même

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

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

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

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

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

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.

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.

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.

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 .

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 

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.

https://projetsdiy.fr/data/uploads/2020/09/ESP32-blink-LED-with-timer-alarm-.mp4?_=1

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

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

Avez-vous aimé cet article ?

[Total: 1 Moyenne: 5]