Archivo de la categoría: Curso Programación Arduino

Prácticas: Clases y Objetos

Montaje Arduino UNO:

Montaje Wemos:

Ejercicio19 – Clase Contador

Hacer un programa que cuente las pulsaciones de los botones A y B usando la clase contador guardada en un fichero “Contador.h”

Solución: https://codeshare.io/5QYNeq

Hacer commit y pull del código en el repositorio “Curso Programacion Arduino 2019” que esté en una carpeta llamada Ejercicio19-Clase_Contador

Si solo usamos funciones como en el ejercicio 17, necesito dos funciones detecta flanco y no puedo usar solo una porque si llamo a una y a otra simultáneamente el valor static se mantiene entre la llamada de una y otra lo que hace que falle. Para resolver este problema, hacer una clase DetectaFlanco y entonces puedo reutilizar el código ya que cada vez que instancio una nueva clase es como una función nueva.

NOTA: la clase se puede definir en un fichero “Contador.h” o dentro del mismo fichero .ino, ver ejemplo en https://github.com/jecrespo/aprendiendoarduino-Curso_Programacion_Arduino_2019

Ejercicio20 – Clase Detecta Flanco

Hacer el programa anterior pero creando  una clase llamada DetectaFlanco en un fichero “DetectaFlanco.h”

Solución: https://codeshare.io/Gb6K0M

Hacer commit y pull del código en el repositorio “Curso Programacion Arduino 2019” que esté en una carpeta llamada Ejercicio20-Clase_Detecta_Flanco

NOTA: Esta clase se puede definir en un fichero “DetectaFlanco.h” y otro “DetectaFlanco.cpp” ver ejemplo en https://github.com/jecrespo/aprendiendoarduino-Curso_Programacion_Arduino_2019

Ejercicio21: Sensor DHT

Para entender mejor las clases y objetos y antes de entrar en el apartado de librerías y cómo crearlas, vemos un ejemplo de la librería DHT22 para las sondas de temperatura y humedad, de forma que entendamos que cuando la usamos para una sonda, lo que hacemos es instanciar un objeto de tipo sonda DHT22 y cuando llamamos al método readTemperature() estamos ejecutando la función que consulta la temperatura. También vamos a ver cómo se estructura la clase en el fichero de cabecera y en el de contenido.

El código de la librería lo tenemos en https://github.com/adafruit/DHT-sensor-library y vemos que tenemos dos ficheros:

En el fichero de cabecera tenemos la definición de la clase:

class DHT {
  public:
   DHT(uint8_t pin, uint8_t type, uint8_t count=6);
   void begin(void);
   float readTemperature(bool S=false, bool force=false);
   float convertCtoF(float);
   float convertFtoC(float);
   float computeHeatIndex(float temperature, float percentHumidity, bool isFahrenheit=true);
   float readHumidity(bool force=false);
   boolean read(bool force=false);

 private:
  uint8_t data[5];
  uint8_t _pin, _type;
  #ifdef __AVR
    // Use direct GPIO access on an 8-bit AVR so keep track of the port and bitmask
    // for the digital pin connected to the DHT.  Other platforms will use digitalRead.
    uint8_t _bit, _port;
  #endif
  uint32_t _lastreadtime, _maxcycles;
  bool _lastresult;

  uint32_t expectPulse(bool level);
};

Y en el fichero de definiciones DHT.cpp tenemos el código.

Constructor:

 
DHT::DHT(uint8_t pin, uint8_t type, uint8_t count) {
  _pin = pin;
  _type = type;
  #ifdef __AVR
    _bit = digitalPinToBitMask(pin);
    _port = digitalPinToPort(pin);
  #endif
  _maxcycles = microsecondsToClockCycles(1000);  // 1 millisecond timeout for
                                                 // reading pulses from DHT sensor.
  // Note that count is now ignored as the DHT reading algorithm adjusts itself
  // basd on the speed of the processor.
}

Método begin():

 
void DHT::begin(void) {
  // set up the pins!
  pinMode(_pin, INPUT_PULLUP);
  // Using this value makes sure that millis() - lastreadtime will be
  // >= MIN_INTERVAL right away. Note that this assignment wraps around,
  // but so will the subtraction.
  _lastreadtime = -MIN_INTERVAL;
  DEBUG_PRINT("Max clock cycles: "); DEBUG_PRINTLN(_maxcycles, DEC);
}

Método readTemperature(), que llama a la función read() que es la que hace toda la operación de consultar a la sonda y guarda en la propiedad privada data la información leída y readTemeprature() se encarga de darle formato en función del tipo de sonda y devolver el float con la temperatura:

 
float DHT::readTemperature(bool S, bool force) {
  float f = NAN;

  if (read(force)) {
    switch (_type) {
    case DHT11:
      f = data[2];
      if(S) {
        f = convertCtoF(f);
      }
      break;
    case DHT22:
    case DHT21:
      f = data[2] & 0x7F;
      f *= 256;
      f += data[3];
      f *= 0.1;
      if (data[2] & 0x80) {
        f *= -1;
      }
      if(S) {
        f = convertCtoF(f);
      }
      break;
    }
  }
  return f;
}

Este esquema explica como funciona este sensor y el protocolo de comunicación. La librería implementa el protocolo y facilita el uso de la sonda con Arduino.

En un ejemplo de uso de esta clase, primero hacemos un include del fichero, luego instanciamos un nuevo objeto sonda llamado dht. En el setup hacemos el begin() para iniciarlo y en el loop llamamos a los métodos de leer temperatura y humedad.

#include "DHT.h"
DHT dht(DHTPIN, DHTTYPE);

void setup() {
  dht.begin();
}
void loop() {
  float h = dht.readHumidity();
  float t = dht.readTemperature();
}

Partiendo del ejemplo de la librería llamado DHTtester, adaptarlo para el montaje de Arduino Uno pero usando dos sondas DHT11 en los pines 11 y 12

Solución: https://codeshare.io/arJ0nv

Hacer commit y pull del código en el repositorio “Curso Programacion Arduino 2019” que esté en una carpeta llamada Ejercicio21-DHT

Anuncio publicitario

Clases y Objetos

Un paradigma es el resultado de un proceso social en el cual un grupo de personas desarrolla nuevas ideas y crea principios y prácticas alrededor de estas ideas, resumiendo: un paradigma es una metodología de trabajo.

En programación, se trata de un enfoque concreto de desarrollar y estructurar el desarrollo de programas.

Paradigma imperativo

  • Consiste en una secuencia de instrucciones que el ordenador debe ejecutar.
  • Los elementos más importantes en esta forma de programar son:
    • Variables, zonas de memoria donde guardamos información.
    • Tipos de datos, son los valores que se pueden almacenar.
    • Expresiones, corresponde a operaciones entre variables (del mismo o distinto tipo)

Paradigma funcional:

  • Consiste en el uso de funciones que realizan su tarea como si de una caja negra se tratase
  • Pese a que trabajamos con funciones, el modelo desarrollado hasta ahora con Arduino no verifica todos los requisitos del paradigma de programación funcional ya que, en nuestro caso existe el concepto de variable, que no se da en programación funcional. 

Paradigma orientado a objetos:

  • Es el más popular en la actualidad.
  • Se fundamenta en la “fusión” de datos y funciones que operan sobre esos datos dentro de un nuevo tipo de dato.
  • Al nuevo tipo de dato se le llama CLASE.
  • A cada variable de una clase se le llama OBJETO.

Propiedades del paradigma orientado a objetos

  • Encapsulamiento
    • Significa que los datos pertenecen a un objeto (espacio de nombres del objeto).
    • Podemos ir más allá y ocultar los datos de un objeto a cualquier otro objeto o código que trate de hacer uso de ellos. Serían sólo accesibles al propio objeto y, en algunos casos, a objetos de sus clases descendientes.
  • Herencia:
    • Es la propiedad de crear nuevos datos a partir de un objeto
  • Una clase es un nuevo tipo de dato. Contiene :
    • otros datos (que pueden ser de cualquier tipo)
    • Funciones, que operan sobre esos datos.

Las variables incluidas en una clase se denominan ATRIBUTOS.

Las clases pueden contener funciones. A éstas se les denomina MÉTODOS.

Una vez definida la clase, crear un objeto es tarea sencilla. Basta con ejecutar la instrucción de asignación: objeto = Nombre_clase ()

Programación Orientada a Objetos

La programación orientada a objetos (POO, u OOP según sus siglas en inglés) es un paradigma de programación que viene a innovar la forma de obtener resultados. Los objetos manipulan los datos de entrada para la obtención de datos de salida específicos, donde cada objeto ofrece una funcionalidad especial.

La modularidad es la capacidad de dividir el problema en pequeñas partes independientes entre sí, esto va más allá de la encapsulación de un simple procedimiento de la programación estructurada que hemos visto hasta ahora. Aquí se encapsula un conjunto de operaciones y datos que tienen mucha relación entre sí formando un módulo.

Un lenguaje basado en objetos nos da la posibilidad de definir objetos y realizar operaciones sobre ellos y es similar al paradigma de abstracción de datos. La adición de de los conceptos de de clases y herencia nos hace hablar de lenguajes orientados a objetos. Un objeto es la instancia de una clase.

Mediante la herencia podemos reutilizar el comportamiento de una clase en la definición de nuevas clases.

Muchos de los objetos pre-diseñados de los lenguajes de programación actuales permiten la agrupación en bibliotecas o librerías, sin embargo, muchos de estos lenguajes permiten al usuario la creación de sus propias bibliotecas.

Está basada en varias técnicas, incluyendo herencia, cohesión, abstracción, polimorfismo, acoplamiento y encapsulamiento.

Los objetos son entidades que tienen un determinado «estado», «comportamiento (método)» e «identidad»:

  • La identidad es una propiedad de un objeto que lo diferencia del resto; dicho con otras palabras, es su identificador (concepto análogo al de identificador de una variable o una constante).
  • Los métodos (comportamiento) y atributos (estado) están estrechamente relacionados por la propiedad de conjunto. Esta propiedad destaca que una clase requiere de métodos para poder tratar los atributos con los que cuenta.

Una clase es una plantilla para la creación de objetos de datos según un modelo predefinido. Las clases se utilizan para representar entidades o conceptos. Cada clase es un modelo que define un conjunto de variables (el estado), y métodos apropiados para operar con dichos datos (el comportamiento). Cada objeto creado a partir de la clase se denomina instancia de la clase.

Las clases son un pilar fundamental de la programación orientada a objetos. Permiten abstraer los datos y sus operaciones asociadas al modo de una caja negra. Los lenguajes de programación que soportan clases difieren sutilmente en su soporte para diversas características relacionadas con clases. La mayoría soportan diversas formas de herencia.

C++ está diseñado para la programación orientada a objetos (POO), y en ese paradigma, todas las entidades que podemos manejar son objetos. Los punteros en C++ sirven para señalar objetos, y también para manipularlos.

La OOP tiene tres características básicas que se suelen presentar como Encapsulación, Polimorfismo y Herencia.

  • La encapsulación es un procedimiento por el cual, los datos y las funciones se encierran en un contenedor llamado objeto, que usamos para aislar ambos elementos de la manipulación exterior y forzar a que esta se haga de un modo controlado y validado por nosotros mismos. El mecanismo que C++ utiliza para encapsular estos objetos se llaman clases.
  • Se suele definir el polimorfismo como: Un interface único, múltiples métodos. Y es algo que reduce la complejidad de los programas de una forma notable. A usar las funciones u operadores de diferentes maneras dependiendo del tipo de los datos, le llamamos polimorfismo y cuando redefinimos un operador o función para comportarse de forma diferente con esos datos, decimos que el operador esta sobrecargado (Overloaded)- Por ejemplo en C++ podemos sumar números enteros o números float con el mismo símbolo “+”, y nos parece tan normal, pero en realidad los procedimientos que se aplican son completamente distintos.
  • La herencia es un proceso por el cual un objeto hereda las propiedades y métodos de otro, sin necesidad de volverlas a definir desde el principio.

Más información: http://www.alegsa.com.ar/Dic/programacion_orientada_a_objetos.php

Ejemplo de uso de un objeto: Imaginemos que hemos hecho un coche coche controlado por bluetooth y tenemos 5 botones para las 4 direcciones y parar. Creamos una clase llamada ‘Coche’ que se inicializa con los pines donde conecto los dos motores del coche.

#include “Coche.h”

Coche MiCocheRC(6,7)	//Construyo el objeto poniendo los pines de los motores

void setup() {
MiCoche.Arranca()
}

void loop() {
char valor = leeBluetooth();

switch (valor) {
    case ‘F’:
      MiCoche.Adelante();
      break;
    case ‘R’:
      MiCoche.Derecha();
      break;
    case ‘L’:
       MiCoche.Izquierda();
      break;
    case ‘B’:
       MiCoche.Atras();
      break;
    case ‘S’:
       MiCoche.Para();
      break;
  }
}

Dentro del fichero ‘Coche.h’ se define la clase y las variables y métodos. Dentro del fichero ‘Coche.cpp’ está el código para mover los motores según cada una de las 5 funciones que he definido.

La función leeBluetooth() simplemente lee valores que llegan por el puerto serie al que está conectado el bluetooth.

Para llamar a los métodos de un objeto y en función de cómo esté construida la librería se puede hacer de varias formas:

Más información sobre clases y objetos:

Clases y Objetos en Arduino

Antes de que empecemos a hablar sobre Clases y Objetos, es importante insistir en que, la OOP no es tanto un lenguaje de programación diferente, sino más bien, una manera diferente de organizar tus programas y tus ideas, de acuerdo con unos principios guía que nos permiten modelar nuestro código de un modo distinto a como lo hemos hecho hasta ahora.

Para definir las Clases, existen una serie de reglas y de nuevas instrucciones, pero por lo demás el lenguaje sigue siendo el de siempre.

Si queremos hacer un contador en Arduino, creamos una variable global llamada “contador”. Pero esto ilustra bastante bien el problema de que si quiero 6 contadores voy a necesitar 6 variables globales. La idea en OOP es crear una Clase que nos permita definir Objetos tipo Contador que se pueda reutilizar y que nos permita mezclar diferentes contadores en un mismo programa.

Una de las ideas básicas tras la OOP es encapsular los datos y las funciones (o propiedades y métodos) de nuestro programa en un contenedor común, y más importante aún, aplicamos el principio de: “Esconder los datos y mostrar los métodos o funciones”.

Cuando definimos Clases, veremos que hay partes que son públicas y otras que son privadas. La sintaxis para definir la Clase contador que nos ocupa:

class Contador{
  private:
    int N;
 
  public:
    void SetContador( int n){
      N = n;
    }
 
    void Incrementar(){
      N++;
    }
 
    int GetCont(){
      return (N);
    }
};

Debajo de la cláusula “private:” viene las variables y funciones ocultas al exterior. Solo pueden ser invocadas desde el interior de la clase, es decir no se pueden ejecutar por una llamada exterior. Y lo contrario ocurre con lo que definamos tras la cláusula “public:”

Vamos a definir una variable privada llamada N, que llevará la situación del contador, y después necesitaremos los métodos necesarios para trabajar con ella. En principio vamos a definir tres funciones públicas: Una que ponga el contador a un valor dado antes de nada, Otra que sirva para incrementar el contador, y otra tercera para que nos entregue el valor del contador en un momento dado.

La variable N se comporta como si fuera una variable global pero sólo dentro del ámbito de la Clase (Encapsulación), y como está definida como private es inaccesible desde el exterior (Cualquier intento de llegar a ella causará un error del compilador)

Para usar esta clase, primero se instancian tantas ocurrencias de la clase como queramos. 

#include "Contador.h"

Contador C1,C2;

void setup()
   {
      C1.SetContador(10);
      C2.SetContador(100);
   }

void loop()
   {
      C1.Incrementar() ;
      Serial.print("C1 = ") ; Serial.println(C1.GetCont());
 
      C2.Incrementar() ; C2.Incrementar() ; C2.Incrementar();
      Serial.print("C2 = ") ; Serial.println(C2.GetCont());
   }

Constructures

Usamos el “.” para referir la función a la que queremos llamar, con el Objeto al que se le aplica, como hemos visto antes en otros programas aunque sin entrar en muchos detalles.

Podemos definir una función que se ejecuta siempre que se crea un objeto, y es tan habitual que tiene nombre. Se le llama Constructor de la Clase, y para ello basta con llamarla igual que la Clase (Sin tipo):

 
Class Contador{
  private:
    int N;  
 
  public:
    Contador( ){    	// Constructor
      N = 0;
    }
  
    void SetContador(int n){
      N = n;
    }
 
    void Incrementar(){
      N++;
    }
 
    int GetCont(){
      return(N);
    }
};

Una peculiaridad de los constructores es que no tienen un tipo definido, otra de las razones por las que el compilador sabe que es un constructor.

En este caso cuando hago Contador C1,C2 ; ya está inicializado el contador a 0 que lo hace el propio constructor.

Ficheros Cabecera (Header)

Cuando las clases y las funciones miembro son tan pequeñas y sencillas como en este caso, la forma que hemos visto de definirlas puede valer, pero en seguida se quedará corta. Por eso podemos declarar las funciones y variables miembros en la declaración de Clase, y definirlas fuera para mayor comodidad y evitar errores de sintaxis complicados de detectar.

Podemos reescribir la clase Contador así:

Fichero “Contador.h”:

 
class Contador
   {  private:
         int N ;

      public:
         Contador( ) ;               	// Constructor
         void SetContador( int n) ;  	// Declaracion de funcion externa
         void Incrementar() ;        	// Declaracion de funcion externa
         int GetCont() ;             	// Declaracion de funcion externa
   } ;

Fichero “Contador.cpp”:

 
#include <Contador.h>
 
  void Contador::SetContador( int n)
        {  N = n ;	}

   void Contador::Incrementar()
        {  N++ ; }
 
   int Contador::GetCont()
        { return (N) ;}

Declaramos las funciones miembros dentro de la Clase (Para informar al compilador), pero no incluimos su código aquí, porque sería muy confuso en cuanto crezcan de tamaño (Pero fijaros que ahora hay un punto y coma al final de las declaraciones que antes no había en el fichero header).

En cualquier otro lugar podemos definir esas funciones sin más que hacer referencia a la Clase a la que pertenecen usando el operador ‘::’ (Scope Operator u Operador Ámbito) y el compilador entiende que son miembros de la clase que precede al operador. Este operador le indica al compilador, que estas funciones o variables son miembros de la clase, y sólo pueden invocarse de acuerdo a las condiciones que se especifican en la declaración de la Clase (Que debe coincidir con esta, claro está).

Si editais cualquiera de las librerías de Arduino, encontrareis que ésta es la forma habitual de programar las clases y librerías.

Cuando declaramos una propiedad miembro de una clase como static, el compilador crea una única variable para ella, que es compartida por todas las instancias de los objetos de esa Clase, rompiendo así la regla de que cada instancia tiene su propio juego de memoria y propiedades.

Polimorfismo y Sobrecarga

El polimorfismo podemos verlo en la clase Serial:

 
Serial.println(5) ;
Serial.println(3.1416 ) ;
Serial.println(“Buenos días”) ;

Según lo que hemos aprendido hasta ahora, una función solo puede aceptar un tipo definido de parámetros.  Es una característica inherente a C++ y que no existía en C, y no es otra que una característica llamada function overloading.

Gracias al Polimorfismo los println() anteriores funcionan, aunque todo indica que no deberían, porque va en contra de todo lo que hemos aprendido hasta ahora de las funciones. El misterio está en que no existe una única función println(), sino que las líneas anteriores invocan 3 funciones completamente diferentes… que se llaman igual.

Hacer un Overloading del Constructor, en la clase contador

 
class Contador
   {  private:
         int N ;

      public:	
         Contador( ) ;           	// Constructor
         Contador( int k ) ;     	// Constructor
         void SetContador( int n) ;
         void Incrementar() ;
         int GetCont() ;
   } ;

Contador::Contador( )          	// Constructor
    { N = 0 ; }       
Contador::Contador( int k)         // Constructor
    { N = k ; }   

Contador C1, C2(23) ;

Así podemos instar un objeto sin una variable y el contador empieza en 0 o con una variable entera y el contador empieza con ese valor. Hemos hecho un Overloading del Constructor de la Clase.

El function Overloading es un aspecto del Polimorfismo que nos permite manejar diferentes objetos con los mismos métodos o propiedades. El Polimorfismo es una cualidad abstracta de los objetos que nos permite usar un interface único, de métodos y propiedades, en una colección de objetos de distintos tipos o Clases.

Operadores

No solo se pueden sobrecargar las funciones, sino también los operadores para que hagan cosas diferentes en función del tipo de los operadores. De hecho, cuando definimos una nueva Clase, lo que estamos haciendo es crear un nuevo tipo de datos, tipo en el sentido de int, long, etc. y dentro de cada clase podemos hacer el Overloading de los operadores que nos interesen, para indicarle al compilador, cómo debe ejecutarse la operación que representa el símbolo del operador.

 
class Contador
   {  private:
         int N ;

      public:
         Contador( ) : N(0) {}    	            // Constructor
         Contador(int k ) : N(k) {}           	// Constructor
         void SetContador( int n) ;
         int GetCont() ;
         void operator ++ ();                     	// Aqui esta ++
   } ;

void Contador::SetContador( int n)  {  N = n ;	}
int  Contador::GetCont() { return (N) ;}
void Contador::operator ++ ()                     	//  <---
     {  ++N }

Usamos la keyword “operator”, para identificar el operador a definir y la definimos como void porque no devolvemos nada, simplemente incrementamos su valor. Ahora podemos hacer:

 
Contador C1(10)  ;
++C1 ;
Serial.println(C1.GetCont());

Para cada objeto declarado de una clase se mantiene una copia de sus datos, pero todos comparten la misma copia de las funciones de esa clase. Esto ahorra memoria y hace que los programas ejecutables sean más compactos, pero plantea un problema. Cada función de una clase puede hacer referencia a los datos de un objeto, modificarlos o leerlos, pero si sólo hay una copia de la función y varios objetos de esa clase, ¿cómo hace la función para referirse a un dato de un objeto en concreto?

La respuesta es: usando el puntero especial llamado this. Se trata de un puntero que tiene asociado cada objeto y que apunta a si mismo. Ese puntero se puede usar, y de hecho se usa, para acceder a sus miembros.

El operador “this” es un puntero que se pasa a disposición de todas las funciones miembro de la clase, (Y eso incluye a todas los funciones de operadores sobrecargados), que apunta al objeto al que pertenecen. Cuando instanciamos C1, cualquier función miembro que reclame el operador this, recibe un puntero a la dirección de memoria que almacena sus datos, que por definición es una la dirección del objeto C1.

 
const Contador &Contador::operator ++()
    { ++N;
      return *this ;
    }

Definimos la función operator ++ como tipo Contador porque va a devolver un objeto de este tipo. La particularidad está en que avisamos al compilador con el símbolo &, de que lo que vamos a devolver es un puntero a un objeto de la clase Contador, y no un objeto. Tras incrementar N, ya hemos realizado la operación que buscábamos y el objeto presente, por ejemplo C1, ya tiene el valor adecuado. Y ahora devolvemos el puntero a nuestra propia instancia del Objeto con la referencia que indica el operador this y de ese modo nos ahorramos el trasiego de crear y eliminar objetos temporales. Lo de especificar la función como const, es para evitar que al pasar la referencia de nuestro objeto actual, haya posibilidad de modificarlo por error.

Sumar contadores, podemos sumar los registros internos de dos contadores, de modo que el resultado sea otro contador con un valor de N interno igual a la suma de los dos operandos. No sería complicado definir una función miembro, que podemos llamar Suma, que devuelva un objeto Contador tras operar con dos contadores. Podríamos hacer algo así:

 
Contador  Contador::Suma( const Contador & C1 ) 
      {   return Contador  ( N + C1.GetCont() ) ; }

Aunque este método funcionará, su uso es un poco extraño :

 
Contador C1(), C2(23) ;
Contador C3 = C1.Suma(C2) ;

Pero si queremos escribir la suma así: Contador C3 = C1 + C2 ;  para eso está la sobrecarga de operadores binarios.

 
class Contador
  {   private:
     	int N ; 

  	public:
     	Contador( ) : N(0) {}                 // Constructor
     	Contador(int k ) : N(k) {}            // Constructor
     	void SetContador( int n) ;
     	int GetCont() ;                 
     	const Contador &operator ++ ();
     	Contador operator ++ (int) ;
         Contador operator + (  Contador &) ;  // Pasamos una referencia a un contador
  } ;

Contador  Contador::operator + (  Contador & C1 )
       	{ return Contador  (  N + C1.GetCont() ) ; }

Herencia

La herencia es la reusabilidad del código. Supongamos que ya tenemos probada y depurada la clase Contador y que ahora necesitamos una Clase nueva que en vez de ir creciendo sin fin sea un descontador, para que haga cuentas a cero desde el número que le damos, como para lanzar un cohete.

Podemos coger el código fuente de contador y modificarlo para incluir un decrementador del mismo, pero también podemos hacer uso de la herencia para que la nueva clase herede de la anterior. El método es derivar una nueva clase de una que ya existe. Esto hace que la clase derivada herede todas las características y métodos de la Clase Base sin tocarla y ahora podamos añadir lo que nos interese, garantizando que la Clase original permanece inalterada.

Queremos definir una nueva clase que se llame CountDown derivada de Contador y añadirle una función de decremento. Para ello lo primero es ver cómo derivamos una clase de otra. La sintaxis es esta:

 
Class CountDown :  public Contador          // Es una clase derivada
   {   public:
       	Counter Operator –()
           	{ return Counter(--N) ; 
   }

En la primera línea declaramos una nueva clase CountDown que deriva de Counter y es de acceso público, y después definimos un prefix operator para decrementar la variable interna. Aunque la sintaxis es buena, el compilador no tragaría con esto. Si te fijas en la definición de Contador, hemos definido N, el contador interno, como private, y eso significa que no permitirá el acceso a ninguna función externa a la clase Contador (Incluido CountDown), lo que nos hace imposible acceder desde la nueva clase derivada.

Para que podamos acceder a propiedades o métodos internos desde clase derivadas (Pero no desde cualquier otro medio), necesitamos definirlo no como private, sino como protected en la clase base:

 
class Contador
   {  protected:                    	// Aqui esta el truco
         int N ;
   
      public:
     	Contador( ) : N(0) {}      	// Constructor
     	Contador(int k ) : N(k) {} 	// Constructor
   
         int GetCont() 
        	{ return (N) ;      }
     
     	Contador operator ++()
     	  {  return Contador( ++N) ;	}
   } ;

Al definir N como protected, significa que podemos acceder a esta variable desde clases derivadas de ella, pero sigue siendo imposible acceder desde un programa externo. A esta capacidad de acceder a los miembros públicos o protected de una clase se le conoce genéricamente por accesibilidad.

 
void loop()
   { CountDown C1 ;
     ++C1; ++C1; ++C1;
     Serial.println (C1.GetCont()) ;

     --C1 ; --C1 ;
     Serial.println (C1.GetCont()) ;
   }

Una clase derivada hereda los  métodos y propiedades, de la clase original, (Que sean public o private, claro) y podemos usarlas sin problema, lo que le confiere una potencia inusitada para definir jerarquías conceptuales.

El compilador puede usar un constructor por defecto sin parámetros, pero cualquier otro debe ser definido en la clase derivada independientemente y no puedo usar CountDown C1(25) ;

 
class CountDown : public Contador
  {   public:
         CountDown( )  :  Contador() {}     	// Constructor
         CountDown(int k )  :  Contador(k) {}

         Contador operator -- ()
            {  return Contador( --N) ;       }
  } ;

Donde simplemente le especificamos al compilador que use los constructores disponibles en  la clase base (O definir unos completamente nuevos), y así podamos crear C1 con un valor especificado. Donde especificamos al compilador que cuando se cree una instancia de CountDown, debe invocar el constructor de la clase base que le indicamos. La primera podríamos omitirla porque ya sabemos que el compilador proporciona un constructor por defecto, pero es buena política definirlo aquí para evitar sobresaltos.

Function Overriding

Hemos visto que podemos definir nuevos constructores porque el compilador no aplicará per se más que el default constructor sin parámetros, y también  hemos visto que las funciones disponibles en la clase original están gentilmente a disposición de las clases derivadas, pero puede ocurrir que nos interese redefinir una de ellas para que funcione de otra manera en la nueva clase derivada. A esta capacidad de redefinir una función miembro con el mismo nombre se le llama Function Overriding.

Podemos forzar un Override de la función miembro GetCont (), en nuestra clase CountDown para hacer que nos devuelva el doble del valor interno del contador.

 
class CountDown : public Contador
   {  public:
         CountDown( ) : Contador() {}     	// Constructor
         CountDown(int k ) : Contador(k) {}

         Contador operator -- ()
             {   return Contador( --N) ;	}

         int GetCont()
             {   return(2*N) ; }
   } ;

En donde simplemente creamos una nueva función miembro de CountDown con el mismo nombre y distinta ejecución.

Destructores

Los destructores son funciones miembro especiales que sirven para eliminar un objeto de una determinada clase. El destructor realizará procesos necesarios cuando un objeto termine su ámbito temporal, por ejemplo liberando la memoria dinámica utilizada por dicho objeto o liberando recursos usados, como ficheros, dispositivos, etc.

Al igual que los constructores, los destructores también tienen algunas características especiales:

  • También tienen el mismo nombre que la clase a la que pertenecen, pero tienen el símbolo ˜ delante.
  • No tienen tipo de retorno, y por lo tanto no retornan ningún valor.
  • No tienen parámetros.
  • No pueden ser heredados.
  • Deben ser públicos, no tendría ningún sentido declarar un destructor como privado, ya que siempre se usan desde el exterior de la clase, ni tampoco como protegido, ya que no puede ser heredado.
  • No pueden ser sobrecargados, lo cual es lógico, puesto que no tienen valor de retorno ni parámetros, no hay posibilidad de sobrecarga.

Cuando se define un destructor para una clase, éste es llamado automáticamente cuando se abandona el ámbito en el que fue definido. Esto es así salvo cuando el objeto fue creado dinámicamente con el operador new, ya que en ese caso, cuando es necesario eliminarlo, hay que hacerlo explícitamente usando el operador delete. En general, será necesario definir un destructor cuando nuestra clase tenga datos miembro de tipo puntero, aunque esto no es una regla estricta.

Ejemplo:

 
#include <iostream>
#include <cstring>
using namespace std;
 
class cadena {
  public:
   cadena();        // Constructor por defecto
   cadena(const char *c); // Constructor desde cadena c
   cadena(int n);   // Constructor de cadena de n caracteres
   cadena(const cadena &);   // Constructor copia
   ~cadena();       // Destructor

   void Asignar(const char *dest);
   char *Leer(char *c);
  private:
   char *cad;       // Puntero a char: cadena de caracteres
};
cadena::~cadena() {
   delete[] cad;        // Libera la memoria reservada a cad
}

Más información:

Clase Serial Arduino

Un ejemplo de clase es una que usamos habitualmente, la clase Serial: https://www.arduino.cc/en/Reference/Serial que está definida en los ficheros:

Por lo tanto cuando estamos Serial.begin(9600) estamos llamando al método begin del objeto Serial. En este caso no hemos hecho un include del fichero donde está incluida esta clase porque el IDE lo incluye automáticamente en el proceso de compilación ni tampoco hacemos una declaración del objeto con el constructor, puesto que también lo hace el IDE de Arduino.

Si vemos los ficheros donde está la clase HardwareSerial, vemos que se  trata de una clase heredada de la clase base Stream (class HardwareSerial : public Stream). La clase Stream está definida en el reference de Arduino en https://www.arduino.cc/en/Reference/Stream que es la clase de la que heredan otras clases como Serial, Wire, Ethernet Client, Ethernet Server y SD.

Vemos que los métodos de la clase Stream son los mismo que para Serial, Ethernet, etc… y por lo tanto nos es más fácil entender las clases derivadas si entendemos la clase base.

Un ejemplo de function overriding lo vemos con el método flush() que en la clase Stream sun función el limpiar el buffer una vez que todos los caracteres han sido enviados, pero en el caso de la clase derivada Serial, el método flush() lo que hace es esperar a la transmisión de todos los caracteres hasta su finalización.

La clase base Stream está definida en los ficheros:

De la librería Stream heredan muchas otras librerías https://www.arduino.cc/en/Reference/Stream

Prácticas: Funciones Definidas por Usuario

Montaje Arduino UNO:

Montaje Wemos:

Ejercicio15 – Funciones

Hacer un menú interactivo con Arduino. Con todo lo visto anteriormente, hacer un ejemplo de un menú interactivo donde se dan 4 opciones y pulsando cada una de ellas se ejecuta una acción concreta. Si el valor pulsado no es ninguna de las opciones avisar y volver a mostrar el menú hasta que se pulse una opción correcta. Usar funciones para cada una de las opciones.

Opciones:

  • 1 – Encender led siguiente (paso por referencia la posición del led)
  • 2 – Sacar por pantalla el LCD que está encendido
  • 3 – Sonar el buzzer 5 segundos
  • 4 – Fin (entra en un bucle infinito y no sale)

Solución: https://codeshare.io/5NYRvm

Hacer commit y pull del código en el repositorio “Curso Programacion Arduino 2019” que esté en una carpeta llamada Ejercicio15-Funciones

Ejercicio16 – Función Detecta flanco

Señales digitales:

Hacer una función que detecte flancos ascendentes y otras flancos descendentes, para ser reutilizada en otros proyectos.

Unificar estas dos funciones en una única función llamada detectaFlanco() donde le paso el pin y devuelve 1 si es flanco ascendente, -1 si es flanco descendente y 0 si no hay cambio de estado.

Ponerla en un ejemplo con alguno de los botones, usando este loop:

 
void loop() {
  int flanco = detectaFlanco(PIN_BOTON_A);
  if (flanco == 1)
    Serial.println("flanco ascendente");
  if (flanco == -1)
    Serial.println("flanco descendente");
}

Solución: https://codeshare.io/amkrV1

Hacer commit y pull del código en el repositorio “Curso Programacion Arduino 2019” que esté en una carpeta llamada Ejercicio16-Funcion_Detecta_Flanco

Ejercicio17 – Función Detecta flanco dos pines

Para ejercicio detecta flanco, probar la función con los dos botones en los pines 2 y 3. La función detecta flanco solo funciona con un pulsador, pero cuando se intenta usar con dos pulsadores ya no funciona. Comprobar porqué.

Solución: https://codeshare.io/5NYrqr

Hacer commit y pull del código en el repositorio “Curso Programacion Arduino 2019” que esté en una carpeta llamada Ejercicio17-Funcion_Detecta_Flanco_2Pines

La función para detectar flanco es la base para luego entender las clases y objetos y luego las librerías.

La solución es crear un objeto detecta flanco, para ello crear una clase y se puede distribuir mediante una librería como https://github.com/jecrespo/Detecta_Flanco_Libreria que se puede descargar desde https://github.com/jecrespo/Detecta_Flanco_Libreria/releases/tag/Version_1.0:

#include <DetectaFlanco.h>
#define PIN_BOTON_A 2
#define PIN_BOTON_B 3

DetectaFlanco df1(PIN_BOTON_A);
DetectaFlanco df2(PIN_BOTON_B);

void setup() {
  Serial.begin(9600);
  df1.inicio(INPUT_PULLUP);
  df2.inicio(INPUT);
}

void loop() {
  // put your main code here, to run repeatedly:
  int flanco1 = df1.comprueba();
  int flanco2 = df2.comprueba();

  if (flanco1 == 1)
    Serial.println("Flanco asc A");

  if (flanco1 == -1)
    Serial.println("Flanco desc A");

  if (flanco2 == 1)
    Serial.println("Flanco asc B");

  if (flanco2 == -1)
    Serial.println("Flanco asc B");

  delay(50); //Evitar rebotes
}

Ejercicio18 – Dado Digital

Usando las funciones de números aleatorios hacer un dado digital que genere un número aleatorio entre 1 y 6 y encienda un led aleatorio cada vez que se pulse el botón A. Usar el montaje del Wemos D1 mini

Usar la función de detección de flanco hecha en el anterior ejercicio.

Random Numbers

  • randomSeed() – Inicializa el generador de número pseudo-aleatorios
  • random() – Genera números pseudo-aleatorios

Paso 1 – Generar un valor aleatorio entre 1 y 6 al pulsar el botón

Paso 2 – Hacer girar el anillo led haciendo el efecto y que baje la velocidad

Paso 2 – Dejar fijo el nuevo número aleatorio

Solución: https://codeshare.io/anmypv

Hacer commit y pull del código en el repositorio “Curso Programacion Arduino 2019” que esté en una carpeta llamada Ejercicio18-Dado

Funciones Definidas por Usuario Avanzado

Una función es un bloque de código que tiene un nombre y un conjunto de instrucciones que son ejecutadas cuando se llama a la función. Son funciones setup() y loop() de las que ya se ha hablado.

Una función tiene un nombre y un conjunto de instrucciones que son ejecutadas cuando se llama a la función. Son funciones setup() y loop() de las que ya se ha hablado anteriormente.

Las funciones de usuario pueden ser escritas para realizar tareas repetitivas y para reducir el tamaño de un programa. Segmentar el código en funciones permite crear piezas de código que hacen una determinada tarea y volver al área del código desde la que han sido llamadas.

Una función puede llamarse múltiples veces e incluso llamarse a sí misma (función recurrente). Las funciones pueden recibir datos desde afuera al ser llamadas a través de los parámetros y puede entregar un resultado.

Las funciones se declaran asociadas a un tipo de valor. Este valor será el que devolverá la función, por ejemplo ‘int’ se utilizará cuando la función devuelva un dato numérico de tipo entero. Si la función no devuelve ningún valor entonces se colocará delante la palabra “void”, que significa “función vacía”

Sintaxis:

 
tipo nombreFunción (parámetros) {
   instrucciones;
}

Para llamar a una función, simplemente:

 
nombreFunción(parámetros);

En una función que devuelve un valor siempre debe tener la instrucción Return, este termina una función y devuelve un valor a quien ha llamado a la función: http://arduino.cc/en/Reference/Return

Si se define una función y no ponemos return el valor devuelto es cero. No da error de compilación.

Ventajas del uso de funciones:

  • Ayuda a tener organizado el código.
  • Una función codifica una tarea en un lugar de nuestro sketch, así que la función solo debe ser pensada una sola vez.
  • Reduce la probabilidad de errores al modificar el código.
  • Hacen que el tamaño del sketch sea menor porque el código de la función es reutilizado.
  • Facilita la lectura del código.
  • Hace más sencillo reutilizar código en otros sketches.

Más información: http://arduino.cc/en/Reference/FunctionDeclaration

Nombres de funciones

Generalmente los nombres de las funciones deben ser en minúscula, con las palabras separadas por un guión bajo, aplicándose éstos tanto como sea necesario para mejorar la legibilidad.

“mixedCase” (primera palabra en minúscula) es aceptado únicamente en contextos en donde éste es el estilo predominante con el objetivo de mantener la compatibilidad con versiones anteriores.

En el caso de las clases, los nombres deben utilizar la convención “CapWords” (palabras que comienzan con mayúsculas).

Las funciones en Arduino pueden estar dentro del mismo fichero .ino o en otro fichero con extensión .ino dentro del directorio del sketch.

Paso por Valor y Paso por Referencia

Hasta ahora siempre hemos declarado los parámetros de nuestras funciones del mismo modo. Sin embargo, éste no es el único modo que existe para pasar parámetros.

La forma en que hemos declarado y pasado los parámetros de las funciones hasta ahora es la que normalmente se conoce como «por valor«. Esto quiere decir que cuando el control pasa a la función, los valores de los parámetros en la llamada se copian a «objetos» locales de la función, estos «objetos» son de hecho los propios parámetros.

 
int funcion(int n, int m) {
  n = n + 2;
  m = m - 5;
  return n+m;
}

int a = 10;
int b = 20;

Serial.println(funcion(a,b));
Serial.println(funcion(10,20));

Empezamos haciendo a = 10 y b = 20, después llamamos a la función «funcion» con las objetos a y b como parámetros. Dentro de «funcion» esos parámetros se llaman n y m, y sus valores son modificados. Sin embargo al retornar al programa que lo llama, a y b conservan sus valores originales. Lo que pasamos no son los objetos a y b, sino que copiamos sus valores a los objetos n y m. Es lo mismo que hacer funcion(10,20), cuando llamamos a la función con parámetros constantes. Si los parámetros por valor no funcionasen así, no sería posible llamar a una función con valores constantes o literales.

Las referencias sirven para definir «alias» o nombres alternativos para un mismo objeto. Para ello se usa el operador de referencia (&).

Por ejemplo: 

 
int a;
int& r = a;  //Referencia a entero y debe siempre inicializarse

a = 10;
Serial.println(r);

En este ejemplo los identificadores a y r se refieren al mismo objeto, cualquier cambio en una de ellos se produce en el otro, ya que son, de hecho, el mismo objeto. El compilador mantiene una tabla en la que se hace corresponder una dirección de memoria para cada identificador de objeto. A cada nuevo objeto declarado se le reserva un espacio de memoria y se almacena su dirección. En el caso de las referencias, se omite ese paso, y se asigna la dirección de otro objeto que ya existía previamente. De ese modo, podemos tener varios identificadores que hacen referencia al mismo objeto, pero sin usar punteros.

El operador de dirección (&) devuelve la dirección de memoria de cualquier objeto

Los punteros se declaran igual que el resto de los objetos, pero precediendo el identificador con un asterisco (*)

 
int *pEntero;
int x = 10;
int y;

pEntero = &y; //pEntero apunta a la variable entera y
*pEntero = x; // y = 10

Más información sobre punteros: https://es.stackoverflow.com/questions/46909/cual-es-la-diferencia-entre-int-e-int

Si queremos que los cambios realizados en los parámetros dentro de la función se conserven al retornar de la llamada, deberemos pasarlos por referencia. Esto se hace declarando los parámetros de la función como referencias a objetos. Por ejemplo:

 
int funcion(int &n, int &m) {
  n = n + 2;
  m = m - 5;
  return n+m;
}

int a = 10;
int b = 20;

Serial.println(funcion(a,b));
Serial.println("a = " + String(a) + " b = " + String(b));
Serial.println(funcion(10,20)); //es ilegal pasar constantes como parámetros cuando estos son referencias

En este caso, los objetos «a» y «b» tendrán valores distintos después de llamar a la función. Cualquier cambio de valor que realicemos en los parámetros dentro de la función, se hará también en los objetos referenciadas. Esto quiere decir que no podremos llamar a la función con parámetros constantes, ya que aunque es posible definir referencias a constantes, en este ejemplo, la función tiene como parámetros referencias a objetos variables. Y si bien es posible hacer un casting implícito de un objeto variable a uno constante, no es posible hacerlo en el sentido inverso. Un objeto constante no puede tratarse como objeto variable.

Una const reference es una referencia a que no permite cambiar la variable a través de esa referencia. Por ejemplo const int& r = a; en r tengo el valor de a pero no puedo cambiar el valor de a usando r.

No confundir este concepto con el modificador de variable static, que es utilizado para crear variables que solo son visibles dentro de una función, sin embargo, al contrario que las variables locales que se crean y destruyen cada vez que se llama a la función, las variables estáticas mantienen sus valores entre las llamadas a las funciones.

Más información:

Sobrecarga de Funciones

Hay operadores que tienen varios usos, como por ejemplo *, &, << o >>. Esto es lo que se conoce en C++ como sobrecarga de operadores. Con las funciones existe un mecanismo análogo, de hecho, en C++, los operadores no son sino un tipo especial de funciones, aunque eso sí, algo peculiares.

En C++ podemos definir varias funciones con el mismo nombre, con la única condición de que el número y/o el tipo de los argumentos sean distintos. El compilador decide cuál de las versiones de la función usará después de analizar el número y el tipo de los parámetros. Si ninguna de las funciones se adapta a los parámetros indicados, se aplicarán las reglas implícitas de conversión de tipos.

Las ventajas son más evidentes cuando debemos hacer las mismas operaciones con objetos de diferentes tipos o con distinto número de objetos. También pueden usarse macros para esto, pero no siempre es posible usarlas, y además las macros tienen la desventaja de que se expanden siempre, y son difíciles de diseñar para funciones complejas. Sin embargo las funciones serán ejecutadas mediante llamadas, y por lo tanto sólo habrá una copia de cada una.

Ejemplo:

 
int mayor(int a, int b);
char mayor(char a, char b);
float mayor(float a, float b);
int mayor(int a, int b, int c, int d);

int mayor(int a, int b) {
  if(a > b) return a; else return b;
}

char mayor(char a, char b) {
  if(a > b) return a; else return b;
}

float mayor(float a, float b) {
  if(a > b) return a; else return b;
}

int mayor(int a, int b, int c, int d) {
  return mayor(mayor(a, b), mayor(c, d));
}

Las llamadas a funciones sobrecargadas se resuelven en la fase de compilación. Es el compilador el que decide qué versión de la función debe ser invocada, después de analizar, y en ciertos casos, tratar los argumentos pasados en la llamadas. A este proceso se le llama resolución de sobrecarga.

Tener en cuenta que el tipo de retorno de la función no se considera en la sobrecarga de funciones. Consideremos el caso en el que desea escribir una función que devuelve un número aleatorio, pero se necesita una versión que devolverá un entero, y otra versión que devolverá un doble.

int getRandomValue();
double getRandomValue();

Pero el compilador toma esto como un error. Estas dos funciones tienen los mismos parámetros (ninguno) y en consecuencia, la segunda getRandomValue () serán tratada como una redeclaración errónea de la primera. En consecuencia, tendrán que ser dado diferentes nombres a estas funciones.

Más información:

Sobrecarga de operadores:

Ámbito de las variables

Una variable puede ser declarada al inicio del programa antes de la parte de configuración setup(), a nivel local dentro de las funciones, y, a veces, dentro de un bloque, como para los bucles del tipo if.. for.., etc. En función del lugar de declaración de la variable así se determinará el ámbito de aplicación, o la capacidad de ciertas partes de un programa para hacer uso de ella.

Una variable global es aquella que puede ser vista y utilizada por cualquier función y estamento de un programa. Esta variable se declara al comienzo del programa, antes de setup().

Recordad que al declarar una variable global, está un espacio en memoria permanente en la zona de static data y el abuso de variables globales supone un uso ineficiente de la memoria.

Una variable local es aquella que se define dentro de una función o como parte de un bucle. Sólo es visible y sólo puede utilizarse dentro de la función en la que se declaró. Por lo tanto, es posible tener dos o más variables del mismo nombre en diferentes partes del mismo programa que pueden contener valores diferentes, pero no es una práctica aconsejable porque complica la lectura de código.

En el reference de Arduino hay una muy buena explicación del ámbito de las variables:http://arduino.cc/en/Reference/Scope

La variables estáticas solo se crean e inicializan la primera vez que la función es llamada. Ver ejemplo en: http://arduino.cc/en/Reference/Static

Más información:

NOTA: Hay una práctica del ámbito de las variables en el capítulo del curso de programación: “Prácticas: Variables y Tipos de Datos en Arduino”, Ejercicio07-Ambito_Variables

Inline

Cuando usamos el nombre de una función, indicando valores para sus argumentos, dentro de un programa, decimos que llamamos o invocamos a esa función. Esto quiere decir que el procesador guarda la dirección actual, «salta» a la dirección donde comienza el código de la función, la ejecuta, recupera la dirección guardada previamente, y retorna al punto desde el que fue llamada.

Esto es cierto para las funciones que hemos usado hasta ahora, pero hay un tipo especial de funciones que trabajan de otro modo. En lugar de existir una única copia de la función dentro del código, si se declara una función como inline, lo que se hace es insertar el código de la función, en el lugar (y cada vez) que sea llamada. Esta indica al compilador que cada llamada a la función inline deberá ser reemplazado por el cuerpo de esta función. En la práctica la función inline es utilizado sólo cuando las funciones son pequeñas para evitar generar un ejecutable de tamaño considerable.

La palabra reservada inline tiene la ventaja de acelerar un programa si éste invoca regularmente a la función inline. Permite reducir considerablemente el código, en particular para los accesadores de una clase. Un accesador de clase es típicamente una función de una línea. 

Ejemplo:

 
inline int mayor(int a, int b) {
  if(a > b) return a;
  return b;
}

Más información de inline:

Prototipos de Funciones

Primero recordar que en el lenguaje de Arduino al contrario que en estandar C, no es necesario declarar los prototipos de las funciones, puesto que de eso se encarga el de incluirlo el arduino builder, al igual que de añadir la función Main.

En C++ es obligatorio usar prototipos. Un prototipo es una declaración de una función. Consiste en una presentación de la función, exactamente con la misma estructura que la definición, pero sin cuerpo y terminada con un «;». 

En general, el prototipo de una función se compone de las siguientes secciones:

  • Opcionalmente, una palabra que especifique el tipo de almacenamiento, puede ser extern o static. Si no se especifica ninguna, por defecto será extern.
  • El tipo del valor de retorno, que puede ser void, si no necesitamos valor de retorno.
  • Modificadores opcionales. 
  • El identificador de la función. Es costumbre, muy útil y muy recomendable, poner nombres que indiquen, lo más claramente posible, qué es lo que hace la función, y que permitan interpretar qué hace el programa con sólo leerlos.
  • Una lista de declaraciones de parámetros entre paréntesis. Los parámetros de una función son los valores de entrada (y en ocasiones también de salida). 

Un prototipo sirve para indicar al compilador los tipos de retorno y los de los parámetros de una función, de modo que compruebe si son del tipo correcto cada vez que se use esta función dentro del programa, o para hacer las conversiones de tipo cuando sea necesario.

Normalmente, los prototipos de las funciones se declaran dentro del fichero del programa, o bien se incluyen desde un fichero externo, llamado fichero de cabecera, (para esto se usa la directiva #include).

Ya lo hemos dicho más arriba, pero las funciones son extern por defecto. Esto quiere decir que son accesibles desde cualquier punto del programa, aunque se encuentren en otros ficheros fuente del mismo programa. En contraposición las funciones declaradas static sólo son accesibles dentro del fichero fuente donde se definen.

Si programamos en Arduino clases o librerías, es posible que debamos utilizar los prototipos de funciones.

Más información: 

Bibliotecas/Librerías en C++

Junto con los compiladores de C y C++, se incluyen ciertos archivos llamados bibliotecas más comúnmente librerías. Las bibliotecas contienen el código objeto de muchos programas que permiten hacer cosas comunes, como leer el teclado, escribir en la pantalla, manejar números, realizar funciones matemáticas, etc.

Las bibliotecas están clasificadas por el tipo de trabajos que hacen, hay bibliotecas de entrada y salida, matemáticas, de manejo de memoria, de manejo de textos y como imaginarás existen muchísimas librerías disponibles y todas con una función específica.

La declaración de librerías, tanto en C como en C++, se debe hacer al principio de todo nuestro código, antes de la declaración de cualquier función o línea de código, debemos indicarle al compilador que librerías usar, para el saber qué términos están correctos en la escritura de nuestro código y cuáles no. La sintaxis es la siguiente: #include <nombre de la librería> o alternativamente #include «nombre de la librería». En tu código puedes declarar todas las librerías que quieras aunque en realidad no tienen sentido declarar una librería que no vas a usar en tu programa, sin embargo no existe límite para esto.

La directiva de preprocesador #include se usa en los lenguajes C y C++ para “incluir” las declaraciones de otro fichero en la compilación. Esta directiva no tiene más misterio para proyectos pequeños. En cambio, puede ayudar aprovechar bien esta directiva en proyectos con un gran número de subdirectorios.

Ejemplo:

 
#include "iostream"
#include "string"
#include <math.h>
using namespace std;

Lo único adicional, es la línea que dice using namespace std; esta línea nos ayuda a declarar un espacio de nombre que evita tener que usarlo cada que accedemos a alguna función específica de una librería. Teniendo este namespace declarado podemos llamar por ejemplo el comando cout >>, que pertenece a la librería iostream, sin embargo sin este namespace sería std::cout >>, imagina tener que hacer esto cada vez que uses algún comando o función de las librerías, sería bastante tedioso.

Algunas de las librerías de uso más común de C++ y que forman parte de las librerías estándar de este lenguaje.

  • fstream: Flujos hacia/desde ficheros. Permite la manipulación de archivos desde el programar, tanto leer como escribir en ellos.
  • iosfwd: Contiene declaraciones adelantadas de todas las plantillas de flujos y sus typedefs estándar. Por ejemplo ostream.
  • iostream: Parte del a STL que contiene los algoritmos estándar, es quizá la más usada e importante (aunque no indispensable).
  • math: Contiene los prototipos de las funciones y otras definiciones para el uso y manipulación de funciones matemáticas.
  • memory: Utilidades relativas a la gestión de memoria, incluyendo asignadores y punteros inteligentes (auto_ptr). «auto_ptr» es una clase que conforma la librería memory y permite un fácil manejo de punteros y su destrucción automáticamente.
  • ostream: Algoritmos estándar para los flujos de salida.
  • Librería stdio: Contiene los prototipos de las funciones, macros, y tipos para manipular datos de entrada y salida.
  • Librería stdlib: Contiene los prototipos de las funciones, macros, y tipos para utilidades de uso general.
  • string: Parte de la STL relativa a contenedores tipo string; una generalización de las cadenas alfanuméricas para albergar cadenas de objetos.
  • vector: Parte de la STL relativa a los contenedores tipo vector; una generalización de las matrices unidimensionales C/C++
  • list: Permite implementar listas doblemente enlazadas (listas enlazadas dobles) fácilmente.
  • iterator: Proporciona un conjunto de clases para iterar elementos.
  • regex: Proporciona fácil acceso al uso de expresiones regulares para la comparación de patrones.
  • thread: Útil para trabajar programación multihilos y crear múltiples hilos en nuestra aplicación.

Arduino ya incluye diversas librerías por defecto sin llamarlas explícitamente, p.e. math.h

Más información:

Software para visualizar dependencias de ficheros:

Funciones Anónimas y Lambda

En la programación, una función anónima (función literal, abstracción lambda o expresión lambda) es una definición de función que no está vinculada a un identificador. Las funciones anónimas son a menudo argumentos que se pasan a funciones de orden superior, o que se utilizan para construir el resultado de una función de orden superior que necesita devolver una función. Si la función sólo se utiliza una vez, o un número limitado de veces, una función anónima puede ser sintácticamente más ligera que la que se utiliza con una función nombrada. Las funciones anónimas se utilizan en casi todos los lenguajes de programación.

En algunos lenguajes de programación, las funciones anónimas se implementan comúnmente para propósitos muy específicos, como la vinculación de eventos funciones de callback, o la instanciación de la función para valores particulares, que pueden ser más eficientes, más legibles y menos propensos a errores que la llamada a una función con nombre más genérico.

C++ soporta funciones anónimas, llamadas expresiones lambda, que tienen la forma::

[capture](parameters) -> return_type { function_body }

Un ejemplo de función lambda se define de la siguiente manera:

 
[ ](int x, int y) -> int { return x + y; }

C++11 también soporta cierres. Los cierres (closures) se definen entre corchetes ([,]) en la declaración de expresión lambda. El mecanismo permite capturar estas variables por valor o por referencia. La siguiente tabla lo demuestra:

  • [ ]        //no variables defined. Attempting to use any external variables in the lambda is an error.
  • [ x, &y ]   //x is captured by value, y is captured by reference
  • [ & ]       //any external variable is implicitly captured by reference if used
  • [ = ]       //any external variable is implicitly captured by value if used
  • [ &, x ]    //x is explicitly captured by value. Other variables will be captured by reference
  • [ =, &z ]   //z is explicitly captured by reference. Other variables will be captured by value

Más información:

Librerías vs Funciones en Arduino

Si en nuestro código tenemos múltiples funciones, podemos incluirlas en un fichero de C++ por ejemplo funciones.h, e incluyéndose en el principal con la instrucción #include “funciones.h”. En C++ el código se organiza en diferentes ficheros con extensiones .h y .cpp a los que se van llamando con #include para añadirlos al fichero que lo llama para poder usar su contenido.

Ejemplo: https://github.com/jecrespo/aprendiendoarduino-Curso_Arduino_2017/tree/master/Ejercicio45-Librerias

Como se ha visto anteriormente, las librerías son trozos de código hechas por terceros que usamos en nuestro sketch. Esto nos facilita mucho la programación y hace que nuestro programa sea más sencillo de hacer y luego de entender. Más adelante veremos cómo hacer una librería.

Las librerías en Arduino incluyen los siguientes archivos comprimidos en un archivo ZIP o dentro de un directorio. Estas siempre contienen:

  • Un archivo .cpp (código de C++)
  • Un archivo .h o encabezado de C, que contiene las propiedades y métodos o funciones de la librería.
  • Un archivo Keywords.txt, que contiene las palabras clave que se resaltan en el IDE (opcional).
  • Muy posiblemente la librería incluye un archivo readme con información adicional de lo que hace y con instrucciones de como usarla.
  • Directorio denominado examples con varios sketchs de ejemplo que nos ayudará a entender cómo usar la librería (opcional).

Más información: https://aprendiendoarduino.wordpress.com/2017/06/20/librerias-arduino-3/ 

Una librería a diferencia de las funciones debe estar al menos en un fichero diferente con extensión .h y opcionalmente en otro .cpp y además debe ser llamada con #include desde el sketch de arduino y estar en una ruta accesible desde el IDE de Arduino, ya sea el mismo directorio del sketch o en algunas de las rutas configuradas para librerías. 

La ventaja de usar librerías frente a las funciones es que no es necesario incluir el código cada vez que se va a reutilizar sino que con tener la librería instalada en el IDE y llamarla mediante #include ya la puedo usar en mi código.

Al llamar a una librería desde un sketch, la librería completa es cargada a la placa de Arduino incrementando el tamaño del espacio usado en el microcontrolador, tanto en la memoria flash como en la RAM.

Las librerías que usamos para los sketches tienen una versión, que se suelen actualizar con frecuencia. También tenemos un control de versiones en el nuevo IDE a partir de 1.6.4 que nos facilita la gestión de la versión de las librerías usadas. Este aspecto es importante porque un sketch que funciona con una versión de una librería es posible que al compilarlo con otra versión en otro IDE no funcione. Por ello es importante documentar con que versión de librería está hecho o distribuir el sketch con la librería con la que se ha creado. Generalmente las librerías tienen compatibilidad hacia atrás, pero puede que no ocurra o que el comportamiento de la librería sea diferente.

Al cambiar el IDE también nos podemos encontrar que nuestro sketch no es compatible con la versión de la librería que estemos usando, que es diferente con la que se diseñó originalmente el sketch.

Listado de librerías Arduino:

Prácticas: Estructuras Propias Arduino

Montaje Arduino UNO:

Montaje Wemos:

Señales digitales:

Resultado de imagen de flip-flop-periodo.gif

Ejercicio13 – Calcular Tiempo Pulso

Vamos a calcular el tiempo entre dos pulsaciones de un pulsador, esto tienes muchas aplicaciones para calcular tiempos entre dos señales digitales, p.e. calcular si pasa una persona, bicicleta, coche o camión en un paso con una fotocelula: 

Ejemplo: https://www.mytienda.es/p225/fotocelulas-sensor-infrarrojo-garaje

Para ello vamos a usar la función PulseIn: https://www.arduino.cc/en/Reference/PulseIn 

Ejemplo de PulseIn para calcular distancia con un sensor ultrasónico: https://www.luisllamas.es/medir-distancia-con-arduino-y-sensor-de-ultrasonidos-hc-sr04/

  • Paso 1 – Usando la función pulsein calcular el tiempo que mantengo pulsado el botón B y mostar por la consola.
  • Paso 2 – Usar ese tiempo para distinguir entre pulsación corta < 2 segundos y pulsación larga >= 2 segundos.
  • Paso 3 – Para una pulsación larga encender el primer led (encendido del sistema) y una vez encendido cada pulsación corta pasa de un led a otro en la secuencia 1-2-3-4-1-2-3-4-… Si se hace una pulsación corta apagar los leds.

Es un sistema que con una pulsación larga apaga o enciende el sistema y con una una corta cambia el led si anteriormente he encendido el sistema.

Sacar también los datos de tiempos por la pantalla LCD.

Solución: https://codeshare.io/2jbRYP

Hacer commit y pull del código en el repositorio “Curso Programacion Arduino 2019” que esté en una carpeta llamada Ejercicio13-Pulse

Ejercicio propuesto: hacer un juego para dos jugadores con los dos botones en el que gana el que más se acerque en la pulsación a un valor de segundos generado aleatoriamente.

Ejercicio14 – Control Efectos LED

Basándonos en el ejercicio Ejercicio12-RGB_Wemos hacer un sketch que controle los efectos, con una pulsación corta cambia el color del led girando y con una larga apaga o enciende el sistema.

Usar la función millis para calcular el tiempo de la pulsación. De esta forma no se bloquea el programa en la función PulseIn. ESTO ES UN EJEMPLO DE MULTITAREA

Solución: https://codeshare.io/5vQRn7

Hacer commit y pull del código en el repositorio “Curso Programacion Arduino 2019” que esté en una carpeta llamada Ejercicio14-Control_RGB_Wemos