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.

v8cutxzu1bsljz6xloau-9352514

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.