domingo, 9 de agosto de 2020

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

Este tutorial es un extracto del tutorial que cree para el foro de Arduino, si quieres ver el tutorial completo, ver los comentario y/o los añadidos al tutorial visita Como NO leer un botón y como SI debemos hacerlo.

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 ccosa. 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 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.

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.

No hay comentarios:

Publicar un comentario