Esta clase poco conocida de Arduino le dará vida a tus displays de texto y te olvidarás de tener que escribir funciones de conversión

Esta clase poco conocida de Arduino le dará vida a tus displays de texto y te olvidarás de tener que escribir funciones de conversión

Introducción

¿Construiste un display y luego te volviste loco escribiendo funciones de conversión para imprimir cadenas de texto y números?

¿Construiste un segundo display y te viste haciendo exactamente lo mismo?

¿Pasaste horas cazando y depurando errores en tus funciones de conversión?

¿Estás feliz con tu display, pero no estás contento con los resultados que imprime y te sientes limitado y frustrado?

A todos nos ha sucedido descubrir la rueda una y otra vez, y la rueda es unas veces más cuadrada que otras.

¿No sería maravilloso que existiera un mecanismo casi automático para que nuestro display imprima correctamente, sin errores, de forma profesional y sin reinventar la rueda?

Te tengo dos buenas noticias: 

1. La primera es que este mecanismo sí existe: es la clase Print de Arduino. 

2. La segunda es que en este artículo te voy a mostrar cómo utilizarla con tu recién creado display.

Sigue leyendo porque estás a una función de llevar tu display a otro nivel.

La clase Print te ayudará a darle vida a tu display. Imagina hacer lo siguiente en tu nuevo y flamante display (7 segmentos, punto decimal y 8 dígitos):

MyAmazingDisplay myLCD( /* initialization */ );

myLCD.clear();                
// You provide this feature

myLCD.print( "HI THERE" ); 
// Output: [HI THERE]
// Arduino takes care of the details

myLCD.print( 12345 );
// Output: [12345___] 
// Arduino takes care of the details

myLCD.showDP( 3 );
// Output: [123.45___] 
// You provide this feature

Como observarás más adelante, la clase que va a modelar a tu display sólo deberá proveer un punto de entrada para que la magia suceda; así mismo, tú podrás incluir funciones auxiliares como .clear(), .showSymbol(), etc.

DATO INTERESANTE: Las clases de Arduino Serial y LiquidCrystal utilizan este mismo mecanismo. Con mucha facilidad podrías ampliarlo a los buses I2C y SPI.

Conceptos

¿Qué vas a necesitar para poder integrar las funciones de impresión a tu nuevo y maravilloso display?

  1. Tener el driver del display. La clase Print no te ayudará con eso.
  2. Escribir una clase que represente a tu display y que herede de la clase base abstracta Print.
  3. Implementar el método virtual puro Print::write().
  4. Escribir funciones auxiliares o utilitarias (si las hubiera).
  5. Disfrutar de tu nuevo display.

Como este artículo no es sobre drivers de displays no voy a ahondar mucho en ello, aunque en el ejemplo completo podrás ver el que escribí para la charla de hoy. Así que comencemos con un poco de teoría para dar contexto a lo que viene. Si ya conoces estos conceptos, entonces podrías ir directamente al desarrollo.

Métodos virtuales puros

Un método virtual puro es aquella función miembro de una clase que ha sido marcada como virtual, igualada a 0 y que no tiene implementación (es decir, carece de código):

struct AbstractBase
{
   // Pure virtual method:
    virtual void method() = 0; // No code

   // Regular method:
   void other_method(){ /* body */ }
};

La implementación (el código) del método virtual puro será escrito en la clases que hereden de la clase que la contiene.

Clases base abstractas

Una clase base abstracta es una clase que contiene al menos un método virtual puro. Las clases que tienen uno o más métodos virtuales puros no pueden ser instanciadas; esto es, no puedes crear objetos a partir de ellas. Su uso es principalmente como una clase base.

Las clases que hereden de una clase base abstracta deberán implementar (escribir el código) del o de los métodos virtuales puros.

struct AbstractBase
{
    virtual void method() = 0;
};

// Your class:
struct Derived : public AbstractBase
{
   // Your code:
    virtual void method() override
    {
        // method body
    }
};

AbstractBase b(); // Error
Derived d();      // Ok
// ...
d.method();

Una forma de pensar sobre las clases base abstractas y los métodos virtuales puros es como un contrato: la clase base indica lo que se debe hacer (el qué), y la clases que hereden de ella deberán proporcionar el código (el cómo).

NOTA: Cuando una clase base abstracta incluye únicamente métodos virtuales puros (sin variables miembro), entonces se convierte en una interfaz. Las interfaces son un concepto avanzado en la programación orientada a objetos y su uso es invaluable en los sistemas embebidos. En este artículo escribí algo sobre interfaces como elemento de apoyo para crear componentes intercambiables.

La clase base abstracta Print

¿Qué tienen que ver los métodos virtuales puros y las clases base abstractas con nuestro display?

Como comenté en la introducción, la plataforma de Arduino incluye la clase base abstracta Print que realiza todas las conversiones numéricas y de texto necesarias para que tú ya no tengas que hacerlo; el punto de conexión entre esta clase y la tuya es el método virtual puro .write().

Retomando la analogía del contrato, la clase Print nos proveerá de las funciones de conversión siempre y cuando nosotros nos comprometamos a proporcionar el código del método Print::write().

Te presento una versión recortada de la clase Print para que observes las partes que nos interesan.

// Print.cpp

class Print
{
  private:
    // ...
  protected:
    // ...
  public:
    // ...
    
    // This is the pure virtual method that we're going to implement later:
    virtual size_t write(uint8_t) = 0; 

    // ...

    // Once the .write() method has been overrided in your class, you'll have
    // access to all this functionality without writing a single line of code.
    // How cool is that?:
    
    size_t print(const __FlashStringHelper *);
    size_t print(const String &);
    size_t print(const char[]);
    size_t print(char);
    size_t print(unsigned char, int = DEC);
    size_t print(int, int = DEC);
    size_t print(unsigned int, int = DEC);
    size_t print(long, int = DEC);
    size_t print(unsigned long, int = DEC);
    size_t print(double, int = 2);
    size_t print(const Printable&);

    size_t println(const __FlashStringHelper *);
    size_t println(const String &s);
    size_t println(const char[]);
    size_t println(char);
    size_t println(unsigned char, int = DEC);
    size_t println(int, int = DEC);
    size_t println(unsigned int, int = DEC);
    size_t println(long, int = DEC);
    size_t println(unsigned long, int = DEC);
    size_t println(double, int = 2);
    size_t println(const Printable&);
    size_t println(void);
};

El driver del display

    El tema de hoy no es acerca del driver del display; sin embargo, sin un display de prueba toda esta discusión no tendría sentido. Te muestro a continuación las partes más interesantes de la clase que implementa el driver de mi display (en el ejemplo podrás ver el código completo):

    // Display_HC595.hpp
    
    #include "Print.h" (1)
    
    class Display_HC595 : public Print // (2)
    {
       private:
          // ...
    
          using Print::write;
          virtual size_t write( uint8_t car ) override; // (3)
    
       public:
          Display_HC595( const uint8_t* _cathodes, const uint8_t _num_digits, const uint8_t _latch_pin, uint8_t _decimal_point, uint8_t* _memory_frame ); // (4)
    
          // ...
    
          void begin(); // (5)
    
          // Auxiliar functionality that isn't part of the Print class, but it's needed for our display:
          void clear();
          void setCursor( uint8_t pos );
          void showDP( uint8_t pos );
          void hideDP( uint8_t pos );
    
          // This kind of display must be refreshed periodically:
          void update();
    };
    

    (1) Incluimos el header de la clase abstracta Print.
    (2) Heredamos de la clase base abstracta Print.
    (3) Avisamos que vamos a implementar el método virtual puro .write().
    (4) Inicializamos el objeto de la manera más conveniente para nuestro display.
    (5) Tradicionalmente el método .begin() es utilizado para la inicialización del hardware.

    Implementando el método Print::write()

    ¿Qué hace el método .write()? Lo que sea necesario para imprimir un carácter a la vez.

    Cualquiera de los métodos .print() o .println() provistos en la clase Print intentarán, a través del método .write(), escribir un carácter a la vez en el display y lo que hagas con este carácter depende completamente de la naturaleza de tu display. 

    En mi driver tengo un arreglo que hace las veces de “memoria de video” (memory_frame) que es a dónde dichos métodos escriben cada carácter (esto es, están escribiendo a RAM en lugar de hacerlo directamente en hardware). Posteriormente, el método .update(), el cual debe ser llamado regularmente, toma un carácter de la memoria de video y la escribe en el hardware, como en cualquier otro display de 7 segmentos.

    Te muestro a continuación la forma en como implementé al método .write() en mi driver:

    // Display_HC595.cpp
    
    size_t Display_HC595::write( uint8_t car )
    {
       // handle everything, but the decimal point:
       if( car != '.' )
       {
          if( this->cursor >= this->len ) this->cursor = 0;
    
          if( car == '\n' )
          {
             this->cursor = 0;
          }
          else
          {
             // I don't write directly into the hardware, I do it into RAM instead:
             this->memory_frame[ this->cursor ] = encode( car ); 
    
             ++this->cursor;
          }
       }
    
       // handle the decimal point:
       else
       {
          if( this->cursor > 0 )
          {
             this->memory_frame[ this->cursor - 1 ] |= this->decimal_point;
          }
          else
          {
             this->memory_frame[0] = this->decimal_point;
             ++this->cursor;
          }
       }
    
       return 1;
    }

    Si tu display ya está provisto de la memoria RAM, como en un LCD de 16×2, entonces el método .write() puede escribir directamente en él, carácter por carácter.

    El siguiente fragmento de código muestra la forma en como la clase LiquidCrystal de Arduino lo implementa:

    // LiquidCrystal.cpp
    
    inline size_t LiquidCrystal::write(uint8_t value) {
      send(value, HIGH);
      return 1; // assume sucess
    }
    
    void LiquidCrystal::send(uint8_t value, uint8_t mode) {
      digitalWrite(_rs_pin, mode);
    
      if (_rw_pin != 255) { digitalWrite(_rw_pin, LOW); }
      
      if (_displayfunction & LCD_8BITMODE) {
        write8bits(value); 
      } else {
        write4bits(value>>4);
        write4bits(value);
      }
    }

    Toma en cuenta que si tu display usa un driver en forma de chip externo (como el HT1621) o interno, como en muchos microcontroladores, entonces tu método .write() deberá escribir a la memoria RAM de ellos y no directamente en el hardware.

    Utilizando el driver

    Debo aclarar que esta técnica sólo funciona con displays que imprimen texto. En mi ejemplo utilicé un display de 4 dígitos y 7 segmentos basado en el registro de corrimiento 74HC595. Aunque yo diseñé y construí este display, en el mercado te puedes encontrar muchas variantes.

    A continuación te muestro el código de prueba donde podrás observar lo fácil y conveniente que es utilizar las funciones .print() y .println() en tus propios displays. No es un archivo de sketch de Arduino, pero podrás darte una idea de cómo usarlo en tus proyectos:

       while( 1 )
       {
          delay( SYSTEM_TICK );
    
          display.update();
          // update the display every 5 ms
    
          if( --time_delay == 0 )
          {
             time_delay = MILLIS_TO_TICKS( 1000 );
    
             if( not first_time )
             {
                display.clear();
    
                display.print( "0000" );
                // we're printing a string (for zero-padding)
    
                if( cont < 10 )        display.setCursor( 3 );
                else if( cont < 100 )  display.setCursor( 2 );
                else if( cont < 1000 ) display.setCursor( 1 );
    
                display.print( cont );
                // we're printing an integer
    
                display.showDP( 3 );
                // insert a decimal point in digit 3 just for fun
    
                --cont;
                if( cont == 0 )
                {
                   cont = 15;
                   first_time = true;
                }
             }
             else
             {
                display.clear();
    
                display.print( "A.BC.D" );
                // decimal point is part of a string, twice!
    
                first_time = false;
             }
          }
       }

    Las siguientes imágenes muestran al display en funcionamiento. Nota la cadena de texto con 2 puntos decimales (puedes usar hasta 4 al mismo tiempo) y la facilidad con que lo logré:

    En caso de que mi driver no sea lo que necesitas, siempre podrás escribir el tuyo o buscar uno que se adapte a tus necesidades.

    Implementación en otros tipos de display

    Podemos encontrar en el mercado cualquier cantidad de displays con comunicación serial, ya sea utilizando los buses I2C o SPI. Algunos LCD de 16×2 utilizan un expansor de puertos PCF8574 con bus I2C, mientras que otros, cuyos chips principales son los famosos MAX7219 o el TM1637, utilizan el bus SPI. En cualquier caso deberás identificar el punto de conexión entre el método Print::write() y el lugar donde efectivamente escribes o guardas un carácter a la vez.

    Código fuente

    En este enlace podrás acceder al código completo de este artículo. Siéntete libre de modificarlo para que se adapte a tu display en particular.

    IMPORTANTE: Vas a notar que el proyecto no incluye ningún archivo .ino de Arduino. En lo personal compilo desde la línea de comandos, pero es muy fácil que uses el driver: sólo toma los archivos Display_HC595.cpp y Display_HC595.hpp y pégalos en tus proyectos.

    ¿Qué sigue?

    Hoy hemos visto cómo utilizar a la clase base abstracta Print de Arduino para que nos ayude en las tareas de impresión. Quizás te preguntaste: ¿pero yo no uso dicha plataforma, cómo puedo utilizarla? O tal vez un cliente te ha pedido que migres de Arduino hacia otra plataforma, ¿cómo llevarte esta maravillosa herramienta?

    Hay una buena noticia: con muy poco trabajo puedes llevarte Print y utilizarla fuera del ecosistema de Arduino. Es más fácil de lo que parece, pero es tema de otro artículo.

    Espero que esta información haya sido de tu agrado y que te sirva en tus proyectos de Arduino. ¡Hasta el próximo artículo!



    ¿Ya conoces mi curso gratuito de Arduino en tiempo real, utilizando el Arduino UNO o el Arduino Due y FreeRTOS?


    ¿Te gustaría ser de los primeros en recibir las actualizaciones a mi blog? ¡Suscríbete, es gratis!


    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