Bases · 15 min de lecture

Interruptions Arduino : attachInterrupt, ISR et anti-rebond

Utiliser les interruptions Arduino : attachInterrupt, broches 2 et 3 de l'Uno, RISING/FALLING/CHANGE, volatile, anti-rebond, encodeur et tr/min, ESP32 et Pico.

Une interruption Arduino pousse le processeur à abandonner ce qu'il est en train de faire dès que le changement que tu attends se produit sur une broche (ou qu'un événement de timer survient), pour exécuter une fonction spéciale que tu as écrite. En bref : au lieu de demander sans arrêt dans loop() « le bouton a-t-il été appuyé ? », tu dis à l'Arduino « préviens-moi quand il est appuyé ». Tu le fais avec la fonction attachInterrupt() ; sur l'Arduino Uno, les broches d'interruption sont D2 et D3, alors que sur l'ESP32 et le Raspberry Pi Pico presque chaque GPIO peut déclencher une interruption.

Les interruptions sont indispensables pour compter des impulsions très rapides sans en perdre une seule (encodeurs, capteurs de vitesse, débitmètres), saisir un événement bref au milieu d'une longue tâche et réveiller une carte en veille. Dans ce guide, tu verras pas à pas comment une interruption fonctionne côté matériel, quelles broches utiliser sur chaque carte, la différence entre les modes RISING, FALLING et CHANGE, les règles pour écrire une routine d'interruption (ISR), comment supprimer les rebonds d'un bouton, ainsi que des exemples avec un bouton, un encodeur et un compte-tours. Pour finir, tu découvriras IRAM_ATTR sur l'ESP32 et irq avec MicroPython sur le Pico.

Broches d'interruption de l'Arduino Uno D2 (INT0) et D3 (INT1)
L'Arduino Uno ne possède que deux broches d'interruption externe : D2 (INT0) et D3 (INT1).

Qu'est-ce qu'une interruption et comment ça marche ?

Pense à une sonnette : sans elle, tu devrais aller à la porte toutes les minutes pour vérifier si quelqu'un est arrivé. C'est ce qu'on appelle la scrutation (polling), et lire une broche avec digitalRead() dans loop(), c'est exactement ça. La sonnette, elle, est une interruption : elle retentit pendant que tu es occupé à autre chose, tu t'arrêtes, tu vas voir à la porte puis tu reprends là où tu en étais. Un microcontrôleur fonctionne de la même façon. Quand le changement choisi se produit sur la broche, le matériel lève un drapeau d'interruption ; le processeur termine l'instruction en cours, sauvegarde ses registres, saute via le vecteur d'interruption vers ton ISR (Interrupt Service Routine, routine de service d'interruption) puis, une fois l'ISR terminée, reprend loop() exactement là où il s'était arrêté. Sur l'Uno, ce basculement ne prend que quelques microsecondes.

CaractéristiqueScrutation (polling)Interruption
Fonctionnementloop() lit la broche en boucleLe matériel détecte le changement et lance l'ISR
Temps de réactionPlus loop() est lente, plus le retard est grandQuelques microsecondes
Avec delay() dans le codeDes événements peuvent être perdusLes événements sont quand même captés
Impulsions très rapidesRatéesComptées une par une
Complexité du codeSimpleNécessite volatile et des lectures atomiques
Idéal pourBoutons, capteurs lentsEncodeurs, capteurs de vitesse et de débit, réveil de la veille

Quelles broches gèrent les interruptions ? Arduino, ESP32 et Pico

CarteBroches d'interruption externeRemarque
Arduino Uno R3, Nano, Pro MiniD2, D3D2 = INT0, D3 = INT1
Arduino Uno R4 (Minima / WiFi)D2, D3Broches indiquées dans la référence officielle Arduino
Arduino Mega 25602, 3, 18, 19, 20, 2120 et 21 sont inutilisables pour les interruptions tant qu'elles servent à l'I2C
Arduino Leonardo, Micro0, 1, 2, 3, 70 et 1 sont aussi les broches du port série
Arduino Nano EveryToutes les broches numériquesProcesseur ATmega4809
ESP32Tous les GPIOGPIO 34–39 sont uniquement des entrées, sans pull-up interne
ESP8266 (NodeMCU)Toutes sauf GPIO 16 (D0)Les ISR doivent être marquées IRAM_ATTR
Raspberry Pi Pico / Pico WToutes, de GP0 à GP28Broches en 3,3 V, n'applique jamais 5 V

Dans ton code, ne passe pas directement le numéro de broche : convertis-le en numéro d'interruption avec digitalPinToInterrupt(broche), car ce numéro change d'une carte à l'autre. Sur l'Uno, D2 correspond à l'interruption 0 et D3 à l'interruption 1, alors que sur le Leonardo l'interruption 0 correspond à D3 et l'interruption 1 à D2. Si tu as besoin de plus de deux interruptions sur l'Uno, les interruptions de changement d'état (Pin Change Interrupt, PCINT) fonctionnent sur presque toutes les broches ; elles indiquent seulement que « quelque chose a changé », et toutes les broches d'un même port partagent une seule ISR. Les bibliothèques PinChangeInterrupt et EnableInterrupt en simplifient l'usage. Tu trouveras le brochage complet de ta carte dans l'outil Pinout et les fonctions des broches de l'Uno dans l'article brochage de l'Arduino Uno.

L'erreur la plus courante consiste à écrire attachInterrupt(2, fonction, FALLING). Ici, le 2 est lu comme un numéro d'interruption et non comme un numéro de broche ; l'Uno n'a pas d'interruption 2, donc il ne se passe rien et aucun message d'erreur n'apparaît. La bonne forme est attachInterrupt(digitalPinToInterrupt(2), fonction, FALLING).

Utiliser attachInterrupt et les modes d'interruption

La syntaxe est attachInterrupt(digitalPinToInterrupt(broche), ISR, mode). Le deuxième paramètre est le nom de la fonction exécutée lorsque l'interruption survient ; cette fonction ne prend aucun paramètre et ne renvoie rien (void). Le troisième paramètre indique quel événement déclenche l'interruption. On utilise detachInterrupt(digitalPinToInterrupt(broche)) pour désactiver une interruption, noInterrupts() pour suspendre brièvement toutes les interruptions et interrupts() pour les réactiver.

ModeQuand se déclenche-t-il ?Usage typique
LOWEn boucle tant que la broche est à LOWRéveil de la veille
CHANGEÀ chaque changement (les deux fronts)Encodeurs, mesure de durée d'impulsion, anti-rebond
RISINGAu passage de LOW à HIGH (front montant)Début d'une impulsion de capteur, bouton avec pull-down
FALLINGAu passage de HIGH à LOW (front descendant)Appui sur un bouton avec pull-up, capteurs à collecteur ouvert
HIGHTant que la broche est à HIGHSeulement sur Due, Zero et MKR1000 ; ONHIGH sur l'ESP32

Choisis le mode selon ton montage. Si tu as câblé le bouton avec INPUT_PULLUP, la broche est normalement à HIGH et passe à LOW à l'appui : c'est donc FALLING qui détecte l'appui. Utilise le mode LOW avec prudence : tant que la broche reste à LOW, l'ISR s'exécute en boucle et loop() n'avance presque plus. C'est pourquoi on le réserve en général au réveil de la carte ; pour sortir l'Uno de son mode de veille le plus profond (power-down), la fiche technique recommande une interruption sur niveau LOW ou une interruption de changement d'état. Sur l'ESP32, les constantes LOW et HIGH ne fonctionnent pas comme prévu en mode d'interruption : LOW n'active pas du tout l'interruption et HIGH se comporte comme un front montant. Pour les interruptions sur niveau, on y écrit ONLOW et ONHIGH.

Premier exemple : compter les appuis avec une interruption

  1. Relie une patte du bouton à D2 et l'autre à GND. Aucune résistance externe n'est nécessaire : on utilise la pull-up interne.
  2. Téléverse le code ci-dessous et ouvre le moniteur série à 9600 bauds.
  3. Appuie plusieurs fois sur le bouton et regarde le compteur augmenter. Malgré le delay(500) dans loop(), aucun appui n'est perdu.
  4. Ne sois pas surpris si le nombre saute parfois de 2 ou 3 d'un coup : ce sont les rebonds des contacts, et on va les corriger juste après.
C++ (Arduino IDE)
1const byte BUTTON_PIN = 2;                // kesme pini D2 (INT0) / interrupt pin D2 (INT0)
2volatile unsigned int pressCount = 0;     // ISR ile paylaşılan sayaç / counter shared with the ISR
3
4void onPress() {                          // ISR: kısa ve hızlı / ISR: short and fast
5  pressCount++;
6}
7
8void setup() {
9  Serial.begin(9600);
10  pinMode(BUTTON_PIN, INPUT_PULLUP);      // dahili pull-up, buton GND'ye / internal pull-up, button to GND
11  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onPress, FALLING);
12}
13
14void loop() {
15  noInterrupts();                         // 2 baytlık değeri güvenle kopyala / copy the 2-byte value safely
16  unsigned int count = pressCount;
17  interrupts();
18
19  Serial.print(F("Basis / Presses: "));
20  Serial.println(count);
21  delay(500);                             // loop yavaş olsa da basış kaçmaz / presses are not missed even if loop is slow
22}

Ce code contient trois points importants. D'abord, onPress() se contente d'ajouter un au compteur et se termine aussitôt : le rôle d'une ISR est de noter l'événement, pas de faire le travail. Ensuite, le compteur est déclaré volatile : le compilateur ne le garde donc pas dans un registre au risque de lire une ancienne valeur, il va chercher la valeur à jour en mémoire à chaque fois. Enfin, loop() désactive brièvement les interruptions pendant qu'elle lit le compteur. Un unsigned int occupe 2 octets sur l'Uno, et le processeur 8 bits le lit en deux étapes. Quand le compteur passe de 255 à 256, c'est-à-dire de 0x00FF à 0x0100, loop() lit d'abord l'octet bas (0xFF) ; si l'interruption arrive à ce moment précis et incrémente le compteur, loop() lit ensuite l'octet haut (0x01) et obtient 0x01FF, soit 511. Comme ce bug apparaît très rarement, il est aussi difficile à trouver. Sur les cartes AVR, la macro ATOMIC_BLOCK(ATOMIC_RESTORESTATE) de util/atomic.h offre la même protection ; les variables d'un seul octet comme bool et byte n'ont pas ce problème.

Règles de l'ISR : courte, volatile et sans delay

  • Garde-la courte et rapide : pendant qu'une ISR s'exécute, les autres interruptions attendent. Sur l'Uno, le compteur de millis() et le port série fonctionnent eux aussi par interruption, donc une ISR longue fausse la temporisation et peut te faire perdre des données série entrantes. Une ISR idéale se termine en quelques microsecondes.
  • N'utilise pas delay() : delay() mesure le temps avec des compteurs mis à jour par l'interruption du Timer0 ; dans une ISR les interruptions sont désactivées, donc elle ne fonctionne pas. Tu peux lire millis(), mais elle n'avance pas pendant l'ISR, et micros() renvoie des valeurs fausses après 1–2 ms. Si tu as vraiment besoin d'une attente très courte, delayMicroseconds() fonctionne.
  • N'utilise pas Serial.print() : l'envoi série dépend lui aussi des interruptions ; il allonge l'ISR et se bloque si le tampon est plein. Laisse l'affichage à loop().
  • Déclare volatile les variables partagées : toute variable utilisée à la fois dans l'ISR et dans loop() doit être déclarée volatile.
  • Lis les variables multi-octets de façon atomique : dans loop(), copie les variables int, long, float et unsigned long entre noInterrupts() et interrupts().
  • Ni paramètres ni valeur de retour : une ISR doit être void et ne prendre aucun paramètre ; les données s'échangent par des variables globales.
  • Pas de travail lourd dans l'ISR : écrire sur un écran I2C, enregistrer sur une carte SD ou appeler des bibliothèques qui ont elles-mêmes besoin d'interruptions, comme Wire, provoque des blocages dans une ISR.
Le schéma le plus robuste est « lever un drapeau, faire le travail dans loop » : l'ISR se contente de passer une variable bool volatile à true ou d'incrémenter un compteur, et loop() fait le vrai travail, comme mettre à jour un écran, piloter un moteur ou calculer, dès qu'elle voit ce drapeau. Si tu écris loop() avec millis() plutôt que delay, tout le système reste rapide et fiable.

Lire un bouton par interruption et supprimer les rebonds

Quand tu appuies sur un bouton mécanique, les contacts métalliques ne se stabilisent pas tout de suite ; ils s'ouvrent et se ferment plusieurs fois pendant environ 0,1 à 10 millisecondes. loop() est peut-être trop lente pour le remarquer, mais une interruption capte chaque front, si bien qu'un seul appui peut produire 3 à 5 interruptions. De plus, les rebonds ne surviennent pas seulement à l'appui mais aussi au relâchement : un code qui utilise uniquement FALLING avec un verrou temporel peut donc compter deux fois si tu maintiens le bouton un moment avant de le relâcher. Le code ci-dessous utilise le mode CHANGE et n'accepte un appui que si la broche est à LOW et que la ligne était calme depuis au moins 50 ms. Les rebonds à l'appui comme au relâchement sont ainsi filtrés.

C++ (Arduino IDE)
1const byte BUTTON_PIN = 2;                // kesme pini / interrupt pin
2const byte LED_PIN = 13;                  // dahili LED / onboard LED
3const unsigned long QUIET_MS = 50;        // gereken sessizlik süresi / required quiet time
4
5volatile bool ledState = false;           // ISR ile paylaşılır / shared with the ISR
6
7void onButtonChange() {                   // iki kenarda da çalışır / runs on both edges
8  static unsigned long lastEdgeMs = 0;
9  unsigned long now = millis();           // ISR'de okunur ama artmaz / readable in an ISR but does not advance
10  bool pressed = (digitalRead(BUTTON_PIN) == LOW);
11
12  if (pressed && now - lastEdgeMs > QUIET_MS) {   // hat sessizdi: gerçek basış / line was quiet: real press
13    ledState = !ledState;
14  }
15  lastEdgeMs = now;                       // her kenar süreyi yeniden başlatır / every edge restarts the timer
16}
17
18void setup() {
19  pinMode(LED_PIN, OUTPUT);
20  pinMode(BUTTON_PIN, INPUT_PULLUP);
21  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonChange, CHANGE);
22}
23
24void loop() {
25  digitalWrite(LED_PIN, ledState ? HIGH : LOW);   // bool tek bayt, okuması atomik / bool is one byte, atomic read
26  // burada uzun işler olsa da basış kaçmaz / long tasks here will not make you miss a press
27}

En plus du logiciel, tu peux aussi supprimer les rebonds côté matériel : place un condensateur de 100 nF entre les deux bornes du bouton et une résistance de pull-up de 10 kΩ sur la broche, et la constante de temps devient τ = R × C = 10 kΩ × 100 nF = 1 ms, ce qui lisse les rebonds courts ; le trigger de Schmitt des entrées de l'Uno nettoie en plus le front. Tu peux calculer d'autres valeurs avec l'outil constante de temps RC. Pour être honnête, pour des boutons appuyés à la main, une interruption n'est souvent pas nécessaire ; si loop() est rapide, la scrutation suffit. Pour la pull-up, la pull-down et l'anti-rebond avec millis(), consulte comment brancher un bouton sur Arduino. Réserve les interruptions aux signaux rapides et brefs.

Lire un encodeur rotatif par interruption

Quand tu tournes un encodeur rotatif (comme le KY-040), il produit sur deux broches appelées CLK et DT des signaux carrés décalés l'un par rapport à l'autre. Si tu le tournes vite, les impulsions durent moins d'une milliseconde et la scrutation rate facilement des pas. La méthode la plus simple consiste à relier CLK à une broche d'interruption et à lire le niveau de DT au moment où CLK descend : si DT est à HIGH, la rotation s'est faite dans un sens, s'il est à LOW, dans l'autre. Cette version compte un pas par cran ; si le sens est inversé, échange les lignes ++ et --. Les encodeurs bon marché rebondissent aussi : ajouter un condensateur de 10–100 nF entre CLK et GND et entre DT et GND améliore nettement le résultat. Pour une version qui place les deux broches sur interruption et utilise une table d'états pour ne rater aucun pas, jette un œil au guide de l'encodeur rotatif KY-040.

C++ (Arduino IDE)
1const byte PIN_CLK = 2;                   // kesme pini / interrupt pin
2const byte PIN_DT = 4;                    // normal dijital giriş / regular digital input
3
4volatile long position = 0;               // 4 baytlık konum / 4-byte position
5
6void onClkFall() {
7  if (digitalRead(PIN_DT) == HIGH) {      // yön DT seviyesinden / direction from the DT level
8    position++;
9  } else {
10    position--;
11  }
12}
13
14void setup() {
15  Serial.begin(115200);
16  pinMode(PIN_CLK, INPUT_PULLUP);
17  pinMode(PIN_DT, INPUT_PULLUP);
18  attachInterrupt(digitalPinToInterrupt(PIN_CLK), onClkFall, FALLING);
19}
20
21void loop() {
22  static long lastPos = 0;
23
24  noInterrupts();                         // atomik kopya / atomic copy
25  long pos = position;
26  interrupts();
27
28  if (pos != lastPos) {
29    Serial.println(pos);
30    lastPos = pos;
31  }
32}

Compter les tours : mesurer la vitesse d'un moteur en tr/min

Mesurer la vitesse de rotation d'un moteur ou d'une roue est l'usage classique des interruptions. Comme capteur, on utilise soit un capteur optique à fourche avec LM393 et un disque encodeur à 20 fentes, soit un capteur à effet Hall comme l'A3144 avec un aimant collé sur l'axe. Chaque fente ou chaque passage de l'aimant produit une impulsion. À partir du nombre d'impulsions comptées en une seconde, la vitesse vaut : tr/min = impulsions par seconde × 60 / impulsions par tour. Par exemple, si un disque à 20 fentes donne 100 impulsions par seconde, le moteur tourne à 100 × 60 / 20 = 300 tr/min.

C++ (Arduino IDE)
1const byte SENSOR_PIN = 3;                // kesme pini D3 (INT1) / interrupt pin D3 (INT1)
2const byte PULSES_PER_REV = 20;           // diskteki yarık sayısı / slots on the encoder disc
3
4volatile unsigned long pulseCount = 0;
5
6void onPulse() {
7  pulseCount++;
8}
9
10void setup() {
11  Serial.begin(9600);
12  pinMode(SENSOR_PIN, INPUT_PULLUP);
13  attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), onPulse, FALLING);
14}
15
16void loop() {
17  static unsigned long lastMs = 0;
18
19  if (millis() - lastMs >= 1000) {        // her saniye hesapla / compute once per second
20    lastMs += 1000;
21
22    noInterrupts();                       // kopyala ve sıfırla / copy and reset
23    unsigned long pulses = pulseCount;
24    pulseCount = 0;
25    interrupts();
26
27    float rpm = pulses * 60.0 / PULSES_PER_REV;
28    Serial.print(F("RPM: "));
29    Serial.println(rpm, 1);
30  }
31}

Ici, loop() copie et remet à zéro le compteur chaque seconde ; les deux opérations se trouvent dans le même bloc noInterrupts() pour qu'aucune impulsion ne soit perdue si une interruption survient entre les deux. Avec un disque à 20 fentes, la résolution est de 3 tr/min. Si tu veux plus de précision à basse vitesse, au lieu de compter les impulsions, mesure dans l'ISR le temps entre deux impulsions avec micros() et utilise tr/min = 60 000 000 / (période_µs × impulsions par tour). La même méthode s'applique au débitmètre YF-S201, à la mesure de vitesse avec un capteur à effet Hall et à la vitesse de roue d'un motoréducteur à courant continu.

Un mot rapide sur les interruptions de timer

Les interruptions ne viennent pas seulement des broches. Les timers internes du microcontrôleur peuvent aussi en générer à intervalles fixes, de sorte qu'une fonction s'exécute exactement toutes les 1 ms ou toutes les 500 ms, quoi que fasse loop(). L'Uno possède trois timers, et certains sont déjà utilisés par les fonctions d'Arduino :

TimerTailleUtilisé parBroches PWM
Timer08 bitsmillis(), delay(), micros()5, 6
Timer116 bitsBibliothèque Servo, TimerOne9, 10
Timer28 bitstone(), IRremote3, 11

Sur l'Uno, le plus simple est la bibliothèque TimerOne : Timer1.initialize(500000) fixe la période en microsecondes et Timer1.attachInterrupt(fonction) associe l'ISR exécutée toutes les 500 ms. Comme elle utilise le Timer1, elle entre en conflit avec la bibliothèque Servo et avec analogWrite() sur les broches 9 et 10. Sur l'ESP32 avec le cœur Arduino 3.x, une interruption de timer se configure avec timerBegin(1000000), timerAttachInterrupt() et timerAlarm() ; timerAlarmWrite() et timerAlarmEnable() de la version 2.x ont été supprimées, si bien que les anciens exemples échouent avec l'erreur « was not declared in this scope ». Sur le Pico, MicroPython crée une interruption périodique en une ligne avec Timer(period=500, mode=Timer.PERIODIC, callback=fonction). Pour le lien entre timers et PWM, consulte Qu'est-ce que le PWM ?

Interruptions sur l'ESP32 : IRAM_ATTR et portMUX

Sur l'ESP32, inutile de chercher une broche particulière : chaque GPIO peut déclencher une interruption. Seuls les GPIO 34–39 n'ont pas de pull-up interne, donc un bouton à cet endroit a besoin d'une pull-up externe de 10 kΩ ; par ailleurs, la liste d'errata d'Espressif signale que les GPIO 36 et 39 peuvent produire de fausses interruptions lorsque l'ADC est utilisé. La vraie différence sur l'ESP32, c'est l'attribut IRAM_ATTR placé devant l'ISR. Il place la fonction dans la RAM interne rapide (IRAM) au lieu de la mémoire flash. Pendant une écriture en flash (Preferences, SPIFFS, OTA), le cache est désactivé ; selon la configuration, une interruption qui accède à ce moment-là à du code en flash peut faire redémarrer la carte avec « Guru Meditation Error: Core 1 panic'ed (Cache disabled but cached memory region accessed) ». L'ESP8266 est encore plus strict : une ISR sans IRAM_ATTR arrête la carte avec le message « ISR not in IRAM! » dès l'appel à attachInterrupt.

C++ (Arduino IDE)
1const int BUTTON_PIN = 27;                // her GPIO kesme verebilir / any GPIO can interrupt
2const int LED_PIN = 2;                    // çoğu DevKit'te dahili LED / onboard LED on most DevKits
3const uint32_t QUIET_MS = 50;             // gereken sessizlik süresi / required quiet time
4
5volatile uint32_t pressCount = 0;
6portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;   // paylaşılan veri kilidi / lock for shared data
7
8void IRAM_ATTR onButtonChange() {         // ISR IRAM'de çalışır / ISR runs from IRAM
9  static uint32_t lastEdgeMs = 0;
10  uint32_t now = millis();
11  bool pressed = (digitalRead(BUTTON_PIN) == LOW);
12
13  if (pressed && now - lastEdgeMs > QUIET_MS) {
14    portENTER_CRITICAL_ISR(&mux);
15    pressCount++;
16    portEXIT_CRITICAL_ISR(&mux);
17  }
18  lastEdgeMs = now;
19}
20
21void setup() {
22  Serial.begin(115200);
23  pinMode(LED_PIN, OUTPUT);
24  pinMode(BUTTON_PIN, INPUT_PULLUP);
25  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonChange, CHANGE);
26}
27
28void loop() {
29  static uint32_t lastCount = 0;
30
31  portENTER_CRITICAL(&mux);               // loop tarafında kilit / lock on the loop side
32  uint32_t count = pressCount;
33  portEXIT_CRITICAL(&mux);
34
35  if (count != lastCount) {
36    lastCount = count;
37    digitalWrite(LED_PIN, (count % 2) ? HIGH : LOW);   // her basışta LED değişir / LED toggles on every press
38    Serial.printf("Basis / Press: %lu\n", (unsigned long)count);
39  }
40}

Comme l'ESP32 exécute FreeRTOS et possède deux cœurs, on protège les variables partagées avec un verrou portMUX_TYPE plutôt qu'avec le noInterrupts() de l'Uno : portENTER_CRITICAL_ISR() dans l'ISR et portENTER_CRITICAL() dans loop(). Lire une variable 32 bits se fait déjà en une seule étape sur l'ESP32, mais le verrou est indispensable pour des opérations comme lire puis remettre à zéro une valeur ou mettre à jour plusieurs variables ensemble. Si tu veux associer la même fonction à plusieurs broches, attachInterruptArg() permet de passer un paramètre à l'ISR, comme le numéro de broche. Pour les bases de l'ESP32, vois Qu'est-ce que l'ESP32 ?, et pour les tâches et les cœurs, ESP32 FreeRTOS et double cœur.

Interruptions sur le Raspberry Pi Pico : irq en MicroPython

Sur le Raspberry Pi Pico, toutes les broches de GP0 à GP28 peuvent déclencher une interruption. En MicroPython, on la configure avec la méthode irq() de l'objet broche : pour trigger, tu passes Pin.IRQ_FALLING, Pin.IRQ_RISING ou les deux combinés, et handler est la fonction exécutée lors de l'interruption ; elle reçoit en paramètre la broche qui l'a déclenchée. Python n'a pas de volatile : il suffit de déclarer global les variables que tu modifies dans l'ISR. Pour mesurer le temps, on utilise time.ticks_ms() et time.ticks_diff(), qui gère le débordement. Le code ci-dessous transpose sur le Pico la logique de temps calme de l'exemple Arduino :

MicroPython
1from machine import Pin
2import time
3
4led = Pin("LED", Pin.OUT)                 # dahili LED (Pico ve Pico W) / onboard LED (Pico and Pico W)
5button = Pin(14, Pin.IN, Pin.PULL_UP)     # buton GP14 ile GND arasında / button between GP14 and GND
6
7QUIET_MS = 50                             # gereken sessizlik süresi / required quiet time
8presses = 0
9last_edge = time.ticks_ms()
10
11def on_change(pin):                       # handler tetikleyen pini alır / handler receives the pin
12    global presses, last_edge
13    now = time.ticks_ms()
14    if pin.value() == 0 and time.ticks_diff(now, last_edge) > QUIET_MS:
15        presses += 1
16        led.toggle()
17    last_edge = now
18
19button.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=on_change)
20
21while True:
22    print("Basis / Presses:", presses)    # ana döngü uyurken bile basışlar sayılır / presses are counted while the loop sleeps
23    time.sleep(1)

Sur le Pico, irq fonctionne par défaut en mode soft : MicroPython met la fonction en file d'attente et l'exécute dès que possible, donc utiliser print ou créer une liste à l'intérieur ne pose pas de problème, mais la latence augmente un peu. Si tu passes hard=True, la latence descend à quelques microsecondes ; en contrepartie, tu ne peux pas allouer de mémoire dans l'ISR, si bien qu'une nouvelle liste, une chaîne de caractères ou même un calcul en float peut lever une « MemoryError ». Si tu programmes le Pico avec l'Arduino IDE (cœur d'Earle Philhower), attachInterrupt() fonctionne de la même façon sur tous les GPIO. Pour l'installation, consulte Qu'est-ce que le Raspberry Pi Pico ?, et pour les commandes de base, les bases de MicroPython.

Idées de projets avec des interruptions

  • Testeur de réflexes : mesure le temps que tu mets à appuyer sur le bouton après l'allumage d'une LED, avec micros() dans l'ISR et une précision inférieure à la milliseconde.
  • Compteur de vélo : place un aimant sur la roue et un capteur à effet Hall sur le cadre ; calcule la vitesse et la distance à partir du temps par tour.
  • Compteur d'eau : compte les impulsions d'un débitmètre YF-S201 et affiche les litres consommés.
  • Voiture robot qui roule droit : mesure la vitesse des deux moteurs avec des encodeurs de roue et égalise-les.
  • Navigation dans un menu : parcours un menu à l'écran et modifie les réglages avec un encodeur rotatif.
  • Alarme sur batterie : laisse la sortie d'un capteur PIR réveiller la carte endormie ; pour l'ESP32, consulte l'article sur le deep sleep.

Erreurs fréquentes et solutions

SymptômeCauseSolution
L'interruption ne se déclenche jamaisUn numéro de broche a été passé à attachInterrupt(2, ...) ou la broche ne gère pas les interruptionsUtilise digitalPinToInterrupt(broche) ; sur l'Uno, branche-toi sur D2 ou D3
Un appui fait monter le compteur de 2 à 5Rebonds des contactsAjoute un anti-rebond logiciel et/ou un condensateur de 100 nF
La variable ne change jamais dans loop()volatile oubliéDéclare volatile la variable partagée
De temps en temps, des valeurs absurdesVariable multi-octets lue de façon non atomiqueCopie-la entre noInterrupts() et interrupts()
Le programme se fige ou millis() dérivedelay(), Serial ou une longue tâche dans l'ISRDans l'ISR, ne mets à jour qu'un drapeau ou un compteur
Des interruptions arrivent sans toucher le boutonBroche en l'air (flottante), pas de pull-upUtilise INPUT_PULLUP ou une résistance externe de 10 kΩ
L'ISR s'exécute une fois aussitôt après un nouvel attachInterruptUn front arrivé pendant que l'interruption était désactivée a levé le drapeau (AVR)Sur l'Uno, écris EIFR = bit(INTF0); avant attachInterrupt (INTF1 pour D3)
L'ESP32 redémarre au hasardL'ISR n'est pas en IRAM ou fait un travail lourdAjoute IRAM_ATTR et raccourcis l'ISR

Les interruptions sont un outil puissant qui te permet de ne manquer aucun événement, même quand loop() est occupée à autre chose ; il suffit de retenir trois règles : garder l'ISR courte, déclarer volatile les variables partagées et les lire de façon atomique dans loop(). Dans l'application BlueGrays, tu trouveras des compteurs à bouton, un test de réflexes, des projets de boutons avec l'ESP32 et un projet Pico qui compare l'anti-rebond par interruption (IRQ) et par scrutation logicielle, chacun avec le schéma du circuit et le code pas à pas.

Questions fréquentes

Quelles broches de l'Arduino Uno gèrent les interruptions ?

L'Arduino Uno R3, le Nano et le Pro Mini n'ont que deux broches d'interruption externe : D2 (INT0) et D3 (INT1). Sur la Mega 2560, tu peux utiliser 2, 3, 18, 19, 20 et 21, et sur la Leonardo 0, 1, 2, 3 et 7. Sur les autres broches, seules les interruptions de changement d'état (PCINT) sont disponibles, et elles signalent uniquement qu'une broche a changé.

Pourquoi delay() ne fonctionne-t-il pas dans une interruption (ISR) ?

delay() mesure le temps avec des compteurs mis à jour par l'interruption du Timer0. Pendant qu'une ISR s'exécute, les autres interruptions attendent, donc ces compteurs n'avancent pas et delay() ne peut pas fonctionner correctement. Si quelque chose doit attendre, lève simplement un drapeau dans l'ISR et fais l'attente dans loop() avec millis().

À quoi sert volatile et quand faut-il l'utiliser ?

volatile indique au compilateur que la variable peut changer à tout moment en dehors du déroulement normal du programme, par exemple dans une ISR ; sa valeur n'est donc pas gardée dans un registre mais relue en mémoire à chaque fois. Toute variable utilisée à la fois dans une ISR et dans loop() doit être volatile. En revanche, volatile ne rend pas atomiques les lectures multi-octets : pour cela, on utilise noInterrupts().

Peut-on utiliser plus de 2 interruptions sur un Arduino Uno ?

Oui. Grâce aux interruptions de changement d'état (PCINT), presque toutes les broches de l'Uno peuvent déclencher une interruption. Elles signalent seulement un changement, et les broches d'un même port (D0–D7, D8–D13, A0–A5) partagent une seule ISR. Avec les bibliothèques PinChangeInterrupt ou EnableInterrupt, leur utilisation ressemble beaucoup à attachInterrupt.

Faut-il utiliser les interruptions ou la scrutation (polling) ?

Pour des boutons appuyés à la main et des capteurs qui évoluent lentement, la scrutation dans loop() suffit généralement et reste plus simple. Utilise les interruptions pour les impulsions rapides et brèves, comme celles des encodeurs, des capteurs de vitesse et des débitmètres, ou pour réveiller la carte de la veille. Si loop() est remplie de longs delay(), les remplacer par millis() règle aussi la plupart des problèmes.

Que se passe-t-il si j'oublie IRAM_ATTR sur l'ESP32 ?

Le code compile généralement et semble fonctionner, mais l'ISR reste en flash, démarre plus lentement et, selon la configuration, peut planter avec « Cache disabled but cached memory region accessed » pendant une écriture en flash. Sur l'ESP8266, sans IRAM_ATTR, la carte s'arrête aussitôt avec « ISR not in IRAM! ». Sur les cartes ESP, place donc IRAM_ATTR devant chaque ISR.

Voir tous les projets
Allumer et éteindre une LED avec un bouton
#10Débutant

Allumer et éteindre une LED avec un bouton

Un appui et la lumière s'allume, un deuxième et elle s'éteint, exactement comme l'interrupteur de ta chambre. L'Arduino remarque chaque appui et fait passer la LED à l'état inverse, en attendant un co

4.8(100)
167
Raspberry Pi Pico Bouton Debounce : IRQ vs Software Polling
#159Intermédiaire
Premium

Raspberry Pi Pico Bouton Debounce : IRQ vs Software Polling

Une pression, le code compte 3-4 ? C'est le contact bounce ! Compare 2 méthodes de comptage dans Serial : RAW polling (attrape bounce, compte faux) vs IRQ + software debounce (précis). Apprends physique du bounce, software debounce, Pin.irq() interrupt, IRQ_FALLING trigger, ISR handlers, sécurité des variables globales ISR.

0.0(0)
12
ESP32 Contrôle de LED avec bouton (Toggle)
#105Débutant

ESP32 Contrôle de LED avec bouton (Toggle)

Chaque fois que l'on appuie sur le bouton, la LED change d'état : si elle est allumée, elle s'éteint, si elle est éteinte, elle s'allume. Modèle classique de basculement. Avec le debounce, nous nettoyons le bruit mécanique du bouton-poussoir pour garantir un seul basculement avec une seule pression.

4.6(39)
57
Raspberry Pi Pico Bouton Toggle LED
#156Débutant

Raspberry Pi Pico Bouton Toggle LED

Bouton + LED — niveau supérieur ! Contrairement au projet bouton de base, pas besoin de maintenir le bouton. Appuie et relâche → LED allumée. Réappuie → LED éteinte (toggle). Même circuit, mais grande différence de code. Apprends edge detection, state tracking et debounce.

0.0(0)
9
Compteur avec afficheur 7 segments et boutons
#43Avancé
Premium

Compteur avec afficheur 7 segments et boutons

Un compteur tout simple à trois boutons : avec l'un, le chiffre affiché monte d'un cran, avec l'autre il descend d'un cran, et le troisième le remet à zéro. L'afficheur 7 segments à un seul chiffre co

4.6(35)
47
Arduino Test de Réaction avec Signal Sonore : Mesure tes Réflexes en Millisecondes (LCD + Buzzer + 3 LED)
#97Intermédiaire
Premium

Arduino Test de Réaction avec Signal Sonore : Mesure tes Réflexes en Millisecondes (LCD + Buzzer + 3 LED)

Tes réflexes sont-ils rapides ? Appuie sur le bouton : la LED rouge dit à vos marques, la jaune prêts, puis une attente aléatoire de 1,5 à 3,5 secondes commence. Dès que la LED verte s'allume et que le buzzer bipe, appuie à nouveau ; ton temps de réaction s'affiche en millisecondes sur l'écran LCD I2C, avec ton record et ta moyenne en dessous. Appuie avant le vert et l'écran affiche TOO EARLY! avec un son de défaite. Maintiens appuyé : nouveau joueur, même ordre d'attentes.

4.6(33)
50
Compteur ESP32 WebSocket (2 boutons + écran LCD + affichage Web en temps réel)
#130Avancé
Premium

Compteur ESP32 WebSocket (2 boutons + écran LCD + affichage Web en temps réel)

Dans ce projet, deux boutons reliés à l'ESP32 (GPIO 5 incrémente, GPIO 4 décrémente) modifient un compteur. La valeur se met à jour instantanément sur un écran LCD I2C 16x2 et, grâce au support WebSocket d'ESPAsyncWebServer, dans tous les navigateurs connectés sans recharger la page. Au lieu du polling HTTP, une seule connexion persistante est utilisée et l'ESP32 envoie la nouvelle valeur à tous avec ws.textAll().

4.5(14)
23
Jeu de mémoire avec Arduino (4 LED + 4 boutons + buzzer)
#93Avancé
Premium

Jeu de mémoire avec Arduino (4 LED + 4 boutons + buzzer)

Tu as confiance en ta mémoire ? Quatre LED de couleur s'allument dans un certain ordre, chacune avec sa propre note, et tu dois refaire la même suite en appuyant sur les boutons. Chaque manche réussie

4.8(46)
69