Fundamentos · 15 min de leitura

Interrupções no Arduino: attachInterrupt, regras da ISR e debounce

Como usar interrupções no Arduino: attachInterrupt, pinos 2 e 3 do Uno, RISING/FALLING/CHANGE, volatile, debounce, exemplos com encoder e RPM, além de ESP32 e Pico.

Uma interrupção no Arduino faz o processador largar o que está fazendo no momento em que acontece em um pino a mudança que você espera (ou em que dispara um evento de timer) e rodar uma função especial escrita por você. Em resumo, em vez de perguntar o tempo todo “o botão foi apertado?” dentro do loop(), você diz ao Arduino “me avise quando for apertado”. Você faz isso com a função attachInterrupt(); no Arduino Uno, os pinos de interrupção são o D2 e o D3, enquanto no ESP32 e no Raspberry Pi Pico quase todo GPIO pode disparar uma interrupção.

As interrupções são essenciais para contar pulsos muito rápidos sem perder nenhum (encoders, sensores de velocidade, sensores de fluxo de água), pegar um evento curto no meio de uma tarefa longa e acordar uma placa do modo de economia. Neste guia você vai ver passo a passo como uma interrupção funciona no hardware, quais pinos usar em cada placa, a diferença entre os modos RISING, FALLING e CHANGE, as regras para escrever uma rotina de serviço de interrupção (ISR), como fazer o debounce de um botão e exemplos com botão, encoder e contador de RPM. No fim, você também vai conhecer o IRAM_ATTR no ESP32 e o uso de irq com MicroPython no Pico.

Pinos de interrupção D2 (INT0) e D3 (INT1) do Arduino Uno
O Arduino Uno tem só dois pinos de interrupção externa: D2 (INT0) e D3 (INT1).

O que é uma interrupção e como ela funciona?

Pense em uma campainha: sem ela, você teria de ir até a porta a cada minuto para ver se alguém chegou. Isso se chama polling (consulta contínua), e ler um pino com digitalRead() dentro do loop() é exatamente isso. A campainha é uma interrupção: ela toca enquanto você está ocupado com outra coisa, você para, atende a porta e volta ao que estava fazendo. Um microcontrolador funciona do mesmo jeito. Quando a mudança escolhida acontece no pino, o hardware levanta uma flag de interrupção; o processador termina a instrução atual, salva os registradores, salta pelo vetor de interrupção para a sua ISR (Interrupt Service Routine, rotina de serviço de interrupção) e, quando a ISR termina, continua no loop() exatamente de onde parou. No Uno, essa troca leva só alguns microssegundos.

CaracterísticaPollingInterrupção
Como funcionaO loop() lê o pino sem pararO hardware pega a mudança e roda a ISR
Tempo de respostaQuanto mais lento o loop(), maior o atrasoAlguns microssegundos
Com delay() no códigoEventos podem ser perdidosOs eventos continuam sendo pegos
Pulsos muito rápidosSão perdidosSão contados um a um
Complexidade do códigoSimplesExige volatile e leituras atômicas
Ideal paraBotões, sensores lentosEncoders, sensores de velocidade e de fluxo, acordar do modo de economia

Quais pinos suportam interrupções? Arduino, ESP32 e Pico

PlacaPinos de interrupção externaObservação
Arduino Uno R3, Nano, Pro MiniD2, D3D2 = INT0, D3 = INT1
Arduino Uno R4 (Minima / WiFi)D2, D3Os pinos listados na referência oficial do Arduino
Arduino Mega 25602, 3, 18, 19, 20, 2120 e 21 não podem ser usados para interrupções enquanto servem ao I2C
Arduino Leonardo, Micro0, 1, 2, 3, 70 e 1 também são os pinos da porta serial
Arduino Nano EveryTodos os pinos digitaisProcessador ATmega4809
ESP32Todos os GPIOsOs GPIO 34–39 são só de entrada e não têm pull-up interno
ESP8266 (NodeMCU)Todos, menos o GPIO 16 (D0)As ISRs precisam ser marcadas com IRAM_ATTR
Raspberry Pi Pico / Pico WTodos de GP0 a GP28Os pinos são de 3,3 V, nunca aplique 5 V

No código, não passe o número do pino diretamente; converta-o em número de interrupção com digitalPinToInterrupt(pin), porque o número da interrupção muda de placa para placa: no Uno, o D2 é a interrupção 0 e o D3 é a 1, enquanto no Leonardo a interrupção 0 é o D3 e a 1 é o D2. Se você precisa de mais de duas interrupções no Uno, as interrupções por mudança de pino (PCINT) funcionam em quase todos os pinos; mas elas só dizem que “algo mudou”, e todos os pinos da mesma porta dividem uma única ISR. As bibliotecas PinChangeInterrupt e EnableInterrupt facilitam o uso. Você encontra o mapa completo dos pinos da sua placa na ferramenta Pinagem e as funções dos pinos do Uno no artigo sobre a pinagem do Arduino Uno.

O erro mais comum é escrever attachInterrupt(2, function, FALLING). Aqui o 2 é lido como número de interrupção, não como número de pino; o Uno não tem interrupção 2, então nada acontece e você também não recebe nenhuma mensagem de erro. A forma correta é attachInterrupt(digitalPinToInterrupt(2), function, FALLING).

Usando o attachInterrupt e os modos de interrupção

A sintaxe é attachInterrupt(digitalPinToInterrupt(pin), ISR, mode). O segundo parâmetro é o nome da função que roda quando a interrupção dispara; essa função não recebe parâmetros e não retorna nada (void). O terceiro parâmetro define qual evento dispara a interrupção. Use detachInterrupt(digitalPinToInterrupt(pin)) para desligar uma interrupção, noInterrupts() para pausar todas as interrupções por um instante e interrupts() para ligá-las de novo.

ModoQuando dispara?Uso típico
LOWSem parar, enquanto o pino estiver em LOWAcordar do modo de economia
CHANGEA cada mudança (as duas bordas)Encoders, medição da duração de pulsos, debounce
RISINGAo passar de LOW para HIGH (borda de subida)Início de um pulso de sensor, botão com pull-down
FALLINGAo passar de HIGH para LOW (borda de descida)Toque em botão com pull-up, sensores de coletor aberto
HIGHEnquanto o pino estiver em HIGHSó no Due, no Zero e no MKR1000; ONHIGH no ESP32

Escolha o modo de acordo com o seu circuito. Se você ligou o botão com INPUT_PULLUP, o pino fica normalmente em HIGH e cai para LOW quando apertado, então FALLING pega o toque. Use o modo LOW com cuidado: enquanto o pino fica em LOW, a ISR roda de novo e de novo e o loop() mal anda. Por isso ele costuma ser usado só para acordar a placa do modo de economia; para acordar o Uno do modo mais profundo (power-down), a folha de dados recomenda uma interrupção por nível LOW ou uma interrupção por mudança de pino. No ESP32, as constantes LOW e HIGH não funcionam como modos de interrupção do jeito que você espera: LOW nem ativa a interrupção e HIGH se comporta como borda de subida. Para interrupções por nível ali, você escreve ONLOW e ONHIGH.

Primeiro exemplo: contando toques de botão com uma interrupção

  1. Ligue uma perna do botão ao D2 e a outra ao GND. Não é preciso resistor externo; vamos usar o pull-up interno.
  2. Carregue o código abaixo e abra o Monitor Serial em 9600 baud.
  3. Aperte o botão algumas vezes e veja o contador subir. Apesar do delay(500) dentro do loop(), nenhum toque é perdido.
  4. Não se surpreenda se às vezes o número pular 2 ou 3 de uma vez: isso é o repique dos contatos, e vamos resolver já já.
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}

Há três pontos importantes neste código. Primeiro, onPress() só soma um ao contador e termina na hora; o trabalho de uma ISR é anotar o evento, não fazer o serviço. Segundo, o contador é declarado volatile, para que o compilador não o guarde em um registrador e leia um valor velho, e sim busque o valor atualizado na memória toda vez. Terceiro, o loop() desliga as interrupções por um instante enquanto lê o contador. Um unsigned int tem 2 bytes no Uno, e o processador de 8 bits o lê em duas etapas. Quando o contador passa de 255 para 256, ou seja, de 0x00FF para 0x0100, o loop() lê primeiro o byte baixo (0xFF); se a interrupção disparar bem nessa hora e incrementar o contador, ele lê depois o byte alto (0x01) e o resultado é 0x01FF, ou seja, 511. Como esse bug aparece muito raramente, também é difícil de rastrear. Nas placas AVR, a macro ATOMIC_BLOCK(ATOMIC_RESTORESTATE) de util/atomic.h dá a mesma proteção; variáveis de um byte, como bool e byte, não têm esse problema.

Regras da ISR: curta, com volatile e sem delay

  • Mantenha curta e rápida: enquanto uma ISR roda, as outras interrupções esperam. No Uno, o contador do millis() e a porta serial também dependem de interrupções, então uma ISR longa bagunça a temporização e pode fazer você perder dados seriais que chegam. Uma ISR ideal termina em alguns microssegundos.
  • Não use delay(): o delay() mede o tempo com contadores atualizados pela interrupção do Timer0; dentro de uma ISR as interrupções estão desligadas, então ele não funciona. Você pode ler o millis(), mas ele não avança durante a ISR, e o micros() passa a retornar valores errados depois de 1–2 ms. Se você precisa mesmo de uma espera bem curta, o delayMicroseconds() funciona.
  • Não use Serial.print(): enviar dados seriais também depende de interrupções; isso alonga a ISR e trava se o buffer encher. Deixe a impressão para o loop().
  • Deixe as variáveis compartilhadas como volatile: toda variável usada na ISR e no loop() precisa ser declarada volatile.
  • Leia variáveis de vários bytes de forma atômica: no loop(), copie variáveis int, long, float e unsigned long entre noInterrupts() e interrupts().
  • Sem parâmetros, sem valor de retorno: uma ISR precisa ser void e não recebe parâmetros; os dados são trocados por variáveis globais.
  • Deixe o trabalho pesado fora da ISR: escrever em um display I2C, gravar em um cartão SD ou chamar bibliotecas que também precisam de interrupções, como a Wire, trava dentro de uma ISR.
O padrão mais robusto é “levantar uma flag e fazer o trabalho no loop”: a ISR só põe um bool volatile em true ou incrementa um contador, e o loop() faz o trabalho de verdade, como atualizar um display, acionar um motor ou fazer contas, quando vê essa flag. Se você escrever o loop() com millis() em vez de delay, o sistema inteiro fica rápido e consistente.

Lendo um botão com interrupção e fazendo o debounce

Quando você aperta um botão mecânico, os contatos metálicos não se assentam na hora; eles abrem e fecham várias vezes durante cerca de 0,1 a 10 milissegundos. O loop() pode ser lento demais para notar, mas uma interrupção pega cada borda, então um único toque pode gerar 3–5 interrupções. E mais: o repique acontece não só quando você aperta, mas também quando solta, então um código que usa só FALLING com um bloqueio de tempo pode contar em dobro quando você segura o botão por um tempo e solta. O código abaixo usa o modo CHANGE e só aceita um toque quando o pino está em LOW e a linha ficou quieta por pelo menos 50 ms antes. Isso filtra o repique de apertar e o de soltar.

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}

Além do software, você também pode suprimir o repique no hardware: ponha um capacitor de 100 nF sobre o botão e um resistor de pull-up de 10 kΩ no pino, e a constante de tempo vira τ = R × C = 10 kΩ × 100 nF = 1 ms, o que suaviza os repiques curtos; o Schmitt trigger das entradas do Uno também limpa a borda. Você pode calcular outros valores com a ferramenta de constante de tempo RC. Sendo honesto, para botões apertados à mão muitas vezes você nem precisa de interrupção; se o loop() for rápido, o polling basta. Para pull-up, pull-down e debounce com millis(), veja Como ligar um botão no Arduino. Guarde as interrupções para sinais rápidos e curtos.

Lendo um encoder rotativo com interrupção

Enquanto você gira um encoder rotativo (como o KY-040), ele produz ondas quadradas em dois pinos, chamados CLK e DT, deslocadas uma em relação à outra. Quando você gira rápido, os pulsos ficam mais curtos que um milissegundo e o polling perde passos com facilidade. O método mais simples é ligar o CLK a um pino de interrupção e conferir o nível do DT no momento em que o CLK cai: se o DT estiver em HIGH, girou para um lado; se estiver em LOW, para o outro. Esta versão conta um passo por clique; se o sentido sair invertido, troque as linhas ++ e --. Encoders baratos também repicam, então acrescentar um capacitor de 10–100 nF entre CLK e GND e entre DT e GND faz uma diferença clara. Para uma versão que põe os dois pinos em interrupções e usa uma tabela de estados para nunca perder um passo, dê uma olhada no guia do encoder rotativo (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}

Contando rotações: medindo o RPM de um motor

Medir a velocidade de um motor ou de uma roda é o uso clássico das interrupções. Como sensor, você usa ou um sensor óptico de velocidade com fenda, com LM393 e um disco encoder de 20 furos, ou um sensor Hall como o A3144 com um ímã colado no eixo. Cada furo ou cada passagem do ímã gera um pulso. A partir do número de pulsos contados em um segundo, a velocidade é: RPM = pulsos por segundo × 60 / pulsos por volta. Por exemplo, se um disco de 20 furos dá 100 pulsos por segundo, o motor gira a 100 × 60 / 20 = 300 RPM.

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}

Aqui o loop() copia e zera o contador a cada segundo; as duas coisas acontecem dentro do mesmo bloco noInterrupts() para que nenhum pulso se perca se uma interrupção chegar entre elas. Com um disco de 20 furos, a resolução é de 3 RPM. Se quiser resultados mais precisos em velocidades baixas, em vez de contar pulsos, meça na ISR o tempo entre dois pulsos com micros() e use RPM = 60.000.000 / (período_µs × pulsos por volta). O mesmo método funciona para o sensor de fluxo de água YF-S201, para medir velocidade com um sensor Hall e para a velocidade da roda de um motor CC com redução.

Uma nota rápida sobre interrupções de timer

As interrupções não vêm só dos pinos. Os timers dentro do microcontrolador também podem gerar interrupções em intervalos fixos, para que uma função rode exatamente a cada 1 ms ou a cada 500 ms, não importa o que o loop() esteja fazendo. O Uno tem três timers, e alguns deles já são usados pelas próprias funções do Arduino:

TimerTamanhoUsado porPinos PWM
Timer08 bitsmillis(), delay(), micros() (funções de tempo)5, 6
Timer116 bitsBiblioteca Servo, TimerOne9, 10
Timer28 bitstone(), IRremote3, 11

No Uno, o caminho mais fácil é a biblioteca TimerOne: Timer1.initialize(500000) define o período em microssegundos e Timer1.attachInterrupt(function) anexa a ISR que roda a cada 500 ms. Como ela usa o Timer1, conflita com a biblioteca Servo e com o analogWrite() nos pinos 9 e 10. No ESP32 com o core Arduino 3.x, uma interrupção de timer é configurada com timerBegin(1000000), timerAttachInterrupt() e timerAlarm(); timerAlarmWrite() e timerAlarmEnable() do 2.x foram removidas, então exemplos antigos falham com o erro “was not declared in this scope”. No Pico, o Timer(period=500, mode=Timer.PERIODIC, callback=function) do MicroPython configura uma interrupção periódica em uma única linha. Para a relação entre timers e PWM, veja O que é PWM?

Interrupções no ESP32: IRAM_ATTR e portMUX

No ESP32, você não precisa procurar um pino especial; todo GPIO pode disparar uma interrupção. Só os GPIO 34–39 não têm pull-up interno, então um botão ali precisa de um pull-up externo de 10 kΩ; a errata da Espressif também observa que os GPIO 36 e 39 podem gerar interrupções falsas enquanto o ADC está em uso. A diferença real no ESP32 é o atributo IRAM_ATTR escrito na frente da ISR. Ele põe a função na RAM interna rápida (IRAM) em vez da flash. Durante a gravação na flash (Preferences, SPIFFS, OTA), o cache é desligado; dependendo da configuração, uma interrupção que mexa em código na flash nesse momento pode reiniciar a placa com “Guru Meditation Error: Core 1 panic'ed (Cache disabled but cached memory region accessed)”. O ESP8266 é ainda mais rígido: uma ISR sem IRAM_ATTR para a placa com a mensagem “ISR not in IRAM!” assim que o attachInterrupt é chamado.

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}

Como o ESP32 roda FreeRTOS e tem dois núcleos, as variáveis compartilhadas são protegidas com uma trava portMUX_TYPE em vez do noInterrupts() do Uno: use portENTER_CRITICAL_ISR() dentro da ISR e portENTER_CRITICAL() no loop(). Ler uma variável de 32 bits já é uma única etapa no ESP32, mas a trava é essencial para coisas como ler e zerar um valor ou atualizar várias variáveis juntas. Se você quer anexar a mesma função a vários pinos, o attachInterruptArg() permite passar um parâmetro, como o número do pino, para a ISR. Para o básico do ESP32, veja O que é o ESP32?, e para tarefas e núcleos, veja FreeRTOS e dual-core no ESP32.

Interrupções no Raspberry Pi Pico: irq do MicroPython

No Raspberry Pi Pico, todo pino de GP0 a GP28 pode disparar uma interrupção. No MicroPython, você configura uma com o método irq() do objeto pino: em trigger você passa Pin.IRQ_FALLING, Pin.IRQ_RISING ou os dois combinados, e handler é a função que roda quando a interrupção dispara; ela recebe como parâmetro o pino que disparou. O Python não tem volatile; declarar como global as variáveis que você muda na ISR basta. Para medir o tempo, use time.ticks_ms() e o time.ticks_diff(), que é seguro contra estouro. O código abaixo leva para o Pico a lógica de tempo quieto do exemplo do 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)

No Pico, o irq roda por padrão como interrupção soft: o MicroPython põe a função em uma fila e a roda o quanto antes, então usar print ou criar uma lista dentro dela não dá problema, mas a latência sobe um pouco. Se você passar hard=True, a latência cai para microssegundos; em troca, não dá para alocar memória dentro da ISR, então uma lista nova, uma string e até uma conta com float podem gerar um “MemoryError”. Se você programa o Pico pelo Arduino IDE (core de Earle Philhower), o attachInterrupt() funciona igual em todos os GPIOs. Para a instalação, veja O que é o Raspberry Pi Pico?, e para os comandos básicos, veja fundamentos de MicroPython.

Ideias de projetos que usam interrupções

  • Medidor de reação: meça quanto tempo você leva para apertar o botão depois que um LED acende, usando micros() na ISR para ter precisão abaixo do milissegundo.
  • Velocímetro de bicicleta: ponha um ímã na roda e um sensor Hall no quadro; calcule a velocidade e a distância a partir do tempo por volta.
  • Medidor de água: conte os pulsos de um sensor de fluxo YF-S201 e mostre os litros consumidos.
  • Carrinho robô que anda reto: meça a velocidade dos dois motores com encoders nas rodas e iguale as duas.
  • Controle de menu: navegue por um menu na tela e mude configurações com um encoder rotativo.
  • Alarme a bateria: deixe a saída de um sensor PIR acordar a placa enquanto ela dorme; para o ESP32, veja o artigo sobre deep sleep.

Erros comuns e como corrigir

SintomaCausaSolução
A interrupção nunca disparaFoi passado um número de pino em attachInterrupt(2, ...), ou foi escolhido um pino sem suporte a interrupçãoUse digitalPinToInterrupt(pin); no Uno, ligue ao D2 ou ao D3
Um toque aumenta o contador em 2–5Repique dos contatosFaça debounce no software e/ou acrescente um capacitor de 100 nF
A variável nunca muda no loop()Esqueceu o volatileDeclare a variável compartilhada como volatile
Valores absurdamente grandes de vez em quandoUma variável de vários bytes não foi lida de forma atômicaCopie-a entre noInterrupts() e interrupts()
O programa trava ou o millis() se desviadelay(), Serial ou uma tarefa longa dentro da ISRNa ISR, só atualize uma flag ou um contador
Interrupções disparam sem tocar no botãoO pino está flutuando, sem pull-upUse INPUT_PULLUP ou um resistor externo de 10 kΩ
A ISR roda uma vez logo depois de ser anexada de novoUma borda chegou com a interrupção desligada e levantou a flag (AVR)No Uno, escreva EIFR = bit(INTF0); antes do attachInterrupt (INTF1 para o D3)
O ESP32 reinicia aleatoriamenteA ISR não está na IRAM ou faz trabalho pesadoAcrescente IRAM_ATTR e encurte a ISR

As interrupções são uma ferramenta poderosa que permite pegar todo evento mesmo com o loop() ocupado com outro trabalho; você só precisa lembrar de três regras: manter a ISR curta, deixar as variáveis compartilhadas como volatile e lê-las de forma atômica no loop(). No app BlueGrays você encontra contadores com botão, um teste de reação, projetos de botão com ESP32 e um projeto com Pico que compara o debounce por interrupção (IRQ) e por consulta no software, cada um com esquema do circuito e código passo a passo.

Perguntas frequentes

Quais pinos suportam interrupções no Arduino Uno?

O Arduino Uno R3, o Nano e o Pro Mini têm só dois pinos de interrupção externa: D2 (INT0) e D3 (INT1). No Mega 2560 você pode usar 2, 3, 18, 19, 20 e 21, e no Leonardo 0, 1, 2, 3 e 7. Nos outros pinos, você só pode usar interrupções por mudança de pino (PCINT), que apenas informam que um pino mudou.

Por que o delay() não funciona dentro de uma interrupção (ISR)?

O delay() mede o tempo com contadores atualizados pela interrupção do Timer0. Enquanto uma ISR roda, as outras interrupções esperam, então esses contadores não avançam e o delay() não consegue funcionar direito. Se algo precisa esperar, só levante uma flag na ISR e faça a espera no loop() com millis().

O que o volatile faz e quando devo usá-lo?

O volatile diz ao compilador que a variável pode mudar a qualquer momento fora do fluxo normal do programa, por exemplo em uma ISR, então o valor dela não é guardado em um registrador, e sim lido da memória toda vez. Toda variável usada em uma ISR e no loop() deve ser volatile. Mas o volatile não torna atômicas as leituras de vários bytes; para isso você usa o noInterrupts().

Posso usar mais de 2 interrupções em um Arduino Uno?

Sim. Graças às interrupções por mudança de pino (PCINT), quase todo pino do Uno pode disparar uma interrupção. Elas só informam uma mudança, e os pinos da mesma porta (D0–D7, D8–D13, A0–A5) dividem uma ISR. Com as bibliotecas PinChangeInterrupt ou EnableInterrupt, o uso fica muito parecido com o do attachInterrupt.

Devo usar interrupções ou polling?

Para botões apertados à mão e sensores que mudam devagar, o polling no loop() geralmente basta e é mais simples. Use interrupções para pulsos rápidos e curtos, como de encoders e sensores de velocidade e de fluxo, ou para acordar a placa do modo de economia. Se o loop() está cheio de delay() longos, trocá-los por millis() também resolve a maioria dos problemas.

O que acontece se eu deixar o IRAM_ATTR de fora no ESP32?

O código geralmente compila e parece funcionar, mas a ISR fica na flash, começa mais devagar e, dependendo da configuração, pode travar com “Cache disabled but cached memory region accessed” enquanto a flash está sendo gravada. No ESP8266, sem IRAM_ATTR a placa para na hora com “ISR not in IRAM!”. Então, nas placas ESP, ponha IRAM_ATTR na frente de toda ISR.

Ver todos os projetos
Ligar e desligar um LED com botão
#10Iniciante

Ligar e desligar um LED com botão

Pressione o botão uma vez para acender a luz e outra vez para apagar, como o interruptor do seu quarto. O Arduino detecta cada toque e inverte o estado do LED, aguardando um breve instante para que as

4.8(100)
167
Raspberry Pi Pico: debounce de botão com IRQ ou consulta por software
#159Intermediário
Premium

Raspberry Pi Pico: debounce de botão com IRQ ou consulta por software

Você pressiona o botão uma vez e o código conta 3-4? Isso é oscilação dos contatos — os contatos metálicos oscilam por microssegundos! Compare 2 métodos de contagem lado a lado no Monitor Serial: consulta sem filtro (detecta as oscilações e conta a mais) e IRQ + debounce por software (contagem correta). Aprenda a física da oscilação dos contatos, debounce por software (ticks_diff() > 50ms), registro de interrupção com Pin.irq(), disparo IRQ_FALLING, rotinas ISR, global e…

0.0(0)
12
Controle de LED com botão no ESP32 (alternância)
#105Iniciante

Controle de LED com botão no ESP32 (alternância)

Cada vez que o botão é pressionado, o estado do LED muda: se estiver aceso, apaga; se estiver apagado, acende. É o padrão clássico de alternância. Com debounce, eliminamos o ruído mecânico dos contatos do botão para garantir uma única mudança a cada pressionamento.

4.6(39)
57
Raspberry Pi Pico: ligar e desligar LED com botão
#156Iniciante

Raspberry Pi Pico: ligar e desligar LED com botão

Botão + LED — próximo nível! Ao contrário do projeto básico de botão, não é preciso manter o botão pressionado. Pressione e solte → LED aceso. De novo → LED apagado (alternância). O circuito é o mesmo (LED no GP14, botão no GP2), mas o código muda bastante. Aprenda detecção de borda (o instante da queda 1→0), rastreamento de estado e debounce (filtro de repique). led.toggle() alterna o estado em uma linha. Base para projetos interativos com botões (seletor de modo, IRQ, clique duplo).

0.0(0)
9
Contador com display de 7 segmentos e botões
#43Avançado
Premium

Contador com display de 7 segmentos e botões

Um contador simples com três botões: ao pressionar um deles, o algarismo no display aumenta em um; com outro, diminui em um; e o terceiro zera a contagem. O display de 7 segmentos de um dígito conta d

4.6(35)
47
Teste de reação com Arduino e aviso sonoro: meça seus reflexos em milissegundos (LCD + buzzer + 3 LEDs)
#97Intermediário
Premium

Teste de reação com Arduino e aviso sonoro: meça seus reflexos em milissegundos (LCD + buzzer + 3 LEDs)

Seus reflexos são rápidos? Aperte o botão: o LED vermelho diz preparar, o amarelo diz apontar e começa uma espera aleatória de 1,5 a 3,5 segundos. Assim que o LED verde acende e o buzzer apita, aperte de novo; seu tempo de reação aparece em milissegundos no LCD I2C, com seu recorde da sessão e sua média embaixo. Apertou antes do verde? A tela mostra TOO EARLY! e toca um som de derrota. Segure o botão para um novo jogador com a mesma ordem de esperas.

4.6(33)
50
Contador WebSocket com ESP32 (2 botões + LCD + web em tempo real)
#130Avançado
Premium

Contador WebSocket com ESP32 (2 botões + LCD + web em tempo real)

Neste projeto, dois botões conectados ao ESP32 (GPIO 5 aumenta, GPIO 4 diminui) alteram um contador. O valor é atualizado instantaneamente no LCD I2C 16x2 e, com o suporte a WebSocket do ESPAsyncWebServer, em todos os navegadores conectados, sem recarregar a página. Em vez de consultas periódicas por HTTP, usamos uma única conexão persistente, e o ESP32 envia o novo valor a todos com ws.textAll().

4.5(14)
23
Jogo da memória com Arduino (4 LEDs + 4 botões + buzzer)
#93Avançado
Premium

Jogo da memória com Arduino (4 LEDs + 4 botões + buzzer)

Você confia na sua memória? Quatro LEDs coloridos acendem em sequência, cada um tocando sua própria nota, e você repete a mesma ordem apertando os botões. A cada rodada correta, uma nova cor é adicion

4.8(46)
69