Como NO leer un botón y como SI debemos hacerlo.

Mucha gente hace cosas raras para leer un pulsador. En este tutorial os enseñaré como no leer un botón y como si hacerlo.

Bien empecemos...

Para este tutorial montaremos el siguiente circuito:

Uno de los errores más comunes que comenten los novatos a la hora de leer un botón con Arduino es usar simplemente la función digitalRead, pero ¿por qué? Imaginad este código:

/*
 * 1. Ejemplo de como no leer un botón.
 */
const int boton = 2; // Botón asignado en el pin 2.

void setup() {
  // Vamos a usar el puerto serie para mostrar el estado del botón.
  Serial.begin(9600);
  // Ponemos el pin como una entrada, puesto que vamos a leer
  // un botón. Habilito la resistencia de PULLUP.
  pinMode(boton,INPUT_PULLUP);
}

void loop() {
  // Cuando la entrada se ponga a 0, el botón "debería" estar
  // pulsado.
  if ( digitalRead(boton)==LOW ) {
    Serial.println("Botón pulsado");
  }
}

Cabría pensar que cuando apretamos el botón mostrará la cadena "Botón pulsado" y ya está, pero no, lo que ocurre es que mostrará muchas veces dicha cadena:

Botón pulsado
Botón pulsado
Botón pulsado
Botón pulsado
Botón pulsado
Botón pulsado

No importa lo rápido que apreteis y soltéis el botón, siempre se mostrará el texto varias veces.

Debéis recordar que el loop de Arduino es un bucle infinito que se ejecuta siempre y además muy rápido. En el programa solo lees el pin una vez, pero el programa se ejecuta muchas veces mientras tu pulsas el botón. Ah vale!, le meto un delay:

/*
 * 2. Ejemplo de como no leer un botón, con un delay
 */
const int boton = 2; // Botón asignado en el pin 2.
  
void setup() {
  Serial.begin(9600);
  // Ponemos el pin como una entrada, puesto que vamos a leer
  // un botón. Habilito la resistencia de PULLUP, 
  pinMode(boton,INPUT_PULLUP);
}
  
void loop() {
  // Cuando la entrada se ponga a 0, el botón "debería" estar
  // pulsado.
  if ( digitalRead(boton)==LOW ) {
    Serial.println("Botón pulsado");
    delay(1000);
  }
}

Ahora cuando pulsas el botón solo te lo muestra una vez. Pero y ¿si pulsas tres veces seguidas? Anda, sigue mostrándolo solo una vez.

En el foro veréis que desaconsejamos el uso de delay para todo. Delay es una cosa que bloquea el programa, mientras está en él no hace otra cosa. Por ejemplo, si pones dos botones, cuando pulses uno, hasta que no acabe el delay, no vas a poder leer ni ese ni el otro.

Solo bajando el tiempo del delay podrás obtener el efecto deseado, pero seguirá bloqueando el programa y perjudicará a otras tareas.

Ahora bien, me direis, ¿por qué en vez de mirar que el botón cuando esta apretado no miramos cuando el botón cambía?, es decir, mirar si en vez de estar HIGH o LOW, miramos cuando el botón cambia de HIGH a LOW que será cuando estamos pulsando el botón. Vamos a hacerlo:

/*
 * 3. Ejemplo de como no leer un botón, usando una variable
 * para mantener el estado anterior.
 */
const int boton = 2; // Botón asignado en el pin 2.
int   anterior;      // guardamos el estado anterior.
void setup() {
  Serial.begin(9600);
  // Ponemos el pin como una entrada, puesto que vamos a leer
  // un botón. Habilito la resistencia de PULLUP.
  // más adelante.
  pinMode(boton,INPUT_PULLUP);
  anterior = digitalRead(boton);
}
  
void loop() {
  // Leemos el estado del botón haciendo un digitalRead, este
  // valor lo tendremos que asignar a la variable "anterior" para
  // que recordemos el estado en el que estaba.
  int estado = digitalRead(boton);
  // Igual que anteriores códigos solo mostramos cuando el estado
  // anterior sea alto (botón sin apretar) y el actual sea bajo
  if ( anterior==HIGH && estado==LOW ) {
    Serial.println("Hemos pulsado");
  }
  // Debemos guardar el estado en la variable anterior para poder
  // usarlo en la siguiente pasada del loop.
  anterior=estado;
}

Este código es mucho mejor. Si pulsáis el botón solo se muestra el código una vez, pero... vaya... de vez en cuando muestra varios mensajes ¿qué ocurre? Ocurre una cosa que se llama debouncing o rebote.

efecto.png

En la figura anterior podeis ver el comportamiento. Cuando apretamos el pulsador durante un tiempo T1 los contactos oscilarán de manera que parecerá una secuencia de 0’s y 1’s. Igualemente cuando lo soltamos ocurre lo mismo (tiempo T2). Los tiempos T1 y T2 son totalemente aleatorios y los valores de 0’s y 1’s también lo serán.

Dependiendo de lo bueno del pulsador, de los materiales con los que esté hecho, de los gastado que esté, etc, estos rebotes serán más o menos. Este efecto es inevitable, pero es previsible.

Hay dos formas de solucionar este problema: hardware y software.

Si usamos hardware adicional podremos recudir el rebote mucho, pero a costa de añadir más componentes a nuestro circuito. Hay circuitos integrados especificos para evitar el rebote, pero nos quedaremos con el circuito más simple: un condensador.

Solo hemos añadido un pequeño condesandor de “lenteja” de 100 nF. Con esto consegimos que el condensador absorba el rebote mientras se carga/descarga. Si ahora probamos el código del ejemplo 3, veremos que ahora podemos pulsar tranquilamente que el texto solo se mostrará una vez… aunque no es del todo perfecto.

La otra solución es usar millis y un temporizador. Cuando detectemos que la salida cambia debemos iniciar un temporizador, y solo aceptaremos la entrada como válida cuando transcurrido un tiempo la señal no haya cambiado:

/*
 * 4. Leyendo un boton con antirebote por software.
 */
const int boton = 2; // Botón asignado en el pin 2.
int   anterior;      // guardamos el estado anterior.
int   estado;        // el estado del botón.
unsigned long temporizador;
unsigned long tiemporebote = 50;

void setup() {
  Serial.begin(9600);
  pinMode(boton,INPUT_PULLUP);
  estado = HIGH;
  anterior = HIGH;
}
  
void loop() {
  // Si el estado es igual a lo leido, la entrada no ha cambiado lo que
  // significa que no hemos apretado el botón (ni lo hemos soltado); asi que
  // tenemos que parar el temporizador.
  if ( estado==digitalRead(2) ) {
    temporizador = 0;
  }
  // Si el valor distinto significa que hemos pulsado/soltado el botón. Ahora
  // tendremos que comprobar el estado del temporizador, si vale 0, significa que
  // no hemos guardado el tiempo en el que sa ha producido el cambio, así que 
  // hemos de guardarlo.
  else 
  if ( temporizador == 0 ) {
    // El temporizador no está iniciado, así que hay que guardar
    // el valor de millis en él.
    temporizador = millis();
  }
  else 
  // El temporizador está iniciado, hemos de comprobar si el
  // el tiempo que deseamos de rebote ha pasado.
  if ( millis()-temporizador > tiemporebote ) {
    // Si el tiempo ha pasado significa que el estado es lo contrario
    // de lo que había, asi pues, lo cambiamos.
    estado = !estado;
  }

  // Ya hemos leido el botón, podemos trabajar con él.
  if ( anterior==HIGH && estado==LOW ) Serial.print("Botón pulsado");

  // Recuerda que hay que guardar el estado anterior.
  anterior = estado;
}

Ahora cada vez que pulsamos el interruptor, solo se muestra el mensaje una sola vez.

debounce_sw.png

efecto.png

Ya hemos leido un botón y hemos eliminado el debounce por software, ahorrandonos un condensador. Pero vamos a complicar la cosa.

Un botón puede estar en cuatro estados:

  • Suelto que es cuando no lo tocas y solo lo miras. En este estado tendrá un valor, en nuestro caso estará a HIGH tal y como lo enchufamos. Generalemente es el estado que menos nos interesa ya que no haremos nada en él.
  • Apretándolo. Justo ese momento en el que el botón pasa de estar suelto a apretado, en nuestro ejemplo pasa del estado HIGH a LOW. Este si es útil, como hemos visto en el apartado anterior.
  • Apretado. Aquí ya lo mantenemos pulsado y su valor será LOW. En este estado podemos hacer cosas, pero ya sabes, el loop es rápido y se ejecutará muchas veces cuando lo mantengas pulsado.
  • Soltándolo. Justo ese momento en el botón pasa de estar apretado a estar suelto. En nuestro caso es cuando se pasa de LOW a HIGH. También es útil para hacer cosas, dependiendo de lo que queramos hacer. Cuando sale de este estado, se queda de nuevo en suelto

Así que sabiendo esto se lo podemos añadir a nuestro código para determinar en que estado está el botón:

/*
 * 5. Leyendo un boton con antirebote por software y determinando su estado.
 * 
 * 
 * NOTA: en el ejemplo "anterior" estado guardaba el valor de la variable, como
 * ahora vamos a tener un estado de verdad cambiamos ese nombre de variable a
 * valor para hora usar un estado como estado propiamente dicho.
 */

#define APRETADO    0
#define SUELTO      1
#define APRETANDOLO 2
#define SOLTANDOLO  3
 
const int boton = 2; // Botón asignado en el pin 2.
int   anterior;      // guardamos el estado anterior.
int   valor;         // valor actual del botón.
int   estado;
unsigned long temporizador;
unsigned long tiemporebote = 50;

void setup() {
  Serial.begin(9600);
  pinMode(boton,INPUT_PULLUP);
  pinMode(13,OUTPUT); // Vamos a usar el led de la placa como señalización.
  valor    = HIGH;
  anterior = HIGH;
}
  
void loop() {
  // Si el estado es igual a lo leido, la entrada no ha cambiado lo que
  // significa que no hemos apretado el botón (ni lo hemos soltado); asi que
  // tenemos que parar el temporizador.
  if ( valor==digitalRead(2) ) {
    temporizador = 0;
  }
  // Si el valor distinto significa que hemos pulsado/soltado el botón. Ahora
  // tendremos que comprobar el estado del temporizador, si vale 0, significa que
  // no hemos guardado el tiempo en el que sa ha producido el cambio, así que 
  // hemos de guardarlo.
  else 
  if ( temporizador == 0 ) {
    // El temporizador no está iniciado, así que hay que guardar
    // el valor de millis en él.
    temporizador = millis();
  }
  else 
  // El temporizador está iniciado, hemos de comprobar si el
  // el tiempo que deseamos de rebote ha pasado.
  if ( millis()-temporizador > tiemporebote ) {
    // Si el tiempo ha pasado significa que el estado es lo contrario
    // de lo que había, asi pues, lo cambiamos.
    valor = !valor;
  }

  // Ahora comprobamos el estado. Recordad que si el boton vale "1" estará suelto,
  // "0" y el botón estará apretado. Si pasa de "1" a "0" es que lo estamos aprentando
  // y si es al contrario es que lo estamos soltando.
  if ( anterior==LOW  && valor==LOW  ) estado = APRETADO;
  if ( anterior==LOW  && valor==HIGH ) estado = SOLTANDOLO;
  if ( anterior==HIGH && valor==LOW  ) estado = APRETANDOLO;
  if ( anterior==HIGH && valor==HIGH ) estado = SUELTO;

  // Recuerda que hay que guardar el estado anterior.
  anterior = valor;

  // Ahora vamos a ver que podemos hacer con el estado.
  switch ( estado ) {
    case SUELTO:   digitalWrite(13,LOW);  break; // Apagamos el led.
    case APRETANDOLO: Serial.write("Has apretado el botón"); break; // Mandamos un mensaje.
    case APRETADO: digitalWrite(13,HIGH); break; // Encendemos el led.
    case SOLTANDOLO: Serial.write("Has soltado el botón"); break; // Mandamos un mensaje.
    default: break;
  }
}

Podéis jugar un poco ahora con el arduino. Si mirais el arduino, no está haciendo nada que veamos; pero cuando pulsamos el botón: plas! se enciende el led y envia un mensaje. Si lo mantenéis pulsado el led sigue encendido, pero no hace nada mas. Cuando lo sueltas, el led se apaga! y envia un mensaje.

Vamos a jugar un poco más pero, uff… que perreria… escribir tanto… venga hagamos una pequeña libreria para leer botones y ahorrarnos código.

Empecé a crear esta parte del tutorial dando una noción de objetos y clases de C++, pero al final llegué a la conclusión de que es tema para otro tutorial que no sé si llegaré a hacer.

Me limitaré a decir que una clase es una manera de encapsular código para cosas que van a realizar la misma función.

Para leer el botón un botón hemos visto que necesitamos unas variables y que vamos a usar esas variables para leer el estado del botón. Podemos poner otro botón y creamos más variables y realizamos la acción de leerla, otro botón y mas variables/acción de leer. Si ponemos más botones habrá que hacer lo mismo, con lo cual estamos agrandando el código, haciendo que sea ilegible y provocando fallos, ya que nos dedicaremos a hacer copy+paste y no cambiaremos el nombre de las variables bien.

Así que se encápsula en una clase y queda mas escueto y elegante.

/*
 * 6. Usando una clase para botones.
 */

#define APRETADO    0
#define SUELTO      1
#define APRETANDOLO 2
#define SOLTANDOLO  3

class BotonSimple {
  private:
    unsigned char pin;
    unsigned char anterior, valor;
    unsigned char estado;

    unsigned long temporizador;
    unsigned long tiempoRebote;
  public:
    BotonSimple(unsigned char _pin, unsigned long _tiempoRebote);
    void actualizar();
    int  leer();
};

BotonSimple::BotonSimple(unsigned char _pin, unsigned long _tiempoRebote=50) {
  pin = _pin;
  tiempoRebote = _tiempoRebote;
  pinMode(pin, INPUT_PULLUP);
  valor=HIGH; anterior=HIGH; estado=SUELTO;
}

void BotonSimple::actualizar() {
  // NOTA: En el ejemplo original en vez de "pin" leia directamente el pin "2", con lo que el
  // codigo no funciona correctamente con el pin que le hayamos asignado.
  if ( valor==digitalRead(pin) ){
    temporizador=0;
  }
  else
  if ( temporizador==0 ) {
    temporizador = millis();
  }
  else
  if ( millis()-temporizador >= tiempoRebote ) {
    valor = !valor;
  }
  if ( anterior==LOW  && valor==LOW  ) estado = APRETADO;
  if ( anterior==LOW  && valor==HIGH ) estado = SOLTANDOLO;
  if ( anterior==HIGH && valor==LOW  ) estado = APRETANDOLO;
  if ( anterior==HIGH && valor==HIGH ) estado = SUELTO;
  anterior = valor;
}

int BotonSimple::leer() { 
  return estado;
}
 

BotonSimple boton(2);

void setup() {
  Serial.begin(9600);
  pinMode(13, OUTPUT);
}
  
void loop() {
  boton.actualizar();
  switch ( boton.leer() ) {
    case SUELTO: digitalWrite(13, LOW); break;
    case APRETANDOLO: Serial.println("Acabas de apretar el boton"); break;
    case APRETADO: digitalWrite(13, HIGH); break;
    case SOLTANDOLO: Serial.println("Acabas de soltar el boton"); break;
    default: break;
  }
}

Este código hace lo mismo que lo anterior, pero hemos usado una clase para encapsular el botón.

Nuestro objeto ahora tiene dos métodos: actualizar, que hace la lectura del botón y que deberemos hacer antes de poder leer su estado; y leer, que al llamarla nos vevolverá el estado del botón (suelto, apretado, apretandolo, soltandolo)

Ahora que lo tenemos encapsulado, si queremos más botones solo tenemos que hacer:

BotonSimple boton1(pin1, tiempoRebote1);
BotonSimple boton2(pin2, tiempoRebote2);
...
BotonSimple botonN(pinN, tiempoReboteN);

pinX será el pin al que asignaremos el botón. tiempoReboteX sera el tiempo de debounce, este parámetro es opcional y si no lo ponemos el tiempo será de 50ms.

Tendremos que comprobar el estado de cada boton en cada ciclo:

void loop() {
  boton1.actualizar();
  boton2.actualizar();
  ...
  botonN.actualizar();
}

Como véis no me he esmerado mucho en la explicación, dado que como dije es un tema más de C++ y programación orientada a objetos que de Arduino en si. Solo básta con saber que el código es el mismo que cuando leiamos el botón sin usar objetos, pero ahora es más cómodo trabajar con él.

Os dejo en attachment la libreria para instalar en Arduino, será útil cuando cree el post para ejemplos de millis y botones.

NOTA: He visto un fallo en la libreria, que por hacer copy+paste era lo esperado. Cuando leía el pin, no leía la variable “pin”, si no que directamente seguía leyendo el pin “2” de todos los ejercicios. He adjuntado la libreria corregida.

NOTA: El usuario @Toberius reporta que la libreria le da error de compilación por qué
no respeté en el include el uso de mayúsculas/minúsculas y a los usuarios que utilizen linux no les
funciona, he cambiado el fichero y ahora debería funcionar también.

BotonSimple.zip (1.37 KB)

Antes de continuar con el post de los ejemplos, os daré una recomendación.

Esto es un tutorial para entender como funciona el botón y que os puede pasar a la hora leerlo.

Aunque la libreria es funcional es muy limitada, yo he optado por lo más simple y rápido de explicar. Pero debeis saber que existen muchas librerias para leer botones, con mucha funcionalidad, ni mejores ni peores, solo que se usan de manera diferente. Por ejemplo hay librerias que permiten conectar el botón de varias maneras, no solo usando la resistencia PULLUP de Arduino, hay otras que añaden funcionalidad como por ejemplo que hagan algo si lo mantienes apretado durante un tiempo, etc.

Sólo hay que buscar en google: Arduino button library. Y elegir. Se que para muchos el ingles es lo que lo mata, y se hecha para atrás. Pero es lo que hay, o sabes inglés, o esperas a que alguién haga un tutorial de la libreria en español. Por eso he optado por usar español a la hora crear la libreria.

Creo que te mande la replica a otro comentario jejeje te resumo intente hacer los ejercicios que me pusiste en mi post pero no me salen incluso copiando el codigo no se si se devera a que tengo un arduino nano y esto es lo que puede hacer con el poco conocimiento que tengo al respecto los millis casi no les entiendo y pues igual los botones no me salieron no se si sea por mi arduino te dejo este codigo para que lo cheques se que esta mal segun yo quiero que al precionar el boton encienda el led por 5 segundos papadee y se pague y espero otro tiempo y siga asi hasta que yo presione denuevo el boton pero no hace eso solo inicia cuando presiono el boton hace el parpadeo y se apaga y ya no hace el loop y tampoco se paga cuadno preciono otra vez el boton

int PULSADOR = 12;
int estado = LOW;
int led = 13;

void setup() {
  
  pinMode(PULSADOR,INPUT);
  pinMode(led,OUTPUT);
  digitalWrite(led, LOW);
}

void loop() {

 while(digitalRead(PULSADOR)==LOW);{
  
 }
 digitalWrite(led, HIGH);
  delay(5000);
 digitalWrite(led, LOW);
  delay(500);
  digitalWrite(led, HIGH);
  delay(500);
 digitalWrite(led, LOW);
  delay(500);
  digitalWrite(led, HIGH);
  delay(500);
 digitalWrite(led, LOW);
  delay(500);
  
while(digitalRead(PULSADOR)==HIGH);{
}
}

Que lo hagas con un nano, un mega, un micro, un uno, un due, un esp, es lo mismo. No se utiliza ninguna función estraña, ni código nativo de ningún microcontrolador. Solo se usa millis y digitalRead. Y ambas instruccionas han de comportarse de igual manera independientemente de la placa que uses.

Los programas están testeados todos, así que doy fé de que funcionan, copiar+pegar y a jugar.

El tutorial me llevó varias horas hacerlo, pero leerlo y hacer los ejemplos te llevará menos de una hora. No veo que haya nada complicado de entender, claro que lo hice yo, y por lo tanto lo entendí todo.

Por ejemplo, en la primera parte del tutorial expongo por qué no utilizar digitalRead a pelo, y en tu código es lo primero que hay....

Te falta también entender el concepto de delay, tienes que leerte el tutorial Entender millis y no morir en el intento

Y, entiende, no tengo tiempo para todo, te dije que me quedaba con tu problema y que tengo la solución, pero la tengo que publicar, y quizás se un ejemplo "más avanzado", así que tendría que poner otros ejemplos más fáciles antes.

He encontrado un fallo en la libreria y lo he corregido, quizás fuera por eso que no te funcionaba bien.

No se si podrá ser de ayuda pero yo he probado tu código en un simulador de Arduino y me marca el error en que pones un punto y coma al final de los WHILE. le he quitado los dos ";" y me funciona perfectamente. Pulso el botón, se enciende 5 segundos y parpadea, quedando apagado a la espera de volver a pulsarlo, repitiendo el ciclo.

Muy buen tutorial para pensar y ver otras opciones que tambien existen (que aqui explicas muy bien) y para aprender. Felicitarte como siempre que haces uno. Gracias

Hola a todos,

Muy buen tutorial sobre Pulsadores (Botones), te felicito y gracias.

Estaba leyendo el post del usuario victorjam Como NO leer un botón y como SI debemos hacerlo. - Documentación - Arduino Forum y no me deja comentar un error de compilacion al igual que otro usuario, la libreria aportada BotonSimple.zip tiene un bug en el archivo .cpp. Donde dice #include botonSimple.h debe decir BotonSimple.h

Me sirvio la explicacion del uso de botones y su comportamiento con respecto a ruidos en el paso de la corriente. Muchas gracias victorjam.

Moderador:
Tema movido y unido al título original.

Ese supuesto te falla porque estás en Linux ¿cierto? En Windows no fallaría imagino que Victoriano programa en win

Efectivamente, uso windows. :slight_smile:

He corregido el "bug" y he actualizado la librería en el post correspondiente. Muchas gracias @Tiberius por tu aportación.

También corregí el fallo en GitHub

TonyDiana:
Ese supuesto te falla porque estás en Linux ¿cierto? En Windows no fallaría imagino que Victoriano programa en win

Cierto.
Estoy en linux, olvide mencionar que uso Gentoo.

He corregido el "bug" y he actualizado la librería en el post correspondiente. Muchas gracias @Tiberius por tu aportación.

También corregí el fallo en GitHub

El agradecimiento es mio por lo mucho que aprendere en este sitio. Hay muy buenos tutoriales interesante en este poco tiempo que llevo.
Buen año para todos.