Cómo decodificar un teclado lineal con Arduino y C++

Cómo decodificar un teclado lineal con Arduino y C++

(For english click here.)

Una tarea muy común en nuestros proyectos electrónicos es la lectura y decodificación de pequeños teclados. Estos teclados (o keypads en inglés) pueden estar físicamente implementados de varias formas: 

  • como interruptores,
  • como escalera analógica,
  • como botones touch.

En este artículo te voy a mostrar cómo escanear y decodificar teclados de presión simple y con presión larga y repeticiones.

Introducción

Los teclados también se pueden presentar en forma lineal o en forma matricial. Esta última ayuda a ahorrar muchos pines del microcontrolador cuando se tienen muchas teclas, pero su escaneo y decodificación también es más elaborada.

Cada tipo de  teclado se escanea y decodifica de manera diferente dependiendo su tecnología; en este artículo hablaré del primero de ellos, teclado lineal con interruptores, por que es la forma más fácil y extendida y no depende de características esotéricas (módulos touch) que no están presentes en todos los microcontroladores, sino que únicamente utiliza terminales comunes GPIO (pines de entrada y salida).

(Es posible implementar un teclado touch utilizando entradas analógicas y GPIOs del microcontrolador, pero eso es tema de otro artículo. Si quieres que escriba sobre ello, házmelo saber en los comentarios.)

(Los términos Interruptor y tecla son sinónimos y los estaré utilizando indistintamente a lo largo de este artículo.)

Muy importante también es saber que en un teclado lineal cada interruptor se corresponde con un pin de entrada; si tienes 5 teclas, entonces utilizarás 5 pines. No obstante, la secuencia de los pines no tienen porque ser consecutivos; por ejemplo, en un teclado de 4 interruptores tú podrías establecer que las teclas K1, K2, K3 y K4 tengan una correspondencia con los pines D9, D2, D14 y D12.

Para el ejemplo a desarrollar más adelante vamos a declarar un arreglo global de 4 entradas:

uint8_t keypad_keys[4]={ 8, 14, 15, 16 };

Como podrás notar, los números no están en estricta secuencia porque no es necesario. Por supuesto que tú puedes establecer tanto una secuencia diferente, asi como una cantidad mayor o menor de teclas.

Después, y antes de escanear y decodificar el teclado, necesitas configurar como terminales de entrada los pines asignados anteriormente, con la resistencia interna de pull-up activada:

   for( uint8_t i = 0; i < this->num_keys; ++i )
   {
      pinMode( keys[ i ], INPUT_PULLUP );
   }

Cuando una tecla no está siendo presionada “entrega” un nivel alto al microcontrolador (gracias a la resistencia de pull-up); cuando la tecla es presionada, el microcontrolador observa un nivel bajo debido a que la resistencia interna de pull-up ha sido conectada a 0V. (La mayoría de microcontroladores proveen una resistencia de pull-up interna y su valor es suficiente para implementar un teclado, así que no tendrás que agregar resistencias a tu lista de hardware.)

Máquina de estados

La decodificación del teclado pasa por varios estados, y vamos a necesitar una máquina de estados que procese cada uno de ellos. Pero antes de entrar en los detalles de tal máquina, es interesante saber porqué la necesitamos y no simplemente leer de manera directa el estado correspondiente a cada una de las teclas.

Rebotes

Es importante saber que los interruptores mecánicos tienen rebotes (en inglés, bounces), y estos rebotes harán que una presión del teclado aparezca múltiples veces. Por eso siempre será necesario deshacerse de ellos. 

Puedes eliminar los rebotes a nivel de hardware colocando un capacitor de 100n en paralelo con cada interruptor; sin embargo, esta solución implica más componentes, más costos y mayor espacio en el circuito impreso.

Anti-debouncing por software

Podemos lograr el mismo resultado con software: detectas la primera vez que una tecla se presionó y guardas su código; luego dejas pasar un tiempo; y finalmente vuelves a preguntar por el estado de la tecla. Si la tecla sigue presionada, entonces das por válida la tecla; en caso contrario, haces caso omiso y te preparas para repetir el ciclo. El tiempo que dejaste pasar “absorberá” los rebotes; y si el usuario suelta la tecla antes de que el tiempo expire, entonces la tecla no se reportará como presionada.

La vida no es tan simple; cuando sueltas la tecla también se generan rebotes que podrían interpretarse como una nueva presión. Para evitarlo debemos agregar otro estado (estado 3): en éste dejaremos pasar un tiempo desde que la tecla se soltó.

La descripción gráfica de la máquina de estados correspondiente al diagrama de tiempos anterior es la siguiente:

Algunas implementaciones reportan la tecla presionada cuando ha transcurrido el tiempo anti-rebote; mientras que otras cuando la tecla ha sido soltada. A mí me gusta usar la primer forma, pero, si así lo deseas, con modificaciones menores tú podrías implementar la segunda.

Una vez que establecimos la manera en que se lleva a cabo el escaneo y decodificación del teclado, finalmente llegamos a la máquina de estados, que nos queda así:

void Keypad::state_machine()
{
   static uint8_t state = 0;
   static uint16_t ticks = 0;

   switch( state )
   {
      case 0:
         this->key = read_array();

         if( this->key < this->num_keys )
         {
            ticks = ANTI_DEBOUNCE_TIME_LOW;
            this->ready = false;
            state = 1;

            //Serial.println( "s0->s1" ); 
         }
         break;

      case 1:
         --ticks;
         if( ticks == 0 )
         {
            if( digitalRead( this->keys[ this->key ] ) == LOW )
            {
               this->ready = true;
               state = 2;

               //Serial.println( "s1->s2" ); 
            }
            else // fue ruido:
            {
               state = 0;

               Serial.println( "s1->s0" ); 
            }
         }
         break;

      case 2:
         if( digitalRead( this->keys[ this->key ] ) == HIGH )
         {
            ticks = ANTI_DEBOUNCE_TIME_HIGH;
            state = 3;

            //Serial.println( "s2->s3" ); 
         }
         break;

      case 3:
         --ticks;
         if( ticks == 0 )
         {
            state = 0;

            //Serial.println( "s3->s0" ); 
         }
         break;

      default:
         state = 0;
         ticks = 0;
         this->ready = false;

         //Serial.println( "def->s0" ); 
         break;
   }
}

En el código anterior dejé unas ayudas de depuración (Serial.println() que deberás descomentar) por si quieres ver cómo evoluciona la máquina de estados cada vez que presionas una tecla.

Es muy importante que mandes llamar a la máquina de estados de forma periódica; de lo contrario el comportamiento será errático y quizás pierdas algunos eventos. En el ejemplo completo te muestro una manera de hacerlo.

Ya casi terminamos. Falta mencionar la función que el cliente del mismo utilizará cada vez que quiera saber si una tecla se presionó, o no:

uint8_t Keypad::read()
{
   uint8_t ret_val = eKeys::NO_KEY;

   if( this->ready )
   {
      this->ready = false;
      ret_val = this->key;
   }
   return ret_val;
}

Ejemplo completo

El código completo del módulo de escaneo y decodificación del teclado junto con una muestra de su utilización, lo puedes ver en el siguiente sketch:

/*Copyright (C) 
 * 
 * 2021 - fjrg76 at hotmail dot com (fjrg76.com)
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * 
 */

#include <stdint.h>
// para uint8_t, uint16_t, etc

#define TICK_PERIOD 5 // ms

#define NUM_KEYS 4
#define ANTI_DEBOUNCE_TIME_LOW  ( 100 / TICK_PERIOD )
#define ANTI_DEBOUNCE_TIME_HIGH ( 200 / TICK_PERIOD )
constexpr uint8_t keys_array[ NUM_KEYS ] = { 8, 14, 15, 16 };

class Keypad
{
public:
   enum eKeys{ KEY_A, KEY_B, KEY_C, KEY_D, NO_KEY };

private:
   uint8_t* keys{ nullptr };
   uint8_t num_keys{ 0 };
   uint8_t key{ 0 };
   bool ready{ false };

public:
   Keypad();

   // no se permiten las copias:
   Keypad( Keypad& ) = delete;
   Keypad& operator=( Keypad& ) = delete;

   void init( uint8_t* keys, uint8_t num_keys );
   void state_machine();
   uint8_t read();

private:
   uint8_t read_array();
};

Keypad::Keypad()
{
   // nada
}

void Keypad::init( uint8_t* keys, uint8_t num_keys )
{
   this->keys = keys;
   this->num_keys = num_keys;

   for( uint8_t i = 0; i < this->num_keys; ++i )
   {
      pinMode( keys[ i ], INPUT_PULLUP );
   }
}

uint8_t Keypad::read_array()
{
   uint8_t cont;
   for( cont = 0; cont < this->num_keys; ++cont )
   {
      if( digitalRead( this->keys[ cont ] ) == LOW )
      {
         break;
      }
   }
   return cont;
}

void Keypad::state_machine()
{
   static uint8_t state = 0;
   static uint16_t ticks = 0;

   switch( state )
   {
      case 0:
         this->key = read_array();

         if( this->key < this->num_keys )
         {
            ticks = ANTI_DEBOUNCE_TIME_LOW;
            this->ready = false;
            state = 1;

            //Serial.println( "s0->s1" ); 
         }
         break;

      case 1:
         --ticks;
         if( ticks == 0 )
         {
            if( digitalRead( this->keys[ this->key ] ) == LOW )
            {
               this->ready = true;
               state = 2;

               //Serial.println( "s1->s2" ); 
            }
            else // fue ruido:
            {
               state = 0;

               Serial.println( "s1->s0" ); 
            }
         }
         break;

      case 2:
         if( digitalRead( this->keys[ this->key ] ) == HIGH )
         {
            ticks = ANTI_DEBOUNCE_TIME_HIGH;
            state = 3;

            //Serial.println( "s2->s3" ); 
         }
         break;

      case 3:
         --ticks;
         if( ticks == 0 )
         {
            state = 0;

            //Serial.println( "s3->s0" ); 
         }
         break;

      default:
         state = 0;
         ticks = 0;
         this->ready = false;

         //Serial.println( "def->s0" ); 
         break;
   }
}

uint8_t Keypad::read()
{
   uint8_t ret_val = eKeys::NO_KEY;

   if( this->ready )
   {
      this->ready = false;
      ret_val = this->key;
   }
   return ret_val;
}


Keypad keypad;
// creamos al objeto teclado


void setup() 
{
   keypad.init( keys_array, NUM_KEYS );
   Serial.begin( 115200 );
}

void loop()
{
   static unsigned long prev = 0;
   auto now = millis();

   if ( now - prev >= ( TICK_PERIOD ) )
   {
      prev = now;
      keypad.state_machine();
   }


//-------------------------------------------------

   uint8_t pressed_key = keypad.read();
   if( pressed_key != Keypad::eKeys::NO_KEY )
   {
      Serial.println( pressed_key );

      switch( pressed_key )
      {
         case Keypad::eKeys::KEY_A:
            Serial.println( "Key A" );
            break;

         case Keypad::eKeys::KEY_B:
            Serial.println( "Key B" );
            break;

         case Keypad::eKeys::KEY_C:
            Serial.println( "Key C" );
            break;

         case Keypad::eKeys::KEY_D:
            Serial.println( "Key D" );
            break;

         default:
            Serial.println( "Not a key" );
            break;
      }
   }
}

Como habrás notado, he implementado este módulo de teclado completamente en C++. Claro que también podría hacerse en C, pero Arduino utiliza C++ como lenguaje de programación, así que para qué liarse con dos lenguajes.

Aquí permíteme darte un consejo: si el módulo que estás desarrollando utiliza variables de estado, entonces tu módulo es un candidato perfecto para ser codificado en C++ con clases, y no simplemente en C.

Variables de estado: Las variables de estado son el subconjunto más pequeño de variables de un sistema que pueden representar su estado dinámico completo en un determinado instante. [WIKIPEDIA]

¿Cuáles variables de estado usa el módulo que te acabo de presentar? El módulo utiliza dos tipos de variables de estado:

  • Variables de estado que se requieren a lo largo de toda la vida del objeto Keypad (es decir, todas las variables declaradas en el bloque private de la clase), y
  • Variables de estado que no son parte intrínseca de la clase Keypad, como por ejemplo las variables state y ticks en la función Keypad::state_machine(). Dado que el teclado no las necesita para nada fuera de la máquina de estados, entonces deben ser locales (y estáticas, para que guarden sus valores entre llamadas) a dicha función.

El código del cliente utiliza 4 funciones de la clase Keypad:

  • Keypad::Keypad() (es decir, el constructor) para crear un objeto de tipo Keypad, pero sin servir para nada más(en ese momento). El objeto es declarado con visibilidad global (¡es la única variable global del ejemplo, qué maravilloso!)
  • Keypad::init() para asociar un arreglo de teclas y su cantidad al objeto Keypad anteriormente creado. Esta función se manda llamar en la función setup() de Arduino.
  • Keypad::state_machine() es la función que escanea y decodifica al teclado. Es llamada en la función loop() de Arduino, y debe ser llamada de manera periódica en dicha función.
  • Keypad::read() es la función que el cliente del teclado utilizará para, efectivamente, leer el teclado. Una vez que una tecla fue debidamente reconocida, entonces se pasa a un switch para que ejecute el código correspondiente. Puede ser llamada dentro de la función loop() de Arduino, o de cualquier otra, siempre y cuando el teclado ya se haya inicializado y se esté decodificando de manera periódica.

Presiones largas y repeticiones

¿Eso fue todo?

No, también podemos agregar presiones largas (como en “Presione el botón por 2 segundos”) y repeticiones (como en “Deje presionado el botón para que los minutos se incrementen de forma automática” de los minuteros de los relojes).

Por supuesto que estas nuevas funcionalidades agregan complejidad al diseño, ¡pero vamos, qué sería de la vida si todo fuera fácil!

Para implementar ambas características tendremos que agregar nuevas variables de estado a la clase, nuevas constantes simbólicas y modificar el comportamiento de la máquina de estados, ya que ahora deberá decodificar cuatro estados por los cuales podría pasar una tecla presionada:

  1. Fue ruido y no se hace nada,
  2. Presión corta,
  3. Presión larga, y
  4. Presión contínua (repeticiones).

Primero veamos cómo nos queda la clase Keypad:

class Keypad
{
public:
   enum eKeys{ KEY_A, KEY_B, KEY_C, KEY_D, NO_KEY };
   enum eType { SHORT, LONG, REPS };

   typedef struct
   {
      uint8_t key;
      eType type;
   } Key;

private:
   uint8_t* keys{nullptr};
   uint8_t num_keys{0};
   bool ready{false};
   uint8_t key{0};
   uint8_t type{eType::SHORT};

public:
   Keypad();

   // no se permiten las copias:
   Keypad( Keypad& ) = delete;
   Keypad& operator=( Keypad& ) = delete;

   void init( uint8_t* keys, uint8_t num_keys );
   void state_machine();
   bool read( Keypad::Key* key );

private:
   uint8_t read_array();
};

En particular échale un vistazo a la dupla (key, type). Cada vez que el módulo reporte una tecla (key), también deberá reportar el tipo de presión (type: SHORT, LONG y REPS).

Nueva máquina de estados

Luego tenemos la máquina de estados en la que agregamos 2 estados más:

void Keypad::state_machine()
{
   static uint8_t state = 0;
   static uint16_t ticks = 0;

   switch( state )
   {
      case 0:
         this->key = read_array();
         if( this->key < this->num_keys )
         {
            ticks = ANTI_DEBOUNCE_TIME_LOW;
            this->ready = false;
            state = 1;

            //Serial.println( "s0->s1" ); 
         }
         break;

      case 1:
         --ticks;
         if( ticks == 0 )
         {
            if( digitalRead( this->keys[ this->key ] ) == LOW )
            {
               ticks = ANTI_DEBOUNCE_TIME_LONG;
               state = 2;

               //Serial.println( "s1->s2" ); 
            }
            else
            {
               ticks = ANTI_DEBOUNCE_TIME_HIGH;
               state = 4;

               //Serial.println( "s1->s4" ); 
            }
         }
         break;

      case 2:
         if( digitalRead( this->keys[ this->key ] ) == LOW )
         {
            --ticks;
            if( ticks == 0 )
            {
               this->type = eType::LONG;
               this->ready = true;
               ticks = ANTI_DEBOUNCE_TIME_BEFORE_REPS;
               state = 3;

               //Serial.println( "s2->s3" ); 
            }
         }
         else
         {
            this->type = eType::SHORT;
            this->ready = true;
            ticks = ANTI_DEBOUNCE_TIME_HIGH;
            state = 4;

            //Serial.println( "s2->s4" ); 
         }
         break;

      case 3:
         if( digitalRead( this->keys[ this->key ] ) == LOW )
         {
            --ticks;
            if( ticks == 0 )
            {
               this->type = eType::REPS;
               this->ready = true;
               ticks = ANTI_DEBOUNCE_TIME_LOW;
               state = 5;

               //Serial.println( "s3->s5" ); 
            }
         }
         else
         {
            ticks = ANTI_DEBOUNCE_TIME_HIGH;
            state = 4;

            //Serial.println( "s3->s4" ); 
         }
         break;

      case 4:
         --ticks;
         if( ticks == 0 )
         {
            state = 0;

            //Serial.println( "s4->s0" ); 
         }
         break;

      case 5:
         if( digitalRead( this->keys[ this->key ] ) == LOW )
         {
            --ticks;
            if( ticks == 0 )
            {
               this->type = eType::REPS;
               this->ready = true;
               ticks = ANTI_DEBOUNCE_TIME_LOW;

               //Serial.println( "s5->s5" ); 
            }
         }
         else
         {
            ticks = ANTI_DEBOUNCE_TIME_HIGH;
            state = 4;

            //Serial.println( "s5->s4" ); 
         }
         break;

      default:
         state = 0;
         ticks = 0;
         this->ready = false;

         //Serial.println( "def->s0" ); 
         break;
   }
}

Más complicada, ¿cierto? Si encuentras una forma más simple de hacer lo mismo, ¡házmelo saber en los comentarios!

Para entender mejor la secuencia de estados anterior quizás te sirva el siguiente diagrama de tiempos:

Lectura de parte del cliente

Y finalmente, tenemos la forma en cómo se hace el reporte al cliente. Recuerda que ahora el cliente tiene 3 opciones para una misma tecla: fue presión corta, fue presión larga y fue presión contínua.

bool Keypad::read( Keypad::Key* k )
{
   bool ret_val = false;

   if( this->ready )
   {
      this->ready = false;

      k->key = this->key;
      k->type = this->type;

      this->type = eType::SHORT;
      ret_val = true;
   }
   return ret_val;
}

Ejemplo completo

El código completo, incluyendo un ejemplo de cómo el usuario podría hacer uso de las nuevas funcionalidades, nos queda así:

/*Copyright (C) 
 * 
 * 2021 - fjrg76 at hotmail dot com (fjrg76.com)
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * 
 */

#include <stdint.h>
// para uint8_t, uint16_t, etc

#define TICK_PERIOD 5 // ms

#define NUM_KEYS 4
#define ANTI_DEBOUNCE_TIME_LOW  ( 100 / TICK_PERIOD )
#define ANTI_DEBOUNCE_TIME_HIGH ( 200 / TICK_PERIOD )
#define ANTI_DEBOUNCE_TIME_LONG ( ( 2000 - ANTI_DEBOUNCE_TIME_LOW ) / TICK_PERIOD )
#define ANTI_DEBOUNCE_TIME_BEFORE_REPS ( ANTI_DEBOUNCE_TIME_LOW * 10 )

constexpr uint8_t keys_array[ NUM_KEYS ] = { 8, 14, 15, 16 };


class Keypad
{
public:
   enum eKeys{ KEY_A, KEY_B, KEY_C, KEY_D, NO_KEY };
   enum eType { SHORT, LONG, REPS };

   typedef struct
   {
      uint8_t key;
      eType   type;
   } Key;

private:
   uint8_t* keys{nullptr};
   uint8_t  num_keys{0};
   bool     ready{false};
   uint8_t  key{0};
   uint8_t  type{eType::SHORT};

public:
   Keypad();

   // no se permiten las copias:
   Keypad( Keypad& ) = delete;
   Keypad& operator=( Keypad& ) = delete;

   void init( uint8_t* keys, uint8_t num_keys );
   void state_machine();
   bool read( Keypad::Key* key );

private:
   uint8_t read_array();
};

Keypad::Keypad()
{
   // nada (nothing)
}

void Keypad::init( uint8_t* keys, uint8_t num_keys )
{
   this->keys = keys;
   this->num_keys = num_keys;

   for( uint8_t i = 0; i < this->num_keys; ++i )
   {
      pinMode( keys[ i ], INPUT_PULLUP );
   }
}

uint8_t Keypad::read_array()
{
   uint8_t cont;
   for( cont = 0; cont < this->num_keys; ++cont )
   {
      if( digitalRead( this->keys[ cont ] ) == LOW )
      {
         break;
      }
   }
   return cont;
}

void Keypad::state_machine()
{
   static uint8_t state = 0;
   static uint16_t ticks = 0;

   switch( state )
   {
      case 0:
         this->key = read_array();
         if( this->key < this->num_keys )
         {
            ticks = ANTI_DEBOUNCE_TIME_LOW;
            this->ready = false;
            state = 1;

            //Serial.println( "s0->s1" ); 
         }
         break;

      case 1:
         --ticks;
         if( ticks == 0 )
         {
            if( digitalRead( this->keys[ this->key ] ) == LOW )
            {
               ticks = ANTI_DEBOUNCE_TIME_LONG;
               state = 2;

               //Serial.println( "s1->s2" ); 
            }
            else
            {
               ticks = ANTI_DEBOUNCE_TIME_HIGH;
               state = 4;

               //Serial.println( "s1->s4" ); 
            }
         }
         break;

      case 2:
         if( digitalRead( this->keys[ this->key ] ) == LOW )
         {
            --ticks;
            if( ticks == 0 )
            {
               this->type = eType::LONG;
               this->ready = true;
               ticks = ANTI_DEBOUNCE_TIME_BEFORE_REPS;
               state = 3;

               //Serial.println( "s2->s3" ); 
            }
         }
         else
         {
            this->type = eType::SHORT;
            this->ready = true;
            ticks = ANTI_DEBOUNCE_TIME_HIGH;
            state = 4;

            //Serial.println( "s2->s4" ); 
         }
         break;

      case 3:
         if( digitalRead( this->keys[ this->key ] ) == LOW )
         {
            --ticks;
            if( ticks == 0 )
            {
               this->type = eType::REPS;
               this->ready = true;
               ticks = ANTI_DEBOUNCE_TIME_LOW;
               state = 5;

               //Serial.println( "s3->s5" ); 
            }
         }
         else
         {
            ticks = ANTI_DEBOUNCE_TIME_HIGH;
            state = 4;

            //Serial.println( "s3->s4" ); 
         }
         break;

      case 4:
         --ticks;
         if( ticks == 0 )
         {
            state = 0;

            //Serial.println( "s4->s0" ); 
         }
         break;

      case 5:
         if( digitalRead( this->keys[ this->key ] ) == LOW )
         {
            --ticks;
            if( ticks == 0 )
            {
               this->type = eType::REPS;
               this->ready = true;
               ticks = ANTI_DEBOUNCE_TIME_LOW;

               //Serial.println( "s5->s5" ); 
            }
         }
         else
         {
            ticks = ANTI_DEBOUNCE_TIME_HIGH;
            state = 4;

            //Serial.println( "s5->s4" ); 
         }
         break;

      default:
         state = 0;
         ticks = 0;
         this->ready = false;

         //Serial.println( "def->s0" ); 
         break;
   }
}

bool Keypad::read( Keypad::Key* k )
{
   bool ret_val = false;

   if( this->ready )
   {
      this->ready = false;

      k->key = this->key;
      k->type = this->type;

      this->type = eType::SHORT;
      ret_val = true;
   }
   return ret_val;
}

Keypad keypad;
// creamos al objeto teclado


void setup() 
{
   keypad.init( keys_array, NUM_KEYS );
   Serial.begin( 115200 );
}

void loop()
{
   static unsigned long prev = 0;
   auto now = millis();

   if ( now - prev >= ( TICK_PERIOD ) )
   {
      prev = now;
      keypad.state_machine();
   }


//-------------------------------------------------------------------------------

   Keypad::Key k;
   if( keypad.read( &k ) == true )
   {
      switch ( k.type )
      {
         case Keypad::eType::SHORT:
            switch( k.key )
            {
               case Keypad::eKeys::KEY_A: Serial.println( "S:A" ); break;
               case Keypad::eKeys::KEY_B: Serial.println( "S:B" ); break;
               case Keypad::eKeys::KEY_C: Serial.println( "S:C" ); break;
               case Keypad::eKeys::KEY_D: Serial.println( "S:D" ); break;
            }
            break;

         case Keypad::eType::LONG:
            switch( k.key )
            {
               case Keypad::eKeys::KEY_A: Serial.println( "L:A" ); break;
               case Keypad::eKeys::KEY_B: Serial.println( "L:B" ); break;
               case Keypad::eKeys::KEY_C: Serial.println( "L:C" ); break;
               case Keypad::eKeys::KEY_D: Serial.println( "L:D" ); break;
            }
            break;

         case Keypad::eType::REPS:
            switch( k.key )
            {
               case Keypad::eKeys::KEY_A: Serial.println( "R:A" ); break;
               case Keypad::eKeys::KEY_B: Serial.println( "R:B" ); break;

               // también puedes usar números enteros:
               case 2: Serial.println( "R:C" ); break;
               case 3: Serial.println( "R:D" ); break;
            }
            break;
      }
   }
}

Una corrida del programa anterior, probando todas las combinaciones, se ve así:

En el ejemplo anterior el cliente discriminó primero sobre el tipo de presión (SHORT, LONG y REPS), y después sobre la tecla; sin embargo, también es posible hacerlo al revés: discriminar primero sobre la tecla y después sobre el tipo. La forma que utilices dependerá de tu aplicación.

¿Qué sigue?

En este artículo hemos visto dos maneras de codificar en Arduino un teclado lineal: de manera simple (una sola presión), y de manera completa (3 tipos de presión).

En los sistemas embebidos las máquinas de estados y las variables de estado son elementos muy importantes de la programación. En el proyecto de hoy vimos cómo escanear y decodificar al teclado usando ambos herramientas.

El siguiente paso consistiría en sacar el código del teclado a un módulo de C++ (Keypad.cpp y Keypad.h) para que puedas reutilizarlo en todos tus proyectos, sin necesidad de hacer copies-and-pastes (yo ya lo saqué a módulo, y te lo dejo a ti como ejercicio).

Espero que este artículo te haya servido y que pronto comiences a utilizar lo que vimos hoy en tus proyectos.


¿Quieres comenzar a escribir aplicaciones de tiempo real en Arduino UNO? ¡Descárgate mi framework KleOS!

Aquí te explico cómo descargarlo en Windows. Por supuesto que también funciona para Linux.

Aquí te muestro cómo realizar tu primer sketch con Arduino y KleOS.

Si el artículo te gustó, quizás quieras suscribirte al blog.


Fco. Javier Rodríguez
Escrito por:

Fco. Javier Rodríguez

Soy Ingeniero Electrónico con 20+ años de experiencia en el diseño y desarrollo de productos electrónicos de consumo y a medida, y 12+ años como profesor. Egresado de la UNAM, también tengo el grado de Maestro en Ingeniería por la misma universidad. Mi perfil completo lo puede encontrar en: https://www.linkedin.com/in/fjrg76-dot-com/

Ver todas las entradas

1 COMENTARIO