Files
formation-arduino/modules/06-interruptions-timers.qmd
ArtemiaandClaude Opus 5.5 372c5a3e1e Illustre les modules 4 à 6 (9 figures)
M4 : trame UART 8N1, câblage série croisé. M5 : bus I2C, trame I2C, bus SPI. M6 : tâches millis(), déroulé d'une ISR, modes de déclenchement, encodeur rotatif.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:43 +02:00

176 lines
7.2 KiB
Plaintext

---
title: "Module 6 — Interruptions matérielles & Timers"
---
## Objectifs du module
- Comprendre les limites de `delay()` et de la boucle `loop()` classique.
- Utiliser `millis()` pour gérer plusieurs tâches périodiques sans bloquer le programme.
- Mettre en œuvre une interruption matérielle avec `attachInterrupt()`.
- Comprendre le rôle de `IRAM_ATTR` et les bonnes pratiques dans une routine d'interruption (ISR).
## 1. Le problème de `delay()`
La fonction `delay()` bloque totalement l'exécution du programme pendant la durée spécifiée : aucune autre instruction (lecture de bouton, mise à jour d'écran, communication réseau) ne peut s'exécuter pendant ce temps. Pour un programme qui ne fait qu'une seule chose, ce n'est pas gênant ; mais dès qu'on veut gérer plusieurs comportements simultanés (ex : clignoter une LED tout en surveillant un bouton), `delay()` devient un frein majeur.
## 2. La technique du « blink sans delay » avec `millis()`
`millis()` renvoie le nombre de millisecondes écoulées depuis le démarrage du programme. En comparant cette valeur à un dernier instant mémorisé, on peut exécuter une action périodique sans jamais bloquer la boucle `loop()`.
```cpp
#define LED_PIN 4
unsigned long dernierChangement = 0;
const unsigned long intervalle = 500; // ms
bool etatLed = false;
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
unsigned long maintenant = millis();
if (maintenant - dernierChangement >= intervalle) {
dernierChangement = maintenant;
etatLed = !etatLed;
digitalWrite(LED_PIN, etatLed);
}
// Ici, on pourrait ajouter d'autres taches (lecture bouton, etc.)
// sans que le clignotement ne soit bloque.
}
```
::: {.callout-note title="Pourquoi unsigned long ?"}
`millis()` renvoie une valeur qui finit par déborder (« overflow ») après environ 49 jours de fonctionnement continu. Utiliser la soustraction (maintenant - dernierChangement) plutôt qu'une comparaison directe des deux valeurs absolues gère correctement ce débordement grâce à l'arithmétique non signée.
:::
## 3. Exercice : plusieurs tâches périodiques indépendantes
Reprenez la logique de « blink sans delay » pour gérer simultanément trois comportements à des fréquences différentes : clignotement d'une LED classique toutes les 300 ms, cycle de couleur de la LED NeoPixel toutes les 1000 ms, et affichage d'un message de debug toutes les 5000 ms.
![Trois tâches périodiques indépendantes gérées avec millis(), sans jamais bloquer loop()](../schemas/svg/m06-taches-millis.svg){#fig-m06-taches-millis}
## 4. Les interruptions matérielles
### 4.1 Principe
Une interruption permet au microcontrôleur de réagir instantanément à un évènement matériel (front montant ou descendant sur une broche), en suspendant momentanément l'exécution de la boucle principale pour exécuter une fonction dédiée, appelée ISR (Interrupt Service Routine). C'est indispensable pour ne manquer aucun évènement rapide, comme une impulsion d'encodeur rotatif ou un appui bref sur un bouton.
![Déroulement d'une interruption : l'ISR lève un drapeau, loop() fait le traitement](../schemas/svg/m06-deroule-isr.svg){#fig-m06-deroule-isr}
### 4.2 `attachInterrupt()`
```cpp
#define BUTTON_PIN 5
volatile bool interruptionDetectee = false;
void IRAM_ATTR gererInterruption() {
interruptionDetectee = true;
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), gererInterruption, FALLING);
}
void loop() {
if (interruptionDetectee) {
interruptionDetectee = false;
Serial.println("Interruption traitee dans loop()");
}
}
```
::: {.callout-note title="Règles d'or d'une ISR"}
Une routine d'interruption doit être aussi courte et rapide que possible : pas de `Serial.print()`, pas de `delay()`, pas d'allocation mémoire. La bonne pratique consiste à se contenter de modifier une variable volatile (comme ci-dessus) et à traiter l'évènement dans `loop()`. Le mot-clé `IRAM_ATTR` indique que le code de la fonction doit être placé en RAM interne pour un temps d'exécution garanti, même si le cache Flash est momentanément indisponible.
:::
### 4.3 Les modes de déclenchement
| Mode | Déclenchement |
|----------|----------------------------------------|
| RISING | Sur front montant (passage de LOW à HIGH) |
| FALLING | Sur front descendant (passage de HIGH à LOW) |
| CHANGE | Sur tout changement d'état |
| LOW / HIGH | Tant que la broche reste dans cet état (peu utilisé) |
![Instants de déclenchement de l'ISR selon le mode choisi](../schemas/svg/m06-modes-declenchement.svg){#fig-m06-modes-declenchement}
## 5. Exercice : compteur d'impulsions (encodeur rotatif)
Un encodeur rotatif génère des impulsions sur deux broches (souvent nommées CLK et DT) permettant de détecter le sens de rotation. Câblez un encodeur rotatif et comptez le nombre de crans parcourus à l'aide d'une interruption sur la broche CLK.
![Encodeur rotatif : l'état de DT au front descendant de CLK donne le sens de rotation](../schemas/svg/m06-encodeur.svg){#fig-m06-encodeur}
```cpp
#define CLK_PIN 6
#define DT_PIN 7
volatile int compteur = 0;
void IRAM_ATTR gererRotation() {
if (digitalRead(DT_PIN) == HIGH) {
compteur++;
} else {
compteur--;
}
}
void setup() {
Serial.begin(115200);
pinMode(CLK_PIN, INPUT_PULLUP);
pinMode(DT_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(CLK_PIN), gererRotation, FALLING);
}
void loop() {
static int dernierAffiche = 0;
if (compteur != dernierAffiche) {
dernierAffiche = compteur;
Serial.printf("Position: %d\n", compteur);
}
}
```
## 6. Les timers matériels
En complément des interruptions déclenchées par un évènement externe, l'ESP32-S3 dispose de timers matériels internes capables de générer une interruption à intervalle régulier, indépendamment de la boucle `loop()`. Utile pour des tâches critiques en temps (échantillonnage régulier, watchdog applicatif).
```cpp
hw_timer_t *minuteur = NULL;
volatile bool declenche = false;
void IRAM_ATTR onTimer() {
declenche = true;
}
void setup() {
Serial.begin(115200);
minuteur = timerBegin(1000000); // 1 MHz -> tick = 1 microseconde
timerAttachInterrupt(minuteur, &onTimer);
timerAlarm(minuteur, 1000000, true, 0); // declenchement toutes les 1 000 000 us = 1 s
}
void loop() {
if (declenche) {
declenche = false;
Serial.println("Tick timer materiel (1s)");
}
}
```
::: {.callout-note title="Version de l'API timer"}
Comme pour LEDC, l'API des timers matériels a évolué entre les versions du core Arduino-ESP32 (l'ancienne API utilisait timerBegin(numero, diviseur, compteMontant)). Vérifiez la signature correspondant à votre version installée si le code ne compile pas.
:::
## Checklist de fin de module
- [ ] Je sais remplacer un `delay()` par une gestion à base de `millis()`.
- [ ] Je sais faire cohabiter plusieurs tâches périodiques indépendantes dans `loop()`.
- [ ] Je sais déclencher une interruption avec `attachInterrupt()` et écrire une ISR minimale et sûre.
- [ ] J'ai réalisé le compteur d'impulsions avec un encodeur rotatif.
- [ ] Je comprends le rôle d'un timer matériel par rapport à une interruption externe.