Sélectionner une page

 

Minuteries sur ESP32

Le but de cet article est d’expliquer comment régler les interruptions de la minuterie sur l’ESP32, en utilisant le cœur Arduino. Les tests ont été réalisés sur un dispositif DFRobot ESP-WROOM-32 intégré à une carte ESP32 FireBeetle.

Le code présenté ici est basé sur cet exemple des bibliothèques de base Arduino, que nous vous encourageons à essayer. Donc, dans ce tutoriel, nous allons vérifier comment régler le minuteur pour générer périodiquement une interruption et comment la gérer.

Alarmes

L’ESP32 dispose de deux groupes de minuteries, chacune avec deux minuteries matérielles à usage général. Tous les minuteries sont basées sur des compteurs 64 bits et des pré-calibres 16 bits.

Le prescaler est utilisé pour diviser la fréquence du signal de base (généralement 80 MHz), qui est ensuite utilisé pour augmenter / diminuer la minuterie. Puisque le préqualificateur est de 16 bits, vous pouvez diviser la fréquence du signal d’horloge par un facteur de 2 à 65536, ce qui donne une grande liberté de configuration.

Les minuteries peuvent être configurées pour compter vers le haut ou vers le bas et prendre en charge le rechargement automatique et le rechargement du logiciel. Ils peuvent également générer des alarmes lorsqu’ils atteignent une valeur spécifique, définie par le logiciel [2]. La valeur du compteur peut être lue par le logiciel.

Variables globales

Nous commençons notre code en déclarant quelques variables globales. Le premier sera un compteur qui sera utilisé par le sous-programme de service d’interruption pour signaler à la boucle principale qu’une interruption s’est produite.

Les ISR doivent fonctionner aussi vite que possible et ne doivent pas effectuer d’opérations longues, telles que l’écriture sur le port série. Ainsi, un bon moyen d’implémenter le code de gestion des interruptions est de faire en sorte que l’ISR ne signale que l’occurrence de l’interruption et reporte le traitement réel (qui peut contenir des opérations qui prennent un certain temps) à la boucle principale.

Le compteur est également utile car si, pour une raison quelconque, la gestion d’une interruption dans la boucle principale prend plus de temps que prévu et entre-temps plus d’interruptions se produisent, elles ne sont pas perdues car le compteur s’incrémentera en conséquence. D’un autre côté, si un drapeau est utilisé comme mécanisme de signalisation, il restera fidèle à la vie et les interruptions seront perdues, puisque la boucle principale supposera seulement qu’une autre s’est produite.

Comme d’habitude, puisque cette variable de compteur sera partagée entre la boucle principale et l’ISR, elle doit alors être déclarée avec le mot-clé volatile, ce qui l’empêche d’être supprimée en raison des optimisations du compilateur.

volatile int interruptCounter;

Nous aurons un compteur supplémentaire pour enregistrer le nombre d’interruptions déjà survenues depuis le début du programme. Cela ne sera utilisé que par la boucle principale et n’a donc pas besoin d’être déclaré volatile.

int totalInterruptCounter;

Pour configurer la minuterie, nous devrons pointer sur une variable de type hw_timer_t, que nous utiliserons plus tard dans la fonction de configuration Arduino.

hw_timer_t * timer = NULL;

Enfin, nous aurons besoin de déclarer une variable de type portMUX_TYPE, que nous utiliserons pour assurer la synchronisation entre la boucle principale et l’ISR, lorsque nous modifierons une variable partagée.

portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

Fonction de configuration

Comme d’habitude, nous allons démarrer notre fonction de configuration en ouvrant une connexion série, afin que plus tard, nous puissions afficher les résultats de notre programme pour être disponibles dans le moniteur série Arduino IDE.

Serial.begin(115200);

Ensuite, nous initialiserons notre timer avec un appel à la fonction timerBegin, qui retournera un pointeur vers une structure de type hw_timer_t, qui est la variable globale timer que nous avons déclarée dans la section précédente.

En entrée, cette fonction reçoit le numéro de la minuterie que nous voulons utiliser (de 0 à 3, puisque nous avons 4 minuteries matérielles), la valeur du prescaler et un drapeau qui indique si le compteur doit compter à la hausse (vrai) ou à la baisse (faux).

Pour cet exemple, nous utiliserons le premier minuteur et passerons true au dernier paramètre, de sorte que le compteur compte.

En ce qui concerne le prescaler, nous avons dit dans la section d’introduction que généralement la fréquence du signal de base utilisée par les compteurs ESP32 est de 80 MHz (cela est vrai pour la carte FireBeetle). Cette valeur est égale à 80 000 000 Hz, ce qui signifie que le signal entraînerait une incrémentation de 80 000 000 de la minuterie par seconde.

Bien que nous puissions faire les calculs avec cette valeur pour définir le numéro du compteur pour générer l’interruption, nous profiterons du prescaler pour le simplifier. Ainsi, si nous divisons cette valeur par 80 (en utilisant 80 comme valeur du prescaler), nous obtiendrons un signal avec une fréquence de 1 MHz qui fera incrémenter le timer de 1 000 000 fois par seconde.

À partir de la valeur précédente, si nous l’inversons, nous savons que le compteur s’incrémentera toutes les microsecondes. Ainsi, en utilisant un prescaler de 80, lorsque nous appelons la fonction pour définir la valeur du compteur pour générer l’interruption, nous spécifierons cette valeur en microsecondes.

timer = timerBegin(0, 80, true);

Mais avant d’activer le minuteur, nous devons le lier à une fonction de gestion, qui sera exécutée lorsque l’interruption est générée. Cela se fait avec un appel à la fonction timerAttachInterrupt.

Cette fonction reçoit en entrée un pointeur vers le timer initialisé, que nous stockons dans notre variable globale, l’adresse de la fonction qui gérera l’interruption et un drapeau qui indique si l’interruption à générer est edge (true) ou level ( faux). Vous pouvez en savoir plus sur la différence entre les sauts de bord et de niveau ici.

Ainsi, comme mentionné, nous passerons notre variable de minuterie globale comme première entrée, comme seconde l’adresse d’une fonction appelée onTimer que nous spécifierons plus tard, et comme troisième la valeur true, donc l’interruption générée est de type edge.

timerAttachInterrupt(timer, &onTimer, true);

Ensuite, nous utiliserons la fonction timerAlarmWrite pour spécifier la valeur du compteur à laquelle l’interruption du minuteur sera générée. Ainsi, cette fonction reçoit comme première entrée le pointeur vers le temporisateur, comme seconde la valeur du compteur auquel l’interruption doit être générée, et comme troisième un drapeau qui indique si le temporisateur doit se recharger automatiquement lors de la génération de l’interruption.

Ainsi, comme premier argument, nous passerons à nouveau notre variable de temporisation globale, et comme troisième argument, nous passerons true, de sorte que le compteur sera rechargé et ainsi l’interruption sera générée périodiquement.

En ce qui concerne le deuxième argument, rappelez-vous que nous définissons le prescaler pour désigner le nombre de microsecondes après lequel l’interruption doit se produire. Donc, pour cet exemple, nous supposons que nous voulons générer une interruption toutes les secondes, et donc nous passons la valeur de 1 000 000 microsecondes, qui est égale à 1 seconde.

Notez que cette valeur est spécifiée en microsecondes uniquement si nous spécifions la valeur 80 pour le prescaler. Nous pouvons utiliser différentes valeurs du prescaler et dans ce cas nous devons faire les calculs pour savoir quand le compteur atteindra une certaine valeur.

timerAlarmWrite(timer, 1000000, true);

Nous avons terminé notre fonction de configuration en activant la minuterie avec un appel à la fonction TimerAlarmEnable, en passant notre variable timer en entrée.

timerAlarmEnable(timer);

Le code final de la fonction de configuration peut être vu ci-dessous.

void setup() {
 
  Serial.begin(115200);
 
  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 1000000, true);
  timerAlarmEnable(timer);
 
}

La boucle principale

Comme déjà dit, la boucle principale sera l’endroit où nous gérons réellement l’interruption de la minuterie, après qu’elle ait été signalée par l’ISR. Pour plus de simplicité, nous utiliserons le sondage pour vérifier la valeur du compteur d’interruptions, mais naturellement une approche beaucoup plus efficace serait d’utiliser un sémaphore pour verrouiller la boucle principale, qui serait alors déverrouillée par l’ISR. C’est l’approche utilisée dans le exemple original.

Ainsi, nous vérifierons si la variable interruptCounter est supérieure à zéro et si c’est le cas, nous introduirons le code de gestion des interruptions. Là, la première chose que nous allons faire est de diminuer ce compteur, signalant que l’interruption a été reconnue et sera traitée.

Puisque cette variable est partagée avec l’ISR, nous le ferons dans une section critique, que nous spécifions à l’aide d’un portENTER_CRITICAL et d’une macro portEXIT_CRITICAL. Les deux appels prennent comme argument l’adresse de notre variable globale portMUX_TYPE.

if (interruptCounter > 0) {
 
    portENTER_CRITICAL(&timerMux);
    interruptCounter--;
    portEXIT_CRITICAL(&timerMux);
 
    // Interrupt handling code
  }

Le traitement réel des interruptions consistera simplement à incrémenter le compteur du nombre total d’interruptions survenues depuis le début du programme et à l’imprimer sur le port série. Vous pouvez vérifier ci-dessous le code complet de la boucle principale, qui comprend déjà cet appel.

void loop() {
 
  if (interruptCounter > 0) {
 
    portENTER_CRITICAL(&timerMux);
    interruptCounter--;
    portEXIT_CRITICAL(&timerMux);
 
    totalInterruptCounter++;
 
    Serial.print("An interrupt as occurred. Total number: ");
    Serial.println(totalInterruptCounter);
 
  }
}

Le code ISR

La routine de service d’interruption doit être une fonction qui renvoie void et ne prend aucun argument.

Notre fonction sera aussi simple que d’incrémenter le compteur d’interruptions qui signalera à la boucle principale qu’une interruption s’est produite. Cela se fera dans une section critique, déclarée avec les macros portENTER_CRITICAL_ISR et portEXIT_CRITICAL_ISR, qui reçoivent toutes deux comme paramètres d’entrée l’adresse de la variable globale portMUX_TYPE que nous avons déclarée précédemment.

La routine de gestion des interruptions doit avoir l’attribut IRAM_ATTR, afin que le compilateur puisse placer le code dans IRAM. De plus, les routines de gestion des interruptions ne doivent appeler que les fonctions également placées dans IRAM, comme vous pouvez le voir ici dans le Documentation IDF.

Le code complet de cette fonction peut être vu ci-dessous.

void IRAM_ATTR onTimer() {
  portENTER_CRITICAL_ISR(&timerMux);
  interruptCounter++;
  portEXIT_CRITICAL_ISR(&timerMux);
 
}

Le code final

Le code source final de notre programme d’interruption de minuterie périodique peut être vu ci-dessous.

volatile int interruptCounter;
int totalInterruptCounter;
 
hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
 
void IRAM_ATTR onTimer() {
  portENTER_CRITICAL_ISR(&timerMux);
  interruptCounter++;
  portEXIT_CRITICAL_ISR(&timerMux);
 
}
 
void setup() {
 
  Serial.begin(115200);
 
  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 1000000, true);
  timerAlarmEnable(timer);
 
}
 
void loop() {
 
  if (interruptCounter > 0) {
 
    portENTER_CRITICAL(&timerMux);
    interruptCounter--;
    portEXIT_CRITICAL(&timerMux);
 
    totalInterruptCounter++;
 
    Serial.print("An interrupt as occurred. Total number: ");
    Serial.println(totalInterruptCounter);
 
  }
}

Tester le code

Test ESP32 - Code Arduino ESP32: interruptions de la minuterie

Pour tester le code, téléchargez-le simplement sur votre carte ESP32 et ouvrez l’IDE Arduino Serial Monitor. Vous devriez obtenir une sortie similaire à celle de la figure précédente, où les messages doivent être imprimés avec une périodicité de 1 seconde.