Aprender

Seu botão dispara duas vezes. Veja por que e como resolver.

Ou veja todas as aulas.

  • 465componentes na bancada
  • 43placas rodando agora
  • 1.00xtempo real, em todas as placas
Arduino Uno: contagem por interrupção, sem debouncelive0.000 s 0.00x
Segure o botão
Cada borda de descida no pino 2 soma um a um contador e inverte o LED da placa. Abra no editor, acompanhe o monitor serial e aperte o botão uma vez.

Um botão tátil é uma pequena cúpula de metal empurrada contra um contato, e ela não encosta e fica parada. Ela encosta, volta uma fração de milímetro, encosta de novo e repete isso algumas vezes antes de assentar, do mesmo jeito que uma moeda que cai não para no instante em que toca a mesa. Eletricamente, o contato abre e fecha várias vezes nos primeiros milissegundos do aperto, e de novo ao soltar, antes de ficar estável. Um sketch que só conta as bordas nesse pino conta errado, porque um único aperto pode ser registrado como dois, três ou mais.

O Mokxi simula isso em vez de esconder. Todo botão na bancada tem bounce por padrão, num perfil chamado typical: de três a oito transições, que assentam entre 1 e 5 milissegundos depois do início do aperto. Isso é bem parecido com um botão tátil de verdade, e quer dizer que o circuito acima não é uma encenação de um bug; é o próprio bug, acontecendo do mesmo jeito que acontece na sua mesa.

Baixar o plano de aula (PDF)

O que o simulador mostra

O programa rodando acima é o exemplo interrupt que já vem pronto no Uno. Ele liga uma rotina de interrupção ao pino 2 que dispara em cada borda de descida (cada momento em que o pino cai de 5 V para 0 V) e soma um a um contador, sem nenhuma lógica de debounce, de propósito. Abra o circuito no editor, abra o monitor serial e aperte o botão uma vez, de forma limpa, como você apertaria qualquer botão. A contagem impressa depois de um único aperto muitas vezes é dois ou três, não um, e o LED da placa, que alterna conforme o contador é par ou ímpar, às vezes termina no estado que você não esperava. Nada está quebrado. O botão teve bounce, a rotina viu cada repique como um aperto separado e contou todos eles fielmente, porque é exatamente isso que o código manda fazer.

A aula inteira cabe numa frase: foi o botão, e não o código, que gerou os apertos a mais, e a correção fica no código porque não dá para deixar o botão perfeitamente limpo.

Três jeitos de resolver

A primeira correção, e a mais barata, é uma janela de debounce por software: depois de qualquer mudança no pino, ignore as próximas mudanças por um tempo curto, longo o bastante para cobrir o tempo de assentamento e curto o bastante para que um segundo aperto de verdade nunca seja confundido com bounce. Algo entre 5 e 20 milissegundos cobre com folga um botão típico.

A segunda é uma correção de hardware: um pequeno capacitor em paralelo com o botão. Ele deixa a subida e a descida da tensão lentas o bastante para que os repiques nunca cheguem a uma transição lógica completa, então o pino realmente só vê uma borda limpa. Isso funciona na bancada porque um capacitor de verdade leva um tempo de verdade para carregar e descarregar, e funciona no Mokxi pelo mesmo motivo: aqui o capacitor integra ao longo do tempo com uma resposta RC exponencial real, em vez de mudar na hora, então uma versão em escala menor dessa correção funciona de fato no simulador, e não só na teoria.

A terceira é fazer o debounce dentro da própria rotina de interrupção, o que exige uma regra a mais: a rotina deve fazer o mínimo possível, porque ela roda com as outras interrupções seguradas. Anote o horário e saia; tome as decisões de verdade, incluindo a janela de debounce, lá no loop().

A versão com millis()

O programa toggle que já vem pronto no Uno é a correção por software completa, no mesmo pino 2 que a demonstração do bounce usa. Ele guarda a última leitura e o momento em que ela mudou pela última vez, e só reage a uma mudança depois que DEBOUNCE_MS, vinte e cinco milissegundos, tiverem passado desde a anterior. Aperte o botão uma vez com esse programa rodando e a lâmpada no pino 13 acende; aperte de novo e ela apaga, exatamente uma vez por aperto, que é justamente o objetivo.

Repare no que o código está comparando de fato. Ele não conta há quanto tempo o botão está pressionado; ele mede quanto tempo passou desde a última mudança aceita, em qualquer borda, ao apertar ou ao soltar. É essa diferença que deixa a janela simétrica: um repique logo depois do aperto é ignorado, e um repique logo depois de soltar também, com a mesma regra de vinte e cinco milissegundos cobrindo os dois, sem nenhum caso especial para saber qual borda é qual.

firmware/examples/toggle (a correção com millis(), ligada do mesmo jeito)
const int BUTTON_PIN = 2;
const int LAMP_PIN = 13;
const unsigned long DEBOUNCE_MS = 25;

int lastReading = HIGH;
unsigned long lastChange = 0;
bool lampOn = false;

void loop() {
  int reading = digitalRead(BUTTON_PIN);
  unsigned long now = millis();

  if (reading != lastReading && now - lastChange >= DEBOUNCE_MS) {
    lastChange = now;
    lastReading = reading;
    if (reading == LOW) {
      lampOn = !lampOn;
      digitalWrite(LAMP_PIN, lampOn ? HIGH : LOW);
    }
  }
}

A questão do resistor de pull-up

Tanto a demonstração do bounce quanto a correção ligam o botão direto no pino 2, sem resistor externo, porque o sketch ativa o pull-up interno do próprio pino com INPUT_PULLUP. Esse pull-up mantém o pino em 5 V até o botão ligá-lo ao terra, e é por isso que o pino lê LOW enquanto o botão está pressionado, e não HIGH. Esse detalhe confunde muita gente na primeira vez, porque é o contrário do que parece que um botão deveria fazer.

A alternativa é um pull-up externo de verdade, um resistor de 10 kilohms do pino até os 5 V, com o botão puxando o mesmo nó para o terra quando apertado. Eletricamente os dois são a mesma ideia; o interno só é mais fraco (cerca de 35 kilohms) e não precisa de componente extra. A página sobre o resistor de pull-up monta a versão externa do zero e vale a leitura se o interno parecer ter surgido do nada.

Por que o perfil padrão é do jeito que é

O typical, com três a oito transições que assentam em 1 a 5 milissegundos, é de propósito um botão tátil comum, daqueles que vêm em quase todo kit de protoboard, e não o pior caso escolhido para fazer o problema parecer pior do que é. Também existe um perfil worst, que chega a vinte transições ao longo de 10 a 20 milissegundos, mais parecido com um botão gasto ou uma chave de fim de curso de alavanca comprida que já foi muito usada. Vale testar a sua janela de debounce contra ele depois de resolver o caso típico: uma correção ajustada para um bounce de 5 milissegundos sem folga ainda pode falhar com um botão que repica por 15.

Perguntas

Quanto tempo deve durar o atraso de debounce?

Algo entre 5 e 20 milissegundos resolve um botão tátil típico, que também é a faixa em que o perfil de bounce padrão do Mokxi assenta. Curto demais e um pouco de bounce passa; longo demais e um duplo aperto rápido começa a parecer que não responde.

Preciso do capacitor se já faço o debounce por software?

Não. Os dois são alternativas que resolvem o mesmo problema em lugares diferentes, e usar os dois não é errado, só redundante. Escolha o que combina com o circuito: o software não custa nada a mais, e o hardware funciona mesmo que o código que lê o pino nunca mude.

Por que minha interrupção ainda dispara duas vezes depois que coloquei um teste de debounce?

Quase sempre é porque o próprio teste da janela de debounce está dentro da rotina de interrupção junto com um trabalho lento, ou porque o tempo está sendo comparado com o valor guardado errado. Deixe a rotina só lendo o relógio e marcando uma flag, e faça a comparação no loop().

O INPUT_PULLUP também precisa de um resistor externo?

Não. Ele ativa um resistor de pull-up embutido no próprio ATmega328P, então o botão só precisa de dois fios: um para o pino e um para o GND.

Qual valor de capacitor faz o debounce de um botão típico por hardware?

Algo perto de 100 nanofarads em paralelo com o botão, muitas vezes junto com um pequeno resistor em série para limitar a corrente de descarga no primeiro aperto, é um ponto de partida comum. O valor certo depende do botão, e vale conferir na bancada com o componente de verdade em vez de confiar num único número para todo botão.

O debounce só é problema em botões?

Não. Todo contato mecânico tem bounce, incluindo relés, chaves alavanca e chaves de fim de curso num motor ou numa porta. A mesma correção vale: uma janela de tempo depois da primeira mudança, ou um capacitor em paralelo com o contato, o que for melhor para o circuito que faz a leitura.

Monte isto de verdade

Abra o editor, mude um valor e veja o número mudar junto. Nada para instalar, e não precisa de conta.