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.

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éristique | Scrutation (polling) | Interruption |
|---|---|---|
| Fonctionnement | loop() lit la broche en boucle | Le matériel détecte le changement et lance l'ISR |
| Temps de réaction | Plus loop() est lente, plus le retard est grand | Quelques microsecondes |
| Avec delay() dans le code | Des événements peuvent être perdus | Les événements sont quand même captés |
| Impulsions très rapides | Ratées | Comptées une par une |
| Complexité du code | Simple | Nécessite volatile et des lectures atomiques |
| Idéal pour | Boutons, capteurs lents | Encodeurs, capteurs de vitesse et de débit, réveil de la veille |
Quelles broches gèrent les interruptions ? Arduino, ESP32 et Pico
| Carte | Broches d'interruption externe | Remarque |
|---|---|---|
| Arduino Uno R3, Nano, Pro Mini | D2, D3 | D2 = INT0, D3 = INT1 |
| Arduino Uno R4 (Minima / WiFi) | D2, D3 | Broches indiquées dans la référence officielle Arduino |
| Arduino Mega 2560 | 2, 3, 18, 19, 20, 21 | 20 et 21 sont inutilisables pour les interruptions tant qu'elles servent à l'I2C |
| Arduino Leonardo, Micro | 0, 1, 2, 3, 7 | 0 et 1 sont aussi les broches du port série |
| Arduino Nano Every | Toutes les broches numériques | Processeur ATmega4809 |
| ESP32 | Tous les GPIO | GPIO 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 W | Toutes, de GP0 à GP28 | Broches 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.
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.
| Mode | Quand se déclenche-t-il ? | Usage typique |
|---|---|---|
| LOW | En boucle tant que la broche est à LOW | Réveil de la veille |
| CHANGE | À chaque changement (les deux fronts) | Encodeurs, mesure de durée d'impulsion, anti-rebond |
| RISING | Au passage de LOW à HIGH (front montant) | Début d'une impulsion de capteur, bouton avec pull-down |
| FALLING | Au passage de HIGH à LOW (front descendant) | Appui sur un bouton avec pull-up, capteurs à collecteur ouvert |
| HIGH | Tant que la broche est à HIGH | Seulement 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
- Relie une patte du bouton à D2 et l'autre à GND. Aucune résistance externe n'est nécessaire : on utilise la pull-up interne.
- Téléverse le code ci-dessous et ouvre le moniteur série à 9600 bauds.
- Appuie plusieurs fois sur le bouton et regarde le compteur augmenter. Malgré le delay(500) dans loop(), aucun appui n'est perdu.
- 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.
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.
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.
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.
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.
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 :
| Timer | Taille | Utilisé par | Broches PWM |
|---|---|---|---|
| Timer0 | 8 bits | millis(), delay(), micros() | 5, 6 |
| Timer1 | 16 bits | Bibliothèque Servo, TimerOne | 9, 10 |
| Timer2 | 8 bits | tone(), IRremote | 3, 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.
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 :
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ôme | Cause | Solution |
|---|---|---|
| L'interruption ne se déclenche jamais | Un numéro de broche a été passé à attachInterrupt(2, ...) ou la broche ne gère pas les interruptions | Utilise digitalPinToInterrupt(broche) ; sur l'Uno, branche-toi sur D2 ou D3 |
| Un appui fait monter le compteur de 2 à 5 | Rebonds des contacts | Ajoute 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 absurdes | Variable multi-octets lue de façon non atomique | Copie-la entre noInterrupts() et interrupts() |
| Le programme se fige ou millis() dérive | delay(), Serial ou une longue tâche dans l'ISR | Dans l'ISR, ne mets à jour qu'un drapeau ou un compteur |
| Des interruptions arrivent sans toucher le bouton | Broche en l'air (flottante), pas de pull-up | Utilise INPUT_PULLUP ou une résistance externe de 10 kΩ |
| L'ISR s'exécute une fois aussitôt après un nouvel attachInterrupt | Un 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 hasard | L'ISR n'est pas en IRAM ou fait un travail lourd | Ajoute 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.
Projets à réaliser après cet article
Voir tous les projets
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

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.

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.

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.

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

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.

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().

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
Autres articles
Application BlueGrays
Réalise ce montage pas à pas sur ton téléphone
Schéma, code et liste des composants sur un seul écran. Plus de 200 projets Arduino, ESP32 et Pico à réaliser.
Scanne avec ton téléphone
Pointe l’appareil photo vers le code : la boutique d’applications de ton téléphone s’ouvre.
ou ouvre la boutique

