Utiliser les timers de l’ESP32 en code Arduino
(Mis à jour le 23/01/2023)
Dans cet article, nous allons explorer le fonctionnement d’un timer sur l’ESP32 en utilisant du code Arduino. Nous verrons comment configurer et utiliser un timer de manière efficace, en utilisant des exemples pratiques pour vous aider à comprendre les concepts de base. Nous allons découvrir les étapes pour configurer un timer sur l’ESP32, avec les paramètres importants pour un fonctionnement optimal. C’est parti 😊.
Le fonctionnement d’un timer sur l’ESP32
Le fonctionnement théorique du timer n’est pas présenté dans cet article pour éviter de le surcharger. Si vous débutez et que vous ne connaissez pas le fonctionnement interne d’un timer, je vous encourage fortement à lire l’article théorique sur son fonctionnement . Il vous permettra de mieux comprendre comment choisir les valeurs des paramètres pour l’utiliser dans votre code Arduino.
Configurer et utiliser un timer de l’ESP32 avec du code Arduino
Voici le code squelette minimal pour utiliser un timer sur l’ESP32 avec du code Arduino. Il permet de déclencher une interruption dès que le timer atteint la valeur seuil threashold
que l’on appelle communément l’autoreload
.
hw_timer_t * timer = NULL;
void IRAM_ATTR timer_isr() {
// This code will be executed every 1000 ticks, 1ms
}
void setup() {
Serial.begin(115200);
uint8_t timer_id = 0;
uint16_t prescaler = 80; // Between 0 and 65 535
int threshold = 1000000; // 64 bits value (limited to int size of 32bits)
timer = timerBegin(timer_id, prescaler, true);
timerAttachInterrupt(timer, &timer_isr, true);
timerAlarmWrite(timer, threshold, true);
timerAlarmEnable(timer);
}
void loop() {
}
Sélection et configuration basique du timer
On définit un objet hw_timer_t
en dehors de la fonction setup()
pour pouvoir y accéder depuis différentes fonctions. La fonction timerBegin(uint8_t id, uint16_t prescaler, bool countUp)
permet de configurer le timer :
Sur l’ESP32 il y a 4 timers complètement indépendants, on les choisit via un id
compris entre 0 et 3. Ensuite, on choisit ensuite le prescaler
que l’on veut appliquer au signal d’horloge du timer. Sur l’ESP32, c’est l’horloge APB_CLK
cadencée à 80 MHz qui est utilisée.
Avertissement
Si vous utilisez des librairies externes dans votre code, elles utilisent peut-être des timers. Dans ce cas il faut faire attention de ne pas choisir un qui est déjà utilisé par celles-ci, sinon votre programme aura assurément des bugs !
Sur la plupart des exemples disponibles, un prescaler
de 80
est appliqué pour avoir une période de comptage de 1 µs. À partir d’une période du timer en microsecondes, on pourra directement connaître la valeur de l’autoreload correspondante sans faire de calcul compliqué :
L’argument countUp
précise dans quel sens on veut compter : true
dans l’ordre croissant, false
dans l’ordre décroissant.
Configurer l’alarme et le déclenchement d’une routine d’interruption
On attache au timer une interruption qui sera déclenchée à chaque fois que la valeur seuil sera dépassée avec timerAttachInterrupt(hw_timer_t *timer, void (*fn)(void), bool edge)
.
Il faut mettre dans l’argument du milieu le nom de la routine d’interruption qui sera exécutée (ici timer_isr()
). On peut également choisir si l’interruption se déclenche au front montant ou descendant : on opte en général pour un front montant pour les timers.
La valeur seuil du compteur est définie par timerAlarmWrite(hw_timer_t *timer, uint64_t alarm_value, bool autoreload)
. On active le mode autoreload
en mettant autoreload
à true
: une fois que le timer a dépassé la valeur alarm_value
, il recommence à compter à partir de zéro et la fonction d’interruption est déclenchée.
Note
Avec un prescaler
de 80, la valeur de alarm_value
correspond directement à la période globale du timer en microsecondes.
Une fois que le timer a été complètement configuré, on peut l’activer avec son alarme avec timerAlarmEnable(timer)
.
Si on exécute ce code, la fonction timer_isr()
sera exécutée toutes les secondes. Puisque la fonction est vide dans ce code “squelette”, il ne se passera rien concrètement. Je vous propose une version qui fait clignoter la LED bleue de l’ESP32 sur la broche GPIO2
.
Code blink avec uniquement un timer
Voici le fameux “blink” récrit en utilisant uniquement un timer hardware qui se fera clignoter la LED bleue toutes les secondes :
#define PIN_LED 2
hw_timer_t * timer = NULL;
volatile uint8_t led_state = 0;
void IRAM_ATTR timer_isr(){
led_state = ! led_state;
digitalWrite(PIN_LED, led_state);
}
void setup() {
Serial.begin(115200);
pinMode(PIN_LED, OUTPUT);
uint8_t timer_id = 0;
uint16_t prescaler = 80;
int threashold = 1000000;
timer = timerBegin(timer_id, prescaler, true);
timerAttachInterrupt(timer, &timer_isr, true);
timerAlarmWrite(timer, threashold, true);
timerAlarmEnable(timer);
}
void loop() {
// put your main code here, to run repeatedly
}
On utilise le mot-clé volatile
pour la variable led_state
pour forcer le compilateur à ne pas faire d’optimisation. En effet, pour le compilateur, la fonction timer_isr ()
n’est jamais appelée dans le code car elle est appelée via une interruption externe (que le compilateur n’a pas connaissance).
Sans l’attribut volatile
, le compilateur pourrait retirer la variable qui n’est a priori pas utilisée pour libérer de l’espace mémoire. En mettant le keyword volatile
, on force le compilateur à ne pas prendre en compte cette variable pour l’optimisation.
Pour faire clignoter la LED, on utilise ici une petite astuce pour inverser l’état de la LED avec l’opérateur !
. Quand on l’utilise sur une variable, tous les zéros dans sa représentation binaire sont remplacés par des 1 (et vice-versa).
Ainsi la valeur de led_state sera inversée à chaque entrée dans la routine d’interruption : 0b0000000
→ 0b11111111
→ 0b0000000
… Puisque la fonction digitalWrite()
regarde la valeur que du premier bit, on aura bien une alternance entre des 0 et 1 .
Note
On rajoute l’attribut IRAM_ATTR
lors de la déclaration de la fonction, pour indiquer au compilateur de charger l’ensemble du code de la routine d’interruption dans la RAM interne de l’ESP32, pour qu’elle s’exécute plus rapidement.
Avec ce programme, la LED bleue se met à clignoter, sans utiliser la fonction loop()
, qui peut être utilisée pour faire d’autres tâches.
Incrémenter une variable et générer des flags
La routine d’interruption doit s’exécuter le plus rapidement possible pour éviter de trop perturber le programme principal. Ainsi on utilise communément des flags
qui changeront d’état (en général un booléen : true
ou false
) dans l’isr
. Ces changements seront ensuite traités dans la boucle principale :
hw_timer_t * timer = NULL;
volatile boolean tick_flags = false;
void IRAM_ATTR timer_isr() {
tick_flags = true;
}
void setup
Cette section est réservée aux abonnés. Il vous reste 78% à découvrir.
Devenir membre premiumDéjà abonné ? Connectez-vous
Utilisations avancées des timers sur l’ESP32 en code Arduino
Utiliser des sémaphores pour gérer l’accès lecture/écriture à une variable partagée
C’est une bonne pratique d’encadrer le code de l’interruption d’un timer dans un sémaphore pour gérer l’accès aux variables globales. Cela permet de prédire le fonctionnement du programme quand la routine d’interruption et le programme principal veulent écrire au même moment la même variable. Puisque le code Arduino pour ESP32 utilise freeRTOS en tant qu’OS en temps réel, les sémaphores sont déjà intégrés : il ne reste qu’à les utiliser directement dans notre programme :
Cette section est réservée aux abonnés. Il vous reste 94% à découvrir.
Devenir membre premiumDéjà abonné ? Connectez-vous